Compare commits
722 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
293e322905 | ||
|
|
354bd5d606 | ||
|
|
8e619fa74e | ||
|
|
5b944fb272 | ||
|
|
36d2fc4b92 | ||
|
|
26f4acb10a | ||
|
|
991277e9de | ||
|
|
133e959ecc | ||
|
|
be1630f7d0 | ||
|
|
c57bf8701e | ||
|
|
ea2ae94cd0 | ||
|
|
356eb7e5e7 | ||
|
|
bdd2afa1a2 | ||
|
|
e02ff26d0e | ||
|
|
128fb2826a | ||
|
|
3521ee16e4 | ||
|
|
af5cb36591 | ||
|
|
8aaae5b78c | ||
|
|
ced87b163f | ||
|
|
c5459abb65 | ||
|
|
83b7c0dd6f | ||
|
|
93fff9a69c | ||
|
|
b2247fa406 | ||
|
|
fafba9af3f | ||
|
|
806436297a | ||
|
|
c67edcf811 | ||
|
|
08a7ee4ffb | ||
|
|
5cdb433669 | ||
|
|
c3d154a856 | ||
|
|
88e648d6f9 | ||
|
|
27818e3e33 | ||
|
|
2b2ab5268a | ||
|
|
2634b333f2 | ||
|
|
973afbcec9 | ||
|
|
35fa7b3989 | ||
|
|
3b24e70db1 | ||
|
|
9b25f8f91d | ||
|
|
4b9cd1c544 | ||
|
|
4daf4a8e64 | ||
|
|
272eb28d7b | ||
|
|
19edea7343 | ||
|
|
e3a8d00f27 | ||
|
|
6631c6456c | ||
|
|
70fedf618a | ||
|
|
b5636a3531 | ||
|
|
aad2407de1 | ||
|
|
0652637fbe | ||
|
|
451ea05393 | ||
|
|
dd9145d650 | ||
|
|
d30093b877 | ||
|
|
89742beb4e | ||
|
|
27b31b7ded | ||
|
|
123fd18af5 | ||
|
|
f4f98c54fa | ||
|
|
7b4a3fc867 | ||
|
|
e90ad3d9c0 | ||
|
|
ef6ffd6a3a | ||
|
|
305c8a2f03 | ||
|
|
da1338278a | ||
|
|
d15a3c6de8 | ||
|
|
cae496565b | ||
|
|
14b9f3d8f5 | ||
|
|
c1cec53c0e | ||
|
|
d83d2e442e | ||
|
|
99263ae351 | ||
|
|
3846ce1f82 | ||
|
|
97ab6db655 | ||
|
|
1c10a1aecf | ||
|
|
0fe0f84546 | ||
|
|
0432ba68c6 | ||
|
|
4b4b3f2bc9 | ||
|
|
00f1e699d4 | ||
|
|
a242ba28d9 | ||
|
|
bb4e5ca740 | ||
|
|
a54e77f118 | ||
|
|
a6a2210ce8 | ||
|
|
dd4710d596 | ||
|
|
f4f926cdca | ||
|
|
a432102be0 | ||
|
|
571d6519e9 | ||
|
|
201739a7e6 | ||
|
|
5a5e8167dd | ||
|
|
8a1d45210c | ||
|
|
8a1c99d3ff | ||
|
|
6c7e2492e9 | ||
|
|
fc7e75c355 | ||
|
|
4067d38cb0 | ||
|
|
81e284c1f1 | ||
|
|
1691156f96 | ||
|
|
fee3b9a02b | ||
|
|
de5ecbf4e7 | ||
|
|
bd5b1f207d | ||
|
|
3007fbf5c2 | ||
|
|
f82d0f0c94 | ||
|
|
e0e8bc805b | ||
|
|
4a1ba45efa | ||
|
|
06510832d0 | ||
|
|
c9f1f11489 | ||
|
|
f43cbed72f | ||
|
|
86ab33c558 | ||
|
|
25555682dc | ||
|
|
b704c7167b | ||
|
|
89a061c536 | ||
|
|
5ae0b0cf87 | ||
|
|
b6c5ff25fd | ||
|
|
8a619ea84c | ||
|
|
f085eb6eca | ||
|
|
2db5b95552 | ||
|
|
b1ecdf2924 | ||
|
|
95d1f48b01 | ||
|
|
32c9f2ac94 | ||
|
|
80433edce4 | ||
|
|
f15fb16ad4 | ||
|
|
cce1747e3e | ||
|
|
606471c4b6 | ||
|
|
3eb782b204 | ||
|
|
2aaf22df76 | ||
|
|
c016c77da4 | ||
|
|
c811db5424 | ||
|
|
e0c0613db6 | ||
|
|
e9ae2156d3 | ||
|
|
449adc2dc1 | ||
|
|
5811a25299 | ||
|
|
6a96e1d6d8 | ||
|
|
c0b40aefdd | ||
|
|
f6e8815871 | ||
|
|
5e73a2ea37 | ||
|
|
7fe4c7e06d | ||
|
|
e97b10517f | ||
|
|
375e60627a | ||
|
|
700756aa16 | ||
|
|
a77e085952 | ||
|
|
c0f9ab9b2b | ||
|
|
eb2bd91e4b | ||
|
|
6d190fc16e | ||
|
|
654d2ac676 | ||
|
|
76935291c0 | ||
|
|
bec8bd0285 | ||
|
|
952daf0479 | ||
|
|
4cd49d42cc | ||
|
|
e0475bf4e1 | ||
|
|
cb9ff97edb | ||
|
|
3ccbf3ce1f | ||
|
|
32304f36dd | ||
|
|
387e35d3e5 | ||
|
|
9edc5a665e | ||
|
|
4778ff6f9c | ||
|
|
ebfe476319 | ||
|
|
a81345c91d | ||
|
|
af54255cee | ||
|
|
8e1f8be5e8 | ||
|
|
8addc19d47 | ||
|
|
0083538491 | ||
|
|
69da5d7545 | ||
|
|
86b12a302e | ||
|
|
e4625a2849 | ||
|
|
f2f0f429fb | ||
|
|
74ee0d88e3 | ||
|
|
81ce9b818f | ||
|
|
439d815601 | ||
|
|
58363d66ce | ||
|
|
8a7550ae8b | ||
|
|
3680bc8ddf | ||
|
|
11075457de | ||
|
|
ff4e17190f | ||
|
|
a78644a199 | ||
|
|
74e1900910 | ||
|
|
1c3516bfac | ||
|
|
84b558ecd2 | ||
|
|
9559a0695d | ||
|
|
4fda35be66 | ||
|
|
2a58cf09c5 | ||
|
|
9b3f17da61 | ||
|
|
a0e32753a5 | ||
|
|
624aa9b41d | ||
|
|
7e55eee1b9 | ||
|
|
38bda67adc | ||
|
|
cd5cb2f50d | ||
|
|
504332cd6e | ||
|
|
bc20b7c313 | ||
|
|
20799f9740 | ||
|
|
456aa8bc97 | ||
|
|
2d152fd036 | ||
|
|
a50363ca67 | ||
|
|
573660f36d | ||
|
|
8000af1664 | ||
|
|
93e0f29254 | ||
|
|
fdce4e9692 | ||
|
|
4c8f84f97d | ||
|
|
e72e7dbf5f | ||
|
|
72e081b8da | ||
|
|
a932183909 | ||
|
|
eed13467f3 | ||
|
|
74706abbc1 | ||
|
|
05bb435687 | ||
|
|
0944293fdd | ||
|
|
d8c999aa73 | ||
|
|
0830b400fe | ||
|
|
20cc17b5a7 | ||
|
|
99fadacda6 | ||
|
|
dad26cc395 | ||
|
|
0336048781 | ||
|
|
91415105f3 | ||
|
|
a52334333b | ||
|
|
abfadb5631 | ||
|
|
ee4fa6c118 | ||
|
|
b76886d2ff | ||
|
|
a2b9749dbf | ||
|
|
bb2847fee9 | ||
|
|
0eb12b8fa0 | ||
|
|
236572e0f5 | ||
|
|
172611a1b9 | ||
|
|
cc6d2ddc1d | ||
|
|
f1ec6e1e00 | ||
|
|
07d31634c6 | ||
|
|
5ae9f0405d | ||
|
|
543aa48e26 | ||
|
|
0bc31e5373 | ||
|
|
b19da05097 | ||
|
|
bc6bf82301 | ||
|
|
6d225a7858 | ||
|
|
123de8783f | ||
|
|
62db2c724f | ||
|
|
cc94e7bfee | ||
|
|
bc080f047e | ||
|
|
241536dcdc | ||
|
|
3ec7a01590 | ||
|
|
ed3fd71e6f | ||
|
|
569e7b11fb | ||
|
|
97ddd674dd | ||
|
|
60c293846f | ||
|
|
41b6f68fd7 | ||
|
|
06190a0488 | ||
|
|
ca7361a8a2 | ||
|
|
35731feeb5 | ||
|
|
25ee48d28b | ||
|
|
42a4c1ce4c | ||
|
|
0c6d6367de | ||
|
|
8a0d89d940 | ||
|
|
a94abb0858 | ||
|
|
b509bc330f | ||
|
|
2fb186efcd | ||
|
|
119f01bd5f | ||
|
|
82b52d8799 | ||
|
|
c681d6203f | ||
|
|
146c44b351 | ||
|
|
88e4102ae1 | ||
|
|
bb5c483b7a | ||
|
|
0c3eb7a31c | ||
|
|
e32e2000bf | ||
|
|
18b6d7ea50 | ||
|
|
4eac8bae76 | ||
|
|
4189d6e327 | ||
|
|
0bef594902 | ||
|
|
abb8d850c2 | ||
|
|
c28dbe84f7 | ||
|
|
3a76df3fd0 | ||
|
|
38c17af191 | ||
|
|
69b160d9d4 | ||
|
|
7e3337a901 | ||
|
|
5d87770513 | ||
|
|
38ac95d907 | ||
|
|
43a4a6a3e7 | ||
|
|
d799970f65 | ||
|
|
4314d6420b | ||
|
|
9efae021fa | ||
|
|
d1ec64cab1 | ||
|
|
1169d5ac52 | ||
|
|
531f4a85ff | ||
|
|
ffe289b410 | ||
|
|
2542f114ad | ||
|
|
9286fadeee | ||
|
|
8f593d948c | ||
|
|
25bc2dc1db | ||
|
|
a9fdf09a04 | ||
|
|
89ac5cba62 | ||
|
|
d8ad0a14af | ||
|
|
0ab44c4f4a | ||
|
|
c6c4762be1 | ||
|
|
5395e0f6e2 | ||
|
|
108d0c8763 | ||
|
|
79ad65ee64 | ||
|
|
67ebdc6eb6 | ||
|
|
702b235981 | ||
|
|
006d8760c4 | ||
|
|
4500bc24d4 | ||
|
|
594b0d2910 | ||
|
|
4aa7649c0a | ||
|
|
c156f53eba | ||
|
|
48f87d1656 | ||
|
|
a4687c6745 | ||
|
|
0f5b4d58e2 | ||
|
|
fab86cce5e | ||
|
|
83e86706ff | ||
|
|
2e12fb3c65 | ||
|
|
10f9ac6c27 | ||
|
|
7c90f8900c | ||
|
|
8c6c976afc | ||
|
|
2dfce20602 | ||
|
|
20daf1f86e | ||
|
|
0df4da5b91 | ||
|
|
e5dc10a29e | ||
|
|
a382b366bc | ||
|
|
c9bb6d0111 | ||
|
|
a84c8ac247 | ||
|
|
90c8078225 | ||
|
|
6826a97910 | ||
|
|
7cc43d3954 | ||
|
|
92ea15cc7a | ||
|
|
609d9b9a25 | ||
|
|
3ff419f685 | ||
|
|
f64916b516 | ||
|
|
033ac0bbad | ||
|
|
513e4d5236 | ||
|
|
108f0e9073 | ||
|
|
d0e79b2af7 | ||
|
|
0b362e76ea | ||
|
|
67b9904772 | ||
|
|
b201b65669 | ||
|
|
6a077f5d5a | ||
|
|
0c4a961505 | ||
|
|
36b0e304fc | ||
|
|
9b1db7ec0b | ||
|
|
9a02dc174d | ||
|
|
007425cf16 | ||
|
|
50acf270d8 | ||
|
|
504bf6eb3b | ||
|
|
ae7cbb6dce | ||
|
|
7ba6752020 | ||
|
|
2b2331754d | ||
|
|
36019c0cab | ||
|
|
116ab5429a | ||
|
|
5c617b861c | ||
|
|
c52e93e296 | ||
|
|
aad8dfc0ce | ||
|
|
9a18bc4ced | ||
|
|
aa579d76c9 | ||
|
|
60c2b9c0b2 | ||
|
|
b45db0f1dd | ||
|
|
cee017cc96 | ||
|
|
ff779ef329 | ||
|
|
74e168c339 | ||
|
|
3aaa4395b8 | ||
|
|
f8cfdf0623 | ||
|
|
d551591b42 | ||
|
|
53719366e0 | ||
|
|
5c53cf0054 | ||
|
|
0c0a624b5c | ||
|
|
07492db9fe | ||
|
|
8534010f02 | ||
|
|
5b66d81f7d | ||
|
|
02f367a308 | ||
|
|
6d11e9ffd8 | ||
|
|
45935c86af | ||
|
|
e4c8bd98cd | ||
|
|
c0cc3f3842 | ||
|
|
5311576c34 | ||
|
|
5878cd37fa | ||
|
|
c429991f88 | ||
|
|
7f791dfcb8 | ||
|
|
b6f1dd963d | ||
|
|
d6267c30f6 | ||
|
|
5ae677376b | ||
|
|
ec4ba31b52 | ||
|
|
c332db4703 | ||
|
|
66f52dbd86 | ||
|
|
893284fea9 | ||
|
|
63968749d4 | ||
|
|
b3563fcee6 | ||
|
|
e3508cc37c | ||
|
|
8f74734718 | ||
|
|
7ef368fc13 | ||
|
|
9676eab592 | ||
|
|
d02ec62e33 | ||
|
|
df8f87f8b6 | ||
|
|
1a42bae627 | ||
|
|
41f82b2ad4 | ||
|
|
59c782f383 | ||
|
|
804b4750ab | ||
|
|
da4f69cf72 | ||
|
|
83dc390808 | ||
|
|
eb692ba7f6 | ||
|
|
774bcbf6b3 | ||
|
|
43b6f18864 | ||
|
|
e5e1a0d95c | ||
|
|
0b491f6caf | ||
|
|
79f83a033d | ||
|
|
7fe818f9c8 | ||
|
|
921e8b50b7 | ||
|
|
2ba2b38277 | ||
|
|
3292c05340 | ||
|
|
6008adcb9f | ||
|
|
302745e0fe | ||
|
|
a90618128d | ||
|
|
af28996f82 | ||
|
|
1e52f459c9 | ||
|
|
67efd1c5e0 | ||
|
|
cf0034d42c | ||
|
|
2636467ea7 | ||
|
|
9d73c93ebe | ||
|
|
65a4c71488 | ||
|
|
fec77b7c32 | ||
|
|
fdd763704f | ||
|
|
42160335dc | ||
|
|
8166a4a76d | ||
|
|
74a9f730da | ||
|
|
37bb26bdd5 | ||
|
|
bc012cbaed | ||
|
|
ad290e8702 | ||
|
|
8e0cb28fe4 | ||
|
|
7e584bb31d | ||
|
|
312daca2b0 | ||
|
|
14042403f6 | ||
|
|
04afcef239 | ||
|
|
a617bc3ef4 | ||
|
|
5474f902dd | ||
|
|
a14f8a201e | ||
|
|
2f9ec515db | ||
|
|
4103e7eba2 | ||
|
|
7be296333a | ||
|
|
7a268a41f6 | ||
|
|
90e88ce0d0 | ||
|
|
63f0171d30 | ||
|
|
449a54c7d0 | ||
|
|
6342febb44 | ||
|
|
0e6c6d3750 | ||
|
|
ec214f4897 | ||
|
|
4cd7d193f1 | ||
|
|
54ff2aa46c | ||
|
|
a9a42e0a99 | ||
|
|
84c44f3395 | ||
|
|
b4a54822a0 | ||
|
|
d50e1be566 | ||
|
|
701cdc7f76 | ||
|
|
af5d199e8f | ||
|
|
44625b254c | ||
|
|
30f68e55d6 | ||
|
|
bf2adf19b2 | ||
|
|
c9deee0835 | ||
|
|
67fd94b189 | ||
|
|
dc07f7ca9b | ||
|
|
39ca471685 | ||
|
|
cc8e7007b4 | ||
|
|
5ec224d1f9 | ||
|
|
b358566156 | ||
|
|
6af1cce45f | ||
|
|
b4972b3c08 | ||
|
|
2b6d35b987 | ||
|
|
dcd62abe2b | ||
|
|
db6a0d53ca | ||
|
|
959e96f05a | ||
|
|
d74fb7ed88 | ||
|
|
85adf7593d | ||
|
|
ea1627c1e6 | ||
|
|
e9128ebb2a | ||
|
|
4d9ea06768 | ||
|
|
8e5014fc0f | ||
|
|
e9662b71f0 | ||
|
|
95fe54f010 | ||
|
|
44a096012b | ||
|
|
aede904b3a | ||
|
|
785e2052f6 | ||
|
|
be2c0e30b6 | ||
|
|
90c34bce17 | ||
|
|
d47a24cdf2 | ||
|
|
7dca8d7329 | ||
|
|
9eefb935c2 | ||
|
|
86f03c7d81 | ||
|
|
f4e3b73c3e | ||
|
|
03a770ebe1 | ||
|
|
1144837e8a | ||
|
|
fd12aaef5f | ||
|
|
e6d9799c4b | ||
|
|
cf0e13d51a | ||
|
|
85299d293f | ||
|
|
2f7b03c8e9 | ||
|
|
c8408c6a5f | ||
|
|
7fa0dc68bf | ||
|
|
a4a8c00e0f | ||
|
|
483d246e0d | ||
|
|
7945e632ba | ||
|
|
dbdbc1d8db | ||
|
|
f74d1f26c7 | ||
|
|
35e16a8e6e | ||
|
|
8bdb1b6b14 | ||
|
|
3a7ced5843 | ||
|
|
58fb2826ee | ||
|
|
fccde768ed | ||
|
|
7e3507aba1 | ||
|
|
289891a828 | ||
|
|
526441bcae | ||
|
|
1952e070fd | ||
|
|
d5d4e42c64 | ||
|
|
af0067526b | ||
|
|
4d7e39470e | ||
|
|
4ad2d63c8a | ||
|
|
1ecccc1133 | ||
|
|
7f70964171 | ||
|
|
3c625790cc | ||
|
|
6bd092a948 | ||
|
|
90ab2a7b38 | ||
|
|
80712caf50 | ||
|
|
fc3c928326 | ||
|
|
4c64619263 | ||
|
|
c33e9555a1 | ||
|
|
c02183652f | ||
|
|
03118bd804 | ||
|
|
039ae74662 | ||
|
|
10f3617b5e | ||
|
|
037be96718 | ||
|
|
a249d8d426 | ||
|
|
258855cf50 | ||
|
|
e944239ae8 | ||
|
|
28258be599 | ||
|
|
d3f0c27a87 | ||
|
|
ef17c86586 | ||
|
|
d6cda0ed27 | ||
|
|
6d33e7843e | ||
|
|
eb276df876 | ||
|
|
041aa61508 | ||
|
|
a1eb26c042 | ||
|
|
d9a58547b0 | ||
|
|
cee1101f67 | ||
|
|
faed088735 | ||
|
|
dfcd3087c2 | ||
|
|
1892915146 | ||
|
|
8a757c8603 | ||
|
|
7ad871fab1 | ||
|
|
9aa881faee | ||
|
|
111f46adc5 | ||
|
|
9394f13a08 | ||
|
|
3bf20c7c7b | ||
|
|
31af303d07 | ||
|
|
e79e01fc97 | ||
|
|
eb6e0212ac | ||
|
|
3864ae8aae | ||
|
|
c8538bdbb8 | ||
|
|
f7784e641e | ||
|
|
d6fafd474b | ||
|
|
0936ed4e61 | ||
|
|
11dc51031d | ||
|
|
cbb6e73b1f | ||
|
|
9c5974c054 | ||
|
|
6e390bdc01 | ||
|
|
beba5a3d6c | ||
|
|
9a889c6866 | ||
|
|
6c5e158fc5 | ||
|
|
dd211adf0f | ||
|
|
b2dbd8baef | ||
|
|
fba78945a4 | ||
|
|
7780fde62f | ||
|
|
be903e0c2f | ||
|
|
79cf6fa095 | ||
|
|
b859bde9dc | ||
|
|
f1c31233c8 | ||
|
|
6774894e9e | ||
|
|
9d520b7312 | ||
|
|
40721a2b6b | ||
|
|
6420cdaa26 | ||
|
|
982ac5150d | ||
|
|
5856b24d20 | ||
|
|
cda6d7c06d | ||
|
|
573654ece1 | ||
|
|
c79f013050 | ||
|
|
23107a242b | ||
|
|
0b116729f7 | ||
|
|
d4e2ece38b | ||
|
|
b8a50dc475 | ||
|
|
e7a816c0c0 | ||
|
|
9c87eaf371 | ||
|
|
14ff684e99 | ||
|
|
cf09e477a4 | ||
|
|
1ddf817f4c | ||
|
|
252dc5bf1b | ||
|
|
1d63e2dff4 | ||
|
|
2e281a19d3 | ||
|
|
082a910250 | ||
|
|
4f5e59d6f8 | ||
|
|
abcb366c12 | ||
|
|
d0fda5467a | ||
|
|
9c2a38938d | ||
|
|
0e631a2c11 | ||
|
|
0203bb3ed5 | ||
|
|
2fbf218a0f | ||
|
|
6014a963d4 | ||
|
|
f61b9fd42c | ||
|
|
7ccc58bb68 | ||
|
|
6c916d166d | ||
|
|
6b7f9fad9e | ||
|
|
b13f2aa6f0 | ||
|
|
00f5b3cf74 | ||
|
|
e17a332400 | ||
|
|
0ca9cd361a | ||
|
|
f515165140 | ||
|
|
5f2ca88176 | ||
|
|
9479b65d25 | ||
|
|
8dd7f080f4 | ||
|
|
adbdfcbad3 | ||
|
|
25dbf3731b | ||
|
|
df03099468 | ||
|
|
f813bc2415 | ||
|
|
5917bbbe5c | ||
|
|
524341fd7a | ||
|
|
f08704e789 | ||
|
|
4bbc1e2d8a | ||
|
|
22d255f49f | ||
|
|
df40b39e3e | ||
|
|
df98312f42 | ||
|
|
046194ad6f | ||
|
|
71b74329a3 | ||
|
|
3f21accaeb | ||
|
|
70cd18fc98 | ||
|
|
5ddc94bc8d | ||
|
|
20610807c1 | ||
|
|
97d5342f0c | ||
|
|
01ffd9f46f | ||
|
|
fce4351463 | ||
|
|
b2c7ab9211 | ||
|
|
1b31a3fee4 | ||
|
|
6327d0fe36 | ||
|
|
c385580b81 | ||
|
|
4ef0c3e09f | ||
|
|
364d069e74 | ||
|
|
d84b15d35c | ||
|
|
8ca85b9c66 | ||
|
|
ab79cd2496 | ||
|
|
ff2024a565 | ||
|
|
60b04422a3 | ||
|
|
879725100f | ||
|
|
ab0cd17772 | ||
|
|
9ed79ad57d | ||
|
|
f004e45566 | ||
|
|
1fb34331e5 | ||
|
|
a0a6b488e6 | ||
|
|
eebed7a5a7 | ||
|
|
eef1604dcd | ||
|
|
0d9cbba3b9 | ||
|
|
aecf410707 | ||
|
|
2b0870084b | ||
|
|
5eafccb604 | ||
|
|
7ed3edd00f | ||
|
|
9e9cedf3e0 | ||
|
|
df6d9d741f | ||
|
|
37022b8c45 | ||
|
|
18b5860584 | ||
|
|
f9645e447a | ||
|
|
6a592576eb | ||
|
|
360f0b6180 | ||
|
|
ab22b7740f | ||
|
|
c9a35e7f1e | ||
|
|
7cc98a1248 | ||
|
|
16ec035418 | ||
|
|
a2c3f8c402 | ||
|
|
916b294976 | ||
|
|
7dc03710b1 | ||
|
|
b1bf75f069 | ||
|
|
2d9900a5ad | ||
|
|
20cdb45da5 | ||
|
|
bd611b7ee4 | ||
|
|
275f1ede82 | ||
|
|
4bb38f1488 | ||
|
|
3f15186a64 | ||
|
|
9720d879ad | ||
|
|
56766acf18 | ||
|
|
4099a40e35 | ||
|
|
d2a1282c0b | ||
|
|
b1b767ed96 | ||
|
|
b0f001d3f1 | ||
|
|
29dd6af976 | ||
|
|
6a97e98007 | ||
|
|
02e30873e1 | ||
|
|
fc755c104b | ||
|
|
ec8dc35a68 | ||
|
|
03ebdfd641 | ||
|
|
677dcd6748 | ||
|
|
2cadac6b6e | ||
|
|
e2994e9375 | ||
|
|
c7edb8e1f2 | ||
|
|
1619b89df7 | ||
|
|
1db662fbff | ||
|
|
05281a7d1f | ||
|
|
f943891ce6 | ||
|
|
362db3d986 | ||
|
|
8873aba09f | ||
|
|
e28c1bf9b8 | ||
|
|
32de5b76a9 | ||
|
|
12cc96a94b | ||
|
|
b89e0f8803 | ||
|
|
9f81a9c3c6 | ||
|
|
cb8b16ecc5 | ||
|
|
aa9e58b520 | ||
|
|
fc06283d91 | ||
|
|
d0979b9fac | ||
|
|
4814abe286 | ||
|
|
38803375f5 | ||
|
|
351420310d | ||
|
|
766a94a539 | ||
|
|
1dfcf99d22 | ||
|
|
2f9051c6e1 | ||
|
|
50b7f260c7 | ||
|
|
86828930a2 | ||
|
|
aaad8588b6 | ||
|
|
f549b4aa1e | ||
|
|
3be81ba62a | ||
|
|
49e6b656f6 | ||
|
|
0ce9b28da7 | ||
|
|
382f7c6bf1 | ||
|
|
ed032ea107 | ||
|
|
4caffccca6 | ||
|
|
1f3a8a60d8 | ||
|
|
a652688566 | ||
|
|
013df51fd2 | ||
|
|
f64a3451fa | ||
|
|
8914404d59 | ||
|
|
26f2ae5ad0 | ||
|
|
9592eb0c69 | ||
|
|
e4b809927f | ||
|
|
497a6e0720 | ||
|
|
ffdc0f664f | ||
|
|
fb5e6e6c35 | ||
|
|
d73491b0c8 | ||
|
|
c575435782 |
@@ -11,7 +11,7 @@ environment:
|
||||
- TESTENV: pylint
|
||||
|
||||
install:
|
||||
- C:\Python27\python -u scripts\dev\ci_install.py
|
||||
- C:\Python27\python -u scripts\dev\ci\install.py
|
||||
|
||||
test_script:
|
||||
- C:\Python34\Scripts\tox -e %TESTENV% -- -p "no:sugar" -v --junitxml=junit.xml
|
||||
- C:\Python34\Scripts\tox -e %TESTENV%
|
||||
|
||||
@@ -30,7 +30,7 @@ rules:
|
||||
no-unneeded-ternary: 2
|
||||
operator-assignment: [2, "always"]
|
||||
operator-linebreak: [2, "after"]
|
||||
space-after-keywords: [2, "always"]
|
||||
keyword-spacing: 2
|
||||
space-before-blocks: [2, "always"]
|
||||
space-before-function-paren: [2, {"anonymous": "never", "named": "never"}]
|
||||
object-curly-spacing: [2, "never"]
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -11,6 +11,7 @@ __pycache__
|
||||
/setuptools-*.egg
|
||||
/setuptools-*.zip
|
||||
/qutebrowser/git-commit-id
|
||||
/qutebrowser/3rdparty
|
||||
/doc/*.html
|
||||
/README.html
|
||||
/CHANGELOG.html
|
||||
@@ -28,4 +29,5 @@ __pycache__
|
||||
/.cache
|
||||
/.testmondata
|
||||
/.hypothesis
|
||||
/prof
|
||||
TODO
|
||||
|
||||
13
.pydocstylerc
Normal file
13
.pydocstylerc
Normal file
@@ -0,0 +1,13 @@
|
||||
[pydocstyle]
|
||||
# Disabled checks:
|
||||
# D102: Missing docstring in public method (will be handled by others)
|
||||
# D103: Missing docstring in public function (will be handled by others)
|
||||
# D104: Missing docstring in public package (will be handled by others)
|
||||
# D105: Missing docstring in magic method (will be handled by others)
|
||||
# D209: Blank line before closing """ (removed from PEP257)
|
||||
# D211: No blank lines allowed before class docstring
|
||||
# (PEP257 got changed, but let's stick to the old standard)
|
||||
# D402: First line should not be function's signature (false-positives)
|
||||
ignore = D102,D103,D104,D105,D209,D211,D402
|
||||
match = (?!resources|test_*).*\.py
|
||||
inherit = false
|
||||
11
.pylintrc
11
.pylintrc
@@ -3,10 +3,10 @@
|
||||
[MASTER]
|
||||
ignore=resources.py
|
||||
extension-pkg-whitelist=PyQt5,sip
|
||||
load-plugins=pylint_checkers.config,
|
||||
pylint_checkers.modeline,
|
||||
pylint_checkers.openencoding,
|
||||
pylint_checkers.settrace
|
||||
load-plugins=qute_pylint.config,
|
||||
qute_pylint.modeline,
|
||||
qute_pylint.openencoding,
|
||||
qute_pylint.settrace
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
enable=all
|
||||
@@ -41,10 +41,11 @@ attr-rgx=[a-z_][a-z0-9_]{0,30}$
|
||||
argument-rgx=[a-z_][a-z0-9_]{0,30}$
|
||||
variable-rgx=[a-z_][a-z0-9_]{0,30}$
|
||||
docstring-min-length=3
|
||||
no-docstring-rgx=(^_|^main$)
|
||||
|
||||
[FORMAT]
|
||||
max-line-length=79
|
||||
ignore-long-lines=(<?https?://|^# Copyright 201\d)
|
||||
ignore-long-lines=(<?https?://|^# Copyright 201\d|# (pylint|flake8): disable=)
|
||||
expected-line-ending-format=LF
|
||||
|
||||
[SIMILARITIES]
|
||||
|
||||
103
.travis.yml
103
.travis.yml
@@ -1,76 +1,71 @@
|
||||
# So we get Ubuntu Trusty - using "dist: trusty" breaks OS X.
|
||||
sudo: required
|
||||
dist: trusty
|
||||
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
|
||||
|
||||
env:
|
||||
global:
|
||||
- PATH=/home/travis/bin:/home/travis/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
matrix:
|
||||
- TESTENV=py35
|
||||
- TESTENV=py34
|
||||
- TESTENV=unittests-nodisp
|
||||
- TESTENV=misc
|
||||
- TESTENV=vulture
|
||||
- TESTENV=pep257
|
||||
- TESTENV=pyflakes
|
||||
- TESTENV=pep8
|
||||
- TESTENV=mccabe
|
||||
- TESTENV=pyroma
|
||||
- TESTENV=check-manifest
|
||||
- TESTENV=pylint
|
||||
- TESTENV=eslint
|
||||
|
||||
# Not really, but this is here so we can do stuff by hand.
|
||||
language: c
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- os: linux
|
||||
env: TESTENV=py34-cov
|
||||
- os: linux
|
||||
env: TESTENV=unittests-nodisp
|
||||
- os: linux
|
||||
env: DOCKER=debian-jessie
|
||||
services: docker
|
||||
- os: linux
|
||||
env: DOCKER=archlinux
|
||||
services: docker
|
||||
- os: linux
|
||||
env: DOCKER=ubuntu-wily
|
||||
services: docker
|
||||
- os: osx
|
||||
env: TESTENV=py35
|
||||
- os: linux
|
||||
env: TESTENV=pylint
|
||||
- os: linux
|
||||
env: TESTENV=flake8
|
||||
- os: linux
|
||||
env: TESTENV=docs
|
||||
- os: linux
|
||||
env: TESTENV=vulture
|
||||
- os: linux
|
||||
env: TESTENV=misc
|
||||
- os: linux
|
||||
env: TESTENV=pyroma
|
||||
- os: linux
|
||||
env: TESTENV=check-manifest
|
||||
- os: linux
|
||||
env: TESTENV=eslint
|
||||
allow_failures:
|
||||
- os: osx
|
||||
env: TESTENV=py35
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.cache/pip
|
||||
- $HOME/build/The-Compiler/qutebrowser/.cache
|
||||
|
||||
install:
|
||||
- python scripts/dev/ci_install.py
|
||||
- python scripts/dev/ci/install.py
|
||||
|
||||
script:
|
||||
- tox -e $TESTENV -- -p no:sugar -v --cov-report term tests
|
||||
- bash scripts/dev/ci/travis_run.sh
|
||||
|
||||
after_success:
|
||||
- '[[ ($TESTENV == py34 || $TESTENV == py35) && $TRAVIS_OX == linux ]] && codecov -e TESTENV -X gcov'
|
||||
|
||||
matrix:
|
||||
exclude:
|
||||
- os: linux
|
||||
env: TESTENV=py35
|
||||
- os: osx
|
||||
env: TESTENV=py34
|
||||
- os: osx
|
||||
env: TESTENV=unittests-nodisp
|
||||
- os: osx
|
||||
env: TESTENV=misc
|
||||
- os: osx
|
||||
env: TESTENV=vulture
|
||||
- os: osx
|
||||
env: TESTENV=pep257
|
||||
- os: osx
|
||||
env: TESTENV=pyflakes
|
||||
- os: osx
|
||||
env: TESTENV=pep8
|
||||
- os: osx
|
||||
env: TESTENV=mccabe
|
||||
- os: osx
|
||||
env: TESTENV=pyroma
|
||||
- os: osx
|
||||
env: TESTENV=check-manifest
|
||||
- os: osx
|
||||
env: TESTENV=pylint
|
||||
- os: osx
|
||||
env: TESTENV=eslint
|
||||
- '[[ $TESTENV == *-cov ]] && codecov -e TESTENV -X gcov'
|
||||
|
||||
notifications:
|
||||
webhooks:
|
||||
- https://buildtimetrend.herokuapp.com/travis
|
||||
irc:
|
||||
channels:
|
||||
- "chat.freenode.net#qutebrowser"
|
||||
on_success: change
|
||||
on_failure: always
|
||||
skip_join: true
|
||||
template:
|
||||
- "%{repository}#%{build_number} (%{branch} - %{commit} : %{author}): %{message}"
|
||||
- "%{compare_url} - %{build_url}"
|
||||
|
||||
@@ -14,6 +14,114 @@ This project adheres to http://semver.org/[Semantic Versioning].
|
||||
// `Fixed` for any bug fixes.
|
||||
// `Security` to invite users to upgrade in case of vulnerabilities.
|
||||
|
||||
v0.6.2
|
||||
------
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
|
||||
- Fixed crash when using `:tab-{prev,next,focus}` right after closing the last
|
||||
tab with `last-close` set to `close`.
|
||||
- Fixed crash when doing `:undo` in a new instance with `tabs -> last-close` set
|
||||
to `default-page`.
|
||||
- Fixed crash when starting with --cachedir=""
|
||||
- Fixed crash in some circumstances when using dictionary hints
|
||||
- Fixed various crashes related to PyQt 5.6
|
||||
|
||||
v0.6.1
|
||||
------
|
||||
|
||||
Fixed
|
||||
~~~~~~
|
||||
|
||||
- Fixed broken cheatsheet image which was missing from package
|
||||
- Fixed occasional crash when switching/disconnecting monitors
|
||||
- Fixed crash when downloading non-ascii files with a broken locale (`LC_ALL=C`)
|
||||
- Added workaround for a Qt/PyQt bug which is too weird to describe here
|
||||
|
||||
v0.6.0
|
||||
------
|
||||
|
||||
Added
|
||||
~~~~~
|
||||
|
||||
- New `:buffer` command to easily switch tabs by name. This is not bound to a
|
||||
key by default for existing users due to a conflict with the `gt`/`gT`
|
||||
bindings (which are now removed from the default bindings).
|
||||
You can bind it by hand by running `:bind -f gt set-cmd-text -s :buffer`.
|
||||
- New `--quiet` argument for the `:debug-pyeval` command to not open a tab with
|
||||
the results. Note `:debug-pyeval` is still only intended for debugging.
|
||||
- The completion now matches each entered word separately.
|
||||
- A new command `:paste-primary` got added to paste the primary selection, and
|
||||
`<Shift-Insert>` got added as a binding so it pastes primary rather than
|
||||
clipboard.
|
||||
- New mode `word` for `hints -> mode` which uses a dictionary and link-texts
|
||||
for hints instead of single characters.
|
||||
- New `--all` argument for `:download-cancel` to cancel all running downloads.
|
||||
- New `password_fill` userscript to fill passwords using the `pass` executable.
|
||||
- New `current` hinting mode which forces opening hints in the current tab
|
||||
(even with `target="_blank"`)
|
||||
|
||||
Changed
|
||||
~~~~~~~
|
||||
|
||||
- Pasting multiple lines via `:paste` now opens each line in a new tab.
|
||||
- `:navigate increment/decrement` now preserves leading zeroes in URLs.
|
||||
- `general -> editor` can now also handle `{}` inside another argument (e.g. to open `vim` via `termite`)
|
||||
- Improved performance when scrolling with many tabs open.
|
||||
- Shift-Insert now also pastes primary selection for prompts.
|
||||
- `:download-remove --all` got un-deprecated to provide symmetry with
|
||||
`:download-cancel --all`. It does the same as `:download-clear`.
|
||||
- Improved detection of URLs/search terms when pasting multiple lines.
|
||||
- Don't remove `qutebrowser-editor-*` temporary file if editor subprocess crashed
|
||||
- Userscripts are also searched in `/usr/share/qutebrowser/userscripts`.
|
||||
- Blocked hosts are now also read from a `blocked-hosts` file in the config dir
|
||||
(e.g. `~/.config/qutebrowser/blocked-hosts`).
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
|
||||
- Fixed starting with -c "".
|
||||
- Fixed crash when a tab is closed twice via javascript (e.g. Dropbox
|
||||
authentication dialogs)
|
||||
- Fixed crash when a notification/geolocation prompt is answered after closing
|
||||
the tab it belongs to.
|
||||
- Fixed crash when downloading a file without any path information (e.g a
|
||||
magnet link).
|
||||
- Fixed crashes when opening an empty URL (e.g. via pasting).
|
||||
- Fixed validation of duplicate values in `hints -> chars`.
|
||||
- Fixed crash when PDF.js was partially installed.
|
||||
- Fixed crash when XDG_DOWNLOAD_DIR was not an absolute path.
|
||||
- Fixed very long filenames when downloading `data://`-URLs.
|
||||
- Fixed ugly UI fonts on Windows when Liberation Mono is installed
|
||||
- Fixed crash when unbinding key from a section which doesn't exist in the config
|
||||
- Fixed report window after a segfault
|
||||
- Fixed some directory browser issues on Windows
|
||||
- Fixed crash when closing a window with a finished download and delayed
|
||||
`remove-finished-downloads` setting.
|
||||
- Fixed crash when hitting `<Tab>` then `<Ctrl-C>` on pages without keyboard
|
||||
focus.
|
||||
- Fixed "Frame load interrupted by policy change" error showing up when
|
||||
downloading files with Qt 5.6.
|
||||
|
||||
Removed
|
||||
~~~~~~~
|
||||
|
||||
- The `gt`/`gT` bindings (luakit-like alternatives to `J`/`K`) were removed
|
||||
(except for existing configs) to make room for the `gt` binding to show
|
||||
buffers.
|
||||
|
||||
v0.5.1
|
||||
------
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
|
||||
- Fixed completion for various config values when using `:set`.
|
||||
- Fixed config validation for various config values.
|
||||
- Prevented an error being logged when a website with HTTP authentication was
|
||||
opened on Windows.
|
||||
|
||||
v0.5.0
|
||||
------
|
||||
|
||||
|
||||
@@ -89,21 +89,39 @@ Checkers
|
||||
qutebrowser uses http://tox.readthedocs.org/en/latest/[tox] to run its
|
||||
unittests and several linters/checkers.
|
||||
|
||||
Currently, the following tools will be invoked when you run `tox`:
|
||||
Currently, following tox environments are available:
|
||||
|
||||
* Unit tests using https://www.pytest.org[pytest].
|
||||
* https://pypi.python.org/pypi/pyflakes[pyflakes] via https://pypi.python.org/pypi/pytest-flakes[pytest-flakes]
|
||||
* https://pypi.python.org/pypi/pep8[pep8] via https://pypi.python.org/pypi/pytest-pep8[pytest-pep8]
|
||||
* https://pypi.python.org/pypi/mccabe[mccabe] via https://pypi.python.org/pypi/pytest-mccabe[pytest-mccabe]
|
||||
* https://github.com/GreenSteam/pep257/[pep257]
|
||||
* http://pylint.org/[pylint]
|
||||
* https://pypi.python.org/pypi/pyroma/[pyroma]
|
||||
* https://github.com/mgedmin/check-manifest[check-manifest]
|
||||
* `scripts/misc_checks.py` which checks for the following things:
|
||||
* Tests using https://www.pytest.org[pytest]:
|
||||
- `py34`: Run pytest for python-3.4.
|
||||
- `py35`: Run pytest for python-3.5.
|
||||
- `py34-cov`: Run pytest for python-3.4 with code coverage report.
|
||||
- `py35-cov`: Run pytest for python-3.5 with code coverage report.
|
||||
* `flake8`: Run https://pypi.python.org/pypi/flake8[flake8] checks:
|
||||
https://pypi.python.org/pypi/pyflakes[pyflakes],
|
||||
https://pypi.python.org/pypi/pep8[pep8],
|
||||
https://pypi.python.org/pypi/mccabe[mccabe]
|
||||
* `vulture`: Run https://pypi.python.org/pypi/vulture[vulture] to find
|
||||
unused code portions.
|
||||
* `pylint`: Run http://pylint.org/[pylint] static code analysis.
|
||||
* `pydocstyle`: Check
|
||||
https://www.python.org/dev/peps/pep-0257/[PEP257] compliance with
|
||||
https://github.com/PyCQA/pydocstyle[pydocstyle]
|
||||
* `pyroma`: Check packaging practices with
|
||||
https://pypi.python.org/pypi/pyroma/[pyroma]
|
||||
* `eslint`: Run http://eslint.org/[ESLint] javascript checker.
|
||||
* `check-manifest`: Check MANIFEST.in completeness with
|
||||
https://github.com/mgedmin/check-manifest[check-manifest]
|
||||
* `mkvenv`: Bootstrap a virtualenv for testing.
|
||||
* `misc`: Run `scripts/misc_checks.py` to check for:
|
||||
- untracked git files
|
||||
- VCS conflict markers
|
||||
- common spelling mistakes
|
||||
|
||||
The default test suite is run with `tox`, the list of default
|
||||
environments is obtained with `tox -l` .
|
||||
|
||||
Please make sure the checks run without any warnings on your new contributions.
|
||||
|
||||
There's of course the possibility of false-positives, and the following
|
||||
techniques are useful to handle these:
|
||||
|
||||
@@ -122,6 +140,34 @@ smallest scope which makes sense. Most of the time, this will be line scope.
|
||||
* If you really think a check shouldn't be done globally as it yields a lot of
|
||||
false-positives, let me know! I'm still tweaking the parameters.
|
||||
|
||||
|
||||
Running Specific Tests
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
While you are developing you often don't want to run the full test
|
||||
suite each time.
|
||||
|
||||
Specific test environments can be run with `tox -e <envlist>`.
|
||||
|
||||
Additional parameters can be passed to the test scripts by separating
|
||||
them from `tox` arguments with `--`.
|
||||
|
||||
Examples:
|
||||
|
||||
----
|
||||
# run only pytest tests which failed in last run:
|
||||
tox -e py35 -- --lf
|
||||
|
||||
# run only the integration feature tests:
|
||||
tox -e py35 -- tests/integration/features
|
||||
|
||||
# run everything with undo in the generated name, based on the scenario text
|
||||
tox -e py35 -- tests/integration/features/test_tabs.py -k undo
|
||||
|
||||
# run coverage test for specific file (updates htmlcov/index.html)
|
||||
tox -e py35-cov -- tests/unit/browser/test_webelem.py
|
||||
----
|
||||
|
||||
Profiling
|
||||
~~~~~~~~~
|
||||
|
||||
@@ -246,22 +292,22 @@ When using Qt objects, two issues must be taken care of:
|
||||
* Methods of Qt objects report their status by using their return values,
|
||||
instead of using exceptions.
|
||||
+
|
||||
If a function gets or returns a Qt object which
|
||||
has an `.isValid()` method such as `QUrl` or `QModelIndex`, there's a helper
|
||||
function `ensure_valid` in `qutebrowser.utils.qt` which should get called on
|
||||
all such objects. It will raise `qutebrowser.utils.qt.QtValueError` if the
|
||||
value is not valid.
|
||||
If a function gets or returns a Qt object which has an `.isValid()`
|
||||
method such as `QUrl` or `QModelIndex`, there's a helper function
|
||||
`ensure_valid` in `qutebrowser.utils.qtutils` which should get called
|
||||
on all such objects. It will raise
|
||||
`qutebrowser.utils.qtutils.QtValueError` if the value is not valid.
|
||||
+
|
||||
If a function returns something else on error, the return value should
|
||||
carefully be checked.
|
||||
|
||||
* Methods of Qt objects have certain maximum values, based on their underlying
|
||||
C++ types.
|
||||
* Methods of Qt objects have certain maximum values, based on their
|
||||
underlying C++ types.
|
||||
+
|
||||
When passing a numeric parameter to a Qt function, all numbers should be
|
||||
range-checked using `qutebrowser.utils.check_overflow`, or passing a value
|
||||
which is too large should be avoided by other means (e.g. by setting a maximum
|
||||
value for a config object).
|
||||
When passing a numeric parameter to a Qt function, all numbers should
|
||||
be range-checked using `qutebrowser.qtutils.check_overflow`, or
|
||||
passing a value which is too large should be avoided by other means
|
||||
(e.g. by setting a maximum value for a config object).
|
||||
|
||||
[[object-registry]]
|
||||
The object registry
|
||||
@@ -363,9 +409,9 @@ then gets passed as the `self` parameter to the handler. The `scope` argument
|
||||
selects which object registry (global, per-tab, etc.) to use. See the
|
||||
<<object-registry,object registry>> section for details.
|
||||
|
||||
There are also other arguments to customize the way the command is registered,
|
||||
see the class documentation for `register` in `qutebrowser.commands.utils` for
|
||||
details.
|
||||
There are also other arguments to customize the way the command is
|
||||
registered, see the class documentation for `register` in
|
||||
`qutebrowser.commands.cmdutils` for details.
|
||||
|
||||
The types of the function arguments are inferred based on their default values,
|
||||
e.g. an argument `foo=True` will be converted to a flag `-f`/`--foo` in
|
||||
|
||||
@@ -143,5 +143,5 @@ My issue is not listed.::
|
||||
https://github.com/The-Compiler/qutebrowser/issues[the issue tracker] or
|
||||
using the `:report` command.
|
||||
If you are reporting a segfault, make sure you read the
|
||||
https://github.com/The-Compiler/qutebrowser/blob/master/doc/stacktrace.asciidoc[guide]
|
||||
on how to report them with all needed information.
|
||||
link:doc/stacktrace.asciidoc[guide] on how to report them with all needed
|
||||
information.
|
||||
|
||||
126
INSTALL.asciidoc
126
INSTALL.asciidoc
@@ -61,61 +61,53 @@ repository (rather than a release):
|
||||
$ python3 scripts/asciidoc2html.py
|
||||
----
|
||||
|
||||
If video or sound don't seem to work, try installing the gstreamer plugins:
|
||||
|
||||
----
|
||||
# apt-get install gstreamer1.0-plugins-{bad,base,good,ugly}
|
||||
----
|
||||
|
||||
Then <<tox,install qutebrowser via tox>>.
|
||||
|
||||
On Fedora
|
||||
---------
|
||||
|
||||
qutebrowser should run on Fedora 22.
|
||||
|
||||
Unfortunately there is no Fedora package yet, but installing qutebrowser is
|
||||
still relatively easy! If you want to help packaging it for Fedora, please
|
||||
mailto:mail@qutebrowser.org[get in touch]!
|
||||
|
||||
Install the dependencies via dnf:
|
||||
qutebrowser is available in the official repositories for Fedora 22 and newer.
|
||||
|
||||
----
|
||||
# dnf update
|
||||
# dnf install python3-qt5 python-tox python3-sip
|
||||
# dnf install qutebrowser
|
||||
----
|
||||
|
||||
To generate the documentation for the `:help` command, when using the git
|
||||
repository (rather than a release):
|
||||
|
||||
----
|
||||
# dnf install asciidoc source-highlight
|
||||
$ python3 scripts/asciidoc2html.py
|
||||
----
|
||||
|
||||
Then <<tox,install qutebrowser via tox>>.
|
||||
|
||||
On Archlinux
|
||||
------------
|
||||
|
||||
There are two Archlinux packages available in the AUR:
|
||||
https://aur.archlinux.org/packages/qutebrowser/[qutebrowser] and
|
||||
https://aur.archlinux.org/packages/qutebrowser-git/[qutebrowser-git].
|
||||
|
||||
You can install them (and the needed pypeg2 dependency) like this:
|
||||
qutebrowser is available in the official [community] repository.
|
||||
|
||||
----
|
||||
$ wget https://aur.archlinux.org/packages/py/python-pypeg2/python-pypeg2.tar.gz
|
||||
$ tar xzf python-pypeg2.tar.gz
|
||||
$ cd python-pypeg2
|
||||
$ makepkg -si
|
||||
$ cd ..
|
||||
$ rm -r python-pypeg2 python-pypeg2.tar.gz
|
||||
# pacman -S qutebrowser
|
||||
----
|
||||
|
||||
$ wget https://aur.archlinux.org/packages/qu/qutebrowser/qutebrowser.tar.gz
|
||||
$ tar xzf qutebrowser.tar.gz
|
||||
$ cd qutebrowser
|
||||
There is also a -git version available in the AUR:
|
||||
https://aur.archlinux.org/packages/qutebrowser-git/[qutebrowser-git].
|
||||
|
||||
You can install it using `makepkg` like this:
|
||||
|
||||
----
|
||||
$ git clone https://aur.archlinux.org/qutebrowser-git.git
|
||||
$ cd qutebrowser-git
|
||||
$ makepkg -si
|
||||
$ cd ..
|
||||
$ rm -r qutebrowser qutebrowser.tar.gz
|
||||
$ rm -r qutebrowser-git
|
||||
----
|
||||
|
||||
or you could use an AUR helper, e.g. `yaourt -S qutebrowser-git`.
|
||||
|
||||
If video or sound don't seem to work, try installing the gstreamer plugins:
|
||||
|
||||
----
|
||||
# pacman -S gst-plugins-{base,good,bad,ugly} gst-libav
|
||||
----
|
||||
|
||||
On Gentoo
|
||||
---------
|
||||
|
||||
@@ -149,12 +141,47 @@ it with:
|
||||
$ nix-env -i qutebrowser
|
||||
----
|
||||
|
||||
On openSUSE
|
||||
-----------
|
||||
|
||||
There are prebuilt RPMs available for Tumbleweed and Leap 42.1:
|
||||
|
||||
http://software.opensuse.org/download.html?project=home%3Aarpraher&package=qutebrowser[One Click Install]
|
||||
|
||||
Or add the repo manually:
|
||||
|
||||
----
|
||||
# zypper addrepo http://download.opensuse.org/repositories/home:arpraher/openSUSE_Tumbleweed/home:arpraher.repo
|
||||
# zypper refresh
|
||||
# zypper install qutebrowser
|
||||
----
|
||||
|
||||
On Windows
|
||||
----------
|
||||
|
||||
You can either use one of the
|
||||
https://github.com/The-Compiler/qutebrowser/releases[prebuilt standalone
|
||||
packages or MSI installers], or install manually:
|
||||
There are different ways to install qutebrowser on Windows:
|
||||
|
||||
Prebuilt binaries
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
Prebuilt standalone packages and MSI installers
|
||||
https://github.com/The-Compiler/qutebrowser/releases[are built] for every
|
||||
release.
|
||||
|
||||
https://chocolatey.org/packages/qutebrowser[Chocolatey package]
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
* PackageManagement PowerShell module
|
||||
----
|
||||
PS C:\> Install-Package qutebrowser
|
||||
----
|
||||
* Chocolatey's client
|
||||
----
|
||||
C:\> choco install qutebrowser
|
||||
----
|
||||
|
||||
Manual install
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
* Use the installer from http://www.python.org/downloads[python.org] to get
|
||||
Python 3 (be sure to install pip).
|
||||
@@ -173,6 +200,10 @@ Then <<tox,install qutebrowser via tox>>.
|
||||
On OS X
|
||||
-------
|
||||
|
||||
*Using qutebrowser with Homebrew on OS X is currently broken, as Homebrew
|
||||
dropped QtWebKit support with Qt 5.6. I'm working on building a standalone
|
||||
`.app` for OS X instead, but it'll still take a few days until it's ready.*
|
||||
|
||||
To install qutebrowser on OS X, you'll want a package manager, e.g.
|
||||
http://brew.sh/[Homebrew] or https://www.macports.org/[MacPorts]. Also make
|
||||
sure, you have https://itunes.apple.com/en/app/xcode/id497799835[XCode]
|
||||
@@ -180,7 +211,7 @@ installed to compile PyQt5 in a later step.
|
||||
|
||||
----
|
||||
$ brew install python3 pyqt5
|
||||
$ pip3.4 install qutebrowser
|
||||
$ pip3.5 install qutebrowser
|
||||
----
|
||||
|
||||
if you are using Homebrew. For MacPorts, run:
|
||||
@@ -208,7 +239,16 @@ it as part of the packaging process.
|
||||
Installing qutebrowser with tox
|
||||
-------------------------------
|
||||
|
||||
Run tox inside the qutebrowser repository to set up a
|
||||
First of all, clone the repository using http://git-scm.org/[git] and switch
|
||||
into the repository folder:
|
||||
|
||||
----
|
||||
$ git clone https://github.com/The-Compiler/qutebrowser.git
|
||||
$ cd qutebrowser
|
||||
----
|
||||
|
||||
|
||||
Then run tox inside the qutebrowser repository to set up a
|
||||
https://docs.python.org/3/library/venv.html[virtual environment]:
|
||||
|
||||
----
|
||||
@@ -226,6 +266,14 @@ your `$PATH` (e.g. `/usr/local/bin/qutebrowser` or `~/bin/qutebrowser`):
|
||||
~/path/to/qutebrowser/.venv/bin/python3 -m qutebrowser "$@"
|
||||
----
|
||||
|
||||
If you are developing on qutebrowser, you may want to redirect it to a local
|
||||
config:
|
||||
|
||||
----
|
||||
#!/bin/bash
|
||||
~/path/to/qutebrowser/.venv/bin/python3 -m qutebrowser -c .qutebrowser-local "$@"
|
||||
----
|
||||
|
||||
Updating
|
||||
~~~~~~~~
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
recursive-include qutebrowser *.py
|
||||
recursive-include qutebrowser/html *.html
|
||||
recursive-include qutebrowser/img *.svg *.png
|
||||
recursive-include qutebrowser/test *.py
|
||||
recursive-include qutebrowser/javascript *.js
|
||||
graft qutebrowser/html
|
||||
graft qutebrowser/3rdparty
|
||||
graft icons
|
||||
graft doc/img
|
||||
@@ -23,6 +23,7 @@ exclude doc/notes
|
||||
recursive-exclude doc *.asciidoc
|
||||
include doc/qutebrowser.1.asciidoc
|
||||
prune tests
|
||||
prune qutebrowser/3rdparty
|
||||
exclude pytest.ini
|
||||
exclude qutebrowser.rcc
|
||||
exclude .coveragerc
|
||||
@@ -32,6 +33,7 @@ exclude .eslintignore
|
||||
exclude doc/help
|
||||
exclude .appveyor.yml
|
||||
exclude .travis.yml
|
||||
exclude .pydocstylerc
|
||||
exclude misc/appveyor_install.py
|
||||
|
||||
global-exclude __pycache__ *.pyc *.pyo
|
||||
|
||||
@@ -22,6 +22,12 @@ on Python, PyQt5 and QtWebKit and free software, licensed under the GPL.
|
||||
|
||||
It was inspired by other browsers/addons like dwb and Vimperator/Pentadactyl.
|
||||
|
||||
// QUTE_WEB_HIDE
|
||||
**qutebrowser is currently running a crowdfunding campaign to add support for
|
||||
the QtWebEngine backend, which fixes many issues. Please
|
||||
link:http://igg.me/at/qutebrowser[check it out]!**
|
||||
// QUTE_WEB_HIDE_END
|
||||
|
||||
Screenshots
|
||||
-----------
|
||||
|
||||
@@ -147,28 +153,37 @@ Contributors, sorted by the number of commits in descending order:
|
||||
* Bruno Oliveira
|
||||
* Alexander Cogneau
|
||||
* Martin Tournoij
|
||||
* Felix Van der Jeugt
|
||||
* Raphael Pierzina
|
||||
* Joel Torstensson
|
||||
* Patric Schmitz
|
||||
* Claude
|
||||
* meles5
|
||||
* Patric Schmitz
|
||||
* Tarcisio Fedrizzi
|
||||
* Artur Shaik
|
||||
* Nathan Isom
|
||||
* Austin Anderson
|
||||
* Thorsten Wißmann
|
||||
* Philipp Hansch
|
||||
* Kevin Velghe
|
||||
* Austin Anderson
|
||||
* Alexey "Averrin" Nabrodov
|
||||
* avk
|
||||
* ZDarian
|
||||
* Milan Svoboda
|
||||
* John ShaggyTwoDope Jenkins
|
||||
* Jimmy
|
||||
* Peter Vilim
|
||||
* Clayton Craft
|
||||
* Oliver Caldwell
|
||||
* Jonas Schürmann
|
||||
* Panagiotis Ktistakis
|
||||
* Jimmy
|
||||
* Jakub Klinkovský
|
||||
* skinnay
|
||||
* error800
|
||||
* Zach-Button
|
||||
* Halfwit
|
||||
* Felix Van der Jeugt
|
||||
* rikn00
|
||||
* Michael Ilsaas
|
||||
* Martin Zimmermann
|
||||
* Brian Jackson
|
||||
* sbinix
|
||||
@@ -176,19 +191,30 @@ Contributors, sorted by the number of commits in descending order:
|
||||
* jnphilipp
|
||||
* Tobias Patzl
|
||||
* Peter Michely
|
||||
* Link
|
||||
* Larry Hynes
|
||||
* Johannes Altmanninger
|
||||
* Samir Benmendil
|
||||
* Ryan Roden-Corrent
|
||||
* Regina Hug
|
||||
* Mathias Fussenegger
|
||||
* Marcelo Santos
|
||||
* Fritz V155 Reichwald
|
||||
* Franz Fellner
|
||||
* Corentin Jule
|
||||
* zwarag
|
||||
* xd1le
|
||||
* issue
|
||||
* haxwithaxe
|
||||
* evan
|
||||
* dylan araps
|
||||
* Tomasz Kramkowski
|
||||
* Tomas Orsava
|
||||
* Tobias Werth
|
||||
* Tim Harder
|
||||
* Thiago Barroso Perrotta
|
||||
* Stefan Tatschner
|
||||
* Sorokin Alexei
|
||||
* Samuel Loury
|
||||
* Matthias Lisin
|
||||
* Marcel Schilling
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|<<bookmark-add,bookmark-add>>|Save the current page as a bookmark.
|
||||
|<<bookmark-del,bookmark-del>>|Delete a bookmark.
|
||||
|<<bookmark-load,bookmark-load>>|Load a bookmark.
|
||||
|<<buffer,buffer>>|Select tab by index or url/title best match.
|
||||
|<<close,close>>|Close the current window.
|
||||
|<<download,download>>|Download a given URL, or current page if no URL given.
|
||||
|<<download-cancel,download-cancel>>|Cancel the last/[count]th download.
|
||||
@@ -72,6 +73,8 @@
|
||||
=== adblock-update
|
||||
Update the adblock block lists.
|
||||
|
||||
This updates ~/.local/share/qutebrowser/blocked-hosts with downloaded host lists and re-reads ~/.config/qutebrowser/blocked-hosts.
|
||||
|
||||
[[back]]
|
||||
=== back
|
||||
Syntax: +:back [*--tab*] [*--bg*] [*--window*]+
|
||||
@@ -140,6 +143,18 @@ Load a bookmark.
|
||||
* This command does not split arguments after the last argument and handles quotes literally.
|
||||
* With this command, +;;+ is interpreted literally instead of splitting off a second command.
|
||||
|
||||
[[buffer]]
|
||||
=== buffer
|
||||
Syntax: +:buffer 'index'+
|
||||
|
||||
Select tab by index or url/title best match.
|
||||
|
||||
Focuses window if necessary.
|
||||
|
||||
==== positional arguments
|
||||
* +'index'+: The [win_id/]index of the tab to focus. Or a substring in which case the closest match will be focused.
|
||||
|
||||
|
||||
[[close]]
|
||||
=== close
|
||||
Close the current window.
|
||||
@@ -161,8 +176,13 @@ The form `:download [url] [dest]` is deprecated, use `:download --dest [dest] [u
|
||||
|
||||
[[download-cancel]]
|
||||
=== download-cancel
|
||||
Syntax: +:download-cancel [*--all*]+
|
||||
|
||||
Cancel the last/[count]th download.
|
||||
|
||||
==== optional arguments
|
||||
* +*-a*+, +*--all*+: Cancel all running downloads
|
||||
|
||||
==== count
|
||||
The index of the download to cancel.
|
||||
|
||||
@@ -175,14 +195,14 @@ Remove all finished downloads from the list.
|
||||
Delete the last/[count]th download from disk.
|
||||
|
||||
==== count
|
||||
The index of the download to cancel.
|
||||
The index of the download to delete.
|
||||
|
||||
[[download-open]]
|
||||
=== download-open
|
||||
Open the last/[count]th download.
|
||||
|
||||
==== count
|
||||
The index of the download to cancel.
|
||||
The index of the download to open.
|
||||
|
||||
[[download-remove]]
|
||||
=== download-remove
|
||||
@@ -191,17 +211,17 @@ Syntax: +:download-remove [*--all*]+
|
||||
Remove the last/[count]th download from the list.
|
||||
|
||||
==== optional arguments
|
||||
* +*-a*+, +*--all*+: Deprecated argument for removing all finished downloads.
|
||||
* +*-a*+, +*--all*+: Remove all finished downloads.
|
||||
|
||||
==== count
|
||||
The index of the download to cancel.
|
||||
The index of the download to remove.
|
||||
|
||||
[[download-retry]]
|
||||
=== download-retry
|
||||
Retry the first failed/[count]th download.
|
||||
|
||||
==== count
|
||||
The index of the download to cancel.
|
||||
The index of the download to retry.
|
||||
|
||||
[[fake-key]]
|
||||
=== fake-key
|
||||
@@ -270,7 +290,8 @@ Start hinting.
|
||||
|
||||
* +'target'+: What to do with the selected element.
|
||||
|
||||
- `normal`: Open the link in the current tab.
|
||||
- `normal`: Open the link.
|
||||
- `current`: Open the link in the current tab.
|
||||
- `tab`: Open the link in a new tab (honoring the
|
||||
background-tabs setting).
|
||||
- `tab-fg`: Open the link in a new foreground tab.
|
||||
@@ -402,6 +423,8 @@ Syntax: +:paste [*--sel*] [*--tab*] [*--bg*] [*--window*]+
|
||||
|
||||
Open a page from the clipboard.
|
||||
|
||||
If the pasted text contains newlines, each line gets opened in its own tab.
|
||||
|
||||
==== optional arguments
|
||||
* +*-s*+, +*--sel*+: Use the primary selection instead of the clipboard.
|
||||
* +*-t*+, +*--tab*+: Open in a new tab.
|
||||
@@ -618,8 +641,11 @@ Note the {url} variable which gets replaced by the current URL might be useful h
|
||||
* +'cmdline'+: The commandline to execute.
|
||||
|
||||
==== optional arguments
|
||||
* +*-u*+, +*--userscript*+: Run the command as a userscript. Either store the userscript in `~/.local/share/qutebrowser/userscripts`
|
||||
(or `$XDG_DATA_DIR`), or use an absolute path.
|
||||
* +*-u*+, +*--userscript*+: Run the command as a userscript. You can use an absolute path, or store the userscript in one of those
|
||||
locations:
|
||||
- `~/.local/share/qutebrowser/userscripts`
|
||||
(or `$XDG_DATA_DIR`)
|
||||
- `/usr/share/qutebrowser/userscripts`
|
||||
|
||||
* +*-v*+, +*--verbose*+: Show notifications when the command started/exited.
|
||||
* +*-d*+, +*--detach*+: Whether the command should be detached from qutebrowser.
|
||||
@@ -831,6 +857,7 @@ How many steps to zoom out.
|
||||
|<<move-to-start-of-next-block,move-to-start-of-next-block>>|Move the cursor or selection to the start of next block.
|
||||
|<<move-to-start-of-prev-block,move-to-start-of-prev-block>>|Move the cursor or selection to the start of previous block.
|
||||
|<<open-editor,open-editor>>|Open an external editor with the currently selected form field.
|
||||
|<<paste-primary,paste-primary>>|Paste the primary selection at cursor position.
|
||||
|<<prompt-accept,prompt-accept>>|Accept the current prompt.
|
||||
|<<prompt-no,prompt-no>>|Answer no to a yes/no prompt.
|
||||
|<<prompt-yes,prompt-yes>>|Answer yes to a yes/no prompt.
|
||||
@@ -1044,6 +1071,10 @@ Open an external editor with the currently selected form field.
|
||||
|
||||
The editor which should be launched can be configured via the `general -> editor` config option.
|
||||
|
||||
[[paste-primary]]
|
||||
=== paste-primary
|
||||
Paste the primary selection at cursor position.
|
||||
|
||||
[[prompt-accept]]
|
||||
=== prompt-accept
|
||||
Accept the current prompt.
|
||||
@@ -1224,10 +1255,12 @@ These commands are mainly intended for debugging. They are hidden if qutebrowser
|
||||
|Command|Description
|
||||
|<<debug-all-objects,debug-all-objects>>|Print a list of all objects to the debug log.
|
||||
|<<debug-cache-stats,debug-cache-stats>>|Print LRU cache stats.
|
||||
|<<debug-clear-ssl-errors,debug-clear-ssl-errors>>|Clear remembered SSL error answers.
|
||||
|<<debug-console,debug-console>>|Show the debugging console.
|
||||
|<<debug-crash,debug-crash>>|Crash for debugging purposes.
|
||||
|<<debug-dump-page,debug-dump-page>>|Dump the current page's content to a file.
|
||||
|<<debug-pyeval,debug-pyeval>>|Evaluate a python string and display the results as a web page.
|
||||
|<<debug-set-fake-clipboard,debug-set-fake-clipboard>>|Put data into the fake clipboard and enable logging, used for tests.
|
||||
|<<debug-trace,debug-trace>>|Trace executed code via hunter.
|
||||
|<<debug-webaction,debug-webaction>>|Execute a webaction.
|
||||
|==============
|
||||
@@ -1239,6 +1272,10 @@ Print a list of all objects to the debug log.
|
||||
=== debug-cache-stats
|
||||
Print LRU cache stats.
|
||||
|
||||
[[debug-clear-ssl-errors]]
|
||||
=== debug-clear-ssl-errors
|
||||
Clear remembered SSL error answers.
|
||||
|
||||
[[debug-console]]
|
||||
=== debug-console
|
||||
Show the debugging console.
|
||||
@@ -1266,17 +1303,29 @@ Dump the current page's content to a file.
|
||||
|
||||
[[debug-pyeval]]
|
||||
=== debug-pyeval
|
||||
Syntax: +:debug-pyeval 's'+
|
||||
Syntax: +:debug-pyeval [*--quiet*] 's'+
|
||||
|
||||
Evaluate a python string and display the results as a web page.
|
||||
|
||||
==== positional arguments
|
||||
* +'s'+: The string to evaluate.
|
||||
|
||||
==== optional arguments
|
||||
* +*-q*+, +*--quiet*+: Don't show the output in a new tab.
|
||||
|
||||
==== note
|
||||
* This command does not split arguments after the last argument and handles quotes literally.
|
||||
* With this command, +;;+ is interpreted literally instead of splitting off a second command.
|
||||
|
||||
[[debug-set-fake-clipboard]]
|
||||
=== debug-set-fake-clipboard
|
||||
Syntax: +:debug-set-fake-clipboard ['s']+
|
||||
|
||||
Put data into the fake clipboard and enable logging, used for tests.
|
||||
|
||||
==== positional arguments
|
||||
* +'s'+: The text to put into the fake clipboard, or unset to enable logging.
|
||||
|
||||
[[debug-trace]]
|
||||
=== debug-trace
|
||||
Syntax: +:debug-trace ['expr']+
|
||||
|
||||
@@ -12,6 +12,7 @@ The following help pages are currently available:
|
||||
* link:commands.html[Documentation of commands]
|
||||
* link:settings.html[Documentation of settings]
|
||||
* link:userscripts.html[How to write userscripts]
|
||||
* link:CONTRIBUTING.html[Contributing to qutebrowser]
|
||||
|
||||
Getting help
|
||||
------------
|
||||
|
||||
@@ -179,7 +179,8 @@
|
||||
|<<hints-min-chars,min-chars>>|Minimum number of chars used for hint strings.
|
||||
|<<hints-scatter,scatter>>|Whether to scatter hint key chains (like Vimium) or not (like dwb).
|
||||
|<<hints-uppercase,uppercase>>|Make chars in hint strings uppercase.
|
||||
|<<hints-auto-follow,auto-follow>>|Whether to auto-follow a hint if there's only one left.
|
||||
|<<hints-dictionary,dictionary>>|The dictionary file to be used by the word hints.
|
||||
|<<hints-auto-follow,auto-follow>>|Follow a hint immediately when the hint text is completely matched.
|
||||
|<<hints-next-regexes,next-regexes>>|A comma-separated list of regexes to use for 'next' links.
|
||||
|<<hints-prev-regexes,prev-regexes>>|A comma-separated list of regexes to use for 'prev' links.
|
||||
|==============
|
||||
@@ -1538,6 +1539,7 @@ Valid values:
|
||||
|
||||
* +number+: Use numeric hints.
|
||||
* +letter+: Use the chars in the hints -> chars setting.
|
||||
* +word+: Use hints words based on the html elements and the extra words.
|
||||
|
||||
Default: +pass:[letter]+
|
||||
|
||||
@@ -1575,9 +1577,15 @@ Valid values:
|
||||
|
||||
Default: +pass:[false]+
|
||||
|
||||
[[hints-dictionary]]
|
||||
=== dictionary
|
||||
The dictionary file to be used by the word hints.
|
||||
|
||||
Default: +pass:[/usr/share/dict/words]+
|
||||
|
||||
[[hints-auto-follow]]
|
||||
=== auto-follow
|
||||
Whether to auto-follow a hint if there's only one left.
|
||||
Follow a hint immediately when the hint text is completely matched.
|
||||
|
||||
Valid values:
|
||||
|
||||
@@ -2033,7 +2041,7 @@ Fonts used for the UI, with optional style/weight/size.
|
||||
=== _monospace
|
||||
Default monospace fonts.
|
||||
|
||||
Default: +pass:[Terminus, Monospace, "DejaVu Sans Mono", Monaco, "Bitstream Vera Sans Mono", "Andale Mono", "Liberation Mono", "Courier New", Courier, monospace, Fixed, Consolas, Terminal]+
|
||||
Default: +pass:[Terminus, Monospace, "DejaVu Sans Mono", Monaco, "Bitstream Vera Sans Mono", "Andale Mono", "Courier New", Courier, "Liberation Mono", monospace, Fixed, Consolas, Terminal]+
|
||||
|
||||
[[fonts-completion]]
|
||||
=== completion
|
||||
|
||||
BIN
doc/img/cheatsheet-big.png
Normal file
BIN
doc/img/cheatsheet-big.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 989 KiB |
BIN
doc/img/cheatsheet-small.png
Normal file
BIN
doc/img/cheatsheet-small.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
@@ -8,7 +8,7 @@ time, use the `:help` command.
|
||||
What to do now
|
||||
--------------
|
||||
|
||||
* View the http://qutebrowser.org/img/cheatsheet-big.png[key binding cheatsheet]
|
||||
* View the link:http://qutebrowser.org/img/cheatsheet-big.png[key binding cheatsheet]
|
||||
to make yourself familiar with the key bindings: +
|
||||
image:http://qutebrowser.org/img/cheatsheet-small.png["qutebrowser key binding cheatsheet",link="http://qutebrowser.org/img/cheatsheet-big.png"]
|
||||
* Run `:adblock-update` to download adblock lists and activate adblocking.
|
||||
|
||||
@@ -66,7 +66,7 @@ show it.
|
||||
How URLs should be opened if there is already a qutebrowser instance running.
|
||||
|
||||
=== debug arguments
|
||||
*-l* 'LOGLEVEL', *--loglevel* 'LOGLEVEL'::
|
||||
*-l* '{critical,error,warning,info,debug,vdebug}', *--loglevel* '{critical,error,warning,info,debug,vdebug}'::
|
||||
Set loglevel
|
||||
|
||||
*--logfilter* 'LOGFILTER'::
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
height="640"
|
||||
id="svg2"
|
||||
sodipodi:version="0.32"
|
||||
inkscape:version="0.48.5 r10040"
|
||||
inkscape:version="0.91 r13725"
|
||||
version="1.0"
|
||||
sodipodi:docname="cheatsheet.svg"
|
||||
inkscape:output_extension="org.inkscape.output.svg.inkscape"
|
||||
@@ -32,18 +32,18 @@
|
||||
objecttolerance="10"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.8791156"
|
||||
inkscape:cx="768.67127"
|
||||
inkscape:cy="133.80749"
|
||||
inkscape:zoom="1.7582312"
|
||||
inkscape:cx="875.18895"
|
||||
inkscape:cy="136.8726"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
width="1024px"
|
||||
height="640px"
|
||||
showgrid="false"
|
||||
inkscape:window-width="636"
|
||||
inkscape:window-height="536"
|
||||
inkscape:window-x="2560"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-width="1362"
|
||||
inkscape:window-height="740"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="24"
|
||||
showguides="true"
|
||||
inkscape:guide-bbox="true"
|
||||
inkscape:window-maximized="0"
|
||||
@@ -3040,17 +3040,13 @@
|
||||
style="font-weight:bold;-inkscape-font-specification:'Sans Bold';fill:#ff0000"
|
||||
id="flowSpan3852">(10)</flowSpan> misc. commands:</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara3725-0"><flowSpan
|
||||
style="fill:#0000ff"
|
||||
id="flowSpan5471">gm</flowSpan> - move tab</flowPara><flowPara
|
||||
id="flowPara3725-0">gt - switch tabs by name</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara3854"><flowSpan
|
||||
id="flowPara4052"><flowSpan
|
||||
style="fill:#0000ff"
|
||||
id="flowSpan5473">gl</flowSpan> - move tab to left</flowPara><flowPara
|
||||
id="flowSpan4054">gm/gl/lr</flowSpan> - move tab</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara3856"><flowSpan
|
||||
style="fill:#0000ff"
|
||||
id="flowSpan5475">gr</flowSpan> - move tab to right</flowPara><flowPara
|
||||
id="flowPara4056"> (to index/left/right)</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara3858">gC - clone tab</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
|
||||
|
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 137 KiB |
35
misc/docker/archlinux/Dockerfile
Normal file
35
misc/docker/archlinux/Dockerfile
Normal file
@@ -0,0 +1,35 @@
|
||||
FROM base/archlinux
|
||||
MAINTAINER Florian Bruhin <me@the-compiler.org>
|
||||
|
||||
RUN echo 'Server = http://mirror.de.leaseweb.net/archlinux/$repo/os/$arch' > /etc/pacman.d/mirrorlist
|
||||
RUN pacman-key --init && pacman-key --populate archlinux && pacman -Sy --noconfirm archlinux-keyring
|
||||
|
||||
RUN pacman -Suyy --noconfirm
|
||||
RUN pacman-db-upgrade
|
||||
|
||||
RUN pacman -S --noconfirm \
|
||||
git \
|
||||
python-tox \
|
||||
qt5-base \
|
||||
qt5-webkit \
|
||||
python-pyqt5 \
|
||||
xorg-xinit \
|
||||
herbstluftwm \
|
||||
xorg-server-xvfb
|
||||
|
||||
RUN echo 'en_US.UTF-8 UTF-8' > /etc/locale.gen && locale-gen
|
||||
|
||||
RUN useradd user && mkdir /home/user && chown -R user:users /home/user
|
||||
USER user
|
||||
WORKDIR /home/user
|
||||
|
||||
ENV DISPLAY=:0
|
||||
ENV LC_ALL=en_US.UTF-8
|
||||
ENV LANG=en_US.UTF-8
|
||||
|
||||
CMD Xvfb -screen 0 800x600x24 :0 & \
|
||||
sleep 2 && \
|
||||
herbstluftwm & \
|
||||
git clone /outside qutebrowser.git && \
|
||||
cd qutebrowser.git && \
|
||||
tox -e py35
|
||||
26
misc/docker/debian-jessie/Dockerfile
Normal file
26
misc/docker/debian-jessie/Dockerfile
Normal file
@@ -0,0 +1,26 @@
|
||||
FROM debian:jessie
|
||||
MAINTAINER Florian Bruhin <me@the-compiler.org>
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN apt-get -y update && \
|
||||
apt-get -y dist-upgrade && \
|
||||
apt-get -y install python3-pyqt5 python3-pyqt5.qtwebkit python-tox \
|
||||
python3-sip xvfb git python3-setuptools wget \
|
||||
herbstluftwm locales
|
||||
RUN echo 'en_US.UTF-8 UTF-8' > /etc/locale.gen && locale-gen
|
||||
|
||||
RUN useradd user && mkdir /home/user && chown -R user:users /home/user
|
||||
USER user
|
||||
WORKDIR /home/user
|
||||
|
||||
ENV DISPLAY=:0
|
||||
ENV LC_ALL=en_US.UTF-8
|
||||
ENV LANG=en_US.UTF-8
|
||||
|
||||
CMD Xvfb -screen 0 800x600x24 :0 & \
|
||||
sleep 2 && \
|
||||
herbstluftwm & \
|
||||
git clone /outside qutebrowser.git && \
|
||||
cd qutebrowser.git && \
|
||||
tox -e py34
|
||||
25
misc/docker/ubuntu-wily/Dockerfile
Normal file
25
misc/docker/ubuntu-wily/Dockerfile
Normal file
@@ -0,0 +1,25 @@
|
||||
FROM ubuntu:wily
|
||||
MAINTAINER Florian Bruhin <me@the-compiler.org>
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN apt-get -y update && \
|
||||
apt-get -y dist-upgrade && \
|
||||
apt-get -y install python3-pyqt5 python3-pyqt5.qtwebkit python-tox \
|
||||
python3-sip xvfb git python3-setuptools wget \
|
||||
herbstluftwm language-pack-en
|
||||
|
||||
RUN useradd user && mkdir /home/user && chown -R user:users /home/user
|
||||
USER user
|
||||
WORKDIR /home/user
|
||||
|
||||
ENV DISPLAY=:0
|
||||
ENV LC_ALL=en_US.UTF-8
|
||||
ENV LANG=en_US.UTF-8
|
||||
|
||||
CMD Xvfb -screen 0 800x600x24 :0 & \
|
||||
sleep 2 && \
|
||||
herbstluftwm & \
|
||||
git clone /outside qutebrowser.git && \
|
||||
cd qutebrowser.git && \
|
||||
tox -e py34
|
||||
@@ -30,9 +30,10 @@ import os
|
||||
import re
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
from urllib.parse import urljoin
|
||||
|
||||
with open(os.environ['QUTE_HTML'], 'r') as f:
|
||||
soup = BeautifulSoup(f)
|
||||
with open(os.environ['QUTE_FIFO'], 'w') as f:
|
||||
for link in soup.find_all('link', rel='alternate', type=re.compile(r'application/((rss|rdf|atom)\+)?xml|text/xml')):
|
||||
f.write('open -t %s\n' % link.get('href'))
|
||||
f.write('open -t %s\n' % urljoin(os.environ['QUTE_URL'], link.get('href')))
|
||||
|
||||
364
misc/userscripts/password_fill
Executable file
364
misc/userscripts/password_fill
Executable file
@@ -0,0 +1,364 @@
|
||||
#!/bin/bash -e
|
||||
help() {
|
||||
blink=$'\e[1;31m' reset=$'\e[0m'
|
||||
cat <<EOF
|
||||
This script can only be used as a userscript for qutebrowser
|
||||
2015, Thorsten Wißmann <edu _at_ thorsten-wissmann _dot_ de>
|
||||
In case of questions or suggestions, do not hesitate to send me an E-Mail or to
|
||||
directly ask me via IRC (nickname thorsten\`) in #qutebrowser on freenode.
|
||||
|
||||
$blink!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!$reset
|
||||
WARNING: the passwords are stored in qutebrowser's
|
||||
debug log reachable via the url qute:log
|
||||
$blink!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!$reset
|
||||
|
||||
Usage: run as a userscript form qutebrowser, e.g.:
|
||||
spawn --userscript ~/.config/qutebrowser/password_fill
|
||||
|
||||
Pass backend: (see also passwordstore.org)
|
||||
This script expects pass to store the credentials of each page in an extra
|
||||
file, where the filename (or filepath) contains the domain of the respective
|
||||
page. The first line of the file must contain the password, the login name
|
||||
must be contained in a later line beginning with "user:", "login:", or
|
||||
"username:" (configurable by the user_pattern variable).
|
||||
|
||||
Behaviour:
|
||||
It will try to find a username/password entry in the configured backend
|
||||
(currently only pass) for the current website and will load that pair of
|
||||
username and password to any form on the current page that has some password
|
||||
entry field. If multiple entries are found, a zenity menu is offered.
|
||||
|
||||
If no entry is found, then it crops subdomains from the url if at least one
|
||||
entry is found in the backend. (In that case, it always shows a menu)
|
||||
|
||||
Configuration:
|
||||
This script loads the bash script ~/.config/qutebrowser/password_fill_rc (if
|
||||
it exists), so you can change any configuration variable and overwrite any
|
||||
function you like.
|
||||
|
||||
EOF
|
||||
}
|
||||
|
||||
set -o pipefail
|
||||
shopt -s nocasematch # make regexp matching in bash case insensitive
|
||||
|
||||
if [ -z "$QUTE_FIFO" ] ; then
|
||||
help
|
||||
exit
|
||||
fi
|
||||
|
||||
error() {
|
||||
local msg="$*"
|
||||
echo "message-error '${msg//\'/\\\'}'" >> "$QUTE_FIFO"
|
||||
}
|
||||
msg() {
|
||||
local msg="$*"
|
||||
echo "message-info '${msg//\'/\\\'}'" >> "$QUTE_FIFO"
|
||||
}
|
||||
die() {
|
||||
error "$*"
|
||||
exit 0
|
||||
}
|
||||
|
||||
javascript_escape() {
|
||||
# print the first argument in a escaped way, such that it can savely
|
||||
# be used within javascripts double quotes
|
||||
sed "s,[\\\'\"],\\\&,g" <<< "$1"
|
||||
}
|
||||
|
||||
# ======================================================= #
|
||||
# CONFIGURATION
|
||||
# ======================================================= #
|
||||
# The configuration file is per default located in
|
||||
# ~/.config/qutebrowser/password_fill_rc and is a bash script that is loaded
|
||||
# later in the present script. So basically you can replace all of the
|
||||
# following definitions and make them fit your needs.
|
||||
|
||||
# The following simplifies a URL to the domain (e.g. "wiki.qutebrowser.org")
|
||||
# which is later used to search the correct entries in the password backend. If
|
||||
# you e.g. don't want the "www." to be removed or if you want to distinguish
|
||||
# between different paths on the same domain.
|
||||
|
||||
simplify_url() {
|
||||
simple_url="${1##*://}" # remove protocoll specification
|
||||
simple_url="${simple_url%%\?*}" # remove GET parameters
|
||||
simple_url="${simple_url%%/*}" # remove directory path
|
||||
simple_url="${simple_url%:*}" # remove port
|
||||
simple_url="${simple_url##www.}" # remove www. subdomain
|
||||
}
|
||||
|
||||
# no_entries_found() is called if the first query_entries() call did not find
|
||||
# any matching entries. Multiple implementations are possible:
|
||||
# The easiest behaviour is to quit:
|
||||
#no_entries_found() {
|
||||
# if [ 0 -eq "${#files[@]}" ] ; then
|
||||
# die "No entry found for »$simple_url«"
|
||||
# fi
|
||||
#}
|
||||
# But you could also fill the files array with all entries from your pass db
|
||||
# if the first db query did not find anything
|
||||
# no_entries_found() {
|
||||
# if [ 0 -eq "${#files[@]}" ] ; then
|
||||
# query_entries ""
|
||||
# if [ 0 -eq "${#files[@]}" ] ; then
|
||||
# die "No entry found for »$simple_url«"
|
||||
# fi
|
||||
# fi
|
||||
# }
|
||||
|
||||
# Another beahviour is to drop another level of subdomains until search hits
|
||||
# are found:
|
||||
no_entries_found() {
|
||||
while [ 0 -eq "${#files[@]}" ] && [ -n "$simple_url" ]; do
|
||||
shorter_simple_url=$(sed 's,^[^.]*\.,,' <<< "$simple_url")
|
||||
if [ "$shorter_simple_url" = "$simple_url" ] ; then
|
||||
# if no dot, then even remove the top level domain
|
||||
simple_url=""
|
||||
query_entries "$simple_url"
|
||||
break
|
||||
fi
|
||||
simple_url="$shorter_simple_url"
|
||||
query_entries "$simple_url"
|
||||
#die "No entry found for »$simple_url«"
|
||||
# enforce menu if we do "fuzzy" matching
|
||||
menu_if_one_entry=1
|
||||
done
|
||||
if [ 0 -eq "${#files[@]}" ] ; then
|
||||
die "No entry found for »$simple_url«"
|
||||
fi
|
||||
}
|
||||
|
||||
# Backend implementations tell, how the actual password store is accessed.
|
||||
# Right now, there is only one fully functional password backend, namely for
|
||||
# the program "pass".
|
||||
# A password backend consists of three actions:
|
||||
# - init() initializes backend-specific things and does sanity checks.
|
||||
# - query_entries() is called with a simplified url and is expected to fill
|
||||
# the bash array $files with the names of matching password entries. There
|
||||
# are no requirements how these names should look like.
|
||||
# - open_entry() is called with some specific entry of the $files array and is
|
||||
# expected to write the username of that entry to the $username variable and
|
||||
# the corresponding password to $password
|
||||
|
||||
reset_backend() {
|
||||
init() { true ; }
|
||||
query_entries() { true ; }
|
||||
open_entry() { true ; }
|
||||
}
|
||||
|
||||
# choose_entry() is expected to choose one entry from the array $files and
|
||||
# write it to the variable $file.
|
||||
choose_entry() {
|
||||
choose_entry_zenity
|
||||
}
|
||||
|
||||
# The default implementation chooses a random entry from the array. So if there
|
||||
# are multiple matching entries, multiple calls to this userscript will
|
||||
# eventually pick the "correct" entry. I.e. if this userscript is bound to
|
||||
# "zl", the user has to press "zl" until the correct username shows up in the
|
||||
# login form.
|
||||
choose_entry_random() {
|
||||
local nr=${#files[@]}
|
||||
file="${files[$((RANDOM % nr))]}"
|
||||
# Warn user, that there might be other matching password entries
|
||||
if [ "$nr" -gt 1 ] ; then
|
||||
msg "Picked $file out of $nr entries: ${files[*]}"
|
||||
fi
|
||||
}
|
||||
|
||||
# another implementation would be to ask the user via some menu (like rofi or
|
||||
# dmenu or zenity or even qutebrowser completion in future?) which entry to
|
||||
# pick
|
||||
MENU_COMMAND=( head -n 1 )
|
||||
# whether to show the menu if there is only one entrie in it
|
||||
menu_if_one_entry=0
|
||||
choose_entry_menu() {
|
||||
local nr=${#files[@]}
|
||||
if [ "$nr" -eq 1 ] && ! ((menu_if_one_entry)) ; then
|
||||
file="${files[0]}"
|
||||
else
|
||||
file=$( printf "%s\n" "${files[@]}" | "${MENU_COMMAND[@]}" )
|
||||
fi
|
||||
}
|
||||
|
||||
choose_entry_rofi() {
|
||||
MENU_COMMAND=( rofi -p "qutebrowser> " -dmenu
|
||||
-mesg $'Pick a password entry for <b>'"${QUTE_URL//&/&}"'</b>' )
|
||||
choose_entry_menu || true
|
||||
}
|
||||
|
||||
choose_entry_zenity() {
|
||||
MENU_COMMAND=( zenity --list --title "Qutebrowser password fill"
|
||||
--text "Pick the password entry:"
|
||||
--column "Name" )
|
||||
choose_entry_menu || true
|
||||
}
|
||||
|
||||
choose_entry_zenity_radio() {
|
||||
zenity_helper() {
|
||||
awk '{ print $0 ; print $0 }' \
|
||||
| zenity --list --radiolist \
|
||||
--title "Qutebrowser password fill" \
|
||||
--text "Pick the password entry:" \
|
||||
--column " " --column "Name"
|
||||
}
|
||||
MENU_COMMAND=( zenity_helper )
|
||||
choose_entry_menu || true
|
||||
}
|
||||
|
||||
# =======================================================
|
||||
# backend: PASS
|
||||
|
||||
# configuration options:
|
||||
match_filename=1 # whether allowing entry match by filepath
|
||||
match_line=0 # whether allowing entry match by URL-Pattern in file
|
||||
# Note: match_line=1 gets very slow, even for small password stores!
|
||||
match_line_pattern='^url: .*' # applied using grep -iE
|
||||
user_pattern='^(user|username|login): '
|
||||
|
||||
GPG_OPTS=( "--quiet" "--yes" "--compress-algo=none" "--no-encrypt-to" )
|
||||
GPG="gpg"
|
||||
export GPG_TTY="${GPG_TTY:-$(tty 2>/dev/null)}"
|
||||
which gpg2 &>/dev/null && GPG="gpg2"
|
||||
[[ -n $GPG_AGENT_INFO || $GPG == "gpg2" ]] && GPG_OPTS+=( "--batch" "--use-agent" )
|
||||
|
||||
pass_backend() {
|
||||
init() {
|
||||
PREFIX="${PASSWORD_STORE_DIR:-$HOME/.password-store}"
|
||||
if ! [ -d "$PREFIX" ] ; then
|
||||
die "Can not open password store dir »$PREFIX«"
|
||||
fi
|
||||
}
|
||||
query_entries() {
|
||||
local url="$1"
|
||||
|
||||
if ((match_line)) ; then
|
||||
# add entries with matching URL-tag
|
||||
while read -r -d "" passfile ; do
|
||||
if $GPG "${GPG_OPTS}" -d "$passfile" \
|
||||
| grep --max-count=1 -iE "${match_line_pattern}${url}" > /dev/null
|
||||
then
|
||||
passfile="${passfile#$PREFIX}"
|
||||
passfile="${passfile#/}"
|
||||
files+=( "${passfile%.gpg}" )
|
||||
fi
|
||||
done < <(find -L "$PREFIX" -iname '*.gpg' -print0)
|
||||
fi
|
||||
if ((match_filename)) ; then
|
||||
# add entries wth matching filepath
|
||||
while read -r passfile ; do
|
||||
passfile="${passfile#$PREFIX}"
|
||||
passfile="${passfile#/}"
|
||||
files+=( "${passfile%.gpg}" )
|
||||
done < <(find -L "$PREFIX" -iname '*.gpg' | grep "$url")
|
||||
fi
|
||||
}
|
||||
open_entry() {
|
||||
local path="$PREFIX/${1}.gpg"
|
||||
password=""
|
||||
local firstline=1
|
||||
while read -r line ; do
|
||||
if ((firstline)) ; then
|
||||
password="$line"
|
||||
firstline=0
|
||||
else
|
||||
if [[ $line =~ $user_pattern ]] ; then
|
||||
# remove the matching prefix "user: " from the beginning of the line
|
||||
username=${line#${BASH_REMATCH[0]}}
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done < <($GPG "${GPG_OPTS}" -d "$path" )
|
||||
}
|
||||
}
|
||||
# =======================================================
|
||||
|
||||
# =======================================================
|
||||
# backend: secret
|
||||
secret_backend() {
|
||||
init() {
|
||||
return
|
||||
}
|
||||
query_entries() {
|
||||
local domain="$1"
|
||||
while read -r line ; do
|
||||
if [[ "$line" =~ "attribute.username = " ]] ; then
|
||||
files+=("$domain ${line#${BASH_REMATCH[0]}}")
|
||||
fi
|
||||
done < <( secret-tool search --unlock --all domain "$domain" 2>&1 )
|
||||
}
|
||||
open_entry() {
|
||||
local domain="${1%% *}"
|
||||
username="${1#* }"
|
||||
password=$(secret-tool lookup domain "$domain" username "$username")
|
||||
}
|
||||
}
|
||||
# =======================================================
|
||||
|
||||
# load some sane default backend
|
||||
reset_backend
|
||||
pass_backend
|
||||
# load configuration
|
||||
QUTE_CONFIG_DIR=${QUTE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/qutebrowser/}
|
||||
PWFILL_CONFIG=${PWFILL_CONFIG:-${QUTE_CONFIG_DIR}/password_fill_rc}
|
||||
if [ -f "$PWFILL_CONFIG" ] ; then
|
||||
source "$PWFILL_CONFIG"
|
||||
fi
|
||||
init
|
||||
|
||||
simplify_url "$QUTE_URL"
|
||||
query_entries "${simple_url}"
|
||||
no_entries_found
|
||||
# remove duplicates
|
||||
mapfile -t files < <(printf "%s\n" "${files[@]}" | sort | uniq )
|
||||
choose_entry
|
||||
if [ -z "$file" ] ; then
|
||||
# choose_entry didn't want any of these entries
|
||||
exit 0
|
||||
fi
|
||||
open_entry "$file"
|
||||
#username="$(date)"
|
||||
#password="XYZ"
|
||||
#msg "$username, ${#password}"
|
||||
|
||||
[ -n "$username" ] || die "Username not set in entry $file"
|
||||
[ -n "$password" ] || die "Password not set in entry $file"
|
||||
|
||||
js() {
|
||||
cat <<EOF
|
||||
function hasPasswordField(form) {
|
||||
var inputs = form.getElementsByTagName("input");
|
||||
for (var j = 0; j < inputs.length; j++) {
|
||||
var input = inputs[j];
|
||||
if (input.type == "password") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
function loadData2Form (form) {
|
||||
var inputs = form.getElementsByTagName("input");
|
||||
for (var j = 0; j < inputs.length; j++) {
|
||||
var input = inputs[j];
|
||||
if (input.type == "text" || input.type == "email") {
|
||||
input.value = "$(javascript_escape "${username}")";
|
||||
}
|
||||
if (input.type == "password") {
|
||||
input.value = "$(javascript_escape "${password}")";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var forms = document.getElementsByTagName("form");
|
||||
for (i = 0; i < forms.length; i++) {
|
||||
if (hasPasswordField(forms[i])) {
|
||||
loadData2Form(forms[i]);
|
||||
}
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
printjs() {
|
||||
js | sed 's,//.*$,,' | tr '\n' ' '
|
||||
}
|
||||
echo "jseval -q $(printjs)" >> "$QUTE_FIFO"
|
||||
28
pytest.ini
28
pytest.ini
@@ -1,5 +1,6 @@
|
||||
[pytest]
|
||||
norecursedirs = .tox .venv
|
||||
addopts = --strict -rfEw --faulthandler-timeout=70 --instafail
|
||||
markers =
|
||||
gui: Tests using the GUI (e.g. spawning widgets)
|
||||
posix: Tests which only can run on a POSIX OS.
|
||||
@@ -8,22 +9,14 @@ markers =
|
||||
osx: Tests which only can run on OS X.
|
||||
not_osx: Tests which can not run on OS X.
|
||||
not_frozen: Tests which can't be run if sys.frozen is True.
|
||||
not_xvfb: Tests which can't be run with Xvfb.
|
||||
no_xvfb: Tests which can't be run with Xvfb.
|
||||
frozen: Tests which can only be run if sys.frozen is True.
|
||||
integration: Tests which test a bigger portion of code, run without coverage.
|
||||
flakes-ignore =
|
||||
UnusedImport
|
||||
UnusedVariable
|
||||
resources.py ALL
|
||||
pep8ignore =
|
||||
E265 # Block comment should start with '#'
|
||||
E501 # Line too long
|
||||
E402 # module level import not at top of file
|
||||
E266 # too many leading '#' for block comment
|
||||
W503 # line break before binary operator
|
||||
resources.py ALL
|
||||
.hypothesis/* ALL
|
||||
mccabe-complexity = 12
|
||||
skip: Always skipped test.
|
||||
pyqt531_or_newer: Needs PyQt 5.3.1 or newer.
|
||||
xfail_norun: xfail the test with out running it
|
||||
ci: Tests which should only run on CI.
|
||||
flaky: Tests which are flaky and should be rerun
|
||||
qt_log_level_fail = WARNING
|
||||
qt_log_ignore =
|
||||
^SpellCheck: .*
|
||||
@@ -31,6 +24,7 @@ qt_log_ignore =
|
||||
^QWindowsWindow::setGeometryDp: Unable to set geometry .*
|
||||
^QProcess: Destroyed while process .* is still running\.
|
||||
^"Method "GetAll" with signature "s" on interface "org\.freedesktop\.DBus\.Properties" doesn't exist
|
||||
^"Method \\"GetAll\\" with signature \\"s\\" on interface \\"org\.freedesktop\.DBus\.Properties\\" doesn't exist\\n"
|
||||
^virtual void QSslSocketBackendPrivate::transmit\(\) SSL write failed with error: -9805
|
||||
^virtual void QSslSocketBackendPrivate::transmit\(\) SSLRead failed with: -9805
|
||||
^Type conversion already registered from type .*
|
||||
@@ -39,3 +33,9 @@ qt_log_ignore =
|
||||
^QXcbXSettings::QXcbXSettings\(QXcbScreen\*\) Failed to get selection owner for XSETTINGS_S atom
|
||||
^QStandardPaths: XDG_RUNTIME_DIR not set, defaulting to .*
|
||||
^QXcbClipboard: SelectionRequest too old
|
||||
^QGeoclueMaster error creating GeoclueMasterClient\.
|
||||
^Geoclue error: Process org\.freedesktop\.Geoclue\.Master exited with status 127
|
||||
^QObject::connect: Cannot connect \(null\)::stateChanged\(QNetworkSession::State\) to QNetworkReplyHttpImpl::_q_networkSessionStateChanged\(QNetworkSession::State\)
|
||||
^QXcbClipboard: Cannot transfer data, no data available
|
||||
qt_wait_signal_raising = true
|
||||
xfail_strict = true
|
||||
|
||||
@@ -28,7 +28,7 @@ __copyright__ = "Copyright 2014-2016 Florian Bruhin (The Compiler)"
|
||||
__license__ = "GPL"
|
||||
__maintainer__ = __author__
|
||||
__email__ = "mail@qutebrowser.org"
|
||||
__version_info__ = (0, 5, 0)
|
||||
__version_info__ = (0, 6, 2)
|
||||
__version__ = '.'.join(str(e) for e in __version_info__)
|
||||
__description__ = "A keyboard-driven, vim-like browser based on PyQt5 and QtWebKit."
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ from PyQt5.QtWidgets import QApplication
|
||||
from PyQt5.QtWebKit import QWebSettings
|
||||
from PyQt5.QtGui import QDesktopServices, QPixmap, QIcon, QCursor, QWindow
|
||||
from PyQt5.QtCore import (pyqtSlot, qInstallMessageHandler, QTimer, QUrl,
|
||||
QObject, Qt, QEvent)
|
||||
QObject, Qt, QEvent, pyqtSignal)
|
||||
try:
|
||||
import hunter
|
||||
except ImportError:
|
||||
@@ -165,11 +165,14 @@ def init(args, crash_handler):
|
||||
def _init_icon():
|
||||
"""Initialize the icon of qutebrowser."""
|
||||
icon = QIcon()
|
||||
fallback_icon = QIcon()
|
||||
for size in (16, 24, 32, 48, 64, 96, 128, 256, 512):
|
||||
filename = ':/icons/qutebrowser-{}x{}.png'.format(size, size)
|
||||
pixmap = QPixmap(filename)
|
||||
qtutils.ensure_not_null(pixmap)
|
||||
icon.addPixmap(pixmap)
|
||||
fallback_icon.addPixmap(pixmap)
|
||||
qtutils.ensure_not_null(fallback_icon)
|
||||
icon = QIcon.fromTheme('qutebrowser', fallback_icon)
|
||||
qtutils.ensure_not_null(icon)
|
||||
qApp.setWindowIcon(icon)
|
||||
|
||||
@@ -241,12 +244,7 @@ def process_pos_args(args, via_ipc=False, cwd=None, target_arg=None):
|
||||
cwd: The cwd to use for fuzzy_url.
|
||||
target_arg: Command line argument received by a running instance via
|
||||
ipc. If the --target argument was not specified, target_arg
|
||||
will be an empty string instead of None. This behavior is
|
||||
caused by the PyQt signal
|
||||
``got_args = pyqtSignal(list, str, str)``
|
||||
used in the misc.ipc.IPCServer class. PyQt converts the
|
||||
None value into a null QString and then back to an empty
|
||||
python string
|
||||
will be an empty string.
|
||||
"""
|
||||
if via_ipc and not args:
|
||||
win_id = mainwindow.get_window(via_ipc, force_window=True)
|
||||
@@ -257,7 +255,7 @@ def process_pos_args(args, via_ipc=False, cwd=None, target_arg=None):
|
||||
if cmd.startswith(':'):
|
||||
if win_id is None:
|
||||
win_id = mainwindow.get_window(via_ipc, force_tab=True)
|
||||
log.init.debug("Startup cmd {}".format(cmd))
|
||||
log.init.debug("Startup cmd {!r}".format(cmd))
|
||||
commandrunner = runners.CommandRunner(win_id)
|
||||
commandrunner.run_safely_init(cmd[1:])
|
||||
elif not cmd:
|
||||
@@ -272,6 +270,8 @@ def process_pos_args(args, via_ipc=False, cwd=None, target_arg=None):
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=win_id)
|
||||
log.init.debug("Startup URL {}".format(cmd))
|
||||
if not cwd: # could also be an empty string due to the PyQt signal
|
||||
cwd = None
|
||||
try:
|
||||
url = urlutils.fuzzy_url(cmd, cwd, relative=True)
|
||||
except urlutils.InvalidUrlError as e:
|
||||
@@ -339,6 +339,7 @@ def _save_version():
|
||||
state_config['general']['version'] = qutebrowser.__version__
|
||||
|
||||
|
||||
@pyqtSlot('QWidget*', 'QWidget*')
|
||||
def on_focus_changed(_old, new):
|
||||
"""Register currently focused main window in the object registry."""
|
||||
if new is None:
|
||||
@@ -550,6 +551,10 @@ class Quitter:
|
||||
else:
|
||||
argdict['session'] = session
|
||||
argdict['override_restore'] = False
|
||||
# Ensure :restart works with --temp-basedir
|
||||
argdict['temp_basedir'] = False
|
||||
argdict['temp_basedir_restarted'] = True
|
||||
|
||||
# Dump the data
|
||||
data = json.dumps(argdict)
|
||||
args += ['--json-args', data]
|
||||
@@ -572,7 +577,7 @@ class Quitter:
|
||||
raise cmdexc.CommandError("SyntaxError in {}:{}: {}".format(
|
||||
e.filename, e.lineno, e))
|
||||
if ok:
|
||||
self.shutdown()
|
||||
self.shutdown(restart=True)
|
||||
|
||||
def restart(self, pages=(), session=None):
|
||||
"""Inner logic to restart qutebrowser.
|
||||
@@ -615,7 +620,8 @@ class Quitter:
|
||||
|
||||
@cmdutils.register(instance='quitter', name=['quit', 'q'],
|
||||
ignore_args=True)
|
||||
def shutdown(self, status=0, session=None, last_window=False):
|
||||
def shutdown(self, status=0, session=None, last_window=False,
|
||||
restart=False):
|
||||
"""Quit qutebrowser.
|
||||
|
||||
Args:
|
||||
@@ -623,6 +629,7 @@ class Quitter:
|
||||
session: A session name if saving should be forced.
|
||||
last_window: If the shutdown was triggered due to the last window
|
||||
closing.
|
||||
restart: If we're planning to restart.
|
||||
"""
|
||||
if self._shutting_down:
|
||||
return
|
||||
@@ -652,13 +659,14 @@ class Quitter:
|
||||
# in the real main event loop, or we'll get a segfault.
|
||||
log.destroy.debug("Deferring real shutdown because question was "
|
||||
"active.")
|
||||
QTimer.singleShot(0, functools.partial(self._shutdown, status))
|
||||
QTimer.singleShot(0, functools.partial(self._shutdown, status,
|
||||
restart=restart))
|
||||
else:
|
||||
# If we have no questions to shut down, we are already in the real
|
||||
# event loop, so we can shut down immediately.
|
||||
self._shutdown(status)
|
||||
self._shutdown(status, restart=restart)
|
||||
|
||||
def _shutdown(self, status):
|
||||
def _shutdown(self, status, restart):
|
||||
"""Second stage of shutdown."""
|
||||
log.destroy.debug("Stage 2 of shutting down...")
|
||||
if qApp is None:
|
||||
@@ -699,7 +707,8 @@ class Quitter:
|
||||
log.destroy.debug("Deactivating crash log...")
|
||||
objreg.get('crash-handler').destroy_crashlogfile()
|
||||
# Delete temp basedir
|
||||
if self._args.temp_basedir:
|
||||
if ((self._args.temp_basedir or self._args.temp_basedir_restarted) and
|
||||
not restart):
|
||||
atexit.register(shutil.rmtree, self._args.basedir,
|
||||
ignore_errors=True)
|
||||
# If we don't kill our custom handler here we might get segfaults
|
||||
@@ -731,6 +740,8 @@ class Application(QApplication):
|
||||
_args: ArgumentParser instance.
|
||||
"""
|
||||
|
||||
new_window = pyqtSignal(mainwindow.MainWindow)
|
||||
|
||||
def __init__(self, args):
|
||||
"""Constructor.
|
||||
|
||||
|
||||
@@ -92,9 +92,11 @@ class HostBlocker:
|
||||
|
||||
Attributes:
|
||||
_blocked_hosts: A set of blocked hosts.
|
||||
_config_blocked_hosts: A set of blocked hosts from ~/.config.
|
||||
_in_progress: The DownloadItems which are currently downloading.
|
||||
_done_count: How many files have been read successfully.
|
||||
_hosts_file: The path to the blocked-hosts file.
|
||||
_local_hosts_file: The path to the blocked-hosts file.
|
||||
_config_hosts_file: The path to a blocked-hosts in ~/.config
|
||||
|
||||
Class attributes:
|
||||
WHITELISTED: Hosts which never should be blocked.
|
||||
@@ -105,13 +107,22 @@ class HostBlocker:
|
||||
|
||||
def __init__(self):
|
||||
self._blocked_hosts = set()
|
||||
self._config_blocked_hosts = set()
|
||||
self._in_progress = []
|
||||
self._done_count = 0
|
||||
|
||||
data_dir = standarddir.data()
|
||||
if data_dir is None:
|
||||
self._hosts_file = None
|
||||
self._local_hosts_file = None
|
||||
else:
|
||||
self._hosts_file = os.path.join(data_dir, 'blocked-hosts')
|
||||
self._local_hosts_file = os.path.join(data_dir, 'blocked-hosts')
|
||||
|
||||
config_dir = standarddir.config()
|
||||
if config_dir is None:
|
||||
self._config_hosts_file = None
|
||||
else:
|
||||
self._config_hosts_file = os.path.join(config_dir, 'blocked-hosts')
|
||||
|
||||
objreg.get('config').changed.connect(self.on_config_changed)
|
||||
|
||||
def is_blocked(self, url):
|
||||
@@ -119,21 +130,46 @@ class HostBlocker:
|
||||
if not config.get('content', 'host-blocking-enabled'):
|
||||
return False
|
||||
host = url.host()
|
||||
return host in self._blocked_hosts and not is_whitelisted_host(host)
|
||||
return ((host in self._blocked_hosts or
|
||||
host in self._config_blocked_hosts) and
|
||||
not is_whitelisted_host(host))
|
||||
|
||||
def _read_hosts_file(self, filename, target):
|
||||
"""Read hosts from the given filename.
|
||||
|
||||
Args:
|
||||
filename: The file to read.
|
||||
target: The set to store the hosts in.
|
||||
|
||||
Return:
|
||||
True if a read was attempted, False otherwise
|
||||
"""
|
||||
if filename is None or not os.path.exists(filename):
|
||||
return False
|
||||
|
||||
try:
|
||||
with open(filename, 'r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
target.add(line.strip())
|
||||
except OSError:
|
||||
log.misc.exception("Failed to read host blocklist!")
|
||||
|
||||
return True
|
||||
|
||||
def read_hosts(self):
|
||||
"""Read hosts from the existing blocked-hosts file."""
|
||||
self._blocked_hosts = set()
|
||||
if self._hosts_file is None:
|
||||
|
||||
if self._local_hosts_file is None:
|
||||
return
|
||||
if os.path.exists(self._hosts_file):
|
||||
try:
|
||||
with open(self._hosts_file, 'r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
self._blocked_hosts.add(line.strip())
|
||||
except OSError:
|
||||
log.misc.exception("Failed to read host blocklist!")
|
||||
else:
|
||||
|
||||
self._read_hosts_file(self._config_hosts_file,
|
||||
self._config_blocked_hosts)
|
||||
|
||||
found = self._read_hosts_file(self._local_hosts_file,
|
||||
self._blocked_hosts)
|
||||
|
||||
if not found:
|
||||
args = objreg.get('args')
|
||||
if (config.get('content', 'host-block-lists') is not None and
|
||||
args.basedir is None):
|
||||
@@ -142,8 +178,14 @@ class HostBlocker:
|
||||
|
||||
@cmdutils.register(instance='host-blocker', win_id='win_id')
|
||||
def adblock_update(self, win_id):
|
||||
"""Update the adblock block lists."""
|
||||
if self._hosts_file is None:
|
||||
"""Update the adblock block lists.
|
||||
|
||||
This updates ~/.local/share/qutebrowser/blocked-hosts with downloaded
|
||||
host lists and re-reads ~/.config/qutebrowser/blocked-hosts.
|
||||
"""
|
||||
self._read_hosts_file(self._config_hosts_file,
|
||||
self._config_blocked_hosts)
|
||||
if self._local_hosts_file is None:
|
||||
raise cmdexc.CommandError("No data storage is configured!")
|
||||
self._blocked_hosts = set()
|
||||
self._done_count = 0
|
||||
@@ -221,7 +263,7 @@ class HostBlocker:
|
||||
|
||||
def on_lists_downloaded(self):
|
||||
"""Install block lists after files have been downloaded."""
|
||||
with open(self._hosts_file, 'w', encoding='utf-8') as f:
|
||||
with open(self._local_hosts_file, 'w', encoding='utf-8') as f:
|
||||
for host in sorted(self._blocked_hosts):
|
||||
f.write(host + '\n')
|
||||
message.info('current', "adblock: Read {} hosts from {} sources."
|
||||
@@ -233,7 +275,7 @@ class HostBlocker:
|
||||
urls = config.get('content', 'host-block-lists')
|
||||
if urls is None:
|
||||
try:
|
||||
os.remove(self._hosts_file)
|
||||
os.remove(self._local_hosts_file)
|
||||
except OSError:
|
||||
log.misc.exception("Failed to delete hosts file.")
|
||||
|
||||
|
||||
@@ -32,16 +32,23 @@ class DiskCache(QNetworkDiskCache):
|
||||
|
||||
"""Disk cache which sets correct cache dir and size.
|
||||
|
||||
If the cache is deactivated via the command line argument --cachedir="",
|
||||
both attributes _cache_dir and _http_cache_dir are set to None.
|
||||
|
||||
Attributes:
|
||||
_activated: Whether the cache should be used.
|
||||
_cache_dir: The base directory for cache files (standarddir.cache())
|
||||
_http_cache_dir: the HTTP subfolder in _cache_dir.
|
||||
_cache_dir: The base directory for cache files (standarddir.cache()) or
|
||||
None.
|
||||
_http_cache_dir: the HTTP subfolder in _cache_dir or None.
|
||||
"""
|
||||
|
||||
def __init__(self, cache_dir, parent=None):
|
||||
super().__init__(parent)
|
||||
self._cache_dir = cache_dir
|
||||
self._http_cache_dir = os.path.join(cache_dir, 'http')
|
||||
if cache_dir is None:
|
||||
self._http_cache_dir = None
|
||||
else:
|
||||
self._http_cache_dir = os.path.join(cache_dir, 'http')
|
||||
self._maybe_activate()
|
||||
objreg.get('config').changed.connect(self.on_config_changed)
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ import xml.etree.ElementTree
|
||||
from PyQt5.QtWebKit import QWebSettings
|
||||
from PyQt5.QtWidgets import QApplication, QTabBar
|
||||
from PyQt5.QtCore import Qt, QUrl, QEvent
|
||||
from PyQt5.QtGui import QClipboard, QKeyEvent
|
||||
from PyQt5.QtGui import QKeyEvent
|
||||
from PyQt5.QtPrintSupport import QPrintDialog, QPrintPreviewDialog
|
||||
from PyQt5.QtWebKitWidgets import QWebPage
|
||||
import pygments
|
||||
@@ -45,6 +45,7 @@ from qutebrowser.utils import (message, usertypes, log, qtutils, urlutils,
|
||||
objreg, utils)
|
||||
from qutebrowser.utils.usertypes import KeyMode
|
||||
from qutebrowser.misc import editor, guiprocess
|
||||
from qutebrowser.completion.models import instances, sortfilter
|
||||
|
||||
|
||||
class CommandDispatcher:
|
||||
@@ -659,7 +660,6 @@ class CommandDispatcher:
|
||||
title: Yank the title instead of the URL.
|
||||
domain: Yank only the scheme, domain, and port number.
|
||||
"""
|
||||
clipboard = QApplication.clipboard()
|
||||
if title:
|
||||
s = self._tabbed_browser.page_title(self._current_index())
|
||||
what = 'title'
|
||||
@@ -673,23 +673,16 @@ class CommandDispatcher:
|
||||
s = self._current_url().toString(
|
||||
QUrl.FullyEncoded | QUrl.RemovePassword)
|
||||
what = 'URL'
|
||||
if sel and clipboard.supportsSelection():
|
||||
mode = QClipboard.Selection
|
||||
|
||||
if sel and QApplication.clipboard().supportsSelection():
|
||||
target = "primary selection"
|
||||
else:
|
||||
mode = QClipboard.Clipboard
|
||||
sel = False
|
||||
target = "clipboard"
|
||||
log.misc.debug("Yanking to {}: '{}'".format(target, s))
|
||||
|
||||
msg = "Yanked {} to {}: {}".format(what, target, s)
|
||||
clipboard.changed.connect(functools.partial(
|
||||
self._display_yank_msg, clipboard, msg))
|
||||
clipboard.setText(s, mode)
|
||||
|
||||
def _display_yank_msg(self, clipboard, msg):
|
||||
"""Display a message when something was yanked."""
|
||||
message.info(self._win_id, msg)
|
||||
clipboard.changed.disconnect()
|
||||
utils.set_clipboard(s, selection=sel)
|
||||
message.info(self._win_id, "Yanked {} to {}: {}".format(
|
||||
what, target, s))
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window',
|
||||
count='count')
|
||||
@@ -781,6 +774,10 @@ class CommandDispatcher:
|
||||
Args:
|
||||
count: How many tabs to switch back.
|
||||
"""
|
||||
if self._count() == 0:
|
||||
# Running :tab-prev after last tab was closed
|
||||
# See https://github.com/The-Compiler/qutebrowser/issues/1448
|
||||
return
|
||||
newidx = self._current_index() - count
|
||||
if newidx >= 0:
|
||||
self._set_current_index(newidx)
|
||||
@@ -797,6 +794,10 @@ class CommandDispatcher:
|
||||
Args:
|
||||
count: How many tabs to switch forward.
|
||||
"""
|
||||
if self._count() == 0:
|
||||
# Running :tab-next after last tab was closed
|
||||
# See https://github.com/The-Compiler/qutebrowser/issues/1448
|
||||
return
|
||||
newidx = self._current_index() + count
|
||||
if newidx < self._count():
|
||||
self._set_current_index(newidx)
|
||||
@@ -809,28 +810,92 @@ class CommandDispatcher:
|
||||
def paste(self, sel=False, tab=False, bg=False, window=False):
|
||||
"""Open a page from the clipboard.
|
||||
|
||||
If the pasted text contains newlines, each line gets opened in its own
|
||||
tab.
|
||||
|
||||
Args:
|
||||
sel: Use the primary selection instead of the clipboard.
|
||||
tab: Open in a new tab.
|
||||
bg: Open in a background tab.
|
||||
window: Open in new window.
|
||||
"""
|
||||
clipboard = QApplication.clipboard()
|
||||
if sel and clipboard.supportsSelection():
|
||||
mode = QClipboard.Selection
|
||||
if sel and QApplication.clipboard().supportsSelection():
|
||||
target = "Primary selection"
|
||||
else:
|
||||
mode = QClipboard.Clipboard
|
||||
sel = False
|
||||
target = "Clipboard"
|
||||
text = clipboard.text(mode)
|
||||
if not text:
|
||||
text = utils.get_clipboard(selection=sel)
|
||||
if not text.strip():
|
||||
raise cmdexc.CommandError("{} is empty.".format(target))
|
||||
log.misc.debug("{} contained: '{}'".format(target, text))
|
||||
log.misc.debug("{} contained: {!r}".format(target, text))
|
||||
text_urls = [u for u in text.split('\n') if u.strip()]
|
||||
if (len(text_urls) > 1 and not urlutils.is_url(text_urls[0]) and
|
||||
urlutils.get_path_if_valid(
|
||||
text_urls[0], check_exists=True) is None):
|
||||
text_urls = [text]
|
||||
for i, text_url in enumerate(text_urls):
|
||||
if not window and i > 0:
|
||||
tab = False
|
||||
bg = True
|
||||
try:
|
||||
url = urlutils.fuzzy_url(text_url)
|
||||
except urlutils.InvalidUrlError as e:
|
||||
raise cmdexc.CommandError(e)
|
||||
self._open(url, tab, bg, window)
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window',
|
||||
completion=[usertypes.Completion.tab])
|
||||
def buffer(self, index):
|
||||
"""Select tab by index or url/title best match.
|
||||
|
||||
Focuses window if necessary.
|
||||
|
||||
Args:
|
||||
index: The [win_id/]index of the tab to focus. Or a substring
|
||||
in which case the closest match will be focused.
|
||||
"""
|
||||
index_parts = index.split('/', 1)
|
||||
|
||||
try:
|
||||
url = urlutils.fuzzy_url(text)
|
||||
except urlutils.InvalidUrlError as e:
|
||||
raise cmdexc.CommandError(e)
|
||||
self._open(url, tab, bg, window)
|
||||
for part in index_parts:
|
||||
int(part)
|
||||
except ValueError:
|
||||
model = instances.get(usertypes.Completion.tab)
|
||||
sf = sortfilter.CompletionFilterModel(source=model)
|
||||
sf.set_pattern(index)
|
||||
if sf.count() > 0:
|
||||
index = sf.data(sf.first_item())
|
||||
index_parts = index.split('/', 1)
|
||||
else:
|
||||
raise cmdexc.CommandError(
|
||||
"No matching tab for: {}".format(index))
|
||||
|
||||
if len(index_parts) == 2:
|
||||
win_id = int(index_parts[0])
|
||||
idx = int(index_parts[1])
|
||||
elif len(index_parts) == 1:
|
||||
idx = int(index_parts[0])
|
||||
active_win = objreg.get('app').activeWindow()
|
||||
if active_win is None:
|
||||
# Not sure how you enter a command without an active window...
|
||||
raise cmdexc.CommandError(
|
||||
"No window specified and couldn't find active window!")
|
||||
win_id = active_win.win_id
|
||||
|
||||
if win_id not in objreg.window_registry:
|
||||
raise cmdexc.CommandError(
|
||||
"There's no window with id {}!".format(win_id))
|
||||
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=win_id)
|
||||
if not 0 < idx <= tabbed_browser.count():
|
||||
raise cmdexc.CommandError(
|
||||
"There's no tab with index {}!".format(idx))
|
||||
|
||||
window = objreg.window_registry[win_id]
|
||||
window.activateWindow()
|
||||
window.raise_()
|
||||
tabbed_browser.setCurrentIndex(idx-1)
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window',
|
||||
count='count')
|
||||
@@ -916,9 +981,12 @@ class CommandDispatcher:
|
||||
useful here.
|
||||
|
||||
Args:
|
||||
userscript: Run the command as a userscript. Either store the
|
||||
userscript in `~/.local/share/qutebrowser/userscripts`
|
||||
(or `$XDG_DATA_DIR`), or use an absolute path.
|
||||
userscript: Run the command as a userscript. You can use an
|
||||
absolute path, or store the userscript in one of those
|
||||
locations:
|
||||
- `~/.local/share/qutebrowser/userscripts`
|
||||
(or `$XDG_DATA_DIR`)
|
||||
- `/usr/share/qutebrowser/userscripts`
|
||||
verbose: Show notifications when the command started/exited.
|
||||
detach: Whether the command should be detached from qutebrowser.
|
||||
cmdline: The commandline to execute.
|
||||
@@ -1298,6 +1366,33 @@ class CommandDispatcher:
|
||||
except webelem.IsNullError:
|
||||
raise cmdexc.CommandError("Element vanished while editing!")
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher',
|
||||
modes=[KeyMode.insert], hide=True, scope='window',
|
||||
needs_js=True)
|
||||
def paste_primary(self):
|
||||
"""Paste the primary selection at cursor position."""
|
||||
frame = self._current_widget().page().currentFrame()
|
||||
try:
|
||||
elem = webelem.focus_elem(frame)
|
||||
except webelem.IsNullError:
|
||||
raise cmdexc.CommandError("No element focused!")
|
||||
if not elem.is_editable(strict=True):
|
||||
raise cmdexc.CommandError("Focused element is not editable!")
|
||||
|
||||
try:
|
||||
sel = utils.get_clipboard(selection=True)
|
||||
except utils.SelectionUnsupportedError:
|
||||
return
|
||||
|
||||
log.misc.debug("Pasting primary selection into element {}".format(
|
||||
elem.debug_text()))
|
||||
elem.evaluateJavaScript("""
|
||||
var sel = '{}';
|
||||
var event = document.createEvent('TextEvent');
|
||||
event.initTextEvent('textInput', true, true, null, sel);
|
||||
this.dispatchEvent(event);
|
||||
""".format(webelem.javascript_escape(sel)))
|
||||
|
||||
def _clear_search(self, view, text):
|
||||
"""Clear search string/highlights for the given view.
|
||||
|
||||
@@ -1462,11 +1557,11 @@ class CommandDispatcher:
|
||||
webview = self._current_widget()
|
||||
if not webview.selection_enabled:
|
||||
act = [QWebPage.MoveToNextWord]
|
||||
if sys.platform == 'win32':
|
||||
if sys.platform == 'win32': # pragma: no cover
|
||||
act.append(QWebPage.MoveToPreviousChar)
|
||||
else:
|
||||
act = [QWebPage.SelectNextWord]
|
||||
if sys.platform == 'win32':
|
||||
if sys.platform == 'win32': # pragma: no cover
|
||||
act.append(QWebPage.SelectPreviousChar)
|
||||
for _ in range(count):
|
||||
for a in act:
|
||||
@@ -1483,11 +1578,11 @@ class CommandDispatcher:
|
||||
webview = self._current_widget()
|
||||
if not webview.selection_enabled:
|
||||
act = [QWebPage.MoveToNextWord]
|
||||
if sys.platform != 'win32':
|
||||
if sys.platform != 'win32': # pragma: no branch
|
||||
act.append(QWebPage.MoveToNextChar)
|
||||
else:
|
||||
act = [QWebPage.SelectNextWord]
|
||||
if sys.platform != 'win32':
|
||||
if sys.platform != 'win32': # pragma: no branch
|
||||
act.append(QWebPage.SelectNextChar)
|
||||
for _ in range(count):
|
||||
for a in act:
|
||||
@@ -1640,14 +1735,12 @@ class CommandDispatcher:
|
||||
message.info(self._win_id, "Nothing to yank")
|
||||
return
|
||||
|
||||
clipboard = QApplication.clipboard()
|
||||
if sel and clipboard.supportsSelection():
|
||||
mode = QClipboard.Selection
|
||||
if sel and QApplication.clipboard().supportsSelection():
|
||||
target = "primary selection"
|
||||
else:
|
||||
mode = QClipboard.Clipboard
|
||||
sel = False
|
||||
target = "clipboard"
|
||||
clipboard.setText(s, mode)
|
||||
utils.set_clipboard(s, sel)
|
||||
message.info(self._win_id, "{} {} yanked to {}".format(
|
||||
len(s), "char" if len(s) == 1 else "chars", target))
|
||||
if not keep:
|
||||
@@ -1755,3 +1848,10 @@ class CommandDispatcher:
|
||||
|
||||
QApplication.postEvent(receiver, press_event)
|
||||
QApplication.postEvent(receiver, release_event)
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window',
|
||||
debug=True)
|
||||
def debug_clear_ssl_errors(self):
|
||||
"""Clear remembered SSL error answers."""
|
||||
nam = self._current_widget().page().networkAccessManager()
|
||||
nam.clear_all_ssl_errors()
|
||||
|
||||
@@ -27,6 +27,7 @@ import shutil
|
||||
import functools
|
||||
import collections
|
||||
|
||||
import sip
|
||||
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, QObject, QTimer,
|
||||
Qt, QVariant, QAbstractListModel, QModelIndex, QUrl)
|
||||
from PyQt5.QtGui import QDesktopServices
|
||||
@@ -103,6 +104,11 @@ def create_full_filename(basename, filename):
|
||||
Return:
|
||||
The full absolute path, or None if filename creation was not possible.
|
||||
"""
|
||||
# Remove chars which can't be encoded in the filename encoding.
|
||||
# See https://github.com/The-Compiler/qutebrowser/issues/427
|
||||
encoding = sys.getfilesystemencoding()
|
||||
filename = utils.force_encoding(filename, encoding)
|
||||
basename = utils.force_encoding(basename, encoding)
|
||||
if os.path.isabs(filename) and os.path.isdir(filename):
|
||||
# We got an absolute directory from the user, so we save it under
|
||||
# the default filename in that directory.
|
||||
@@ -225,7 +231,7 @@ class DownloadItemStats(QObject):
|
||||
else:
|
||||
return remaining_bytes / avg
|
||||
|
||||
@pyqtSlot(int, int)
|
||||
@pyqtSlot('qint64', 'qint64')
|
||||
def on_download_progress(self, bytes_done, bytes_total):
|
||||
"""Update local variables when the download progress changed.
|
||||
|
||||
@@ -522,10 +528,6 @@ class DownloadItem(QObject):
|
||||
"existing: {}, fileobj {}".format(
|
||||
filename, self._filename, self.fileobj))
|
||||
filename = os.path.expanduser(filename)
|
||||
# Remove chars which can't be encoded in the filename encoding.
|
||||
# See https://github.com/The-Compiler/qutebrowser/issues/427
|
||||
encoding = sys.getfilesystemencoding()
|
||||
filename = utils.force_encoding(filename, encoding)
|
||||
self._filename = create_full_filename(self.basename, filename)
|
||||
if self._filename is None:
|
||||
# We only got a filename (without directory) or a relative path
|
||||
@@ -534,6 +536,22 @@ class DownloadItem(QObject):
|
||||
self._filename = create_full_filename(
|
||||
self.basename, os.path.join(download_dir(), filename))
|
||||
|
||||
# At this point, we have a misconfigured XDG_DOWNLOAd_DIR, as
|
||||
# download_dir() + filename is still no absolute path.
|
||||
# The config value is checked for "absoluteness", but
|
||||
# ~/.config/user-dirs.dirs may be misconfigured and a non-absolute path
|
||||
# may be set for XDG_DOWNLOAD_DIR
|
||||
if self._filename is None:
|
||||
message.error(
|
||||
self._win_id,
|
||||
"XDG_DOWNLOAD_DIR points to a relative path - please check"
|
||||
" your ~/.config/user-dirs.dirs. The download is saved in"
|
||||
" your home directory.",
|
||||
)
|
||||
# fall back to $HOME as download_dir
|
||||
self._filename = create_full_filename(
|
||||
self.basename, os.path.expanduser(os.path.join('~', filename)))
|
||||
|
||||
self.basename = os.path.basename(self._filename)
|
||||
last_used_directory = os.path.dirname(self._filename)
|
||||
|
||||
@@ -632,7 +650,7 @@ class DownloadItem(QObject):
|
||||
except OSError as e:
|
||||
self._die(e.strerror)
|
||||
|
||||
@pyqtSlot(int)
|
||||
@pyqtSlot('QNetworkReply::NetworkError')
|
||||
def on_reply_error(self, code):
|
||||
"""Handle QNetworkReply errors."""
|
||||
if code == QNetworkReply.OperationCanceledError:
|
||||
@@ -774,7 +792,28 @@ class DownloadManager(QAbstractListModel):
|
||||
# https://bugreports.qt.io/browse/QTBUG-42757
|
||||
request.setAttribute(QNetworkRequest.CacheLoadControlAttribute,
|
||||
QNetworkRequest.AlwaysNetwork)
|
||||
suggested_fn = urlutils.filename_from_url(request.url())
|
||||
|
||||
if request.url().scheme().lower() != 'data':
|
||||
suggested_fn = urlutils.filename_from_url(request.url())
|
||||
else:
|
||||
# We might be downloading a binary blob embedded on a page or even
|
||||
# generated dynamically via javascript. We try to figure out a more
|
||||
# sensible name than the base64 content of the data.
|
||||
origin = request.originatingObject()
|
||||
try:
|
||||
origin_url = origin.url()
|
||||
except AttributeError:
|
||||
# Raised either if origin is None or some object that doesn't
|
||||
# have its own url. We're probably fine with a default fallback
|
||||
# then.
|
||||
suggested_fn = 'binary blob'
|
||||
else:
|
||||
# Use the originating URL as a base for the filename (works
|
||||
# e.g. for pdf.js).
|
||||
suggested_fn = urlutils.filename_from_url(origin_url)
|
||||
|
||||
if suggested_fn is None:
|
||||
suggested_fn = 'qutebrowser-download'
|
||||
|
||||
# We won't need a question if a filename or fileobj is already given
|
||||
if fileobj is None and filename is None:
|
||||
@@ -911,22 +950,30 @@ class DownloadManager(QAbstractListModel):
|
||||
|
||||
@cmdutils.register(instance='download-manager', scope='window',
|
||||
count='count')
|
||||
def download_cancel(self, count=0):
|
||||
def download_cancel(self, all_=False, count=0):
|
||||
"""Cancel the last/[count]th download.
|
||||
|
||||
Args:
|
||||
all_: Cancel all running downloads
|
||||
count: The index of the download to cancel.
|
||||
"""
|
||||
try:
|
||||
download = self.downloads[count - 1]
|
||||
except IndexError:
|
||||
self.raise_no_download(count)
|
||||
if download.done:
|
||||
if not count:
|
||||
count = len(self.downloads)
|
||||
raise cmdexc.CommandError("Download {} is already done!"
|
||||
.format(count))
|
||||
download.cancel()
|
||||
if all_:
|
||||
# We need to make a copy as we're indirectly mutating
|
||||
# self.downloads here
|
||||
for download in self.downloads[:]:
|
||||
if not download.done:
|
||||
download.cancel()
|
||||
else:
|
||||
try:
|
||||
download = self.downloads[count - 1]
|
||||
except IndexError:
|
||||
self.raise_no_download(count)
|
||||
if download.done:
|
||||
if not count:
|
||||
count = len(self.downloads)
|
||||
raise cmdexc.CommandError("Download {} is already done!"
|
||||
.format(count))
|
||||
download.cancel()
|
||||
|
||||
@cmdutils.register(instance='download-manager', scope='window',
|
||||
count='count')
|
||||
@@ -934,7 +981,7 @@ class DownloadManager(QAbstractListModel):
|
||||
"""Delete the last/[count]th download from disk.
|
||||
|
||||
Args:
|
||||
count: The index of the download to cancel.
|
||||
count: The index of the download to delete.
|
||||
"""
|
||||
try:
|
||||
download = self.downloads[count - 1]
|
||||
@@ -953,7 +1000,7 @@ class DownloadManager(QAbstractListModel):
|
||||
"""Open the last/[count]th download.
|
||||
|
||||
Args:
|
||||
count: The index of the download to cancel.
|
||||
count: The index of the download to open.
|
||||
"""
|
||||
try:
|
||||
download = self.downloads[count - 1]
|
||||
@@ -971,7 +1018,7 @@ class DownloadManager(QAbstractListModel):
|
||||
"""Retry the first failed/[count]th download.
|
||||
|
||||
Args:
|
||||
count: The index of the download to cancel.
|
||||
count: The index of the download to retry.
|
||||
"""
|
||||
if count:
|
||||
try:
|
||||
@@ -1057,12 +1104,10 @@ class DownloadManager(QAbstractListModel):
|
||||
"""Remove the last/[count]th download from the list.
|
||||
|
||||
Args:
|
||||
all_: Deprecated argument for removing all finished downloads.
|
||||
count: The index of the download to cancel.
|
||||
all_: Remove all finished downloads.
|
||||
count: The index of the download to remove.
|
||||
"""
|
||||
if all_:
|
||||
message.warning(self._win_id, ":download-remove --all is "
|
||||
"deprecated - use :download-clear instead!")
|
||||
self.download_clear()
|
||||
else:
|
||||
try:
|
||||
@@ -1087,6 +1132,9 @@ class DownloadManager(QAbstractListModel):
|
||||
|
||||
def remove_item(self, download):
|
||||
"""Remove a given download."""
|
||||
if sip.isdeleted(self):
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/1242
|
||||
return
|
||||
try:
|
||||
idx = self.downloads.index(download)
|
||||
except ValueError:
|
||||
|
||||
@@ -79,6 +79,7 @@ class DownloadView(QListView):
|
||||
self.setResizeMode(QListView.Adjust)
|
||||
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||
self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed)
|
||||
self.setFocusPolicy(Qt.NoFocus)
|
||||
self.setFlow(QListView.LeftToRight)
|
||||
self.setSpacing(1)
|
||||
self._menu = None
|
||||
|
||||
@@ -19,14 +19,15 @@
|
||||
|
||||
"""A HintManager to draw hints over links."""
|
||||
|
||||
import math
|
||||
import functools
|
||||
import collections
|
||||
import functools
|
||||
import math
|
||||
import re
|
||||
import string
|
||||
|
||||
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, QObject, QEvent, Qt, QUrl,
|
||||
QTimer)
|
||||
from PyQt5.QtGui import QMouseEvent, QClipboard
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PyQt5.QtGui import QMouseEvent
|
||||
from PyQt5.QtWebKit import QWebElement
|
||||
from PyQt5.QtWebKitWidgets import QWebPage
|
||||
|
||||
@@ -34,17 +35,22 @@ from qutebrowser.config import config
|
||||
from qutebrowser.keyinput import modeman, modeparsers
|
||||
from qutebrowser.browser import webelem
|
||||
from qutebrowser.commands import userscripts, cmdexc, cmdutils, runners
|
||||
from qutebrowser.utils import usertypes, log, qtutils, message, objreg
|
||||
from qutebrowser.utils import usertypes, log, qtutils, message, objreg, utils
|
||||
from qutebrowser.misc import guiprocess
|
||||
|
||||
|
||||
ElemTuple = collections.namedtuple('ElemTuple', ['elem', 'label'])
|
||||
|
||||
|
||||
Target = usertypes.enum('Target', ['normal', 'tab', 'tab_fg', 'tab_bg',
|
||||
'window', 'yank', 'yank_primary', 'run',
|
||||
'fill', 'hover', 'download', 'userscript',
|
||||
'spawn'])
|
||||
Target = usertypes.enum('Target', ['normal', 'current', 'tab', 'tab_fg',
|
||||
'tab_bg', 'window', 'yank', 'yank_primary',
|
||||
'run', 'fill', 'hover', 'download',
|
||||
'userscript', 'spawn'])
|
||||
|
||||
|
||||
class WordHintingError(Exception):
|
||||
|
||||
"""Exception raised on errors during word hinting."""
|
||||
|
||||
|
||||
@pyqtSlot(usertypes.KeyMode)
|
||||
@@ -65,7 +71,8 @@ class HintContext:
|
||||
elems: A mapping from key strings to (elem, label) namedtuples.
|
||||
baseurl: The URL of the current page.
|
||||
target: What to do with the opened links.
|
||||
normal/tab/tab_fg/tab_bg/window: Get passed to BrowserTab.
|
||||
normal/current/tab/tab_fg/tab_bg/window: Get passed to
|
||||
BrowserTab.
|
||||
yank/yank_primary: Yank to clipboard/primary selection.
|
||||
run: Run a command.
|
||||
fill: Fill commandline with link.
|
||||
@@ -122,6 +129,7 @@ class HintManager(QObject):
|
||||
|
||||
HINT_TEXTS = {
|
||||
Target.normal: "Follow hint",
|
||||
Target.current: "Follow hint in current tab",
|
||||
Target.tab: "Follow hint in new tab",
|
||||
Target.tab_fg: "Follow hint in foreground tab",
|
||||
Target.tab_bg: "Follow hint in background tab",
|
||||
@@ -146,6 +154,7 @@ class HintManager(QObject):
|
||||
self._win_id = win_id
|
||||
self._tab_id = tab_id
|
||||
self._context = None
|
||||
self._word_hinter = WordHinter()
|
||||
mode_manager = objreg.get('mode-manager', scope='window',
|
||||
window=win_id)
|
||||
mode_manager.left.connect(self.on_mode_left)
|
||||
@@ -198,7 +207,14 @@ class HintManager(QObject):
|
||||
Return:
|
||||
A list of hint strings, in the same order as the elements.
|
||||
"""
|
||||
if config.get('hints', 'mode') == 'number':
|
||||
hint_mode = config.get('hints', 'mode')
|
||||
if hint_mode == 'word':
|
||||
try:
|
||||
return self._word_hinter.hint(elems)
|
||||
except WordHintingError as e:
|
||||
message.error(self._win_id, str(e), immediately=True)
|
||||
# falls back on letter hints
|
||||
if hint_mode == 'number':
|
||||
chars = '0123456789'
|
||||
else:
|
||||
chars = config.get('hints', 'chars')
|
||||
@@ -373,7 +389,7 @@ class HintManager(QObject):
|
||||
label.setStyleProperty('left', '{}px !important'.format(left))
|
||||
label.setStyleProperty('top', '{}px !important'.format(top))
|
||||
|
||||
def _draw_label(self, elem, string):
|
||||
def _draw_label(self, elem, text):
|
||||
"""Draw a hint label over an element.
|
||||
|
||||
Args:
|
||||
@@ -398,7 +414,7 @@ class HintManager(QObject):
|
||||
label = webelem.WebElementWrapper(parent.lastChild())
|
||||
label['class'] = 'qutehint'
|
||||
self._set_style_properties(elem, label)
|
||||
label.setPlainText(string)
|
||||
label.setPlainText(text)
|
||||
return label
|
||||
|
||||
def _show_url_error(self):
|
||||
@@ -415,6 +431,7 @@ class HintManager(QObject):
|
||||
"""
|
||||
target_mapping = {
|
||||
Target.normal: usertypes.ClickTarget.normal,
|
||||
Target.current: usertypes.ClickTarget.normal,
|
||||
Target.tab_fg: usertypes.ClickTarget.tab,
|
||||
Target.tab_bg: usertypes.ClickTarget.tab_bg,
|
||||
Target.window: usertypes.ClickTarget.window,
|
||||
@@ -449,6 +466,8 @@ class HintManager(QObject):
|
||||
QMouseEvent(QEvent.MouseButtonRelease, pos, Qt.LeftButton,
|
||||
Qt.NoButton, modifiers),
|
||||
]
|
||||
if context.target == Target.current:
|
||||
elem.remove_blank_target()
|
||||
for evt in events:
|
||||
self.mouse_event.emit(evt)
|
||||
if elem.is_text_input() and elem.is_editable():
|
||||
@@ -465,11 +484,13 @@ class HintManager(QObject):
|
||||
context: The HintContext to use.
|
||||
"""
|
||||
sel = context.target == Target.yank_primary
|
||||
mode = QClipboard.Selection if sel else QClipboard.Clipboard
|
||||
urlstr = url.toString(QUrl.FullyEncoded | QUrl.RemovePassword)
|
||||
QApplication.clipboard().setText(urlstr, mode)
|
||||
message.info(self._win_id, "URL yanked to {}".format(
|
||||
"primary selection" if sel else "clipboard"))
|
||||
utils.set_clipboard(urlstr, selection=sel)
|
||||
|
||||
msg = "Yanked URL to {}: {}".format(
|
||||
"primary selection" if sel else "clipboard",
|
||||
urlstr)
|
||||
message.info(self._win_id, msg)
|
||||
|
||||
def _run_cmd(self, url, context):
|
||||
"""Run the command based on a hint URL.
|
||||
@@ -660,14 +681,15 @@ class HintManager(QObject):
|
||||
elems = [e for e in elems if filterfunc(e)]
|
||||
if not elems:
|
||||
raise cmdexc.CommandError("No elements found.")
|
||||
strings = self._hint_strings(elems)
|
||||
for e, string in zip(elems, strings):
|
||||
label = self._draw_label(e, string)
|
||||
self._context.elems[string] = ElemTuple(e, label)
|
||||
hints = self._hint_strings(elems)
|
||||
log.hints.debug("hints: {}".format(', '.join(hints)))
|
||||
for e, hint in zip(elems, hints):
|
||||
label = self._draw_label(e, hint)
|
||||
self._context.elems[hint] = ElemTuple(e, label)
|
||||
keyparsers = objreg.get('keyparsers', scope='window',
|
||||
window=self._win_id)
|
||||
keyparser = keyparsers[usertypes.KeyMode.hint]
|
||||
keyparser.update_bindings(strings)
|
||||
keyparser.update_bindings(hints)
|
||||
|
||||
def follow_prevnext(self, frame, baseurl, prev=False, tab=False,
|
||||
background=False, window=False):
|
||||
@@ -724,7 +746,8 @@ class HintManager(QObject):
|
||||
|
||||
target: What to do with the selected element.
|
||||
|
||||
- `normal`: Open the link in the current tab.
|
||||
- `normal`: Open the link.
|
||||
- `current`: Open the link in the current tab.
|
||||
- `tab`: Open the link in a new tab (honoring the
|
||||
background-tabs setting).
|
||||
- `tab-fg`: Open the link in a new foreground tab.
|
||||
@@ -810,11 +833,11 @@ class HintManager(QObject):
|
||||
def handle_partial_key(self, keystr):
|
||||
"""Handle a new partial keypress."""
|
||||
log.hints.debug("Handling new keystring: '{}'".format(keystr))
|
||||
for (string, elems) in self._context.elems.items():
|
||||
for (text, elems) in self._context.elems.items():
|
||||
try:
|
||||
if string.startswith(keystr):
|
||||
matched = string[:len(keystr)]
|
||||
rest = string[len(keystr):]
|
||||
if text.startswith(keystr):
|
||||
matched = text[:len(keystr)]
|
||||
rest = text[len(keystr):]
|
||||
match_color = config.get('colors', 'hints.fg.match')
|
||||
elems.label.setInnerXml(
|
||||
'<font color="{}">{}</font>{}'.format(
|
||||
@@ -874,6 +897,7 @@ class HintManager(QObject):
|
||||
# Handlers which take a QWebElement
|
||||
elem_handlers = {
|
||||
Target.normal: self._click,
|
||||
Target.current: self._click,
|
||||
Target.tab: self._click,
|
||||
Target.tab_fg: self._click,
|
||||
Target.tab_bg: self._click,
|
||||
@@ -915,8 +939,8 @@ class HintManager(QObject):
|
||||
# Show all hints again
|
||||
self.filter_hints(None)
|
||||
# Undo keystring highlighting
|
||||
for (string, elems) in self._context.elems.items():
|
||||
elems.label.setInnerXml(string)
|
||||
for (text, elems) in self._context.elems.items():
|
||||
elems.label.setInnerXml(text)
|
||||
handler()
|
||||
|
||||
@cmdutils.register(instance='hintmanager', scope='tab', hide=True,
|
||||
@@ -959,3 +983,120 @@ class HintManager(QObject):
|
||||
# hinting.
|
||||
return
|
||||
self._cleanup()
|
||||
|
||||
|
||||
class WordHinter:
|
||||
|
||||
"""Generator for word hints.
|
||||
|
||||
Attributes:
|
||||
words: A set of words to be used when no "smart hint" can be
|
||||
derived from the hinted element.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
# will be initialized on first use.
|
||||
self.words = set()
|
||||
self.dictionary = None
|
||||
|
||||
def ensure_initialized(self):
|
||||
"""Generate the used words if yet uninialized."""
|
||||
dictionary = config.get("hints", "dictionary")
|
||||
if not self.words or self.dictionary != dictionary:
|
||||
self.words.clear()
|
||||
self.dictionary = dictionary
|
||||
try:
|
||||
with open(dictionary, encoding="UTF-8") as wordfile:
|
||||
alphabet = set(string.ascii_lowercase)
|
||||
hints = set()
|
||||
lines = (line.rstrip().lower() for line in wordfile)
|
||||
for word in lines:
|
||||
if set(word) - alphabet:
|
||||
# contains none-alphabetic chars
|
||||
continue
|
||||
if len(word) > 4:
|
||||
# we don't need words longer than 4
|
||||
continue
|
||||
for i in range(len(word)):
|
||||
# remove all prefixes of this word
|
||||
hints.discard(word[:i + 1])
|
||||
hints.add(word)
|
||||
self.words.update(hints)
|
||||
except IOError as e:
|
||||
error = "Word hints requires reading the file at {}: {}"
|
||||
raise WordHintingError(error.format(dictionary, str(e)))
|
||||
|
||||
def extract_tag_words(self, elem):
|
||||
"""Extract tag words form the given element."""
|
||||
attr_extractors = {
|
||||
"alt": lambda elem: elem["alt"],
|
||||
"name": lambda elem: elem["name"],
|
||||
"title": lambda elem: elem["title"],
|
||||
"src": lambda elem: elem["src"].split('/')[-1],
|
||||
"href": lambda elem: elem["href"].split('/')[-1],
|
||||
"text": str,
|
||||
}
|
||||
|
||||
extractable_attrs = collections.defaultdict(
|
||||
list, {
|
||||
"IMG": ["alt", "title", "src"],
|
||||
"A": ["title", "href", "text"],
|
||||
"INPUT": ["name"]
|
||||
}
|
||||
)
|
||||
|
||||
return (attr_extractors[attr](elem)
|
||||
for attr in extractable_attrs[elem.tagName()]
|
||||
if attr in elem or attr == "text")
|
||||
|
||||
def tag_words_to_hints(self, words):
|
||||
"""Take words and transform them to proper hints if possible."""
|
||||
for candidate in words:
|
||||
if not candidate:
|
||||
continue
|
||||
match = re.search('[A-Za-z]{3,}', candidate)
|
||||
if not match:
|
||||
continue
|
||||
if 4 < match.end() - match.start() < 8:
|
||||
yield candidate[match.start():match.end()].lower()
|
||||
|
||||
def any_prefix(self, hint, existing):
|
||||
return any(hint.startswith(e) or e.startswith(hint)
|
||||
for e in existing)
|
||||
|
||||
def filter_prefixes(self, hints, existing):
|
||||
return (h for h in hints if not self.any_prefix(h, existing))
|
||||
|
||||
def new_hint_for(self, elem, existing, fallback):
|
||||
"""Return a hint for elem, not conflicting with the existing."""
|
||||
new = self.tag_words_to_hints(self.extract_tag_words(elem))
|
||||
new_no_prefixes = self.filter_prefixes(new, existing)
|
||||
fallback_no_prefixes = self.filter_prefixes(fallback, existing)
|
||||
# either the first good, or None
|
||||
return (next(new_no_prefixes, None) or
|
||||
next(fallback_no_prefixes, None))
|
||||
|
||||
def hint(self, elems):
|
||||
"""Produce hint labels based on the html tags.
|
||||
|
||||
Produce hint words based on the link text and random words
|
||||
from the words arg as fallback.
|
||||
|
||||
Args:
|
||||
words: Words to use as fallback when no link text can be used.
|
||||
elems: The elements to get hint strings for.
|
||||
|
||||
Return:
|
||||
A list of hint strings, in the same order as the elements.
|
||||
"""
|
||||
self.ensure_initialized()
|
||||
hints = []
|
||||
used_hints = set()
|
||||
words = iter(self.words)
|
||||
for elem in elems:
|
||||
hint = self.new_hint_for(elem, used_hints, words)
|
||||
if not hint:
|
||||
raise WordHintingError("Not enough words in the dictionary.")
|
||||
used_hints.add(hint)
|
||||
hints.append(hint)
|
||||
return hints
|
||||
|
||||
@@ -57,9 +57,29 @@ def is_root(directory):
|
||||
Return:
|
||||
Whether the directory is a root directory or not.
|
||||
"""
|
||||
# If you're curious as why this works:
|
||||
# dirname('/') = '/'
|
||||
# dirname('/home') = '/'
|
||||
# dirname('/home/') = '/home'
|
||||
# dirname('/home/foo') = '/home'
|
||||
# basically, for files (no trailing slash) it removes the file part, and
|
||||
# for directories, it removes the trailing slash, so the only way for this
|
||||
# to be equal is if the directory is the root directory.
|
||||
return os.path.dirname(directory) == directory
|
||||
|
||||
|
||||
def parent_dir(directory):
|
||||
"""Return the parent directory for the given directory.
|
||||
|
||||
Args:
|
||||
directory: The path to the directory.
|
||||
|
||||
Return:
|
||||
The path to the parent directory.
|
||||
"""
|
||||
return os.path.normpath(os.path.join(directory, os.pardir))
|
||||
|
||||
|
||||
def dirbrowser_html(path):
|
||||
"""Get the directory browser web page.
|
||||
|
||||
@@ -70,30 +90,25 @@ def dirbrowser_html(path):
|
||||
The HTML of the web page.
|
||||
"""
|
||||
title = "Browse directory: {}".format(path)
|
||||
template = jinja.env.get_template('dirbrowser.html')
|
||||
# pylint: disable=no-member
|
||||
# WORKAROUND for https://bitbucket.org/logilab/pylint/issue/490/
|
||||
|
||||
if is_root(path):
|
||||
parent = None
|
||||
else:
|
||||
parent = os.path.dirname(path)
|
||||
parent = parent_dir(path)
|
||||
|
||||
try:
|
||||
all_files = os.listdir(path)
|
||||
except OSError as e:
|
||||
html = jinja.env.get_template('error.html').render(
|
||||
title="Error while reading directory",
|
||||
url='file://%s' % path,
|
||||
error=str(e),
|
||||
icon='')
|
||||
html = jinja.render('error.html',
|
||||
title="Error while reading directory",
|
||||
url='file:///{}'.format(path), error=str(e),
|
||||
icon='')
|
||||
return html.encode('UTF-8', errors='xmlcharrefreplace')
|
||||
|
||||
files = get_file_list(path, all_files, os.path.isfile)
|
||||
directories = get_file_list(path, all_files, os.path.isdir)
|
||||
html = template.render(title=title, url=path, icon='',
|
||||
parent=parent, files=files,
|
||||
directories=directories)
|
||||
html = jinja.render('dirbrowser.html', title=title, url=path, icon='',
|
||||
parent=parent, files=files, directories=directories)
|
||||
return html.encode('UTF-8', errors='xmlcharrefreplace')
|
||||
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
"""Our own QNetworkAccessManager."""
|
||||
|
||||
import os
|
||||
import collections
|
||||
import netrc
|
||||
|
||||
@@ -29,7 +30,7 @@ from PyQt5.QtNetwork import (QNetworkAccessManager, QNetworkReply, QSslError,
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import (message, log, usertypes, utils, objreg, qtutils,
|
||||
urlutils)
|
||||
urlutils, debug)
|
||||
from qutebrowser.browser import cookies
|
||||
from qutebrowser.browser.network import qutescheme, networkreply
|
||||
from qutebrowser.browser.network import filescheme
|
||||
@@ -62,6 +63,11 @@ class SslError(QSslError):
|
||||
except TypeError:
|
||||
return hash((self.certificate().toDer(), self.error()))
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(
|
||||
self, error=debug.qenum_key(QSslError, self.error()),
|
||||
string=self.errorString())
|
||||
|
||||
|
||||
class NetworkManager(QNetworkAccessManager):
|
||||
|
||||
@@ -189,44 +195,51 @@ class NetworkManager(QNetworkAccessManager):
|
||||
"""
|
||||
errors = [SslError(e) for e in errors]
|
||||
ssl_strict = config.get('network', 'ssl-strict')
|
||||
log.webview.debug("SSL errors {!r}, strict {}".format(
|
||||
errors, ssl_strict))
|
||||
|
||||
try:
|
||||
host_tpl = urlutils.host_tuple(reply.url())
|
||||
except ValueError:
|
||||
host_tpl = None
|
||||
is_accepted = False
|
||||
is_rejected = False
|
||||
else:
|
||||
is_accepted = set(errors).issubset(
|
||||
self._accepted_ssl_errors[host_tpl])
|
||||
is_rejected = set(errors).issubset(
|
||||
self._rejected_ssl_errors[host_tpl])
|
||||
|
||||
if (ssl_strict and ssl_strict != 'ask') or is_rejected:
|
||||
return
|
||||
elif is_accepted:
|
||||
reply.ignoreSslErrors()
|
||||
return
|
||||
|
||||
if ssl_strict == 'ask':
|
||||
try:
|
||||
host_tpl = urlutils.host_tuple(reply.url())
|
||||
except ValueError:
|
||||
host_tpl = None
|
||||
is_accepted = False
|
||||
is_rejected = False
|
||||
else:
|
||||
is_accepted = set(errors).issubset(
|
||||
self._accepted_ssl_errors[host_tpl])
|
||||
is_rejected = set(errors).issubset(
|
||||
self._rejected_ssl_errors[host_tpl])
|
||||
if is_accepted:
|
||||
err_string = '\n'.join('- ' + err.errorString() for err in errors)
|
||||
answer = self._ask('SSL errors - continue?\n{}'.format(err_string),
|
||||
mode=usertypes.PromptMode.yesno, owner=reply)
|
||||
if answer:
|
||||
reply.ignoreSslErrors()
|
||||
elif is_rejected:
|
||||
pass
|
||||
err_dict = self._accepted_ssl_errors
|
||||
else:
|
||||
err_string = '\n'.join('- ' + err.errorString() for err in
|
||||
errors)
|
||||
answer = self._ask('SSL errors - continue?\n{}'.format(
|
||||
err_string), mode=usertypes.PromptMode.yesno,
|
||||
owner=reply)
|
||||
if answer:
|
||||
reply.ignoreSslErrors()
|
||||
d = self._accepted_ssl_errors
|
||||
else:
|
||||
d = self._rejected_ssl_errors
|
||||
if host_tpl is not None:
|
||||
d[host_tpl] += errors
|
||||
elif ssl_strict:
|
||||
pass
|
||||
err_dict = self._rejected_ssl_errors
|
||||
if host_tpl is not None:
|
||||
err_dict[host_tpl] += errors
|
||||
else:
|
||||
for err in errors:
|
||||
# FIXME we might want to use warn here (non-fatal error)
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/114
|
||||
message.error(self._win_id,
|
||||
'SSL error: {}'.format(err.errorString()))
|
||||
message.error(self._win_id, 'SSL error: {}'.format(
|
||||
err.errorString()))
|
||||
reply.ignoreSslErrors()
|
||||
self._accepted_ssl_errors[host_tpl] += errors
|
||||
|
||||
def clear_all_ssl_errors(self):
|
||||
"""Clear all remembered SSL errors."""
|
||||
self._accepted_ssl_errors.clear()
|
||||
self._rejected_ssl_errors.clear()
|
||||
|
||||
@pyqtSlot(QUrl)
|
||||
def clear_rejected_ssl_errors(self, url):
|
||||
@@ -240,11 +253,14 @@ class NetworkManager(QNetworkAccessManager):
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
@pyqtSlot('QNetworkReply', 'QAuthenticator')
|
||||
@pyqtSlot('QNetworkReply*', 'QAuthenticator*')
|
||||
def on_authentication_required(self, reply, authenticator):
|
||||
"""Called when a website needs authentication."""
|
||||
user, password = None, None
|
||||
if not hasattr(reply, "netrc_used"):
|
||||
if not hasattr(reply, "netrc_used") and 'HOME' in os.environ:
|
||||
# We'll get an OSError by netrc if 'HOME' isn't available in
|
||||
# os.environ. We don't want to log that, so we prevent it
|
||||
# altogether.
|
||||
reply.netrc_used = True
|
||||
try:
|
||||
net = netrc.netrc()
|
||||
@@ -270,7 +286,7 @@ class NetworkManager(QNetworkAccessManager):
|
||||
authenticator.setUser(user)
|
||||
authenticator.setPassword(password)
|
||||
|
||||
@pyqtSlot('QNetworkProxy', 'QAuthenticator')
|
||||
@pyqtSlot('QNetworkProxy', 'QAuthenticator*')
|
||||
def on_proxy_authentication_required(self, proxy, authenticator):
|
||||
"""Called when a proxy needs authentication."""
|
||||
proxy_id = ProxyId(proxy.type(), proxy.hostName(), proxy.port())
|
||||
|
||||
@@ -30,7 +30,8 @@ class FixedDataNetworkReply(QNetworkReply):
|
||||
|
||||
"""QNetworkReply subclass for fixed data."""
|
||||
|
||||
def __init__(self, request, fileData, mimeType, parent=None):
|
||||
def __init__(self, request, fileData, mimeType, # flake8: disable=N803
|
||||
parent=None):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
|
||||
@@ -23,8 +23,6 @@ import urllib.parse
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QUrl
|
||||
|
||||
from qutebrowser.misc import httpclient
|
||||
|
||||
|
||||
class PastebinClient(QObject):
|
||||
|
||||
@@ -47,11 +45,17 @@ class PastebinClient(QObject):
|
||||
success = pyqtSignal(str)
|
||||
error = pyqtSignal(str)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
def __init__(self, client, parent=None):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
client: The HTTPClient to use. Will be reparented.
|
||||
"""
|
||||
super().__init__(parent)
|
||||
self._client = httpclient.HTTPClient(self)
|
||||
self._client.error.connect(self.error)
|
||||
self._client.success.connect(self.on_client_success)
|
||||
client.setParent(self)
|
||||
client.error.connect(self.error)
|
||||
client.success.connect(self.on_client_success)
|
||||
self._client = client
|
||||
|
||||
def paste(self, name, title, text, parent=None):
|
||||
"""Paste the text into a pastebin and return the URL.
|
||||
|
||||
@@ -16,12 +16,6 @@
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# pylint complains when using .render() on jinja templates, so we make it shut
|
||||
# up for this whole module.
|
||||
|
||||
# pylint: disable=no-member
|
||||
# WORKAROUND for https://bitbucket.org/logilab/pylint/issue/490/
|
||||
|
||||
"""Handler functions for different qute:... pages.
|
||||
|
||||
@@ -58,6 +52,25 @@ def add_handler(name):
|
||||
return namedecorator
|
||||
|
||||
|
||||
class QuteSchemeError(Exception):
|
||||
|
||||
"""Exception to signal that a handler should return an ErrorReply.
|
||||
|
||||
Attributes correspond to the arguments in
|
||||
networkreply.ErrorNetworkReply.
|
||||
|
||||
Attributes:
|
||||
errorstring: Error string to print.
|
||||
error: Numerical error value.
|
||||
"""
|
||||
|
||||
def __init__(self, errorstring, error):
|
||||
"""Constructor."""
|
||||
self.errorstring = errorstring
|
||||
self.error = error
|
||||
super().__init__(errorstring)
|
||||
|
||||
|
||||
class QuteSchemeHandler(schemehandler.SchemeHandler):
|
||||
|
||||
"""Scheme handler for qute: URLs."""
|
||||
@@ -95,6 +108,9 @@ class QuteSchemeHandler(schemehandler.SchemeHandler):
|
||||
return networkreply.ErrorNetworkReply(
|
||||
request, str(e), QNetworkReply.ContentNotFoundError,
|
||||
self.parent())
|
||||
except QuteSchemeError as e:
|
||||
return networkreply.ErrorNetworkReply(
|
||||
request, e.errorstring, e.error, self.parent())
|
||||
mimetype, _encoding = mimetypes.guess_type(request.url().fileName())
|
||||
if mimetype is None:
|
||||
mimetype = 'text/html'
|
||||
@@ -127,17 +143,17 @@ class JSBridge(QObject):
|
||||
@add_handler('pyeval')
|
||||
def qute_pyeval(_win_id, _request):
|
||||
"""Handler for qute:pyeval. Return HTML content as bytes."""
|
||||
html = jinja.env.get_template('pre.html').render(
|
||||
title='pyeval', content=pyeval_output)
|
||||
html = jinja.render('pre.html', title='pyeval', content=pyeval_output)
|
||||
return html.encode('UTF-8', errors='xmlcharrefreplace')
|
||||
|
||||
|
||||
@add_handler('version')
|
||||
@add_handler('verizon')
|
||||
def qute_version(_win_id, _request):
|
||||
"""Handler for qute:version. Return HTML content as bytes."""
|
||||
html = jinja.env.get_template('version.html').render(
|
||||
title='Version info', version=version.version(),
|
||||
copyright=qutebrowser.__copyright__)
|
||||
html = jinja.render('version.html', title='Version info',
|
||||
version=version.version(),
|
||||
copyright=qutebrowser.__copyright__)
|
||||
return html.encode('UTF-8', errors='xmlcharrefreplace')
|
||||
|
||||
|
||||
@@ -148,7 +164,7 @@ def qute_plainlog(_win_id, _request):
|
||||
text = "Log output was disabled."
|
||||
else:
|
||||
text = log.ram_handler.dump_log()
|
||||
html = jinja.env.get_template('pre.html').render(title='log', content=text)
|
||||
html = jinja.render('pre.html', title='log', content=text)
|
||||
return html.encode('UTF-8', errors='xmlcharrefreplace')
|
||||
|
||||
|
||||
@@ -159,8 +175,7 @@ def qute_log(_win_id, _request):
|
||||
html_log = None
|
||||
else:
|
||||
html_log = log.ram_handler.dump_log(html=True)
|
||||
html = jinja.env.get_template('log.html').render(
|
||||
title='log', content=html_log)
|
||||
html = jinja.render('log.html', title='log', content=html_log)
|
||||
return html.encode('UTF-8', errors='xmlcharrefreplace')
|
||||
|
||||
|
||||
@@ -176,7 +191,8 @@ def qute_help(win_id, request):
|
||||
try:
|
||||
utils.read_file('html/doc/index.html')
|
||||
except OSError:
|
||||
html = jinja.env.get_template('error.html').render(
|
||||
html = jinja.render(
|
||||
'error.html',
|
||||
title="Error while loading documentation",
|
||||
url=request.url().toDisplayString(),
|
||||
error="This most likely means the documentation was not generated "
|
||||
@@ -195,16 +211,19 @@ def qute_help(win_id, request):
|
||||
message.error(win_id, "Your documentation is outdated! Please re-run "
|
||||
"scripts/asciidoc2html.py.")
|
||||
path = 'html/doc/{}'.format(urlpath)
|
||||
return utils.read_file(path).encode('UTF-8', errors='xmlcharrefreplace')
|
||||
if urlpath.endswith('.png'):
|
||||
return utils.read_file(path, binary=True)
|
||||
else:
|
||||
data = utils.read_file(path)
|
||||
return data.encode('UTF-8', errors='xmlcharrefreplace')
|
||||
|
||||
|
||||
@add_handler('settings')
|
||||
def qute_settings(win_id, _request):
|
||||
"""Handler for qute:settings. View/change qute configuration."""
|
||||
config_getter = functools.partial(objreg.get('config').get, raw=True)
|
||||
html = jinja.env.get_template('settings.html').render(
|
||||
win_id=win_id, title='settings', config=configdata,
|
||||
confget=config_getter)
|
||||
html = jinja.render('settings.html', win_id=win_id, title='settings',
|
||||
config=configdata, confget=config_getter)
|
||||
return html.encode('UTF-8', errors='xmlcharrefreplace')
|
||||
|
||||
|
||||
@@ -212,4 +231,13 @@ def qute_settings(win_id, _request):
|
||||
def qute_pdfjs(_win_id, request):
|
||||
"""Handler for qute://pdfjs. Return the pdf.js viewer."""
|
||||
urlpath = request.url().path()
|
||||
return pdfjs.get_pdfjs_res(urlpath)
|
||||
try:
|
||||
return pdfjs.get_pdfjs_res(urlpath)
|
||||
except pdfjs.PDFJSNotFound as e:
|
||||
# Logging as the error might get lost otherwise since we're not showing
|
||||
# the error page if a single asset is missing. This way we don't lose
|
||||
# information, as the failed pdfjs requests are still in the log.
|
||||
log.misc.warning(
|
||||
"pdfjs resource requested but not found: {}".format(e.path))
|
||||
raise QuteSchemeError("Can't find pdfjs resource '{}'".format(e.path),
|
||||
QNetworkReply.ContentNotFoundError)
|
||||
|
||||
@@ -29,9 +29,16 @@ from qutebrowser.utils import utils
|
||||
|
||||
class PDFJSNotFound(Exception):
|
||||
|
||||
"""Raised when no pdf.js installation is found."""
|
||||
"""Raised when no pdf.js installation is found.
|
||||
|
||||
pass
|
||||
Attributes:
|
||||
path: path of the file that was requested but not found.
|
||||
"""
|
||||
|
||||
def __init__(self, path):
|
||||
self.path = path
|
||||
message = "Path '{}' not found".format(path)
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
def generate_pdfjs_page(url):
|
||||
@@ -63,54 +70,63 @@ def _generate_pdfjs_script(url):
|
||||
|
||||
|
||||
def fix_urls(asset):
|
||||
"""Take a html page and replace each relative URL wth an absolute.
|
||||
"""Take a html page and replace each relative URL with an absolute.
|
||||
|
||||
This is specialized for pdf.js files and not a general purpose function.
|
||||
|
||||
Args:
|
||||
asset: js file or html page as string.
|
||||
"""
|
||||
new_urls = {
|
||||
'viewer.css': 'qute://pdfjs/web/viewer.css',
|
||||
'compatibility.js': 'qute://pdfjs/web/compatibility.js',
|
||||
'locale/locale.properties':
|
||||
'qute://pdfjs/web/locale/locale.properties',
|
||||
'l10n.js': 'qute://pdfjs/web/l10n.js',
|
||||
'../build/pdf.js': 'qute://pdfjs/build/pdf.js',
|
||||
'debugger.js': 'qute://pdfjs/web/debugger.js',
|
||||
'viewer.js': 'qute://pdfjs/web/viewer.js',
|
||||
'compressed.tracemonkey-pldi-09.pdf': '',
|
||||
'./images/': 'qute://pdfjs/web/images/',
|
||||
'../build/pdf.worker.js': 'qute://pdfjs/build/pdf.worker.js',
|
||||
'../web/cmaps/': 'qute://pdfjs/web/cmaps/',
|
||||
}
|
||||
for original, new in new_urls.items():
|
||||
new_urls = [
|
||||
('viewer.css', 'qute://pdfjs/web/viewer.css'),
|
||||
('compatibility.js', 'qute://pdfjs/web/compatibility.js'),
|
||||
('locale/locale.properties',
|
||||
'qute://pdfjs/web/locale/locale.properties'),
|
||||
('l10n.js', 'qute://pdfjs/web/l10n.js'),
|
||||
('../build/pdf.js', 'qute://pdfjs/build/pdf.js'),
|
||||
('debugger.js', 'qute://pdfjs/web/debugger.js'),
|
||||
('viewer.js', 'qute://pdfjs/web/viewer.js'),
|
||||
('compressed.tracemonkey-pldi-09.pdf', ''),
|
||||
('./images/', 'qute://pdfjs/web/images/'),
|
||||
('../build/pdf.worker.js', 'qute://pdfjs/build/pdf.worker.js'),
|
||||
('../web/cmaps/', 'qute://pdfjs/web/cmaps/'),
|
||||
]
|
||||
for original, new in new_urls:
|
||||
asset = asset.replace(original, new)
|
||||
return asset
|
||||
|
||||
|
||||
SYSTEM_PDFJS_PATHS = [
|
||||
'/usr/share/pdf.js/', # Debian pdf.js-common
|
||||
'/usr/share/javascript/pdf/', # Debian libjs-pdf
|
||||
os.path.expanduser('~/.local/share/qutebrowser/pdfjs/'), # fallback
|
||||
# Debian pdf.js-common
|
||||
# Arch Linux pdfjs (AUR)
|
||||
'/usr/share/pdf.js/',
|
||||
# Debian libjs-pdf
|
||||
'/usr/share/javascript/pdf/',
|
||||
# fallback
|
||||
os.path.expanduser('~/.local/share/qutebrowser/pdfjs/'),
|
||||
]
|
||||
|
||||
|
||||
def get_pdfjs_res(path):
|
||||
def get_pdfjs_res_and_path(path):
|
||||
"""Get a pdf.js resource in binary format.
|
||||
|
||||
Returns a (content, path) tuple, where content is the file content and path
|
||||
is the path where the file was found. If path is None, the bundled version
|
||||
was used.
|
||||
|
||||
Args:
|
||||
path: The path inside the pdfjs directory.
|
||||
"""
|
||||
path = path.lstrip('/')
|
||||
content = None
|
||||
file_path = None
|
||||
|
||||
# First try a system wide installation
|
||||
# System installations might strip off the 'build/' or 'web/' prefixes.
|
||||
# qute expects them, so we need to adjust for it.
|
||||
names_to_try = [path, _remove_prefix(path)]
|
||||
for system_path in SYSTEM_PDFJS_PATHS:
|
||||
content = _read_from_system(system_path, names_to_try)
|
||||
content, file_path = _read_from_system(system_path, names_to_try)
|
||||
if content is not None:
|
||||
break
|
||||
|
||||
@@ -120,15 +136,25 @@ def get_pdfjs_res(path):
|
||||
try:
|
||||
content = utils.read_file(res_path, binary=True)
|
||||
except FileNotFoundError:
|
||||
raise PDFJSNotFound
|
||||
raise PDFJSNotFound(path) from None
|
||||
|
||||
try:
|
||||
# Might be script/html or might be binary
|
||||
text_content = content.decode('utf-8')
|
||||
except UnicodeDecodeError:
|
||||
return content
|
||||
return (content, file_path)
|
||||
text_content = fix_urls(text_content)
|
||||
return text_content.encode('utf-8')
|
||||
return (text_content.encode('utf-8'), file_path)
|
||||
|
||||
|
||||
def get_pdfjs_res(path):
|
||||
"""Get a pdf.js resource in binary format.
|
||||
|
||||
Args:
|
||||
path: The path inside the pdfjs directory.
|
||||
"""
|
||||
content, _path = get_pdfjs_res_and_path(path)
|
||||
return content
|
||||
|
||||
|
||||
def _remove_prefix(path):
|
||||
@@ -147,10 +173,13 @@ def _remove_prefix(path):
|
||||
def _read_from_system(system_path, names):
|
||||
"""Try to read a file with one of the given names in system_path.
|
||||
|
||||
Returns a (content, path) tuple, where the path is the filepath that was
|
||||
used.
|
||||
|
||||
Each file in names is considered equal, the first file that is found
|
||||
is read and its binary content returned.
|
||||
|
||||
Returns None if no file could be found
|
||||
Returns (None, None) if no file could be found
|
||||
|
||||
Args:
|
||||
system_path: The folder where the file should be searched.
|
||||
@@ -158,11 +187,12 @@ def _read_from_system(system_path, names):
|
||||
"""
|
||||
for name in names:
|
||||
try:
|
||||
with open(os.path.join(system_path, name), 'rb') as f:
|
||||
return f.read()
|
||||
full_path = os.path.join(system_path, name)
|
||||
with open(full_path, 'rb') as f:
|
||||
return (f.read(), full_path)
|
||||
except OSError:
|
||||
continue
|
||||
return None
|
||||
return (None, None)
|
||||
|
||||
|
||||
def is_available():
|
||||
|
||||
@@ -49,7 +49,7 @@ attr_chars = string.ascii_letters + string.digits + attr_chars_nonalnum
|
||||
|
||||
|
||||
# RFC 5987 gives this alternative construction of the token character class
|
||||
token_chars = attr_chars + "*'%"
|
||||
token_chars = attr_chars + "*'%" # flake8: disable=S001
|
||||
|
||||
|
||||
# Definitions from https://tools.ietf.org/html/rfc2616#section-2.2
|
||||
|
||||
@@ -285,6 +285,19 @@ class WebElementWrapper(collections.abc.MutableMapping):
|
||||
tag = self._elem.tagName().lower()
|
||||
return self.get('role', None) in roles or tag in ('input', 'textarea')
|
||||
|
||||
def remove_blank_target(self):
|
||||
"""Remove target from link."""
|
||||
elem = self._elem
|
||||
for _ in range(5):
|
||||
if elem is None:
|
||||
break
|
||||
tag = elem.tagName().lower()
|
||||
if tag == 'a' or tag == 'area':
|
||||
if elem.attribute('target') == '_blank':
|
||||
elem.setAttribute('target', '_top')
|
||||
break
|
||||
elem = elem.parent()
|
||||
|
||||
def debug_text(self):
|
||||
"""Get a text based on an element suitable for debug output."""
|
||||
self._check_vanished()
|
||||
|
||||
@@ -122,7 +122,10 @@ class BrowserPage(QWebPage):
|
||||
"""
|
||||
ignored_errors = [
|
||||
(QWebPage.QtNetwork, QNetworkReply.OperationCanceledError),
|
||||
(QWebPage.WebKit, 203), # "Loading is handled by the media engine"
|
||||
# "Loading is handled by the media engine"
|
||||
(QWebPage.WebKit, 203),
|
||||
# "Frame load interrupted by policy change"
|
||||
(QWebPage.WebKit, 102),
|
||||
]
|
||||
errpage.baseUrl = info.url
|
||||
urlstr = info.url.toDisplayString()
|
||||
@@ -166,10 +169,8 @@ class BrowserPage(QWebPage):
|
||||
log.webview.debug("Error domain: {}, error code: {}".format(
|
||||
info.domain, info.error))
|
||||
title = "Error loading page: {}".format(urlstr)
|
||||
template = jinja.env.get_template('error.html')
|
||||
# pylint: disable=no-member
|
||||
# WORKAROUND for https://bitbucket.org/logilab/pylint/issue/490/
|
||||
html = template.render(
|
||||
html = jinja.render(
|
||||
'error.html',
|
||||
title=title, url=urlstr, error=error_str, icon='')
|
||||
errpage.content = html.encode('utf-8')
|
||||
errpage.encoding = 'utf-8'
|
||||
@@ -221,14 +222,12 @@ class BrowserPage(QWebPage):
|
||||
def _show_pdfjs(self, reply):
|
||||
"""Show the reply with pdfjs."""
|
||||
try:
|
||||
page = pdfjs.generate_pdfjs_page(reply.url()).encode('utf-8')
|
||||
page = pdfjs.generate_pdfjs_page(reply.url())
|
||||
except pdfjs.PDFJSNotFound:
|
||||
# pylint: disable=no-member
|
||||
# WORKAROUND for https://bitbucket.org/logilab/pylint/issue/490/
|
||||
page = (jinja.env.get_template('no_pdfjs.html')
|
||||
.render(url=reply.url().toDisplayString())
|
||||
.encode('utf-8'))
|
||||
self.mainFrame().setContent(page, 'text/html', reply.url())
|
||||
page = jinja.render('no_pdfjs.html',
|
||||
url=reply.url().toDisplayString())
|
||||
self.mainFrame().setContent(page.encode('utf-8'), 'text/html',
|
||||
reply.url())
|
||||
reply.deleteLater()
|
||||
|
||||
def shutdown(self):
|
||||
@@ -289,7 +288,7 @@ class BrowserPage(QWebPage):
|
||||
window=self._win_id)
|
||||
download_manager.get_request(req, page=self)
|
||||
|
||||
@pyqtSlot('QNetworkReply')
|
||||
@pyqtSlot('QNetworkReply*')
|
||||
def on_unsupported_content(self, reply):
|
||||
"""Handle an unsupportedContent signal.
|
||||
|
||||
@@ -335,7 +334,7 @@ class BrowserPage(QWebPage):
|
||||
else:
|
||||
self.error_occurred = False
|
||||
|
||||
@pyqtSlot('QWebFrame', 'QWebPage::Feature')
|
||||
@pyqtSlot('QWebFrame*', 'QWebPage::Feature')
|
||||
def on_feature_permission_requested(self, frame, feature):
|
||||
"""Ask the user for approval for geolocation/notifications."""
|
||||
options = {
|
||||
@@ -372,6 +371,7 @@ class BrowserPage(QWebPage):
|
||||
q.answered_no.connect(no_action)
|
||||
q.cancelled.connect(no_action)
|
||||
|
||||
self.shutting_down.connect(q.abort)
|
||||
q.completed.connect(q.deleteLater)
|
||||
|
||||
self.featurePermissionRequestCanceled.connect(functools.partial(
|
||||
@@ -439,7 +439,7 @@ class BrowserPage(QWebPage):
|
||||
if 'scroll-pos' in data and frame.scrollPosition() == QPoint(0, 0):
|
||||
frame.setScrollPosition(data['scroll-pos'])
|
||||
|
||||
@pyqtSlot(str)
|
||||
@pyqtSlot(usertypes.ClickTarget)
|
||||
def on_start_hinting(self, hint_target):
|
||||
"""Emitted before a hinting-click takes place.
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QTimer, QUrl
|
||||
from PyQt5.QtGui import QPalette
|
||||
from PyQt5.QtWidgets import QApplication, QStyleFactory
|
||||
from PyQt5.QtWebKit import QWebSettings
|
||||
from PyQt5.QtWebKitWidgets import QWebView, QWebPage
|
||||
from PyQt5.QtWebKitWidgets import QWebView, QWebPage, QWebFrame
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.keyinput import modeman
|
||||
@@ -352,9 +352,14 @@ class WebView(QWebView):
|
||||
frame = self.page().mainFrame()
|
||||
frame.javaScriptWindowObjectCleared.connect(self.add_js_bridge)
|
||||
|
||||
@pyqtSlot()
|
||||
def add_js_bridge(self):
|
||||
"""Add the javascript bridge for qute:... pages."""
|
||||
frame = self.sender()
|
||||
if not isinstance(frame, QWebFrame):
|
||||
log.webview.error("Got non-QWebFrame in add_js_bridge")
|
||||
return
|
||||
|
||||
if frame.url().scheme() == 'qute':
|
||||
bridge = objreg.get('js-bridge')
|
||||
frame.addToJavaScriptWindowObject('qute', bridge)
|
||||
|
||||
@@ -249,6 +249,7 @@ class CommandRunner(QObject):
|
||||
result.cmd.run(self._win_id, args)
|
||||
|
||||
@pyqtSlot(str, int)
|
||||
@pyqtSlot(str)
|
||||
def run_safely(self, text, count=None):
|
||||
"""Run a command and display exceptions in the statusbar."""
|
||||
try:
|
||||
|
||||
@@ -344,14 +344,18 @@ def run(cmd, *args, win_id, env, verbose=False):
|
||||
user_agent = config.get('network', 'user-agent')
|
||||
if user_agent is not None:
|
||||
env['QUTE_USER_AGENT'] = user_agent
|
||||
cmd = os.path.expanduser(cmd)
|
||||
cmd_path = os.path.expanduser(cmd)
|
||||
|
||||
# if cmd is not given as an absolute path, look it up
|
||||
# ~/.local/share/qutebrowser/userscripts (or $XDG_DATA_DIR)
|
||||
if not os.path.isabs(cmd):
|
||||
log.misc.debug("{} is no absolute path".format(cmd))
|
||||
cmd = os.path.join(standarddir.data(), "userscripts", cmd)
|
||||
if not os.path.isabs(cmd_path):
|
||||
log.misc.debug("{} is no absolute path".format(cmd_path))
|
||||
cmd_path = os.path.join(standarddir.data(), "userscripts", cmd)
|
||||
if not os.path.exists(cmd_path):
|
||||
cmd_path = os.path.join(standarddir.system_data(),
|
||||
"userscripts", cmd)
|
||||
log.misc.debug("Userscript to run: {}".format(cmd_path))
|
||||
|
||||
runner.run(cmd, *args, env=env, verbose=verbose)
|
||||
runner.run(cmd_path, *args, env=env, verbose=verbose)
|
||||
runner.finished.connect(commandrunner.deleteLater)
|
||||
runner.finished.connect(runner.deleteLater)
|
||||
|
||||
@@ -198,8 +198,8 @@ class CompletionItemDelegate(QStyledItemDelegate):
|
||||
columns_to_filter = index.model().srcmodel.columns_to_filter
|
||||
if index.column() in columns_to_filter and pattern:
|
||||
repl = r'<span class="highlight">\g<0></span>'
|
||||
text = re.sub(re.escape(pattern), repl, self._opt.text,
|
||||
flags=re.IGNORECASE)
|
||||
text = re.sub(re.escape(pattern).replace(r'\ ', r'.*'),
|
||||
repl, self._opt.text, flags=re.IGNORECASE)
|
||||
self._doc.setHtml(text)
|
||||
else:
|
||||
self._doc.setPlainText(self._opt.text)
|
||||
|
||||
@@ -59,6 +59,14 @@ def _init_url_completion():
|
||||
_instances[usertypes.Completion.url] = model
|
||||
|
||||
|
||||
def _init_tab_completion():
|
||||
"""Initialize the tab completion model."""
|
||||
log.completion.debug("Initializing tab completion.")
|
||||
with debug.log_time(log.completion, 'tab completion init'):
|
||||
model = miscmodels.TabCompletionModel()
|
||||
_instances[usertypes.Completion.tab] = model
|
||||
|
||||
|
||||
def _init_setting_completions():
|
||||
"""Initialize setting completion models."""
|
||||
log.completion.debug("Initializing setting completion.")
|
||||
@@ -115,6 +123,7 @@ INITIALIZERS = {
|
||||
usertypes.Completion.command: _init_command_completion,
|
||||
usertypes.Completion.helptopic: _init_helptopic_completion,
|
||||
usertypes.Completion.url: _init_url_completion,
|
||||
usertypes.Completion.tab: _init_tab_completion,
|
||||
usertypes.Completion.section: _init_setting_completions,
|
||||
usertypes.Completion.option: _init_setting_completions,
|
||||
usertypes.Completion.value: _init_setting_completions,
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
|
||||
"""Misc. CompletionModels."""
|
||||
|
||||
from PyQt5.QtCore import Qt, QTimer, pyqtSlot
|
||||
|
||||
from qutebrowser.browser import webview
|
||||
from qutebrowser.config import config, configdata
|
||||
from qutebrowser.utils import objreg, log
|
||||
from qutebrowser.commands import cmdutils
|
||||
@@ -138,3 +141,74 @@ class SessionCompletionModel(base.BaseCompletionModel):
|
||||
self.new_item(cat, name)
|
||||
except OSError:
|
||||
log.completion.exception("Failed to list sessions!")
|
||||
|
||||
|
||||
class TabCompletionModel(base.BaseCompletionModel):
|
||||
|
||||
"""A model to complete on open tabs across all windows.
|
||||
|
||||
Used for switching the buffer command."""
|
||||
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/545
|
||||
# pylint: disable=abstract-method
|
||||
|
||||
#IDX_COLUMN = 0
|
||||
URL_COLUMN = 1
|
||||
TEXT_COLUMN = 2
|
||||
|
||||
COLUMN_WIDTHS = (6, 40, 54)
|
||||
DUMB_SORT = Qt.DescendingOrder
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.columns_to_filter = [self.URL_COLUMN, self.TEXT_COLUMN]
|
||||
|
||||
for win_id in objreg.window_registry:
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=win_id)
|
||||
for i in range(tabbed_browser.count()):
|
||||
tab = tabbed_browser.widget(i)
|
||||
tab.url_text_changed.connect(self.rebuild)
|
||||
tab.shutting_down.connect(self.delayed_rebuild)
|
||||
tabbed_browser.new_tab.connect(self.on_new_tab)
|
||||
objreg.get("app").new_window.connect(self.on_new_window)
|
||||
self.rebuild()
|
||||
|
||||
def on_new_window(self, window):
|
||||
"""Add hooks to new windows."""
|
||||
window.tabbed_browser.new_tab.connect(self.on_new_tab)
|
||||
|
||||
@pyqtSlot(webview.WebView)
|
||||
def on_new_tab(self, tab):
|
||||
"""Add hooks to new tabs."""
|
||||
tab.url_text_changed.connect(self.rebuild)
|
||||
tab.shutting_down.connect(self.delayed_rebuild)
|
||||
self.rebuild()
|
||||
|
||||
@pyqtSlot()
|
||||
def delayed_rebuild(self):
|
||||
"""Fire a rebuild indirectly so widgets get a chance to update."""
|
||||
QTimer.singleShot(0, self.rebuild)
|
||||
|
||||
@pyqtSlot()
|
||||
def rebuild(self):
|
||||
"""Rebuild completion model from current tabs.
|
||||
|
||||
Very lazy method of keeping the model up to date. We could connect to
|
||||
signals for new tab, tab url/title changed, tab close, tab moved and
|
||||
make sure we handled background loads too ... but iterating over a
|
||||
few/few dozen/few hundred tabs doesn't take very long at all.
|
||||
"""
|
||||
self.removeRows(0, self.rowCount())
|
||||
for win_id in objreg.window_registry:
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=win_id)
|
||||
if tabbed_browser.shutting_down:
|
||||
continue
|
||||
c = self.new_category("{}".format(win_id))
|
||||
for i in range(tabbed_browser.count()):
|
||||
tab = tabbed_browser.widget(i)
|
||||
self.new_item(c, "{}/{}".format(win_id, i+1),
|
||||
tab.url().toDisplayString(),
|
||||
tabbed_browser.page_title(i))
|
||||
|
||||
@@ -27,6 +27,7 @@ from PyQt5.QtCore import QSortFilterProxyModel, QModelIndex, Qt
|
||||
|
||||
from qutebrowser.utils import log, qtutils, debug
|
||||
from qutebrowser.completion.models import base as completion
|
||||
import re
|
||||
|
||||
|
||||
class CompletionFilterModel(QSortFilterProxyModel):
|
||||
@@ -46,6 +47,7 @@ class CompletionFilterModel(QSortFilterProxyModel):
|
||||
super().setSourceModel(source)
|
||||
self.srcmodel = source
|
||||
self.pattern = ''
|
||||
self.pattern_re = None
|
||||
|
||||
dumb_sort = self.srcmodel.DUMB_SORT
|
||||
if dumb_sort is None:
|
||||
@@ -69,6 +71,9 @@ class CompletionFilterModel(QSortFilterProxyModel):
|
||||
"""
|
||||
with debug.log_time(log.completion, 'Setting filter pattern'):
|
||||
self.pattern = val
|
||||
val = re.escape(val)
|
||||
val = val.replace(r'\ ', r'.*')
|
||||
self.pattern_re = re.compile(val, re.IGNORECASE)
|
||||
self.invalidateFilter()
|
||||
sortcol = 0
|
||||
try:
|
||||
@@ -146,7 +151,7 @@ class CompletionFilterModel(QSortFilterProxyModel):
|
||||
data = self.srcmodel.data(idx)
|
||||
if not data:
|
||||
continue
|
||||
elif self.pattern.casefold() in data.casefold():
|
||||
elif self.pattern_re.search(data):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@@ -95,6 +95,7 @@ class change_filter: # pylint: disable=invalid-name
|
||||
"""
|
||||
if self._function:
|
||||
@pyqtSlot(str, str)
|
||||
@pyqtSlot()
|
||||
@functools.wraps(func)
|
||||
def wrapper(sectname=None, optname=None):
|
||||
if sectname is None and optname is None:
|
||||
@@ -108,6 +109,7 @@ class change_filter: # pylint: disable=invalid-name
|
||||
return func()
|
||||
else:
|
||||
@pyqtSlot(str, str)
|
||||
@pyqtSlot()
|
||||
@functools.wraps(func)
|
||||
def wrapper(wrapper_self, sectname=None, optname=None):
|
||||
if sectname is None and optname is None:
|
||||
|
||||
@@ -863,12 +863,14 @@ def data(readonly=False):
|
||||
valid_values=typ.ValidValues(
|
||||
('number', "Use numeric hints."),
|
||||
('letter', "Use the chars in the hints -> "
|
||||
"chars setting.")
|
||||
"chars setting."),
|
||||
('word', "Use hints words based on the html "
|
||||
"elements and the extra words."),
|
||||
)), 'letter'),
|
||||
"Mode to use for hints."),
|
||||
|
||||
('chars',
|
||||
SettingValue(typ.String(minlen=2, completions=[
|
||||
SettingValue(typ.UniqueCharString(minlen=2, completions=[
|
||||
('asdfghjkl', "Home row"),
|
||||
('dhtnaoeu', "Home row (Dvorak)"),
|
||||
('abcdefghijklmnopqrstuvwxyz', "All letters"),
|
||||
@@ -888,9 +890,14 @@ def data(readonly=False):
|
||||
SettingValue(typ.Bool(), 'false'),
|
||||
"Make chars in hint strings uppercase."),
|
||||
|
||||
('dictionary',
|
||||
SettingValue(typ.File(required=False), '/usr/share/dict/words'),
|
||||
"The dictionary file to be used by the word hints."),
|
||||
|
||||
('auto-follow',
|
||||
SettingValue(typ.Bool(), 'true'),
|
||||
"Whether to auto-follow a hint if there's only one left."),
|
||||
"Follow a hint immediately when the hint text is completely "
|
||||
"matched."),
|
||||
|
||||
('next-regexes',
|
||||
SettingValue(typ.RegexList(flags=re.IGNORECASE),
|
||||
@@ -1193,7 +1200,7 @@ def data(readonly=False):
|
||||
SettingValue(typ.Font(), 'Terminus, Monospace, '
|
||||
'"DejaVu Sans Mono", Monaco, '
|
||||
'"Bitstream Vera Sans Mono", "Andale Mono", '
|
||||
'"Liberation Mono", "Courier New", Courier, '
|
||||
'"Courier New", Courier, "Liberation Mono", '
|
||||
'monospace, Fixed, Consolas, Terminal'),
|
||||
"Default monospace fonts."),
|
||||
|
||||
@@ -1324,7 +1331,8 @@ KEY_SECTION_DESC = {
|
||||
"Since normal keypresses are passed through, only special keys are "
|
||||
"supported in this mode.\n"
|
||||
"Useful hidden commands to map in this section:\n\n"
|
||||
" * `open-editor`: Open a texteditor with the focused field."),
|
||||
" * `open-editor`: Open a texteditor with the focused field.\n"
|
||||
" * `paste-primary`: Paste primary selection at cursor position."),
|
||||
'hint': (
|
||||
"Keybindings for hint mode.\n"
|
||||
"Since normal keypresses are passed through, only special keys are "
|
||||
@@ -1389,8 +1397,8 @@ KEY_DATA = collections.OrderedDict([
|
||||
('tab-move', ['gm']),
|
||||
('tab-move -', ['gl']),
|
||||
('tab-move +', ['gr']),
|
||||
('tab-focus', ['J', 'gt']),
|
||||
('tab-prev', ['K', 'gT']),
|
||||
('tab-focus', ['J']),
|
||||
('tab-prev', ['K']),
|
||||
('tab-clone', ['gC']),
|
||||
('reload', ['r']),
|
||||
('reload -f', ['R']),
|
||||
@@ -1469,6 +1477,7 @@ KEY_DATA = collections.OrderedDict([
|
||||
('download-cancel', ['ad']),
|
||||
('download-clear', ['cd']),
|
||||
('view-source', ['gf']),
|
||||
('set-cmd-text -s :buffer', ['gt']),
|
||||
('tab-focus last', ['<Ctrl-Tab>']),
|
||||
('enter-mode passthrough', ['<Ctrl-V>']),
|
||||
('quit', ['<Ctrl-Q>']),
|
||||
@@ -1495,6 +1504,7 @@ KEY_DATA = collections.OrderedDict([
|
||||
|
||||
('insert', collections.OrderedDict([
|
||||
('open-editor', ['<Ctrl-E>']),
|
||||
('paste-primary', ['<Shift-Ins>']),
|
||||
])),
|
||||
|
||||
('hint', collections.OrderedDict([
|
||||
|
||||
@@ -258,6 +258,13 @@ class String(BaseType):
|
||||
self._basic_validation(value)
|
||||
if not value:
|
||||
return
|
||||
|
||||
if self.valid_values is not None:
|
||||
if value not in self.valid_values:
|
||||
raise configexc.ValidationError(
|
||||
value, "valid values: {}".format(', '.join(
|
||||
self.valid_values)))
|
||||
|
||||
if self.forbidden is not None and any(c in value
|
||||
for c in self.forbidden):
|
||||
raise configexc.ValidationError(value, "may not contain the chars "
|
||||
@@ -270,7 +277,25 @@ class String(BaseType):
|
||||
"long!".format(self.maxlen))
|
||||
|
||||
def complete(self):
|
||||
return self._completions
|
||||
if self._completions is not None:
|
||||
return self._completions
|
||||
else:
|
||||
return super().complete()
|
||||
|
||||
|
||||
class UniqueCharString(String):
|
||||
|
||||
"""A string which may not contain duplicate chars."""
|
||||
|
||||
def validate(self, value):
|
||||
super().validate(value)
|
||||
if not value:
|
||||
return
|
||||
|
||||
# Check for duplicate values
|
||||
if len(set(value)) != len(value):
|
||||
raise configexc.ValidationError(
|
||||
value, "String contains duplicate values!")
|
||||
|
||||
|
||||
class List(BaseType):
|
||||
@@ -891,6 +916,10 @@ class File(BaseType):
|
||||
|
||||
"""A file on the local filesystem."""
|
||||
|
||||
def __init__(self, required=True, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.required = required
|
||||
|
||||
def transform(self, value):
|
||||
if not value:
|
||||
return None
|
||||
@@ -899,7 +928,7 @@ class File(BaseType):
|
||||
if not os.path.isabs(value):
|
||||
cfgdir = standarddir.config()
|
||||
assert cfgdir is not None
|
||||
return os.path.join(cfgdir, value)
|
||||
value = os.path.join(cfgdir, value)
|
||||
return value
|
||||
|
||||
def validate(self, value):
|
||||
@@ -915,15 +944,13 @@ class File(BaseType):
|
||||
raise configexc.ValidationError(
|
||||
value, "must be an absolute path when not using a "
|
||||
"config directory!")
|
||||
elif not os.path.isfile(os.path.join(cfgdir, value)):
|
||||
raise configexc.ValidationError(
|
||||
value, "must be a valid path relative to the config "
|
||||
"directory!")
|
||||
else:
|
||||
return
|
||||
elif not os.path.isfile(value):
|
||||
raise configexc.ValidationError(
|
||||
value, "must be a valid file!")
|
||||
value = os.path.join(cfgdir, value)
|
||||
not_isfile_message = ("must be a valid path relative to the "
|
||||
"config directory!")
|
||||
else:
|
||||
not_isfile_message = "must be a valid file!"
|
||||
if self.required and not os.path.isfile(value):
|
||||
raise configexc.ValidationError(value, not_isfile_message)
|
||||
except UnicodeEncodeError as e:
|
||||
raise configexc.ValidationError(value, e)
|
||||
|
||||
@@ -1084,7 +1111,7 @@ class ShellCommand(BaseType):
|
||||
shlex.split(value)
|
||||
except ValueError as e:
|
||||
raise configexc.ValidationError(value, str(e))
|
||||
if self.placeholder and '{}' not in self.transform(value):
|
||||
if self.placeholder and '{}' not in value:
|
||||
raise configexc.ValidationError(value, "needs to contain a "
|
||||
"{}-placeholder.")
|
||||
|
||||
@@ -1169,7 +1196,7 @@ class SearchEngineUrl(BaseType):
|
||||
self._basic_validation(value)
|
||||
if not value:
|
||||
return
|
||||
elif '{}' not in value:
|
||||
elif not ('{}' in value or '{0}' in value):
|
||||
raise configexc.ValidationError(value, "must contain \"{}\"")
|
||||
try:
|
||||
value.format("")
|
||||
@@ -1259,8 +1286,16 @@ class UserStyleSheet(File):
|
||||
def transform(self, value):
|
||||
if not value:
|
||||
return None
|
||||
path = super().transform(value)
|
||||
if os.path.exists(path):
|
||||
|
||||
if standarddir.config() is None:
|
||||
# We can't call super().transform() here as this counts on the
|
||||
# validation previously ensuring that we don't have a relative path
|
||||
# when starting with -c "".
|
||||
path = None
|
||||
else:
|
||||
path = super().transform(value)
|
||||
|
||||
if path is not None and os.path.exists(path):
|
||||
return QUrl.fromLocalFile(path)
|
||||
else:
|
||||
data = base64.b64encode(value.encode('utf-8')).decode('ascii')
|
||||
|
||||
@@ -201,7 +201,7 @@ class KeyConfigParser(QObject):
|
||||
sect = self.keybindings[mode]
|
||||
except KeyError:
|
||||
raise cmdexc.CommandError("Can't find mode section '{}'!".format(
|
||||
sect))
|
||||
mode))
|
||||
try:
|
||||
del sect[key]
|
||||
except KeyError:
|
||||
|
||||
@@ -46,21 +46,21 @@ ul.files > li {
|
||||
<p id="dirbrowserTitleText">Browse directory: {{url}}</p>
|
||||
</div>
|
||||
|
||||
{% if parent %}
|
||||
{% if parent is not none %}
|
||||
<ul class="parent">
|
||||
<li><a href="{{parent}}">..</a></li>
|
||||
<li><a href="{{ file_url(parent) }}">..</a></li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
<ul class="folders">
|
||||
{% for item in directories %}
|
||||
<li><a href="file://{{item.absname}}">{{item.name}}</a></li>
|
||||
<li><a href="{{ file_url(item.absname) }}">{{item.name}}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<ul class="files">
|
||||
{% for item in files %}
|
||||
<li><a href="file://{{item.absname}}">{{item.name}}</a></li>
|
||||
<li><a href="{{ file_url(item.absname) }}">{{item.name}}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
22
qutebrowser/html/undef_error.html
Normal file
22
qutebrowser/html/undef_error.html
Normal file
@@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
vim: ft=html fileencoding=utf-8 sts=4 sw=4 et:
|
||||
-->
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Error while rendering HTML</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Error while rendering internal qutebrowser page</h1>
|
||||
<p>There was an error while rendering {pagename}.</p>
|
||||
|
||||
<p>This most likely happened because you updated qutebrowser but didn't restart yet.</p>
|
||||
|
||||
<p>If you believe this isn't the case and this is a bug, please do :report.<p>
|
||||
|
||||
<h2>Traceback</h2>
|
||||
<pre>{traceback}</pre>
|
||||
</body>
|
||||
</html>
|
||||
@@ -23,7 +23,6 @@ import functools
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, Qt, QObject, QEvent
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PyQt5.QtWebKitWidgets import QWebView
|
||||
|
||||
from qutebrowser.keyinput import modeparsers, keyparser
|
||||
from qutebrowser.config import config
|
||||
@@ -171,13 +170,9 @@ class ModeManager(QObject):
|
||||
is_non_alnum = (
|
||||
event.modifiers() not in (Qt.NoModifier, Qt.ShiftModifier) or
|
||||
not event.text().strip())
|
||||
focus_widget = QApplication.instance().focusWidget()
|
||||
is_tab = event.key() in (Qt.Key_Tab, Qt.Key_Backtab)
|
||||
|
||||
if handled:
|
||||
filter_this = True
|
||||
elif is_tab and not isinstance(focus_widget, QWebView):
|
||||
filter_this = True
|
||||
elif (parser.passthrough or
|
||||
self._forward_unbound_keys == 'all' or
|
||||
(self._forward_unbound_keys == 'auto' and is_non_alnum)):
|
||||
@@ -189,11 +184,12 @@ class ModeManager(QObject):
|
||||
self._releaseevents_to_pass.add(KeyEvent(event))
|
||||
|
||||
if curmode != usertypes.KeyMode.insert:
|
||||
focus_widget = QApplication.instance().focusWidget()
|
||||
log.modes.debug("handled: {}, forward-unbound-keys: {}, "
|
||||
"passthrough: {}, is_non_alnum: {}, is_tab {} --> "
|
||||
"passthrough: {}, is_non_alnum: {} --> "
|
||||
"filter: {} (focused: {!r})".format(
|
||||
handled, self._forward_unbound_keys,
|
||||
parser.passthrough, is_non_alnum, is_tab,
|
||||
parser.passthrough, is_non_alnum,
|
||||
filter_this, focus_widget))
|
||||
return filter_this
|
||||
|
||||
|
||||
@@ -79,8 +79,8 @@ def get_window(via_ipc, force_window=False, force_tab=False,
|
||||
if open_target not in ('tab-silent', 'tab-bg-silent'):
|
||||
window_to_raise = window
|
||||
if window_to_raise is not None:
|
||||
window_to_raise.setWindowState(window.windowState() &
|
||||
~Qt.WindowMinimized | Qt.WindowActive)
|
||||
window_to_raise.setWindowState(
|
||||
window.windowState() & ~Qt.WindowMinimized | Qt.WindowActive)
|
||||
window_to_raise.raise_()
|
||||
window_to_raise.activateWindow()
|
||||
QApplication.instance().alert(window_to_raise)
|
||||
@@ -187,6 +187,8 @@ class MainWindow(QWidget):
|
||||
#self.tabWidget.setCurrentIndex(0)
|
||||
#QtCore.QMetaObject.connectSlotsByName(MainWindow)
|
||||
|
||||
objreg.get("app").new_window.emit(self)
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self)
|
||||
|
||||
@@ -417,9 +419,6 @@ class MainWindow(QWidget):
|
||||
window=self.win_id)
|
||||
download_count = download_manager.rowCount()
|
||||
quit_texts = []
|
||||
# Close if set to never ask for confirmation
|
||||
if 'never' in confirm_quit:
|
||||
pass
|
||||
# Ask if multiple-tabs are open
|
||||
if 'multiple-tabs' in confirm_quit and tab_count > 1:
|
||||
quit_texts.append("{} {} open.".format(
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
from PyQt5.QtCore import pyqtSlot
|
||||
|
||||
from qutebrowser.mainwindow.statusbar import textbase
|
||||
from qutebrowser.browser import webview
|
||||
|
||||
|
||||
class Percentage(textbase.TextBase):
|
||||
@@ -48,7 +49,7 @@ class Percentage(textbase.TextBase):
|
||||
else:
|
||||
self.setText('[{:2}%]'.format(y))
|
||||
|
||||
@pyqtSlot(object)
|
||||
@pyqtSlot(webview.WebView)
|
||||
def on_tab_changed(self, tab):
|
||||
"""Update scroll position when tab changed."""
|
||||
self.set_perc(*tab.scroll_pos)
|
||||
|
||||
@@ -59,7 +59,7 @@ class Progress(QProgressBar):
|
||||
self.setValue(0)
|
||||
self.show()
|
||||
|
||||
@pyqtSlot(int)
|
||||
@pyqtSlot(webview.WebView)
|
||||
def on_tab_changed(self, tab):
|
||||
"""Set the correct value when the current tab changed."""
|
||||
if self is None: # pragma: no branch
|
||||
|
||||
@@ -24,6 +24,7 @@ from PyQt5.QtCore import pyqtSlot
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.mainwindow.statusbar import textbase
|
||||
from qutebrowser.utils import usertypes, log, objreg
|
||||
from qutebrowser.browser import webview
|
||||
|
||||
|
||||
class Text(textbase.TextBase):
|
||||
@@ -98,7 +99,7 @@ class Text(textbase.TextBase):
|
||||
"""Clear jstext when page loading started."""
|
||||
self._jstext = ''
|
||||
|
||||
@pyqtSlot(int)
|
||||
@pyqtSlot(webview.WebView)
|
||||
def on_tab_changed(self, tab):
|
||||
"""Set the correct jstext when the current tab changed."""
|
||||
self._jstext = tab.statusbar_message
|
||||
|
||||
@@ -158,7 +158,7 @@ class UrlText(textbase.TextBase):
|
||||
self._hover_url = None
|
||||
self._update_url()
|
||||
|
||||
@pyqtSlot(int)
|
||||
@pyqtSlot(webview.WebView)
|
||||
def on_tab_changed(self, tab):
|
||||
"""Update URL if the tab changed."""
|
||||
self._hover_url = None
|
||||
|
||||
@@ -63,7 +63,7 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
tabbar -> new-tab-position set to 'left'.
|
||||
_tab_insert_idx_right: Same as above, for 'right'.
|
||||
_undo_stack: List of UndoEntry namedtuples of closed tabs.
|
||||
_shutting_down: Whether we're currently shutting down.
|
||||
shutting_down: Whether we're currently shutting down.
|
||||
|
||||
Signals:
|
||||
cur_progress: Progress of the current tab changed (loadProgress).
|
||||
@@ -82,6 +82,7 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
widget can adjust its size to it.
|
||||
arg: The new size.
|
||||
current_tab_changed: The current tab changed to the emitted WebView.
|
||||
new_tab: Emits the new WebView and its index when a new tab is opened.
|
||||
"""
|
||||
|
||||
cur_progress = pyqtSignal(int)
|
||||
@@ -96,13 +97,14 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
resized = pyqtSignal('QRect')
|
||||
got_cmd = pyqtSignal(str)
|
||||
current_tab_changed = pyqtSignal(webview.WebView)
|
||||
new_tab = pyqtSignal(webview.WebView, int)
|
||||
|
||||
def __init__(self, win_id, parent=None):
|
||||
super().__init__(win_id, parent)
|
||||
self._win_id = win_id
|
||||
self._tab_insert_idx_left = 0
|
||||
self._tab_insert_idx_right = -1
|
||||
self._shutting_down = False
|
||||
self.shutting_down = False
|
||||
self.tabCloseRequested.connect(self.on_tab_close_requested)
|
||||
self.currentChanged.connect(self.on_current_changed)
|
||||
self.cur_load_started.connect(self.on_cur_load_started)
|
||||
@@ -234,7 +236,7 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
|
||||
def shutdown(self):
|
||||
"""Try to shut down all tabs cleanly."""
|
||||
self._shutting_down = True
|
||||
self.shutting_down = True
|
||||
for tab in self.widgets():
|
||||
self._remove_tab(tab)
|
||||
|
||||
@@ -272,8 +274,8 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
"""
|
||||
idx = self.indexOf(tab)
|
||||
if idx == -1:
|
||||
raise ValueError("tab {} is not contained in TabbedWidget!".format(
|
||||
tab))
|
||||
raise TabDeletedError("tab {} is not contained in "
|
||||
"TabbedWidget!".format(tab))
|
||||
if tab is self._now_focused:
|
||||
self._now_focused = None
|
||||
if tab is objreg.get('last-focused-tab', None, scope='window',
|
||||
@@ -303,6 +305,7 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
"""Undo removing of a tab."""
|
||||
# Remove unused tab which may be created after the last tab is closed
|
||||
last_close = config.get('tabs', 'last-close')
|
||||
use_current_tab = False
|
||||
if last_close in ['blank', 'startpage', 'default-page']:
|
||||
only_one_tab_open = self.count() == 1
|
||||
no_history = self.widget(0).history().count() == 1
|
||||
@@ -315,12 +318,17 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
last_close_urlstr = urls[last_close].toString().rstrip('/')
|
||||
first_tab_urlstr = first_tab_url.toString().rstrip('/')
|
||||
last_close_url_used = first_tab_urlstr == last_close_urlstr
|
||||
|
||||
if only_one_tab_open and no_history and last_close_url_used:
|
||||
self.removeTab(0)
|
||||
use_current_tab = (only_one_tab_open and no_history and
|
||||
last_close_url_used)
|
||||
|
||||
url, history_data = self._undo_stack.pop()
|
||||
newtab = self.tabopen(url, background=False)
|
||||
|
||||
if use_current_tab:
|
||||
self.openurl(url, newtab=False)
|
||||
newtab = self.widget(0)
|
||||
else:
|
||||
newtab = self.tabopen(url, background=False)
|
||||
|
||||
qtutils.deserialize(history_data, newtab.history())
|
||||
|
||||
@pyqtSlot('QUrl', bool)
|
||||
@@ -350,7 +358,11 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
@pyqtSlot(webview.WebView)
|
||||
def on_window_close_requested(self, widget):
|
||||
"""Close a tab with a widget given."""
|
||||
self.close_tab(widget)
|
||||
try:
|
||||
self.close_tab(widget)
|
||||
except TabDeletedError:
|
||||
log.webview.debug("Requested to close {!r} which does not "
|
||||
"exist!".format(widget))
|
||||
|
||||
@pyqtSlot('QUrl', bool)
|
||||
def tabopen(self, url=None, background=None, explicit=False):
|
||||
@@ -394,6 +406,7 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
if not background:
|
||||
self.setCurrentWidget(tab)
|
||||
tab.show()
|
||||
self.new_tab.emit(tab, idx)
|
||||
return tab
|
||||
|
||||
def _get_new_tab_idx(self, explicit):
|
||||
@@ -542,7 +555,7 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
@pyqtSlot(int)
|
||||
def on_current_changed(self, idx):
|
||||
"""Set last-focused-tab and leave hinting mode when focus changed."""
|
||||
if idx == -1 or self._shutting_down:
|
||||
if idx == -1 or self.shutting_down:
|
||||
# closing the last tab (before quitting) or shutting down
|
||||
return
|
||||
tab = self.widget(idx)
|
||||
@@ -609,7 +622,7 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
def on_scroll_pos_changed(self):
|
||||
"""Update tab and window title when scroll position changed."""
|
||||
self.update_window_title()
|
||||
self.update_tab_titles()
|
||||
self.update_tab_title(self.currentIndex())
|
||||
|
||||
def resizeEvent(self, e):
|
||||
"""Extend resizeEvent of QWidget to emit a resized signal afterwards.
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014-2016 Florian Bruhin (The-Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
# qutebrowser is free software: you can redistribute it and/or modify
|
||||
|
||||
@@ -26,6 +26,7 @@ import html
|
||||
import getpass
|
||||
import fnmatch
|
||||
import traceback
|
||||
import datetime
|
||||
|
||||
import pkg_resources
|
||||
from PyQt5.QtCore import pyqtSlot, Qt, QSize, qVersion
|
||||
@@ -35,7 +36,7 @@ from PyQt5.QtWidgets import (QDialog, QLabel, QTextEdit, QPushButton,
|
||||
|
||||
import qutebrowser
|
||||
from qutebrowser.utils import version, log, utils, objreg, qtutils
|
||||
from qutebrowser.misc import miscwidgets, autoupdate, msgbox
|
||||
from qutebrowser.misc import miscwidgets, autoupdate, msgbox, httpclient
|
||||
from qutebrowser.browser.network import pastebin
|
||||
from qutebrowser.config import config
|
||||
|
||||
@@ -95,12 +96,13 @@ def get_fatal_crash_dialog(debug, data):
|
||||
|
||||
def _get_environment_vars():
|
||||
"""Gather environment variables for the crash info."""
|
||||
masks = ('DESKTOP_SESSION', 'DE', 'QT_*', 'PYTHON*', 'LC_*', 'LANG')
|
||||
masks = ('DESKTOP_SESSION', 'DE', 'QT_*', 'PYTHON*', 'LC_*', 'LANG',
|
||||
'XDG_*')
|
||||
info = []
|
||||
for key, value in os.environ.items():
|
||||
for m in masks:
|
||||
if fnmatch.fnmatch(key, m):
|
||||
info.append('%s = %s' % (key, value))
|
||||
info.append('{} = {}'.format(key, value))
|
||||
return '\n'.join(sorted(info))
|
||||
|
||||
|
||||
@@ -139,7 +141,8 @@ class _CrashDialog(QDialog):
|
||||
self.setWindowTitle("Whoops!")
|
||||
self.resize(QSize(640, 600))
|
||||
self._vbox = QVBoxLayout(self)
|
||||
self._paste_client = pastebin.PastebinClient(self)
|
||||
http_client = httpclient.HTTPClient()
|
||||
self._paste_client = pastebin.PastebinClient(http_client, self)
|
||||
self._pypi_client = autoupdate.PyPIVersionClient(self)
|
||||
self._init_text()
|
||||
|
||||
@@ -236,7 +239,9 @@ class _CrashDialog(QDialog):
|
||||
try:
|
||||
application = QApplication.instance()
|
||||
launch_time = application.launch_time.ctime()
|
||||
self._crash_info.append(('Launch time', launch_time))
|
||||
crash_time = datetime.datetime.now().ctime()
|
||||
text = 'Launch: {}\nCrash: {}'.format(launch_time, crash_time)
|
||||
self._crash_info.append(('Timestamps', text))
|
||||
except Exception:
|
||||
self._crash_info.append(("Launch time", traceback.format_exc()))
|
||||
try:
|
||||
@@ -504,11 +509,23 @@ class FatalCrashDialog(_CrashDialog):
|
||||
def _init_text(self):
|
||||
super()._init_text()
|
||||
text = ("<b>qutebrowser was restarted after a fatal crash.</b><br/>"
|
||||
"<br/>Note: Crash reports for fatal crashes sometimes don't "
|
||||
"QTWEBENGINE_NOTE"
|
||||
"<br/>Crash reports for fatal crashes sometimes don't "
|
||||
"contain the information necessary to fix an issue. Please "
|
||||
"follow the steps in <a href='https://github.com/The-Compiler/"
|
||||
"qutebrowser/blob/master/doc/stacktrace.asciidoc'>"
|
||||
"stacktrace.asciidoc</a> to submit a stacktrace.<br/>")
|
||||
|
||||
if datetime.datetime.now() < datetime.datetime(2016, 4, 23):
|
||||
note = ("<br/>Fatal crashes like this are often caused by the "
|
||||
"current QtWebKit backend.<br/><b>I'm currently running a "
|
||||
"crowdfunding for the new QtWebEngine backend, based on "
|
||||
"Chromium:</b> <a href='http://igg.me/at/qutebrowser'>"
|
||||
"igg.me/at/qutebrowser</a><br/>")
|
||||
text = text.replace('QTWEBENGINE_NOTE', note)
|
||||
else:
|
||||
text = text.replace('QTWEBENGINE_NOTE', '')
|
||||
|
||||
self._lbl.setText(text)
|
||||
|
||||
def _init_checkboxes(self):
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
import os
|
||||
import sys
|
||||
import bdb
|
||||
import pdb
|
||||
import pdb # flake8: disable=T002
|
||||
import signal
|
||||
import functools
|
||||
import faulthandler
|
||||
@@ -72,7 +72,7 @@ class CrashHandler(QObject):
|
||||
|
||||
def handle_segfault(self):
|
||||
"""Handle a segfault from a previous run."""
|
||||
data_dir = None
|
||||
data_dir = standarddir.data()
|
||||
if data_dir is None:
|
||||
return
|
||||
logname = os.path.join(data_dir, 'crash.log')
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014-2016 Florian Bruhin (The-Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
# qutebrowser is free software: you can redistribute it and/or modify
|
||||
|
||||
@@ -58,7 +58,8 @@ class ExternalEditor(QObject):
|
||||
return
|
||||
try:
|
||||
os.close(self._oshandle)
|
||||
os.remove(self._filename)
|
||||
if self._proc.exit_status() != QProcess.CrashExit:
|
||||
os.remove(self._filename)
|
||||
except OSError as e:
|
||||
# NOTE: Do not replace this with "raise CommandError" as it's
|
||||
# executed async.
|
||||
@@ -124,6 +125,6 @@ class ExternalEditor(QObject):
|
||||
self._proc.error.connect(self.on_proc_error)
|
||||
editor = config.get('general', 'editor')
|
||||
executable = editor[0]
|
||||
args = [self._filename if arg == '{}' else arg for arg in editor[1:]]
|
||||
args = [arg.replace('{}', self._filename) for arg in editor[1:]]
|
||||
log.procs.debug("Calling \"{}\" with args {}".format(executable, args))
|
||||
self._proc.start(executable, args)
|
||||
|
||||
@@ -160,3 +160,6 @@ class GUIProcess(QObject):
|
||||
else:
|
||||
message.error(self._win_id, "Error while spawning {}: {}.".format(
|
||||
self._what, self._proc.error()), immediately=True)
|
||||
|
||||
def exit_status(self):
|
||||
return self._proc.exitStatus()
|
||||
|
||||
@@ -221,7 +221,7 @@ class IPCServer(QObject):
|
||||
# This means we only use setSocketOption on Windows...
|
||||
os.chmod(self._server.fullServerName(), 0o700)
|
||||
|
||||
@pyqtSlot(int)
|
||||
@pyqtSlot('QLocalSocket::LocalSocketError')
|
||||
def on_error(self, err):
|
||||
"""Raise SocketError on fatal errors."""
|
||||
if self._socket is None:
|
||||
@@ -229,8 +229,9 @@ class IPCServer(QObject):
|
||||
log.ipc.debug("In on_error with None socket!")
|
||||
return
|
||||
self._timer.stop()
|
||||
log.ipc.debug("Socket error {}: {}".format(
|
||||
self._socket.error(), self._socket.errorString()))
|
||||
log.ipc.debug("Socket 0x{:x}: error {}: {}".format(
|
||||
id(self._socket), self._socket.error(),
|
||||
self._socket.errorString()))
|
||||
if err != QLocalSocket.PeerClosedError:
|
||||
raise SocketError("handling IPC connection", self._socket)
|
||||
|
||||
@@ -241,13 +242,14 @@ class IPCServer(QObject):
|
||||
return
|
||||
if self._socket is not None:
|
||||
log.ipc.debug("Got new connection but ignoring it because we're "
|
||||
"still handling another one.")
|
||||
"still handling another one (0x{:x}).".format(
|
||||
id(self._socket)))
|
||||
return
|
||||
socket = self._server.nextPendingConnection()
|
||||
if socket is None:
|
||||
log.ipc.debug("No new connection to handle.")
|
||||
return
|
||||
log.ipc.debug("Client connected.")
|
||||
log.ipc.debug("Client connected (socket 0x{:x}).".format(id(socket)))
|
||||
self._timer.start()
|
||||
self._socket = socket
|
||||
socket.readyRead.connect(self.on_ready_read)
|
||||
@@ -267,7 +269,8 @@ class IPCServer(QObject):
|
||||
@pyqtSlot()
|
||||
def on_disconnected(self):
|
||||
"""Clean up socket when the client disconnected."""
|
||||
log.ipc.debug("Client disconnected.")
|
||||
log.ipc.debug("Client disconnected from socket 0x{:x}.".format(
|
||||
id(self._socket)))
|
||||
self._timer.stop()
|
||||
if self._socket is None:
|
||||
log.ipc.debug("In on_disconnected with None socket!")
|
||||
@@ -279,11 +282,61 @@ class IPCServer(QObject):
|
||||
|
||||
def _handle_invalid_data(self):
|
||||
"""Handle invalid data we got from a QLocalSocket."""
|
||||
log.ipc.error("Ignoring invalid IPC data.")
|
||||
log.ipc.error("Ignoring invalid IPC data from socket 0x{:x}.".format(
|
||||
id(self._socket)))
|
||||
self.got_invalid_data.emit()
|
||||
self._socket.error.connect(self.on_error)
|
||||
self._socket.disconnectFromServer()
|
||||
|
||||
def _handle_data(self, data):
|
||||
"""Handle data (as bytes) we got from on_ready_ready_read."""
|
||||
try:
|
||||
decoded = data.decode('utf-8')
|
||||
except UnicodeDecodeError:
|
||||
log.ipc.error("invalid utf-8: {}".format(
|
||||
binascii.hexlify(data)))
|
||||
self._handle_invalid_data()
|
||||
return
|
||||
|
||||
log.ipc.debug("Processing: {}".format(decoded))
|
||||
try:
|
||||
json_data = json.loads(decoded)
|
||||
except ValueError:
|
||||
log.ipc.error("invalid json: {}".format(decoded.strip()))
|
||||
self._handle_invalid_data()
|
||||
return
|
||||
|
||||
for name in ('args', 'target_arg'):
|
||||
if name not in json_data:
|
||||
log.ipc.error("Missing {}: {}".format(name, decoded.strip()))
|
||||
self._handle_invalid_data()
|
||||
return
|
||||
|
||||
try:
|
||||
protocol_version = int(json_data['protocol_version'])
|
||||
except (KeyError, ValueError):
|
||||
log.ipc.error("invalid version: {}".format(decoded.strip()))
|
||||
self._handle_invalid_data()
|
||||
return
|
||||
|
||||
if protocol_version != PROTOCOL_VERSION:
|
||||
log.ipc.error("incompatible version: expected {}, got {}".format(
|
||||
PROTOCOL_VERSION, protocol_version))
|
||||
self._handle_invalid_data()
|
||||
return
|
||||
|
||||
args = json_data['args']
|
||||
|
||||
target_arg = json_data['target_arg']
|
||||
if target_arg is None:
|
||||
# https://www.riverbankcomputing.com/pipermail/pyqt/2016-April/037375.html
|
||||
target_arg = ''
|
||||
|
||||
cwd = json_data.get('cwd', '')
|
||||
assert cwd is not None
|
||||
|
||||
self.got_args.emit(args, target_arg, cwd)
|
||||
|
||||
@pyqtSlot()
|
||||
def on_ready_read(self):
|
||||
"""Read json data from the client."""
|
||||
@@ -292,56 +345,20 @@ class IPCServer(QObject):
|
||||
# active for some reason.
|
||||
log.ipc.warning("In on_ready_read with None socket!")
|
||||
return
|
||||
self._timer.start()
|
||||
self._timer.stop()
|
||||
while self._socket is not None and self._socket.canReadLine():
|
||||
data = bytes(self._socket.readLine())
|
||||
self.got_raw.emit(data)
|
||||
log.ipc.debug("Read from socket: {}".format(data))
|
||||
|
||||
try:
|
||||
decoded = data.decode('utf-8')
|
||||
except UnicodeDecodeError:
|
||||
log.ipc.error("invalid utf-8: {}".format(
|
||||
binascii.hexlify(data)))
|
||||
self._handle_invalid_data()
|
||||
return
|
||||
|
||||
log.ipc.debug("Processing: {}".format(decoded))
|
||||
try:
|
||||
json_data = json.loads(decoded)
|
||||
except ValueError:
|
||||
log.ipc.error("invalid json: {}".format(decoded.strip()))
|
||||
self._handle_invalid_data()
|
||||
return
|
||||
|
||||
for name in ('args', 'target_arg'):
|
||||
if name not in json_data:
|
||||
log.ipc.error("Missing {}: {}".format(name,
|
||||
decoded.strip()))
|
||||
self._handle_invalid_data()
|
||||
return
|
||||
|
||||
try:
|
||||
protocol_version = int(json_data['protocol_version'])
|
||||
except (KeyError, ValueError):
|
||||
log.ipc.error("invalid version: {}".format(decoded.strip()))
|
||||
self._handle_invalid_data()
|
||||
return
|
||||
|
||||
if protocol_version != PROTOCOL_VERSION:
|
||||
log.ipc.error("incompatible version: expected {}, "
|
||||
"got {}".format(
|
||||
PROTOCOL_VERSION, protocol_version))
|
||||
self._handle_invalid_data()
|
||||
return
|
||||
|
||||
cwd = json_data.get('cwd', None)
|
||||
self.got_args.emit(json_data['args'], json_data['target_arg'], cwd)
|
||||
log.ipc.debug("Read from socket 0x{:x}: {}".format(
|
||||
id(self._socket), data))
|
||||
self._handle_data(data)
|
||||
self._timer.start()
|
||||
|
||||
@pyqtSlot()
|
||||
def on_timeout(self):
|
||||
"""Cancel the current connection if it was idle for too long."""
|
||||
log.ipc.error("IPC connection timed out.")
|
||||
log.ipc.error("IPC connection timed out "
|
||||
"(socket 0x{:x}).".format(id(self._socket)))
|
||||
self._socket.disconnectFromServer()
|
||||
if self._socket is not None: # pragma: no cover
|
||||
# on_socket_disconnected sets it to None
|
||||
@@ -369,7 +386,8 @@ class IPCServer(QObject):
|
||||
|
||||
def shutdown(self):
|
||||
"""Shut down the IPC server cleanly."""
|
||||
log.ipc.debug("Shutting down IPC")
|
||||
log.ipc.debug("Shutting down IPC (socket 0x{:x})".format(
|
||||
id(self._socket)))
|
||||
if self._socket is not None:
|
||||
self._socket.deleteLater()
|
||||
self._socket = None
|
||||
|
||||
@@ -20,9 +20,9 @@
|
||||
"""Misc. widgets used at different places."""
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QSize
|
||||
from PyQt5.QtWidgets import (QLineEdit, QApplication, QWidget, QHBoxLayout,
|
||||
QLabel, QStyleOption, QStyle)
|
||||
from PyQt5.QtGui import QValidator, QClipboard, QPainter
|
||||
from PyQt5.QtWidgets import (QLineEdit, QWidget, QHBoxLayout, QLabel,
|
||||
QStyleOption, QStyle)
|
||||
from PyQt5.QtGui import QValidator, QPainter
|
||||
|
||||
from qutebrowser.utils import utils
|
||||
from qutebrowser.misc import cmdhistory
|
||||
@@ -42,6 +42,19 @@ class MinimalLineEditMixin:
|
||||
""")
|
||||
self.setAttribute(Qt.WA_MacShowFocusRect, False)
|
||||
|
||||
def keyPressEvent(self, e):
|
||||
"""Override keyPressEvent to paste primary selection on Shift + Ins."""
|
||||
if e.key() == Qt.Key_Insert and e.modifiers() == Qt.ShiftModifier:
|
||||
try:
|
||||
text = utils.get_clipboard(selection=True)
|
||||
except utils.SelectionUnsupportedError:
|
||||
pass
|
||||
else:
|
||||
e.accept()
|
||||
self.insert(text)
|
||||
return
|
||||
super().keyPressEvent(e)
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self)
|
||||
|
||||
@@ -98,17 +111,6 @@ class CommandLineEdit(QLineEdit):
|
||||
if mark:
|
||||
self.setSelection(self._promptlen, oldpos - self._promptlen)
|
||||
|
||||
def keyPressEvent(self, e):
|
||||
"""Override keyPressEvent to paste primary selection on Shift + Ins."""
|
||||
if e.key() == Qt.Key_Insert and e.modifiers() == Qt.ShiftModifier:
|
||||
clipboard = QApplication.clipboard()
|
||||
if clipboard.supportsSelection():
|
||||
e.accept()
|
||||
text = clipboard.text(QClipboard.Selection)
|
||||
self.insert(text)
|
||||
return
|
||||
super().keyPressEvent(e)
|
||||
|
||||
|
||||
class _CommandValidator(QValidator):
|
||||
|
||||
|
||||
@@ -86,9 +86,9 @@ class Saveable:
|
||||
(not config.get(*self._config_opt)) and
|
||||
(not explicit) and (not force)):
|
||||
if not silent:
|
||||
log.save.debug("Not saving {} because autosaving has been "
|
||||
log.save.debug("Not saving {name} because autosaving has been "
|
||||
"disabled by {cfg[0]} -> {cfg[1]}.".format(
|
||||
self._name, cfg=self._config_opt))
|
||||
name=self._name, cfg=self._config_opt))
|
||||
return
|
||||
do_save = self._dirty or (self._save_on_exit and is_exit) or force
|
||||
if not silent:
|
||||
|
||||
@@ -143,10 +143,20 @@ class SessionManager(QObject):
|
||||
history = tab.page().history()
|
||||
for idx, item in enumerate(history.items()):
|
||||
qtutils.ensure_valid(item)
|
||||
|
||||
item_data = {
|
||||
'url': bytes(item.url().toEncoded()).decode('ascii'),
|
||||
'title': item.title(),
|
||||
}
|
||||
|
||||
if item.title():
|
||||
item_data['title'] = item.title()
|
||||
else:
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/879
|
||||
if history.currentItemIndex() == idx:
|
||||
item_data['title'] = tab.page().mainFrame().title()
|
||||
else:
|
||||
item_data['title'] = item_data['url']
|
||||
|
||||
if item.originalUrl() != item.url():
|
||||
encoded = item.originalUrl().toEncoded()
|
||||
item_data['original-url'] = bytes(encoded).decode('ascii')
|
||||
@@ -231,7 +241,9 @@ class SessionManager(QObject):
|
||||
log.sessions.debug("Saving session {} to {}...".format(name, path))
|
||||
if last_window:
|
||||
data = self._last_window_session
|
||||
assert data is not None
|
||||
if data is None:
|
||||
log.sessions.error("last_window_session is None while saving!")
|
||||
return
|
||||
else:
|
||||
data = self._save_all()
|
||||
log.sessions.vdebug("Saving data: {}".format(data))
|
||||
|
||||
@@ -29,12 +29,14 @@ except ImportError:
|
||||
hunter = None
|
||||
|
||||
from qutebrowser.browser.network import qutescheme
|
||||
from qutebrowser.utils import log, objreg, usertypes, message, debug
|
||||
from qutebrowser.utils import log, objreg, usertypes, message, debug, utils
|
||||
from qutebrowser.commands import cmdutils, runners, cmdexc
|
||||
from qutebrowser.config import style
|
||||
from qutebrowser.misc import consolewidget
|
||||
|
||||
from PyQt5.QtCore import QUrl
|
||||
# so it's available for :debug-pyeval
|
||||
from PyQt5.QtWidgets import QApplication # pylint: disable=unused-import
|
||||
|
||||
|
||||
@cmdutils.register(maxsplit=1, no_cmd_split=True, win_id='win_id')
|
||||
@@ -176,18 +178,36 @@ def debug_trace(expr=""):
|
||||
|
||||
|
||||
@cmdutils.register(maxsplit=0, debug=True, no_cmd_split=True)
|
||||
def debug_pyeval(s):
|
||||
def debug_pyeval(s, quiet=False):
|
||||
"""Evaluate a python string and display the results as a web page.
|
||||
|
||||
Args:
|
||||
s: The string to evaluate.
|
||||
quiet: Don't show the output in a new tab.
|
||||
"""
|
||||
try:
|
||||
r = eval(s)
|
||||
out = repr(r)
|
||||
except Exception:
|
||||
out = traceback.format_exc()
|
||||
|
||||
qutescheme.pyeval_output = out
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window='last-focused')
|
||||
tabbed_browser.openurl(QUrl('qute:pyeval'), newtab=True)
|
||||
if quiet:
|
||||
log.misc.debug("pyeval output: {}".format(out))
|
||||
else:
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window='last-focused')
|
||||
tabbed_browser.openurl(QUrl('qute:pyeval'), newtab=True)
|
||||
|
||||
|
||||
@cmdutils.register(debug=True)
|
||||
def debug_set_fake_clipboard(s=None):
|
||||
"""Put data into the fake clipboard and enable logging, used for tests.
|
||||
|
||||
Args:
|
||||
s: The text to put into the fake clipboard, or unset to enable logging.
|
||||
"""
|
||||
if s is None:
|
||||
utils.log_clipboard = True
|
||||
else:
|
||||
utils.fake_clipboard = s
|
||||
|
||||
@@ -70,10 +70,13 @@ def get_argparser():
|
||||
help="How URLs should be opened if there is already a "
|
||||
"qutebrowser instance running.")
|
||||
parser.add_argument('--json-args', help=argparse.SUPPRESS)
|
||||
parser.add_argument('--temp-basedir-restarted', help=argparse.SUPPRESS)
|
||||
|
||||
debug = parser.add_argument_group('debug arguments')
|
||||
debug.add_argument('-l', '--loglevel', dest='loglevel',
|
||||
help="Set loglevel", default='info')
|
||||
help="Set loglevel", default='info',
|
||||
choices=['critical', 'error', 'warning', 'info',
|
||||
'debug', 'vdebug'])
|
||||
debug.add_argument('--logfilter',
|
||||
help="Comma-separated list of things to be logged "
|
||||
"to the debug log on stdout.")
|
||||
@@ -129,7 +132,6 @@ def get_argparser():
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point for qutebrowser."""
|
||||
parser = get_argparser()
|
||||
if sys.platform == 'darwin' and getattr(sys, 'frozen', False):
|
||||
# Ignore Mac OS X' idiotic -psn_* argument...
|
||||
|
||||
@@ -21,10 +21,12 @@
|
||||
|
||||
import os
|
||||
import os.path
|
||||
import traceback
|
||||
|
||||
import jinja2
|
||||
import jinja2.exceptions
|
||||
|
||||
from qutebrowser.utils import utils
|
||||
from qutebrowser.utils import utils, log
|
||||
|
||||
from PyQt5.QtCore import QUrl
|
||||
|
||||
@@ -71,5 +73,28 @@ def resource_url(path):
|
||||
image = utils.resource_filename(path)
|
||||
return QUrl.fromLocalFile(image).toString(QUrl.FullyEncoded)
|
||||
|
||||
env = jinja2.Environment(loader=Loader('html'), autoescape=_guess_autoescape)
|
||||
env.globals['resource_url'] = resource_url
|
||||
|
||||
def file_url(path):
|
||||
"""Return a file:// url (as string) to the given local path.
|
||||
|
||||
Arguments:
|
||||
path: The absolute path to the local file
|
||||
"""
|
||||
return QUrl.fromLocalFile(path).toString(QUrl.FullyEncoded)
|
||||
|
||||
|
||||
def render(template, **kwargs):
|
||||
"""Render the given template and pass the given arguments to it."""
|
||||
try:
|
||||
return _env.get_template(template).render(**kwargs)
|
||||
except jinja2.exceptions.UndefinedError:
|
||||
log.misc.exception("UndefinedError while rendering " + template)
|
||||
err_path = os.path.join('html', 'undef_error.html')
|
||||
err_template = utils.read_file(err_path)
|
||||
tb = traceback.format_exc()
|
||||
return err_template.format(pagename=template, traceback=tb)
|
||||
|
||||
|
||||
_env = jinja2.Environment(loader=Loader('html'), autoescape=_guess_autoescape)
|
||||
_env.globals['resource_url'] = resource_url
|
||||
_env.globals['file_url'] = file_url
|
||||
|
||||
@@ -51,10 +51,11 @@ else:
|
||||
colorama.deinit()
|
||||
|
||||
# Log formats to use.
|
||||
SIMPLE_FMT = '{levelname}: {message}'
|
||||
SIMPLE_FMT = '{asctime:8} {levelname}: {message}'
|
||||
EXTENDED_FMT = ('{asctime:8} {levelname:8} {name:10} {module}:{funcName}:'
|
||||
'{lineno} {message}')
|
||||
SIMPLE_FMT_COLORED = '%(log_color)s%(levelname)s%(reset)s: %(message)s'
|
||||
SIMPLE_FMT_COLORED = ('%(green)s%(asctime)-8s%(reset)s '
|
||||
'%(log_color)s%(levelname)s%(reset)s: %(message)s')
|
||||
EXTENDED_FMT_COLORED = (
|
||||
'%(green)s%(asctime)-8s%(reset)s '
|
||||
'%(log_color)s%(levelname)-8s%(reset)s '
|
||||
@@ -274,7 +275,7 @@ def qt_message_handler(msg_type, context, msg):
|
||||
# PNGs in Qt with broken color profile
|
||||
# https://bugreports.qt.io/browse/QTBUG-39788
|
||||
'libpng warning: iCCP: Not recognizing known sRGB profile that has '
|
||||
'been edited', # noqa
|
||||
'been edited', # flake8: disable=E131
|
||||
'libpng warning: iCCP: known incorrect sRGB profile',
|
||||
# Hopefully harmless warning
|
||||
'OpenType support missing for script ',
|
||||
@@ -294,6 +295,8 @@ def qt_message_handler(msg_type, context, msg):
|
||||
# Hopefully harmless
|
||||
'"Method "GetAll" with signature "s" on interface '
|
||||
'"org.freedesktop.DBus.Properties" doesn\'t exist',
|
||||
'"Method \\"GetAll\\" with signature \\"s\\" on interface '
|
||||
'\\"org.freedesktop.DBus.Properties\\" doesn\'t exist\\n"',
|
||||
'WOFF support requires QtWebKit to be built with zlib support.',
|
||||
# Weird Enlightment/GTK X extensions
|
||||
'QXcbWindow: Unhandled client message: "_E_',
|
||||
@@ -312,7 +315,7 @@ def qt_message_handler(msg_type, context, msg):
|
||||
'libpng warning: iCCP: known incorrect sRGB profile',
|
||||
# https://bugreports.qt.io/browse/QTBUG-47154
|
||||
'virtual void QSslSocketBackendPrivate::transmit() SSLRead failed '
|
||||
'with: -9805', # noqa
|
||||
'with: -9805', # flake8: disable=E131
|
||||
]
|
||||
|
||||
# Messages which will trigger an exception immediately
|
||||
|
||||
@@ -25,7 +25,7 @@ import os.path
|
||||
|
||||
from PyQt5.QtCore import QCoreApplication, QStandardPaths
|
||||
|
||||
from qutebrowser.utils import log, qtutils
|
||||
from qutebrowser.utils import log, qtutils, debug
|
||||
|
||||
|
||||
# The argparse namespace passed to init()
|
||||
@@ -65,6 +65,17 @@ def data():
|
||||
return path
|
||||
|
||||
|
||||
def system_data():
|
||||
"""Get a location for system-wide data. This path may be read-only."""
|
||||
if sys.platform.startswith('linux'):
|
||||
path = "/usr/share/qutebrowser"
|
||||
if not os.path.exists(path):
|
||||
path = data()
|
||||
else:
|
||||
path = data()
|
||||
return path
|
||||
|
||||
|
||||
def cache():
|
||||
"""Get a location for the cache."""
|
||||
typ = QStandardPaths.CacheLocation
|
||||
@@ -113,6 +124,8 @@ def _writable_location(typ):
|
||||
"""Wrapper around QStandardPaths.writableLocation."""
|
||||
with qtutils.unset_organization():
|
||||
path = QStandardPaths.writableLocation(typ)
|
||||
typ_str = debug.qenum_key(QStandardPaths, typ)
|
||||
log.misc.debug("writable location for {}: {}".format(typ_str, path))
|
||||
if not path:
|
||||
raise ValueError("QStandardPaths returned an empty value!")
|
||||
# Qt seems to use '/' as path separator even on Windows...
|
||||
|
||||
@@ -74,11 +74,13 @@ def _parse_search_term(s):
|
||||
term = s
|
||||
else:
|
||||
term = split[1]
|
||||
elif not split:
|
||||
raise ValueError("Empty search term!")
|
||||
else:
|
||||
engine = None
|
||||
term = s
|
||||
|
||||
log.url.debug("engine {}, term '{}'".format(engine, term))
|
||||
log.url.debug("engine {}, term {!r}".format(engine, term))
|
||||
return (engine, term)
|
||||
|
||||
|
||||
@@ -91,7 +93,7 @@ def _get_search_url(txt):
|
||||
Return:
|
||||
The search URL as a QUrl.
|
||||
"""
|
||||
log.url.debug("Finding search engine for '{}'".format(txt))
|
||||
log.url.debug("Finding search engine for {!r}".format(txt))
|
||||
engine, term = _parse_search_term(txt)
|
||||
assert term
|
||||
if engine is None:
|
||||
@@ -168,24 +170,13 @@ def fuzzy_url(urlstr, cwd=None, relative=False, do_search=True):
|
||||
Return:
|
||||
A target QUrl to a search page or the original URL.
|
||||
"""
|
||||
expanded = os.path.expanduser(urlstr)
|
||||
if os.path.isabs(expanded):
|
||||
path = expanded
|
||||
elif relative and cwd:
|
||||
path = os.path.join(cwd, expanded)
|
||||
elif relative:
|
||||
try:
|
||||
path = os.path.abspath(expanded)
|
||||
except OSError:
|
||||
path = None
|
||||
else:
|
||||
path = None
|
||||
urlstr = urlstr.strip()
|
||||
path = get_path_if_valid(urlstr, cwd=cwd, relative=relative,
|
||||
check_exists=True)
|
||||
|
||||
stripped = urlstr.strip()
|
||||
if path is not None and os.path.exists(path):
|
||||
log.url.debug("URL is a local file")
|
||||
if path is not None:
|
||||
url = QUrl.fromLocalFile(path)
|
||||
elif (not do_search) or is_url(stripped):
|
||||
elif (not do_search) or is_url(urlstr):
|
||||
# probably an address
|
||||
log.url.debug("URL is a fuzzy address")
|
||||
url = qurl_from_user_input(urlstr)
|
||||
@@ -194,10 +185,10 @@ def fuzzy_url(urlstr, cwd=None, relative=False, do_search=True):
|
||||
try:
|
||||
url = _get_search_url(urlstr)
|
||||
except ValueError: # invalid search engine
|
||||
url = qurl_from_user_input(stripped)
|
||||
log.url.debug("Converting fuzzy term {} to URL -> {}".format(
|
||||
url = qurl_from_user_input(urlstr)
|
||||
log.url.debug("Converting fuzzy term {!r} to URL -> {}".format(
|
||||
urlstr, url.toDisplayString()))
|
||||
if do_search and config.get('general', 'auto-search'):
|
||||
if do_search and config.get('general', 'auto-search') and urlstr:
|
||||
qtutils.ensure_valid(url)
|
||||
else:
|
||||
if not url.isValid():
|
||||
@@ -215,9 +206,9 @@ def _has_explicit_scheme(url):
|
||||
# after the scheme delimiter. Since we don't know of any URIs
|
||||
# using this and want to support e.g. searching for scoped C++
|
||||
# symbols, we treat this as not an URI anyways.
|
||||
return (url.isValid() and url.scheme()
|
||||
and not url.path().startswith(' ')
|
||||
and not url.path().startswith(':'))
|
||||
return (url.isValid() and url.scheme() and
|
||||
not url.path().startswith(' ') and
|
||||
not url.path().startswith(':'))
|
||||
|
||||
|
||||
def is_special_url(url):
|
||||
@@ -243,7 +234,7 @@ def is_url(urlstr):
|
||||
"""
|
||||
autosearch = config.get('general', 'auto-search')
|
||||
|
||||
log.url.debug("Checking if '{}' is a URL (autosearch={}).".format(
|
||||
log.url.debug("Checking if {!r} is a URL (autosearch={}).".format(
|
||||
urlstr, autosearch))
|
||||
|
||||
urlstr = urlstr.strip()
|
||||
@@ -253,8 +244,12 @@ def is_url(urlstr):
|
||||
if not autosearch:
|
||||
# no autosearch, so everything is a URL unless it has an explicit
|
||||
# search engine.
|
||||
engine, _term = _parse_search_term(urlstr)
|
||||
return engine is None
|
||||
try:
|
||||
engine, _term = _parse_search_term(urlstr)
|
||||
except ValueError:
|
||||
return False
|
||||
else:
|
||||
return engine is None
|
||||
|
||||
if not qurl_userinput.isValid():
|
||||
# This will also catch URLs containing spaces.
|
||||
@@ -342,6 +337,44 @@ def raise_cmdexc_if_invalid(url):
|
||||
raise cmdexc.CommandError(get_errstring(url))
|
||||
|
||||
|
||||
def get_path_if_valid(pathstr, cwd=None, relative=False, check_exists=False):
|
||||
"""Check if path is a valid path.
|
||||
|
||||
Args:
|
||||
pathstr: The path as string.
|
||||
cwd: The current working directory, or None.
|
||||
relative: Whether to resolve relative files.
|
||||
check_exists: Whether to check if the file
|
||||
actually exists of filesystem.
|
||||
|
||||
Return:
|
||||
The path if it is a valid path, None otherwise.
|
||||
"""
|
||||
pathstr = pathstr.strip()
|
||||
log.url.debug("Checking if {!r} is a path".format(pathstr))
|
||||
expanded = os.path.expanduser(pathstr)
|
||||
|
||||
if os.path.isabs(expanded):
|
||||
path = expanded
|
||||
elif relative and cwd:
|
||||
path = os.path.join(cwd, expanded)
|
||||
elif relative:
|
||||
try:
|
||||
path = os.path.abspath(expanded)
|
||||
except OSError:
|
||||
path = None
|
||||
else:
|
||||
path = None
|
||||
|
||||
if check_exists:
|
||||
if path is not None and os.path.exists(path):
|
||||
log.url.debug("URL is a local file")
|
||||
else:
|
||||
path = None
|
||||
|
||||
return path
|
||||
|
||||
|
||||
def filename_from_url(url):
|
||||
"""Get a suitable filename from an URL.
|
||||
|
||||
@@ -462,6 +495,28 @@ class IncDecError(Exception):
|
||||
return '{}: {}'.format(self.msg, self.url.toString())
|
||||
|
||||
|
||||
def _get_incdec_value(match, incdec, url):
|
||||
"""Get a incremented/decremented URL based on a URL match."""
|
||||
pre, zeroes, number, post = match.groups()
|
||||
# This should always succeed because we match \d+
|
||||
val = int(number)
|
||||
if incdec == 'decrement':
|
||||
if val <= 0:
|
||||
raise IncDecError("Can't decrement {}!".format(val), url)
|
||||
val -= 1
|
||||
elif incdec == 'increment':
|
||||
val += 1
|
||||
else:
|
||||
raise ValueError("Invalid value {} for indec!".format(incdec))
|
||||
if zeroes:
|
||||
if len(number) < len(str(val)):
|
||||
zeroes = zeroes[1:]
|
||||
elif len(number) > len(str(val)):
|
||||
zeroes += '0'
|
||||
|
||||
return ''.join([pre, zeroes, str(val), post])
|
||||
|
||||
|
||||
def incdec_number(url, incdec, segments=None):
|
||||
"""Find a number in the url and increment or decrement it.
|
||||
|
||||
@@ -503,23 +558,11 @@ def incdec_number(url, incdec, segments=None):
|
||||
continue
|
||||
|
||||
# Get the last number in a string
|
||||
match = re.match(r'(.*\D|^)(\d+)(.*)', getter())
|
||||
match = re.match(r'(.*\D|^)(0*)(\d+)(.*)', getter())
|
||||
if not match:
|
||||
continue
|
||||
|
||||
pre, number, post = match.groups()
|
||||
# This should always succeed because we match \d+
|
||||
val = int(number)
|
||||
if incdec == 'decrement':
|
||||
if val <= 0:
|
||||
raise IncDecError("Can't decrement {}!".format(val), url)
|
||||
val -= 1
|
||||
elif incdec == 'increment':
|
||||
val += 1
|
||||
else:
|
||||
raise ValueError("Invalid value {} for indec!".format(incdec))
|
||||
new_value = ''.join([pre, str(val), post])
|
||||
setter(new_value)
|
||||
setter(_get_incdec_value(match, incdec, url))
|
||||
return url
|
||||
|
||||
raise IncDecError("No number found in URL!", url)
|
||||
|
||||
@@ -237,7 +237,7 @@ KeyMode = enum('KeyMode', ['normal', 'hint', 'command', 'yesno', 'prompt',
|
||||
# Available command completions
|
||||
Completion = enum('Completion', ['command', 'section', 'option', 'value',
|
||||
'helptopic', 'quickmark_by_name',
|
||||
'bookmark_by_url', 'url', 'sessions'])
|
||||
'bookmark_by_url', 'url', 'tab', 'sessions'])
|
||||
|
||||
|
||||
# Exit statuses for errors. Needs to be an int for sys.exit.
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
import io
|
||||
import sys
|
||||
import enum
|
||||
import json
|
||||
import os.path
|
||||
import collections
|
||||
import functools
|
||||
@@ -29,13 +30,23 @@ import contextlib
|
||||
import itertools
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtGui import QKeySequence, QColor
|
||||
from PyQt5.QtGui import QKeySequence, QColor, QClipboard
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
import pkg_resources
|
||||
|
||||
import qutebrowser
|
||||
from qutebrowser.utils import qtutils, log
|
||||
|
||||
|
||||
fake_clipboard = None
|
||||
log_clipboard = False
|
||||
|
||||
|
||||
class SelectionUnsupportedError(Exception):
|
||||
|
||||
"""Raised if [gs]et_clipboard is used and selection=True is unsupported."""
|
||||
|
||||
|
||||
def elide(text, length):
|
||||
"""Elide text so it uses a maximum of length chars."""
|
||||
if length < 1:
|
||||
@@ -743,3 +754,33 @@ def newest_slice(iterable, count):
|
||||
return iterable
|
||||
else:
|
||||
return itertools.islice(iterable, len(iterable) - count, len(iterable))
|
||||
|
||||
|
||||
def set_clipboard(data, selection=False):
|
||||
"""Set the clipboard to some given data."""
|
||||
clipboard = QApplication.clipboard()
|
||||
if selection and not clipboard.supportsSelection():
|
||||
raise SelectionUnsupportedError
|
||||
if log_clipboard:
|
||||
what = 'primary selection' if selection else 'clipboard'
|
||||
log.misc.debug("Setting fake {}: {}".format(what, json.dumps(data)))
|
||||
else:
|
||||
mode = QClipboard.Selection if selection else QClipboard.Clipboard
|
||||
clipboard.setText(data, mode=mode)
|
||||
|
||||
|
||||
def get_clipboard(selection=False):
|
||||
"""Get data from the clipboard."""
|
||||
global fake_clipboard
|
||||
clipboard = QApplication.clipboard()
|
||||
if selection and not clipboard.supportsSelection():
|
||||
raise SelectionUnsupportedError
|
||||
|
||||
if fake_clipboard is not None:
|
||||
data = fake_clipboard
|
||||
fake_clipboard = None
|
||||
else:
|
||||
mode = QClipboard.Selection if selection else QClipboard.Clipboard
|
||||
data = clipboard.text(mode=mode)
|
||||
|
||||
return data
|
||||
|
||||
@@ -192,16 +192,20 @@ def _pdfjs_version():
|
||||
A string with the version number.
|
||||
"""
|
||||
try:
|
||||
pdfjs_file = pdfjs.get_pdfjs_res('build/pdf.js').decode('utf-8')
|
||||
pdfjs_file, file_path = pdfjs.get_pdfjs_res_and_path('build/pdf.js')
|
||||
except pdfjs.PDFJSNotFound:
|
||||
return 'no'
|
||||
else:
|
||||
pdfjs_file = pdfjs_file.decode('utf-8')
|
||||
version_re = re.compile(r"^PDFJS\.version = '([^']+)';$", re.MULTILINE)
|
||||
match = version_re.search(pdfjs_file)
|
||||
if not match:
|
||||
return 'unknown'
|
||||
pdfjs_version = 'unknown'
|
||||
else:
|
||||
return match.group(1)
|
||||
pdfjs_version = match.group(1)
|
||||
if file_path is None:
|
||||
file_path = 'bundled'
|
||||
return '{} ({})'.format(pdfjs_version, file_path)
|
||||
|
||||
|
||||
def version(short=False):
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
Jinja2==2.8.0
|
||||
MarkupSafe==0.23
|
||||
Pygments==2.0.2
|
||||
Pygments==2.1.3
|
||||
pyPEG2==2.15.2
|
||||
PyYAML==3.11
|
||||
# "ValueError: I/O operation on closed file" with pytest since 0.3.5
|
||||
# WORKAROUND for https://github.com/tartley/colorama/issues/81
|
||||
colorama==0.3.3 # rq.filter: <=0.3.3
|
||||
colorlog==2.6.0
|
||||
colorama==0.3.7
|
||||
colorlog==2.6.1
|
||||
cssutils==1.0.1
|
||||
|
||||
@@ -43,6 +43,7 @@ class AsciiDoc:
|
||||
FILES = [
|
||||
('FAQ.asciidoc', 'qutebrowser/html/doc/FAQ.html'),
|
||||
('CHANGELOG.asciidoc', 'qutebrowser/html/doc/CHANGELOG.html'),
|
||||
('CONTRIBUTING.asciidoc', 'qutebrowser/html/doc/CONTRIBUTING.html'),
|
||||
('doc/quickstart.asciidoc', 'qutebrowser/html/doc/quickstart.html'),
|
||||
('doc/userscripts.asciidoc', 'qutebrowser/html/doc/userscripts.html'),
|
||||
]
|
||||
@@ -75,6 +76,7 @@ class AsciiDoc:
|
||||
self._build_website()
|
||||
else:
|
||||
self._build_docs()
|
||||
self._copy_images()
|
||||
|
||||
def _build_docs(self):
|
||||
"""Render .asciidoc files to .html sites."""
|
||||
@@ -83,8 +85,38 @@ class AsciiDoc:
|
||||
name, _ext = os.path.splitext(os.path.basename(src))
|
||||
dst = 'qutebrowser/html/doc/{}.html'.format(name)
|
||||
files.append((src, dst))
|
||||
|
||||
# patch image links to use local copy
|
||||
replacements = [
|
||||
("http://qutebrowser.org/img/cheatsheet-big.png",
|
||||
"qute://help/img/cheatsheet-big.png"),
|
||||
("http://qutebrowser.org/img/cheatsheet-small.png",
|
||||
"qute://help/img/cheatsheet-small.png")
|
||||
]
|
||||
|
||||
for src, dst in files:
|
||||
self.call(src, dst)
|
||||
src_basename = os.path.basename(src)
|
||||
modified_src = os.path.join(self._tempdir, src_basename)
|
||||
with open(modified_src, 'w', encoding='utf-8') as modified_f, \
|
||||
open(src, 'r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
for orig, repl in replacements:
|
||||
line = line.replace(orig, repl)
|
||||
modified_f.write(line)
|
||||
self.call(modified_src, dst)
|
||||
|
||||
def _copy_images(self):
|
||||
"""Copy image files to qutebrowser/html/doc."""
|
||||
print("Copying files...")
|
||||
dst_path = os.path.join('qutebrowser', 'html', 'doc', 'img')
|
||||
try:
|
||||
os.mkdir(dst_path)
|
||||
except FileExistsError:
|
||||
pass
|
||||
for filename in ['cheatsheet-big.png', 'cheatsheet-small.png']:
|
||||
src = os.path.join('doc', 'img', filename)
|
||||
dst = os.path.join(dst_path, filename)
|
||||
shutil.copy(src, dst)
|
||||
|
||||
def _build_website_file(self, root, filename):
|
||||
"""Build a single website file."""
|
||||
@@ -248,6 +280,8 @@ def main(colors=False):
|
||||
"asciidoc.py. If not given, it's searched in PATH.",
|
||||
nargs=2, required=False,
|
||||
metavar=('PYTHON', 'ASCIIDOC'))
|
||||
parser.add_argument('--no-authors', help=argparse.SUPPRESS,
|
||||
action='store_true')
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
os.mkdir('qutebrowser/html/doc')
|
||||
|
||||
@@ -91,7 +91,7 @@ def smoke_test(executable):
|
||||
def build_windows():
|
||||
"""Build windows executables/setups."""
|
||||
utils.print_title("Updating 3rdparty content")
|
||||
update_3rdparty.main()
|
||||
update_3rdparty.update_pdfjs()
|
||||
|
||||
utils.print_title("Building Windows binaries")
|
||||
parts = str(sys.version_info.major), str(sys.version_info.minor)
|
||||
@@ -182,7 +182,6 @@ def build_sdist():
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--asciidoc', help="Full path to python and "
|
||||
"asciidoc.py. If not given, it's searched in PATH.",
|
||||
|
||||
@@ -65,6 +65,8 @@ PERFECT_FILES = [
|
||||
'qutebrowser/browser/network/filescheme.py'),
|
||||
('tests/unit/browser/network/test_networkreply.py',
|
||||
'qutebrowser/browser/network/networkreply.py'),
|
||||
('tests/unit/browser/network/test_pastebin.py',
|
||||
'qutebrowser/browser/network/pastebin.py'),
|
||||
('tests/unit/browser/test_signalfilter.py',
|
||||
'qutebrowser/browser/signalfilter.py'),
|
||||
|
||||
@@ -100,6 +102,10 @@ PERFECT_FILES = [
|
||||
'qutebrowser/mainwindow/statusbar/tabindex.py'),
|
||||
('tests/unit/mainwindow/statusbar/test_textbase.py',
|
||||
'qutebrowser/mainwindow/statusbar/textbase.py'),
|
||||
('tests/unit/mainwindow/statusbar/test_prompt.py',
|
||||
'qutebrowser/mainwindow/statusbar/prompt.py'),
|
||||
('tests/unit/mainwindow/statusbar/test_url.py',
|
||||
'qutebrowser/mainwindow/statusbar/url.py'),
|
||||
|
||||
('tests/unit/config/test_configtypes.py',
|
||||
'qutebrowser/config/configtypes.py'),
|
||||
@@ -133,6 +139,10 @@ PERFECT_FILES = [
|
||||
]
|
||||
|
||||
|
||||
# 100% coverage because of integration tests, but no perfect unit tests yet.
|
||||
WHITELISTED_FILES = []
|
||||
|
||||
|
||||
class Skipped(Exception):
|
||||
|
||||
"""Exception raised when skipping coverage checks."""
|
||||
@@ -199,7 +209,8 @@ def check(fileobj, perfect_files):
|
||||
text = "{} has {}% line and {}% branch coverage!".format(
|
||||
filename, line_cov, branch_cov)
|
||||
messages.append(Message(MsgType.insufficent_coverage, text))
|
||||
elif filename not in perfect_src_files and not is_bad:
|
||||
elif (filename not in perfect_src_files and not is_bad and
|
||||
filename not in WHITELISTED_FILES):
|
||||
text = ("{} has 100% coverage but is not in "
|
||||
"perfect_files!".format(filename))
|
||||
messages.append(Message(MsgType.perfect_file, text))
|
||||
@@ -216,8 +227,16 @@ def main_check():
|
||||
print(e)
|
||||
messages = []
|
||||
|
||||
for msg in messages:
|
||||
print(msg.text)
|
||||
if messages:
|
||||
print()
|
||||
print()
|
||||
utils.print_title("Coverage check failed")
|
||||
for msg in messages:
|
||||
print(msg.text)
|
||||
print()
|
||||
print("You can run 'tox -e py35-cov' (or py34-cov) locally and check "
|
||||
"htmlcov/index.html to debug this.")
|
||||
print()
|
||||
|
||||
if 'CI' in os.environ:
|
||||
print("Keeping coverage.xml on CI.")
|
||||
@@ -257,11 +276,6 @@ def main_check_all():
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point.
|
||||
|
||||
Return:
|
||||
The return code to return.
|
||||
"""
|
||||
utils.change_cwd()
|
||||
if '--check-all' in sys.argv:
|
||||
return main_check_all()
|
||||
|
||||
35
scripts/dev/check_doc_changes.py
Executable file
35
scripts/dev/check_doc_changes.py
Executable file
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env python3
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
# qutebrowser is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# qutebrowser is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Check if docs changed and output an error if so."""
|
||||
|
||||
import sys
|
||||
import subprocess
|
||||
|
||||
code = subprocess.call(['git', '--no-pager', 'diff', '--exit-code', '--stat'])
|
||||
if code != 0:
|
||||
print()
|
||||
print('The autogenerated docs changed, please run this to update them:')
|
||||
print(' tox -e docs')
|
||||
print(' git commit -am "Update docs"')
|
||||
print()
|
||||
print('(Or you have uncommitted changes, in which case you can ignore '
|
||||
'this.)')
|
||||
sys.exit(code)
|
||||
@@ -29,35 +29,51 @@ CI machines.
|
||||
from __future__ import print_function
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import subprocess
|
||||
import urllib
|
||||
import contextlib
|
||||
|
||||
try:
|
||||
import _winreg as winreg
|
||||
except ImportError:
|
||||
winreg = None
|
||||
|
||||
TESTENV = os.environ['TESTENV']
|
||||
TESTENV = os.environ.get('TESTENV', None)
|
||||
TRAVIS_OS = os.environ.get('TRAVIS_OS_NAME', None)
|
||||
INSTALL_PYQT = TESTENV in ('py34', 'py35', 'unittests-nodisp', 'vulture',
|
||||
'pylint')
|
||||
INSTALL_PYQT = TESTENV in ('py34', 'py35', 'py34-cov', 'py35-cov',
|
||||
'unittests-nodisp', 'vulture', 'pylint', 'docs')
|
||||
XVFB = TRAVIS_OS == 'linux' and TESTENV == 'py34'
|
||||
pip_packages = ['tox']
|
||||
if TESTENV in ['py34', 'py35'] and TRAVIS_OS == 'linux':
|
||||
if TESTENV is not None and TESTENV.endswith('-cov'):
|
||||
pip_packages.append('codecov')
|
||||
|
||||
|
||||
def apt_get(args):
|
||||
subprocess.check_call(['sudo', 'apt-get', '-y', '-q'] + args)
|
||||
|
||||
|
||||
def brew(args, silent=False):
|
||||
if silent:
|
||||
with open(os.devnull, 'w') as f:
|
||||
subprocess.check_call(['brew'] + args, stdout=f)
|
||||
@contextlib.contextmanager
|
||||
def travis_fold(text):
|
||||
if 'TRAVIS' in os.environ:
|
||||
marker = re.compile(r'\W+').sub('-', text.lower()).strip('-')
|
||||
print("travis_fold:start:{}".format(marker))
|
||||
yield
|
||||
print("travis_fold:end:{}".format(marker))
|
||||
else:
|
||||
subprocess.check_call(['brew'] + args + ['--verbose'])
|
||||
yield
|
||||
|
||||
|
||||
def folded_cmd(argv):
|
||||
"""Output a command with travis folding markers."""
|
||||
with travis_fold(''.join(argv)):
|
||||
print(" $ " + ' '.join(argv))
|
||||
subprocess.check_call(argv)
|
||||
|
||||
|
||||
def apt_get(args):
|
||||
folded_cmd(['sudo', 'apt-get', '-y', '-q'] + args)
|
||||
|
||||
|
||||
def brew(args):
|
||||
folded_cmd(['brew'] + args)
|
||||
|
||||
|
||||
def check_setup(executable):
|
||||
@@ -65,6 +81,7 @@ def check_setup(executable):
|
||||
print("Checking setup...")
|
||||
subprocess.check_call([executable, '-c', 'import PyQt5'])
|
||||
subprocess.check_call([executable, '-c', 'import sip'])
|
||||
subprocess.check_call([executable, '--version'])
|
||||
|
||||
|
||||
if 'APPVEYOR' in os.environ:
|
||||
@@ -85,21 +102,18 @@ if 'APPVEYOR' in os.environ:
|
||||
print("Installing PyQt5...")
|
||||
subprocess.check_call([r'C:\install-PyQt5.exe', '/S'])
|
||||
|
||||
print("Installing tox...")
|
||||
subprocess.check_call([r'C:\Python34\Scripts\pip', 'install', '-U'] +
|
||||
pip_packages)
|
||||
folded_cmd([r'C:\Python34\Scripts\pip', 'install', '-U'] + pip_packages)
|
||||
|
||||
print("Linking Python...")
|
||||
with open(r'C:\Windows\system32\python3.bat', 'w') as f:
|
||||
f.write(r'@C:\Python34\python %*')
|
||||
|
||||
check_setup(r'C:\Python34\python')
|
||||
elif TRAVIS_OS == 'linux' and 'DOCKER' in os.environ:
|
||||
pass
|
||||
elif TRAVIS_OS == 'linux':
|
||||
print("travis_fold:start:ci_install")
|
||||
print("Installing via pip...")
|
||||
subprocess.check_call(['sudo', 'pip', 'install'] + pip_packages)
|
||||
folded_cmd(['sudo', 'pip', 'install'] + pip_packages)
|
||||
|
||||
print("Installing packages...")
|
||||
pkgs = []
|
||||
|
||||
if XVFB:
|
||||
@@ -108,33 +122,35 @@ elif TRAVIS_OS == 'linux':
|
||||
pkgs += ['python3-pyqt5', 'python3-pyqt5.qtwebkit']
|
||||
if TESTENV == 'eslint':
|
||||
pkgs += ['npm', 'nodejs', 'nodejs-legacy']
|
||||
if TESTENV == 'docs':
|
||||
pkgs += ['asciidoc']
|
||||
|
||||
if pkgs:
|
||||
print("apt-get update...")
|
||||
apt_get(['update'])
|
||||
print("apt-get install...")
|
||||
apt_get(['install'] + pkgs)
|
||||
|
||||
if TESTENV == 'flake8':
|
||||
apt_get(['update'])
|
||||
# We need an up-to-date Python because of:
|
||||
# https://github.com/google/yapf/issues/46
|
||||
apt_get(['install', '-t', 'trusty-updates', 'python3.4'])
|
||||
|
||||
if TESTENV == 'eslint':
|
||||
subprocess.check_call(['sudo', 'npm', 'install', '-g', 'eslint'])
|
||||
folded_cmd(['sudo', 'npm', 'install', '-g', 'eslint'])
|
||||
else:
|
||||
check_setup('python3')
|
||||
print("travis_fold:end:ci_install")
|
||||
elif TRAVIS_OS == 'osx':
|
||||
print("Disabling App Nap...")
|
||||
subprocess.check_call(['defaults', 'write', 'NSGlobalDomain',
|
||||
'NSAppSleepDisabled', '-bool', 'YES'])
|
||||
print("brew update...")
|
||||
brew(['update'], silent=True)
|
||||
brew(['update'])
|
||||
|
||||
print("Installing packages...")
|
||||
pkgs = ['python3']
|
||||
if INSTALL_PYQT:
|
||||
pkgs.append('pyqt5')
|
||||
brew(['install'] + pkgs)
|
||||
brew(['install', '--verbose'] + pkgs)
|
||||
|
||||
print("Installing tox/codecov...")
|
||||
subprocess.check_call(['sudo', 'pip3', 'install'] + pip_packages)
|
||||
folded_cmd(['sudo', 'pip3', 'install'] + pip_packages)
|
||||
|
||||
check_setup('python3')
|
||||
else:
|
||||
15
scripts/dev/ci/travis_run.sh
Normal file
15
scripts/dev/ci/travis_run.sh
Normal file
@@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [[ $DOCKER ]]; then
|
||||
# To build a fresh image:
|
||||
# docker build -t img misc/docker/$DOCKER
|
||||
# docker run --privileged -v $PWD:/outside img
|
||||
|
||||
docker run --privileged -v $PWD:/outside \
|
||||
thecompiler/qutebrowser-manual:$DOCKER
|
||||
else
|
||||
args=()
|
||||
[[ $TESTENV == docs ]] && args=('--no-authors')
|
||||
|
||||
tox -e $TESTENV -- "${args[@]}"
|
||||
fi
|
||||
@@ -86,7 +86,9 @@ def get_build_exe_options(skip_html=False):
|
||||
'include_msvcr': True,
|
||||
'includes': [],
|
||||
'excludes': ['tkinter'],
|
||||
'packages': ['pygments'],
|
||||
'packages': ['pygments', 'pkg_resources._vendor.packaging',
|
||||
'pkg_resources._vendor.pyparsing',
|
||||
'pkg_resources._vendor.six'],
|
||||
}
|
||||
|
||||
|
||||
@@ -100,7 +102,6 @@ def get_exe(base, target_name):
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
if sys.platform.startswith('win'):
|
||||
base = 'Win32GUI'
|
||||
target_name = 'qutebrowser.exe'
|
||||
|
||||
@@ -55,9 +55,8 @@ def get_build_exe_options():
|
||||
opts = freeze.get_build_exe_options(skip_html=True)
|
||||
opts['includes'] += pytest.freeze_includes() # pylint: disable=no-member
|
||||
opts['includes'] += ['unittest.mock', 'PyQt5.QtTest', 'hypothesis', 'bs4',
|
||||
'httpbin', 'jinja2.ext', 'xvfbwrapper',
|
||||
'cherrypy.wsgiserver',
|
||||
'cherrypy.wsgiserver.wsgiserver3']
|
||||
'httpbin', 'jinja2.ext', 'cherrypy.wsgiserver',
|
||||
'cherrypy.wsgiserver.wsgiserver3', 'pstats']
|
||||
|
||||
httpbin_dir = os.path.dirname(httpbin.__file__)
|
||||
opts['include_files'] += [
|
||||
@@ -70,7 +69,6 @@ def get_build_exe_options():
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
base = 'Win32GUI' if sys.platform.startswith('win') else None
|
||||
with temp_git_commit_file():
|
||||
cx.setup(
|
||||
|
||||
@@ -135,7 +135,6 @@ def check_prerequisites():
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
check_prerequisites()
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
|
||||
@@ -141,7 +141,6 @@ def check_vcs_conflict():
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('checker', choices=('git', 'vcs', 'spelling'),
|
||||
help="Which checker to run.")
|
||||
|
||||
@@ -41,7 +41,8 @@ class ConfigChecker(checkers.BaseChecker):
|
||||
__implements__ = interfaces.IAstroidChecker
|
||||
name = 'config'
|
||||
msgs = {
|
||||
'E0000': ('"%s -> %s" is no valid config option.', 'bad-config-call',
|
||||
'E0000': ('"%s -> %s" is no valid config option.', # flake8: disable=S001
|
||||
'bad-config-call',
|
||||
None),
|
||||
}
|
||||
priority = -1
|
||||
25
scripts/dev/pylint_checkers/setup.py
Normal file
25
scripts/dev/pylint_checkers/setup.py
Normal file
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
# qutebrowser is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# qutebrowser is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""This is only here so we can install those plugins in tox.ini easily."""
|
||||
|
||||
from setuptools import setup
|
||||
setup(name='qute_pylint', packages=['qute_pylint'])
|
||||
@@ -26,6 +26,10 @@ import pytest
|
||||
import pytestqt.plugin
|
||||
import pytest_mock
|
||||
import pytest_catchlog
|
||||
import pytest_instafail
|
||||
import pytest_faulthandler
|
||||
import pytest_xvfb
|
||||
|
||||
sys.exit(pytest.main(plugins=[pytestqt.plugin, pytest_mock,
|
||||
pytest_catchlog]))
|
||||
pytest_catchlog, pytest_instafail,
|
||||
pytest_faulthandler, pytest_xvfb]))
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user