Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
479fb0e782 | ||
|
|
8ba11a2bd0 | ||
|
|
abc3f3b94d | ||
|
|
67bc0456e0 | ||
|
|
3e8ddd1966 | ||
|
|
6223342890 | ||
|
|
9ddabb3fee | ||
|
|
af0030b580 | ||
|
|
b93271d9cd | ||
|
|
934ca6508f | ||
|
|
7e210c7e6b | ||
|
|
e4970266d8 | ||
|
|
0a5d7a7bd2 | ||
|
|
31aab8ea6f | ||
|
|
857d9bcf2a | ||
|
|
7463166aec | ||
|
|
f80159fedc | ||
|
|
3180a6f6ec | ||
|
|
35d26f1d27 | ||
|
|
ca03f8928d | ||
|
|
0b3d1e50a8 | ||
|
|
401886c7c3 | ||
|
|
4eac7661c2 | ||
|
|
b8163cfbbf | ||
|
|
a9f82907ee | ||
|
|
57f4ae5320 | ||
|
|
d1712424c8 | ||
|
|
16f865987d | ||
|
|
f071531494 | ||
|
|
618768c7ed | ||
|
|
cfe22e6fd2 | ||
|
|
3961dd5a44 | ||
|
|
471e332036 | ||
|
|
467e8c563a | ||
|
|
5971630f64 | ||
|
|
f7cc1058c7 | ||
|
|
645a5920da | ||
|
|
f95bc64971 | ||
|
|
0c7c307de3 | ||
|
|
1c84881d00 | ||
|
|
f1252abbbe | ||
|
|
11d6ca2aa1 | ||
|
|
eb59b90790 | ||
|
|
4c3aa97917 | ||
|
|
ba54f25b8c | ||
|
|
af268f4968 | ||
|
|
63d63c27e7 | ||
|
|
ef1e2ab9e2 | ||
|
|
7b46940772 | ||
|
|
6f5f68df1a | ||
|
|
dc165175cf | ||
|
|
32f1782c84 | ||
|
|
8811d96a8b | ||
|
|
d11d74688e | ||
|
|
8e79d53d78 | ||
|
|
6dbc476cdc | ||
|
|
57921e3fe0 | ||
|
|
75b74ac4ff | ||
|
|
1c659841fa | ||
|
|
9573357778 | ||
|
|
f11362a80d | ||
|
|
a01fbb636a | ||
|
|
320fb55ddc | ||
|
|
4b170ca43f | ||
|
|
c6e7fb74ab | ||
|
|
b05814e6a5 | ||
|
|
07882c42dc | ||
|
|
a59b2f8433 | ||
|
|
7f2e94c30c | ||
|
|
95aed8fcb0 | ||
|
|
663d3b24cd | ||
|
|
a87303f375 | ||
|
|
259858fac2 | ||
|
|
a4514ee98a | ||
|
|
d27b7bfa7a | ||
|
|
ce61795c38 | ||
|
|
9cc878cea7 | ||
|
|
087acf8a9a | ||
|
|
7612180276 | ||
|
|
201e3e4dab | ||
|
|
55f51ad169 | ||
|
|
405d1ee52d | ||
|
|
5f9ad5c19e | ||
|
|
c5cfa409b1 | ||
|
|
2a0f44b0fb | ||
|
|
7d0e585c44 | ||
|
|
a5ced5d909 | ||
|
|
0a99f65f01 | ||
|
|
c33814a7a6 | ||
|
|
0950369a5c | ||
|
|
346b5177bc | ||
|
|
9246d524cb | ||
|
|
a889fef752 | ||
|
|
6075498c24 | ||
|
|
ee1da73a69 | ||
|
|
29ac6a153e | ||
|
|
264757e964 | ||
|
|
e88a9ffd89 | ||
|
|
d56a611a19 | ||
|
|
28ed7772c6 | ||
|
|
a047359f81 | ||
|
|
b81c165bea | ||
|
|
4405f5e163 | ||
|
|
2012c8c91d | ||
|
|
f719a4e91b | ||
|
|
5d33727ae4 | ||
|
|
1343b83fc3 | ||
|
|
9e47ec9af2 | ||
|
|
9c459aacc7 | ||
|
|
72724abac8 | ||
|
|
9a86eeb333 | ||
|
|
46f6fd18b0 | ||
|
|
603f22a7bb | ||
|
|
13c401cbaa | ||
|
|
b812991072 | ||
|
|
2b2431824f | ||
|
|
7147787441 | ||
|
|
ec3bf1139d | ||
|
|
d0b603d033 | ||
|
|
2621be819e | ||
|
|
f380153f42 | ||
|
|
fd91c5e1a8 | ||
|
|
f61d852f3c | ||
|
|
5bc1112151 | ||
|
|
59cc0b675e | ||
|
|
db8ded2a02 | ||
|
|
4b7279146a | ||
|
|
c4a2f7911d | ||
|
|
db9fe53af3 | ||
|
|
d3865ad23f | ||
|
|
8b1067d180 | ||
|
|
a53a0acd65 | ||
|
|
42724c7f42 | ||
|
|
616f069538 | ||
|
|
11cec31da6 | ||
|
|
d9991b25a4 | ||
|
|
cb6533f3a6 | ||
|
|
54e7f65d4e | ||
|
|
ec61f3acf0 | ||
|
|
910aa1ad7f | ||
|
|
24bed01fa4 | ||
|
|
f7d6ab5f2a | ||
|
|
0716cffd7d | ||
|
|
e29ab29441 | ||
|
|
9f271de803 | ||
|
|
43c796a781 | ||
|
|
25152e4441 | ||
|
|
3846fe0a7d | ||
|
|
b3b2bd1faf | ||
|
|
2c3d98abc8 | ||
|
|
0dc3dd0336 | ||
|
|
c4762e7228 | ||
|
|
35b32b721e | ||
|
|
ccf1a047d3 | ||
|
|
db0338cbcf | ||
|
|
06742eb507 | ||
|
|
0fab4ff5d1 | ||
|
|
d2b0d514f8 | ||
|
|
ab40908fa4 | ||
|
|
a5c48c891e | ||
|
|
70e35de56b | ||
|
|
c323e0ad4a | ||
|
|
4d32826891 | ||
|
|
13088f55a0 | ||
|
|
ea008411e4 | ||
|
|
e63fdf580b | ||
|
|
597c6939c8 | ||
|
|
293ae18a11 | ||
|
|
32b34cbe4c | ||
|
|
2f6d05f99d | ||
|
|
ad3ae67e70 | ||
|
|
b594407cd5 | ||
|
|
207c4cf961 | ||
|
|
fe11837a61 | ||
|
|
fee912023f | ||
|
|
a95ac990ff | ||
|
|
aad8439542 | ||
|
|
1a777edffe | ||
|
|
537018f71f | ||
|
|
67ca3a9334 | ||
|
|
5fb30979c1 | ||
|
|
cf863d4849 | ||
|
|
47814847f9 | ||
|
|
9ec8974545 | ||
|
|
c887c6e0f6 | ||
|
|
6c8949bba1 | ||
|
|
508d3ab68c | ||
|
|
6dad013783 | ||
|
|
0532e5cbf0 | ||
|
|
abcdf3e252 | ||
|
|
3baa6b9237 | ||
|
|
23e6a475ee | ||
|
|
65256c8751 | ||
|
|
b36d8780fd | ||
|
|
70fda99415 | ||
|
|
2221f70ff0 | ||
|
|
daad8d9c04 | ||
|
|
03d26e39c7 | ||
|
|
64d3c089a2 | ||
|
|
938c011e63 | ||
|
|
66f6b88333 | ||
|
|
35f746c925 | ||
|
|
944da28089 | ||
|
|
8f3e2b645a | ||
|
|
82b2a5305f | ||
|
|
6a2852ee26 | ||
|
|
526d6c4da9 | ||
|
|
46d62f4972 | ||
|
|
5cafcdb695 | ||
|
|
2674258ead | ||
|
|
b38be3a1d0 | ||
|
|
18a9f05626 | ||
|
|
bf34980c43 | ||
|
|
d2f8f9dc1a | ||
|
|
df13469b6e | ||
|
|
584ec681af | ||
|
|
0c5b5cca4a | ||
|
|
64663bf4a8 | ||
|
|
fe8334a873 | ||
|
|
c74fd40e2a | ||
|
|
0f8ff67451 | ||
|
|
8b0032c210 | ||
|
|
d9254ef2d9 | ||
|
|
66be05a962 | ||
|
|
0e85a09dd3 | ||
|
|
0c4ba9fd67 | ||
|
|
aca81c764a | ||
|
|
6b3ddb97d1 | ||
|
|
26ccf2a316 | ||
|
|
361894194a | ||
|
|
8c2db8ddb8 | ||
|
|
5546baa927 | ||
|
|
d814009721 | ||
|
|
08fd050c3f | ||
|
|
0b096d7d55 | ||
|
|
44cfb2dc87 | ||
|
|
b0624cadb7 | ||
|
|
045f361b0d | ||
|
|
cb77b9dc1a | ||
|
|
0723dac596 | ||
|
|
365f9b4c1b | ||
|
|
2cf90d90f7 | ||
|
|
da14f64d1b | ||
|
|
ef1b1c8ffc | ||
|
|
adde8ab2c3 | ||
|
|
22cc8079dc | ||
|
|
6d25706300 | ||
|
|
81419c980b | ||
|
|
097e21f641 | ||
|
|
b68180b97b | ||
|
|
d9059d18c0 | ||
|
|
5225a6e27a | ||
|
|
9055bace96 | ||
|
|
eb25e88754 | ||
|
|
0cb8ecc6f7 | ||
|
|
8cf76cc6e9 | ||
|
|
3c3a646d42 | ||
|
|
ff29fd69a3 | ||
|
|
91994a399c | ||
|
|
bef7137c92 | ||
|
|
11f62bab34 | ||
|
|
b66776a047 | ||
|
|
2c151f444c | ||
|
|
d8f8b060cb | ||
|
|
ea9376019c | ||
|
|
008d1df106 | ||
|
|
ec91a6c7f4 | ||
|
|
4b2ae6a79f | ||
|
|
2ec756fd6b | ||
|
|
f0d5b40121 | ||
|
|
31a0996b2f | ||
|
|
0bbd8f0a16 | ||
|
|
b1619e42fd | ||
|
|
4cfec2fc09 | ||
|
|
3d721a95bb | ||
|
|
dc3a136704 | ||
|
|
235ae2fbb9 | ||
|
|
1f518b0b45 | ||
|
|
ab34d53d6b | ||
|
|
aafb5333b9 | ||
|
|
5e2791f981 | ||
|
|
1ecc5674ac | ||
|
|
c9ad3a595a | ||
|
|
43558fa1dc | ||
|
|
15a867d2b7 | ||
|
|
43cb4f471f | ||
|
|
012051c1db | ||
|
|
b5483898da | ||
|
|
c7b4c2beaf | ||
|
|
8693d067e3 | ||
|
|
78d5fc9506 | ||
|
|
7ac4f1a9be | ||
|
|
670fb01fbf | ||
|
|
ec9608d54b | ||
|
|
dcffaf1c8c | ||
|
|
efac5293c0 | ||
|
|
c7563346a2 | ||
|
|
69906532b1 | ||
|
|
9404fae725 | ||
|
|
3542f15575 | ||
|
|
15d79143e2 | ||
|
|
f6dafee748 | ||
|
|
356d86ae74 | ||
|
|
e673c6ec97 | ||
|
|
3e5fcf7004 | ||
|
|
96d2a2bc68 | ||
|
|
b401992178 | ||
|
|
4c34351383 | ||
|
|
202b54afbb | ||
|
|
26ebc3d944 | ||
|
|
d0f36e9939 | ||
|
|
4c99c032ca | ||
|
|
6198556e50 | ||
|
|
514eba0e4e | ||
|
|
7279ee0f5f | ||
|
|
ccadf3fcca | ||
|
|
cb80ce0d2c | ||
|
|
9aae43961f | ||
|
|
b810687163 | ||
|
|
28d4ec779d | ||
|
|
cf4da6c748 | ||
|
|
e569b5669a | ||
|
|
695a9523fe | ||
|
|
01b8e28a7d | ||
|
|
e221030a2b | ||
|
|
88dc697392 | ||
|
|
99a270216c | ||
|
|
d06c920a2d | ||
|
|
5452986ce3 | ||
|
|
ae69841174 | ||
|
|
e6fc80c13b | ||
|
|
a10ee8f58c | ||
|
|
3f69da2455 | ||
|
|
a14a0834e9 | ||
|
|
c4f431849b | ||
|
|
ade3389cea | ||
|
|
6873ba0909 | ||
|
|
c3f31b5f65 | ||
|
|
3b62aa444b | ||
|
|
fb2b10b565 | ||
|
|
345553012f | ||
|
|
fddc13d98e | ||
|
|
aeb9f798c9 | ||
|
|
8fd44266a2 | ||
|
|
ef4155fbf6 | ||
|
|
92f27d3144 | ||
|
|
5183332f6e | ||
|
|
ee1de0980f | ||
|
|
c53183d94b | ||
|
|
1ee774d01d | ||
|
|
1c030f57ef | ||
|
|
c8bb321654 | ||
|
|
a1d71c9198 | ||
|
|
55cb43d70b | ||
|
|
551159e0c3 | ||
|
|
3bf31aa478 | ||
|
|
1316682343 | ||
|
|
7167b06bf8 | ||
|
|
34582f9f65 | ||
|
|
8dc9ec5733 | ||
|
|
e8adf8ad2b | ||
|
|
1306431e7c | ||
|
|
f7bc0fba4d | ||
|
|
639a2fd509 | ||
|
|
b00083e0e5 | ||
|
|
c3256556aa | ||
|
|
8dd11f5587 | ||
|
|
0f3ee21d9d | ||
|
|
a489037ec4 | ||
|
|
c48388af14 | ||
|
|
2207d356f2 | ||
|
|
795cdb52dc | ||
|
|
e79e67c3ce | ||
|
|
e0639e1b62 | ||
|
|
56b541b44f | ||
|
|
03e46fbdee | ||
|
|
ee0d6b4533 | ||
|
|
937a9bf433 | ||
|
|
3bcdc4a354 | ||
|
|
382cd0bbf4 | ||
|
|
e58a6975da | ||
|
|
833eb5216b | ||
|
|
742d28fcc3 | ||
|
|
cc78fdd63c | ||
|
|
20f22fb658 | ||
|
|
3c635f3ce5 | ||
|
|
9d8f6c9ae7 | ||
|
|
cc6893d0f2 | ||
|
|
ca4e99e810 | ||
|
|
8d7df670b4 | ||
|
|
4de7ed46bf | ||
|
|
f9aac1bcc6 | ||
|
|
92068f36f2 | ||
|
|
047729ee9e | ||
|
|
637a5bd9cc | ||
|
|
6cd5c2b846 | ||
|
|
365a9f949c | ||
|
|
d6de156cd8 | ||
|
|
23c9fca088 | ||
|
|
c2a69721f3 | ||
|
|
d1ad8a7831 | ||
|
|
cee2285818 | ||
|
|
ca19da16e0 | ||
|
|
5850cbcd05 | ||
|
|
b789a7e85b | ||
|
|
9f0dbdd5c7 | ||
|
|
98614f26ef | ||
|
|
3b16696672 | ||
|
|
0219ad2ed8 | ||
|
|
e771483041 | ||
|
|
4a7c3320fb | ||
|
|
9359dca0f3 | ||
|
|
c8ccc411d4 | ||
|
|
537c8bad10 | ||
|
|
1a55367fc9 | ||
|
|
4f7ee7dab0 | ||
|
|
da16dd7cae | ||
|
|
2aa7608ac0 | ||
|
|
74c3a4a6f7 | ||
|
|
d30fb89fde | ||
|
|
1b344ce789 | ||
|
|
99fbcd3d65 | ||
|
|
72063329fe | ||
|
|
adaf604ce0 | ||
|
|
d5f0fff523 | ||
|
|
43c3349494 | ||
|
|
b5d16ed7d7 | ||
|
|
aab67c2ed9 | ||
|
|
0461a822c2 | ||
|
|
7b87e78ed3 | ||
|
|
6e1628bc3b | ||
|
|
370c5a971d | ||
|
|
9a86e4e109 | ||
|
|
8cb46f2726 | ||
|
|
f4293a6dfa | ||
|
|
1c759f1d6f | ||
|
|
6918e83787 | ||
|
|
9626b1e39d | ||
|
|
9227aed906 | ||
|
|
0bd469f505 | ||
|
|
5bd803382a | ||
|
|
ef60e50e1d | ||
|
|
ed1ba14c6b | ||
|
|
a4080d3706 | ||
|
|
e2b2f98011 | ||
|
|
ed13218f90 | ||
|
|
1cdd2e00ab | ||
|
|
94eb08fb73 | ||
|
|
5e1ccb358b | ||
|
|
259338b8c2 | ||
|
|
a05bb50230 | ||
|
|
8a07b8c798 | ||
|
|
4d5046b5e7 | ||
|
|
12160a50cb | ||
|
|
375ccbd9e2 | ||
|
|
cc0f0d94f6 | ||
|
|
6309752208 | ||
|
|
4448ebe391 | ||
|
|
d1dac57f7a | ||
|
|
3422f84e00 | ||
|
|
ac6faf3dd8 | ||
|
|
7679475bfe | ||
|
|
5a932b2a20 | ||
|
|
b1ef934aea | ||
|
|
a2ab0ffc46 | ||
|
|
ecd7e69f1a | ||
|
|
fa3484cd14 | ||
|
|
f4846aa154 | ||
|
|
9b66208029 | ||
|
|
c43c3b411a | ||
|
|
f9a286a851 | ||
|
|
3de3912b96 | ||
|
|
23169e89c8 | ||
|
|
0f5f34e8f5 | ||
|
|
342e862d69 | ||
|
|
081fe3f301 | ||
|
|
fc528f6c0c | ||
|
|
411c974960 | ||
|
|
0ec125557a | ||
|
|
0b977fc35f | ||
|
|
0739a4be06 | ||
|
|
c4de07135f | ||
|
|
d52b53889e | ||
|
|
d9682bd6d6 | ||
|
|
411b80cdc0 | ||
|
|
db81c28bd7 | ||
|
|
c7fa2d55b4 | ||
|
|
7572dd5fa6 | ||
|
|
9942477bff | ||
|
|
635a4d7682 | ||
|
|
672dc1a251 | ||
|
|
6deaaa8ef8 | ||
|
|
f4267c4f74 | ||
|
|
1848fff4c9 | ||
|
|
991b2843d3 | ||
|
|
940bd2699e | ||
|
|
4a27b9a53f | ||
|
|
a31921ae73 | ||
|
|
27a7f1504c | ||
|
|
aee8ddeafb | ||
|
|
6c29ccfbd1 | ||
|
|
da7ea104de | ||
|
|
665a11acc0 | ||
|
|
6b5ced0078 | ||
|
|
dddc91fbcc | ||
|
|
ecb892d995 | ||
|
|
ada96a8373 | ||
|
|
cabe23b782 | ||
|
|
b25bf9562e | ||
|
|
e9da2a6330 | ||
|
|
9385b9309d | ||
|
|
facc2659e7 | ||
|
|
06852cd904 | ||
|
|
a1ee8aefbb | ||
|
|
eb3b3fa107 | ||
|
|
3457b73ac8 | ||
|
|
28b38dc76d | ||
|
|
659fb90c1c | ||
|
|
04b1ebf1a7 | ||
|
|
1685cf2d8f | ||
|
|
43d4d4f2bb | ||
|
|
3de3c0c6b6 | ||
|
|
bebe43e5e1 | ||
|
|
520f2bc9d6 | ||
|
|
996c25a364 | ||
|
|
d9d60063d7 | ||
|
|
ff523bfc79 | ||
|
|
c7e8498166 | ||
|
|
2b4c019f13 | ||
|
|
335850380e | ||
|
|
94c01ec378 | ||
|
|
2b8820fdac | ||
|
|
0ec7b661f2 | ||
|
|
dc5efa886a | ||
|
|
e66b1c5704 | ||
|
|
6b8c43ed7f | ||
|
|
b9d374affa | ||
|
|
d2ef19dffe | ||
|
|
efdf37b887 | ||
|
|
6ffb8f545b | ||
|
|
2e4147b4fa | ||
|
|
07a8b8f4f7 | ||
|
|
74483fb294 | ||
|
|
4f18d842f9 | ||
|
|
188e197e20 | ||
|
|
7234bfb6eb | ||
|
|
97feb3321c | ||
|
|
3e1a607179 | ||
|
|
c3f46e4a6b | ||
|
|
1fbd3c8f65 | ||
|
|
965ebde22f | ||
|
|
0d9d3aca1a | ||
|
|
937cf43e09 | ||
|
|
23472e6fb4 | ||
|
|
445728b2a2 | ||
|
|
61c213f9e3 | ||
|
|
dba432d1b6 | ||
|
|
2f416371bd | ||
|
|
2bfbdb1c06 | ||
|
|
a4c958c54f | ||
|
|
b2e257bf37 | ||
|
|
6b6314227e | ||
|
|
1e0eb3784e | ||
|
|
f00ea4bfac | ||
|
|
0da4c9d90d | ||
|
|
df83298c54 | ||
|
|
5d29d4ea22 | ||
|
|
a9e1ed5a72 | ||
|
|
e6ad8d8b50 | ||
|
|
50cfe2b21a | ||
|
|
d7f2ec89d7 | ||
|
|
cc5ae8193b | ||
|
|
30a1f1ccb5 | ||
|
|
6f7cb990ca | ||
|
|
06f5746c86 | ||
|
|
cd44fe7bcd | ||
|
|
bb3d6ee72c | ||
|
|
bf16fdc01d | ||
|
|
969a2e953c | ||
|
|
bfaf9cd6eb | ||
|
|
be2e4b5d85 | ||
|
|
cc0fc953cf | ||
|
|
3b35d12e60 | ||
|
|
4bc679b0b3 | ||
|
|
be56c72b5f | ||
|
|
db92840ffc | ||
|
|
593a4ec0b0 | ||
|
|
d018a547a2 | ||
|
|
a246200675 | ||
|
|
1cb6517812 | ||
|
|
2824da371f | ||
|
|
22eb86089f | ||
|
|
2e17366bce | ||
|
|
ef407ae93a | ||
|
|
9032ce99d3 | ||
|
|
adaaeb6f36 | ||
|
|
4ee1e00808 | ||
|
|
fd25bd7ca2 | ||
|
|
607c819109 | ||
|
|
61fbbb0054 | ||
|
|
49b76baf3f |
@@ -0,0 +1,16 @@
|
|||||||
|
node_modules/
|
||||||
|
*.swp
|
||||||
|
.lock-wscript
|
||||||
|
lib/
|
||||||
|
Makefile.gyp
|
||||||
|
*.Makefile
|
||||||
|
*.target.gyp.mk
|
||||||
|
*.node
|
||||||
|
example/*.log
|
||||||
|
docs/
|
||||||
|
npm-debug.log
|
||||||
|
/.idea/
|
||||||
|
.env
|
||||||
|
build/
|
||||||
|
.vscode/
|
||||||
|
.git/
|
||||||
@@ -5,6 +5,7 @@ indent_style = space
|
|||||||
indent_size = 2
|
indent_size = 2
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
|
end_of_line = lf
|
||||||
|
|
||||||
[*.{j,t}s]
|
[*.{j,t}s]
|
||||||
max_line_length = 100
|
max_line_length = 100
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
* text=auto
|
||||||
@@ -12,3 +12,7 @@ npm-debug.log
|
|||||||
/.idea/
|
/.idea/
|
||||||
.env
|
.env
|
||||||
build/
|
build/
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# Directories needed for code coverage
|
||||||
|
/coverage/
|
||||||
|
|||||||
@@ -16,3 +16,4 @@ addons:
|
|||||||
notifications:
|
notifications:
|
||||||
email: false
|
email: false
|
||||||
script: npm run $NPM_COMMAND
|
script: npm run $NPM_COMMAND
|
||||||
|
after_success: npm run coveralls
|
||||||
|
|||||||
Vendored
+23
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,34 +1,62 @@
|
|||||||
List of xterm.js contributors. Updated before every release.
|
List of xterm.js contributors. Updated before every release.
|
||||||
|
|
||||||
|
Aleksandr Andrienko <aandrienko@codenvy.com>
|
||||||
|
Aleksandr Andriienko <oandriie@redhat.com>
|
||||||
Alessandro Nadalin <alessandro.nadalin@gmail.com>
|
Alessandro Nadalin <alessandro.nadalin@gmail.com>
|
||||||
Alexander Olsson <noseglid@gmail.com>
|
Alexander Olsson <noseglid@gmail.com>
|
||||||
|
Alexey Kontsevoy <alexey@gravitational.com>
|
||||||
|
Anish Athalye <me@anishathalye.com>
|
||||||
Antonis Kalipetis <akalipetis@sourcelair.com>
|
Antonis Kalipetis <akalipetis@sourcelair.com>
|
||||||
Anton Skshidlevsky <meefik@gmail.com>
|
Anton Skshidlevsky <meefik@gmail.com>
|
||||||
Anton Yurovskykh <anton.yurovskykh@gmail.com>
|
Anton Yurovskykh <anton.yurovskykh@gmail.com>
|
||||||
|
Artem Arbatskiy <artyom.arbatskiy@gmail.com>
|
||||||
Austin Robertson <austinrobertson@gmail.com>
|
Austin Robertson <austinrobertson@gmail.com>
|
||||||
ayapi <colors.aya@gmail.com>
|
ayapi <colors.aya@gmail.com>
|
||||||
|
Ben Hall <ben@benhall.me.uk>
|
||||||
Benjamin Fischer <benjamin.fischer@rwth-aachen.de>
|
Benjamin Fischer <benjamin.fischer@rwth-aachen.de>
|
||||||
|
Bill Church <billchurch@users.noreply.github.com>
|
||||||
|
Bob Reid <bobreid@Bobs-MacBook-Pro.local>
|
||||||
bottleofwater <nison.mael+bottleofwater@gmail.com>
|
bottleofwater <nison.mael+bottleofwater@gmail.com>
|
||||||
|
Brian Mock <brian@mockbrian.com>
|
||||||
Carson Anderson <carson@betterservers.com>
|
Carson Anderson <carson@betterservers.com>
|
||||||
|
CHaBou <chabup@delean.fr>
|
||||||
Christian Budde Christensen <budde377@gmail.com>
|
Christian Budde Christensen <budde377@gmail.com>
|
||||||
Christopher Jeffrey <chjjeffrey@gmail.com>
|
Christopher Jeffrey <chjjeffrey@gmail.com>
|
||||||
|
coderaiser <mnemonic.enemy@gmail.com>
|
||||||
Daniel Imms <daimms@microsoft.com>
|
Daniel Imms <daimms@microsoft.com>
|
||||||
Daniel Risacher <drisacher@gmail.com>
|
Daniel Risacher <drisacher@gmail.com>
|
||||||
Dan Kaplun <dbkaplun@twitch.tv>
|
Dan Kaplun <dbkaplun@twitch.tv>
|
||||||
Darin Morrison <freebroccolo@users.noreply.github.com>
|
Darin Morrison <freebroccolo@users.noreply.github.com>
|
||||||
|
Edgar Andrés Margffoy Tuay <andfoy@gmail.com>
|
||||||
Elliot Saba <staticfloat@gmail.com>
|
Elliot Saba <staticfloat@gmail.com>
|
||||||
|
Gary Ritchie <gary@rstudio.com>
|
||||||
hiro-su <h.sugipon@gmail.com>
|
hiro-su <h.sugipon@gmail.com>
|
||||||
Ian Lewis <ianlewis@google.com>
|
Ian Lewis <ianlewis@google.com>
|
||||||
imoses <ido@twiggle.com>
|
imoses <ido@twiggle.com>
|
||||||
|
InDieTasten <indietasten@gmail.com>
|
||||||
|
irokas <akasidiari@inf.uth.gr>
|
||||||
Jean Bruenn <himself@jeanbruenn.info>
|
Jean Bruenn <himself@jeanbruenn.info>
|
||||||
|
Jeremy Danyow <jedanyow@microsoft.com>
|
||||||
Jörg Breitbart <jerch@rockborn.de>
|
Jörg Breitbart <jerch@rockborn.de>
|
||||||
|
Justin Mecham <justin@mecham.me>
|
||||||
|
Lucian Buzzo <lucian.buzzo@gmail.com>
|
||||||
Maël Nison <nison.mael@gmail.com>
|
Maël Nison <nison.mael@gmail.com>
|
||||||
|
Martin Chloride <i@martincl2.me>
|
||||||
|
Martin Wang <jiahaow@ca.ibm.com>
|
||||||
|
Michael Irwin <mikesir87@gmail.com>
|
||||||
Mikko Karvonen <mikko.karvonen@arm.com>
|
Mikko Karvonen <mikko.karvonen@arm.com>
|
||||||
|
Nicolas Ramz <nicolas.ramz@gmail.com>
|
||||||
|
Oleksandr Andriienko <oandriie@redhat.com>
|
||||||
Paris Kasidiaris <pariskasidiaris@gmail.com>
|
Paris Kasidiaris <pariskasidiaris@gmail.com>
|
||||||
Paris Kasidiaris <paris@sourcelair.com>
|
Paris Kasidiaris <paris@sourcelair.com>
|
||||||
runarberg <runar@greenqloud.com>
|
runarberg <runar@greenqloud.com>
|
||||||
|
Saswat Das <saswatds@users.noreply.github.com>
|
||||||
|
Saul Costa <saul@codevolve.com>
|
||||||
Shuanglei Tao <tsl0922@gmail.com>
|
Shuanglei Tao <tsl0922@gmail.com>
|
||||||
Steven Silvester <steven.silvester@ieee.org>
|
Steven Silvester <steven.silvester@ieee.org>
|
||||||
Thanasis Daglis <thanasis@sourcelair.com>
|
Thanasis Daglis <thanasis@sourcelair.com>
|
||||||
Tine Jozelj <tine.jozelj@outlook.com>
|
Tine Jozelj <tine.jozelj@outlook.com>
|
||||||
|
Tyler Jewell <tjewell@codenvy.com>
|
||||||
|
Vincent Woo <me@vincentwoo.com>
|
||||||
|
yutaka <shigure@refy.net>
|
||||||
YuviPanda <yuvipanda@gmail.com>
|
YuviPanda <yuvipanda@gmail.com>
|
||||||
|
|||||||
+17
-2
@@ -1,4 +1,19 @@
|
|||||||
FROM node:4-onbuild
|
FROM node:6.9
|
||||||
MAINTAINER Paris Kasidiaris <paris@sourcelair.com>
|
MAINTAINER Paris Kasidiaris <paris@sourcelair.com>
|
||||||
|
|
||||||
EXPOSE 3000
|
# Set the working directory
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
# Set an entrypoint, to automatically install node modules
|
||||||
|
ENTRYPOINT ["/bin/bash", "-c", "if [[ ! -d node_modules ]]; then npm install; fi; exec \"${@:0}\";"]
|
||||||
|
CMD ["npm", "run", "dev"]
|
||||||
|
|
||||||
|
# First, install dependencies to improve layer caching
|
||||||
|
COPY package.json /usr/src/app/
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
# Add the code
|
||||||
|
COPY . /usr/src/app
|
||||||
|
|
||||||
|
# Run the tests and build, to make sure everything is working nicely
|
||||||
|
RUN npm run build && npm run test
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
web: npm start
|
web: npm run dev
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# xterm.js
|
# [](https://xtermjs.org)
|
||||||
|
|
||||||
[](https://travis-ci.org/sourcelair/xterm.js) [](https://gitter.im/sourcelair/xterm.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
[](https://travis-ci.org/sourcelair/xterm.js) [](https://coveralls.io/github/sourcelair/xterm.js) [](https://gitter.im/sourcelair/xterm.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
||||||
|
|
||||||
Xterm.js is a terminal front-end component written in JavaScript that works in the browser.
|
Xterm.js is a terminal front-end component written in JavaScript that works in the browser.
|
||||||
|
|
||||||
@@ -25,6 +25,19 @@ Xterm.js is used in several world-class applications to provide great terminal e
|
|||||||
- [**SourceLair**](https://www.sourcelair.com/): In-browser IDE that provides its users with fully-featured Linux terminals based on xterm.js
|
- [**SourceLair**](https://www.sourcelair.com/): In-browser IDE that provides its users with fully-featured Linux terminals based on xterm.js
|
||||||
- [**Microsoft Visual Studio Code**](http://code.visualstudio.com/): Modern, versatile and powerful open source code editor that provides an integrated terminal based on xterm.js
|
- [**Microsoft Visual Studio Code**](http://code.visualstudio.com/): Modern, versatile and powerful open source code editor that provides an integrated terminal based on xterm.js
|
||||||
- [**ttyd**](https://github.com/tsl0922/ttyd): A command-line tool for sharing terminal over the web, with fully-featured terminal emulation based on xterm.js
|
- [**ttyd**](https://github.com/tsl0922/ttyd): A command-line tool for sharing terminal over the web, with fully-featured terminal emulation based on xterm.js
|
||||||
|
- [**Katacoda**](https://www.katacoda.com/): Katacoda is an Interactive Learning Platform for software developers, covering the latest Cloud Native technologies.
|
||||||
|
- [**Eclipse Che**](http://www.eclipse.org/che): Developer workspace server, cloud IDE, and Eclipse next-generation IDE.
|
||||||
|
- [**Codenvy**](http://www.codenvy.com): Cloud workspaces for development teams.
|
||||||
|
- [**CoderPad**](https://coderpad.io): Online interviewing platform for programmers. Run code in many programming languages, with results displayed by `xterm.js`.
|
||||||
|
- [**WebSSH2**](https://github.com/billchurch/WebSSH2): A web based SSH2 client using `xterm.js`, socket.io, and ssh2.
|
||||||
|
- [**Spyder Terminal**](https://github.com/spyder-ide/spyder-terminal): A full fledged system terminal embedded on Spyder IDE.
|
||||||
|
- [**Cloud Commander**](https://cloudcmd.io "Cloud Commander"): Orthodox web file manager with console and editor.
|
||||||
|
- [**Codevolve**](https://www.codevolve.com "Codevolve"): Online platform for interactive coding and web development courses. Live container-backed terminal uses `xterm.js`.
|
||||||
|
- [**RStudio**](https://www.rstudio.com/products/RStudio "RStudio"): RStudio is an integrated development environment (IDE) for R.
|
||||||
|
- [**Terminal for Atom**](https://github.com/jsmecham/atom-terminal-tab): A simple terminal for the Atom text editor.
|
||||||
|
- [**Eclipse Orion**](https://orionhub.org): A modern, open source software development environment that runs in the cloud. Code, deploy and run in the cloud.
|
||||||
|
- [**Gravitational Teleport**](https://github.com/gravitational/teleport): Gravitational Teleport is a modern SSH server for remotely accessing clusters of Linux servers via SSH or HTTPS.
|
||||||
|
- [**Hexlet**](https://en.hexlet.io): Practical programming courses (JavaScript, PHP, Unix, databases, functional programming). A steady path from the first line of code to the first job.
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
@@ -43,14 +56,51 @@ Xterm.js works seamlessly in Electron apps and may even work on earlier versions
|
|||||||
|
|
||||||
## Demo
|
## Demo
|
||||||
|
|
||||||
To launch the demo simply run:
|
### Linux or macOS
|
||||||
|
|
||||||
|
Run the following commands:
|
||||||
|
|
||||||
```
|
```
|
||||||
npm install
|
$ npm install
|
||||||
npm start
|
$ npm start
|
||||||
```
|
```
|
||||||
|
|
||||||
Then open http://0.0.0.0:3000 in a web browser (use http://127.0.0.1:3000 if running under Windows).
|
Then open http://0.0.0.0:3000 in a web browser.
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
|
||||||
|
First, ensure [node-gyp](https://github.com/nodejs/node-gyp) is installed and configured correctly, then run these commands.
|
||||||
|
|
||||||
|
Note: Do not use ConEmu, as it seems to break the demo for some reason.
|
||||||
|
|
||||||
|
```
|
||||||
|
> npm install
|
||||||
|
> npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
Then open http://127.0.0.1:3000 in a web browser.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to the head of your html page. Then create a `<div id="terminal"></div>` onto which xterm can attach itself.
|
||||||
|
```html
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<link rel="stylesheet" href="bower_components/xterm.js/dist/xterm.css" />
|
||||||
|
<script src="bower_components/xterm.js/dist/xterm.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="terminal"></div>
|
||||||
|
<script>
|
||||||
|
var term = new Terminal();
|
||||||
|
term.open(document.getElementById('terminal'));
|
||||||
|
term.write('Hello from \033[1;3;31mxterm.js\033[0m $ ')
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
Finally instantiate the `Terminal` object and then call the `open` function with the DOM object of the `div`.
|
||||||
|
|
||||||
## Addons
|
## Addons
|
||||||
|
|
||||||
@@ -79,19 +129,7 @@ The existing releases are available at this GitHub repo's [Releases](https://git
|
|||||||
|
|
||||||
Xterm.js is maintained by [SourceLair](https://www.sourcelair.com/) and a few external contributors, but we would love to receive contributions from everyone!
|
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) before.
|
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.
|
||||||
|
|
||||||
The development of xterm.js does not require any special tool. All you need is an editor that supports JavaScript and a browser (if you would like to run the demo you will need Node.js to get all features).
|
|
||||||
|
|
||||||
It is recommended though to use a development tool that uses xterm.js internally, to develop for xterm.js. [Eating our own dogfood](https://en.wikipedia.org/wiki/Eating_your_own_dog_food) has been proved extremely beneficial for this project. Known tools that use xterm.js internally are:
|
|
||||||
|
|
||||||
#### [SourceLair](https://www.sourcelair.com)
|
|
||||||
|
|
||||||
Visit https://lair.io/sourcelair/xterm and follow the instructions. All development will happen in your browser.
|
|
||||||
|
|
||||||
#### [Visual Studio Code](http://code.visualstudio.com/)
|
|
||||||
|
|
||||||
[Download Visual Studio Code](http://code.visualstudio.com/Download), clone xterm.js and you are all set.
|
|
||||||
|
|
||||||
## License Agreement
|
## License Agreement
|
||||||
|
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
#! /usr/bin/env bash
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# $BUILD_DIR should default to "build"
|
|
||||||
BUILD_DIR=${BUILD_DIR:=build}
|
|
||||||
|
|
||||||
# Create the build directory
|
|
||||||
mkdir -p $BUILD_DIR
|
|
||||||
|
|
||||||
|
|
||||||
# Clean lib/* to prevent confusion if files were deleted in src/
|
|
||||||
rm -rf lib/*
|
|
||||||
|
|
||||||
# Build all TypeScript files (including tests) to lib/
|
|
||||||
tsc
|
|
||||||
|
|
||||||
# Concat all xterm.js files into a single file and output as a UMD to $BUILD_DIR/xterm.js
|
|
||||||
browserify ./lib/xterm.js --standalone Terminal --debug --outfile ./$BUILD_DIR/xterm.js
|
|
||||||
cat ./$BUILD_DIR/xterm.js | exorcist ./$BUILD_DIR/xterm.js.map -b ./$BUILD_DIR > ./$BUILD_DIR/xterm.temp.js
|
|
||||||
rm ./$BUILD_DIR/xterm.js
|
|
||||||
mv ./$BUILD_DIR/xterm.temp.js ./$BUILD_DIR/xterm.js
|
|
||||||
|
|
||||||
# Resolve the chain of sourcemaps so that ./$BUILD_DIR/xterm.js.map points at ./src
|
|
||||||
sorcery -i $BUILD_DIR/xterm.js
|
|
||||||
|
|
||||||
# Copy all CSS files from src/ to $BUILD_DIR/
|
|
||||||
cd src
|
|
||||||
find . -name '*.css' | cpio -pdm ../$BUILD_DIR
|
|
||||||
cd ..
|
|
||||||
|
|
||||||
# Copy addons from lib/ to $BUILD_DIR/
|
|
||||||
cd lib/addons
|
|
||||||
find . -name '*.js' | cpio -pdm ../../$BUILD_DIR/addons
|
|
||||||
cd ../..
|
|
||||||
+1
-1
@@ -21,7 +21,7 @@ CURRENT_BOWER_JSON_VERSION=$(cat bower.json \
|
|||||||
|
|
||||||
# Build xterm.js into `dist`
|
# Build xterm.js into `dist`
|
||||||
export BUILD_DIR=dist
|
export BUILD_DIR=dist
|
||||||
./bin/build
|
npm run build
|
||||||
|
|
||||||
# Update AUTHORS file
|
# Update AUTHORS file
|
||||||
sh bin/generate-authors
|
sh bin/generate-authors
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "xterm.js",
|
"name": "xterm.js",
|
||||||
"version": "2.2.2",
|
"version": "2.8.0",
|
||||||
"ignore": ["demo", "test", ".gitignore"],
|
"ignore": ["demo", "test", ".gitignore"],
|
||||||
"main": [
|
"main": [
|
||||||
"dist/xterm.js",
|
"dist/xterm.js",
|
||||||
|
|||||||
+2
-2
@@ -2,7 +2,7 @@ var express = require('express');
|
|||||||
var app = express();
|
var app = express();
|
||||||
var expressWs = require('express-ws')(app);
|
var expressWs = require('express-ws')(app);
|
||||||
var os = require('os');
|
var os = require('os');
|
||||||
var pty = require('pty.js');
|
var pty = require('node-pty');
|
||||||
|
|
||||||
var terminals = {},
|
var terminals = {},
|
||||||
logs = {};
|
logs = {};
|
||||||
@@ -69,7 +69,7 @@ app.ws('/terminals/:pid', function (ws, req) {
|
|||||||
term.write(msg);
|
term.write(msg);
|
||||||
});
|
});
|
||||||
ws.on('close', function () {
|
ws.on('close', function () {
|
||||||
process.kill(term.pid);
|
term.kill();
|
||||||
console.log('Closed terminal ' + term.pid);
|
console.log('Closed terminal ' + term.pid);
|
||||||
// Clean things up
|
// Clean things up
|
||||||
delete terminals[term.pid];
|
delete terminals[term.pid];
|
||||||
|
|||||||
+20
-1
@@ -5,6 +5,7 @@
|
|||||||
<link rel="stylesheet" href="/build/xterm.css" />
|
<link rel="stylesheet" href="/build/xterm.css" />
|
||||||
<link rel="stylesheet" href="/build/addons/fullscreen/fullscreen.css" />
|
<link rel="stylesheet" href="/build/addons/fullscreen/fullscreen.css" />
|
||||||
<link rel="stylesheet" href="style.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>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/fetch/1.0.0/fetch.min.js"></script>
|
||||||
<script src="/build/xterm.js" ></script>
|
<script src="/build/xterm.js" ></script>
|
||||||
<script src="/build/addons/attach/attach.js" ></script>
|
<script src="/build/addons/attach/attach.js" ></script>
|
||||||
@@ -16,7 +17,25 @@
|
|||||||
<div id="terminal-container"></div>
|
<div id="terminal-container"></div>
|
||||||
<div>
|
<div>
|
||||||
<h2>Options</h2>
|
<h2>Options</h2>
|
||||||
<label><input type="checkbox" id="option-cursor-blink"> cursorBlink</label>
|
<p>
|
||||||
|
<label><input type="checkbox" id="option-cursor-blink"> cursorBlink</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>scrollback <input type="number" id="option-scrollback" value="1000" /></label>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<label>tabStopWidth <input type="number" id="option-tabstopwidth" value="8" /></label>
|
||||||
|
</p>
|
||||||
<div>
|
<div>
|
||||||
<h3>Size</h3>
|
<h3>Size</h3>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
+21
-6
@@ -8,14 +8,17 @@ var term,
|
|||||||
|
|
||||||
var terminalContainer = document.getElementById('terminal-container'),
|
var terminalContainer = document.getElementById('terminal-container'),
|
||||||
optionElements = {
|
optionElements = {
|
||||||
cursorBlink: document.querySelector('#option-cursor-blink')
|
cursorBlink: document.querySelector('#option-cursor-blink'),
|
||||||
|
cursorStyle: document.querySelector('#option-cursor-style'),
|
||||||
|
scrollback: document.querySelector('#option-scrollback'),
|
||||||
|
tabstopwidth: document.querySelector('#option-tabstopwidth')
|
||||||
},
|
},
|
||||||
colsElement = document.getElementById('cols'),
|
colsElement = document.getElementById('cols'),
|
||||||
rowsElement = document.getElementById('rows');
|
rowsElement = document.getElementById('rows');
|
||||||
|
|
||||||
function setTerminalSize () {
|
function setTerminalSize () {
|
||||||
var cols = parseInt(colsElement.value),
|
var cols = parseInt(colsElement.value, 10),
|
||||||
rows = parseInt(rowsElement.value),
|
rows = parseInt(rowsElement.value, 10),
|
||||||
width = (cols * charWidth).toString() + 'px',
|
width = (cols * charWidth).toString() + 'px',
|
||||||
height = (rows * charHeight).toString() + 'px';
|
height = (rows * charHeight).toString() + 'px';
|
||||||
|
|
||||||
@@ -27,7 +30,18 @@ function setTerminalSize () {
|
|||||||
colsElement.addEventListener('change', setTerminalSize);
|
colsElement.addEventListener('change', setTerminalSize);
|
||||||
rowsElement.addEventListener('change', setTerminalSize);
|
rowsElement.addEventListener('change', setTerminalSize);
|
||||||
|
|
||||||
optionElements.cursorBlink.addEventListener('change', createTerminal);
|
optionElements.cursorBlink.addEventListener('change', function () {
|
||||||
|
term.setOption('cursorBlink', optionElements.cursorBlink.checked);
|
||||||
|
});
|
||||||
|
optionElements.cursorStyle.addEventListener('change', function () {
|
||||||
|
term.setOption('cursorStyle', optionElements.cursorStyle.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));
|
||||||
|
});
|
||||||
|
|
||||||
createTerminal();
|
createTerminal();
|
||||||
|
|
||||||
@@ -37,7 +51,9 @@ function createTerminal() {
|
|||||||
terminalContainer.removeChild(terminalContainer.children[0]);
|
terminalContainer.removeChild(terminalContainer.children[0]);
|
||||||
}
|
}
|
||||||
term = new Terminal({
|
term = new Terminal({
|
||||||
cursorBlink: optionElements.cursorBlink.checked
|
cursorBlink: optionElements.cursorBlink.checked,
|
||||||
|
scrollback: parseInt(optionElements.scrollback.value, 10),
|
||||||
|
tabStopWidth: parseInt(optionElements.tabstopwidth.value, 10)
|
||||||
});
|
});
|
||||||
term.on('resize', function (size) {
|
term.on('resize', function (size) {
|
||||||
if (!pid) {
|
if (!pid) {
|
||||||
@@ -78,7 +94,6 @@ function createTerminal() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function runRealTerminal() {
|
function runRealTerminal() {
|
||||||
term.attach(socket);
|
term.attach(socket);
|
||||||
term._initialized = true;
|
term._initialized = true;
|
||||||
|
|||||||
@@ -20,7 +20,3 @@ h1 {
|
|||||||
color: #fafafa;
|
color: #fafafa;
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#terminal-container .terminal:focus .terminal-cursor {
|
|
||||||
background-color: #fafafa;
|
|
||||||
}
|
|
||||||
|
|||||||
Vendored
+114
-102
@@ -3,112 +3,124 @@
|
|||||||
* @module xterm/addons/attach/attach
|
* @module xterm/addons/attach/attach
|
||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
(function (attach) {
|
(function (attach) {
|
||||||
if (typeof exports === 'object' && typeof module === 'object') {
|
if (typeof exports === 'object' && typeof module === 'object') {
|
||||||
/*
|
/*
|
||||||
* CommonJS environment
|
* CommonJS environment
|
||||||
*/
|
*/
|
||||||
module.exports = attach(require('../../xterm'));
|
module.exports = attach(require('../../xterm'));
|
||||||
}
|
} else if (typeof define == 'function') {
|
||||||
else if (typeof define == 'function') {
|
/*
|
||||||
/*
|
* Require.js is available
|
||||||
* Require.js is available
|
*/
|
||||||
*/
|
define(['../../xterm'], attach);
|
||||||
define(['../../xterm'], attach);
|
} else {
|
||||||
}
|
/*
|
||||||
else {
|
* Plain browser environment
|
||||||
/*
|
*/
|
||||||
* Plain browser environment
|
attach(window.Terminal);
|
||||||
*/
|
}
|
||||||
attach(window.Terminal);
|
|
||||||
}
|
|
||||||
})(function (Xterm) {
|
})(function (Xterm) {
|
||||||
'use strict';
|
'use strict';
|
||||||
var exports = {};
|
|
||||||
/**
|
var exports = {};
|
||||||
* Attaches the given terminal to the given socket.
|
|
||||||
*
|
/**
|
||||||
* @param {Xterm} term - The terminal to be attached to the given socket.
|
* Attaches the given terminal to the given socket.
|
||||||
* @param {WebSocket} socket - The socket to attach the current terminal.
|
*
|
||||||
* @param {boolean} bidirectional - Whether the terminal should send data
|
* @param {Xterm} term - The terminal to be attached to the given socket.
|
||||||
* to the socket as well.
|
* @param {WebSocket} socket - The socket to attach the current terminal.
|
||||||
* @param {boolean} buffered - Whether the rendering of incoming data
|
* @param {boolean} bidirectional - Whether the terminal should send data
|
||||||
* should happen instantly or at a maximum
|
* to the socket as well.
|
||||||
* frequency of 1 rendering per 10ms.
|
* @param {boolean} buffered - Whether the rendering of incoming data
|
||||||
*/
|
* should happen instantly or at a maximum
|
||||||
exports.attach = function (term, socket, bidirectional, buffered) {
|
* frequency of 1 rendering per 10ms.
|
||||||
bidirectional = (typeof bidirectional == 'undefined') ? true : bidirectional;
|
*/
|
||||||
term.socket = socket;
|
exports.attach = function (term, socket, bidirectional, buffered) {
|
||||||
term._flushBuffer = function () {
|
bidirectional = (typeof bidirectional == 'undefined') ? true : bidirectional;
|
||||||
term.write(term._attachSocketBuffer);
|
term.socket = socket;
|
||||||
term._attachSocketBuffer = null;
|
|
||||||
clearTimeout(term._attachSocketBufferTimer);
|
term._flushBuffer = function () {
|
||||||
term._attachSocketBufferTimer = null;
|
term.write(term._attachSocketBuffer);
|
||||||
};
|
term._attachSocketBuffer = null;
|
||||||
term._pushToBuffer = function (data) {
|
clearTimeout(term._attachSocketBufferTimer);
|
||||||
if (term._attachSocketBuffer) {
|
term._attachSocketBufferTimer = null;
|
||||||
term._attachSocketBuffer += data;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
term._attachSocketBuffer = data;
|
|
||||||
setTimeout(term._flushBuffer, 10);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
term._getMessage = function (ev) {
|
|
||||||
if (buffered) {
|
|
||||||
term._pushToBuffer(ev.data);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
term.write(ev.data);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
term._sendData = function (data) {
|
|
||||||
socket.send(data);
|
|
||||||
};
|
|
||||||
socket.addEventListener('message', term._getMessage);
|
|
||||||
if (bidirectional) {
|
|
||||||
term.on('data', term._sendData);
|
|
||||||
}
|
|
||||||
socket.addEventListener('close', term.detach.bind(term, socket));
|
|
||||||
socket.addEventListener('error', term.detach.bind(term, socket));
|
|
||||||
};
|
};
|
||||||
/**
|
|
||||||
* Detaches the given terminal from the given socket
|
term._pushToBuffer = function (data) {
|
||||||
*
|
if (term._attachSocketBuffer) {
|
||||||
* @param {Xterm} term - The terminal to be detached from the given socket.
|
term._attachSocketBuffer += data;
|
||||||
* @param {WebSocket} socket - The socket from which to detach the current
|
} else {
|
||||||
* terminal.
|
term._attachSocketBuffer = data;
|
||||||
*/
|
setTimeout(term._flushBuffer, 10);
|
||||||
exports.detach = function (term, socket) {
|
}
|
||||||
term.off('data', term._sendData);
|
|
||||||
socket = (typeof socket == 'undefined') ? term.socket : socket;
|
|
||||||
if (socket) {
|
|
||||||
socket.removeEventListener('message', term._getMessage);
|
|
||||||
}
|
|
||||||
delete term.socket;
|
|
||||||
};
|
};
|
||||||
/**
|
|
||||||
* Attaches the current terminal to the given socket
|
term._getMessage = function (ev) {
|
||||||
*
|
if (buffered) {
|
||||||
* @param {WebSocket} socket - The socket to attach the current terminal.
|
term._pushToBuffer(ev.data);
|
||||||
* @param {boolean} bidirectional - Whether the terminal should send data
|
} else {
|
||||||
* to the socket as well.
|
term.write(ev.data);
|
||||||
* @param {boolean} buffered - Whether the rendering of incoming data
|
}
|
||||||
* should happen instantly or at a maximum
|
|
||||||
* frequency of 1 rendering per 10ms.
|
|
||||||
*/
|
|
||||||
Xterm.prototype.attach = function (socket, bidirectional, buffered) {
|
|
||||||
return exports.attach(this, socket, bidirectional, buffered);
|
|
||||||
};
|
};
|
||||||
/**
|
|
||||||
* Detaches the current terminal from the given socket.
|
term._sendData = function (data) {
|
||||||
*
|
socket.send(data);
|
||||||
* @param {WebSocket} socket - The socket from which to detach the current
|
|
||||||
* terminal.
|
|
||||||
*/
|
|
||||||
Xterm.prototype.detach = function (socket) {
|
|
||||||
return exports.detach(this, socket);
|
|
||||||
};
|
};
|
||||||
return exports;
|
|
||||||
|
socket.addEventListener('message', term._getMessage);
|
||||||
|
|
||||||
|
if (bidirectional) {
|
||||||
|
term.on('data', term._sendData);
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.addEventListener('close', term.detach.bind(term, socket));
|
||||||
|
socket.addEventListener('error', term.detach.bind(term, socket));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detaches the given terminal from the given socket
|
||||||
|
*
|
||||||
|
* @param {Xterm} term - The terminal to be detached from the given socket.
|
||||||
|
* @param {WebSocket} socket - The socket from which to detach the current
|
||||||
|
* terminal.
|
||||||
|
*/
|
||||||
|
exports.detach = function (term, socket) {
|
||||||
|
term.off('data', term._sendData);
|
||||||
|
|
||||||
|
socket = (typeof socket == 'undefined') ? term.socket : socket;
|
||||||
|
|
||||||
|
if (socket) {
|
||||||
|
socket.removeEventListener('message', term._getMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete term.socket;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attaches the current terminal to the given socket
|
||||||
|
*
|
||||||
|
* @param {WebSocket} socket - The socket to attach the current terminal.
|
||||||
|
* @param {boolean} bidirectional - Whether the terminal should send data
|
||||||
|
* to the socket as well.
|
||||||
|
* @param {boolean} buffered - Whether the rendering of incoming data
|
||||||
|
* should happen instantly or at a maximum
|
||||||
|
* frequency of 1 rendering per 10ms.
|
||||||
|
*/
|
||||||
|
Xterm.prototype.attach = function (socket, bidirectional, buffered) {
|
||||||
|
return exports.attach(this, socket, bidirectional, buffered);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detaches the current terminal from the given socket.
|
||||||
|
*
|
||||||
|
* @param {WebSocket} socket - The socket from which to detach the current
|
||||||
|
* terminal.
|
||||||
|
*/
|
||||||
|
Xterm.prototype.detach = function (socket) {
|
||||||
|
return exports.detach(this, socket);
|
||||||
|
};
|
||||||
|
|
||||||
|
return exports;
|
||||||
});
|
});
|
||||||
//# sourceMappingURL=attach.js.map
|
|
||||||
Vendored
+71
-44
@@ -10,50 +10,77 @@
|
|||||||
* @module xterm/addons/fit/fit
|
* @module xterm/addons/fit/fit
|
||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
(function (fit) {
|
(function (fit) {
|
||||||
if (typeof exports === 'object' && typeof module === 'object') {
|
if (typeof exports === 'object' && typeof module === 'object') {
|
||||||
/*
|
/*
|
||||||
* CommonJS environment
|
* CommonJS environment
|
||||||
*/
|
*/
|
||||||
module.exports = fit(require('../../xterm'));
|
module.exports = fit(require('../../xterm'));
|
||||||
}
|
} else if (typeof define == 'function') {
|
||||||
else if (typeof define == 'function') {
|
/*
|
||||||
/*
|
* Require.js is available
|
||||||
* Require.js is available
|
*/
|
||||||
*/
|
define(['../../xterm'], fit);
|
||||||
define(['../../xterm'], fit);
|
} else {
|
||||||
}
|
/*
|
||||||
else {
|
* Plain browser environment
|
||||||
/*
|
*/
|
||||||
* Plain browser environment
|
fit(window.Terminal);
|
||||||
*/
|
}
|
||||||
fit(window.Terminal);
|
|
||||||
}
|
|
||||||
})(function (Xterm) {
|
})(function (Xterm) {
|
||||||
var exports = {};
|
var exports = {};
|
||||||
exports.proposeGeometry = function (term) {
|
|
||||||
var parentElementStyle = window.getComputedStyle(term.element.parentElement), parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height')), parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')) - 17), elementStyle = window.getComputedStyle(term.element), elementPaddingVer = parseInt(elementStyle.getPropertyValue('padding-top')) + parseInt(elementStyle.getPropertyValue('padding-bottom')), elementPaddingHor = parseInt(elementStyle.getPropertyValue('padding-right')) + parseInt(elementStyle.getPropertyValue('padding-left')), availableHeight = parentElementHeight - elementPaddingVer, availableWidth = parentElementWidth - elementPaddingHor, container = term.rowContainer, subjectRow = term.rowContainer.firstElementChild, contentBuffer = subjectRow.innerHTML, characterHeight, rows, characterWidth, cols, geometry;
|
exports.proposeGeometry = function (term) {
|
||||||
subjectRow.style.display = 'inline';
|
if (!term.element.parentElement) {
|
||||||
subjectRow.innerHTML = 'W'; // Common character for measuring width, although on monospace
|
return null;
|
||||||
characterWidth = subjectRow.getBoundingClientRect().width;
|
}
|
||||||
subjectRow.style.display = ''; // Revert style before calculating height, since they differ.
|
var parentElementStyle = window.getComputedStyle(term.element.parentElement),
|
||||||
characterHeight = parseInt(subjectRow.offsetHeight);
|
parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height')),
|
||||||
subjectRow.innerHTML = contentBuffer;
|
parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')) - 17),
|
||||||
rows = parseInt(availableHeight / characterHeight);
|
elementStyle = window.getComputedStyle(term.element),
|
||||||
cols = parseInt(availableWidth / characterWidth);
|
elementPaddingVer = parseInt(elementStyle.getPropertyValue('padding-top')) + parseInt(elementStyle.getPropertyValue('padding-bottom')),
|
||||||
geometry = { cols: cols, rows: rows };
|
elementPaddingHor = parseInt(elementStyle.getPropertyValue('padding-right')) + parseInt(elementStyle.getPropertyValue('padding-left')),
|
||||||
return geometry;
|
availableHeight = parentElementHeight - elementPaddingVer,
|
||||||
};
|
availableWidth = parentElementWidth - elementPaddingHor,
|
||||||
exports.fit = function (term) {
|
container = term.rowContainer,
|
||||||
var geometry = exports.proposeGeometry(term);
|
subjectRow = term.rowContainer.firstElementChild,
|
||||||
term.resize(geometry.cols, geometry.rows);
|
contentBuffer = subjectRow.innerHTML,
|
||||||
};
|
characterHeight,
|
||||||
Xterm.prototype.proposeGeometry = function () {
|
rows,
|
||||||
return exports.proposeGeometry(this);
|
characterWidth,
|
||||||
};
|
cols,
|
||||||
Xterm.prototype.fit = function () {
|
geometry;
|
||||||
return exports.fit(this);
|
|
||||||
};
|
subjectRow.style.display = 'inline';
|
||||||
return exports;
|
subjectRow.innerHTML = 'W'; // Common character for measuring width, although on monospace
|
||||||
|
characterWidth = subjectRow.getBoundingClientRect().width;
|
||||||
|
subjectRow.style.display = ''; // Revert style before calculating height, since they differ.
|
||||||
|
characterHeight = subjectRow.getBoundingClientRect().height;
|
||||||
|
subjectRow.innerHTML = contentBuffer;
|
||||||
|
|
||||||
|
rows = parseInt(availableHeight / characterHeight);
|
||||||
|
cols = parseInt(availableWidth / characterWidth);
|
||||||
|
|
||||||
|
geometry = {cols: cols, rows: rows};
|
||||||
|
return geometry;
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.fit = function (term) {
|
||||||
|
var geometry = exports.proposeGeometry(term);
|
||||||
|
|
||||||
|
if (geometry) {
|
||||||
|
term.resize(geometry.cols, geometry.rows);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Xterm.prototype.proposeGeometry = function () {
|
||||||
|
return exports.proposeGeometry(this);
|
||||||
|
};
|
||||||
|
|
||||||
|
Xterm.prototype.fit = function () {
|
||||||
|
return exports.fit(this);
|
||||||
|
};
|
||||||
|
|
||||||
|
return exports;
|
||||||
});
|
});
|
||||||
//# sourceMappingURL=fit.js.map
|
|
||||||
Vendored
+42
-42
@@ -4,47 +4,47 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
(function (fullscreen) {
|
(function (fullscreen) {
|
||||||
if (typeof exports === 'object' && typeof module === 'object') {
|
if (typeof exports === 'object' && typeof module === 'object') {
|
||||||
/*
|
/*
|
||||||
* CommonJS environment
|
* CommonJS environment
|
||||||
*/
|
|
||||||
module.exports = fullscreen(require('../../xterm'));
|
|
||||||
}
|
|
||||||
else if (typeof define == 'function') {
|
|
||||||
/*
|
|
||||||
* Require.js is available
|
|
||||||
*/
|
|
||||||
define(['../../xterm'], fullscreen);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
/*
|
|
||||||
* Plain browser environment
|
|
||||||
*/
|
|
||||||
fullscreen(window.Terminal);
|
|
||||||
}
|
|
||||||
})(function (Xterm) {
|
|
||||||
var exports = {};
|
|
||||||
/**
|
|
||||||
* Toggle the given terminal's fullscreen mode.
|
|
||||||
* @param {Xterm} term - The terminal to toggle full screen mode
|
|
||||||
* @param {boolean} fullscreen - Toggle fullscreen on (true) or off (false)
|
|
||||||
*/
|
*/
|
||||||
exports.toggleFullScreen = function (term, fullscreen) {
|
module.exports = fullscreen(require('../../xterm'));
|
||||||
var fn;
|
} else if (typeof define == 'function') {
|
||||||
if (typeof fullscreen == 'undefined') {
|
/*
|
||||||
fn = (term.element.classList.contains('fullscreen')) ? 'remove' : 'add';
|
* Require.js is available
|
||||||
}
|
*/
|
||||||
else if (!fullscreen) {
|
define(['../../xterm'], fullscreen);
|
||||||
fn = 'remove';
|
} else {
|
||||||
}
|
/*
|
||||||
else {
|
* Plain browser environment
|
||||||
fn = 'add';
|
*/
|
||||||
}
|
fullscreen(window.Terminal);
|
||||||
term.element.classList[fn]('fullscreen');
|
}
|
||||||
};
|
})(function (Xterm) {
|
||||||
Xterm.prototype.toggleFullscreen = function (fullscreen) {
|
var exports = {};
|
||||||
exports.toggleFullScreen(this, fullscreen);
|
|
||||||
};
|
/**
|
||||||
return exports;
|
* Toggle the given terminal's fullscreen mode.
|
||||||
|
* @param {Xterm} term - The terminal to toggle full screen mode
|
||||||
|
* @param {boolean} fullscreen - Toggle fullscreen on (true) or off (false)
|
||||||
|
*/
|
||||||
|
exports.toggleFullScreen = function (term, fullscreen) {
|
||||||
|
var fn;
|
||||||
|
|
||||||
|
if (typeof fullscreen == 'undefined') {
|
||||||
|
fn = (term.element.classList.contains('fullscreen')) ? 'remove' : 'add';
|
||||||
|
} else if (!fullscreen) {
|
||||||
|
fn = 'remove';
|
||||||
|
} else {
|
||||||
|
fn = 'add';
|
||||||
|
}
|
||||||
|
|
||||||
|
term.element.classList[fn]('fullscreen');
|
||||||
|
};
|
||||||
|
|
||||||
|
Xterm.prototype.toggleFullscreen = function (fullscreen) {
|
||||||
|
exports.toggleFullScreen(this, fullscreen);
|
||||||
|
};
|
||||||
|
|
||||||
|
return exports;
|
||||||
});
|
});
|
||||||
//# sourceMappingURL=fullscreen.js.map
|
|
||||||
Vendored
+193
-151
@@ -3,163 +3,205 @@
|
|||||||
* @module xterm/addons/linkify/linkify
|
* @module xterm/addons/linkify/linkify
|
||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
(function (linkify) {
|
(function (linkify) {
|
||||||
if (typeof exports === 'object' && typeof module === 'object') {
|
if (typeof exports === 'object' && typeof module === 'object') {
|
||||||
/*
|
/*
|
||||||
* CommonJS environment
|
* CommonJS environment
|
||||||
*/
|
*/
|
||||||
module.exports = linkify(require('../../xterm'));
|
module.exports = linkify(require('../../xterm'));
|
||||||
}
|
} else if (typeof define == 'function') {
|
||||||
else if (typeof define == 'function') {
|
/*
|
||||||
/*
|
* Require.js is available
|
||||||
* Require.js is available
|
*/
|
||||||
*/
|
define(['../../xterm'], linkify);
|
||||||
define(['../../xterm'], linkify);
|
} else {
|
||||||
}
|
/*
|
||||||
else {
|
* Plain browser environment
|
||||||
/*
|
*/
|
||||||
* Plain browser environment
|
linkify(window.Terminal);
|
||||||
*/
|
}
|
||||||
linkify(window.Terminal);
|
|
||||||
}
|
|
||||||
})(function (Xterm) {
|
})(function (Xterm) {
|
||||||
'use strict';
|
'use strict';
|
||||||
var exports = {}, protocolClause = '(https?:\\/\\/)', domainCharacterSet = '[\\da-z\\.-]+', negatedDomainCharacterSet = '[^\\da-z\\.-]+', domainBodyClause = '(' + domainCharacterSet + ')', tldClause = '([a-z\\.]{2,6})', ipClause = '((\\d{1,3}\\.){3}\\d{1,3})', portClause = '(:\\d{1,5})', hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|' + ipClause + ')' + portClause + '?', pathClause = '(\\/[\\/\\w\\.-]*)*', negatedPathCharacterSet = '[^\\/\\w\\.-]+', bodyClause = hostClause + pathClause, start = '(?:^|' + negatedDomainCharacterSet + ')(', end = ')($|' + negatedPathCharacterSet + ')', lenientUrlClause = start + protocolClause + '?' + bodyClause + end, strictUrlClause = start + protocolClause + bodyClause + end, lenientUrlRegex = new RegExp(lenientUrlClause), strictUrlRegex = new RegExp(strictUrlClause);
|
|
||||||
|
var exports = {},
|
||||||
|
protocolClause = '(https?:\\/\\/)',
|
||||||
|
domainCharacterSet = '[\\da-z\\.-]+',
|
||||||
|
negatedDomainCharacterSet = '[^\\da-z\\.-]+',
|
||||||
|
domainBodyClause = '(' + domainCharacterSet + ')',
|
||||||
|
tldClause = '([a-z\\.]{2,6})',
|
||||||
|
ipClause = '((\\d{1,3}\\.){3}\\d{1,3})',
|
||||||
|
portClause = '(:\\d{1,5})',
|
||||||
|
hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|' + ipClause + ')' + portClause + '?',
|
||||||
|
pathClause = '(\\/[\\/\\w\\.-]*)*',
|
||||||
|
negatedPathCharacterSet = '[^\\/\\w\\.-]+',
|
||||||
|
bodyClause = hostClause + pathClause,
|
||||||
|
start = '(?:^|' + negatedDomainCharacterSet + ')(',
|
||||||
|
end = ')($|' + negatedPathCharacterSet + ')',
|
||||||
|
lenientUrlClause = start + protocolClause + '?' + bodyClause + end,
|
||||||
|
strictUrlClause = start + protocolClause + bodyClause + end,
|
||||||
|
lenientUrlRegex = new RegExp(lenientUrlClause),
|
||||||
|
strictUrlRegex = new RegExp(strictUrlClause);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts all valid URLs found in the given terminal line into
|
||||||
|
* hyperlinks. The terminal line can be either the HTML element itself
|
||||||
|
* or the index of the termina line in the children of the terminal
|
||||||
|
* rows container.
|
||||||
|
*
|
||||||
|
* @param {Xterm} terminal - The terminal that owns the given line.
|
||||||
|
* @param {number|HTMLDivElement} line - The terminal line that should get
|
||||||
|
* "linkified".
|
||||||
|
* @param {boolean} lenient - The regex type that will be used to identify links. If lenient is
|
||||||
|
* false, the regex requires a protocol clause. Defaults to true.
|
||||||
|
* @param {string} target - Sets target="" attribute with value provided to links.
|
||||||
|
* Default doesn't set target attribute
|
||||||
|
* @emits linkify
|
||||||
|
* @emits linkify:line
|
||||||
|
*/
|
||||||
|
exports.linkifyTerminalLine = function (terminal, line, lenient, target) {
|
||||||
|
if (typeof line == 'number') {
|
||||||
|
line = terminal.rowContainer.children[line];
|
||||||
|
} else if (! (line instanceof HTMLDivElement)) {
|
||||||
|
var message = 'The "line" argument should be either a number';
|
||||||
|
message += ' or an HTMLDivElement';
|
||||||
|
|
||||||
|
throw new TypeError(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof target === 'undefined') {
|
||||||
|
target = '';
|
||||||
|
} else {
|
||||||
|
target = 'target="' + target + '"';
|
||||||
|
}
|
||||||
|
|
||||||
|
var buffer = document.createElement('span'),
|
||||||
|
nodes = line.childNodes;
|
||||||
|
|
||||||
|
for (var j=0; j<nodes.length; j++) {
|
||||||
|
var node = nodes[j],
|
||||||
|
match;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Since we cannot access the TextNode's HTML representation
|
||||||
|
* from the instance itself, we assign its data as textContent
|
||||||
|
* to a dummy buffer span, in order to retrieve the TextNode's
|
||||||
|
* HTML representation from the buffer's innerHTML.
|
||||||
|
*/
|
||||||
|
buffer.textContent = node.data;
|
||||||
|
|
||||||
|
var nodeHTML = buffer.innerHTML;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply function only on TextNodes
|
||||||
|
*/
|
||||||
|
if (node.nodeType != node.TEXT_NODE) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var url = exports.findLinkMatch(node.data, lenient);
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var startsWithProtocol = new RegExp('^' + protocolClause),
|
||||||
|
urlHasProtocol = url.match(startsWithProtocol),
|
||||||
|
href = (urlHasProtocol) ? url : 'http://' + url,
|
||||||
|
link = '<a href="' + href + '" ' + target + '>' + url + '</a>',
|
||||||
|
newHTML = nodeHTML.replace(url, link);
|
||||||
|
|
||||||
|
line.innerHTML = line.innerHTML.replace(nodeHTML, newHTML);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts all valid URLs found in the given terminal line into
|
* This event gets emitted when conversion of all URL susbtrings
|
||||||
* hyperlinks. The terminal line can be either the HTML element itself
|
* to HTML anchor elements (links) has finished, for a specific
|
||||||
* or the index of the termina line in the children of the terminal
|
* line of the current Xterm instance.
|
||||||
* rows container.
|
|
||||||
*
|
*
|
||||||
* @param {Xterm} terminal - The terminal that owns the given line.
|
* @event linkify:line
|
||||||
* @param {number|HTMLDivElement} line - The terminal line that should get
|
|
||||||
* "linkified".
|
|
||||||
* @param {boolean} lenient - The regex type that will be used to identify links. If lenient is
|
|
||||||
* false, the regex requires a protocol clause. Defaults to true.
|
|
||||||
* @param {string} target - Sets target="" attribute with value provided to links.
|
|
||||||
* Default doesn't set target attribute
|
|
||||||
* @emits linkify
|
|
||||||
* @emits linkify:line
|
|
||||||
*/
|
*/
|
||||||
exports.linkifyTerminalLine = function (terminal, line, lenient, target) {
|
terminal.emit('linkify:line', line);
|
||||||
if (typeof line == 'number') {
|
};
|
||||||
line = terminal.rowContainer.children[line];
|
|
||||||
}
|
/**
|
||||||
else if (!(line instanceof HTMLDivElement)) {
|
* Finds a link within a block of text.
|
||||||
var message = 'The "line" argument should be either a number';
|
*
|
||||||
message += ' or an HTMLDivElement';
|
* @param {string} text - The text to search .
|
||||||
throw new TypeError(message);
|
* @param {boolean} lenient - Whether to use the lenient search.
|
||||||
}
|
* @return {string} A URL.
|
||||||
if (typeof target === 'undefined') {
|
*/
|
||||||
target = '';
|
exports.findLinkMatch = function (text, lenient) {
|
||||||
}
|
var match = text.match(lenient ? lenientUrlRegex : strictUrlRegex);
|
||||||
else {
|
if (!match || match.length === 0) {
|
||||||
target = 'target="' + target + '"';
|
return null;
|
||||||
}
|
}
|
||||||
var buffer = document.createElement('span'), nodes = line.childNodes;
|
return match[1];
|
||||||
for (var j = 0; j < nodes.length; j++) {
|
}
|
||||||
var node = nodes[j], match;
|
|
||||||
/**
|
/**
|
||||||
* Since we cannot access the TextNode's HTML representation
|
* Converts all valid URLs found in the terminal view into hyperlinks.
|
||||||
* from the instance itself, we assign its data as textContent
|
*
|
||||||
* to a dummy buffer span, in order to retrieve the TextNode's
|
* @param {Xterm} terminal - The terminal that should get "linkified".
|
||||||
* HTML representation from the buffer's innerHTML.
|
* @param {boolean} lenient - The regex type that will be used to identify links. If lenient is
|
||||||
*/
|
* false, the regex requires a protocol clause. Defaults to true.
|
||||||
buffer.textContent = node.data;
|
* @param {string} target - Sets target="" attribute with value provided to links.
|
||||||
var nodeHTML = buffer.innerHTML;
|
* Default doesn't set target attribute
|
||||||
/**
|
* @emits linkify
|
||||||
* Apply function only on TextNodes
|
* @emits linkify:line
|
||||||
*/
|
*/
|
||||||
if (node.nodeType != node.TEXT_NODE) {
|
exports.linkify = function (terminal, lenient, target) {
|
||||||
continue;
|
var rows = terminal.rowContainer.children;
|
||||||
}
|
|
||||||
var url = exports.findLinkMatch(node.data, lenient);
|
lenient = (typeof lenient == "boolean") ? lenient : true;
|
||||||
if (!url) {
|
for (var i=0; i<rows.length; i++) {
|
||||||
continue;
|
var line = rows[i];
|
||||||
}
|
|
||||||
var startsWithProtocol = new RegExp('^' + protocolClause), urlHasProtocol = url.match(startsWithProtocol), href = (urlHasProtocol) ? url : 'http://' + url, link = '<a href="' + href + '" ' + target + '>' + url + '</a>', newHTML = nodeHTML.replace(url, link);
|
exports.linkifyTerminalLine(terminal, line, lenient, target);
|
||||||
line.innerHTML = line.innerHTML.replace(nodeHTML, newHTML);
|
}
|
||||||
}
|
|
||||||
/**
|
|
||||||
* This event gets emitted when conversion of all URL susbtrings
|
|
||||||
* to HTML anchor elements (links) has finished, for a specific
|
|
||||||
* line of the current Xterm instance.
|
|
||||||
*
|
|
||||||
* @event linkify:line
|
|
||||||
*/
|
|
||||||
terminal.emit('linkify:line', line);
|
|
||||||
};
|
|
||||||
/**
|
/**
|
||||||
* Finds a link within a block of text.
|
* This event gets emitted when conversion of all URL substrings to
|
||||||
|
* HTML anchor elements (links) has finished for the current Xterm
|
||||||
|
* instance's view.
|
||||||
*
|
*
|
||||||
* @param {string} text - The text to search .
|
* @event linkify
|
||||||
* @param {boolean} lenient - Whether to use the lenient search.
|
|
||||||
* @return {string} A URL.
|
|
||||||
*/
|
*/
|
||||||
exports.findLinkMatch = function (text, lenient) {
|
terminal.emit('linkify');
|
||||||
var match = text.match(lenient ? lenientUrlRegex : strictUrlRegex);
|
};
|
||||||
if (!match || match.length === 0) {
|
|
||||||
return null;
|
/**
|
||||||
}
|
* Extend Xterm prototype.
|
||||||
return match[1];
|
*/
|
||||||
};
|
|
||||||
/**
|
/**
|
||||||
* Converts all valid URLs found in the terminal view into hyperlinks.
|
* Converts all valid URLs found in the current terminal linte into
|
||||||
*
|
* hyperlinks.
|
||||||
* @param {Xterm} terminal - The terminal that should get "linkified".
|
*
|
||||||
* @param {boolean} lenient - The regex type that will be used to identify links. If lenient is
|
* @memberof Xterm
|
||||||
* false, the regex requires a protocol clause. Defaults to true.
|
* @param {number|HTMLDivElement} line - The terminal line that should get
|
||||||
* @param {string} target - Sets target="" attribute with value provided to links.
|
* "linkified".
|
||||||
* Default doesn't set target attribute
|
* @param {boolean} lenient - The regex type that will be used to identify links. If lenient is
|
||||||
* @emits linkify
|
* false, the regex requires a protocol clause. Defaults to true.
|
||||||
* @emits linkify:line
|
* @param {string} target - Sets target="" attribute with value provided to links.
|
||||||
*/
|
* Default doesn't set target attribute
|
||||||
exports.linkify = function (terminal, lenient, target) {
|
*/
|
||||||
var rows = terminal.rowContainer.children;
|
Xterm.prototype.linkifyTerminalLine = function (line, lenient, target) {
|
||||||
lenient = (typeof lenient == "boolean") ? lenient : true;
|
return exports.linkifyTerminalLine(this, line, lenient, target);
|
||||||
for (var i = 0; i < rows.length; i++) {
|
};
|
||||||
var line = rows[i];
|
|
||||||
exports.linkifyTerminalLine(terminal, line, lenient, target);
|
/**
|
||||||
}
|
* Converts all valid URLs found in the current terminal into hyperlinks.
|
||||||
/**
|
*
|
||||||
* This event gets emitted when conversion of all URL substrings to
|
* @memberof Xterm
|
||||||
* HTML anchor elements (links) has finished for the current Xterm
|
* @param {boolean} lenient - The regex type that will be used to identify links. If lenient is
|
||||||
* instance's view.
|
* false, the regex requires a protocol clause. Defaults to true.
|
||||||
*
|
* @param {string} target - Sets target="" attribute with value provided to links.
|
||||||
* @event linkify
|
* Default doesn't set target attribute
|
||||||
*/
|
*/
|
||||||
terminal.emit('linkify');
|
Xterm.prototype.linkify = function (lenient, target) {
|
||||||
};
|
return exports.linkify(this, lenient, target);
|
||||||
/**
|
};
|
||||||
* Extend Xterm prototype.
|
|
||||||
*/
|
return exports;
|
||||||
/**
|
|
||||||
* Converts all valid URLs found in the current terminal linte into
|
|
||||||
* hyperlinks.
|
|
||||||
*
|
|
||||||
* @memberof Xterm
|
|
||||||
* @param {number|HTMLDivElement} line - The terminal line that should get
|
|
||||||
* "linkified".
|
|
||||||
* @param {boolean} lenient - The regex type that will be used to identify links. If lenient is
|
|
||||||
* false, the regex requires a protocol clause. Defaults to true.
|
|
||||||
* @param {string} target - Sets target="" attribute with value provided to links.
|
|
||||||
* Default doesn't set target attribute
|
|
||||||
*/
|
|
||||||
Xterm.prototype.linkifyTerminalLine = function (line, lenient, target) {
|
|
||||||
return exports.linkifyTerminalLine(this, line, lenient, target);
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* Converts all valid URLs found in the current terminal into hyperlinks.
|
|
||||||
*
|
|
||||||
* @memberof Xterm
|
|
||||||
* @param {boolean} lenient - The regex type that will be used to identify links. If lenient is
|
|
||||||
* false, the regex requires a protocol clause. Defaults to true.
|
|
||||||
* @param {string} target - Sets target="" attribute with value provided to links.
|
|
||||||
* Default doesn't set target attribute
|
|
||||||
*/
|
|
||||||
Xterm.prototype.linkify = function (lenient, target) {
|
|
||||||
return exports.linkify(this, lenient, target);
|
|
||||||
};
|
|
||||||
return exports;
|
|
||||||
});
|
});
|
||||||
//# sourceMappingURL=linkify.js.map
|
|
||||||
Vendored
+122
-109
@@ -4,119 +4,132 @@
|
|||||||
* @module xterm/addons/terminado/terminado
|
* @module xterm/addons/terminado/terminado
|
||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
(function (attach) {
|
(function (attach) {
|
||||||
if (typeof exports === 'object' && typeof module === 'object') {
|
if (typeof exports === 'object' && typeof module === 'object') {
|
||||||
/*
|
/*
|
||||||
* CommonJS environment
|
* CommonJS environment
|
||||||
*/
|
*/
|
||||||
module.exports = attach(require('../../xterm'));
|
module.exports = attach(require('../../xterm'));
|
||||||
}
|
} else if (typeof define == 'function') {
|
||||||
else if (typeof define == 'function') {
|
/*
|
||||||
/*
|
* Require.js is available
|
||||||
* Require.js is available
|
*/
|
||||||
*/
|
define(['../../xterm'], attach);
|
||||||
define(['../../xterm'], attach);
|
} else {
|
||||||
}
|
/*
|
||||||
else {
|
* Plain browser environment
|
||||||
/*
|
*/
|
||||||
* Plain browser environment
|
attach(window.Terminal);
|
||||||
*/
|
}
|
||||||
attach(window.Terminal);
|
|
||||||
}
|
|
||||||
})(function (Xterm) {
|
})(function (Xterm) {
|
||||||
'use strict';
|
'use strict';
|
||||||
var exports = {};
|
|
||||||
/**
|
var exports = {};
|
||||||
* Attaches the given terminal to the given socket.
|
|
||||||
*
|
/**
|
||||||
* @param {Xterm} term - The terminal to be attached to the given socket.
|
* Attaches the given terminal to the given socket.
|
||||||
* @param {WebSocket} socket - The socket to attach the current terminal.
|
*
|
||||||
* @param {boolean} bidirectional - Whether the terminal should send data
|
* @param {Xterm} term - The terminal to be attached to the given socket.
|
||||||
* to the socket as well.
|
* @param {WebSocket} socket - The socket to attach the current terminal.
|
||||||
* @param {boolean} buffered - Whether the rendering of incoming data
|
* @param {boolean} bidirectional - Whether the terminal should send data
|
||||||
* should happen instantly or at a maximum
|
* to the socket as well.
|
||||||
* frequency of 1 rendering per 10ms.
|
* @param {boolean} buffered - Whether the rendering of incoming data
|
||||||
*/
|
* should happen instantly or at a maximum
|
||||||
exports.terminadoAttach = function (term, socket, bidirectional, buffered) {
|
* frequency of 1 rendering per 10ms.
|
||||||
bidirectional = (typeof bidirectional == 'undefined') ? true : bidirectional;
|
*/
|
||||||
term.socket = socket;
|
exports.terminadoAttach = function (term, socket, bidirectional, buffered) {
|
||||||
term._flushBuffer = function () {
|
bidirectional = (typeof bidirectional == 'undefined') ? true : bidirectional;
|
||||||
term.write(term._attachSocketBuffer);
|
term.socket = socket;
|
||||||
term._attachSocketBuffer = null;
|
|
||||||
clearTimeout(term._attachSocketBufferTimer);
|
term._flushBuffer = function () {
|
||||||
term._attachSocketBufferTimer = null;
|
term.write(term._attachSocketBuffer);
|
||||||
};
|
term._attachSocketBuffer = null;
|
||||||
term._pushToBuffer = function (data) {
|
clearTimeout(term._attachSocketBufferTimer);
|
||||||
if (term._attachSocketBuffer) {
|
term._attachSocketBufferTimer = null;
|
||||||
term._attachSocketBuffer += data;
|
};
|
||||||
}
|
|
||||||
else {
|
term._pushToBuffer = function (data) {
|
||||||
term._attachSocketBuffer = data;
|
if (term._attachSocketBuffer) {
|
||||||
setTimeout(term._flushBuffer, 10);
|
term._attachSocketBuffer += data;
|
||||||
}
|
} else {
|
||||||
};
|
term._attachSocketBuffer = data;
|
||||||
term._getMessage = function (ev) {
|
setTimeout(term._flushBuffer, 10);
|
||||||
var data = JSON.parse(ev.data);
|
}
|
||||||
if (data[0] == "stdout") {
|
};
|
||||||
if (buffered) {
|
|
||||||
term._pushToBuffer(data[1]);
|
term._getMessage = function (ev) {
|
||||||
}
|
var data = JSON.parse(ev.data)
|
||||||
else {
|
if( data[0] == "stdout" ) {
|
||||||
term.write(data[1]);
|
if (buffered) {
|
||||||
}
|
term._pushToBuffer(data[1]);
|
||||||
}
|
} else {
|
||||||
};
|
term.write(data[1]);
|
||||||
term._sendData = function (data) {
|
|
||||||
socket.send(JSON.stringify(['stdin', data]));
|
|
||||||
};
|
|
||||||
term._setSize = function (size) {
|
|
||||||
socket.send(JSON.stringify(['set_size', size.rows, size.cols]));
|
|
||||||
};
|
|
||||||
socket.addEventListener('message', term._getMessage);
|
|
||||||
if (bidirectional) {
|
|
||||||
term.on('data', term._sendData);
|
|
||||||
}
|
}
|
||||||
term.on('resize', term._setSize);
|
}
|
||||||
socket.addEventListener('close', term.terminadoDetach.bind(term, socket));
|
|
||||||
socket.addEventListener('error', term.terminadoDetach.bind(term, socket));
|
|
||||||
};
|
};
|
||||||
/**
|
|
||||||
* Detaches the given terminal from the given socket
|
term._sendData = function (data) {
|
||||||
*
|
socket.send(JSON.stringify(['stdin', data]));
|
||||||
* @param {Xterm} term - The terminal to be detached from the given socket.
|
|
||||||
* @param {WebSocket} socket - The socket from which to detach the current
|
|
||||||
* terminal.
|
|
||||||
*/
|
|
||||||
exports.terminadoDetach = function (term, socket) {
|
|
||||||
term.off('data', term._sendData);
|
|
||||||
socket = (typeof socket == 'undefined') ? term.socket : socket;
|
|
||||||
if (socket) {
|
|
||||||
socket.removeEventListener('message', term._getMessage);
|
|
||||||
}
|
|
||||||
delete term.socket;
|
|
||||||
};
|
};
|
||||||
/**
|
|
||||||
* Attaches the current terminal to the given socket
|
term._setSize = function (size) {
|
||||||
*
|
socket.send(JSON.stringify(['set_size', size.rows, size.cols]));
|
||||||
* @param {WebSocket} socket - The socket to attach the current terminal.
|
|
||||||
* @param {boolean} bidirectional - Whether the terminal should send data
|
|
||||||
* to the socket as well.
|
|
||||||
* @param {boolean} buffered - Whether the rendering of incoming data
|
|
||||||
* should happen instantly or at a maximum
|
|
||||||
* frequency of 1 rendering per 10ms.
|
|
||||||
*/
|
|
||||||
Xterm.prototype.terminadoAttach = function (socket, bidirectional, buffered) {
|
|
||||||
return exports.terminadoAttach(this, socket, bidirectional, buffered);
|
|
||||||
};
|
};
|
||||||
/**
|
|
||||||
* Detaches the current terminal from the given socket.
|
socket.addEventListener('message', term._getMessage);
|
||||||
*
|
|
||||||
* @param {WebSocket} socket - The socket from which to detach the current
|
if (bidirectional) {
|
||||||
* terminal.
|
term.on('data', term._sendData);
|
||||||
*/
|
}
|
||||||
Xterm.prototype.terminadoDetach = function (socket) {
|
term.on('resize', term._setSize);
|
||||||
return exports.terminadoDetach(this, socket);
|
|
||||||
};
|
socket.addEventListener('close', term.terminadoDetach.bind(term, socket));
|
||||||
return exports;
|
socket.addEventListener('error', term.terminadoDetach.bind(term, socket));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detaches the given terminal from the given socket
|
||||||
|
*
|
||||||
|
* @param {Xterm} term - The terminal to be detached from the given socket.
|
||||||
|
* @param {WebSocket} socket - The socket from which to detach the current
|
||||||
|
* terminal.
|
||||||
|
*/
|
||||||
|
exports.terminadoDetach = function (term, socket) {
|
||||||
|
term.off('data', term._sendData);
|
||||||
|
|
||||||
|
socket = (typeof socket == 'undefined') ? term.socket : socket;
|
||||||
|
|
||||||
|
if (socket) {
|
||||||
|
socket.removeEventListener('message', term._getMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete term.socket;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attaches the current terminal to the given socket
|
||||||
|
*
|
||||||
|
* @param {WebSocket} socket - The socket to attach the current terminal.
|
||||||
|
* @param {boolean} bidirectional - Whether the terminal should send data
|
||||||
|
* to the socket as well.
|
||||||
|
* @param {boolean} buffered - Whether the rendering of incoming data
|
||||||
|
* should happen instantly or at a maximum
|
||||||
|
* frequency of 1 rendering per 10ms.
|
||||||
|
*/
|
||||||
|
Xterm.prototype.terminadoAttach = function (socket, bidirectional, buffered) {
|
||||||
|
return exports.terminadoAttach(this, socket, bidirectional, buffered);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detaches the current terminal from the given socket.
|
||||||
|
*
|
||||||
|
* @param {WebSocket} socket - The socket from which to detach the current
|
||||||
|
* terminal.
|
||||||
|
*/
|
||||||
|
Xterm.prototype.terminadoDetach = function (socket) {
|
||||||
|
return exports.terminadoDetach(this, socket);
|
||||||
|
};
|
||||||
|
|
||||||
|
return exports;
|
||||||
});
|
});
|
||||||
//# sourceMappingURL=terminado.js.map
|
|
||||||
Vendored
+76
-12
@@ -41,6 +41,9 @@
|
|||||||
font-family: courier-new, courier, monospace;
|
font-family: courier-new, courier, monospace;
|
||||||
font-feature-settings: "liga" 0;
|
font-feature-settings: "liga" 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.terminal.focus,
|
.terminal.focus,
|
||||||
@@ -71,7 +74,22 @@
|
|||||||
resize: none;
|
resize: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.terminal .terminal-cursor {
|
.terminal a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal a:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal a.xterm-invalid-link:hover {
|
||||||
|
cursor: text;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal.focus:not(.xterm-cursor-style-underline):not(.xterm-cursor-style-bar) .terminal-cursor {
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
color: #000;
|
color: #000;
|
||||||
}
|
}
|
||||||
@@ -82,19 +100,41 @@
|
|||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.terminal.focus .terminal-cursor.blinking {
|
.terminal:not(.xterm-cursor-style-underline):not(.xterm-cursor-style-bar).focus.xterm-cursor-blink-on .terminal-cursor {
|
||||||
animation: blink-cursor 1.2s infinite step-end;
|
background-color: transparent;
|
||||||
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes blink-cursor {
|
.terminal.xterm-cursor-style-bar .terminal-cursor,
|
||||||
0% {
|
.terminal.xterm-cursor-style-underline .terminal-cursor {
|
||||||
background-color: #fff;
|
position: relative;
|
||||||
color: #000;
|
}
|
||||||
}
|
.terminal.xterm-cursor-style-bar .terminal-cursor::before,
|
||||||
50% {
|
.terminal.xterm-cursor-style-underline .terminal-cursor::before {
|
||||||
background-color: transparent;
|
content: "";
|
||||||
color: #FFF;
|
display: block;
|
||||||
}
|
position: absolute;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
.terminal.xterm-cursor-style-bar .terminal-cursor::before {
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 1px;
|
||||||
|
}
|
||||||
|
.terminal.xterm-cursor-style-underline .terminal-cursor::before {
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 1px;
|
||||||
|
}
|
||||||
|
.terminal.xterm-cursor-style-bar.focus.xterm-cursor-blink.xterm-cursor-blink-on .terminal-cursor::before,
|
||||||
|
.terminal.xterm-cursor-style-underline.focus.xterm-cursor-blink.xterm-cursor-blink-on .terminal-cursor::before {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
.terminal.xterm-cursor-style-bar.focus.xterm-cursor-blink .terminal-cursor::before,
|
||||||
|
.terminal.xterm-cursor-style-underline.focus.xterm-cursor-blink .terminal-cursor::before {
|
||||||
|
background-color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.terminal .composition-view {
|
.terminal .composition-view {
|
||||||
@@ -116,6 +156,11 @@
|
|||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.terminal .xterm-wide-char,
|
||||||
|
.terminal .xterm-normal-char {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
.terminal .xterm-rows {
|
.terminal .xterm-rows {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
@@ -138,6 +183,25 @@
|
|||||||
left: -9999em;
|
left: -9999em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.terminal.enable-mouse-events {
|
||||||
|
/* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal .xterm-selection {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 1;
|
||||||
|
opacity: 0.3;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal .xterm-selection div {
|
||||||
|
position: absolute;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Determine default colors for xterm.js
|
* Determine default colors for xterm.js
|
||||||
*/
|
*/
|
||||||
|
|||||||
Vendored
+4833
-5038
File diff suppressed because it is too large
Load Diff
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,9 @@
|
|||||||
|
version: '2'
|
||||||
|
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
build: ./
|
||||||
|
volumes:
|
||||||
|
- ./:/usr/src/app
|
||||||
|
ports:
|
||||||
|
- 3000:3000
|
||||||
+111
@@ -0,0 +1,111 @@
|
|||||||
|
const browserify = require('browserify');
|
||||||
|
const buffer = require('vinyl-buffer');
|
||||||
|
const coveralls = require('gulp-coveralls');
|
||||||
|
const fs = require('fs-extra');
|
||||||
|
const gulp = require('gulp');
|
||||||
|
const istanbul = require('gulp-istanbul');
|
||||||
|
const merge = require('merge-stream');
|
||||||
|
const mocha = require('gulp-mocha');
|
||||||
|
const sorcery = require('sorcery');
|
||||||
|
const source = require('vinyl-source-stream');
|
||||||
|
const sourcemaps = require('gulp-sourcemaps');
|
||||||
|
const ts = require('gulp-typescript');
|
||||||
|
|
||||||
|
|
||||||
|
let buildDir = process.env.BUILD_DIR || 'build';
|
||||||
|
let tsProject = ts.createProject('tsconfig.json');
|
||||||
|
let srcDir = tsProject.config.compilerOptions.rootDir;
|
||||||
|
let outDir = tsProject.config.compilerOptions.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}`);
|
||||||
|
|
||||||
|
// 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 = tsResult.js.pipe(sourcemaps.write('.', {includeContent: false, sourceRoot: ''})).pipe(gulp.dest(outDir));
|
||||||
|
|
||||||
|
// 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));
|
||||||
|
|
||||||
|
return merge(tsc, copyAddons, copyStylesheets);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bundle JavaScript files produced by the `tsc` task, into a single file named `xterm.js` with
|
||||||
|
* Browserify.
|
||||||
|
*/
|
||||||
|
gulp.task('browserify', ['tsc'], function() {
|
||||||
|
// Ensure that the build directory exists
|
||||||
|
fs.ensureDirSync(buildDir);
|
||||||
|
|
||||||
|
let browserifyOptions = {
|
||||||
|
basedir: buildDir,
|
||||||
|
debug: true,
|
||||||
|
entries: [`../${outDir}/xterm.js`],
|
||||||
|
standalone: 'Terminal',
|
||||||
|
cache: {},
|
||||||
|
packageCache: {}
|
||||||
|
};
|
||||||
|
let bundleStream = browserify(browserifyOptions)
|
||||||
|
.bundle()
|
||||||
|
.pipe(source('xterm.js'))
|
||||||
|
.pipe(buffer())
|
||||||
|
.pipe(sourcemaps.init({loadMaps: true, sourceRoot: '..'}))
|
||||||
|
.pipe(sourcemaps.write('./'))
|
||||||
|
.pipe(gulp.dest(buildDir));
|
||||||
|
|
||||||
|
// Copy all add-ons from ${outDir}/ to buildDir
|
||||||
|
let copyAddons = gulp.src(`${outDir}/addons/**/*`).pipe(gulp.dest(`${buildDir}/addons`));
|
||||||
|
|
||||||
|
// Copy stylesheets from ${outDir}/ to ${buildDir}/
|
||||||
|
let copyStylesheets = gulp.src(`${outDir}/**/*.css`).pipe(gulp.dest(buildDir));
|
||||||
|
|
||||||
|
return merge(bundleStream, copyAddons, copyStylesheets);
|
||||||
|
});
|
||||||
|
|
||||||
|
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`], {read: false})
|
||||||
|
.pipe(mocha())
|
||||||
|
.once('error', () => process.exit(1))
|
||||||
|
.pipe(istanbul.writeReports());
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use `sorcery` to resolve the source map chain and point back to the TypeScript files.
|
||||||
|
* (Without this task the source maps produced for the JavaScript bundle points into the
|
||||||
|
* compiled JavaScript files in ${outDir}/).
|
||||||
|
*/
|
||||||
|
gulp.task('sorcery', ['browserify'], function () {
|
||||||
|
var chain = sorcery.loadSync(`${buildDir}/xterm.js`);
|
||||||
|
chain.apply();
|
||||||
|
chain.writeSync();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submit coverage results to coveralls.io
|
||||||
|
*/
|
||||||
|
gulp.task('coveralls', function () {
|
||||||
|
gulp.src('coverage/**/lcov.info')
|
||||||
|
.pipe(coveralls());
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task('build', ['sorcery']);
|
||||||
|
gulp.task('test', ['mocha']);
|
||||||
|
gulp.task('default', ['build']);
|
||||||
+47
-11
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "xterm",
|
"name": "xterm",
|
||||||
"version": "2.2.2",
|
"description": "Full xterm terminal, in your browser",
|
||||||
|
"version": "2.8.0",
|
||||||
"ignore": [
|
"ignore": [
|
||||||
"demo",
|
"demo",
|
||||||
"test",
|
"test",
|
||||||
@@ -10,34 +11,69 @@
|
|||||||
"repository": "https://github.com/sourcelair/xterm.js",
|
"repository": "https://github.com/sourcelair/xterm.js",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"files": [
|
"files": [
|
||||||
"lib"
|
"*.js",
|
||||||
|
"*.json",
|
||||||
|
"dist/*.css",
|
||||||
|
"dist/**/*.css",
|
||||||
|
"dist/*.js",
|
||||||
|
"dist/*.js.map",
|
||||||
|
"dist/**/*.js",
|
||||||
|
"dist/**/*.js.map",
|
||||||
|
"lib/*.css",
|
||||||
|
"lib/**/*.css",
|
||||||
|
"lib/*.js",
|
||||||
|
"lib/*.js.map",
|
||||||
|
"lib/**/*.js",
|
||||||
|
"lib/**/*.js.map",
|
||||||
|
"src/*.css",
|
||||||
|
"src/**/*.css",
|
||||||
|
"src/*.js",
|
||||||
|
"src/*.js.map",
|
||||||
|
"src/*.ts",
|
||||||
|
"src/**/*.js",
|
||||||
|
"src/**/*.js.map",
|
||||||
|
"src/**/*.ts"
|
||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/chai": "^3.4.34",
|
"@types/chai": "^3.4.34",
|
||||||
|
"@types/jsdom": "^2.0.30",
|
||||||
"@types/mocha": "^2.2.33",
|
"@types/mocha": "^2.2.33",
|
||||||
"@types/node": "^6.0.41",
|
"@types/node": "^6.0.41",
|
||||||
"browserify": "^13.1.0",
|
"browserify": "^13.1.0",
|
||||||
"chai": "3.5.0",
|
"chai": "3.5.0",
|
||||||
"docdash": "0.4.0",
|
"docdash": "0.4.0",
|
||||||
"exorcist": "^0.4.0",
|
|
||||||
"express": "4.13.4",
|
"express": "4.13.4",
|
||||||
"express-ws": "2.0.0-rc.1",
|
"express-ws": "2.0.0-rc.1",
|
||||||
|
"fs-extra": "^1.0.0",
|
||||||
"glob": "^7.0.5",
|
"glob": "^7.0.5",
|
||||||
|
"gulp": "^3.9.1",
|
||||||
|
"gulp-cli": "^1.2.2",
|
||||||
|
"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",
|
||||||
"jsdoc": "3.4.3",
|
"jsdoc": "3.4.3",
|
||||||
"mocha": "2.5.3",
|
"jsdom": "^9.11.0",
|
||||||
|
"merge-stream": "^1.0.1",
|
||||||
|
"node-pty": "^0.4.1",
|
||||||
"nodemon": "1.10.2",
|
"nodemon": "1.10.2",
|
||||||
"pty.js": "0.3.1",
|
|
||||||
"sleep": "^3.0.1",
|
"sleep": "^3.0.1",
|
||||||
"sorcery": "^0.10.0",
|
"sorcery": "^0.10.0",
|
||||||
"tslint": "^4.0.2",
|
"tslint": "^4.0.2",
|
||||||
"typescript": "^2.0.3"
|
"typescript": "~2.2.0",
|
||||||
|
"vinyl-buffer": "^1.0.0",
|
||||||
|
"vinyl-source-stream": "^1.1.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "nodemon --watch src --watch addons --watch demo --exec bash -c './bin/build && node demo/app'",
|
"prestart": "npm run build",
|
||||||
"lint": "tslint src/**/*.ts",
|
"start": "node demo/app",
|
||||||
"test": "mocha --recursive ./lib",
|
"dev": "nodemon -e js,ts,css --watch src --watch demo --exec npm start",
|
||||||
|
"lint": "tslint src/*.ts src/**/*.ts",
|
||||||
|
"test": "gulp test",
|
||||||
"build:docs": "jsdoc -c jsdoc.json",
|
"build:docs": "jsdoc -c jsdoc.json",
|
||||||
"build": "./bin/build",
|
"build": "gulp build",
|
||||||
"prepublish": "tsc"
|
"prepublish": "npm run build",
|
||||||
|
"coveralls": "gulp coveralls"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+252
@@ -0,0 +1,252 @@
|
|||||||
|
/**
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The character sets supported by the terminal. These enable several languages
|
||||||
|
* to be represented within the terminal with only 8-bit encoding. See ISO 2022
|
||||||
|
* for a discussion on character sets. Only VT100 character sets are supported.
|
||||||
|
*/
|
||||||
|
export const CHARSETS: {[key: string]: {[key: string]: string}} = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default character set, US.
|
||||||
|
*/
|
||||||
|
export const DEFAULT_CHARSET = CHARSETS['B'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DEC Special Character and Line Drawing Set.
|
||||||
|
* Reference: http://vt100.net/docs/vt102-ug/table5-13.html
|
||||||
|
* A lot of curses apps use this if they see TERM=xterm.
|
||||||
|
* testing: echo -e '\e(0a\e(B'
|
||||||
|
* The xterm output sometimes seems to conflict with the
|
||||||
|
* reference above. xterm seems in line with the reference
|
||||||
|
* when running vttest however.
|
||||||
|
* The table below now uses xterm's output from vttest.
|
||||||
|
*/
|
||||||
|
CHARSETS['0'] = {
|
||||||
|
'`': '\u25c6', // '◆'
|
||||||
|
'a': '\u2592', // '▒'
|
||||||
|
'b': '\u0009', // '\t'
|
||||||
|
'c': '\u000c', // '\f'
|
||||||
|
'd': '\u000d', // '\r'
|
||||||
|
'e': '\u000a', // '\n'
|
||||||
|
'f': '\u00b0', // '°'
|
||||||
|
'g': '\u00b1', // '±'
|
||||||
|
'h': '\u2424', // '\u2424' (NL)
|
||||||
|
'i': '\u000b', // '\v'
|
||||||
|
'j': '\u2518', // '┘'
|
||||||
|
'k': '\u2510', // '┐'
|
||||||
|
'l': '\u250c', // '┌'
|
||||||
|
'm': '\u2514', // '└'
|
||||||
|
'n': '\u253c', // '┼'
|
||||||
|
'o': '\u23ba', // '⎺'
|
||||||
|
'p': '\u23bb', // '⎻'
|
||||||
|
'q': '\u2500', // '─'
|
||||||
|
'r': '\u23bc', // '⎼'
|
||||||
|
's': '\u23bd', // '⎽'
|
||||||
|
't': '\u251c', // '├'
|
||||||
|
'u': '\u2524', // '┤'
|
||||||
|
'v': '\u2534', // '┴'
|
||||||
|
'w': '\u252c', // '┬'
|
||||||
|
'x': '\u2502', // '│'
|
||||||
|
'y': '\u2264', // '≤'
|
||||||
|
'z': '\u2265', // '≥'
|
||||||
|
'{': '\u03c0', // 'π'
|
||||||
|
'|': '\u2260', // '≠'
|
||||||
|
'}': '\u00a3', // '£'
|
||||||
|
'~': '\u00b7' // '·'
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* British character set
|
||||||
|
* ESC (A
|
||||||
|
* Reference: http://vt100.net/docs/vt220-rm/table2-5.html
|
||||||
|
*/
|
||||||
|
CHARSETS['A'] = {
|
||||||
|
'#': '£'
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* United States character set
|
||||||
|
* ESC (B
|
||||||
|
*/
|
||||||
|
CHARSETS['B'] = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dutch character set
|
||||||
|
* ESC (4
|
||||||
|
* Reference: http://vt100.net/docs/vt220-rm/table2-6.html
|
||||||
|
*/
|
||||||
|
CHARSETS['4'] = {
|
||||||
|
'#': '£',
|
||||||
|
'@': '¾',
|
||||||
|
'[': 'ij',
|
||||||
|
'\\': '½',
|
||||||
|
']': '|',
|
||||||
|
'{': '¨',
|
||||||
|
'|': 'f',
|
||||||
|
'}': '¼',
|
||||||
|
'~': '´'
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finnish character set
|
||||||
|
* ESC (C or ESC (5
|
||||||
|
* Reference: http://vt100.net/docs/vt220-rm/table2-7.html
|
||||||
|
*/
|
||||||
|
CHARSETS['C'] =
|
||||||
|
CHARSETS['5'] = {
|
||||||
|
'[': 'Ä',
|
||||||
|
'\\': 'Ö',
|
||||||
|
']': 'Å',
|
||||||
|
'^': 'Ü',
|
||||||
|
'`': 'é',
|
||||||
|
'{': 'ä',
|
||||||
|
'|': 'ö',
|
||||||
|
'}': 'å',
|
||||||
|
'~': 'ü'
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* French character set
|
||||||
|
* ESC (R
|
||||||
|
* Reference: http://vt100.net/docs/vt220-rm/table2-8.html
|
||||||
|
*/
|
||||||
|
CHARSETS['R'] = {
|
||||||
|
'#': '£',
|
||||||
|
'@': 'à',
|
||||||
|
'[': '°',
|
||||||
|
'\\': 'ç',
|
||||||
|
']': '§',
|
||||||
|
'{': 'é',
|
||||||
|
'|': 'ù',
|
||||||
|
'}': 'è',
|
||||||
|
'~': '¨'
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* French Canadian character set
|
||||||
|
* ESC (Q
|
||||||
|
* Reference: http://vt100.net/docs/vt220-rm/table2-9.html
|
||||||
|
*/
|
||||||
|
CHARSETS['Q'] = {
|
||||||
|
'@': 'à',
|
||||||
|
'[': 'â',
|
||||||
|
'\\': 'ç',
|
||||||
|
']': 'ê',
|
||||||
|
'^': 'î',
|
||||||
|
'`': 'ô',
|
||||||
|
'{': 'é',
|
||||||
|
'|': 'ù',
|
||||||
|
'}': 'è',
|
||||||
|
'~': 'û'
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* German character set
|
||||||
|
* ESC (K
|
||||||
|
* Reference: http://vt100.net/docs/vt220-rm/table2-10.html
|
||||||
|
*/
|
||||||
|
CHARSETS['K'] = {
|
||||||
|
'@': '§',
|
||||||
|
'[': 'Ä',
|
||||||
|
'\\': 'Ö',
|
||||||
|
']': 'Ü',
|
||||||
|
'{': 'ä',
|
||||||
|
'|': 'ö',
|
||||||
|
'}': 'ü',
|
||||||
|
'~': 'ß'
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Italian character set
|
||||||
|
* ESC (Y
|
||||||
|
* Reference: http://vt100.net/docs/vt220-rm/table2-11.html
|
||||||
|
*/
|
||||||
|
CHARSETS['Y'] = {
|
||||||
|
'#': '£',
|
||||||
|
'@': '§',
|
||||||
|
'[': '°',
|
||||||
|
'\\': 'ç',
|
||||||
|
']': 'é',
|
||||||
|
'`': 'ù',
|
||||||
|
'{': 'à',
|
||||||
|
'|': 'ò',
|
||||||
|
'}': 'è',
|
||||||
|
'~': 'ì'
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Norwegian/Danish character set
|
||||||
|
* ESC (E or ESC (6
|
||||||
|
* Reference: http://vt100.net/docs/vt220-rm/table2-12.html
|
||||||
|
*/
|
||||||
|
CHARSETS['E'] =
|
||||||
|
CHARSETS['6'] = {
|
||||||
|
'@': 'Ä',
|
||||||
|
'[': 'Æ',
|
||||||
|
'\\': 'Ø',
|
||||||
|
']': 'Å',
|
||||||
|
'^': 'Ü',
|
||||||
|
'`': 'ä',
|
||||||
|
'{': 'æ',
|
||||||
|
'|': 'ø',
|
||||||
|
'}': 'å',
|
||||||
|
'~': 'ü'
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spanish character set
|
||||||
|
* ESC (Z
|
||||||
|
* Reference: http://vt100.net/docs/vt220-rm/table2-13.html
|
||||||
|
*/
|
||||||
|
CHARSETS['Z'] = {
|
||||||
|
'#': '£',
|
||||||
|
'@': '§',
|
||||||
|
'[': '¡',
|
||||||
|
'\\': 'Ñ',
|
||||||
|
']': '¿',
|
||||||
|
'{': '°',
|
||||||
|
'|': 'ñ',
|
||||||
|
'}': 'ç'
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Swedish character set
|
||||||
|
* ESC (H or ESC (7
|
||||||
|
* Reference: http://vt100.net/docs/vt220-rm/table2-14.html
|
||||||
|
*/
|
||||||
|
CHARSETS['H'] =
|
||||||
|
CHARSETS['7'] = {
|
||||||
|
'@': 'É',
|
||||||
|
'[': 'Ä',
|
||||||
|
'\\': 'Ö',
|
||||||
|
']': 'Å',
|
||||||
|
'^': 'Ü',
|
||||||
|
'`': 'é',
|
||||||
|
'{': 'ä',
|
||||||
|
'|': 'ö',
|
||||||
|
'}': 'å',
|
||||||
|
'~': 'ü'
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Swiss character set
|
||||||
|
* ESC (=
|
||||||
|
* Reference: http://vt100.net/docs/vt220-rm/table2-15.html
|
||||||
|
*/
|
||||||
|
CHARSETS['='] = {
|
||||||
|
'#': 'ù',
|
||||||
|
'@': 'à',
|
||||||
|
'[': 'é',
|
||||||
|
'\\': 'ç',
|
||||||
|
']': 'ê',
|
||||||
|
'^': 'î',
|
||||||
|
'_': 'è',
|
||||||
|
'`': 'ô',
|
||||||
|
'{': 'ä',
|
||||||
|
'|': 'ö',
|
||||||
|
'}': 'ü',
|
||||||
|
'~': 'û'
|
||||||
|
};
|
||||||
@@ -2,11 +2,11 @@ import { assert } from 'chai';
|
|||||||
import { CompositionHelper } from './CompositionHelper';
|
import { CompositionHelper } from './CompositionHelper';
|
||||||
|
|
||||||
describe('CompositionHelper', () => {
|
describe('CompositionHelper', () => {
|
||||||
var terminal;
|
let terminal;
|
||||||
var compositionHelper;
|
let compositionHelper;
|
||||||
var compositionView;
|
let compositionView;
|
||||||
var textarea;
|
let textarea;
|
||||||
var handledText;
|
let handledText;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
compositionView = {
|
compositionView = {
|
||||||
@@ -125,7 +125,7 @@ describe('CompositionHelper', () => {
|
|||||||
compositionHelper.compositionend();
|
compositionHelper.compositionend();
|
||||||
compositionHelper.compositionstart();
|
compositionHelper.compositionstart();
|
||||||
compositionHelper.compositionupdate({ data: '아' });
|
compositionHelper.compositionupdate({ data: '아' });
|
||||||
textarea.value = '아아'
|
textarea.value = '아아';
|
||||||
setTimeout(() => { // wait for any textarea updates
|
setTimeout(() => { // wait for any textarea updates
|
||||||
compositionHelper.compositionend();
|
compositionHelper.compositionend();
|
||||||
setTimeout(() => { // wait for any textarea updates
|
setTimeout(() => { // wait for any textarea updates
|
||||||
@@ -187,7 +187,7 @@ describe('CompositionHelper', () => {
|
|||||||
}, 0);
|
}, 0);
|
||||||
}, 0);
|
}, 0);
|
||||||
}, 0);
|
}, 0);
|
||||||
})
|
});
|
||||||
|
|
||||||
it('Should insert multi-character compositions that are converted to other characters with different lengths', function (done) {
|
it('Should insert multi-character compositions that are converted to other characters with different lengths', function (done) {
|
||||||
// First character 'い'
|
// First character 'い'
|
||||||
|
|||||||
+25
-28
@@ -24,7 +24,7 @@ export class CompositionHelper {
|
|||||||
/**
|
/**
|
||||||
* The position within the input textarea's value of the current composition.
|
* The position within the input textarea's value of the current composition.
|
||||||
*/
|
*/
|
||||||
private compositionPosition: IPosition;;
|
private compositionPosition: IPosition;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether a composition is in the process of being sent, setting this to false will cancel any
|
* Whether a composition is in the process of being sent, setting this to false will cancel any
|
||||||
@@ -65,9 +65,8 @@ export class CompositionHelper {
|
|||||||
public compositionupdate(ev: CompositionEvent) {
|
public compositionupdate(ev: CompositionEvent) {
|
||||||
this.compositionView.textContent = ev.data;
|
this.compositionView.textContent = ev.data;
|
||||||
this.updateCompositionElements();
|
this.updateCompositionElements();
|
||||||
var self = this;
|
setTimeout(() => {
|
||||||
setTimeout(function() {
|
this.compositionPosition.end = this.textarea.value.length;
|
||||||
self.compositionPosition.end = self.textarea.value.length;
|
|
||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,15 +124,15 @@ export class CompositionHelper {
|
|||||||
if (!waitForPropogation) {
|
if (!waitForPropogation) {
|
||||||
// Cancel any delayed composition send requests and send the input immediately.
|
// Cancel any delayed composition send requests and send the input immediately.
|
||||||
this.isSendingComposition = false;
|
this.isSendingComposition = false;
|
||||||
var input = this.textarea.value.substring(this.compositionPosition.start, this.compositionPosition.end);
|
const input = this.textarea.value.substring(this.compositionPosition.start, this.compositionPosition.end);
|
||||||
this.terminal.handler(input);
|
this.terminal.handler(input);
|
||||||
} else {
|
} else {
|
||||||
// Make a deep copy of the composition position here as a new compositionstart event may
|
// Make a deep copy of the composition position here as a new compositionstart event may
|
||||||
// fire before the setTimeout executes.
|
// fire before the setTimeout executes.
|
||||||
var currentCompositionPosition = {
|
const currentCompositionPosition = {
|
||||||
start: this.compositionPosition.start,
|
start: this.compositionPosition.start,
|
||||||
end: this.compositionPosition.end,
|
end: this.compositionPosition.end,
|
||||||
}
|
};
|
||||||
|
|
||||||
// Since composition* events happen before the changes take place in the textarea on most
|
// Since composition* events happen before the changes take place in the textarea on most
|
||||||
// browsers, use a setTimeout with 0ms time to allow the native compositionend event to
|
// browsers, use a setTimeout with 0ms time to allow the native compositionend event to
|
||||||
@@ -143,23 +142,22 @@ export class CompositionHelper {
|
|||||||
// - The last compositionupdate event's data property does not always accurately describe
|
// - The last compositionupdate event's data property does not always accurately describe
|
||||||
// the character, a counter example being Korean where an ending consonsant can move to
|
// the character, a counter example being Korean where an ending consonsant can move to
|
||||||
// the following character if the following input is a vowel.
|
// the following character if the following input is a vowel.
|
||||||
var self = this;
|
|
||||||
this.isSendingComposition = true;
|
this.isSendingComposition = true;
|
||||||
setTimeout(function () {
|
setTimeout(() => {
|
||||||
// Ensure that the input has not already been sent
|
// Ensure that the input has not already been sent
|
||||||
if (self.isSendingComposition) {
|
if (this.isSendingComposition) {
|
||||||
self.isSendingComposition = false;
|
this.isSendingComposition = false;
|
||||||
var input;
|
let input;
|
||||||
if (self.isComposing) {
|
if (this.isComposing) {
|
||||||
// Use the end position to get the string if a new composition has started.
|
// Use the end position to get the string if a new composition has started.
|
||||||
input = self.textarea.value.substring(currentCompositionPosition.start, currentCompositionPosition.end);
|
input = this.textarea.value.substring(currentCompositionPosition.start, currentCompositionPosition.end);
|
||||||
} else {
|
} else {
|
||||||
// Don't use the end position here in order to pick up any characters after the
|
// Don't use the end position here in order to pick up any characters after the
|
||||||
// composition has finished, for example when typing a non-composition character
|
// composition has finished, for example when typing a non-composition character
|
||||||
// (eg. 2) after a composition character.
|
// (eg. 2) after a composition character.
|
||||||
input = self.textarea.value.substring(currentCompositionPosition.start);
|
input = this.textarea.value.substring(currentCompositionPosition.start);
|
||||||
}
|
}
|
||||||
self.terminal.handler(input);
|
this.terminal.handler(input);
|
||||||
}
|
}
|
||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
@@ -172,15 +170,14 @@ export class CompositionHelper {
|
|||||||
* IME is active.
|
* IME is active.
|
||||||
*/
|
*/
|
||||||
private handleAnyTextareaChanges() {
|
private handleAnyTextareaChanges() {
|
||||||
var oldValue = this.textarea.value;
|
const oldValue = this.textarea.value;
|
||||||
var self = this;
|
setTimeout(() => {
|
||||||
setTimeout(function() {
|
|
||||||
// Ignore if a composition has started since the timeout
|
// Ignore if a composition has started since the timeout
|
||||||
if (!self.isComposing) {
|
if (!this.isComposing) {
|
||||||
var newValue = self.textarea.value;
|
const newValue = this.textarea.value;
|
||||||
var diff = newValue.replace(oldValue, '');
|
const diff = newValue.replace(oldValue, '');
|
||||||
if (diff.length > 0) {
|
if (diff.length > 0) {
|
||||||
self.terminal.handler(diff);
|
this.terminal.handler(diff);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 0);
|
}, 0);
|
||||||
@@ -196,12 +193,12 @@ export class CompositionHelper {
|
|||||||
if (!this.isComposing) {
|
if (!this.isComposing) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var cursor = <HTMLElement>this.terminal.element.querySelector('.terminal-cursor');
|
const cursor = <HTMLElement>this.terminal.element.querySelector('.terminal-cursor');
|
||||||
if (cursor) {
|
if (cursor) {
|
||||||
// Take .xterm-rows offsetTop into account as well in case it's positioned absolutely within
|
// Take .xterm-rows offsetTop into account as well in case it's positioned absolutely within
|
||||||
// the .xterm element.
|
// the .xterm element.
|
||||||
var xtermRows = <HTMLElement>this.terminal.element.querySelector('.xterm-rows');
|
const xtermRows = <HTMLElement>this.terminal.element.querySelector('.xterm-rows');
|
||||||
var cursorTop = xtermRows.offsetTop + cursor.offsetTop;
|
const cursorTop = xtermRows.offsetTop + cursor.offsetTop;
|
||||||
|
|
||||||
this.compositionView.style.left = cursor.offsetLeft + 'px';
|
this.compositionView.style.left = cursor.offsetLeft + 'px';
|
||||||
this.compositionView.style.top = cursorTop + 'px';
|
this.compositionView.style.top = cursorTop + 'px';
|
||||||
@@ -209,7 +206,7 @@ export class CompositionHelper {
|
|||||||
this.compositionView.style.lineHeight = cursor.offsetHeight + 'px';
|
this.compositionView.style.lineHeight = cursor.offsetHeight + 'px';
|
||||||
// Sync the textarea to the exact position of the composition view so the IME knows where the
|
// Sync the textarea to the exact position of the composition view so the IME knows where the
|
||||||
// text is.
|
// text is.
|
||||||
var compositionViewBounds = this.compositionView.getBoundingClientRect();
|
const compositionViewBounds = this.compositionView.getBoundingClientRect();
|
||||||
this.textarea.style.left = cursor.offsetLeft + 'px';
|
this.textarea.style.left = cursor.offsetLeft + 'px';
|
||||||
this.textarea.style.top = cursorTop + 'px';
|
this.textarea.style.top = cursorTop + 'px';
|
||||||
this.textarea.style.width = compositionViewBounds.width + 'px';
|
this.textarea.style.width = compositionViewBounds.width + 'px';
|
||||||
@@ -217,7 +214,7 @@ export class CompositionHelper {
|
|||||||
this.textarea.style.lineHeight = compositionViewBounds.height + 'px';
|
this.textarea.style.lineHeight = compositionViewBounds.height + 'px';
|
||||||
}
|
}
|
||||||
if (!dontRecurse) {
|
if (!dontRecurse) {
|
||||||
setTimeout(this.updateCompositionElements.bind(this, true), 0);
|
setTimeout(() => this.updateCompositionElements(true), 0);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
/**
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* C0 control codes
|
||||||
|
* See = https://en.wikipedia.org/wiki/C0_and_C1_control_codes
|
||||||
|
*/
|
||||||
|
export namespace C0 {
|
||||||
|
/** Null (Caret = ^@, C = \0) */
|
||||||
|
export const NUL = '\x00';
|
||||||
|
/** Start of Heading (Caret = ^A) */
|
||||||
|
export const SOH = '\x01';
|
||||||
|
/** Start of Text (Caret = ^B) */
|
||||||
|
export const STX = '\x02';
|
||||||
|
/** End of Text (Caret = ^C) */
|
||||||
|
export const ETX = '\x03';
|
||||||
|
/** End of Transmission (Caret = ^D) */
|
||||||
|
export const EOT = '\x04';
|
||||||
|
/** Enquiry (Caret = ^E) */
|
||||||
|
export const ENQ = '\x05';
|
||||||
|
/** Acknowledge (Caret = ^F) */
|
||||||
|
export const ACK = '\x06';
|
||||||
|
/** Bell (Caret = ^G, C = \a) */
|
||||||
|
export const BEL = '\x07';
|
||||||
|
/** Backspace (Caret = ^H, C = \b) */
|
||||||
|
export const BS = '\x08';
|
||||||
|
/** Character Tabulation, Horizontal Tabulation (Caret = ^I, C = \t) */
|
||||||
|
export const HT = '\x09';
|
||||||
|
/** Line Feed (Caret = ^J, C = \n) */
|
||||||
|
export const LF = '\x0a';
|
||||||
|
/** Line Tabulation, Vertical Tabulation (Caret = ^K, C = \v) */
|
||||||
|
export const VT = '\x0b';
|
||||||
|
/** Form Feed (Caret = ^L, C = \f) */
|
||||||
|
export const FF = '\x0c';
|
||||||
|
/** Carriage Return (Caret = ^M, C = \r) */
|
||||||
|
export const CR = '\x0d';
|
||||||
|
/** Shift Out (Caret = ^N) */
|
||||||
|
export const SO = '\x0e';
|
||||||
|
/** Shift In (Caret = ^O) */
|
||||||
|
export const SI = '\x0f';
|
||||||
|
/** Data Link Escape (Caret = ^P) */
|
||||||
|
export const DLE = '\x10';
|
||||||
|
/** Device Control One (XON) (Caret = ^Q) */
|
||||||
|
export const DC1 = '\x11';
|
||||||
|
/** Device Control Two (Caret = ^R) */
|
||||||
|
export const DC2 = '\x12';
|
||||||
|
/** Device Control Three (XOFF) (Caret = ^S) */
|
||||||
|
export const DC3 = '\x13';
|
||||||
|
/** Device Control Four (Caret = ^T) */
|
||||||
|
export const DC4 = '\x14';
|
||||||
|
/** Negative Acknowledge (Caret = ^U) */
|
||||||
|
export const NAK = '\x15';
|
||||||
|
/** Synchronous Idle (Caret = ^V) */
|
||||||
|
export const SYN = '\x16';
|
||||||
|
/** End of Transmission Block (Caret = ^W) */
|
||||||
|
export const ETB = '\x17';
|
||||||
|
/** Cancel (Caret = ^X) */
|
||||||
|
export const CAN = '\x18';
|
||||||
|
/** End of Medium (Caret = ^Y) */
|
||||||
|
export const EM = '\x19';
|
||||||
|
/** Substitute (Caret = ^Z) */
|
||||||
|
export const SUB = '\x1a';
|
||||||
|
/** Escape (Caret = ^[, C = \e) */
|
||||||
|
export const ESC = '\x1b';
|
||||||
|
/** File Separator (Caret = ^\) */
|
||||||
|
export const FS = '\x1c';
|
||||||
|
/** Group Separator (Caret = ^]) */
|
||||||
|
export const GS = '\x1d';
|
||||||
|
/** Record Separator (Caret = ^^) */
|
||||||
|
export const RS = '\x1e';
|
||||||
|
/** Unit Separator (Caret = ^_) */
|
||||||
|
export const US = '\x1f';
|
||||||
|
/** Space */
|
||||||
|
export const SP = '\x20';
|
||||||
|
/** Delete (Caret = ^?) */
|
||||||
|
export const DEL = '\x7f';
|
||||||
|
};
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
/**
|
|
||||||
* @license MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
function EventEmitter() {
|
|
||||||
this._events = this._events || {};
|
|
||||||
}
|
|
||||||
|
|
||||||
EventEmitter.prototype.addListener = function(type, listener) {
|
|
||||||
this._events[type] = this._events[type] || [];
|
|
||||||
this._events[type].push(listener);
|
|
||||||
};
|
|
||||||
|
|
||||||
EventEmitter.prototype.on = EventEmitter.prototype.addListener;
|
|
||||||
|
|
||||||
EventEmitter.prototype.removeListener = function(type, listener) {
|
|
||||||
if (!this._events[type]) return;
|
|
||||||
|
|
||||||
var obj = this._events[type]
|
|
||||||
, i = obj.length;
|
|
||||||
|
|
||||||
while (i--) {
|
|
||||||
if (obj[i] === listener || obj[i].listener === listener) {
|
|
||||||
obj.splice(i, 1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
EventEmitter.prototype.off = EventEmitter.prototype.removeListener;
|
|
||||||
|
|
||||||
EventEmitter.prototype.removeAllListeners = function(type) {
|
|
||||||
if (this._events[type]) delete this._events[type];
|
|
||||||
};
|
|
||||||
|
|
||||||
EventEmitter.prototype.once = function(type, listener) {
|
|
||||||
var self = this;
|
|
||||||
function on() {
|
|
||||||
var args = Array.prototype.slice.call(arguments);
|
|
||||||
this.removeListener(type, on);
|
|
||||||
return listener.apply(this, args);
|
|
||||||
}
|
|
||||||
on.listener = listener;
|
|
||||||
return this.on(type, on);
|
|
||||||
};
|
|
||||||
|
|
||||||
EventEmitter.prototype.emit = function(type) {
|
|
||||||
if (!this._events[type]) return;
|
|
||||||
|
|
||||||
var args = Array.prototype.slice.call(arguments, 1)
|
|
||||||
, obj = this._events[type]
|
|
||||||
, l = obj.length
|
|
||||||
, i = 0;
|
|
||||||
|
|
||||||
for (; i < l; i++) {
|
|
||||||
obj[i].apply(this, args);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
EventEmitter.prototype.listeners = function(type) {
|
|
||||||
return this._events[type] = this._events[type] || [];
|
|
||||||
};
|
|
||||||
|
|
||||||
export { EventEmitter };
|
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import { assert } from 'chai';
|
||||||
|
import { EventEmitter } from './EventEmitter';
|
||||||
|
|
||||||
|
describe('EventEmitter', () => {
|
||||||
|
let eventEmitter: EventEmitter;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
eventEmitter = new EventEmitter();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('once', () => {
|
||||||
|
it('should trigger the listener only once', () => {
|
||||||
|
let count = 0;
|
||||||
|
const listener = () => count++;
|
||||||
|
eventEmitter.once('test', listener);
|
||||||
|
eventEmitter.emit('test');
|
||||||
|
assert.equal(count, 1);
|
||||||
|
eventEmitter.emit('test');
|
||||||
|
assert.equal(count, 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('emit', () => {
|
||||||
|
it('should emit events to listeners', () => {
|
||||||
|
let count1 = 0;
|
||||||
|
let count2 = 0;
|
||||||
|
const listener1 = () => count1++;
|
||||||
|
const listener2 = () => count2++;
|
||||||
|
eventEmitter.on('test', listener1);
|
||||||
|
eventEmitter.on('test', listener2);
|
||||||
|
eventEmitter.emit('test');
|
||||||
|
assert.equal(count1, 1);
|
||||||
|
assert.equal(count2, 1);
|
||||||
|
eventEmitter.emit('test');
|
||||||
|
assert.equal(count1, 2);
|
||||||
|
assert.equal(count2, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should manage multiple listener types', () => {
|
||||||
|
let count1 = 0;
|
||||||
|
let count2 = 0;
|
||||||
|
const listener1 = () => count1++;
|
||||||
|
const listener2 = () => count2++;
|
||||||
|
eventEmitter.on('test', listener1);
|
||||||
|
eventEmitter.on('foo', listener2);
|
||||||
|
eventEmitter.emit('test');
|
||||||
|
assert.equal(count1, 1);
|
||||||
|
assert.equal(count2, 0);
|
||||||
|
eventEmitter.emit('foo');
|
||||||
|
assert.equal(count1, 1);
|
||||||
|
assert.equal(count2, 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('listeners', () => {
|
||||||
|
it('should return listeners for the type requested', () => {
|
||||||
|
assert.equal(eventEmitter.listeners('test').length, 0);
|
||||||
|
const listener = () => {};
|
||||||
|
eventEmitter.on('test', listener);
|
||||||
|
assert.deepEqual(eventEmitter.listeners('test'), [listener]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('off', () => {
|
||||||
|
it('should remove the specific listener', () => {
|
||||||
|
const listener1 = () => {};
|
||||||
|
const listener2 = () => {};
|
||||||
|
eventEmitter.on('foo', listener1);
|
||||||
|
eventEmitter.on('foo', listener2);
|
||||||
|
assert.equal(eventEmitter.listeners('foo').length, 2);
|
||||||
|
eventEmitter.off('foo', listener1);
|
||||||
|
assert.deepEqual(eventEmitter.listeners('foo'), [listener2]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('removeAllListeners', () => {
|
||||||
|
it('should clear all listeners', () => {
|
||||||
|
eventEmitter.on('foo', () => {});
|
||||||
|
assert.equal(eventEmitter.listeners('foo').length, 1);
|
||||||
|
eventEmitter.removeAllListeners('foo');
|
||||||
|
assert.equal(eventEmitter.listeners('foo').length, 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
/**
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface ListenerType {
|
||||||
|
(): void;
|
||||||
|
listener?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class EventEmitter {
|
||||||
|
private _events: {[type: string]: ListenerType[]};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
// Restore the previous events if available, this will happen if the
|
||||||
|
// constructor is called multiple times on the same object (terminal reset).
|
||||||
|
this._events = this._events || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
public on(type, listener): void {
|
||||||
|
this._events[type] = this._events[type] || [];
|
||||||
|
this._events[type].push(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public off(type, listener): void {
|
||||||
|
if (!this._events[type]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let obj = this._events[type];
|
||||||
|
let i = obj.length;
|
||||||
|
|
||||||
|
while (i--) {
|
||||||
|
if (obj[i] === listener || obj[i].listener === listener) {
|
||||||
|
obj.splice(i, 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeAllListeners(type): void {
|
||||||
|
if (this._events[type]) {
|
||||||
|
delete this._events[type];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public once(type, listener): any {
|
||||||
|
function on() {
|
||||||
|
let args = Array.prototype.slice.call(arguments);
|
||||||
|
this.off(type, on);
|
||||||
|
return listener.apply(this, args);
|
||||||
|
}
|
||||||
|
(<any>on).listener = listener;
|
||||||
|
return this.on(type, on);
|
||||||
|
}
|
||||||
|
|
||||||
|
public emit(type: string, ...args: any[]): void {
|
||||||
|
if (!this._events[type]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let obj = this._events[type];
|
||||||
|
for (let i = 0; i < obj.length; i++) {
|
||||||
|
obj[i].apply(this, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public listeners(type): ListenerType[] {
|
||||||
|
return this._events[type] || [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { assert } from 'chai';
|
||||||
|
import { InputHandler } from './InputHandler';
|
||||||
|
|
||||||
|
describe('InputHandler', () => {
|
||||||
|
describe('setCursorStyle', () => {
|
||||||
|
it('should call Terminal.setOption with correct params', () => {
|
||||||
|
let options = {};
|
||||||
|
let terminal = {
|
||||||
|
setOption: (option, value) => options[option] = value
|
||||||
|
};
|
||||||
|
let inputHandler = new InputHandler(terminal);
|
||||||
|
|
||||||
|
inputHandler.setCursorStyle([0]);
|
||||||
|
assert.equal(options['cursorStyle'], 'block');
|
||||||
|
assert.equal(options['cursorBlink'], true);
|
||||||
|
|
||||||
|
options = {};
|
||||||
|
inputHandler.setCursorStyle([1]);
|
||||||
|
assert.equal(options['cursorStyle'], 'block');
|
||||||
|
assert.equal(options['cursorBlink'], true);
|
||||||
|
|
||||||
|
options = {};
|
||||||
|
inputHandler.setCursorStyle([2]);
|
||||||
|
assert.equal(options['cursorStyle'], 'block');
|
||||||
|
assert.equal(options['cursorBlink'], false);
|
||||||
|
|
||||||
|
options = {};
|
||||||
|
inputHandler.setCursorStyle([3]);
|
||||||
|
assert.equal(options['cursorStyle'], 'underline');
|
||||||
|
assert.equal(options['cursorBlink'], true);
|
||||||
|
|
||||||
|
options = {};
|
||||||
|
inputHandler.setCursorStyle([4]);
|
||||||
|
assert.equal(options['cursorStyle'], 'underline');
|
||||||
|
assert.equal(options['cursorBlink'], false);
|
||||||
|
|
||||||
|
options = {};
|
||||||
|
inputHandler.setCursorStyle([5]);
|
||||||
|
assert.equal(options['cursorStyle'], 'bar');
|
||||||
|
assert.equal(options['cursorBlink'], true);
|
||||||
|
|
||||||
|
options = {};
|
||||||
|
inputHandler.setCursorStyle([6]);
|
||||||
|
assert.equal(options['cursorStyle'], 'bar');
|
||||||
|
assert.equal(options['cursorBlink'], false);
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
+1589
File diff suppressed because it is too large
Load Diff
+119
-1
@@ -2,6 +2,9 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { LinkMatcherOptions } from './Interfaces';
|
||||||
|
import { LinkMatcherHandler, LinkMatcherValidationCallback } from './Types';
|
||||||
|
|
||||||
export interface IBrowser {
|
export interface IBrowser {
|
||||||
isNode: boolean;
|
isNode: boolean;
|
||||||
userAgent: string;
|
userAgent: string;
|
||||||
@@ -17,11 +20,22 @@ export interface IBrowser {
|
|||||||
export interface ITerminal {
|
export interface ITerminal {
|
||||||
element: HTMLElement;
|
element: HTMLElement;
|
||||||
rowContainer: HTMLElement;
|
rowContainer: HTMLElement;
|
||||||
|
selectionContainer: HTMLElement;
|
||||||
|
charMeasure: ICharMeasure;
|
||||||
textarea: HTMLTextAreaElement;
|
textarea: HTMLTextAreaElement;
|
||||||
|
ybase: number;
|
||||||
ydisp: number;
|
ydisp: number;
|
||||||
lines: string[];
|
lines: ICircularList<string>;
|
||||||
rows: number;
|
rows: number;
|
||||||
|
cols: number;
|
||||||
browser: IBrowser;
|
browser: IBrowser;
|
||||||
|
writeBuffer: string[];
|
||||||
|
children: HTMLElement[];
|
||||||
|
cursorHidden: boolean;
|
||||||
|
cursorState: number;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
defAttr: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emit the 'data' event and populate the given data.
|
* Emit the 'data' event and populate the given data.
|
||||||
@@ -31,4 +45,108 @@ export interface ITerminal {
|
|||||||
on(event: string, callback: () => void);
|
on(event: string, callback: () => void);
|
||||||
scrollDisp(disp: number, suppressScrollEvent: boolean);
|
scrollDisp(disp: number, suppressScrollEvent: boolean);
|
||||||
cancel(ev: Event, force?: boolean);
|
cancel(ev: Event, force?: boolean);
|
||||||
|
log(text: string): void;
|
||||||
|
emit(event: string, data: any);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISelectionManager {
|
||||||
|
selectionText: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ICharMeasure {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
measure(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ILinkifier {
|
||||||
|
linkifyRow(rowIndex: number): void;
|
||||||
|
attachHypertextLinkHandler(handler: LinkMatcherHandler): void;
|
||||||
|
registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options?: LinkMatcherOptions): number;
|
||||||
|
deregisterLinkMatcher(matcherId: number): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ICircularList<T> {
|
||||||
|
length: number;
|
||||||
|
maxLength: number;
|
||||||
|
|
||||||
|
forEach(callbackfn: (value: T, index: number, array: T[]) => void): void;
|
||||||
|
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 LinkMatcherOptions {
|
||||||
|
/**
|
||||||
|
* The index of the link from the regex.match(text) call. This defaults to 0
|
||||||
|
* (for regular expressions without capture groups).
|
||||||
|
*/
|
||||||
|
matchIndex?: number;
|
||||||
|
/**
|
||||||
|
* A callback that validates an individual link, returning true if valid and
|
||||||
|
* false if invalid.
|
||||||
|
*/
|
||||||
|
validationCallback?: LinkMatcherValidationCallback;
|
||||||
|
/**
|
||||||
|
* The priority of the link matcher, this defines the order in which the link
|
||||||
|
* matcher is evaluated relative to others, from highest to lowest. The
|
||||||
|
* default value is 0.
|
||||||
|
*/
|
||||||
|
priority?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles actions generated by the parser.
|
||||||
|
*/
|
||||||
|
export interface IInputHandler {
|
||||||
|
addChar(char: string, code: number): void;
|
||||||
|
|
||||||
|
/** C0 BEL */ bell(): void;
|
||||||
|
/** C0 LF */ lineFeed(): void;
|
||||||
|
/** C0 CR */ carriageReturn(): void;
|
||||||
|
/** C0 BS */ backspace(): void;
|
||||||
|
/** C0 HT */ tab(): void;
|
||||||
|
/** C0 SO */ shiftOut(): void;
|
||||||
|
/** C0 SI */ shiftIn(): void;
|
||||||
|
|
||||||
|
/** CSI @ */ insertChars(params?: number[]): void;
|
||||||
|
/** CSI A */ cursorUp(params?: number[]): void;
|
||||||
|
/** CSI B */ cursorDown(params?: number[]): void;
|
||||||
|
/** CSI C */ cursorForward(params?: number[]): void;
|
||||||
|
/** CSI D */ cursorBackward(params?: number[]): void;
|
||||||
|
/** CSI E */ cursorNextLine(params?: number[]): void;
|
||||||
|
/** CSI F */ cursorPrecedingLine(params?: number[]): void;
|
||||||
|
/** CSI G */ cursorCharAbsolute(params?: number[]): void;
|
||||||
|
/** CSI H */ cursorPosition(params?: number[]): void;
|
||||||
|
/** CSI I */ cursorForwardTab(params?: number[]): void;
|
||||||
|
/** CSI J */ eraseInDisplay(params?: number[]): void;
|
||||||
|
/** CSI K */ eraseInLine(params?: number[]): void;
|
||||||
|
/** CSI L */ insertLines(params?: number[]): void;
|
||||||
|
/** 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 X */ eraseChars(params?: number[]): void;
|
||||||
|
/** CSI Z */ cursorBackwardTab(params?: number[]): void;
|
||||||
|
/** CSI ` */ charPosAbsolute(params?: number[]): void;
|
||||||
|
/** CSI a */ HPositionRelative(params?: number[]): void;
|
||||||
|
/** CSI b */ repeatPrecedingCharacter(params?: number[]): void;
|
||||||
|
/** CSI c */ sendDeviceAttributes(params?: number[]): void;
|
||||||
|
/** CSI d */ linePosAbsolute(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 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 s */ saveCursor(params?: number[]): void;
|
||||||
|
/** CSI u */ restoreCursor(params?: number[]): void;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,193 @@
|
|||||||
|
/**
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
import jsdom = require('jsdom');
|
||||||
|
import { assert } from 'chai';
|
||||||
|
import { ITerminal, ILinkifier } from './Interfaces';
|
||||||
|
import { Linkifier } from './Linkifier';
|
||||||
|
import { LinkMatcher } from './Types';
|
||||||
|
|
||||||
|
class TestLinkifier extends Linkifier {
|
||||||
|
constructor() {
|
||||||
|
Linkifier.TIME_BEFORE_LINKIFY = 0;
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public get linkMatchers(): LinkMatcher[] { return this._linkMatchers; }
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Linkifier', () => {
|
||||||
|
let window: Window;
|
||||||
|
let document: Document;
|
||||||
|
|
||||||
|
let container: HTMLElement;
|
||||||
|
let rows: HTMLElement[];
|
||||||
|
let linkifier: TestLinkifier;
|
||||||
|
|
||||||
|
beforeEach(done => {
|
||||||
|
jsdom.env('', (err, w) => {
|
||||||
|
window = w;
|
||||||
|
document = window.document;
|
||||||
|
linkifier = new TestLinkifier();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function addRow(html: string) {
|
||||||
|
const element = document.createElement('div');
|
||||||
|
element.innerHTML = html;
|
||||||
|
container.appendChild(element);
|
||||||
|
rows.push(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('before attachToDom', () => {
|
||||||
|
it('should allow link matcher registration', done => {
|
||||||
|
assert.doesNotThrow(() => {
|
||||||
|
const linkMatcherId = linkifier.registerLinkMatcher(/foo/, () => {});
|
||||||
|
assert.isTrue(linkifier.deregisterLinkMatcher(linkMatcherId));
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('after attachToDom', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
rows = [];
|
||||||
|
linkifier.attachToDom(document, rows);
|
||||||
|
container = document.createElement('div');
|
||||||
|
document.body.appendChild(container);
|
||||||
|
});
|
||||||
|
|
||||||
|
function clickElement(element: Node) {
|
||||||
|
const event = document.createEvent('MouseEvent');
|
||||||
|
event.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
|
||||||
|
element.dispatchEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertLinkifiesEntireRow(uri: string, done: MochaDone) {
|
||||||
|
addRow(uri);
|
||||||
|
linkifier.linkifyRow(0);
|
||||||
|
setTimeout(() => {
|
||||||
|
assert.equal((<HTMLElement>rows[0].firstChild).tagName, 'A');
|
||||||
|
assert.equal((<HTMLElement>rows[0].firstChild).textContent, uri);
|
||||||
|
done();
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('http links', () => {
|
||||||
|
function assertLinkifiesEntireRow(uri: string, done: MochaDone) {
|
||||||
|
addRow(uri);
|
||||||
|
linkifier.linkifyRow(0);
|
||||||
|
setTimeout(() => {
|
||||||
|
assert.equal((<HTMLElement>rows[0].firstChild).tagName, 'A');
|
||||||
|
assert.equal((<HTMLElement>rows[0].firstChild).textContent, uri);
|
||||||
|
done();
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
it('should allow ~ character in URI path', done => assertLinkifiesEntireRow('http://foo.com/a~b#c~d?e~f', done));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('link matcher', () => {
|
||||||
|
function assertLinkifiesRow(rowText: string, linkMatcherRegex: RegExp, expectedHtml: string, done: MochaDone) {
|
||||||
|
addRow(rowText);
|
||||||
|
linkifier.registerLinkMatcher(linkMatcherRegex, () => {});
|
||||||
|
linkifier.linkifyRow(0);
|
||||||
|
// Allow linkify to happen
|
||||||
|
setTimeout(() => {
|
||||||
|
assert.equal(rows[0].innerHTML, expectedHtml);
|
||||||
|
done();
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
it('should match a single link', done => {
|
||||||
|
assertLinkifiesRow('foo', /foo/, '<a>foo</a>', done);
|
||||||
|
});
|
||||||
|
it('should match a single link at the start of a text node', done => {
|
||||||
|
assertLinkifiesRow('foo bar', /foo/, '<a>foo</a> bar', done);
|
||||||
|
});
|
||||||
|
it('should match a single link in the middle of a text node', done => {
|
||||||
|
assertLinkifiesRow('foo bar baz', /bar/, 'foo <a>bar</a> baz', done);
|
||||||
|
});
|
||||||
|
it('should match a single link at the end of a text node', done => {
|
||||||
|
assertLinkifiesRow('foo bar', /bar/, 'foo <a>bar</a>', done);
|
||||||
|
});
|
||||||
|
it('should match a link after a link at the start of a text node', done => {
|
||||||
|
assertLinkifiesRow('foo bar', /foo|bar/, '<a>foo</a> <a>bar</a>', done);
|
||||||
|
});
|
||||||
|
it('should match a link after a link in the middle of a text node', done => {
|
||||||
|
assertLinkifiesRow('foo bar baz', /bar|baz/, 'foo <a>bar</a> <a>baz</a>', done);
|
||||||
|
});
|
||||||
|
it('should match a link immediately after a link at the end of a text node', done => {
|
||||||
|
assertLinkifiesRow('<span>foo bar</span>baz', /bar|baz/, '<span>foo <a>bar</a></span><a>baz</a>', done);
|
||||||
|
});
|
||||||
|
it('should not duplicate text after a unicode character (wrapped in a span)', done => {
|
||||||
|
// This is a regression test for an issue that came about when using
|
||||||
|
// an oh-my-zsh theme that added the large blue diamond unicode
|
||||||
|
// character (U+1F537) which caused the path to be duplicated. See #642.
|
||||||
|
assertLinkifiesRow('echo \'<span class="xterm-normal-char">🔷</span>foo\'', /foo/, 'echo \'<span class="xterm-normal-char">🔷</span><a>foo</a>\'', done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('validationCallback', () => {
|
||||||
|
it('should enable link if true', done => {
|
||||||
|
addRow('test');
|
||||||
|
linkifier.registerLinkMatcher(/test/, () => done(), {
|
||||||
|
validationCallback: (url, element, cb) => {
|
||||||
|
cb(true);
|
||||||
|
assert.equal((<HTMLElement>rows[0].firstChild).tagName, 'A');
|
||||||
|
setTimeout(() => clickElement(rows[0].firstChild), 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
linkifier.linkifyRow(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should disable link if false', done => {
|
||||||
|
addRow('test');
|
||||||
|
linkifier.registerLinkMatcher(/test/, () => assert.fail(), {
|
||||||
|
validationCallback: (url, element, cb) => {
|
||||||
|
cb(false);
|
||||||
|
assert.equal((<HTMLElement>rows[0].firstChild).tagName, 'A');
|
||||||
|
setTimeout(() => clickElement(rows[0].firstChild), 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
linkifier.linkifyRow(0);
|
||||||
|
// Allow time for the click to be performed
|
||||||
|
setTimeout(() => done(), 10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should trigger for multiple link matches on one row', done => {
|
||||||
|
addRow('test test');
|
||||||
|
let count = 0;
|
||||||
|
linkifier.registerLinkMatcher(/test/, () => assert.fail(), {
|
||||||
|
validationCallback: (url, element, cb) => {
|
||||||
|
count += 1;
|
||||||
|
if (count === 2) {
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
cb(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
linkifier.linkifyRow(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('priority', () => {
|
||||||
|
it('should order the list from highest priority to lowest #1', () => {
|
||||||
|
const aId = linkifier.registerLinkMatcher(/a/, () => {}, { priority: 1 });
|
||||||
|
const bId = linkifier.registerLinkMatcher(/b/, () => {}, { priority: -1 });
|
||||||
|
assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [aId, 0, bId]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should order the list from highest priority to lowest #2', () => {
|
||||||
|
const aId = linkifier.registerLinkMatcher(/a/, () => {}, { priority: -1 });
|
||||||
|
const bId = linkifier.registerLinkMatcher(/b/, () => {}, { priority: 1 });
|
||||||
|
assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [bId, 0, aId]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should order items of equal priority in the order they are added', () => {
|
||||||
|
const aId = linkifier.registerLinkMatcher(/a/, () => {}, { priority: 0 });
|
||||||
|
const bId = linkifier.registerLinkMatcher(/b/, () => {}, { priority: 0 });
|
||||||
|
assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [0, aId, bId]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,363 @@
|
|||||||
|
/**
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { LinkMatcherOptions } from './Interfaces';
|
||||||
|
import { LinkMatcher, LinkMatcherHandler, LinkMatcherValidationCallback } from './Types';
|
||||||
|
|
||||||
|
const INVALID_LINK_CLASS = 'xterm-invalid-link';
|
||||||
|
|
||||||
|
const protocolClause = '(https?:\\/\\/)';
|
||||||
|
const domainCharacterSet = '[\\da-z\\.-]+';
|
||||||
|
const negatedDomainCharacterSet = '[^\\da-z\\.-]+';
|
||||||
|
const domainBodyClause = '(' + domainCharacterSet + ')';
|
||||||
|
const tldClause = '([a-z\\.]{2,6})';
|
||||||
|
const ipClause = '((\\d{1,3}\\.){3}\\d{1,3})';
|
||||||
|
const localHostClause = '(localhost)';
|
||||||
|
const portClause = '(:\\d{1,5})';
|
||||||
|
const hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|' + ipClause + '|' + localHostClause + ')' + portClause + '?';
|
||||||
|
const pathClause = '(\\/[\\/\\w\\.\\-%~]*)*';
|
||||||
|
const queryStringHashFragmentCharacterSet = '[0-9\\w\\[\\]\\(\\)\\/\\?\\!#@$%&\'*+,:;~\\=\\.\\-]*';
|
||||||
|
const queryStringClause = '(\\?' + queryStringHashFragmentCharacterSet + ')?';
|
||||||
|
const hashFragmentClause = '(#' + queryStringHashFragmentCharacterSet + ')?';
|
||||||
|
const negatedPathCharacterSet = '[^\\/\\w\\.\\-%]+';
|
||||||
|
const bodyClause = hostClause + pathClause + queryStringClause + hashFragmentClause;
|
||||||
|
const start = '(?:^|' + negatedDomainCharacterSet + ')(';
|
||||||
|
const end = ')($|' + negatedPathCharacterSet + ')';
|
||||||
|
const strictUrlRegex = new RegExp(start + protocolClause + bodyClause + end);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ID of the built in http(s) link matcher.
|
||||||
|
*/
|
||||||
|
const HYPERTEXT_LINK_MATCHER_ID = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Linkifier applies links to rows shortly after they have been refreshed.
|
||||||
|
*/
|
||||||
|
export class Linkifier {
|
||||||
|
/**
|
||||||
|
* The time to wait after a row is changed before it is linkified. This prevents
|
||||||
|
* the costly operation of searching every row multiple times, potentially a
|
||||||
|
* huge amount of times.
|
||||||
|
*/
|
||||||
|
protected static TIME_BEFORE_LINKIFY = 200;
|
||||||
|
|
||||||
|
protected _linkMatchers: LinkMatcher[];
|
||||||
|
|
||||||
|
private _document: Document;
|
||||||
|
private _rows: HTMLElement[];
|
||||||
|
private _rowTimeoutIds: number[];
|
||||||
|
private _nextLinkMatcherId = HYPERTEXT_LINK_MATCHER_ID;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this._rowTimeoutIds = [];
|
||||||
|
this._linkMatchers = [];
|
||||||
|
this.registerLinkMatcher(strictUrlRegex, null, { matchIndex: 1 });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attaches the linkifier to the DOM, enabling linkification.
|
||||||
|
* @param document The document object.
|
||||||
|
* @param rows The array of rows to apply links to.
|
||||||
|
*/
|
||||||
|
public attachToDom(document: Document, rows: HTMLElement[]) {
|
||||||
|
this._document = document;
|
||||||
|
this._rows = rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queues a row for linkification.
|
||||||
|
* @param {number} rowIndex The index of the row to linkify.
|
||||||
|
*/
|
||||||
|
public linkifyRow(rowIndex: number): void {
|
||||||
|
// Don't attempt linkify if not yet attached to DOM
|
||||||
|
if (!this._document) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeoutId = this._rowTimeoutIds[rowIndex];
|
||||||
|
if (timeoutId) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
this._rowTimeoutIds[rowIndex] = setTimeout(this._linkifyRow.bind(this, rowIndex), Linkifier.TIME_BEFORE_LINKIFY);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attaches a handler for hypertext links, overriding default <a> behavior
|
||||||
|
* for standard http(s) links.
|
||||||
|
* @param {LinkHandler} handler The handler to use, this can be cleared with
|
||||||
|
* null.
|
||||||
|
*/
|
||||||
|
public setHypertextLinkHandler(handler: LinkMatcherHandler): void {
|
||||||
|
this._linkMatchers[HYPERTEXT_LINK_MATCHER_ID].handler = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attaches a validation callback for hypertext links.
|
||||||
|
* @param {LinkMatcherValidationCallback} callback The callback to use, this
|
||||||
|
* can be cleared with null.
|
||||||
|
*/
|
||||||
|
public setHypertextValidationCallback(callback: LinkMatcherValidationCallback): void {
|
||||||
|
this._linkMatchers[HYPERTEXT_LINK_MATCHER_ID].validationCallback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a link matcher, allowing custom link patterns to be matched and
|
||||||
|
* handled.
|
||||||
|
* @param {RegExp} regex The regular expression to search for, specifically
|
||||||
|
* this searches the textContent of the rows. You will want to use \s to match
|
||||||
|
* a space ' ' character for example.
|
||||||
|
* @param {LinkHandler} handler The callback when the link is called.
|
||||||
|
* @param {LinkMatcherOptions} [options] Options for the link matcher.
|
||||||
|
* @return {number} The ID of the new matcher, this can be used to deregister.
|
||||||
|
*/
|
||||||
|
public registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options: LinkMatcherOptions = {}): number {
|
||||||
|
if (this._nextLinkMatcherId !== HYPERTEXT_LINK_MATCHER_ID && !handler) {
|
||||||
|
throw new Error('handler must be defined');
|
||||||
|
}
|
||||||
|
const matcher: LinkMatcher = {
|
||||||
|
id: this._nextLinkMatcherId++,
|
||||||
|
regex,
|
||||||
|
handler,
|
||||||
|
matchIndex: options.matchIndex,
|
||||||
|
validationCallback: options.validationCallback,
|
||||||
|
priority: options.priority || 0
|
||||||
|
};
|
||||||
|
this._addLinkMatcherToList(matcher);
|
||||||
|
return matcher.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts a link matcher to the list in the correct position based on the
|
||||||
|
* priority of each link matcher. New link matchers of equal priority are
|
||||||
|
* considered after older link matchers.
|
||||||
|
* @param matcher The link matcher to be added.
|
||||||
|
*/
|
||||||
|
private _addLinkMatcherToList(matcher: LinkMatcher): void {
|
||||||
|
if (this._linkMatchers.length === 0) {
|
||||||
|
this._linkMatchers.push(matcher);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = this._linkMatchers.length - 1; i >= 0; i--) {
|
||||||
|
if (matcher.priority <= this._linkMatchers[i].priority) {
|
||||||
|
this._linkMatchers.splice(i + 1, 0, matcher);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._linkMatchers.splice(0, 0, matcher);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deregisters a link matcher if it has been registered.
|
||||||
|
* @param {number} matcherId The link matcher's ID (returned after register)
|
||||||
|
* @return {boolean} Whether a link matcher was found and deregistered.
|
||||||
|
*/
|
||||||
|
public deregisterLinkMatcher(matcherId: number): boolean {
|
||||||
|
// ID 0 is the hypertext link matcher which cannot be deregistered
|
||||||
|
for (let i = 1; i < this._linkMatchers.length; i++) {
|
||||||
|
if (this._linkMatchers[i].id === matcherId) {
|
||||||
|
this._linkMatchers.splice(i, 1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Linkifies a row.
|
||||||
|
* @param {number} rowIndex The index of the row to linkify.
|
||||||
|
*/
|
||||||
|
private _linkifyRow(rowIndex: number): void {
|
||||||
|
const row = this._rows[rowIndex];
|
||||||
|
if (!row) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const text = row.textContent;
|
||||||
|
for (let i = 0; i < this._linkMatchers.length; i++) {
|
||||||
|
const matcher = this._linkMatchers[i];
|
||||||
|
const linkElements = this._doLinkifyRow(row, matcher);
|
||||||
|
if (linkElements.length > 0) {
|
||||||
|
// Fire validation callback
|
||||||
|
if (matcher.validationCallback) {
|
||||||
|
for (let j = 0; j < linkElements.length; j++) {
|
||||||
|
const element = linkElements[j];
|
||||||
|
matcher.validationCallback(element.textContent, element, isValid => {
|
||||||
|
if (!isValid) {
|
||||||
|
element.classList.add(INVALID_LINK_CLASS);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Only allow a single LinkMatcher to trigger on any given row.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Linkifies a row given a specific handler.
|
||||||
|
* @param {HTMLElement} row The row to linkify.
|
||||||
|
* @param {LinkMatcher} matcher The link matcher for this line.
|
||||||
|
* @return The link element(s) that were added.
|
||||||
|
*/
|
||||||
|
private _doLinkifyRow(row: HTMLElement, matcher: LinkMatcher): HTMLElement[] {
|
||||||
|
// Iterate over nodes as we want to consider text nodes
|
||||||
|
let result = [];
|
||||||
|
const isHttpLinkMatcher = matcher.id === HYPERTEXT_LINK_MATCHER_ID;
|
||||||
|
const nodes = row.childNodes;
|
||||||
|
|
||||||
|
// Find the first match
|
||||||
|
let match = row.textContent.match(matcher.regex);
|
||||||
|
if (!match || match.length === 0) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
let uri = match[typeof matcher.matchIndex !== 'number' ? 0 : matcher.matchIndex];
|
||||||
|
// Set the next searches start index
|
||||||
|
let rowStartIndex = match.index + uri.length;
|
||||||
|
|
||||||
|
for (let i = 0; i < nodes.length; i++) {
|
||||||
|
const node = nodes[i];
|
||||||
|
const searchIndex = node.textContent.indexOf(uri);
|
||||||
|
if (searchIndex >= 0) {
|
||||||
|
const linkElement = this._createAnchorElement(uri, matcher.handler, isHttpLinkMatcher);
|
||||||
|
if (node.textContent.length === uri.length) {
|
||||||
|
// Matches entire string
|
||||||
|
if (node.nodeType === 3 /*Node.TEXT_NODE*/) {
|
||||||
|
this._replaceNode(node, linkElement);
|
||||||
|
} else {
|
||||||
|
const element = (<HTMLElement>node);
|
||||||
|
if (element.nodeName === 'A') {
|
||||||
|
// This row has already been linkified
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
element.innerHTML = '';
|
||||||
|
element.appendChild(linkElement);
|
||||||
|
}
|
||||||
|
} else if (node.childNodes.length > 1) {
|
||||||
|
// Matches part of string in an element with multiple child nodes
|
||||||
|
for (let j = 0; j < node.childNodes.length; j++) {
|
||||||
|
const childNode = node.childNodes[j];
|
||||||
|
const childSearchIndex = childNode.textContent.indexOf(uri);
|
||||||
|
if (childSearchIndex !== -1) {
|
||||||
|
// Match found in currentNode
|
||||||
|
this._replaceNodeSubstringWithNode(childNode, linkElement, uri, childSearchIndex);
|
||||||
|
// Don't need to count nodesAdded by replacing the node as this
|
||||||
|
// is a child node, not a top-level node.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Matches part of string in a single text node
|
||||||
|
const nodesAdded = this._replaceNodeSubstringWithNode(node, linkElement, uri, searchIndex);
|
||||||
|
// No need to consider the new nodes
|
||||||
|
i += nodesAdded;
|
||||||
|
}
|
||||||
|
result.push(linkElement);
|
||||||
|
|
||||||
|
// Find the next match
|
||||||
|
match = row.textContent.substring(rowStartIndex).match(matcher.regex);
|
||||||
|
if (!match || match.length === 0) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
uri = match[typeof matcher.matchIndex !== 'number' ? 0 : matcher.matchIndex];
|
||||||
|
rowStartIndex += match.index + uri.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a link anchor element.
|
||||||
|
* @param {string} uri The uri of the link.
|
||||||
|
* @return {HTMLAnchorElement} The link.
|
||||||
|
*/
|
||||||
|
private _createAnchorElement(uri: string, handler: LinkMatcherHandler, isHypertextLinkHandler: boolean): HTMLAnchorElement {
|
||||||
|
const element = this._document.createElement('a');
|
||||||
|
element.textContent = uri;
|
||||||
|
element.draggable = false;
|
||||||
|
if (isHypertextLinkHandler) {
|
||||||
|
element.href = uri;
|
||||||
|
// Force link on another tab so work is not lost
|
||||||
|
element.target = '_blank';
|
||||||
|
element.addEventListener('click', (event: MouseEvent) => {
|
||||||
|
if (handler) {
|
||||||
|
return handler(event, uri);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
element.addEventListener('click', (event: MouseEvent) => {
|
||||||
|
// Don't execute the handler if the link is flagged as invalid
|
||||||
|
if (element.classList.contains(INVALID_LINK_CLASS)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return handler(event, uri);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace a node with 1 or more other nodes.
|
||||||
|
* @param {Node} oldNode The node to replace.
|
||||||
|
* @param {Node[]} newNodes The new nodes to insert in order.
|
||||||
|
*/
|
||||||
|
private _replaceNode(oldNode: Node, ...newNodes: Node[]): void {
|
||||||
|
const parent = oldNode.parentNode;
|
||||||
|
for (let i = 0; i < newNodes.length; i++) {
|
||||||
|
parent.insertBefore(newNodes[i], oldNode);
|
||||||
|
}
|
||||||
|
parent.removeChild(oldNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace a substring within a node with a new node.
|
||||||
|
* @param {Node} targetNode The target node; either a text node or a <span>
|
||||||
|
* containing a single text node.
|
||||||
|
* @param {Node} newNode The new node to insert.
|
||||||
|
* @param {string} substring The substring to replace.
|
||||||
|
* @param {number} substringIndex The index of the substring within the string.
|
||||||
|
* @return The number of nodes to skip when searching for the next uri.
|
||||||
|
*/
|
||||||
|
private _replaceNodeSubstringWithNode(targetNode: Node, newNode: Node, substring: string, substringIndex: number): number {
|
||||||
|
// If the targetNode is a non-text node with a single child, make the child
|
||||||
|
// the new targetNode.
|
||||||
|
if (targetNode.childNodes.length === 1) {
|
||||||
|
targetNode = targetNode.childNodes[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// The targetNode will be either a text node or a <span>. The text node
|
||||||
|
// (targetNode or its only-child) needs to be replaced with newNode plus new
|
||||||
|
// text nodes potentially on either side.
|
||||||
|
if (targetNode.nodeType !== 3/*Node.TEXT_NODE*/) {
|
||||||
|
throw new Error('targetNode must be a text node or only contain a single text node');
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullText = targetNode.textContent;
|
||||||
|
|
||||||
|
if (substringIndex === 0) {
|
||||||
|
// Replace with <newNode><textnode>
|
||||||
|
const rightText = fullText.substring(substring.length);
|
||||||
|
const rightTextNode = this._document.createTextNode(rightText);
|
||||||
|
this._replaceNode(targetNode, newNode, rightTextNode);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (substringIndex === targetNode.textContent.length - substring.length) {
|
||||||
|
// Replace with <textnode><newNode>
|
||||||
|
const leftText = fullText.substring(0, substringIndex);
|
||||||
|
const leftTextNode = this._document.createTextNode(leftText);
|
||||||
|
this._replaceNode(targetNode, leftTextNode, newNode);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace with <textnode><newNode><textnode>
|
||||||
|
const leftText = fullText.substring(0, substringIndex);
|
||||||
|
const leftTextNode = this._document.createTextNode(leftText);
|
||||||
|
const rightText = fullText.substring(substringIndex + substring.length);
|
||||||
|
const rightTextNode = this._document.createTextNode(rightText);
|
||||||
|
this._replaceNode(targetNode, leftTextNode, newNode, rightTextNode);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
+638
@@ -0,0 +1,638 @@
|
|||||||
|
/**
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { C0 } from './EscapeSequences';
|
||||||
|
import { IInputHandler } from './Interfaces';
|
||||||
|
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.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);
|
||||||
|
|
||||||
|
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 {
|
||||||
|
let l = data.length, j, cs, ch, code, low;
|
||||||
|
|
||||||
|
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':
|
||||||
|
break;
|
||||||
|
// ESC O
|
||||||
|
// Single Shift Select of G3 Character Set
|
||||||
|
// ( SS3 is 0x8f). This affects next character only.
|
||||||
|
case 'O':
|
||||||
|
break;
|
||||||
|
// ESC n
|
||||||
|
// Invoke the G2 Character Set as GL (LS2).
|
||||||
|
case 'n':
|
||||||
|
this._terminal.setgLevel(2);
|
||||||
|
break;
|
||||||
|
// ESC o
|
||||||
|
// Invoke the G3 Character Set as GL (LS3).
|
||||||
|
case 'o':
|
||||||
|
this._terminal.setgLevel(3);
|
||||||
|
break;
|
||||||
|
// ESC |
|
||||||
|
// Invoke the G3 Character Set as GR (LS3R).
|
||||||
|
case '|':
|
||||||
|
this._terminal.setgLevel(3);
|
||||||
|
break;
|
||||||
|
// ESC }
|
||||||
|
// Invoke the G2 Character Set as GR (LS2R).
|
||||||
|
case '}':
|
||||||
|
this._terminal.setgLevel(2);
|
||||||
|
break;
|
||||||
|
// ESC ~
|
||||||
|
// Invoke the G1 Character Set as GR (LS1R).
|
||||||
|
case '~':
|
||||||
|
this._terminal.setgLevel(1);
|
||||||
|
break;
|
||||||
|
|
||||||
|
// ESC 7 Save Cursor (DECSC).
|
||||||
|
case '7':
|
||||||
|
this._inputHandler.saveCursor();
|
||||||
|
this._state = ParserState.NORMAL;
|
||||||
|
break;
|
||||||
|
|
||||||
|
// ESC 8 Restore Cursor (DECRC).
|
||||||
|
case '8':
|
||||||
|
this._inputHandler.restoreCursor();
|
||||||
|
this._state = ParserState.NORMAL;
|
||||||
|
break;
|
||||||
|
|
||||||
|
// ESC # 3 DEC line height/width
|
||||||
|
case '#':
|
||||||
|
this._state = ParserState.NORMAL;
|
||||||
|
this._position++;
|
||||||
|
break;
|
||||||
|
|
||||||
|
// ESC H Tab Set (HTS is 0x88).
|
||||||
|
case 'H':
|
||||||
|
this._terminal.tabSet();
|
||||||
|
this._state = ParserState.NORMAL;
|
||||||
|
break;
|
||||||
|
|
||||||
|
// ESC = Application Keypad (DECKPAM).
|
||||||
|
case '=':
|
||||||
|
this._terminal.log('Serial port requested application keypad.');
|
||||||
|
this._terminal.applicationKeypad = true;
|
||||||
|
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;
|
||||||
|
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) {
|
||||||
|
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.scrollTop + 1)
|
||||||
|
+ ';'
|
||||||
|
+ (this._terminal.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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) {
|
||||||
|
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--;
|
||||||
|
// }
|
||||||
|
}
|
||||||
+398
@@ -0,0 +1,398 @@
|
|||||||
|
/**
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ITerminal } from './Interfaces';
|
||||||
|
import { DomElementObjectPool } from './utils/DomElementObjectPool';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The maximum number of refresh frames to skip when the write buffer is non-
|
||||||
|
* empty. Note that these frames may be intermingled with frames that are
|
||||||
|
* skipped via requestAnimationFrame's mechanism.
|
||||||
|
*/
|
||||||
|
const MAX_REFRESH_FRAME_SKIP = 5;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flags used to render terminal text properly.
|
||||||
|
*/
|
||||||
|
enum FLAGS {
|
||||||
|
BOLD = 1,
|
||||||
|
UNDERLINE = 2,
|
||||||
|
BLINK = 4,
|
||||||
|
INVERSE = 8,
|
||||||
|
INVISIBLE = 16
|
||||||
|
};
|
||||||
|
|
||||||
|
let brokenBold: boolean = null;
|
||||||
|
|
||||||
|
export class Renderer {
|
||||||
|
/** A queue of the rows to be refreshed */
|
||||||
|
private _refreshRowsQueue: {start: number, end: number}[] = [];
|
||||||
|
private _refreshFramesSkipped = 0;
|
||||||
|
private _refreshAnimationFrame = null;
|
||||||
|
|
||||||
|
private _spanElementObjectPool = new DomElementObjectPool('span');
|
||||||
|
|
||||||
|
constructor(private _terminal: ITerminal) {
|
||||||
|
// Figure out whether boldness affects
|
||||||
|
// the character width of monospace fonts.
|
||||||
|
if (brokenBold === null) {
|
||||||
|
brokenBold = checkBoldBroken((<any>this._terminal).element);
|
||||||
|
}
|
||||||
|
this._spanElementObjectPool = new DomElementObjectPool('span');
|
||||||
|
|
||||||
|
// TODO: Pull more DOM interactions into Renderer.constructor, element for
|
||||||
|
// example should be owned by Renderer (and also exposed by Terminal due to
|
||||||
|
// to established public API).
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
public queueRefresh(start: number, end: number): void {
|
||||||
|
this._refreshRowsQueue.push({ start: start, end: end });
|
||||||
|
if (!this._refreshAnimationFrame) {
|
||||||
|
this._refreshAnimationFrame = window.requestAnimationFrame(this._refreshLoop.bind(this));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs the refresh loop callback, calling refresh only if a refresh is
|
||||||
|
* necessary before queueing up the next one.
|
||||||
|
*/
|
||||||
|
private _refreshLoop(): void {
|
||||||
|
// Skip MAX_REFRESH_FRAME_SKIP frames if the writeBuffer is non-empty as it
|
||||||
|
// will need to be immediately refreshed anyway. This saves a lot of
|
||||||
|
// rendering time as the viewport DOM does not need to be refreshed, no
|
||||||
|
// scroll events, no layouts, etc.
|
||||||
|
const skipFrame = this._terminal.writeBuffer.length > 0 && this._refreshFramesSkipped++ <= MAX_REFRESH_FRAME_SKIP;
|
||||||
|
if (skipFrame) {
|
||||||
|
this._refreshAnimationFrame = window.requestAnimationFrame(this._refreshLoop.bind(this));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._refreshFramesSkipped = 0;
|
||||||
|
let start;
|
||||||
|
let end;
|
||||||
|
if (this._refreshRowsQueue.length > 4) {
|
||||||
|
// Just do a full refresh when 5+ refreshes are queued
|
||||||
|
start = 0;
|
||||||
|
end = this._terminal.rows - 1;
|
||||||
|
} else {
|
||||||
|
// Get start and end rows that need refreshing
|
||||||
|
start = this._refreshRowsQueue[0].start;
|
||||||
|
end = this._refreshRowsQueue[0].end;
|
||||||
|
for (let i = 1; i < this._refreshRowsQueue.length; i++) {
|
||||||
|
if (this._refreshRowsQueue[i].start < start) {
|
||||||
|
start = this._refreshRowsQueue[i].start;
|
||||||
|
}
|
||||||
|
if (this._refreshRowsQueue[i].end > end) {
|
||||||
|
end = this._refreshRowsQueue[i].end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._refreshRowsQueue = [];
|
||||||
|
this._refreshAnimationFrame = null;
|
||||||
|
this._refresh(start, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refreshes (re-renders) terminal content within two rows (inclusive)
|
||||||
|
*
|
||||||
|
* Rendering Engine:
|
||||||
|
*
|
||||||
|
* In the screen buffer, each character is stored as a an array with a character
|
||||||
|
* and a 32-bit integer:
|
||||||
|
* - First value: a utf-16 character.
|
||||||
|
* - Second value:
|
||||||
|
* - Next 9 bits: background color (0-511).
|
||||||
|
* - Next 9 bits: foreground color (0-511).
|
||||||
|
* - Next 14 bits: a mask for misc. flags:
|
||||||
|
* - 1=bold
|
||||||
|
* - 2=underline
|
||||||
|
* - 4=blink
|
||||||
|
* - 8=inverse
|
||||||
|
* - 16=invisible
|
||||||
|
*
|
||||||
|
* @param {number} start The row to start from (between 0 and terminal's height terminal - 1)
|
||||||
|
* @param {number} end The row to end at (between fromRow and terminal's height terminal - 1)
|
||||||
|
*/
|
||||||
|
private _refresh(start: number, end: number): void {
|
||||||
|
// If this is a big refresh, remove the terminal rows from the DOM for faster calculations
|
||||||
|
let parent;
|
||||||
|
if (end - start >= this._terminal.rows / 2) {
|
||||||
|
parent = this._terminal.element.parentNode;
|
||||||
|
if (parent) {
|
||||||
|
this._terminal.element.removeChild(this._terminal.rowContainer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let width = this._terminal.cols;
|
||||||
|
let y = start;
|
||||||
|
|
||||||
|
if (end >= this._terminal.rows) {
|
||||||
|
this._terminal.log('`end` is too large. Most likely a bad CSR.');
|
||||||
|
end = this._terminal.rows - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (; y <= end; y++) {
|
||||||
|
let row = y + this._terminal.ydisp;
|
||||||
|
|
||||||
|
let line = this._terminal.lines.get(row);
|
||||||
|
|
||||||
|
let x;
|
||||||
|
if (this._terminal.y === y - (this._terminal.ybase - this._terminal.ydisp) &&
|
||||||
|
this._terminal.cursorState &&
|
||||||
|
!this._terminal.cursorHidden) {
|
||||||
|
x = this._terminal.x;
|
||||||
|
} else {
|
||||||
|
x = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let attr = this._terminal.defAttr;
|
||||||
|
|
||||||
|
const documentFragment = document.createDocumentFragment();
|
||||||
|
let innerHTML = '';
|
||||||
|
let currentElement;
|
||||||
|
|
||||||
|
// Return the row's spans to the pool
|
||||||
|
while (this._terminal.children[y].children.length) {
|
||||||
|
const child = this._terminal.children[y].children[0];
|
||||||
|
this._terminal.children[y].removeChild(child);
|
||||||
|
this._spanElementObjectPool.release(<HTMLElement>child);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < width; i++) {
|
||||||
|
// TODO: Could data be a more specific type?
|
||||||
|
let data: any = line[i][0];
|
||||||
|
const ch = line[i][1];
|
||||||
|
const ch_width: any = line[i][2];
|
||||||
|
if (!ch_width) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i === x) {
|
||||||
|
data = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data !== attr) {
|
||||||
|
if (attr !== this._terminal.defAttr) {
|
||||||
|
if (innerHTML) {
|
||||||
|
currentElement.innerHTML = innerHTML;
|
||||||
|
innerHTML = '';
|
||||||
|
}
|
||||||
|
documentFragment.appendChild(currentElement);
|
||||||
|
currentElement = null;
|
||||||
|
}
|
||||||
|
if (data !== this._terminal.defAttr) {
|
||||||
|
if (innerHTML && !currentElement) {
|
||||||
|
currentElement = this._spanElementObjectPool.acquire();
|
||||||
|
}
|
||||||
|
if (currentElement) {
|
||||||
|
if (innerHTML) {
|
||||||
|
currentElement.innerHTML = innerHTML;
|
||||||
|
innerHTML = '';
|
||||||
|
}
|
||||||
|
documentFragment.appendChild(currentElement);
|
||||||
|
}
|
||||||
|
currentElement = this._spanElementObjectPool.acquire();
|
||||||
|
if (data === -1) {
|
||||||
|
currentElement.classList.add('reverse-video');
|
||||||
|
currentElement.classList.add('terminal-cursor');
|
||||||
|
} else {
|
||||||
|
let bg = data & 0x1ff;
|
||||||
|
let fg = (data >> 9) & 0x1ff;
|
||||||
|
let flags = data >> 18;
|
||||||
|
|
||||||
|
if (flags & FLAGS.BOLD) {
|
||||||
|
if (!brokenBold) {
|
||||||
|
currentElement.classList.add('xterm-bold');
|
||||||
|
}
|
||||||
|
// See: XTerm*boldColors
|
||||||
|
if (fg < 8) {
|
||||||
|
fg += 8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flags & FLAGS.UNDERLINE) {
|
||||||
|
currentElement.classList.add('xterm-underline');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flags & FLAGS.BLINK) {
|
||||||
|
currentElement.classList.add('xterm-blink');
|
||||||
|
}
|
||||||
|
|
||||||
|
// If inverse flag is on, then swap the foreground and background variables.
|
||||||
|
if (flags & FLAGS.INVERSE) {
|
||||||
|
let temp = bg;
|
||||||
|
bg = fg;
|
||||||
|
fg = temp;
|
||||||
|
// Should inverse just be before the above boldColors effect instead?
|
||||||
|
if ((flags & 1) && fg < 8) {
|
||||||
|
fg += 8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flags & FLAGS.INVISIBLE) {
|
||||||
|
currentElement.classList.add('xterm-hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Weird situation: Invert flag used black foreground and white background results
|
||||||
|
* in invalid background color, positioned at the 256 index of the 256 terminal
|
||||||
|
* color map. Pin the colors manually in such a case.
|
||||||
|
*
|
||||||
|
* Source: https://github.com/sourcelair/xterm.js/issues/57
|
||||||
|
*/
|
||||||
|
if (flags & FLAGS.INVERSE) {
|
||||||
|
if (bg === 257) {
|
||||||
|
bg = 15;
|
||||||
|
}
|
||||||
|
if (fg === 256) {
|
||||||
|
fg = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bg < 256) {
|
||||||
|
currentElement.classList.add(`xterm-bg-color-${bg}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fg < 256) {
|
||||||
|
currentElement.classList.add(`xterm-color-${fg}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ch_width === 2) {
|
||||||
|
// Wrap wide characters so they're sized correctly. It's more difficult to release these
|
||||||
|
// from the object pool so just create new ones via innerHTML.
|
||||||
|
innerHTML += `<span class="xterm-wide-char">${ch}</span>`;
|
||||||
|
} else if (ch.charCodeAt(0) > 255) {
|
||||||
|
// Wrap any non-wide unicode character as some fonts size them badly
|
||||||
|
innerHTML += `<span class="xterm-normal-char">${ch}</span>`;
|
||||||
|
} else {
|
||||||
|
switch (ch) {
|
||||||
|
case '&':
|
||||||
|
innerHTML += '&';
|
||||||
|
break;
|
||||||
|
case '<':
|
||||||
|
innerHTML += '<';
|
||||||
|
break;
|
||||||
|
case '>':
|
||||||
|
innerHTML += '>';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (ch <= ' ') {
|
||||||
|
innerHTML += ' ';
|
||||||
|
} else {
|
||||||
|
innerHTML += ch;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
attr = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (innerHTML && !currentElement) {
|
||||||
|
currentElement = this._spanElementObjectPool.acquire();
|
||||||
|
}
|
||||||
|
if (currentElement) {
|
||||||
|
if (innerHTML) {
|
||||||
|
currentElement.innerHTML = innerHTML;
|
||||||
|
innerHTML = '';
|
||||||
|
}
|
||||||
|
documentFragment.appendChild(currentElement);
|
||||||
|
currentElement = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._terminal.children[y].appendChild(documentFragment);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parent) {
|
||||||
|
this._terminal.element.appendChild(this._terminal.rowContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._terminal.emit('refresh', {element: this._terminal.element, start: start, end: end});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refreshes the selection in the DOM.
|
||||||
|
* @param start The selection start.
|
||||||
|
* @param end The selection end.
|
||||||
|
*/
|
||||||
|
public refreshSelection(start: [number, number], end: [number, number]) {
|
||||||
|
// Remove all selections
|
||||||
|
while (this._terminal.selectionContainer.children.length) {
|
||||||
|
this._terminal.selectionContainer.removeChild(this._terminal.selectionContainer.children[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Selection does not exist
|
||||||
|
if (!start || !end) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Translate from buffer position to viewport position
|
||||||
|
const viewportStartRow = start[1] - this._terminal.ydisp;
|
||||||
|
const viewportEndRow = end[1] - this._terminal.ydisp;
|
||||||
|
const viewportCappedStartRow = Math.max(viewportStartRow, 0);
|
||||||
|
const viewportCappedEndRow = Math.min(viewportEndRow, this._terminal.rows - 1);
|
||||||
|
|
||||||
|
// No need to draw the selection
|
||||||
|
if (viewportCappedStartRow >= this._terminal.rows || viewportCappedEndRow < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the selections
|
||||||
|
const documentFragment = document.createDocumentFragment();
|
||||||
|
// Draw first row
|
||||||
|
const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0;
|
||||||
|
const endCol = viewportCappedStartRow === viewportCappedEndRow ? end[0] : this._terminal.cols;
|
||||||
|
documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow, startCol, endCol));
|
||||||
|
// Draw middle rows
|
||||||
|
const middleRowsCount = viewportCappedEndRow - viewportCappedStartRow - 1;
|
||||||
|
documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow + 1, 0, this._terminal.cols, middleRowsCount));
|
||||||
|
// Draw final row
|
||||||
|
if (viewportCappedStartRow !== viewportCappedEndRow) {
|
||||||
|
// Only draw viewportEndRow if it's not the same as viewporttartRow
|
||||||
|
const endCol = viewportEndRow === viewportCappedEndRow ? end[0] : this._terminal.cols;
|
||||||
|
documentFragment.appendChild(this._createSelectionElement(viewportCappedEndRow, 0, endCol));
|
||||||
|
}
|
||||||
|
this._terminal.selectionContainer.appendChild(documentFragment);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a selection element at the specified position.
|
||||||
|
* @param row The row of the selection.
|
||||||
|
* @param colStart The start column.
|
||||||
|
* @param colEnd The end columns.
|
||||||
|
*/
|
||||||
|
private _createSelectionElement(row: number, colStart: number, colEnd: number, rowCount: number = 1): HTMLElement {
|
||||||
|
const element = document.createElement('div');
|
||||||
|
element.style.height = `${rowCount * this._terminal.charMeasure.height}px`;
|
||||||
|
element.style.top = `${row * this._terminal.charMeasure.height}px`;
|
||||||
|
element.style.left = `${colStart * this._terminal.charMeasure.width}px`;
|
||||||
|
element.style.width = `${this._terminal.charMeasure.width * (colEnd - colStart)}px`;
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// If bold is broken, we can't use it in the terminal.
|
||||||
|
function checkBoldBroken(terminal) {
|
||||||
|
const document = terminal.ownerDocument;
|
||||||
|
const el = document.createElement('span');
|
||||||
|
el.innerHTML = 'hello world';
|
||||||
|
terminal.appendChild(el);
|
||||||
|
const w1 = el.offsetWidth;
|
||||||
|
const h1 = el.offsetHeight;
|
||||||
|
el.style.fontWeight = 'bold';
|
||||||
|
const w2 = el.offsetWidth;
|
||||||
|
const h2 = el.offsetHeight;
|
||||||
|
terminal.removeChild(el);
|
||||||
|
return w1 !== w2 || h1 !== h2;
|
||||||
|
}
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
/**
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
import jsdom = require('jsdom');
|
||||||
|
import { assert } from 'chai';
|
||||||
|
import { ITerminal } from './Interfaces';
|
||||||
|
import { CharMeasure } from './utils/CharMeasure';
|
||||||
|
import { CircularList } from './utils/CircularList';
|
||||||
|
import { SelectionManager } from './SelectionManager';
|
||||||
|
import { SelectionModel } from './SelectionModel';
|
||||||
|
|
||||||
|
class TestSelectionManager extends SelectionManager {
|
||||||
|
constructor(
|
||||||
|
terminal: ITerminal,
|
||||||
|
buffer: CircularList<any>,
|
||||||
|
rowContainer: HTMLElement,
|
||||||
|
charMeasure: CharMeasure
|
||||||
|
) {
|
||||||
|
super(terminal, buffer, rowContainer, charMeasure);
|
||||||
|
}
|
||||||
|
|
||||||
|
public get model(): SelectionModel { return this._model; }
|
||||||
|
|
||||||
|
public selectLineAt(line: number): void { this._selectLineAt(line); }
|
||||||
|
public selectWordAt(coords: [number, number]): void { this._selectWordAt(coords); }
|
||||||
|
|
||||||
|
// Disable DOM interaction
|
||||||
|
public enable(): void {}
|
||||||
|
public disable(): void {}
|
||||||
|
public refresh(): void {}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('SelectionManager', () => {
|
||||||
|
let window: Window;
|
||||||
|
let document: Document;
|
||||||
|
|
||||||
|
let terminal: ITerminal;
|
||||||
|
let buffer: CircularList<any>;
|
||||||
|
let rowContainer: HTMLElement;
|
||||||
|
let selectionManager: TestSelectionManager;
|
||||||
|
|
||||||
|
beforeEach(done => {
|
||||||
|
jsdom.env('', (err, w) => {
|
||||||
|
window = w;
|
||||||
|
document = window.document;
|
||||||
|
buffer = new CircularList<any>(100);
|
||||||
|
terminal = <any>{ cols: 80, rows: 2 };
|
||||||
|
selectionManager = new TestSelectionManager(terminal, buffer, rowContainer, null);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function stringToRow(text: string): [number, string, number][] {
|
||||||
|
let result: [number, string, number][] = [];
|
||||||
|
for (let i = 0; i < text.length; i++) {
|
||||||
|
result.push([0, text.charAt(i), 1]);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('_selectWordAt', () => {
|
||||||
|
it('should expand selection for normal width chars', () => {
|
||||||
|
buffer.push(stringToRow('foo bar'));
|
||||||
|
selectionManager.selectWordAt([0, 0]);
|
||||||
|
assert.equal(selectionManager.selectionText, 'foo');
|
||||||
|
selectionManager.selectWordAt([1, 0]);
|
||||||
|
assert.equal(selectionManager.selectionText, 'foo');
|
||||||
|
selectionManager.selectWordAt([2, 0]);
|
||||||
|
assert.equal(selectionManager.selectionText, 'foo');
|
||||||
|
selectionManager.selectWordAt([3, 0]);
|
||||||
|
assert.equal(selectionManager.selectionText, ' ');
|
||||||
|
selectionManager.selectWordAt([4, 0]);
|
||||||
|
assert.equal(selectionManager.selectionText, 'bar');
|
||||||
|
selectionManager.selectWordAt([5, 0]);
|
||||||
|
assert.equal(selectionManager.selectionText, 'bar');
|
||||||
|
selectionManager.selectWordAt([6, 0]);
|
||||||
|
assert.equal(selectionManager.selectionText, 'bar');
|
||||||
|
});
|
||||||
|
it('should expand selection for whitespace', () => {
|
||||||
|
buffer.push(stringToRow('a b'));
|
||||||
|
selectionManager.selectWordAt([0, 0]);
|
||||||
|
assert.equal(selectionManager.selectionText, 'a');
|
||||||
|
selectionManager.selectWordAt([1, 0]);
|
||||||
|
assert.equal(selectionManager.selectionText, ' ');
|
||||||
|
selectionManager.selectWordAt([2, 0]);
|
||||||
|
assert.equal(selectionManager.selectionText, ' ');
|
||||||
|
selectionManager.selectWordAt([3, 0]);
|
||||||
|
assert.equal(selectionManager.selectionText, ' ');
|
||||||
|
selectionManager.selectWordAt([4, 0]);
|
||||||
|
assert.equal(selectionManager.selectionText, 'b');
|
||||||
|
});
|
||||||
|
it('should expand selection for wide characters', () => {
|
||||||
|
// Wide characters use a special format
|
||||||
|
buffer.push([
|
||||||
|
[null, '中', 2],
|
||||||
|
[null, '', 0],
|
||||||
|
[null, '文', 2],
|
||||||
|
[null, '', 0],
|
||||||
|
[null, ' ', 1],
|
||||||
|
[null, 'a', 1],
|
||||||
|
[null, '中', 2],
|
||||||
|
[null, '', 0],
|
||||||
|
[null, '文', 2],
|
||||||
|
[null, '', 0],
|
||||||
|
[null, 'b', 1],
|
||||||
|
[null, ' ', 1],
|
||||||
|
[null, 'f', 1],
|
||||||
|
[null, 'o', 1],
|
||||||
|
[null, 'o', 1]
|
||||||
|
]);
|
||||||
|
// Ensure wide characters take up 2 columns
|
||||||
|
selectionManager.selectWordAt([0, 0]);
|
||||||
|
assert.equal(selectionManager.selectionText, '中文');
|
||||||
|
selectionManager.selectWordAt([1, 0]);
|
||||||
|
assert.equal(selectionManager.selectionText, '中文');
|
||||||
|
selectionManager.selectWordAt([2, 0]);
|
||||||
|
assert.equal(selectionManager.selectionText, '中文');
|
||||||
|
selectionManager.selectWordAt([3, 0]);
|
||||||
|
assert.equal(selectionManager.selectionText, '中文');
|
||||||
|
selectionManager.selectWordAt([4, 0]);
|
||||||
|
assert.equal(selectionManager.selectionText, ' ');
|
||||||
|
// Ensure wide characters work when wrapped in normal width characters
|
||||||
|
selectionManager.selectWordAt([5, 0]);
|
||||||
|
assert.equal(selectionManager.selectionText, 'a中文b');
|
||||||
|
selectionManager.selectWordAt([6, 0]);
|
||||||
|
assert.equal(selectionManager.selectionText, 'a中文b');
|
||||||
|
selectionManager.selectWordAt([7, 0]);
|
||||||
|
assert.equal(selectionManager.selectionText, 'a中文b');
|
||||||
|
selectionManager.selectWordAt([8, 0]);
|
||||||
|
assert.equal(selectionManager.selectionText, 'a中文b');
|
||||||
|
selectionManager.selectWordAt([9, 0]);
|
||||||
|
assert.equal(selectionManager.selectionText, 'a中文b');
|
||||||
|
selectionManager.selectWordAt([10, 0]);
|
||||||
|
assert.equal(selectionManager.selectionText, 'a中文b');
|
||||||
|
selectionManager.selectWordAt([11, 0]);
|
||||||
|
assert.equal(selectionManager.selectionText, ' ');
|
||||||
|
// Ensure normal width characters work fine in a line containing wide characters
|
||||||
|
selectionManager.selectWordAt([12, 0]);
|
||||||
|
assert.equal(selectionManager.selectionText, 'foo');
|
||||||
|
selectionManager.selectWordAt([13, 0]);
|
||||||
|
assert.equal(selectionManager.selectionText, 'foo');
|
||||||
|
selectionManager.selectWordAt([14, 0]);
|
||||||
|
assert.equal(selectionManager.selectionText, 'foo');
|
||||||
|
});
|
||||||
|
it('should select up to non-path characters that are commonly adjacent to paths', () => {
|
||||||
|
buffer.push(stringToRow('(cd)[ef]{gh}\'ij"'));
|
||||||
|
selectionManager.selectWordAt([0, 0]);
|
||||||
|
assert.equal(selectionManager.selectionText, '(cd');
|
||||||
|
selectionManager.selectWordAt([1, 0]);
|
||||||
|
assert.equal(selectionManager.selectionText, 'cd');
|
||||||
|
selectionManager.selectWordAt([2, 0]);
|
||||||
|
assert.equal(selectionManager.selectionText, 'cd');
|
||||||
|
selectionManager.selectWordAt([3, 0]);
|
||||||
|
assert.equal(selectionManager.selectionText, 'cd)');
|
||||||
|
selectionManager.selectWordAt([4, 0]);
|
||||||
|
assert.equal(selectionManager.selectionText, '[ef');
|
||||||
|
selectionManager.selectWordAt([5, 0]);
|
||||||
|
assert.equal(selectionManager.selectionText, 'ef');
|
||||||
|
selectionManager.selectWordAt([6, 0]);
|
||||||
|
assert.equal(selectionManager.selectionText, 'ef');
|
||||||
|
selectionManager.selectWordAt([7, 0]);
|
||||||
|
assert.equal(selectionManager.selectionText, 'ef]');
|
||||||
|
selectionManager.selectWordAt([8, 0]);
|
||||||
|
assert.equal(selectionManager.selectionText, '{gh');
|
||||||
|
selectionManager.selectWordAt([9, 0]);
|
||||||
|
assert.equal(selectionManager.selectionText, 'gh');
|
||||||
|
selectionManager.selectWordAt([10, 0]);
|
||||||
|
assert.equal(selectionManager.selectionText, 'gh');
|
||||||
|
selectionManager.selectWordAt([11, 0]);
|
||||||
|
assert.equal(selectionManager.selectionText, 'gh}');
|
||||||
|
selectionManager.selectWordAt([12, 0]);
|
||||||
|
assert.equal(selectionManager.selectionText, '\'ij');
|
||||||
|
selectionManager.selectWordAt([13, 0]);
|
||||||
|
assert.equal(selectionManager.selectionText, 'ij');
|
||||||
|
selectionManager.selectWordAt([14, 0]);
|
||||||
|
assert.equal(selectionManager.selectionText, 'ij');
|
||||||
|
selectionManager.selectWordAt([15, 0]);
|
||||||
|
assert.equal(selectionManager.selectionText, 'ij"');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('_selectLineAt', () => {
|
||||||
|
it('should select the entire line', () => {
|
||||||
|
buffer.push(stringToRow('foo bar'));
|
||||||
|
selectionManager.selectLineAt(0);
|
||||||
|
assert.equal(selectionManager.selectionText, 'foo bar', 'The selected text is correct');
|
||||||
|
assert.deepEqual(selectionManager.model.finalSelectionStart, [0, 0]);
|
||||||
|
assert.deepEqual(selectionManager.model.finalSelectionEnd, [terminal.cols, 0], 'The actual selection spans the entire column');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('selectAll', () => {
|
||||||
|
it('should select the entire buffer, beyond the viewport', () => {
|
||||||
|
buffer.push(stringToRow('1'));
|
||||||
|
buffer.push(stringToRow('2'));
|
||||||
|
buffer.push(stringToRow('3'));
|
||||||
|
buffer.push(stringToRow('4'));
|
||||||
|
buffer.push(stringToRow('5'));
|
||||||
|
selectionManager.selectAll();
|
||||||
|
terminal.ybase = buffer.length - terminal.rows;
|
||||||
|
assert.equal(selectionManager.selectionText, '1\n2\n3\n4\n5');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('hasSelection', () => {
|
||||||
|
it('should return whether there is a selection', () => {
|
||||||
|
selectionManager.model.selectionStart = [0, 0];
|
||||||
|
selectionManager.model.selectionStartLength = 0;
|
||||||
|
assert.equal(selectionManager.hasSelection, false);
|
||||||
|
selectionManager.model.selectionEnd = [0, 0];
|
||||||
|
assert.equal(selectionManager.hasSelection, false);
|
||||||
|
selectionManager.model.selectionEnd = [1, 0];
|
||||||
|
assert.equal(selectionManager.hasSelection, true);
|
||||||
|
selectionManager.model.selectionEnd = [0, 1];
|
||||||
|
assert.equal(selectionManager.hasSelection, true);
|
||||||
|
selectionManager.model.selectionEnd = [1, 1];
|
||||||
|
assert.equal(selectionManager.hasSelection, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,718 @@
|
|||||||
|
/**
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as Mouse from './utils/Mouse';
|
||||||
|
import * as Browser from './utils/Browser';
|
||||||
|
import { CharMeasure } from './utils/CharMeasure';
|
||||||
|
import { CircularList } from './utils/CircularList';
|
||||||
|
import { EventEmitter } from './EventEmitter';
|
||||||
|
import { ITerminal } from './Interfaces';
|
||||||
|
import { SelectionModel } from './SelectionModel';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of pixels the mouse needs to be above or below the viewport in
|
||||||
|
* order to scroll at the maximum speed.
|
||||||
|
*/
|
||||||
|
const DRAG_SCROLL_MAX_THRESHOLD = 50;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The maximum scrolling speed
|
||||||
|
*/
|
||||||
|
const DRAG_SCROLL_MAX_SPEED = 15;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of milliseconds between drag scroll updates.
|
||||||
|
*/
|
||||||
|
const DRAG_SCROLL_INTERVAL = 50;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The amount of time before mousedown events are no longer stacked to create
|
||||||
|
* double/triple click events.
|
||||||
|
*/
|
||||||
|
const CLEAR_MOUSE_DOWN_TIME = 400;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of pixels in each direction that the mouse must move before
|
||||||
|
* mousedown events are no longer stacked to create double/triple click events.
|
||||||
|
*/
|
||||||
|
const CLEAR_MOUSE_DISTANCE = 10;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A string containing all characters that are considered word separated by the
|
||||||
|
* double click to select work logic.
|
||||||
|
*/
|
||||||
|
const WORD_SEPARATORS = ' ()[]{}\'"';
|
||||||
|
|
||||||
|
// TODO: Move these constants elsewhere, they belong in a buffer or buffer
|
||||||
|
// data/line class.
|
||||||
|
const LINE_DATA_CHAR_INDEX = 1;
|
||||||
|
const LINE_DATA_WIDTH_INDEX = 2;
|
||||||
|
|
||||||
|
const NON_BREAKING_SPACE_CHAR = String.fromCharCode(160);
|
||||||
|
const ALL_NON_BREAKING_SPACE_REGEX = new RegExp(NON_BREAKING_SPACE_CHAR, 'g');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a position of a word on a line.
|
||||||
|
*/
|
||||||
|
interface IWordPosition {
|
||||||
|
start: number;
|
||||||
|
length: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A selection mode, this drives how the selection behaves on mouse move.
|
||||||
|
*/
|
||||||
|
enum SelectionMode {
|
||||||
|
NORMAL,
|
||||||
|
WORD,
|
||||||
|
LINE
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class that manages the selection of the terminal. With help from
|
||||||
|
* SelectionModel, SelectionManager handles with all logic associated with
|
||||||
|
* dealing with the selection, including handling mouse interaction, wide
|
||||||
|
* characters and fetching the actual text within the selection. Rendering is
|
||||||
|
* not handled by the SelectionManager but a 'refresh' event is fired when the
|
||||||
|
* selection is ready to be redrawn.
|
||||||
|
*/
|
||||||
|
export class SelectionManager extends EventEmitter {
|
||||||
|
protected _model: SelectionModel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The amount to scroll every drag scroll update (depends on how far the mouse
|
||||||
|
* drag is above or below the terminal).
|
||||||
|
*/
|
||||||
|
private _dragScrollAmount: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The last time the mousedown event fired, this is used to track double and
|
||||||
|
* triple clicks.
|
||||||
|
*/
|
||||||
|
private _lastMouseDownTime: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The last position the mouse was clicked [x, y].
|
||||||
|
*/
|
||||||
|
private _lastMousePosition: [number, number];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of clicks of the mousedown event. This is used to keep track of
|
||||||
|
* double and triple clicks.
|
||||||
|
*/
|
||||||
|
private _clickCount: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current selection mode.
|
||||||
|
*/
|
||||||
|
private _activeSelectionMode: SelectionMode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A setInterval timer that is active while the mouse is down whose callback
|
||||||
|
* scrolls the viewport when necessary.
|
||||||
|
*/
|
||||||
|
private _dragScrollIntervalTimer: NodeJS.Timer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The animation frame ID used for refreshing the selection.
|
||||||
|
*/
|
||||||
|
private _refreshAnimationFrame: number;
|
||||||
|
|
||||||
|
private _bufferTrimListener: any;
|
||||||
|
private _mouseMoveListener: EventListener;
|
||||||
|
private _mouseDownListener: EventListener;
|
||||||
|
private _mouseUpListener: EventListener;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private _terminal: ITerminal,
|
||||||
|
private _buffer: CircularList<any>,
|
||||||
|
private _rowContainer: HTMLElement,
|
||||||
|
private _charMeasure: CharMeasure
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
this._initListeners();
|
||||||
|
this.enable();
|
||||||
|
|
||||||
|
this._model = new SelectionModel(_terminal);
|
||||||
|
this._lastMouseDownTime = 0;
|
||||||
|
this._activeSelectionMode = SelectionMode.NORMAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes listener variables.
|
||||||
|
*/
|
||||||
|
private _initListeners() {
|
||||||
|
this._bufferTrimListener = (amount: number) => this._onTrim(amount);
|
||||||
|
this._mouseMoveListener = event => this._onMouseMove(<MouseEvent>event);
|
||||||
|
this._mouseDownListener = event => this._onMouseDown(<MouseEvent>event);
|
||||||
|
this._mouseUpListener = event => this._onMouseUp(<MouseEvent>event);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disables the selection manager. This is useful for when terminal mouse
|
||||||
|
* are enabled.
|
||||||
|
*/
|
||||||
|
public disable() {
|
||||||
|
this.clearSelection();
|
||||||
|
this._buffer.off('trim', this._bufferTrimListener);
|
||||||
|
this._rowContainer.removeEventListener('mousedown', this._mouseDownListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable the selection manager.
|
||||||
|
*/
|
||||||
|
public enable() {
|
||||||
|
// Only adjust the selection on trim, shiftElements is rarely used (only in
|
||||||
|
// reverseIndex) and delete in a splice is only ever used when the same
|
||||||
|
// number of elements was just added. Given this is could actually be
|
||||||
|
// beneficial to leave the selection as is for these cases.
|
||||||
|
this._buffer.on('trim', this._bufferTrimListener);
|
||||||
|
this._rowContainer.addEventListener('mousedown', this._mouseDownListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the active buffer, this should be called when the alt buffer is
|
||||||
|
* switched in or out.
|
||||||
|
* @param buffer The active buffer.
|
||||||
|
*/
|
||||||
|
public setBuffer(buffer: CircularList<any>): void {
|
||||||
|
this._buffer = buffer;
|
||||||
|
this.clearSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets whether there is an active text selection.
|
||||||
|
*/
|
||||||
|
public get hasSelection(): boolean {
|
||||||
|
const start = this._model.finalSelectionStart;
|
||||||
|
const end = this._model.finalSelectionEnd;
|
||||||
|
if (!start || !end) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return start[0] !== end[0] || start[1] !== end[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the text currently selected.
|
||||||
|
*/
|
||||||
|
public get selectionText(): string {
|
||||||
|
const start = this._model.finalSelectionStart;
|
||||||
|
const end = this._model.finalSelectionEnd;
|
||||||
|
if (!start || !end) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get first row
|
||||||
|
const startRowEndCol = start[1] === end[1] ? end[0] : null;
|
||||||
|
let result: string[] = [];
|
||||||
|
result.push(this._translateBufferLineToString(this._buffer.get(start[1]), true, start[0], startRowEndCol));
|
||||||
|
|
||||||
|
// Get middle rows
|
||||||
|
for (let i = start[1] + 1; i <= end[1] - 1; i++) {
|
||||||
|
const bufferLine = this._buffer.get(i);
|
||||||
|
const lineText = this._translateBufferLineToString(bufferLine, 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.get(end[1]);
|
||||||
|
const lineText = this._translateBufferLineToString(bufferLine, true, 0, end[0]);
|
||||||
|
if (bufferLine.isWrapped) {
|
||||||
|
result[result.length - 1] += lineText;
|
||||||
|
} else {
|
||||||
|
result.push(lineText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format string by replacing non-breaking space chars with regular spaces
|
||||||
|
// and joining the array into a multi-line string.
|
||||||
|
const formattedResult = result.map(line => {
|
||||||
|
return line.replace(ALL_NON_BREAKING_SPACE_REGEX, ' ');
|
||||||
|
}).join(Browser.isMSWindows ? '\r\n' : '\n');
|
||||||
|
|
||||||
|
return formattedResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the current terminal selection.
|
||||||
|
*/
|
||||||
|
public clearSelection(): void {
|
||||||
|
this._model.clearSelection();
|
||||||
|
this._removeMouseDownListeners();
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
* 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.
|
||||||
|
* @param startCol The column to start at.
|
||||||
|
* @param endCol The column to end at.
|
||||||
|
*/
|
||||||
|
private _translateBufferLineToString(line: any, trimRight: boolean, startCol: number = 0, endCol: number = null): string {
|
||||||
|
// TODO: This function should live in a buffer or buffer line class
|
||||||
|
|
||||||
|
// Get full line
|
||||||
|
let lineString = '';
|
||||||
|
let widthAdjustedStartCol = startCol;
|
||||||
|
let widthAdjustedEndCol = endCol;
|
||||||
|
for (let i = 0; i < line.length; i++) {
|
||||||
|
const char = line[i];
|
||||||
|
lineString += char[LINE_DATA_CHAR_INDEX];
|
||||||
|
// Adjust start and end cols for wide characters if they affect their
|
||||||
|
// column indexes
|
||||||
|
if (char[LINE_DATA_WIDTH_INDEX] === 0) {
|
||||||
|
if (startCol >= i) {
|
||||||
|
widthAdjustedStartCol--;
|
||||||
|
}
|
||||||
|
if (endCol >= i) {
|
||||||
|
widthAdjustedEndCol--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the final end col by trimming whitespace on the right of the
|
||||||
|
// line if needed.
|
||||||
|
let finalEndCol = widthAdjustedEndCol || line.length;
|
||||||
|
if (trimRight) {
|
||||||
|
const rightWhitespaceIndex = lineString.search(/\s+$/);
|
||||||
|
if (rightWhitespaceIndex !== -1) {
|
||||||
|
finalEndCol = Math.min(finalEndCol, rightWhitespaceIndex);
|
||||||
|
}
|
||||||
|
// Return the empty string if only trimmed whitespace is selected
|
||||||
|
if (finalEndCol <= widthAdjustedStartCol) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lineString.substring(widthAdjustedStartCol, finalEndCol);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queues a refresh, redrawing the selection on the next opportunity.
|
||||||
|
* @param isNewSelection Whether the selection should be registered as a new
|
||||||
|
* selection on Linux.
|
||||||
|
*/
|
||||||
|
public refresh(isNewSelection?: boolean): void {
|
||||||
|
// Queue the refresh for the renderer
|
||||||
|
if (!this._refreshAnimationFrame) {
|
||||||
|
this._refreshAnimationFrame = window.requestAnimationFrame(() => this._refresh());
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the platform is Linux and the refresh call comes from a mouse event,
|
||||||
|
// we need to update the selection for middle click to paste selection.
|
||||||
|
if (Browser.isLinux && isNewSelection) {
|
||||||
|
const selectionText = this.selectionText;
|
||||||
|
if (selectionText.length) {
|
||||||
|
this.emit('newselection', this.selectionText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires the refresh event, causing consumers to pick it up and redraw the
|
||||||
|
* selection state.
|
||||||
|
*/
|
||||||
|
private _refresh(): void {
|
||||||
|
this._refreshAnimationFrame = null;
|
||||||
|
this.emit('refresh', { start: this._model.finalSelectionStart, end: this._model.finalSelectionEnd });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selects all text within the terminal.
|
||||||
|
*/
|
||||||
|
public selectAll(): void {
|
||||||
|
this._model.isSelectAllActive = true;
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the buffer being trimmed, adjust the selection position.
|
||||||
|
* @param amount The amount the buffer is being trimmed.
|
||||||
|
*/
|
||||||
|
private _onTrim(amount: number) {
|
||||||
|
const needsRefresh = this._model.onTrim(amount);
|
||||||
|
if (needsRefresh) {
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the 0-based [x, y] buffer coordinates of the current mouse event.
|
||||||
|
* @param event The mouse event.
|
||||||
|
*/
|
||||||
|
private _getMouseBufferCoords(event: MouseEvent): [number, number] {
|
||||||
|
const coords = Mouse.getCoords(event, this._rowContainer, this._charMeasure, this._terminal.cols, this._terminal.rows, true);
|
||||||
|
// Convert to 0-based
|
||||||
|
coords[0]--;
|
||||||
|
coords[1]--;
|
||||||
|
// Convert viewport coords to buffer coords
|
||||||
|
coords[1] += this._terminal.ydisp;
|
||||||
|
return coords;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the amount the viewport should be scrolled based on how far out of the
|
||||||
|
* terminal the mouse is.
|
||||||
|
* @param event The mouse event.
|
||||||
|
*/
|
||||||
|
private _getMouseEventScrollAmount(event: MouseEvent): number {
|
||||||
|
let offset = Mouse.getCoordsRelativeToElement(event, this._rowContainer)[1];
|
||||||
|
const terminalHeight = this._terminal.rows * this._charMeasure.height;
|
||||||
|
if (offset >= 0 && offset <= terminalHeight) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (offset > terminalHeight) {
|
||||||
|
offset -= terminalHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
offset = Math.min(Math.max(offset, -DRAG_SCROLL_MAX_THRESHOLD), DRAG_SCROLL_MAX_THRESHOLD);
|
||||||
|
offset /= DRAG_SCROLL_MAX_THRESHOLD;
|
||||||
|
return (offset / Math.abs(offset)) + Math.round(offset * (DRAG_SCROLL_MAX_SPEED - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles te mousedown event, setting up for a new selection.
|
||||||
|
* @param event The mousedown event.
|
||||||
|
*/
|
||||||
|
private _onMouseDown(event: MouseEvent) {
|
||||||
|
// Only action the primary button
|
||||||
|
if (event.button !== 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tell the browser not to start a regular selection
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
// Reset drag scroll state
|
||||||
|
this._dragScrollAmount = 0;
|
||||||
|
|
||||||
|
this._setMouseClickCount(event);
|
||||||
|
|
||||||
|
if (event.shiftKey) {
|
||||||
|
this._onShiftClick(event);
|
||||||
|
} else {
|
||||||
|
if (this._clickCount === 1) {
|
||||||
|
this._onSingleClick(event);
|
||||||
|
} else if (this._clickCount === 2) {
|
||||||
|
this._onDoubleClick(event);
|
||||||
|
} else if (this._clickCount === 3) {
|
||||||
|
this._onTripleClick(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._addMouseDownListeners();
|
||||||
|
this.refresh(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds listeners when mousedown is triggered.
|
||||||
|
*/
|
||||||
|
private _addMouseDownListeners(): void {
|
||||||
|
// Listen on the document so that dragging outside of viewport works
|
||||||
|
this._rowContainer.ownerDocument.addEventListener('mousemove', this._mouseMoveListener);
|
||||||
|
this._rowContainer.ownerDocument.addEventListener('mouseup', this._mouseUpListener);
|
||||||
|
this._dragScrollIntervalTimer = setInterval(() => this._dragScroll(), DRAG_SCROLL_INTERVAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the listeners that are registered when mousedown is triggered.
|
||||||
|
*/
|
||||||
|
private _removeMouseDownListeners(): void {
|
||||||
|
this._rowContainer.ownerDocument.removeEventListener('mousemove', this._mouseMoveListener);
|
||||||
|
this._rowContainer.ownerDocument.removeEventListener('mouseup', this._mouseUpListener);
|
||||||
|
clearInterval(this._dragScrollIntervalTimer);
|
||||||
|
this._dragScrollIntervalTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs a shift click, setting the selection end position to the mouse
|
||||||
|
* position.
|
||||||
|
* @param event The mouse event.
|
||||||
|
*/
|
||||||
|
private _onShiftClick(event: MouseEvent): void {
|
||||||
|
if (this._model.selectionStart) {
|
||||||
|
this._model.selectionEnd = this._getMouseBufferCoords(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs a single click, resetting relevant state and setting the selection
|
||||||
|
* start position.
|
||||||
|
* @param event The mouse event.
|
||||||
|
*/
|
||||||
|
private _onSingleClick(event: MouseEvent): void {
|
||||||
|
this._model.selectionStartLength = 0;
|
||||||
|
this._model.isSelectAllActive = false;
|
||||||
|
this._activeSelectionMode = SelectionMode.NORMAL;
|
||||||
|
this._model.selectionStart = this._getMouseBufferCoords(event);
|
||||||
|
if (this._model.selectionStart) {
|
||||||
|
this._model.selectionEnd = null;
|
||||||
|
// If the mouse is over the second half of a wide character, adjust the
|
||||||
|
// selection to cover the whole character
|
||||||
|
const char = this._buffer.get(this._model.selectionStart[1])[this._model.selectionStart[0]];
|
||||||
|
if (char[LINE_DATA_WIDTH_INDEX] === 0) {
|
||||||
|
this._model.selectionStart[0]++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs a double click, selecting the current work.
|
||||||
|
* @param event The mouse event.
|
||||||
|
*/
|
||||||
|
private _onDoubleClick(event: MouseEvent): void {
|
||||||
|
const coords = this._getMouseBufferCoords(event);
|
||||||
|
if (coords) {
|
||||||
|
this._activeSelectionMode = SelectionMode.WORD;
|
||||||
|
this._selectWordAt(coords);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs a triple click, selecting the current line and activating line
|
||||||
|
* select mode.
|
||||||
|
* @param event The mouse event.
|
||||||
|
*/
|
||||||
|
private _onTripleClick(event: MouseEvent): void {
|
||||||
|
const coords = this._getMouseBufferCoords(event);
|
||||||
|
if (coords) {
|
||||||
|
this._activeSelectionMode = SelectionMode.LINE;
|
||||||
|
this._selectLineAt(coords[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the number of clicks for the current mousedown event based on the time
|
||||||
|
* and position of the last mousedown event.
|
||||||
|
* @param event The mouse event.
|
||||||
|
*/
|
||||||
|
private _setMouseClickCount(event: MouseEvent): void {
|
||||||
|
let currentTime = (new Date()).getTime();
|
||||||
|
if (currentTime - this._lastMouseDownTime > CLEAR_MOUSE_DOWN_TIME || this._distanceFromLastMousePosition(event) > CLEAR_MOUSE_DISTANCE) {
|
||||||
|
this._clickCount = 0;
|
||||||
|
}
|
||||||
|
this._lastMouseDownTime = currentTime;
|
||||||
|
this._lastMousePosition = [event.pageX, event.pageY];
|
||||||
|
this._clickCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the maximum number of pixels in each direction the mouse has moved.
|
||||||
|
* @param event The mouse event.
|
||||||
|
*/
|
||||||
|
private _distanceFromLastMousePosition(event: MouseEvent): number {
|
||||||
|
const result = Math.max(
|
||||||
|
Math.abs(this._lastMousePosition[0] - event.pageX),
|
||||||
|
Math.abs(this._lastMousePosition[1] - event.pageY));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the mousemove event when the mouse button is down, recording the
|
||||||
|
* end of the selection and refreshing the selection.
|
||||||
|
* @param event The mousemove event.
|
||||||
|
*/
|
||||||
|
private _onMouseMove(event: MouseEvent) {
|
||||||
|
// Record the previous position so we know whether to redraw the selection
|
||||||
|
// at the end.
|
||||||
|
const previousSelectionEnd = this._model.selectionEnd ? [this._model.selectionEnd[0], this._model.selectionEnd[1]] : null;
|
||||||
|
|
||||||
|
// Set the initial selection end based on the mouse coordinates
|
||||||
|
this._model.selectionEnd = this._getMouseBufferCoords(event);
|
||||||
|
|
||||||
|
// Select the entire line if line select mode is active.
|
||||||
|
if (this._activeSelectionMode === SelectionMode.LINE) {
|
||||||
|
if (this._model.selectionEnd[1] < this._model.selectionStart[1]) {
|
||||||
|
this._model.selectionEnd[0] = 0;
|
||||||
|
} else {
|
||||||
|
this._model.selectionEnd[0] = this._terminal.cols;
|
||||||
|
}
|
||||||
|
} else if (this._activeSelectionMode === SelectionMode.WORD) {
|
||||||
|
this._selectToWordAt(this._model.selectionEnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the amount of scrolling that will happen.
|
||||||
|
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 - 1;
|
||||||
|
} 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.length) {
|
||||||
|
const char = this._buffer.get(this._model.selectionEnd[1])[this._model.selectionEnd[0]];
|
||||||
|
if (char && char[2] === 0) {
|
||||||
|
this._model.selectionEnd[0]++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only draw here if the selection changes.
|
||||||
|
if (!previousSelectionEnd ||
|
||||||
|
previousSelectionEnd[0] !== this._model.selectionEnd[0] ||
|
||||||
|
previousSelectionEnd[1] !== this._model.selectionEnd[1]) {
|
||||||
|
this.refresh(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The callback that occurs every DRAG_SCROLL_INTERVAL ms that does the
|
||||||
|
* scrolling of the viewport.
|
||||||
|
*/
|
||||||
|
private _dragScroll() {
|
||||||
|
if (this._dragScrollAmount) {
|
||||||
|
this._terminal.scrollDisp(this._dragScrollAmount, false);
|
||||||
|
// Re-evaluate selection
|
||||||
|
if (this._dragScrollAmount > 0) {
|
||||||
|
this._model.selectionEnd = [this._terminal.cols - 1, this._terminal.ydisp + this._terminal.rows];
|
||||||
|
} else {
|
||||||
|
this._model.selectionEnd = [0, this._terminal.ydisp];
|
||||||
|
}
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the mouseup event, removing the mousedown listeners.
|
||||||
|
* @param event The mouseup event.
|
||||||
|
*/
|
||||||
|
private _onMouseUp(event: MouseEvent) {
|
||||||
|
this._removeMouseDownListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a viewport column to the character index on the buffer line, the
|
||||||
|
* latter takes into account wide characters.
|
||||||
|
* @param coords The coordinates to find the 2 index for.
|
||||||
|
*/
|
||||||
|
private _convertViewportColToCharacterIndex(bufferLine: any, coords: [number, number]): number {
|
||||||
|
let charIndex = coords[0];
|
||||||
|
for (let i = 0; coords[0] >= i; i++) {
|
||||||
|
const char = bufferLine[i];
|
||||||
|
if (char[LINE_DATA_WIDTH_INDEX] === 0) {
|
||||||
|
charIndex--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return charIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets positional information for the word at the coordinated specified.
|
||||||
|
* @param coords The coordinates to get the word at.
|
||||||
|
*/
|
||||||
|
private _getWordAt(coords: [number, number]): IWordPosition {
|
||||||
|
const bufferLine = this._buffer.get(coords[1]);
|
||||||
|
const line = this._translateBufferLineToString(bufferLine, false);
|
||||||
|
|
||||||
|
// Get actual index, taking into consideration wide characters
|
||||||
|
let endIndex = this._convertViewportColToCharacterIndex(bufferLine, coords);
|
||||||
|
let startIndex = endIndex;
|
||||||
|
|
||||||
|
// Record offset to be used later
|
||||||
|
const charOffset = coords[0] - startIndex;
|
||||||
|
let leftWideCharCount = 0;
|
||||||
|
let rightWideCharCount = 0;
|
||||||
|
|
||||||
|
if (line.charAt(startIndex) === ' ') {
|
||||||
|
// Expand until non-whitespace is hit
|
||||||
|
while (startIndex > 0 && line.charAt(startIndex - 1) === ' ') {
|
||||||
|
startIndex--;
|
||||||
|
}
|
||||||
|
while (endIndex < line.length && line.charAt(endIndex + 1) === ' ') {
|
||||||
|
endIndex++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Expand until whitespace is hit. This algorithm works by scanning left
|
||||||
|
// and right from the starting position, keeping both the index format
|
||||||
|
// (line) and the column format (bufferLine) in sync. When a wide
|
||||||
|
// character is hit, it is recorded and the column index is adjusted.
|
||||||
|
let startCol = coords[0];
|
||||||
|
let endCol = coords[0];
|
||||||
|
// Consider the initial position, skip it and increment the wide char
|
||||||
|
// variable
|
||||||
|
if (bufferLine[startCol][LINE_DATA_WIDTH_INDEX] === 0) {
|
||||||
|
leftWideCharCount++;
|
||||||
|
startCol--;
|
||||||
|
}
|
||||||
|
if (bufferLine[endCol][LINE_DATA_WIDTH_INDEX] === 2) {
|
||||||
|
rightWideCharCount++;
|
||||||
|
endCol++;
|
||||||
|
}
|
||||||
|
// Expand the string in both directions until a space is hit
|
||||||
|
while (startIndex > 0 && !this._isCharWordSeparator(line.charAt(startIndex - 1))) {
|
||||||
|
if (bufferLine[startCol - 1][LINE_DATA_WIDTH_INDEX] === 0) {
|
||||||
|
// If the next character is a wide char, record it and skip the column
|
||||||
|
leftWideCharCount++;
|
||||||
|
startCol--;
|
||||||
|
}
|
||||||
|
startIndex--;
|
||||||
|
startCol--;
|
||||||
|
}
|
||||||
|
while (endIndex + 1 < line.length && !this._isCharWordSeparator(line.charAt(endIndex + 1))) {
|
||||||
|
if (bufferLine[endCol + 1][LINE_DATA_WIDTH_INDEX] === 2) {
|
||||||
|
// If the next character is a wide char, record it and skip the column
|
||||||
|
rightWideCharCount++;
|
||||||
|
endCol++;
|
||||||
|
}
|
||||||
|
endIndex++;
|
||||||
|
endCol++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = startIndex + charOffset - leftWideCharCount;
|
||||||
|
const length = Math.min(endIndex - startIndex + leftWideCharCount + rightWideCharCount + 1/*include endIndex char*/, this._terminal.cols);
|
||||||
|
return {start, length};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selects the word at the coordinates specified.
|
||||||
|
* @param coords The coordinates to get the word at.
|
||||||
|
*/
|
||||||
|
protected _selectWordAt(coords: [number, number]): void {
|
||||||
|
const wordPosition = this._getWordAt(coords);
|
||||||
|
this._model.selectionStart = [wordPosition.start, coords[1]];
|
||||||
|
this._model.selectionStartLength = wordPosition.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the selection end to the word at the coordinated specified.
|
||||||
|
* @param coords The coordinates to get the word at.
|
||||||
|
*/
|
||||||
|
private _selectToWordAt(coords: [number, number]): void {
|
||||||
|
const wordPosition = this._getWordAt(coords);
|
||||||
|
this._model.selectionEnd = [this._model.areSelectionValuesReversed() ? wordPosition.start : (wordPosition.start + wordPosition.length), coords[1]];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets whether the character is considered a word separator by the select
|
||||||
|
* word logic.
|
||||||
|
* @param char The character to check.
|
||||||
|
*/
|
||||||
|
private _isCharWordSeparator(char: string): boolean {
|
||||||
|
return WORD_SEPARATORS.indexOf(char) >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selects the line specified.
|
||||||
|
* @param line The line index.
|
||||||
|
*/
|
||||||
|
protected _selectLineAt(line: number): void {
|
||||||
|
this._model.selectionStart = [0, line];
|
||||||
|
this._model.selectionStartLength = this._terminal.cols;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
/**
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
import { assert } from 'chai';
|
||||||
|
import { ITerminal } from './Interfaces';
|
||||||
|
import { SelectionModel } from './SelectionModel';
|
||||||
|
|
||||||
|
class TestSelectionModel extends SelectionModel {
|
||||||
|
constructor(
|
||||||
|
terminal: ITerminal
|
||||||
|
) {
|
||||||
|
super(terminal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('SelectionManager', () => {
|
||||||
|
let window: Window;
|
||||||
|
let document: Document;
|
||||||
|
|
||||||
|
let terminal: ITerminal;
|
||||||
|
let model: TestSelectionModel;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
terminal = <any>{ cols: 80, rows: 2, ybase: 0 };
|
||||||
|
model = new TestSelectionModel(terminal);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('clearSelection', () => {
|
||||||
|
it('should clear the final selection', () => {
|
||||||
|
model.selectionStart = [0, 0];
|
||||||
|
model.selectionEnd = [10, 2];
|
||||||
|
assert.deepEqual(model.finalSelectionStart, [0, 0]);
|
||||||
|
assert.deepEqual(model.finalSelectionEnd, [10, 2]);
|
||||||
|
model.clearSelection();
|
||||||
|
assert.deepEqual(model.finalSelectionStart, null);
|
||||||
|
assert.deepEqual(model.finalSelectionEnd, null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('areSelectionValuesReversed', () => {
|
||||||
|
it('should return true when the selection end is before selection start', () => {
|
||||||
|
model.selectionStart = [1, 0];
|
||||||
|
model.selectionEnd = [0, 0];
|
||||||
|
assert.equal(model.areSelectionValuesReversed(), true);
|
||||||
|
model.selectionStart = [10, 2];
|
||||||
|
model.selectionEnd = [0, 0];
|
||||||
|
assert.equal(model.areSelectionValuesReversed(), true);
|
||||||
|
});
|
||||||
|
it('should return false when the selection end is after selection start', () => {
|
||||||
|
model.selectionStart = [0, 0];
|
||||||
|
model.selectionEnd = [1, 0];
|
||||||
|
assert.equal(model.areSelectionValuesReversed(), false);
|
||||||
|
model.selectionStart = [0, 0];
|
||||||
|
model.selectionEnd = [10, 2];
|
||||||
|
assert.equal(model.areSelectionValuesReversed(), false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onTrim', () => {
|
||||||
|
it('should trim a portion of the selection when a part of it is trimmed', () => {
|
||||||
|
model.selectionStart = [0, 0];
|
||||||
|
model.selectionEnd = [10, 2];
|
||||||
|
model.onTrim(1);
|
||||||
|
assert.deepEqual(model.finalSelectionStart, [0, 0]);
|
||||||
|
assert.deepEqual(model.finalSelectionEnd, [10, 1]);
|
||||||
|
model.onTrim(1);
|
||||||
|
assert.deepEqual(model.finalSelectionStart, [0, 0]);
|
||||||
|
assert.deepEqual(model.finalSelectionEnd, [10, 0]);
|
||||||
|
});
|
||||||
|
it('should clear selection when it is trimmed in its entirety', () => {
|
||||||
|
model.selectionStart = [0, 0];
|
||||||
|
model.selectionEnd = [10, 0];
|
||||||
|
model.onTrim(1);
|
||||||
|
assert.deepEqual(model.finalSelectionStart, null);
|
||||||
|
assert.deepEqual(model.finalSelectionEnd, null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('finalSelectionStart', () => {
|
||||||
|
it('should return the start of the buffer if select all is active', () => {
|
||||||
|
model.isSelectAllActive = true;
|
||||||
|
assert.deepEqual(model.finalSelectionStart, [0, 0]);
|
||||||
|
});
|
||||||
|
it('should return selection start if there is no selection end', () => {
|
||||||
|
model.selectionStart = [2, 2];
|
||||||
|
assert.deepEqual(model.finalSelectionStart, [2, 2]);
|
||||||
|
});
|
||||||
|
it('should return selection end if values are reversed', () => {
|
||||||
|
model.selectionStart = [2, 2];
|
||||||
|
model.selectionEnd = [3, 2];
|
||||||
|
assert.deepEqual(model.finalSelectionStart, [2, 2]);
|
||||||
|
model.selectionEnd = [1, 2];
|
||||||
|
assert.deepEqual(model.finalSelectionStart, [1, 2]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('finalSelectionEnd', () => {
|
||||||
|
it('should return the end of the buffer if select all is active', () => {
|
||||||
|
model.isSelectAllActive = true;
|
||||||
|
assert.deepEqual(model.finalSelectionEnd, [80, 1]);
|
||||||
|
});
|
||||||
|
it('should return null if there is no selection start', () => {
|
||||||
|
assert.equal(model.finalSelectionEnd, null);
|
||||||
|
model.selectionEnd = [1, 2];
|
||||||
|
assert.equal(model.finalSelectionEnd, null);
|
||||||
|
});
|
||||||
|
it('should return selection start + length if there is no selection end', () => {
|
||||||
|
model.selectionStart = [2, 2];
|
||||||
|
model.selectionStartLength = 2;
|
||||||
|
assert.deepEqual(model.finalSelectionEnd, [4, 2]);
|
||||||
|
});
|
||||||
|
it('should return selection start + length if values are reversed', () => {
|
||||||
|
model.selectionStart = [2, 2];
|
||||||
|
model.selectionStartLength = 2;
|
||||||
|
model.selectionEnd = [2, 1];
|
||||||
|
assert.deepEqual(model.finalSelectionEnd, [4, 2]);
|
||||||
|
});
|
||||||
|
it('should return selection start + length if selection end is inside the start selection', () => {
|
||||||
|
model.selectionStart = [2, 2];
|
||||||
|
model.selectionStartLength = 2;
|
||||||
|
model.selectionEnd = [3, 2];
|
||||||
|
assert.deepEqual(model.finalSelectionEnd, [4, 2]);
|
||||||
|
});
|
||||||
|
it('should return selection end if selection end is after selection start + length', () => {
|
||||||
|
model.selectionStart = [2, 2];
|
||||||
|
model.selectionStartLength = 2;
|
||||||
|
model.selectionEnd = [5, 2];
|
||||||
|
assert.deepEqual(model.finalSelectionEnd, [5, 2]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
/**
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ITerminal } from './Interfaces';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a selection within the buffer. This model only cares about column
|
||||||
|
* and row coordinates, not wide characters.
|
||||||
|
*/
|
||||||
|
export class SelectionModel {
|
||||||
|
/**
|
||||||
|
* Whether select all is currently active.
|
||||||
|
*/
|
||||||
|
public isSelectAllActive: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The [x, y] position the selection starts at.
|
||||||
|
*/
|
||||||
|
public selectionStart: [number, number];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The minimal length of the selection from the start position. When double
|
||||||
|
* clicking on a word, the word will be selected which makes the selection
|
||||||
|
* start at the start of the word and makes this variable the length.
|
||||||
|
*/
|
||||||
|
public selectionStartLength: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The [x, y] position the selection ends at.
|
||||||
|
*/
|
||||||
|
public selectionEnd: [number, number];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private _terminal: ITerminal
|
||||||
|
) {
|
||||||
|
this.clearSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the current selection.
|
||||||
|
*/
|
||||||
|
public clearSelection(): void {
|
||||||
|
this.selectionStart = null;
|
||||||
|
this.selectionEnd = null;
|
||||||
|
this.isSelectAllActive = false;
|
||||||
|
this.selectionStartLength = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The final selection start, taking into consideration select all.
|
||||||
|
*/
|
||||||
|
public get finalSelectionStart(): [number, number] {
|
||||||
|
if (this.isSelectAllActive) {
|
||||||
|
return [0, 0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.selectionEnd || !this.selectionStart) {
|
||||||
|
return this.selectionStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.areSelectionValuesReversed() ? this.selectionEnd : this.selectionStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The final selection end, taking into consideration select all, double click
|
||||||
|
* word selection and triple click line selection.
|
||||||
|
*/
|
||||||
|
public get finalSelectionEnd(): [number, number] {
|
||||||
|
if (this.isSelectAllActive) {
|
||||||
|
return [this._terminal.cols, this._terminal.ybase + this._terminal.rows - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.selectionStart) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the selection start if the end doesn't exist or they're reversed
|
||||||
|
if (!this.selectionEnd || this.areSelectionValuesReversed()) {
|
||||||
|
return [this.selectionStart[0] + this.selectionStartLength, this.selectionStart[1]];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the the word/line is selected after a double/triple click
|
||||||
|
if (this.selectionStartLength) {
|
||||||
|
// Select the larger of the two when start and end are on the same line
|
||||||
|
if (this.selectionEnd[1] === this.selectionStart[1]) {
|
||||||
|
return [Math.max(this.selectionStart[0] + this.selectionStartLength, this.selectionEnd[0]), this.selectionEnd[1]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.selectionEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the selection start and end are reversed.
|
||||||
|
*/
|
||||||
|
public areSelectionValuesReversed(): boolean {
|
||||||
|
const start = this.selectionStart;
|
||||||
|
const end = this.selectionEnd;
|
||||||
|
return start[1] > end[1] || (start[1] === end[1] && start[0] > end[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the buffer being trimmed, adjust the selection position.
|
||||||
|
* @param amount The amount the buffer is being trimmed.
|
||||||
|
* @return Whether a refresh is necessary.
|
||||||
|
*/
|
||||||
|
public onTrim(amount: number): boolean {
|
||||||
|
// Adjust the selection position based on the trimmed amount.
|
||||||
|
if (this.selectionStart) {
|
||||||
|
this.selectionStart[1] -= amount;
|
||||||
|
}
|
||||||
|
if (this.selectionEnd) {
|
||||||
|
this.selectionEnd[1] -= amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The selection has moved off the buffer, clear it.
|
||||||
|
if (this.selectionEnd && this.selectionEnd[1] < 0) {
|
||||||
|
this.clearSelection();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the selection start is trimmed, ensure the start column is 0.
|
||||||
|
if (this.selectionStart && this.selectionStart[1] < 0) {
|
||||||
|
this.selectionStart[1] = 0;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type LinkMatcher = {
|
||||||
|
id: number,
|
||||||
|
regex: RegExp,
|
||||||
|
handler: LinkMatcherHandler,
|
||||||
|
matchIndex?: number,
|
||||||
|
validationCallback?: LinkMatcherValidationCallback,
|
||||||
|
priority?: number
|
||||||
|
};
|
||||||
|
export type LinkMatcherHandler = (event: MouseEvent, uri: string) => boolean | void;
|
||||||
|
export type LinkMatcherValidationCallback = (uri: string, element: HTMLElement, callback: (isValid: boolean) => void) => void;
|
||||||
+41
-33
@@ -2,11 +2,12 @@ import { assert } from 'chai';
|
|||||||
import { Viewport } from './Viewport';
|
import { Viewport } from './Viewport';
|
||||||
|
|
||||||
describe('Viewport', () => {
|
describe('Viewport', () => {
|
||||||
var terminal;
|
let terminal;
|
||||||
var viewportElement;
|
let viewportElement;
|
||||||
var charMeasureElement;
|
let selectionContainer;
|
||||||
var viewport;
|
let charMeasure;
|
||||||
var scrollAreaElement;
|
let viewport;
|
||||||
|
let scrollAreaElement;
|
||||||
|
|
||||||
const CHARACTER_HEIGHT = 10;
|
const CHARACTER_HEIGHT = 10;
|
||||||
|
|
||||||
@@ -20,6 +21,11 @@ describe('Viewport', () => {
|
|||||||
style: {
|
style: {
|
||||||
lineHeight: 0
|
lineHeight: 0
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
selectionContainer: {
|
||||||
|
style: {
|
||||||
|
height: 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
viewportElement = {
|
viewportElement = {
|
||||||
@@ -34,24 +40,24 @@ describe('Viewport', () => {
|
|||||||
height: 0
|
height: 0
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
charMeasureElement = {
|
charMeasure = {
|
||||||
getBoundingClientRect: () => {
|
height: CHARACTER_HEIGHT
|
||||||
return { width: null, height: CHARACTER_HEIGHT };
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
viewport = new Viewport(terminal, viewportElement, scrollAreaElement, charMeasureElement);
|
viewport = new Viewport(terminal, viewportElement, scrollAreaElement, charMeasure);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('refresh', () => {
|
describe('refresh', () => {
|
||||||
it('should set the line-height of the terminal', () => {
|
it('should set the line-height of the terminal', done => {
|
||||||
assert.equal(viewportElement.style.lineHeight, CHARACTER_HEIGHT + 'px');
|
// Allow CharMeasure to be initialized
|
||||||
assert.equal(terminal.rowContainer.style.lineHeight, CHARACTER_HEIGHT + 'px');
|
setTimeout(() => {
|
||||||
charMeasureElement.getBoundingClientRect = () => {
|
assert.equal(viewportElement.style.lineHeight, CHARACTER_HEIGHT + 'px');
|
||||||
return { width: null, height: 1 };
|
assert.equal(terminal.rowContainer.style.lineHeight, CHARACTER_HEIGHT + 'px');
|
||||||
};
|
charMeasure.height = 1;
|
||||||
viewport.refresh();
|
viewport.refresh();
|
||||||
assert.equal(viewportElement.style.lineHeight, '1px');
|
assert.equal(viewportElement.style.lineHeight, '1px');
|
||||||
assert.equal(terminal.rowContainer.style.lineHeight, '1px');
|
assert.equal(terminal.rowContainer.style.lineHeight, '1px');
|
||||||
|
done();
|
||||||
|
}, 0);
|
||||||
});
|
});
|
||||||
it('should set the height of the viewport when the line-height changed', () => {
|
it('should set the height of the viewport when the line-height changed', () => {
|
||||||
terminal.lines.push('');
|
terminal.lines.push('');
|
||||||
@@ -59,26 +65,28 @@ describe('Viewport', () => {
|
|||||||
terminal.rows = 1;
|
terminal.rows = 1;
|
||||||
viewport.refresh();
|
viewport.refresh();
|
||||||
assert.equal(viewportElement.style.height, 1 * CHARACTER_HEIGHT + 'px');
|
assert.equal(viewportElement.style.height, 1 * CHARACTER_HEIGHT + 'px');
|
||||||
charMeasureElement.getBoundingClientRect = () => {
|
charMeasure.height = 20;
|
||||||
return { width: null, height: 20 };
|
|
||||||
};
|
|
||||||
viewport.refresh();
|
viewport.refresh();
|
||||||
assert.equal(viewportElement.style.height, 20 + 'px');
|
assert.equal(viewportElement.style.height, 20 + 'px');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('syncScrollArea', () => {
|
describe('syncScrollArea', () => {
|
||||||
it('should sync the scroll area', () => {
|
it('should sync the scroll area', done => {
|
||||||
terminal.lines.push('');
|
// Allow CharMeasure to be initialized
|
||||||
terminal.rows = 1;
|
setTimeout(() => {
|
||||||
assert.equal(scrollAreaElement.style.height, 0 * CHARACTER_HEIGHT + 'px');
|
terminal.lines.push('');
|
||||||
viewport.syncScrollArea();
|
terminal.rows = 1;
|
||||||
assert.equal(viewportElement.style.height, 1 * CHARACTER_HEIGHT + 'px');
|
assert.equal(scrollAreaElement.style.height, 0 * CHARACTER_HEIGHT + 'px');
|
||||||
assert.equal(scrollAreaElement.style.height, 1 * CHARACTER_HEIGHT + 'px');
|
viewport.syncScrollArea();
|
||||||
terminal.lines.push('');
|
assert.equal(viewportElement.style.height, 1 * CHARACTER_HEIGHT + 'px');
|
||||||
viewport.syncScrollArea();
|
assert.equal(scrollAreaElement.style.height, 1 * CHARACTER_HEIGHT + 'px');
|
||||||
assert.equal(viewportElement.style.height, 1 * CHARACTER_HEIGHT + 'px');
|
terminal.lines.push('');
|
||||||
assert.equal(scrollAreaElement.style.height, 2 * CHARACTER_HEIGHT + 'px');
|
viewport.syncScrollArea();
|
||||||
|
assert.equal(viewportElement.style.height, 1 * CHARACTER_HEIGHT + 'px');
|
||||||
|
assert.equal(scrollAreaElement.style.height, 2 * CHARACTER_HEIGHT + 'px');
|
||||||
|
done();
|
||||||
|
}, 0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+43
-19
@@ -3,6 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { ITerminal } from './Interfaces';
|
import { ITerminal } from './Interfaces';
|
||||||
|
import { CharMeasure } from './utils/CharMeasure';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents the viewport of a terminal, the visible area within the larger buffer of output.
|
* Represents the viewport of a terminal, the visible area within the larger buffer of output.
|
||||||
@@ -12,6 +13,7 @@ export class Viewport {
|
|||||||
private currentRowHeight: number;
|
private currentRowHeight: number;
|
||||||
private lastRecordedBufferLength: number;
|
private lastRecordedBufferLength: number;
|
||||||
private lastRecordedViewportHeight: number;
|
private lastRecordedViewportHeight: number;
|
||||||
|
private lastTouchY: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new Viewport.
|
* Creates a new Viewport.
|
||||||
@@ -24,7 +26,7 @@ export class Viewport {
|
|||||||
private terminal: ITerminal,
|
private terminal: ITerminal,
|
||||||
private viewportElement: HTMLElement,
|
private viewportElement: HTMLElement,
|
||||||
private scrollArea: HTMLElement,
|
private scrollArea: HTMLElement,
|
||||||
private charMeasureElement: HTMLElement
|
private charMeasure: CharMeasure
|
||||||
) {
|
) {
|
||||||
this.currentRowHeight = 0;
|
this.currentRowHeight = 0;
|
||||||
this.lastRecordedBufferLength = 0;
|
this.lastRecordedBufferLength = 0;
|
||||||
@@ -34,7 +36,8 @@ export class Viewport {
|
|||||||
this.terminal.on('resize', this.syncScrollArea.bind(this));
|
this.terminal.on('resize', this.syncScrollArea.bind(this));
|
||||||
this.viewportElement.addEventListener('scroll', this.onScroll.bind(this));
|
this.viewportElement.addEventListener('scroll', this.onScroll.bind(this));
|
||||||
|
|
||||||
this.syncScrollArea();
|
// Perform this async to ensure the CharMeasure is ready.
|
||||||
|
setTimeout(() => this.syncScrollArea(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -43,21 +46,21 @@ export class Viewport {
|
|||||||
* @param charSize A character size measurement bounding rect object, if it doesn't exist it will
|
* @param charSize A character size measurement bounding rect object, if it doesn't exist it will
|
||||||
* be created.
|
* be created.
|
||||||
*/
|
*/
|
||||||
private refresh(charSize?: ClientRect): void {
|
private refresh(): void {
|
||||||
var size = charSize || this.charMeasureElement.getBoundingClientRect();
|
if (this.charMeasure.height > 0) {
|
||||||
if (size.height > 0) {
|
const rowHeightChanged = this.charMeasure.height !== this.currentRowHeight;
|
||||||
var rowHeightChanged = size.height !== this.currentRowHeight;
|
|
||||||
if (rowHeightChanged) {
|
if (rowHeightChanged) {
|
||||||
this.currentRowHeight = size.height;
|
this.currentRowHeight = this.charMeasure.height;
|
||||||
this.viewportElement.style.lineHeight = size.height + 'px';
|
this.viewportElement.style.lineHeight = this.charMeasure.height + 'px';
|
||||||
this.terminal.rowContainer.style.lineHeight = size.height + 'px';
|
this.terminal.rowContainer.style.lineHeight = this.charMeasure.height + 'px';
|
||||||
}
|
}
|
||||||
var viewportHeightChanged = this.lastRecordedViewportHeight !== this.terminal.rows;
|
const viewportHeightChanged = this.lastRecordedViewportHeight !== this.terminal.rows;
|
||||||
if (rowHeightChanged || viewportHeightChanged) {
|
if (rowHeightChanged || viewportHeightChanged) {
|
||||||
this.lastRecordedViewportHeight = this.terminal.rows;
|
this.lastRecordedViewportHeight = this.terminal.rows;
|
||||||
this.viewportElement.style.height = size.height * this.terminal.rows + 'px';
|
this.viewportElement.style.height = this.charMeasure.height * this.terminal.rows + 'px';
|
||||||
|
this.terminal.selectionContainer.style.height = this.viewportElement.style.height;
|
||||||
}
|
}
|
||||||
this.scrollArea.style.height = (size.height * this.lastRecordedBufferLength) + 'px';
|
this.scrollArea.style.height = (this.charMeasure.height * this.lastRecordedBufferLength) + 'px';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,14 +77,13 @@ export class Viewport {
|
|||||||
this.refresh();
|
this.refresh();
|
||||||
} else {
|
} else {
|
||||||
// If size has changed, refresh viewport
|
// If size has changed, refresh viewport
|
||||||
var size = this.charMeasureElement.getBoundingClientRect();
|
if (this.charMeasure.height !== this.currentRowHeight) {
|
||||||
if (size.height !== this.currentRowHeight) {
|
this.refresh();
|
||||||
this.refresh(size);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync scrollTop
|
// Sync scrollTop
|
||||||
var scrollTop = this.terminal.ydisp * this.currentRowHeight;
|
const scrollTop = this.terminal.ydisp * this.currentRowHeight;
|
||||||
if (this.viewportElement.scrollTop !== scrollTop) {
|
if (this.viewportElement.scrollTop !== scrollTop) {
|
||||||
this.viewportElement.scrollTop = scrollTop;
|
this.viewportElement.scrollTop = scrollTop;
|
||||||
}
|
}
|
||||||
@@ -93,8 +95,8 @@ export class Viewport {
|
|||||||
* @param ev The scroll event.
|
* @param ev The scroll event.
|
||||||
*/
|
*/
|
||||||
private onScroll(ev: Event) {
|
private onScroll(ev: Event) {
|
||||||
var newRow = Math.round(this.viewportElement.scrollTop / this.currentRowHeight);
|
const newRow = Math.round(this.viewportElement.scrollTop / this.currentRowHeight);
|
||||||
var diff = newRow - this.terminal.ydisp;
|
const diff = newRow - this.terminal.ydisp;
|
||||||
this.terminal.scrollDisp(diff, true);
|
this.terminal.scrollDisp(diff, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,7 +112,7 @@ export class Viewport {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Fallback to WheelEvent.DOM_DELTA_PIXEL
|
// Fallback to WheelEvent.DOM_DELTA_PIXEL
|
||||||
var multiplier = 1;
|
let multiplier = 1;
|
||||||
if (ev.deltaMode === WheelEvent.DOM_DELTA_LINE) {
|
if (ev.deltaMode === WheelEvent.DOM_DELTA_LINE) {
|
||||||
multiplier = this.currentRowHeight;
|
multiplier = this.currentRowHeight;
|
||||||
} else if (ev.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
|
} else if (ev.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
|
||||||
@@ -120,4 +122,26 @@ export class Viewport {
|
|||||||
// Prevent the page from scrolling when the terminal scrolls
|
// Prevent the page from scrolling when the terminal scrolls
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the touchstart event, recording the touch occurred.
|
||||||
|
* @param ev The touch event.
|
||||||
|
*/
|
||||||
|
public onTouchStart(ev: TouchEvent) {
|
||||||
|
this.lastTouchY = ev.touches[0].pageY;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the touchmove event, scrolling the viewport if the position shifted.
|
||||||
|
* @param ev The touch event.
|
||||||
|
*/
|
||||||
|
public onTouchMove(ev: TouchEvent) {
|
||||||
|
let deltaY = this.lastTouchY - ev.touches[0].pageY;
|
||||||
|
this.lastTouchY = ev.touches[0].pageY;
|
||||||
|
if (deltaY === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.viewportElement.scrollTop += deltaY;
|
||||||
|
ev.preventDefault();
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,9 @@
|
|||||||
var exports = {};
|
var exports = {};
|
||||||
|
|
||||||
exports.proposeGeometry = function (term) {
|
exports.proposeGeometry = function (term) {
|
||||||
|
if (!term.element.parentElement) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
var parentElementStyle = window.getComputedStyle(term.element.parentElement),
|
var parentElementStyle = window.getComputedStyle(term.element.parentElement),
|
||||||
parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height')),
|
parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height')),
|
||||||
parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')) - 17),
|
parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')) - 17),
|
||||||
@@ -53,7 +56,7 @@
|
|||||||
subjectRow.innerHTML = 'W'; // Common character for measuring width, although on monospace
|
subjectRow.innerHTML = 'W'; // Common character for measuring width, although on monospace
|
||||||
characterWidth = subjectRow.getBoundingClientRect().width;
|
characterWidth = subjectRow.getBoundingClientRect().width;
|
||||||
subjectRow.style.display = ''; // Revert style before calculating height, since they differ.
|
subjectRow.style.display = ''; // Revert style before calculating height, since they differ.
|
||||||
characterHeight = parseInt(subjectRow.offsetHeight);
|
characterHeight = subjectRow.getBoundingClientRect().height;
|
||||||
subjectRow.innerHTML = contentBuffer;
|
subjectRow.innerHTML = contentBuffer;
|
||||||
|
|
||||||
rows = parseInt(availableHeight / characterHeight);
|
rows = parseInt(availableHeight / characterHeight);
|
||||||
@@ -66,7 +69,9 @@
|
|||||||
exports.fit = function (term) {
|
exports.fit = function (term) {
|
||||||
var geometry = exports.proposeGeometry(term);
|
var geometry = exports.proposeGeometry(term);
|
||||||
|
|
||||||
term.resize(geometry.cols, geometry.rows);
|
if (geometry) {
|
||||||
|
term.resize(geometry.cols, geometry.rows);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Xterm.prototype.proposeGeometry = function () {
|
Xterm.prototype.proposeGeometry = function () {
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<link rel="stylesheet" href="../../src/xterm.css" />
|
|
||||||
<link rel="stylesheet" href="../../demo/style.css" />
|
|
||||||
<script src="../../src/xterm.js"></script>
|
|
||||||
<script src="linkify.js"></script>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
color: #111;
|
|
||||||
}
|
|
||||||
|
|
||||||
#terminal-container {
|
|
||||||
max-width: 900px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#terminal-container a {
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="terminal-container"></div>
|
|
||||||
<script>
|
|
||||||
var term = new Terminal(),
|
|
||||||
container = document.getElementById('terminal-container');
|
|
||||||
|
|
||||||
term.open(container);
|
|
||||||
term.writeln('Hello, welcome to xterm.js');
|
|
||||||
term.writeln('');
|
|
||||||
term.writeln('Check it out at github.com/sourcelair/xterm.js');
|
|
||||||
term.linkify();
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,207 +0,0 @@
|
|||||||
/**
|
|
||||||
* Methods for turning URL subscrings in the terminal's content into links (`a` DOM elements).
|
|
||||||
* @module xterm/addons/linkify/linkify
|
|
||||||
* @license MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
(function (linkify) {
|
|
||||||
if (typeof exports === 'object' && typeof module === 'object') {
|
|
||||||
/*
|
|
||||||
* CommonJS environment
|
|
||||||
*/
|
|
||||||
module.exports = linkify(require('../../xterm'));
|
|
||||||
} else if (typeof define == 'function') {
|
|
||||||
/*
|
|
||||||
* Require.js is available
|
|
||||||
*/
|
|
||||||
define(['../../xterm'], linkify);
|
|
||||||
} else {
|
|
||||||
/*
|
|
||||||
* Plain browser environment
|
|
||||||
*/
|
|
||||||
linkify(window.Terminal);
|
|
||||||
}
|
|
||||||
})(function (Xterm) {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var exports = {},
|
|
||||||
protocolClause = '(https?:\\/\\/)',
|
|
||||||
domainCharacterSet = '[\\da-z\\.-]+',
|
|
||||||
negatedDomainCharacterSet = '[^\\da-z\\.-]+',
|
|
||||||
domainBodyClause = '(' + domainCharacterSet + ')',
|
|
||||||
tldClause = '([a-z\\.]{2,6})',
|
|
||||||
ipClause = '((\\d{1,3}\\.){3}\\d{1,3})',
|
|
||||||
portClause = '(:\\d{1,5})',
|
|
||||||
hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|' + ipClause + ')' + portClause + '?',
|
|
||||||
pathClause = '(\\/[\\/\\w\\.-]*)*',
|
|
||||||
negatedPathCharacterSet = '[^\\/\\w\\.-]+',
|
|
||||||
bodyClause = hostClause + pathClause,
|
|
||||||
start = '(?:^|' + negatedDomainCharacterSet + ')(',
|
|
||||||
end = ')($|' + negatedPathCharacterSet + ')',
|
|
||||||
lenientUrlClause = start + protocolClause + '?' + bodyClause + end,
|
|
||||||
strictUrlClause = start + protocolClause + bodyClause + end,
|
|
||||||
lenientUrlRegex = new RegExp(lenientUrlClause),
|
|
||||||
strictUrlRegex = new RegExp(strictUrlClause);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts all valid URLs found in the given terminal line into
|
|
||||||
* hyperlinks. The terminal line can be either the HTML element itself
|
|
||||||
* or the index of the termina line in the children of the terminal
|
|
||||||
* rows container.
|
|
||||||
*
|
|
||||||
* @param {Xterm} terminal - The terminal that owns the given line.
|
|
||||||
* @param {number|HTMLDivElement} line - The terminal line that should get
|
|
||||||
* "linkified".
|
|
||||||
* @param {boolean} lenient - The regex type that will be used to identify links. If lenient is
|
|
||||||
* false, the regex requires a protocol clause. Defaults to true.
|
|
||||||
* @param {string} target - Sets target="" attribute with value provided to links.
|
|
||||||
* Default doesn't set target attribute
|
|
||||||
* @emits linkify
|
|
||||||
* @emits linkify:line
|
|
||||||
*/
|
|
||||||
exports.linkifyTerminalLine = function (terminal, line, lenient, target) {
|
|
||||||
if (typeof line == 'number') {
|
|
||||||
line = terminal.rowContainer.children[line];
|
|
||||||
} else if (! (line instanceof HTMLDivElement)) {
|
|
||||||
var message = 'The "line" argument should be either a number';
|
|
||||||
message += ' or an HTMLDivElement';
|
|
||||||
|
|
||||||
throw new TypeError(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof target === 'undefined') {
|
|
||||||
target = '';
|
|
||||||
} else {
|
|
||||||
target = 'target="' + target + '"';
|
|
||||||
}
|
|
||||||
|
|
||||||
var buffer = document.createElement('span'),
|
|
||||||
nodes = line.childNodes;
|
|
||||||
|
|
||||||
for (var j=0; j<nodes.length; j++) {
|
|
||||||
var node = nodes[j],
|
|
||||||
match;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Since we cannot access the TextNode's HTML representation
|
|
||||||
* from the instance itself, we assign its data as textContent
|
|
||||||
* to a dummy buffer span, in order to retrieve the TextNode's
|
|
||||||
* HTML representation from the buffer's innerHTML.
|
|
||||||
*/
|
|
||||||
buffer.textContent = node.data;
|
|
||||||
|
|
||||||
var nodeHTML = buffer.innerHTML;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Apply function only on TextNodes
|
|
||||||
*/
|
|
||||||
if (node.nodeType != node.TEXT_NODE) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var url = exports.findLinkMatch(node.data, lenient);
|
|
||||||
|
|
||||||
if (!url) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var startsWithProtocol = new RegExp('^' + protocolClause),
|
|
||||||
urlHasProtocol = url.match(startsWithProtocol),
|
|
||||||
href = (urlHasProtocol) ? url : 'http://' + url,
|
|
||||||
link = '<a href="' + href + '" ' + target + '>' + url + '</a>',
|
|
||||||
newHTML = nodeHTML.replace(url, link);
|
|
||||||
|
|
||||||
line.innerHTML = line.innerHTML.replace(nodeHTML, newHTML);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This event gets emitted when conversion of all URL susbtrings
|
|
||||||
* to HTML anchor elements (links) has finished, for a specific
|
|
||||||
* line of the current Xterm instance.
|
|
||||||
*
|
|
||||||
* @event linkify:line
|
|
||||||
*/
|
|
||||||
terminal.emit('linkify:line', line);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds a link within a block of text.
|
|
||||||
*
|
|
||||||
* @param {string} text - The text to search .
|
|
||||||
* @param {boolean} lenient - Whether to use the lenient search.
|
|
||||||
* @return {string} A URL.
|
|
||||||
*/
|
|
||||||
exports.findLinkMatch = function (text, lenient) {
|
|
||||||
var match = text.match(lenient ? lenientUrlRegex : strictUrlRegex);
|
|
||||||
if (!match || match.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return match[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts all valid URLs found in the terminal view into hyperlinks.
|
|
||||||
*
|
|
||||||
* @param {Xterm} terminal - The terminal that should get "linkified".
|
|
||||||
* @param {boolean} lenient - The regex type that will be used to identify links. If lenient is
|
|
||||||
* false, the regex requires a protocol clause. Defaults to true.
|
|
||||||
* @param {string} target - Sets target="" attribute with value provided to links.
|
|
||||||
* Default doesn't set target attribute
|
|
||||||
* @emits linkify
|
|
||||||
* @emits linkify:line
|
|
||||||
*/
|
|
||||||
exports.linkify = function (terminal, lenient, target) {
|
|
||||||
var rows = terminal.rowContainer.children;
|
|
||||||
|
|
||||||
lenient = (typeof lenient == "boolean") ? lenient : true;
|
|
||||||
for (var i=0; i<rows.length; i++) {
|
|
||||||
var line = rows[i];
|
|
||||||
|
|
||||||
exports.linkifyTerminalLine(terminal, line, lenient, target);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This event gets emitted when conversion of all URL substrings to
|
|
||||||
* HTML anchor elements (links) has finished for the current Xterm
|
|
||||||
* instance's view.
|
|
||||||
*
|
|
||||||
* @event linkify
|
|
||||||
*/
|
|
||||||
terminal.emit('linkify');
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extend Xterm prototype.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts all valid URLs found in the current terminal linte into
|
|
||||||
* hyperlinks.
|
|
||||||
*
|
|
||||||
* @memberof Xterm
|
|
||||||
* @param {number|HTMLDivElement} line - The terminal line that should get
|
|
||||||
* "linkified".
|
|
||||||
* @param {boolean} lenient - The regex type that will be used to identify links. If lenient is
|
|
||||||
* false, the regex requires a protocol clause. Defaults to true.
|
|
||||||
* @param {string} target - Sets target="" attribute with value provided to links.
|
|
||||||
* Default doesn't set target attribute
|
|
||||||
*/
|
|
||||||
Xterm.prototype.linkifyTerminalLine = function (line, lenient, target) {
|
|
||||||
return exports.linkifyTerminalLine(this, line, lenient, target);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts all valid URLs found in the current terminal into hyperlinks.
|
|
||||||
*
|
|
||||||
* @memberof Xterm
|
|
||||||
* @param {boolean} lenient - The regex type that will be used to identify links. If lenient is
|
|
||||||
* false, the regex requires a protocol clause. Defaults to true.
|
|
||||||
* @param {string} target - Sets target="" attribute with value provided to links.
|
|
||||||
* Default doesn't set target attribute
|
|
||||||
*/
|
|
||||||
Xterm.prototype.linkify = function (lenient, target) {
|
|
||||||
return exports.linkify(this, lenient, target);
|
|
||||||
};
|
|
||||||
|
|
||||||
return exports;
|
|
||||||
});
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "xterm.linkify",
|
|
||||||
"main": "linkify.js",
|
|
||||||
"private": true
|
|
||||||
}
|
|
||||||
@@ -2,17 +2,13 @@ import { assert } from 'chai';
|
|||||||
import * as Terminal from '../xterm';
|
import * as Terminal from '../xterm';
|
||||||
import * as Clipboard from './Clipboard';
|
import * as Clipboard from './Clipboard';
|
||||||
|
|
||||||
|
describe('evaluatePastedTextProcessing', function () {
|
||||||
|
it('should replace carriage return + line feed with line feed on windows', function () {
|
||||||
|
const pastedText = 'foo\r\nbar\r\n',
|
||||||
|
processedText = Clipboard.prepareTextForTerminal(pastedText, false),
|
||||||
|
windowsProcessedText = Clipboard.prepareTextForTerminal(pastedText, true);
|
||||||
|
|
||||||
describe('evaluateCopiedTextProcessing', function () {
|
assert.equal(processedText, 'foo\r\nbar\r\n');
|
||||||
it('should strip trailing whitespaces and replace nbsps with spaces', function () {
|
assert.equal(windowsProcessedText, 'foo\rbar\r');
|
||||||
let nonBreakingSpace = String.fromCharCode(160),
|
|
||||||
copiedText = 'echo' + nonBreakingSpace + 'hello' + nonBreakingSpace,
|
|
||||||
processedText = Clipboard.prepareTextForClipboard(copiedText);
|
|
||||||
|
|
||||||
// No trailing spaces
|
|
||||||
assert.equal(processedText.match(/\s+$/), null);
|
|
||||||
|
|
||||||
// No non-breaking space
|
|
||||||
assert.equal(processedText.indexOf(nonBreakingSpace), -1);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+51
-89
@@ -5,7 +5,7 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ITerminal } from '../Interfaces';
|
import { ITerminal, ISelectionManager } from '../Interfaces';
|
||||||
|
|
||||||
interface IWindow extends Window {
|
interface IWindow extends Window {
|
||||||
clipboardData?: {
|
clipboardData?: {
|
||||||
@@ -17,44 +17,29 @@ interface IWindow extends Window {
|
|||||||
declare var window: IWindow;
|
declare var window: IWindow;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prepares text copied from terminal selection, to be saved in the clipboard by:
|
* Prepares text to be pasted into the terminal by normalizing the line endings
|
||||||
* 1. stripping all trailing white spaces
|
* @param text The pasted text that needs processing before inserting into the terminal
|
||||||
* 2. converting all non-breaking spaces to regular spaces
|
|
||||||
* @param {string} text The copied text that needs processing for storing in clipboard
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
*/
|
||||||
export function prepareTextForClipboard(text: string): string {
|
export function prepareTextForTerminal(text: string, isMSWindows: boolean): string {
|
||||||
let space = String.fromCharCode(32),
|
if (isMSWindows) {
|
||||||
nonBreakingSpace = String.fromCharCode(160),
|
return text.replace(/\r?\n/g, '\r');
|
||||||
allNonBreakingSpaces = new RegExp(nonBreakingSpace, 'g'),
|
}
|
||||||
processedText = text.split('\n').map(function (line) {
|
return text;
|
||||||
// Strip all trailing white spaces and convert all non-breaking spaces
|
|
||||||
// to regular spaces.
|
|
||||||
let processedLine = line.replace(/\s+$/g, '').replace(allNonBreakingSpaces, space);
|
|
||||||
|
|
||||||
return processedLine;
|
|
||||||
}).join('\n');
|
|
||||||
|
|
||||||
return processedText;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Binds copy functionality to the given terminal.
|
* Binds copy functionality to the given terminal.
|
||||||
* @param {ClipboardEvent} ev The original copy event to be handled
|
* @param {ClipboardEvent} ev The original copy event to be handled
|
||||||
*/
|
*/
|
||||||
export function copyHandler(ev: ClipboardEvent, term: ITerminal) {
|
export function copyHandler(ev: ClipboardEvent, term: ITerminal, selectionManager: ISelectionManager) {
|
||||||
// We cast `window` to `any` type, because TypeScript has not declared the `clipboardData`
|
|
||||||
// property that we use below for Internet Explorer.
|
|
||||||
let copiedText = window.getSelection().toString(),
|
|
||||||
text = prepareTextForClipboard(copiedText);
|
|
||||||
|
|
||||||
if (term.browser.isMSIE) {
|
if (term.browser.isMSIE) {
|
||||||
window.clipboardData.setData('Text', text);
|
window.clipboardData.setData('Text', selectionManager.selectionText);
|
||||||
} else {
|
} else {
|
||||||
ev.clipboardData.setData('text/plain', text);
|
ev.clipboardData.setData('text/plain', selectionManager.selectionText);
|
||||||
}
|
}
|
||||||
|
|
||||||
ev.preventDefault(); // Prevent or the original text will be copied.
|
// Prevent or the original text will be copied.
|
||||||
|
ev.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -68,8 +53,11 @@ export function pasteHandler(ev: ClipboardEvent, term: ITerminal) {
|
|||||||
let text: string;
|
let text: string;
|
||||||
|
|
||||||
let dispatchPaste = function(text) {
|
let dispatchPaste = function(text) {
|
||||||
|
text = prepareTextForTerminal(text, term.browser.isMSWindows);
|
||||||
term.handler(text);
|
term.handler(text);
|
||||||
term.textarea.value = '';
|
term.textarea.value = '';
|
||||||
|
term.emit('paste', text);
|
||||||
|
|
||||||
return term.cancel(ev);
|
return term.cancel(ev);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -87,68 +75,42 @@ export function pasteHandler(ev: ClipboardEvent, term: ITerminal) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bind to right-click event and allow right-click copy and paste.
|
* Moves the textarea under the mouse cursor and focuses it.
|
||||||
*
|
* @param ev The original right click event to be handled.
|
||||||
* **Logic**
|
* @param textarea The terminal's textarea.
|
||||||
* If text is selected and right-click happens on selected text, then
|
|
||||||
* do nothing to allow seamless copying.
|
|
||||||
* If no text is selected or right-click is outside of the selection
|
|
||||||
* area, then bring the terminal's input below the cursor, in order to
|
|
||||||
* trigger the event on the textarea and allow-right click paste, without
|
|
||||||
* caring about disappearing selection.
|
|
||||||
* @param {MouseEvent} ev The original right click event to be handled
|
|
||||||
* @param {Terminal} term The terminal on which to apply the handled paste event
|
|
||||||
*/
|
*/
|
||||||
export function rightClickHandler(ev: MouseEvent, term: ITerminal) {
|
export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextAreaElement) {
|
||||||
let s = document.getSelection(),
|
|
||||||
selectedText = prepareTextForClipboard(s.toString()),
|
|
||||||
clickIsOnSelection = false,
|
|
||||||
x = ev.clientX,
|
|
||||||
y = ev.clientY;
|
|
||||||
|
|
||||||
if (s.rangeCount) {
|
|
||||||
let r = s.getRangeAt(0),
|
|
||||||
cr = r.getClientRects();
|
|
||||||
|
|
||||||
for (let i = 0; i < cr.length; i++) {
|
|
||||||
let rect = cr[i];
|
|
||||||
|
|
||||||
clickIsOnSelection = (
|
|
||||||
(x > rect.left) && (x < rect.right) &&
|
|
||||||
(y > rect.top) && (y < rect.bottom)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (clickIsOnSelection) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If we clicked on selection and selection is not a single space,
|
|
||||||
// then mark the right click as copy-only. We check for the single
|
|
||||||
// space selection, as this can happen when clicking on an
|
|
||||||
// and there is not much pointing in copying a single space.
|
|
||||||
if (selectedText.match(/^\s$/) || !selectedText.length) {
|
|
||||||
clickIsOnSelection = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bring textarea at the cursor position
|
// Bring textarea at the cursor position
|
||||||
if (!clickIsOnSelection) {
|
textarea.style.position = 'fixed';
|
||||||
term.textarea.style.position = 'fixed';
|
textarea.style.width = '20px';
|
||||||
term.textarea.style.width = '20px';
|
textarea.style.height = '20px';
|
||||||
term.textarea.style.height = '20px';
|
textarea.style.left = (ev.clientX - 10) + 'px';
|
||||||
term.textarea.style.left = (x - 10) + 'px';
|
textarea.style.top = (ev.clientY - 10) + 'px';
|
||||||
term.textarea.style.top = (y - 10) + 'px';
|
textarea.style.zIndex = '1000';
|
||||||
term.textarea.style.zIndex = '1000';
|
|
||||||
term.textarea.focus();
|
|
||||||
|
|
||||||
// Reset the terminal textarea's styling
|
textarea.focus();
|
||||||
setTimeout(function () {
|
|
||||||
term.textarea.style.position = null;
|
// Reset the terminal textarea's styling
|
||||||
term.textarea.style.width = null;
|
setTimeout(function () {
|
||||||
term.textarea.style.height = null;
|
textarea.style.position = null;
|
||||||
term.textarea.style.left = null;
|
textarea.style.width = null;
|
||||||
term.textarea.style.top = null;
|
textarea.style.height = null;
|
||||||
term.textarea.style.zIndex = null;
|
textarea.style.left = null;
|
||||||
}, 4);
|
textarea.style.top = null;
|
||||||
}
|
textarea.style.zIndex = null;
|
||||||
|
}, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bind to right-click event and allow right-click copy and paste.
|
||||||
|
* @param ev The original right click event to be handled.
|
||||||
|
* @param textarea The terminal's textarea.
|
||||||
|
* @param selectionManager The terminal's selection manager.
|
||||||
|
*/
|
||||||
|
export function rightClickHandler(ev: MouseEvent, textarea: HTMLTextAreaElement, selectionManager: ISelectionManager) {
|
||||||
|
moveTextAreaUnderMouseCursor(ev, textarea);
|
||||||
|
|
||||||
|
// Get textarea ready to copy from the context menu
|
||||||
|
textarea.value = selectionManager.selectionText;
|
||||||
|
textarea.select();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
var assert = require('chai').assert;
|
|
||||||
var Terminal = require('../../xterm');
|
|
||||||
var linkify = require('../../addons/linkify/linkify');
|
|
||||||
|
|
||||||
describe('linkify addon', function () {
|
|
||||||
var xterm;
|
|
||||||
|
|
||||||
describe('API', function () {
|
|
||||||
it('should define Terminal.prototype.linkify', function () {
|
|
||||||
assert.isDefined(Terminal.prototype.linkify);
|
|
||||||
});
|
|
||||||
it('should define Terminal.prototype.linkifyTerminalLine', function () {
|
|
||||||
assert.isDefined(Terminal.prototype.linkifyTerminalLine);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('findUrlMatchOnLine', function () {
|
|
||||||
describe('strict regex', function () {
|
|
||||||
it('should match when the entire text is a match', function () {
|
|
||||||
assert.equal(linkify.findLinkMatch('http://github.com', false), 'http://github.com');
|
|
||||||
assert.equal(linkify.findLinkMatch('http://127.0.0.1', false), 'http://127.0.0.1');
|
|
||||||
});
|
|
||||||
it('should match simple domains', function () {
|
|
||||||
assert.equal(linkify.findLinkMatch('foo http://github.com bar', false), 'http://github.com');
|
|
||||||
assert.equal(linkify.findLinkMatch('foo http://www.github.com bar', false), 'http://www.github.com');
|
|
||||||
assert.equal(linkify.findLinkMatch('foo https://github.com bar', false), 'https://github.com');
|
|
||||||
assert.equal(linkify.findLinkMatch('foo https://www.github.com bar', false), 'https://www.github.com');
|
|
||||||
});
|
|
||||||
it('should match web addresses with alpha paths', function () {
|
|
||||||
assert.equal(linkify.findLinkMatch('foo http://github.com/a/b/c bar', false), 'http://github.com/a/b/c');
|
|
||||||
assert.equal(linkify.findLinkMatch('foo http://www.github.com/a/b/c bar', false), 'http://www.github.com/a/b/c');
|
|
||||||
});
|
|
||||||
it('should not include whitespace surrounding a match', function () {
|
|
||||||
assert.equal(linkify.findLinkMatch(' http://github.com', false), 'http://github.com');
|
|
||||||
assert.equal(linkify.findLinkMatch('http://github.com ', false), 'http://github.com');
|
|
||||||
assert.equal(linkify.findLinkMatch(' http://github.com ', false), 'http://github.com');
|
|
||||||
});
|
|
||||||
it('should match IP addresses', function () {
|
|
||||||
assert.equal(linkify.findLinkMatch('foo http://127.0.0.1 bar', false), 'http://127.0.0.1');
|
|
||||||
assert.equal(linkify.findLinkMatch('foo https://127.0.0.1 bar', false), 'https://127.0.0.1');
|
|
||||||
});
|
|
||||||
it('should match ports on both domains and IP addresses', function () {
|
|
||||||
assert.equal(linkify.findLinkMatch('foo http://127.0.0.1:8080 bar', false), 'http://127.0.0.1:8080');
|
|
||||||
assert.equal(linkify.findLinkMatch('foo http://www.github.com:8080 bar', false), 'http://www.github.com:8080');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,9 +1,15 @@
|
|||||||
var glob = require('glob');
|
var glob = require('glob');
|
||||||
var fs = require('fs');
|
var fs = require('fs');
|
||||||
var pty = require('pty.js');
|
var os = require('os');
|
||||||
|
var pty = require('node-pty');
|
||||||
var sleep = require('sleep');
|
var sleep = require('sleep');
|
||||||
var Terminal = require('../xterm');
|
var Terminal = require('../xterm');
|
||||||
|
|
||||||
|
if (os.platform() === 'win32') {
|
||||||
|
// Skip tests on Windows since pty.open isn't supported
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var CONSOLE_LOG = console.log;
|
var CONSOLE_LOG = console.log;
|
||||||
|
|
||||||
// expect files need terminal at 80x25!
|
// expect files need terminal at 80x25!
|
||||||
@@ -56,10 +62,10 @@ function formatError(in_, out_, expected) {
|
|||||||
function terminalToString(term) {
|
function terminalToString(term) {
|
||||||
var result = '';
|
var result = '';
|
||||||
var line_s = '';
|
var line_s = '';
|
||||||
for (var line=0; line<term.rows; ++line) {
|
for (var line = term.ybase; line < term.ybase + term.rows; line++) {
|
||||||
line_s = '';
|
line_s = '';
|
||||||
for (var cell=0; cell<term.cols; ++cell) {
|
for (var cell=0; cell<term.cols; ++cell) {
|
||||||
line_s += term.lines[line][cell][1];
|
line_s += term.lines.get(line)[cell][1];
|
||||||
}
|
}
|
||||||
// rtrim empty cells as xterm does
|
// rtrim empty cells as xterm does
|
||||||
line_s = line_s.replace(/\s+$/, '');
|
line_s = line_s.replace(/\s+$/, '');
|
||||||
@@ -76,31 +82,49 @@ describe('xterm output comparison', function() {
|
|||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
xterm = new Terminal(COLS, ROWS);
|
xterm = new Terminal(COLS, ROWS);
|
||||||
xterm.refresh = function() {};
|
xterm.refresh = function() {};
|
||||||
|
xterm.viewport = {
|
||||||
|
syncScrollArea: function() {}
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// omit stack trace for escape sequence files
|
// omit stack trace for escape sequence files
|
||||||
Error.stackTraceLimit = 0;
|
Error.stackTraceLimit = 0;
|
||||||
var files = glob.sync('**/escape_sequence_files/*.in');
|
var files = glob.sync('**/escape_sequence_files/*.in');
|
||||||
// only successful tests for now
|
// only successful tests for now
|
||||||
var successful = [0, 2, 6, 12, 13, 18, 20, 22, 27, 28];
|
var skip = [
|
||||||
console.log(files);
|
10, 16, 17, 19, 32, 33, 34, 35, 36, 39,
|
||||||
for (var a in successful) {
|
40, 42, 43, 44, 45, 46, 47, 48, 49, 50,
|
||||||
var i = successful[a];
|
51, 52, 54, 55, 56, 57, 58, 59, 60, 61,
|
||||||
(function(filename){
|
63, 68
|
||||||
|
];
|
||||||
|
for (var i = 0; i < files.length; i++) {
|
||||||
|
if (skip.indexOf(i) >= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
(function(filename) {
|
||||||
it(filename.split('/').slice(-1)[0], function () {
|
it(filename.split('/').slice(-1)[0], function () {
|
||||||
pty_reset();
|
pty_reset();
|
||||||
var in_file = fs.readFileSync(filename, 'utf8');
|
var in_file = fs.readFileSync(filename, 'utf8');
|
||||||
var from_pty = pty_write_read(in_file);
|
var from_pty = pty_write_read(in_file);
|
||||||
// uncomment this to get log from terminal
|
// uncomment this to get log from terminal
|
||||||
console.log = function(){};
|
//console.log = function(){};
|
||||||
xterm.write(from_pty);
|
|
||||||
|
// Perform a synchronous .write(data)
|
||||||
|
xterm.writeBuffer.push(from_pty);
|
||||||
|
xterm.innerWrite();
|
||||||
|
|
||||||
var from_emulator = terminalToString(xterm);
|
var from_emulator = terminalToString(xterm);
|
||||||
console.log = CONSOLE_LOG;
|
console.log = CONSOLE_LOG;
|
||||||
var expected = fs.readFileSync(filename.split('.')[0] + '.text', 'utf8');
|
var expected = fs.readFileSync(filename.split('.')[0] + '.text', 'utf8');
|
||||||
if (from_emulator != expected) {
|
// 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.
|
||||||
|
var expectedRightTrimmed = expected.split('\n').map(function (l) {
|
||||||
|
return l.replace(/\s+$/, '');
|
||||||
|
}).join('\n');
|
||||||
|
if (from_emulator != expectedRightTrimmed) {
|
||||||
// uncomment to get noisy output
|
// uncomment to get noisy output
|
||||||
//throw new Error(formatError(in_file, from_emulator, expected));
|
throw new Error(formatError(in_file, from_emulator, expected));
|
||||||
throw new Error('mismatch');
|
// throw new Error('mismatch');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
})(files[i]);
|
})(files[i]);
|
||||||
|
|||||||
+133
-81
@@ -14,6 +14,17 @@ describe('xterm.js', function() {
|
|||||||
xterm.compositionHelper = {
|
xterm.compositionHelper = {
|
||||||
keydown: function(){ return true; }
|
keydown: function(){ return true; }
|
||||||
};
|
};
|
||||||
|
// Force synchronous writes
|
||||||
|
xterm.write = function(data) {
|
||||||
|
xterm.writeBuffer.push(data);
|
||||||
|
xterm.innerWrite();
|
||||||
|
};
|
||||||
|
xterm.element = {
|
||||||
|
classList: {
|
||||||
|
toggle: function(){},
|
||||||
|
remove: function(){}
|
||||||
|
}
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getOption', function() {
|
describe('getOption', function() {
|
||||||
@@ -44,19 +55,24 @@ describe('xterm.js', function() {
|
|||||||
it('should throw when setting a non-existant option', function() {
|
it('should throw when setting a non-existant option', function() {
|
||||||
assert.throws(xterm.setOption.bind(xterm, 'fake', true));
|
assert.throws(xterm.setOption.bind(xterm, 'fake', true));
|
||||||
});
|
});
|
||||||
|
it('should not allow scrollback less than number of rows', function() {
|
||||||
|
let setOptionCall = xterm.setOption.bind(xterm, 'scrollback', xterm.rows - 1);
|
||||||
|
|
||||||
|
assert.equal(setOptionCall(), false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('clear', function() {
|
describe('clear', function() {
|
||||||
it('should clear a buffer equal to rows', function() {
|
it('should clear a buffer equal to rows', function() {
|
||||||
var promptLine = xterm.lines[xterm.ybase + xterm.y];
|
var promptLine = xterm.lines.get(xterm.ybase + xterm.y);
|
||||||
xterm.clear();
|
xterm.clear();
|
||||||
assert.equal(xterm.y, 0);
|
assert.equal(xterm.y, 0);
|
||||||
assert.equal(xterm.ybase, 0);
|
assert.equal(xterm.ybase, 0);
|
||||||
assert.equal(xterm.ydisp, 0);
|
assert.equal(xterm.ydisp, 0);
|
||||||
assert.equal(xterm.lines.length, xterm.rows);
|
assert.equal(xterm.lines.length, xterm.rows);
|
||||||
assert.deepEqual(xterm.lines[0], promptLine);
|
assert.deepEqual(xterm.lines.get(0), promptLine);
|
||||||
for (var i = 1; i < xterm.rows; i++) {
|
for (var i = 1; i < xterm.rows; i++) {
|
||||||
assert.deepEqual(xterm.lines[0], xterm.blankLine());
|
assert.deepEqual(xterm.lines.get(i), xterm.blankLine());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
it('should clear a buffer larger than rows', function() {
|
it('should clear a buffer larger than rows', function() {
|
||||||
@@ -65,28 +81,28 @@ describe('xterm.js', function() {
|
|||||||
xterm.write('test\n');
|
xterm.write('test\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
var promptLine = xterm.lines[xterm.ybase + xterm.y];
|
var promptLine = xterm.lines.get(xterm.ybase + xterm.y);
|
||||||
xterm.clear();
|
xterm.clear();
|
||||||
assert.equal(xterm.y, 0);
|
assert.equal(xterm.y, 0);
|
||||||
assert.equal(xterm.ybase, 0);
|
assert.equal(xterm.ybase, 0);
|
||||||
assert.equal(xterm.ydisp, 0);
|
assert.equal(xterm.ydisp, 0);
|
||||||
assert.equal(xterm.lines.length, xterm.rows);
|
assert.equal(xterm.lines.length, xterm.rows);
|
||||||
assert.deepEqual(xterm.lines[0], promptLine);
|
assert.deepEqual(xterm.lines.get(0), promptLine);
|
||||||
for (var i = 1; i < xterm.rows; i++) {
|
for (var i = 1; i < xterm.rows; i++) {
|
||||||
assert.deepEqual(xterm.lines[i], xterm.blankLine());
|
assert.deepEqual(xterm.lines.get(i), xterm.blankLine());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
it('should not break the prompt when cleared twice', function() {
|
it('should not break the prompt when cleared twice', function() {
|
||||||
var promptLine = xterm.lines[xterm.ybase + xterm.y];
|
var promptLine = xterm.lines.get(xterm.ybase + xterm.y);
|
||||||
xterm.clear();
|
xterm.clear();
|
||||||
xterm.clear();
|
xterm.clear();
|
||||||
assert.equal(xterm.y, 0);
|
assert.equal(xterm.y, 0);
|
||||||
assert.equal(xterm.ybase, 0);
|
assert.equal(xterm.ybase, 0);
|
||||||
assert.equal(xterm.ydisp, 0);
|
assert.equal(xterm.ydisp, 0);
|
||||||
assert.equal(xterm.lines.length, xterm.rows);
|
assert.equal(xterm.lines.length, xterm.rows);
|
||||||
assert.deepEqual(xterm.lines[0], promptLine);
|
assert.deepEqual(xterm.lines.get(0), promptLine);
|
||||||
for (var i = 1; i < xterm.rows; i++) {
|
for (var i = 1; i < xterm.rows; i++) {
|
||||||
assert.deepEqual(xterm.lines[i], xterm.blankLine());
|
assert.deepEqual(xterm.lines.get(i), xterm.blankLine());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -286,14 +302,33 @@ describe('xterm.js', function() {
|
|||||||
it('should return \\x1b[5B for ctrl+down', function() {
|
it('should return \\x1b[5B for ctrl+down', function() {
|
||||||
assert.equal(xterm.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 40 }).key, '\x1b[1;5B'); // CSI 5 B
|
assert.equal(xterm.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 40 }).key, '\x1b[1;5B'); // CSI 5 B
|
||||||
});
|
});
|
||||||
// Evalueate alt + arrow key movement, which is a feature of terminal emulators but not VT100
|
|
||||||
// http://unix.stackexchange.com/a/108106
|
describe('On non-macOS platforms', function() {
|
||||||
it('should return \\x1b[5D for alt+left', function() {
|
beforeEach(function() {
|
||||||
assert.equal(xterm.evaluateKeyEscapeSequence({ altKey: true, keyCode: 37 }).key, '\x1b[1;5D'); // CSI 5 D
|
xterm.browser.isMac = false;
|
||||||
|
});
|
||||||
|
// Evalueate alt + arrow key movement, which is a feature of terminal emulators but not VT100
|
||||||
|
// http://unix.stackexchange.com/a/108106
|
||||||
|
it('should return \\x1b[5D for alt+left', function() {
|
||||||
|
assert.equal(xterm.evaluateKeyEscapeSequence({ altKey: true, keyCode: 37 }).key, '\x1b[1;5D'); // CSI 5 D
|
||||||
|
});
|
||||||
|
it('should return \\x1b[5C for alt+right', function() {
|
||||||
|
assert.equal(xterm.evaluateKeyEscapeSequence({ altKey: true, keyCode: 39 }).key, '\x1b[1;5C'); // CSI 5 C
|
||||||
|
});
|
||||||
});
|
});
|
||||||
it('should return \\x1b[5C for alt+right', function() {
|
|
||||||
assert.equal(xterm.evaluateKeyEscapeSequence({ altKey: true, keyCode: 39 }).key, '\x1b[1;5C'); // CSI 5 C
|
describe('On macOS platforms', function() {
|
||||||
|
beforeEach(function() {
|
||||||
|
xterm.browser.isMac = true;
|
||||||
|
});
|
||||||
|
it('should return \\x1bb for alt+left', function() {
|
||||||
|
assert.equal(xterm.evaluateKeyEscapeSequence({ altKey: true, keyCode: 37 }).key, '\x1bb'); // CSI 5 D
|
||||||
|
});
|
||||||
|
it('should return \\x1bf for alt+right', function() {
|
||||||
|
assert.equal(xterm.evaluateKeyEscapeSequence({ altKey: true, keyCode: 39 }).key, '\x1bf'); // CSI 5 C
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return \\x1b[5A for alt+up', function() {
|
it('should return \\x1b[5A for alt+up', function() {
|
||||||
assert.equal(xterm.evaluateKeyEscapeSequence({ altKey: true, keyCode: 38 }).key, '\x1b[1;5A'); // CSI 5 A
|
assert.equal(xterm.evaluateKeyEscapeSequence({ altKey: true, keyCode: 38 }).key, '\x1b[1;5A'); // CSI 5 A
|
||||||
});
|
});
|
||||||
@@ -341,12 +376,17 @@ describe('xterm.js', function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('attachCustomEventHandler', function () {
|
describe('attachCustomKeyEventHandler', function () {
|
||||||
var evKeyDown = {
|
var evKeyDown = {
|
||||||
preventDefault: function() {},
|
preventDefault: function() {},
|
||||||
stopPropagation: function() {},
|
stopPropagation: function() {},
|
||||||
type: 'keydown'
|
type: 'keydown'
|
||||||
}
|
}
|
||||||
|
var evKeyPress = {
|
||||||
|
preventDefault: function() {},
|
||||||
|
stopPropagation: function() {},
|
||||||
|
type: 'keypress'
|
||||||
|
}
|
||||||
|
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
xterm.handler = function() {};
|
xterm.handler = function() {};
|
||||||
@@ -357,29 +397,39 @@ describe('xterm.js', function() {
|
|||||||
bind: function() {
|
bind: function() {
|
||||||
return function () { return true; }
|
return function () { return true; }
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
keypress: {
|
||||||
|
bind: function() {
|
||||||
|
return function () { return true; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should process the keydown event based on what the handler returns', function () {
|
it('should process the keydown/keypress event based on what the handler returns', function () {
|
||||||
assert.equal(xterm.keyDown(Object.assign({}, evKeyDown, { keyCode: 77 })), true);
|
assert.equal(xterm.keyDown(Object.assign({}, evKeyDown, { keyCode: 77 })), true);
|
||||||
xterm.attachCustomKeydownHandler(function (ev) {
|
assert.equal(xterm.keyPress(Object.assign({}, evKeyPress, { keyCode: 77 })), true);
|
||||||
|
xterm.attachCustomKeyEventHandler(function (ev) {
|
||||||
return ev.keyCode === 77;
|
return ev.keyCode === 77;
|
||||||
});
|
});
|
||||||
assert.equal(xterm.keyDown(Object.assign({}, evKeyDown, { keyCode: 77 })), true);
|
assert.equal(xterm.keyDown(Object.assign({}, evKeyDown, { keyCode: 77 })), true);
|
||||||
xterm.attachCustomKeydownHandler(function (ev) {
|
assert.equal(xterm.keyPress(Object.assign({}, evKeyPress, { keyCode: 77 })), true);
|
||||||
|
xterm.attachCustomKeyEventHandler(function (ev) {
|
||||||
return ev.keyCode !== 77;
|
return ev.keyCode !== 77;
|
||||||
});
|
});
|
||||||
assert.equal(xterm.keyDown(Object.assign({}, evKeyDown, { keyCode: 77 })), false);
|
assert.equal(xterm.keyDown(Object.assign({}, evKeyDown, { keyCode: 77 })), false);
|
||||||
|
assert.equal(xterm.keyPress(Object.assign({}, evKeyPress, { keyCode: 77 })), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should alive after reset(ESC c Full Reset (RIS))', function () {
|
it('should alive after reset(ESC c Full Reset (RIS))', function () {
|
||||||
xterm.attachCustomKeydownHandler(function (ev) {
|
xterm.attachCustomKeyEventHandler(function (ev) {
|
||||||
return ev.keyCode !== 77;
|
return ev.keyCode !== 77;
|
||||||
});
|
});
|
||||||
assert.equal(xterm.keyDown(Object.assign({}, evKeyDown, { keyCode: 77 })), false);
|
assert.equal(xterm.keyDown(Object.assign({}, evKeyDown, { keyCode: 77 })), false);
|
||||||
|
assert.equal(xterm.keyPress(Object.assign({}, evKeyPress, { keyCode: 77 })), false);
|
||||||
xterm.reset();
|
xterm.reset();
|
||||||
assert.equal(xterm.keyDown(Object.assign({}, evKeyDown, { keyCode: 77 })), false);
|
assert.equal(xterm.keyDown(Object.assign({}, evKeyDown, { keyCode: 77 })), false);
|
||||||
|
assert.equal(xterm.keyPress(Object.assign({}, evKeyPress, { keyCode: 77 })), false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -521,14 +571,15 @@ describe('xterm.js', function() {
|
|||||||
|
|
||||||
describe('unicode - surrogates', function() {
|
describe('unicode - surrogates', function() {
|
||||||
it('2 characters per cell', function () {
|
it('2 characters per cell', function () {
|
||||||
|
this.timeout(10000); // This is needed because istanbul patches code and slows it down
|
||||||
var high = String.fromCharCode(0xD800);
|
var high = String.fromCharCode(0xD800);
|
||||||
for (var i=0xDC00; i<=0xDCFF; ++i) {
|
for (var i=0xDC00; i<=0xDCFF; ++i) {
|
||||||
xterm.write(high + String.fromCharCode(i));
|
xterm.write(high + String.fromCharCode(i));
|
||||||
var tchar = xterm.lines[0][0];
|
var tchar = xterm.lines.get(0)[0];
|
||||||
expect(tchar[1]).eql(high + String.fromCharCode(i));
|
expect(tchar[1]).eql(high + String.fromCharCode(i));
|
||||||
expect(tchar[1].length).eql(2);
|
expect(tchar[1].length).eql(2);
|
||||||
expect(tchar[2]).eql(1);
|
expect(tchar[2]).eql(1);
|
||||||
expect(xterm.lines[0][1][1]).eql(' ');
|
expect(xterm.lines.get(0)[1][1]).eql(' ');
|
||||||
xterm.reset();
|
xterm.reset();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -537,9 +588,9 @@ describe('xterm.js', function() {
|
|||||||
for (var i=0xDC00; i<=0xDCFF; ++i) {
|
for (var i=0xDC00; i<=0xDCFF; ++i) {
|
||||||
xterm.x = xterm.cols - 1;
|
xterm.x = xterm.cols - 1;
|
||||||
xterm.write(high + String.fromCharCode(i));
|
xterm.write(high + String.fromCharCode(i));
|
||||||
expect(xterm.lines[0][xterm.x-1][1]).eql(high + String.fromCharCode(i));
|
expect(xterm.lines.get(0)[xterm.x-1][1]).eql(high + String.fromCharCode(i));
|
||||||
expect(xterm.lines[0][xterm.x-1][1].length).eql(2);
|
expect(xterm.lines.get(0)[xterm.x-1][1].length).eql(2);
|
||||||
expect(xterm.lines[1][0][1]).eql(' ');
|
expect(xterm.lines.get(1)[0][1]).eql(' ');
|
||||||
xterm.reset();
|
xterm.reset();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -549,10 +600,10 @@ describe('xterm.js', function() {
|
|||||||
xterm.x = xterm.cols - 1;
|
xterm.x = xterm.cols - 1;
|
||||||
xterm.wraparoundMode = true;
|
xterm.wraparoundMode = true;
|
||||||
xterm.write('a' + high + String.fromCharCode(i));
|
xterm.write('a' + high + String.fromCharCode(i));
|
||||||
expect(xterm.lines[0][xterm.cols-1][1]).eql('a');
|
expect(xterm.lines.get(0)[xterm.cols-1][1]).eql('a');
|
||||||
expect(xterm.lines[1][0][1]).eql(high + String.fromCharCode(i));
|
expect(xterm.lines.get(1)[0][1]).eql(high + String.fromCharCode(i));
|
||||||
expect(xterm.lines[1][0][1].length).eql(2);
|
expect(xterm.lines.get(1)[0][1].length).eql(2);
|
||||||
expect(xterm.lines[1][1][1]).eql(' ');
|
expect(xterm.lines.get(1)[1][1]).eql(' ');
|
||||||
xterm.reset();
|
xterm.reset();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -562,9 +613,10 @@ describe('xterm.js', function() {
|
|||||||
xterm.x = xterm.cols - 1;
|
xterm.x = xterm.cols - 1;
|
||||||
xterm.wraparoundMode = false;
|
xterm.wraparoundMode = false;
|
||||||
xterm.write('a' + high + String.fromCharCode(i));
|
xterm.write('a' + high + String.fromCharCode(i));
|
||||||
expect(xterm.lines[0][xterm.cols-1][1]).eql(high + String.fromCharCode(i));
|
// auto wraparound mode should cut off the rest of the line
|
||||||
expect(xterm.lines[0][xterm.cols-1][1].length).eql(2);
|
expect(xterm.lines.get(0)[xterm.cols-1][1]).eql('a');
|
||||||
expect(xterm.lines[1][1][1]).eql(' ');
|
expect(xterm.lines.get(0)[xterm.cols-1][1].length).eql(1);
|
||||||
|
expect(xterm.lines.get(1)[1][1]).eql(' ');
|
||||||
xterm.reset();
|
xterm.reset();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -573,11 +625,11 @@ describe('xterm.js', function() {
|
|||||||
for (var i=0xDC00; i<=0xDCFF; ++i) {
|
for (var i=0xDC00; i<=0xDCFF; ++i) {
|
||||||
xterm.write(high);
|
xterm.write(high);
|
||||||
xterm.write(String.fromCharCode(i));
|
xterm.write(String.fromCharCode(i));
|
||||||
var tchar = xterm.lines[0][0];
|
var tchar = xterm.lines.get(0)[0];
|
||||||
expect(tchar[1]).eql(high + String.fromCharCode(i));
|
expect(tchar[1]).eql(high + String.fromCharCode(i));
|
||||||
expect(tchar[1].length).eql(2);
|
expect(tchar[1].length).eql(2);
|
||||||
expect(tchar[2]).eql(1);
|
expect(tchar[2]).eql(1);
|
||||||
expect(xterm.lines[0][1][1]).eql(' ');
|
expect(xterm.lines.get(0)[1][1]).eql(' ');
|
||||||
xterm.reset();
|
xterm.reset();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -586,30 +638,30 @@ describe('xterm.js', function() {
|
|||||||
describe('unicode - combining characters', function() {
|
describe('unicode - combining characters', function() {
|
||||||
it('café', function () {
|
it('café', function () {
|
||||||
xterm.write('cafe\u0301');
|
xterm.write('cafe\u0301');
|
||||||
expect(xterm.lines[0][3][1]).eql('e\u0301');
|
expect(xterm.lines.get(0)[3][1]).eql('e\u0301');
|
||||||
expect(xterm.lines[0][3][1].length).eql(2);
|
expect(xterm.lines.get(0)[3][1].length).eql(2);
|
||||||
expect(xterm.lines[0][3][2]).eql(1);
|
expect(xterm.lines.get(0)[3][2]).eql(1);
|
||||||
});
|
});
|
||||||
it('café - end of line', function() {
|
it('café - end of line', function() {
|
||||||
xterm.x = xterm.cols - 1 - 3;
|
xterm.x = xterm.cols - 1 - 3;
|
||||||
xterm.write('cafe\u0301');
|
xterm.write('cafe\u0301');
|
||||||
expect(xterm.lines[0][xterm.cols-1][1]).eql('e\u0301');
|
expect(xterm.lines.get(0)[xterm.cols-1][1]).eql('e\u0301');
|
||||||
expect(xterm.lines[0][xterm.cols-1][1].length).eql(2);
|
expect(xterm.lines.get(0)[xterm.cols-1][1].length).eql(2);
|
||||||
expect(xterm.lines[0][xterm.cols-1][2]).eql(1);
|
expect(xterm.lines.get(0)[xterm.cols-1][2]).eql(1);
|
||||||
expect(xterm.lines[0][1][1]).eql(' ');
|
expect(xterm.lines.get(0)[1][1]).eql(' ');
|
||||||
expect(xterm.lines[0][1][1].length).eql(1);
|
expect(xterm.lines.get(0)[1][1].length).eql(1);
|
||||||
expect(xterm.lines[0][1][2]).eql(1);
|
expect(xterm.lines.get(0)[1][2]).eql(1);
|
||||||
});
|
});
|
||||||
it('multiple combined é', function() {
|
it('multiple combined é', function() {
|
||||||
xterm.wraparoundMode = true;
|
xterm.wraparoundMode = true;
|
||||||
xterm.write(Array(100).join('e\u0301'));
|
xterm.write(Array(100).join('e\u0301'));
|
||||||
for (var i=0; i<xterm.cols; ++i) {
|
for (var i=0; i<xterm.cols; ++i) {
|
||||||
var tchar = xterm.lines[0][i];
|
var tchar = xterm.lines.get(0)[i];
|
||||||
expect(tchar[1]).eql('e\u0301');
|
expect(tchar[1]).eql('e\u0301');
|
||||||
expect(tchar[1].length).eql(2);
|
expect(tchar[1].length).eql(2);
|
||||||
expect(tchar[2]).eql(1);
|
expect(tchar[2]).eql(1);
|
||||||
}
|
}
|
||||||
tchar = xterm.lines[1][0];
|
tchar = xterm.lines.get(1)[0];
|
||||||
expect(tchar[1]).eql('e\u0301');
|
expect(tchar[1]).eql('e\u0301');
|
||||||
expect(tchar[1].length).eql(2);
|
expect(tchar[1].length).eql(2);
|
||||||
expect(tchar[2]).eql(1);
|
expect(tchar[2]).eql(1);
|
||||||
@@ -618,12 +670,12 @@ describe('xterm.js', function() {
|
|||||||
xterm.wraparoundMode = true;
|
xterm.wraparoundMode = true;
|
||||||
xterm.write(Array(100).join('\uD800\uDC00\u0301'));
|
xterm.write(Array(100).join('\uD800\uDC00\u0301'));
|
||||||
for (var i=0; i<xterm.cols; ++i) {
|
for (var i=0; i<xterm.cols; ++i) {
|
||||||
var tchar = xterm.lines[0][i];
|
var tchar = xterm.lines.get(0)[i];
|
||||||
expect(tchar[1]).eql('\uD800\uDC00\u0301');
|
expect(tchar[1]).eql('\uD800\uDC00\u0301');
|
||||||
expect(tchar[1].length).eql(3);
|
expect(tchar[1].length).eql(3);
|
||||||
expect(tchar[2]).eql(1);
|
expect(tchar[2]).eql(1);
|
||||||
}
|
}
|
||||||
tchar = xterm.lines[1][0];
|
tchar = xterm.lines.get(1)[0];
|
||||||
expect(tchar[1]).eql('\uD800\uDC00\u0301');
|
expect(tchar[1]).eql('\uD800\uDC00\u0301');
|
||||||
expect(tchar[1].length).eql(3);
|
expect(tchar[1].length).eql(3);
|
||||||
expect(tchar[2]).eql(1);
|
expect(tchar[2]).eql(1);
|
||||||
@@ -646,7 +698,7 @@ describe('xterm.js', function() {
|
|||||||
xterm.wraparoundMode = true;
|
xterm.wraparoundMode = true;
|
||||||
xterm.write(Array(50).join('¥'));
|
xterm.write(Array(50).join('¥'));
|
||||||
for (var i=0; i<xterm.cols; ++i) {
|
for (var i=0; i<xterm.cols; ++i) {
|
||||||
var tchar = xterm.lines[0][i];
|
var tchar = xterm.lines.get(0)[i];
|
||||||
if (i % 2) {
|
if (i % 2) {
|
||||||
expect(tchar[1]).eql('');
|
expect(tchar[1]).eql('');
|
||||||
expect(tchar[1].length).eql(0);
|
expect(tchar[1].length).eql(0);
|
||||||
@@ -657,7 +709,7 @@ describe('xterm.js', function() {
|
|||||||
expect(tchar[2]).eql(2);
|
expect(tchar[2]).eql(2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tchar = xterm.lines[1][0];
|
tchar = xterm.lines.get(1)[0];
|
||||||
expect(tchar[1]).eql('¥');
|
expect(tchar[1]).eql('¥');
|
||||||
expect(tchar[1].length).eql(1);
|
expect(tchar[1].length).eql(1);
|
||||||
expect(tchar[2]).eql(2);
|
expect(tchar[2]).eql(2);
|
||||||
@@ -667,7 +719,7 @@ describe('xterm.js', function() {
|
|||||||
xterm.x = 1;
|
xterm.x = 1;
|
||||||
xterm.write(Array(50).join('¥'));
|
xterm.write(Array(50).join('¥'));
|
||||||
for (var i=1; i<xterm.cols-1; ++i) {
|
for (var i=1; i<xterm.cols-1; ++i) {
|
||||||
var tchar = xterm.lines[0][i];
|
var tchar = xterm.lines.get(0)[i];
|
||||||
if (!(i % 2)) {
|
if (!(i % 2)) {
|
||||||
expect(tchar[1]).eql('');
|
expect(tchar[1]).eql('');
|
||||||
expect(tchar[1].length).eql(0);
|
expect(tchar[1].length).eql(0);
|
||||||
@@ -678,11 +730,11 @@ describe('xterm.js', function() {
|
|||||||
expect(tchar[2]).eql(2);
|
expect(tchar[2]).eql(2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tchar = xterm.lines[0][xterm.cols-1];
|
tchar = xterm.lines.get(0)[xterm.cols-1];
|
||||||
expect(tchar[1]).eql(' ');
|
expect(tchar[1]).eql(' ');
|
||||||
expect(tchar[1].length).eql(1);
|
expect(tchar[1].length).eql(1);
|
||||||
expect(tchar[2]).eql(1);
|
expect(tchar[2]).eql(1);
|
||||||
tchar = xterm.lines[1][0];
|
tchar = xterm.lines.get(1)[0];
|
||||||
expect(tchar[1]).eql('¥');
|
expect(tchar[1]).eql('¥');
|
||||||
expect(tchar[1].length).eql(1);
|
expect(tchar[1].length).eql(1);
|
||||||
expect(tchar[2]).eql(2);
|
expect(tchar[2]).eql(2);
|
||||||
@@ -692,7 +744,7 @@ describe('xterm.js', function() {
|
|||||||
xterm.x = 1;
|
xterm.x = 1;
|
||||||
xterm.write(Array(50).join('¥\u0301'));
|
xterm.write(Array(50).join('¥\u0301'));
|
||||||
for (var i=1; i<xterm.cols-1; ++i) {
|
for (var i=1; i<xterm.cols-1; ++i) {
|
||||||
var tchar = xterm.lines[0][i];
|
var tchar = xterm.lines.get(0)[i];
|
||||||
if (!(i % 2)) {
|
if (!(i % 2)) {
|
||||||
expect(tchar[1]).eql('');
|
expect(tchar[1]).eql('');
|
||||||
expect(tchar[1].length).eql(0);
|
expect(tchar[1].length).eql(0);
|
||||||
@@ -703,11 +755,11 @@ describe('xterm.js', function() {
|
|||||||
expect(tchar[2]).eql(2);
|
expect(tchar[2]).eql(2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tchar = xterm.lines[0][xterm.cols-1];
|
tchar = xterm.lines.get(0)[xterm.cols-1];
|
||||||
expect(tchar[1]).eql(' ');
|
expect(tchar[1]).eql(' ');
|
||||||
expect(tchar[1].length).eql(1);
|
expect(tchar[1].length).eql(1);
|
||||||
expect(tchar[2]).eql(1);
|
expect(tchar[2]).eql(1);
|
||||||
tchar = xterm.lines[1][0];
|
tchar = xterm.lines.get(1)[0];
|
||||||
expect(tchar[1]).eql('¥\u0301');
|
expect(tchar[1]).eql('¥\u0301');
|
||||||
expect(tchar[1].length).eql(2);
|
expect(tchar[1].length).eql(2);
|
||||||
expect(tchar[2]).eql(2);
|
expect(tchar[2]).eql(2);
|
||||||
@@ -716,7 +768,7 @@ describe('xterm.js', function() {
|
|||||||
xterm.wraparoundMode = true;
|
xterm.wraparoundMode = true;
|
||||||
xterm.write(Array(50).join('¥\u0301'));
|
xterm.write(Array(50).join('¥\u0301'));
|
||||||
for (var i=0; i<xterm.cols; ++i) {
|
for (var i=0; i<xterm.cols; ++i) {
|
||||||
var tchar = xterm.lines[0][i];
|
var tchar = xterm.lines.get(0)[i];
|
||||||
if (i % 2) {
|
if (i % 2) {
|
||||||
expect(tchar[1]).eql('');
|
expect(tchar[1]).eql('');
|
||||||
expect(tchar[1].length).eql(0);
|
expect(tchar[1].length).eql(0);
|
||||||
@@ -727,7 +779,7 @@ describe('xterm.js', function() {
|
|||||||
expect(tchar[2]).eql(2);
|
expect(tchar[2]).eql(2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tchar = xterm.lines[1][0];
|
tchar = xterm.lines.get(1)[0];
|
||||||
expect(tchar[1]).eql('¥\u0301');
|
expect(tchar[1]).eql('¥\u0301');
|
||||||
expect(tchar[1].length).eql(2);
|
expect(tchar[1].length).eql(2);
|
||||||
expect(tchar[2]).eql(2);
|
expect(tchar[2]).eql(2);
|
||||||
@@ -737,7 +789,7 @@ describe('xterm.js', function() {
|
|||||||
xterm.x = 1;
|
xterm.x = 1;
|
||||||
xterm.write(Array(50).join('\ud843\ude6d\u0301'));
|
xterm.write(Array(50).join('\ud843\ude6d\u0301'));
|
||||||
for (var i=1; i<xterm.cols-1; ++i) {
|
for (var i=1; i<xterm.cols-1; ++i) {
|
||||||
var tchar = xterm.lines[0][i];
|
var tchar = xterm.lines.get(0)[i];
|
||||||
if (!(i % 2)) {
|
if (!(i % 2)) {
|
||||||
expect(tchar[1]).eql('');
|
expect(tchar[1]).eql('');
|
||||||
expect(tchar[1].length).eql(0);
|
expect(tchar[1].length).eql(0);
|
||||||
@@ -748,11 +800,11 @@ describe('xterm.js', function() {
|
|||||||
expect(tchar[2]).eql(2);
|
expect(tchar[2]).eql(2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tchar = xterm.lines[0][xterm.cols-1];
|
tchar = xterm.lines.get(0)[xterm.cols-1];
|
||||||
expect(tchar[1]).eql(' ');
|
expect(tchar[1]).eql(' ');
|
||||||
expect(tchar[1].length).eql(1);
|
expect(tchar[1].length).eql(1);
|
||||||
expect(tchar[2]).eql(1);
|
expect(tchar[2]).eql(1);
|
||||||
tchar = xterm.lines[1][0];
|
tchar = xterm.lines.get(1)[0];
|
||||||
expect(tchar[1]).eql('\ud843\ude6d\u0301');
|
expect(tchar[1]).eql('\ud843\ude6d\u0301');
|
||||||
expect(tchar[1].length).eql(3);
|
expect(tchar[1].length).eql(3);
|
||||||
expect(tchar[2]).eql(2);
|
expect(tchar[2]).eql(2);
|
||||||
@@ -761,7 +813,7 @@ describe('xterm.js', function() {
|
|||||||
xterm.wraparoundMode = true;
|
xterm.wraparoundMode = true;
|
||||||
xterm.write(Array(50).join('\ud843\ude6d\u0301'));
|
xterm.write(Array(50).join('\ud843\ude6d\u0301'));
|
||||||
for (var i=0; i<xterm.cols; ++i) {
|
for (var i=0; i<xterm.cols; ++i) {
|
||||||
var tchar = xterm.lines[0][i];
|
var tchar = xterm.lines.get(0)[i];
|
||||||
if (i % 2) {
|
if (i % 2) {
|
||||||
expect(tchar[1]).eql('');
|
expect(tchar[1]).eql('');
|
||||||
expect(tchar[1].length).eql(0);
|
expect(tchar[1].length).eql(0);
|
||||||
@@ -772,7 +824,7 @@ describe('xterm.js', function() {
|
|||||||
expect(tchar[2]).eql(2);
|
expect(tchar[2]).eql(2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tchar = xterm.lines[1][0];
|
tchar = xterm.lines.get(1)[0];
|
||||||
expect(tchar[1]).eql('\ud843\ude6d\u0301');
|
expect(tchar[1]).eql('\ud843\ude6d\u0301');
|
||||||
expect(tchar[1].length).eql(3);
|
expect(tchar[1].length).eql(3);
|
||||||
expect(tchar[2]).eql(2);
|
expect(tchar[2]).eql(2);
|
||||||
@@ -786,11 +838,11 @@ describe('xterm.js', function() {
|
|||||||
xterm.y = 0;
|
xterm.y = 0;
|
||||||
xterm.insertMode = true;
|
xterm.insertMode = true;
|
||||||
xterm.write('abcde');
|
xterm.write('abcde');
|
||||||
expect(xterm.lines[0].length).eql(xterm.cols);
|
expect(xterm.lines.get(0).length).eql(xterm.cols);
|
||||||
expect(xterm.lines[0][10][1]).eql('a');
|
expect(xterm.lines.get(0)[10][1]).eql('a');
|
||||||
expect(xterm.lines[0][14][1]).eql('e');
|
expect(xterm.lines.get(0)[14][1]).eql('e');
|
||||||
expect(xterm.lines[0][15][1]).eql('0');
|
expect(xterm.lines.get(0)[15][1]).eql('0');
|
||||||
expect(xterm.lines[0][79][1]).eql('4');
|
expect(xterm.lines.get(0)[79][1]).eql('4');
|
||||||
});
|
});
|
||||||
it('fullwidth - insert', function() {
|
it('fullwidth - insert', function() {
|
||||||
xterm.write(Array(9).join('0123456789').slice(-80));
|
xterm.write(Array(9).join('0123456789').slice(-80));
|
||||||
@@ -798,12 +850,12 @@ describe('xterm.js', function() {
|
|||||||
xterm.y = 0;
|
xterm.y = 0;
|
||||||
xterm.insertMode = true;
|
xterm.insertMode = true;
|
||||||
xterm.write('¥¥¥');
|
xterm.write('¥¥¥');
|
||||||
expect(xterm.lines[0].length).eql(xterm.cols);
|
expect(xterm.lines.get(0).length).eql(xterm.cols);
|
||||||
expect(xterm.lines[0][10][1]).eql('¥');
|
expect(xterm.lines.get(0)[10][1]).eql('¥');
|
||||||
expect(xterm.lines[0][11][1]).eql('');
|
expect(xterm.lines.get(0)[11][1]).eql('');
|
||||||
expect(xterm.lines[0][14][1]).eql('¥');
|
expect(xterm.lines.get(0)[14][1]).eql('¥');
|
||||||
expect(xterm.lines[0][15][1]).eql('');
|
expect(xterm.lines.get(0)[15][1]).eql('');
|
||||||
expect(xterm.lines[0][79][1]).eql('3');
|
expect(xterm.lines.get(0)[79][1]).eql('3');
|
||||||
});
|
});
|
||||||
it('fullwidth - right border', function() {
|
it('fullwidth - right border', function() {
|
||||||
xterm.write(Array(41).join('¥'));
|
xterm.write(Array(41).join('¥'));
|
||||||
@@ -811,15 +863,15 @@ describe('xterm.js', function() {
|
|||||||
xterm.y = 0;
|
xterm.y = 0;
|
||||||
xterm.insertMode = true;
|
xterm.insertMode = true;
|
||||||
xterm.write('a');
|
xterm.write('a');
|
||||||
expect(xterm.lines[0].length).eql(xterm.cols);
|
expect(xterm.lines.get(0).length).eql(xterm.cols);
|
||||||
expect(xterm.lines[0][10][1]).eql('a');
|
expect(xterm.lines.get(0)[10][1]).eql('a');
|
||||||
expect(xterm.lines[0][11][1]).eql('¥');
|
expect(xterm.lines.get(0)[11][1]).eql('¥');
|
||||||
expect(xterm.lines[0][79][1]).eql(' '); // fullwidth char got replaced
|
expect(xterm.lines.get(0)[79][1]).eql(' '); // fullwidth char got replaced
|
||||||
xterm.write('b');
|
xterm.write('b');
|
||||||
expect(xterm.lines[0].length).eql(xterm.cols);
|
expect(xterm.lines.get(0).length).eql(xterm.cols);
|
||||||
expect(xterm.lines[0][11][1]).eql('b');
|
expect(xterm.lines.get(0)[11][1]).eql('b');
|
||||||
expect(xterm.lines[0][12][1]).eql('¥');
|
expect(xterm.lines.get(0)[12][1]).eql('¥');
|
||||||
expect(xterm.lines[0][79][1]).eql(''); // empty cell after fullwidth
|
expect(xterm.lines.get(0)[79][1]).eql(''); // empty cell after fullwidth
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
/**
|
|
||||||
* Attributes and methods to help with identifying the current browser and platform.
|
|
||||||
* @module xterm/utils/Browser
|
|
||||||
* @license MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { contains } from './Generic.js';
|
|
||||||
|
|
||||||
let isNode = (typeof navigator == 'undefined') ? true : false;
|
|
||||||
let userAgent = (isNode) ? 'node' : navigator.userAgent;
|
|
||||||
let platform = (isNode) ? 'node' : navigator.platform;
|
|
||||||
|
|
||||||
export let isFirefox = !!~userAgent.indexOf('Firefox');
|
|
||||||
export let isMSIE = !!~userAgent.indexOf('MSIE') || !!~userAgent.indexOf('Trident');
|
|
||||||
|
|
||||||
// Find the users platform. We use this to interpret the meta key
|
|
||||||
// and ISO third level shifts.
|
|
||||||
// http://stackoverflow.com/q/19877924/577598
|
|
||||||
export let isMac = contains(['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K'], platform);
|
|
||||||
export let isIpad = platform === 'iPad';
|
|
||||||
export let isIphone = platform === 'iPhone';
|
|
||||||
export let isMSWindows = contains(['Windows', 'Win16', 'Win32', 'WinCE'], platform);
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* Attributes and methods to help with identifying the current browser and platform.
|
||||||
|
* @module xterm/utils/Browser
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { contains } from './Generic';
|
||||||
|
|
||||||
|
const isNode = (typeof navigator === 'undefined') ? true : false;
|
||||||
|
const userAgent = (isNode) ? 'node' : navigator.userAgent;
|
||||||
|
const platform = (isNode) ? 'node' : navigator.platform;
|
||||||
|
|
||||||
|
export const isFirefox = !!~userAgent.indexOf('Firefox');
|
||||||
|
export const isMSIE = !!~userAgent.indexOf('MSIE') || !!~userAgent.indexOf('Trident');
|
||||||
|
|
||||||
|
// Find the users platform. We use this to interpret the meta key
|
||||||
|
// and ISO third level shifts.
|
||||||
|
// http://stackoverflow.com/q/19877924/577598
|
||||||
|
export const isMac = contains(['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K'], platform);
|
||||||
|
export const isIpad = platform === 'iPad';
|
||||||
|
export const isIphone = platform === 'iPhone';
|
||||||
|
export const isMSWindows = contains(['Windows', 'Win16', 'Win32', 'WinCE'], platform);
|
||||||
|
export const isLinux = platform.indexOf('Linux') >= 0;
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
/**
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
import jsdom = require('jsdom');
|
||||||
|
import { assert } from 'chai';
|
||||||
|
import { ICharMeasure, ITerminal } from '../Interfaces';
|
||||||
|
import { CharMeasure } from './CharMeasure';
|
||||||
|
|
||||||
|
describe('CharMeasure', () => {
|
||||||
|
let window: Window;
|
||||||
|
let document: Document;
|
||||||
|
|
||||||
|
let container: HTMLElement;
|
||||||
|
let charMeasure: ICharMeasure;
|
||||||
|
|
||||||
|
beforeEach(done => {
|
||||||
|
jsdom.env('', (err, w) => {
|
||||||
|
window = w;
|
||||||
|
document = window.document;
|
||||||
|
container = document.createElement('div');
|
||||||
|
document.body.appendChild(container);
|
||||||
|
charMeasure = new CharMeasure(document, container);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('measure', () => {
|
||||||
|
it('should set _measureElement on first call', () => {
|
||||||
|
charMeasure.measure();
|
||||||
|
assert.isDefined((<any>charMeasure)._measureElement, 'CharMeasure.measure should have created _measureElement');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be performed async on first call', done => {
|
||||||
|
assert.equal(charMeasure.width, null);
|
||||||
|
charMeasure.measure();
|
||||||
|
// Mock getBoundingClientRect since jsdom doesn't have a layout engine
|
||||||
|
(<any>charMeasure)._measureElement.getBoundingClientRect = () => {
|
||||||
|
return { width: 1, height: 1 };
|
||||||
|
};
|
||||||
|
assert.equal(charMeasure.width, null);
|
||||||
|
setTimeout(() => {
|
||||||
|
assert.equal(charMeasure.width, 1);
|
||||||
|
done();
|
||||||
|
}, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be performed sync on successive calls', done => {
|
||||||
|
charMeasure.measure();
|
||||||
|
// Mock getBoundingClientRect since jsdom doesn't have a layout engine
|
||||||
|
(<any>charMeasure)._measureElement.getBoundingClientRect = () => {
|
||||||
|
return { width: 1, height: 1 };
|
||||||
|
};
|
||||||
|
setTimeout(() => {
|
||||||
|
const firstWidth = charMeasure.width;
|
||||||
|
// Mock getBoundingClientRect since jsdom doesn't have a layout engine
|
||||||
|
(<any>charMeasure)._measureElement.getBoundingClientRect = () => {
|
||||||
|
return { width: 2, height: 2 };
|
||||||
|
};
|
||||||
|
charMeasure.measure();
|
||||||
|
assert.equal(charMeasure.width, firstWidth * 2);
|
||||||
|
done();
|
||||||
|
}, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should NOT do a measure when the parent is hidden', done => {
|
||||||
|
charMeasure.measure();
|
||||||
|
setTimeout(() => {
|
||||||
|
const firstWidth = charMeasure.width;
|
||||||
|
container.style.display = 'none';
|
||||||
|
container.style.fontSize = '2em';
|
||||||
|
charMeasure.measure();
|
||||||
|
assert.equal(charMeasure.width, firstWidth);
|
||||||
|
done();
|
||||||
|
}, 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
/**
|
||||||
|
* @module xterm/utils/CharMeasure
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { EventEmitter } from '../EventEmitter.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class that measures the size of a character.
|
||||||
|
*/
|
||||||
|
export class CharMeasure extends EventEmitter {
|
||||||
|
private _document: Document;
|
||||||
|
private _parentElement: HTMLElement;
|
||||||
|
private _measureElement: HTMLElement;
|
||||||
|
private _width: number;
|
||||||
|
private _height: number;
|
||||||
|
|
||||||
|
constructor(document: Document, parentElement: HTMLElement) {
|
||||||
|
super();
|
||||||
|
this._document = document;
|
||||||
|
this._parentElement = parentElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get width(): number {
|
||||||
|
return this._width;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get height(): number {
|
||||||
|
return this._height;
|
||||||
|
}
|
||||||
|
|
||||||
|
public measure(): void {
|
||||||
|
if (!this._measureElement) {
|
||||||
|
this._measureElement = this._document.createElement('span');
|
||||||
|
this._measureElement.style.position = 'absolute';
|
||||||
|
this._measureElement.style.top = '0';
|
||||||
|
this._measureElement.style.left = '-9999em';
|
||||||
|
this._measureElement.textContent = 'W';
|
||||||
|
this._measureElement.setAttribute('aria-hidden', 'true');
|
||||||
|
this._parentElement.appendChild(this._measureElement);
|
||||||
|
// Perform _doMeasure async if the element was just attached as sometimes
|
||||||
|
// getBoundingClientRect does not return accurate values without this.
|
||||||
|
setTimeout(() => this._doMeasure(), 0);
|
||||||
|
} else {
|
||||||
|
this._doMeasure();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _doMeasure(): void {
|
||||||
|
const geometry = this._measureElement.getBoundingClientRect();
|
||||||
|
// The element is likely currently display:none, we should retain the
|
||||||
|
// previous value.
|
||||||
|
if (geometry.width === 0 || geometry.height === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this._width !== geometry.width || this._height !== geometry.height) {
|
||||||
|
this._width = geometry.width;
|
||||||
|
this._height = geometry.height;
|
||||||
|
this.emit('charsizechanged');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,255 @@
|
|||||||
|
import { assert } from 'chai';
|
||||||
|
import { CircularList } from './CircularList';
|
||||||
|
|
||||||
|
describe('CircularList', () => {
|
||||||
|
describe('push', () => {
|
||||||
|
it('should push values onto the array', () => {
|
||||||
|
const list = new CircularList<string>(5);
|
||||||
|
list.push('1');
|
||||||
|
list.push('2');
|
||||||
|
list.push('3');
|
||||||
|
list.push('4');
|
||||||
|
list.push('5');
|
||||||
|
assert.equal(list.get(0), '1');
|
||||||
|
assert.equal(list.get(1), '2');
|
||||||
|
assert.equal(list.get(2), '3');
|
||||||
|
assert.equal(list.get(3), '4');
|
||||||
|
assert.equal(list.get(4), '5');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should push old values from the start out of the array when max length is reached', () => {
|
||||||
|
const list = new CircularList<string>(2);
|
||||||
|
list.push('1');
|
||||||
|
list.push('2');
|
||||||
|
assert.equal(list.get(0), '1');
|
||||||
|
assert.equal(list.get(1), '2');
|
||||||
|
list.push('3');
|
||||||
|
assert.equal(list.get(0), '2');
|
||||||
|
assert.equal(list.get(1), '3');
|
||||||
|
list.push('4');
|
||||||
|
assert.equal(list.get(0), '3');
|
||||||
|
assert.equal(list.get(1), '4');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('maxLength', () => {
|
||||||
|
it('should increase the size of the list', () => {
|
||||||
|
const list = new CircularList<string>(2);
|
||||||
|
list.push('1');
|
||||||
|
list.push('2');
|
||||||
|
assert.equal(list.get(0), '1');
|
||||||
|
assert.equal(list.get(1), '2');
|
||||||
|
list.maxLength = 4;
|
||||||
|
list.push('3');
|
||||||
|
list.push('4');
|
||||||
|
assert.equal(list.get(0), '1');
|
||||||
|
assert.equal(list.get(1), '2');
|
||||||
|
assert.equal(list.get(2), '3');
|
||||||
|
assert.equal(list.get(3), '4');
|
||||||
|
list.push('wrapped');
|
||||||
|
assert.equal(list.get(0), '2');
|
||||||
|
assert.equal(list.get(1), '3');
|
||||||
|
assert.equal(list.get(2), '4');
|
||||||
|
assert.equal(list.get(3), 'wrapped');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the maximum length of the list', () => {
|
||||||
|
const list = new CircularList<string>(2);
|
||||||
|
assert.equal(list.maxLength, 2);
|
||||||
|
list.push('1');
|
||||||
|
list.push('2');
|
||||||
|
assert.equal(list.maxLength, 2);
|
||||||
|
list.push('3');
|
||||||
|
assert.equal(list.maxLength, 2);
|
||||||
|
list.maxLength = 4;
|
||||||
|
assert.equal(list.maxLength, 4);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('length', () => {
|
||||||
|
it('should return the current length of the list, capped at the maximum length', () => {
|
||||||
|
const list = new CircularList<string>(2);
|
||||||
|
assert.equal(list.length, 0);
|
||||||
|
list.push('1');
|
||||||
|
assert.equal(list.length, 1);
|
||||||
|
list.push('2');
|
||||||
|
assert.equal(list.length, 2);
|
||||||
|
list.push('3');
|
||||||
|
assert.equal(list.length, 2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('splice', () => {
|
||||||
|
it('should delete items', () => {
|
||||||
|
const list = new CircularList<string>(2);
|
||||||
|
list.push('1');
|
||||||
|
list.push('2');
|
||||||
|
list.splice(0, 1);
|
||||||
|
assert.equal(list.length, 1);
|
||||||
|
assert.equal(list.get(0), '2');
|
||||||
|
list.push('3');
|
||||||
|
list.splice(1, 1);
|
||||||
|
assert.equal(list.length, 1);
|
||||||
|
assert.equal(list.get(0), '2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should insert items', () => {
|
||||||
|
const list = new CircularList<string>(2);
|
||||||
|
list.push('1');
|
||||||
|
list.splice(0, 0, '2');
|
||||||
|
assert.equal(list.length, 2);
|
||||||
|
assert.equal(list.get(0), '2');
|
||||||
|
assert.equal(list.get(1), '1');
|
||||||
|
list.splice(1, 0, '3');
|
||||||
|
assert.equal(list.length, 2);
|
||||||
|
assert.equal(list.get(0), '3');
|
||||||
|
assert.equal(list.get(1), '1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete items then insert items', () => {
|
||||||
|
const list = new CircularList<string>(3);
|
||||||
|
list.push('1');
|
||||||
|
list.push('2');
|
||||||
|
list.splice(0, 1, '3', '4');
|
||||||
|
assert.equal(list.length, 3);
|
||||||
|
assert.equal(list.get(0), '3');
|
||||||
|
assert.equal(list.get(1), '4');
|
||||||
|
assert.equal(list.get(2), '2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should wrap the array correctly when more items are inserted than deleted', () => {
|
||||||
|
const list = new CircularList<string>(3);
|
||||||
|
list.push('1');
|
||||||
|
list.push('2');
|
||||||
|
list.splice(1, 0, '3', '4');
|
||||||
|
assert.equal(list.length, 3);
|
||||||
|
assert.equal(list.get(0), '3');
|
||||||
|
assert.equal(list.get(1), '4');
|
||||||
|
assert.equal(list.get(2), '2');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('trimStart', () => {
|
||||||
|
it('should remove items from the beginning of the list', () => {
|
||||||
|
const list = new CircularList<string>(5);
|
||||||
|
list.push('1');
|
||||||
|
list.push('2');
|
||||||
|
list.push('3');
|
||||||
|
list.push('4');
|
||||||
|
list.push('5');
|
||||||
|
list.trimStart(1);
|
||||||
|
assert.equal(list.length, 4);
|
||||||
|
assert.deepEqual(list.get(0), '2');
|
||||||
|
assert.deepEqual(list.get(1), '3');
|
||||||
|
assert.deepEqual(list.get(2), '4');
|
||||||
|
assert.deepEqual(list.get(3), '5');
|
||||||
|
list.trimStart(2);
|
||||||
|
assert.equal(list.length, 2);
|
||||||
|
assert.deepEqual(list.get(0), '4');
|
||||||
|
assert.deepEqual(list.get(1), '5');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove all items if the requested trim amount is larger than the list\'s length', () => {
|
||||||
|
const list = new CircularList<string>(5);
|
||||||
|
list.push('1');
|
||||||
|
list.trimStart(2);
|
||||||
|
assert.equal(list.length, 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('shiftElements', () => {
|
||||||
|
it('should not mutate the list when count is 0', () => {
|
||||||
|
const list = new CircularList<number>(5);
|
||||||
|
list.push(1);
|
||||||
|
list.push(2);
|
||||||
|
list.shiftElements(0, 0, 1);
|
||||||
|
assert.equal(list.length, 2);
|
||||||
|
assert.equal(list.get(0), 1);
|
||||||
|
assert.equal(list.get(1), 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw for invalid args', () => {
|
||||||
|
const list = new CircularList<number>(5);
|
||||||
|
list.push(1);
|
||||||
|
assert.throws(() => list.shiftElements(-1, 1, 1), 'start argument out of range');
|
||||||
|
assert.throws(() => list.shiftElements(1, 1, 1), 'start argument out of range');
|
||||||
|
assert.throws(() => list.shiftElements(0, 1, -1), 'Cannot shift elements in list beyond index 0');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should shift an element forward', () => {
|
||||||
|
const list = new CircularList<number>(5);
|
||||||
|
list.push(1);
|
||||||
|
list.push(2);
|
||||||
|
list.shiftElements(0, 1, 1);
|
||||||
|
assert.equal(list.length, 2);
|
||||||
|
assert.equal(list.get(0), 1);
|
||||||
|
assert.equal(list.get(1), 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should shift elements forward', () => {
|
||||||
|
const list = new CircularList<number>(5);
|
||||||
|
list.push(1);
|
||||||
|
list.push(2);
|
||||||
|
list.push(3);
|
||||||
|
list.push(4);
|
||||||
|
list.shiftElements(0, 2, 2);
|
||||||
|
assert.equal(list.length, 4);
|
||||||
|
assert.equal(list.get(0), 1);
|
||||||
|
assert.equal(list.get(1), 2);
|
||||||
|
assert.equal(list.get(2), 1);
|
||||||
|
assert.equal(list.get(3), 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should shift elements forward, expanding the list if needed', () => {
|
||||||
|
const list = new CircularList<number>(5);
|
||||||
|
list.push(1);
|
||||||
|
list.push(2);
|
||||||
|
list.shiftElements(0, 2, 2);
|
||||||
|
assert.equal(list.length, 4);
|
||||||
|
assert.equal(list.get(0), 1);
|
||||||
|
assert.equal(list.get(1), 2);
|
||||||
|
assert.equal(list.get(2), 1);
|
||||||
|
assert.equal(list.get(3), 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should shift elements forward, wrapping the list if needed', () => {
|
||||||
|
const list = new CircularList<number>(5);
|
||||||
|
list.push(1);
|
||||||
|
list.push(2);
|
||||||
|
list.push(3);
|
||||||
|
list.push(4);
|
||||||
|
list.push(5);
|
||||||
|
list.shiftElements(2, 2, 3);
|
||||||
|
assert.equal(list.length, 5);
|
||||||
|
assert.equal(list.get(0), 3);
|
||||||
|
assert.equal(list.get(1), 4);
|
||||||
|
assert.equal(list.get(2), 5);
|
||||||
|
assert.equal(list.get(3), 3);
|
||||||
|
assert.equal(list.get(4), 4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should shift an element backwards', () => {
|
||||||
|
const list = new CircularList<number>(5);
|
||||||
|
list.push(1);
|
||||||
|
list.push(2);
|
||||||
|
list.shiftElements(1, 1, -1);
|
||||||
|
assert.equal(list.length, 2);
|
||||||
|
assert.equal(list.get(0), 2);
|
||||||
|
assert.equal(list.get(1), 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should shift elements backwards', () => {
|
||||||
|
const list = new CircularList<number>(5);
|
||||||
|
list.push(1);
|
||||||
|
list.push(2);
|
||||||
|
list.push(3);
|
||||||
|
list.push(4);
|
||||||
|
list.shiftElements(2, 2, -2);
|
||||||
|
assert.equal(list.length, 4);
|
||||||
|
assert.equal(list.get(0), 3);
|
||||||
|
assert.equal(list.get(1), 4);
|
||||||
|
assert.equal(list.get(2), 3);
|
||||||
|
assert.equal(list.get(3), 4);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
* @module xterm/utils/CircularList
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
import { EventEmitter } from '../EventEmitter';
|
||||||
|
|
||||||
|
export class CircularList<T> extends EventEmitter {
|
||||||
|
private _array: T[];
|
||||||
|
private _startIndex: number;
|
||||||
|
private _length: number;
|
||||||
|
|
||||||
|
constructor(maxLength: number) {
|
||||||
|
super();
|
||||||
|
this._array = new Array<T>(maxLength);
|
||||||
|
this._startIndex = 0;
|
||||||
|
this._length = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get maxLength(): number {
|
||||||
|
return this._array.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public set maxLength(newMaxLength: number) {
|
||||||
|
// Reconstruct array, starting at index 0. Only transfer values from the
|
||||||
|
// indexes 0 to length.
|
||||||
|
let newArray = new Array<T>(newMaxLength);
|
||||||
|
for (let i = 0; i < Math.min(newMaxLength, this.length); i++) {
|
||||||
|
newArray[i] = this._array[this._getCyclicIndex(i)];
|
||||||
|
}
|
||||||
|
this._array = newArray;
|
||||||
|
this._startIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get length(): number {
|
||||||
|
return this._length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public set length(newLength: number) {
|
||||||
|
if (newLength > this._length) {
|
||||||
|
for (let i = this._length; i < newLength; i++) {
|
||||||
|
this._array[i] = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._length = newLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get forEach(): (callbackfn: (value: T, index: number) => void) => void {
|
||||||
|
return (callbackfn: (value: T, index: number) => void) => {
|
||||||
|
let i = 0;
|
||||||
|
let length = this.length;
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
callbackfn(this.get(i), i);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the value at an index.
|
||||||
|
*
|
||||||
|
* Note that for performance reasons there is no bounds checking here, the index reference is
|
||||||
|
* circular so this should always return a value and never throw.
|
||||||
|
* @param index The index of the value to get.
|
||||||
|
* @return The value corresponding to the index.
|
||||||
|
*/
|
||||||
|
public get(index: number): T {
|
||||||
|
return this._array[this._getCyclicIndex(index)];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the value at an index.
|
||||||
|
*
|
||||||
|
* Note that for performance reasons there is no bounds checking here, the index reference is
|
||||||
|
* circular so this should always return a value and never throw.
|
||||||
|
* @param index The index to set.
|
||||||
|
* @param value The value to set.
|
||||||
|
*/
|
||||||
|
public set(index: number, value: T): void {
|
||||||
|
this._array[this._getCyclicIndex(index)] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pushes a new value onto the list, wrapping around to the start of the array, overriding index 0
|
||||||
|
* if the maximum length is reached.
|
||||||
|
* @param value The value to push onto the list.
|
||||||
|
*/
|
||||||
|
public push(value: T): void {
|
||||||
|
this._array[this._getCyclicIndex(this._length)] = value;
|
||||||
|
if (this._length === this.maxLength) {
|
||||||
|
this._startIndex++;
|
||||||
|
if (this._startIndex === this.maxLength) {
|
||||||
|
this._startIndex = 0;
|
||||||
|
}
|
||||||
|
this.emit('trim', 1);
|
||||||
|
} else {
|
||||||
|
this._length++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes and returns the last value on the list.
|
||||||
|
* @return The popped value.
|
||||||
|
*/
|
||||||
|
public pop(): T {
|
||||||
|
return this._array[this._getCyclicIndex(this._length-- - 1)];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes and/or inserts items at a particular index (in that order). Unlike
|
||||||
|
* Array.prototype.splice, this operation does not return the deleted items as a new array in
|
||||||
|
* order to save creating a new array. Note that this operation may shift all values in the list
|
||||||
|
* in the worst case.
|
||||||
|
* @param start The index to delete and/or insert.
|
||||||
|
* @param deleteCount The number of elements to delete.
|
||||||
|
* @param items The items to insert.
|
||||||
|
*/
|
||||||
|
public splice(start: number, deleteCount: number, ...items: T[]): void {
|
||||||
|
// Delete items
|
||||||
|
if (deleteCount) {
|
||||||
|
for (let i = start; i < this._length - deleteCount; i++) {
|
||||||
|
this._array[this._getCyclicIndex(i)] = this._array[this._getCyclicIndex(i + deleteCount)];
|
||||||
|
}
|
||||||
|
this._length -= deleteCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (items && items.length) {
|
||||||
|
// Add items
|
||||||
|
for (let i = this._length - 1; i >= start; i--) {
|
||||||
|
this._array[this._getCyclicIndex(i + items.length)] = this._array[this._getCyclicIndex(i)];
|
||||||
|
}
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
this._array[this._getCyclicIndex(start + i)] = items[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust length as needed
|
||||||
|
if (this._length + items.length > this.maxLength) {
|
||||||
|
const countToTrim = (this._length + items.length) - this.maxLength;
|
||||||
|
this._startIndex += countToTrim;
|
||||||
|
this._length = this.maxLength;
|
||||||
|
this.emit('trim', countToTrim);
|
||||||
|
} else {
|
||||||
|
this._length += items.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trims a number of items from the start of the list.
|
||||||
|
* @param count The number of items to remove.
|
||||||
|
*/
|
||||||
|
public trimStart(count: number): void {
|
||||||
|
if (count > this._length) {
|
||||||
|
count = this._length;
|
||||||
|
}
|
||||||
|
this._startIndex += count;
|
||||||
|
this._length -= count;
|
||||||
|
this.emit('trim', count);
|
||||||
|
}
|
||||||
|
|
||||||
|
public shiftElements(start: number, count: number, offset: number): void {
|
||||||
|
if (count <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (start < 0 || start >= this._length) {
|
||||||
|
throw new Error('start argument out of range');
|
||||||
|
}
|
||||||
|
if (start + offset < 0) {
|
||||||
|
throw new Error('Cannot shift elements in list beyond index 0');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (offset > 0) {
|
||||||
|
for (let i = count - 1; i >= 0; i--) {
|
||||||
|
this.set(start + i + offset, this.get(start + i));
|
||||||
|
}
|
||||||
|
const expandListBy = (start + count + offset) - this._length;
|
||||||
|
if (expandListBy > 0) {
|
||||||
|
this._length += expandListBy;
|
||||||
|
while (this._length > this.maxLength) {
|
||||||
|
this._length--;
|
||||||
|
this._startIndex++;
|
||||||
|
this.emit('trim', 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
this.set(start + i + offset, this.get(start + i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the cyclic index for the specified regular index. The cyclic index can then be used on the
|
||||||
|
* backing array to get the element associated with the regular index.
|
||||||
|
* @param index The regular index.
|
||||||
|
* @returns The cyclic index.
|
||||||
|
*/
|
||||||
|
private _getCyclicIndex(index: number): number {
|
||||||
|
return (this._startIndex + index) % this.maxLength;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { assert } from 'chai';
|
||||||
|
import { DomElementObjectPool } from './DomElementObjectPool';
|
||||||
|
|
||||||
|
class MockDocument {
|
||||||
|
private _attr: {[key: string]: string} = {};
|
||||||
|
constructor() {}
|
||||||
|
public getAttribute(key: string): string { return this._attr[key]; };
|
||||||
|
public setAttribute(key: string, value: string): void { this._attr[key] = value; }
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('DomElementObjectPool', () => {
|
||||||
|
let pool: DomElementObjectPool;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
pool = new DomElementObjectPool('span');
|
||||||
|
(<any>global).document = {
|
||||||
|
createElement: () => new MockDocument()
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should acquire distinct elements', () => {
|
||||||
|
const element1 = pool.acquire();
|
||||||
|
const element2 = pool.acquire();
|
||||||
|
assert.notEqual(element1, element2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should acquire released elements', () => {
|
||||||
|
const element = pool.acquire();
|
||||||
|
pool.release(element);
|
||||||
|
assert.equal(pool.acquire(), element);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle a series of acquisitions and releases', () => {
|
||||||
|
const element1 = pool.acquire();
|
||||||
|
const element2 = pool.acquire();
|
||||||
|
pool.release(element1);
|
||||||
|
assert.equal(pool.acquire(), element1);
|
||||||
|
pool.release(element1);
|
||||||
|
pool.release(element2);
|
||||||
|
assert.equal(pool.acquire(), element2);
|
||||||
|
assert.equal(pool.acquire(), element1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw when releasing an element that was not acquired', () => {
|
||||||
|
assert.throws(() => pool.release(document.createElement('span')));
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
/**
|
||||||
|
* @module xterm/utils/DomElementObjectPool
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object pool that manages acquisition and releasing of DOM elements for
|
||||||
|
* when reuse is desirable.
|
||||||
|
*/
|
||||||
|
export class DomElementObjectPool {
|
||||||
|
private static readonly OBJECT_ID_ATTRIBUTE = 'data-obj-id';
|
||||||
|
|
||||||
|
private static _objectCount = 0;
|
||||||
|
|
||||||
|
private _type: string;
|
||||||
|
private _pool: HTMLElement[];
|
||||||
|
private _inUse: {[key: string]: HTMLElement};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param type The DOM element type (div, span, etc.).
|
||||||
|
*/
|
||||||
|
constructor(private type: string) {
|
||||||
|
this._type = type;
|
||||||
|
this._pool = [];
|
||||||
|
this._inUse = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Acquire an element from the pool, creating it if the pool is empty.
|
||||||
|
*/
|
||||||
|
public acquire(): HTMLElement {
|
||||||
|
let element: HTMLElement;
|
||||||
|
if (this._pool.length === 0) {
|
||||||
|
element = this._createNew();
|
||||||
|
} else {
|
||||||
|
element = this._pool.pop();
|
||||||
|
}
|
||||||
|
this._inUse[element.getAttribute(DomElementObjectPool.OBJECT_ID_ATTRIBUTE)] = element;
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Release an element back into the pool. It's up to the caller of this
|
||||||
|
* function to ensure that all external references to the element have been
|
||||||
|
* removed.
|
||||||
|
* @param element The element being released.
|
||||||
|
*/
|
||||||
|
public release(element: HTMLElement): void {
|
||||||
|
if (!this._inUse[element.getAttribute(DomElementObjectPool.OBJECT_ID_ATTRIBUTE)]) {
|
||||||
|
throw new Error('Could not release an element not yet acquired');
|
||||||
|
}
|
||||||
|
delete this._inUse[element.getAttribute(DomElementObjectPool.OBJECT_ID_ATTRIBUTE)];
|
||||||
|
this._cleanElement(element);
|
||||||
|
this._pool.push(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new element for the pool.
|
||||||
|
*/
|
||||||
|
private _createNew(): HTMLElement {
|
||||||
|
const element = document.createElement(this._type);
|
||||||
|
const id = DomElementObjectPool._objectCount++;
|
||||||
|
element.setAttribute(DomElementObjectPool.OBJECT_ID_ATTRIBUTE, id.toString(10));
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets an element back to a "clean state".
|
||||||
|
* @param element The element to be cleaned.
|
||||||
|
*/
|
||||||
|
private _cleanElement(element: HTMLElement): void {
|
||||||
|
element.className = '';
|
||||||
|
element.innerHTML = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,6 @@
|
|||||||
* @param {Array} array The array to search for the given element.
|
* @param {Array} array The array to search for the given element.
|
||||||
* @param {Object} el The element to look for into the array
|
* @param {Object} el The element to look for into the array
|
||||||
*/
|
*/
|
||||||
export let contains = function(arr, el) {
|
export function contains(arr: any[], el: any) {
|
||||||
return arr.indexOf(el) >= 0;
|
return arr.indexOf(el) >= 0;
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
/**
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { CharMeasure } from './CharMeasure';
|
||||||
|
|
||||||
|
export function getCoordsRelativeToElement(event: MouseEvent, element: HTMLElement): [number, number] {
|
||||||
|
// Ignore browsers that don't support MouseEvent.pageX
|
||||||
|
if (event.pageX == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let x = event.pageX;
|
||||||
|
let y = event.pageY;
|
||||||
|
|
||||||
|
// Converts the coordinates from being relative to the document to being
|
||||||
|
// relative to the terminal.
|
||||||
|
while (element && element !== self.document.documentElement) {
|
||||||
|
x -= element.offsetLeft;
|
||||||
|
y -= element.offsetTop;
|
||||||
|
element = 'offsetParent' in element ? <HTMLElement>element.offsetParent : <HTMLElement>element.parentElement;
|
||||||
|
}
|
||||||
|
return [x, y];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets coordinates within the terminal for a particular mouse event. The result
|
||||||
|
* is returned as an array in the form [x, y] instead of an object as it's a
|
||||||
|
* little faster and this function is used in some low level code.
|
||||||
|
* @param event The mouse event.
|
||||||
|
* @param rowContainer The terminal's row container.
|
||||||
|
* @param charMeasure The char measure object used to determine character sizes.
|
||||||
|
* @param colCount The number of columns in the terminal.
|
||||||
|
* @param rowCount The number of rows n the terminal.
|
||||||
|
* @param isSelection Whether the request is for the selection or not. This will
|
||||||
|
* apply an offset to the x value such that the left half of the cell will
|
||||||
|
* select that cell and the right half will select the next cell.
|
||||||
|
*/
|
||||||
|
export function getCoords(event: MouseEvent, rowContainer: HTMLElement, charMeasure: CharMeasure, colCount: number, rowCount: number, isSelection?: boolean): [number, number] {
|
||||||
|
const coords = getCoordsRelativeToElement(event, rowContainer);
|
||||||
|
|
||||||
|
// Convert to cols/rows.
|
||||||
|
coords[0] = Math.ceil((coords[0] + (isSelection ? charMeasure.width / 2 : 0)) / charMeasure.width);
|
||||||
|
coords[1] = Math.ceil(coords[1] / charMeasure.height);
|
||||||
|
|
||||||
|
// Ensure coordinates are within the terminal viewport.
|
||||||
|
coords[0] = Math.min(Math.max(coords[0], 1), colCount + 1);
|
||||||
|
coords[1] = Math.min(Math.max(coords[1], 1), rowCount + 1);
|
||||||
|
|
||||||
|
return coords;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets coordinates within the terminal for a particular mouse event, wrapping
|
||||||
|
* them to the bounds of the terminal and adding 32 to both the x and y values
|
||||||
|
* as expected by xterm.
|
||||||
|
* @param event The mouse event.
|
||||||
|
* @param rowContainer The terminal's row container.
|
||||||
|
* @param charMeasure The char measure object used to determine character sizes.
|
||||||
|
* @param colCount The number of columns in the terminal.
|
||||||
|
* @param rowCount The number of rows in the terminal.
|
||||||
|
*/
|
||||||
|
export function getRawByteCoords(event: MouseEvent, rowContainer: HTMLElement, charMeasure: CharMeasure, colCount: number, rowCount: number): { x: number, y: number } {
|
||||||
|
const coords = getCoords(event, rowContainer, charMeasure, colCount, rowCount);
|
||||||
|
let x = coords[0];
|
||||||
|
let y = coords[1];
|
||||||
|
|
||||||
|
// xterm sends raw bytes and starts at 32 (SP) for each.
|
||||||
|
x += 32;
|
||||||
|
y += 32;
|
||||||
|
|
||||||
|
return { x, y };
|
||||||
|
}
|
||||||
+76
-12
@@ -41,6 +41,9 @@
|
|||||||
font-family: courier-new, courier, monospace;
|
font-family: courier-new, courier, monospace;
|
||||||
font-feature-settings: "liga" 0;
|
font-feature-settings: "liga" 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.terminal.focus,
|
.terminal.focus,
|
||||||
@@ -71,7 +74,22 @@
|
|||||||
resize: none;
|
resize: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.terminal .terminal-cursor {
|
.terminal a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal a:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal a.xterm-invalid-link:hover {
|
||||||
|
cursor: text;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal.focus:not(.xterm-cursor-style-underline):not(.xterm-cursor-style-bar) .terminal-cursor {
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
color: #000;
|
color: #000;
|
||||||
}
|
}
|
||||||
@@ -82,19 +100,41 @@
|
|||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.terminal.focus .terminal-cursor.blinking {
|
.terminal:not(.xterm-cursor-style-underline):not(.xterm-cursor-style-bar).focus.xterm-cursor-blink-on .terminal-cursor {
|
||||||
animation: blink-cursor 1.2s infinite step-end;
|
background-color: transparent;
|
||||||
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes blink-cursor {
|
.terminal.xterm-cursor-style-bar .terminal-cursor,
|
||||||
0% {
|
.terminal.xterm-cursor-style-underline .terminal-cursor {
|
||||||
background-color: #fff;
|
position: relative;
|
||||||
color: #000;
|
}
|
||||||
}
|
.terminal.xterm-cursor-style-bar .terminal-cursor::before,
|
||||||
50% {
|
.terminal.xterm-cursor-style-underline .terminal-cursor::before {
|
||||||
background-color: transparent;
|
content: "";
|
||||||
color: #FFF;
|
display: block;
|
||||||
}
|
position: absolute;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
.terminal.xterm-cursor-style-bar .terminal-cursor::before {
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 1px;
|
||||||
|
}
|
||||||
|
.terminal.xterm-cursor-style-underline .terminal-cursor::before {
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 1px;
|
||||||
|
}
|
||||||
|
.terminal.xterm-cursor-style-bar.focus.xterm-cursor-blink.xterm-cursor-blink-on .terminal-cursor::before,
|
||||||
|
.terminal.xterm-cursor-style-underline.focus.xterm-cursor-blink.xterm-cursor-blink-on .terminal-cursor::before {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
.terminal.xterm-cursor-style-bar.focus.xterm-cursor-blink .terminal-cursor::before,
|
||||||
|
.terminal.xterm-cursor-style-underline.focus.xterm-cursor-blink .terminal-cursor::before {
|
||||||
|
background-color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.terminal .composition-view {
|
.terminal .composition-view {
|
||||||
@@ -116,6 +156,11 @@
|
|||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.terminal .xterm-wide-char,
|
||||||
|
.terminal .xterm-normal-char {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
.terminal .xterm-rows {
|
.terminal .xterm-rows {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
@@ -138,6 +183,25 @@
|
|||||||
left: -9999em;
|
left: -9999em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.terminal.enable-mouse-events {
|
||||||
|
/* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal .xterm-selection {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 1;
|
||||||
|
opacity: 0.3;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal .xterm-selection div {
|
||||||
|
position: absolute;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Determine default colors for xterm.js
|
* Determine default colors for xterm.js
|
||||||
*/
|
*/
|
||||||
|
|||||||
+606
-3272
File diff suppressed because it is too large
Load Diff
+6
-8
@@ -5,15 +5,13 @@
|
|||||||
"rootDir": "src",
|
"rootDir": "src",
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"outDir": "lib",
|
"outDir": "lib",
|
||||||
"sourceMap": true
|
"sourceMap": true,
|
||||||
|
"removeComments": true
|
||||||
},
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*"
|
||||||
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"addons",
|
"src/addons/**/*"
|
||||||
"build",
|
|
||||||
"demo",
|
|
||||||
"dist",
|
|
||||||
"out",
|
|
||||||
"test",
|
|
||||||
"node_modules"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user