Compare commits
1324 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d9e9851f1 | ||
|
|
c1ab3ebc71 | ||
|
|
8376b21f92 | ||
|
|
9a9b0643a5 | ||
|
|
715157a7d0 | ||
|
|
f88036e780 | ||
|
|
aefac1ad19 | ||
|
|
1654bc4448 | ||
|
|
8a3a785c19 | ||
|
|
d615b49e60 | ||
|
|
4a5f243ff0 | ||
|
|
7a33dff986 | ||
|
|
e62a18f8e8 | ||
|
|
83203820ad | ||
|
|
376d7f2986 | ||
|
|
03b5248ce6 | ||
|
|
25cc0534e6 | ||
|
|
6fe89a3800 | ||
|
|
239b25cc8a | ||
|
|
7fc054d12c | ||
|
|
5679649fcf | ||
|
|
2e0f8a22f0 | ||
|
|
b8998e7741 | ||
|
|
2e228703b5 | ||
|
|
8ce7f76b16 | ||
|
|
b603b7e20a | ||
|
|
b9e8aedf8b | ||
|
|
43bab89bdd | ||
|
|
e11fcda240 | ||
|
|
2f36204e08 | ||
|
|
5fe420efb5 | ||
|
|
5d90e0ecd3 | ||
|
|
b06578a816 | ||
|
|
9ca001e71c | ||
|
|
c4c06467da | ||
|
|
c77956c9c5 | ||
|
|
28c8e4acbf | ||
|
|
b126030f62 | ||
|
|
e2c07d3cef | ||
|
|
c4828cf67c | ||
|
|
d15cc07ed3 | ||
|
|
729be7e7cc | ||
|
|
087e9a4266 | ||
|
|
87ccc31cce | ||
|
|
1dba5b0bbd | ||
|
|
d2bce5dca9 | ||
|
|
e6cf76e40c | ||
|
|
5be408b79f | ||
|
|
58073fd768 | ||
|
|
1311e99e7c | ||
|
|
bcee12853d | ||
|
|
d217fe09c6 | ||
|
|
c8f69d29a8 | ||
|
|
2ca8070e7a | ||
|
|
f7d3070c34 | ||
|
|
37a2523bff | ||
|
|
1d7c2b6b82 | ||
|
|
011b571336 | ||
|
|
b135569d5c | ||
|
|
69e735c42e | ||
|
|
34bd000442 | ||
|
|
4daa7e6979 | ||
|
|
3573b57130 | ||
|
|
78cb0eaf85 | ||
|
|
74d7997a67 | ||
|
|
a4bc4ad478 | ||
|
|
9d9372c6a8 | ||
|
|
44b1ca5c2f | ||
|
|
7dedf73ea4 | ||
|
|
b0c2021eca | ||
|
|
2f8b39df1c | ||
|
|
f7c405d2f4 | ||
|
|
d18c33987d | ||
|
|
0ec9efcdc0 | ||
|
|
909cafb516 | ||
|
|
66ed4e9c4e | ||
|
|
09c265ddb0 | ||
|
|
f7e40f73ab | ||
|
|
f5d9e967ef | ||
|
|
376edd739b | ||
|
|
e81432fd6e | ||
|
|
6a0994038e | ||
|
|
bfd8faafef | ||
|
|
b95fd2c814 | ||
|
|
e9608a6aea | ||
|
|
1832d44da1 | ||
|
|
cc80cfdfd6 | ||
|
|
ef9e1bef1b | ||
|
|
02a539f2d7 | ||
|
|
bf9a897ce8 | ||
|
|
9521da3c73 | ||
|
|
9b1d0af20d | ||
|
|
ea0cbea1dd | ||
|
|
2a4cd02704 | ||
|
|
f77bdb7aec | ||
|
|
0d0e704b6b | ||
|
|
fab6e2eafc | ||
|
|
a6b9d28b96 | ||
|
|
b40fb4dd43 | ||
|
|
27ee09a7a1 | ||
|
|
2f92ea92d8 | ||
|
|
f682477960 | ||
|
|
ab9c046d54 | ||
|
|
3a5cd8f3ff | ||
|
|
46b28387db | ||
|
|
f4d2e0746e | ||
|
|
efe40fa7e0 | ||
|
|
154c380fd0 | ||
|
|
d966720900 | ||
|
|
b4c90c5db4 | ||
|
|
6468e60a36 | ||
|
|
e0c6a322c6 | ||
|
|
82c647a87d | ||
|
|
a63f0d5409 | ||
|
|
18af0b4b35 | ||
|
|
38ebd806cc | ||
|
|
1953bb8458 | ||
|
|
6b4b65e585 | ||
|
|
cbcd6261b1 | ||
|
|
52210442c4 | ||
|
|
0fcab6a632 | ||
|
|
fe06db2571 | ||
|
|
6ffcb387eb | ||
|
|
1cbc555933 | ||
|
|
b23ca85b37 | ||
|
|
1fe1200b71 | ||
|
|
0b911d2e20 | ||
|
|
44f594662a | ||
|
|
216ecd16cc | ||
|
|
0c3ee46fe0 | ||
|
|
2d12c26e8c | ||
|
|
8a1a090dea | ||
|
|
16e3bad7af | ||
|
|
fecc4be057 | ||
|
|
3046d2f068 | ||
|
|
8ab85d6246 | ||
|
|
8ea91b5bbc | ||
|
|
03f4d738ab | ||
|
|
9e6bcb97ca | ||
|
|
ee77951e66 | ||
|
|
e499d8932f | ||
|
|
79c1867e6c | ||
|
|
6df00f8266 | ||
|
|
8913c1883e | ||
|
|
6dbd669efe | ||
|
|
a1cb47936a | ||
|
|
39e40a7887 | ||
|
|
fe493f8565 | ||
|
|
d5668dd687 | ||
|
|
a7bf0744e0 | ||
|
|
ff6e96347b | ||
|
|
0cd265296e | ||
|
|
4cd0ad77a6 | ||
|
|
8d7249ebc6 | ||
|
|
4891fe9457 | ||
|
|
84a41cf9cf | ||
|
|
37aebc7580 | ||
|
|
afddf75bae | ||
|
|
3059d82c5d | ||
|
|
ed9a29fb9a | ||
|
|
3fac246744 | ||
|
|
4a48ef2573 | ||
|
|
38c5abfc98 | ||
|
|
66700e9a94 | ||
|
|
3e8136e353 | ||
|
|
4544c78395 | ||
|
|
9eb0240739 | ||
|
|
4fb3114af8 | ||
|
|
182dd26fb7 | ||
|
|
29e5726101 | ||
|
|
d3e9ffec65 | ||
|
|
1f26b3090c | ||
|
|
f78fb0c027 | ||
|
|
3e255eae64 | ||
|
|
cb0e92b4e8 | ||
|
|
2750c6ab5a | ||
|
|
ae2ee68b85 | ||
|
|
fa8e207101 | ||
|
|
35cbee41d6 | ||
|
|
1d25194bfb | ||
|
|
4f1c27d230 | ||
|
|
57918b651f | ||
|
|
cd34fc4b57 | ||
|
|
fd6e0559a6 | ||
|
|
3a4069667a | ||
|
|
8ffe591f98 | ||
|
|
b4f4c97cf9 | ||
|
|
933d683ff4 | ||
|
|
9ef9224c32 | ||
|
|
a1dff7d535 | ||
|
|
d3488172ec | ||
|
|
18d42d1f0a | ||
|
|
9229957b93 | ||
|
|
f3b4d0ce38 | ||
|
|
11f4fbc772 | ||
|
|
38a07cc152 | ||
|
|
e94f2f11d8 | ||
|
|
124099ac4c | ||
|
|
0ef323c26a | ||
|
|
8171a53478 | ||
|
|
abba3215f2 | ||
|
|
8c76db3892 | ||
|
|
26058f4e80 | ||
|
|
a981688509 | ||
|
|
5eed9e55ad | ||
|
|
987b39885a | ||
|
|
f54295f95c | ||
|
|
09161faca5 | ||
|
|
d17744ffed | ||
|
|
183a5910de | ||
|
|
830136540d | ||
|
|
05eb9bd08c | ||
|
|
1a227ae3a7 | ||
|
|
1a062035eb | ||
|
|
af9647221a | ||
|
|
a8d549cb24 | ||
|
|
201f2f67d3 | ||
|
|
f79e2c92a4 | ||
|
|
9d601e8eb9 | ||
|
|
8b40603562 | ||
|
|
e7a52a0833 | ||
|
|
4f250ba5d7 | ||
|
|
4577659342 | ||
|
|
70243d6e2f | ||
|
|
7bc5996d52 | ||
|
|
9ff97d31da | ||
|
|
b783069f48 | ||
|
|
2556e2e27b | ||
|
|
0907d8bf76 | ||
|
|
17215822bc | ||
|
|
6d8854bc07 | ||
|
|
ece2786d40 | ||
|
|
ab04ca4f36 | ||
|
|
193a8d5242 | ||
|
|
c18b68ac99 | ||
|
|
fc6c49f57c | ||
|
|
17f971344d | ||
|
|
2a5d352c7b | ||
|
|
3d5599facb | ||
|
|
7ed4977d64 | ||
|
|
729c10e0a9 | ||
|
|
387c84beff | ||
|
|
355074f248 | ||
|
|
ba636ebbb0 | ||
|
|
543053c8f5 | ||
|
|
7ee4d2f2c9 | ||
|
|
ed70d636d0 | ||
|
|
506917882e | ||
|
|
774ef58432 | ||
|
|
92facb6f50 | ||
|
|
1a1bc4b8a8 | ||
|
|
a79c139aa4 | ||
|
|
1d5cae3146 | ||
|
|
5a975d1b90 | ||
|
|
b11e075047 | ||
|
|
b3395a1a9c | ||
|
|
9b82fae6fb | ||
|
|
dc0e8b4626 | ||
|
|
1ddd65304a | ||
|
|
3d4fd2652b | ||
|
|
086c6c81a1 | ||
|
|
685bbaae6d | ||
|
|
aa367fa004 | ||
|
|
acfdf8b956 | ||
|
|
aca082ce83 | ||
|
|
8fe3a1e9ce | ||
|
|
4efa022528 | ||
|
|
45e7be4940 | ||
|
|
07c6c40548 | ||
|
|
1a61e53daa | ||
|
|
e4a0f1972f | ||
|
|
6d1b0ba260 | ||
|
|
2c5269acd6 | ||
|
|
d3d999e041 | ||
|
|
3b747d91d2 | ||
|
|
d8734a668c | ||
|
|
1e08a6a202 | ||
|
|
5d013a67a7 | ||
|
|
9892c10f49 | ||
|
|
127514f719 | ||
|
|
9cd2f6ba24 | ||
|
|
00d81a74c2 | ||
|
|
7ce78bb560 | ||
|
|
e909b1f36d | ||
|
|
dff4c37f54 | ||
|
|
75fd97f74f | ||
|
|
6656e6aa9b | ||
|
|
8655ebc9c8 | ||
|
|
641c09c011 | ||
|
|
044a63d4a4 | ||
|
|
a463811940 | ||
|
|
ffe6411a5a | ||
|
|
83efd6c33f | ||
|
|
38ada881a3 | ||
|
|
023fa54cda | ||
|
|
6b19a7b1fa | ||
|
|
d94c1736db | ||
|
|
97a9255400 | ||
|
|
a6c104f0ef | ||
|
|
2a0dd341de | ||
|
|
fb1cffd158 | ||
|
|
3bfd049a0a | ||
|
|
402f9be7e9 | ||
|
|
9eca7ae556 | ||
|
|
952893d984 | ||
|
|
f08d871c24 | ||
|
|
f2c8ff8aa5 | ||
|
|
36a9b816a7 | ||
|
|
149ca68853 | ||
|
|
5d0ffcd14d | ||
|
|
d8a49f95a8 | ||
|
|
2405bf1984 | ||
|
|
fb48059ae9 | ||
|
|
515c9611c4 | ||
|
|
d20f7b76b3 | ||
|
|
3bfcfaba4c | ||
|
|
4dee427f0e | ||
|
|
433bdbfedb | ||
|
|
11502b7942 | ||
|
|
abeb7e3390 | ||
|
|
bbb581eaf8 | ||
|
|
b3df642b21 | ||
|
|
7b3de27b44 | ||
|
|
8450093de0 | ||
|
|
814841200a | ||
|
|
77190554cc | ||
|
|
591a5b8c56 | ||
|
|
72e5df0d57 | ||
|
|
3e2aafa7a4 | ||
|
|
313f37bbc9 | ||
|
|
6f4141956b | ||
|
|
5a0b160736 | ||
|
|
61a6b196e9 | ||
|
|
25b43d528c | ||
|
|
5a1663c584 | ||
|
|
8609663f40 | ||
|
|
c1484553c1 | ||
|
|
c55cb5b16b | ||
|
|
927cf84e14 | ||
|
|
e47e131d41 | ||
|
|
a6d09b8cc9 | ||
|
|
da9cb88c81 | ||
|
|
83a4451f93 | ||
|
|
cf45d97046 | ||
|
|
4d7949cc3e | ||
|
|
637325fc76 | ||
|
|
44f8cf4b1a | ||
|
|
705544cb05 | ||
|
|
15c5cf75cd | ||
|
|
9c3c46f677 | ||
|
|
6b6bceeb3a | ||
|
|
8d8b46604d | ||
|
|
8351c6d951 | ||
|
|
88d92db6e4 | ||
|
|
2980bc808e | ||
|
|
14545a3a22 | ||
|
|
dc2b3ff433 | ||
|
|
0f47ef17a5 | ||
|
|
68f80c602b | ||
|
|
55bf555bfb | ||
|
|
52ec9ed28f | ||
|
|
bf156cf554 | ||
|
|
5bd55cb38b | ||
|
|
2d8aaecd65 | ||
|
|
64a1cad604 | ||
|
|
a3a7f8936b | ||
|
|
307bbde6e0 | ||
|
|
0ca96740c9 | ||
|
|
caedb57e56 | ||
|
|
10298e9692 | ||
|
|
da6d12a657 | ||
|
|
8be433f5f6 | ||
|
|
7412e4f723 | ||
|
|
dcaae51b4f | ||
|
|
c8679d6544 | ||
|
|
9cabd4828c | ||
|
|
69f6822c82 | ||
|
|
2fe1bcfc2b | ||
|
|
fbf53168c2 | ||
|
|
c4ebfcd4b3 | ||
|
|
aed915b1ec | ||
|
|
fe3eb30892 | ||
|
|
052d18147e | ||
|
|
0bf651d1fa | ||
|
|
4d2a55190f | ||
|
|
0896d7a8b3 | ||
|
|
e5779d0775 | ||
|
|
753f87aa15 | ||
|
|
a28b0c3386 | ||
|
|
409b32122e | ||
|
|
3179455e69 | ||
|
|
a6010e3ead | ||
|
|
14ae308279 | ||
|
|
7e20d77bdf | ||
|
|
2969599390 | ||
|
|
e6521b047d | ||
|
|
b8809f879d | ||
|
|
ec5049f801 | ||
|
|
e29c642bc2 | ||
|
|
f2c3cc6a3e | ||
|
|
0e9f268817 | ||
|
|
36372418ca | ||
|
|
5c2d3ec96a | ||
|
|
06a82c5566 | ||
|
|
cef0ac46b8 | ||
|
|
f76b741fb6 | ||
|
|
f6c1d355e3 | ||
|
|
bb6d6e51f6 | ||
|
|
72c65a812f | ||
|
|
2be0743378 | ||
|
|
fedf9d9c72 | ||
|
|
4c1ed35390 | ||
|
|
c8d3fc57c2 | ||
|
|
c3f6246274 | ||
|
|
9e98ab181a | ||
|
|
c4c3a83ac0 | ||
|
|
0acbd77ada | ||
|
|
6e52b789ed | ||
|
|
3a85afe1f4 | ||
|
|
43e0ac1844 | ||
|
|
73c28c12f3 | ||
|
|
276b163e0d | ||
|
|
0f3aa0bd8c | ||
|
|
5f122759db | ||
|
|
cdd53a4515 | ||
|
|
01f71e980d | ||
|
|
8aab87e2a2 | ||
|
|
aa1ea9b063 | ||
|
|
5ccdec4162 | ||
|
|
2ab7ad59ee | ||
|
|
87af685f26 | ||
|
|
3f445ba6ca | ||
|
|
58a9677af8 | ||
|
|
e1c2250690 | ||
|
|
61df5fcd7a | ||
|
|
9c6782be1d | ||
|
|
36adaf0cf3 | ||
|
|
5fca27ad02 | ||
|
|
46f3be2df7 | ||
|
|
c715b24bd3 | ||
|
|
55d282dadb | ||
|
|
a34fa93f62 | ||
|
|
8ffe73cc5f | ||
|
|
9a85b66452 | ||
|
|
c6c14e967d | ||
|
|
58aa1a738d | ||
|
|
4314b96512 | ||
|
|
5a25f0b98b | ||
|
|
15e854237e | ||
|
|
d59fa24fd5 | ||
|
|
0a16f29bd1 | ||
|
|
3fa99abca8 | ||
|
|
e43a1e6444 | ||
|
|
0ce9ae070c | ||
|
|
93d27cbb5f | ||
|
|
8ac48699f2 | ||
|
|
7e7fe9b4ce | ||
|
|
c67f7b6b21 | ||
|
|
c4183bc34d | ||
|
|
43266ac08a | ||
|
|
ed4fc4d1ba | ||
|
|
8011cefea6 | ||
|
|
0330adf284 | ||
|
|
21a60f06c0 | ||
|
|
030678602a | ||
|
|
c0941ab19b | ||
|
|
c6a5731560 | ||
|
|
3d9e4817f2 | ||
|
|
5c58641c81 | ||
|
|
b850df090b | ||
|
|
b93b32c92f | ||
|
|
982a6eccfb | ||
|
|
d23096f898 | ||
|
|
7d284fa575 | ||
|
|
e5056e1c43 | ||
|
|
2190316b27 | ||
|
|
f8f03ea99d | ||
|
|
07641830ae | ||
|
|
6c0f523c89 | ||
|
|
62fde783be | ||
|
|
2c7dd5c60c | ||
|
|
25d1064aee | ||
|
|
aaf90d0fe3 | ||
|
|
cd7c3ec3a4 | ||
|
|
33915b65cf | ||
|
|
b4a695d5b8 | ||
|
|
359b886ba0 | ||
|
|
1d4bb8d8da | ||
|
|
b2cb9d6d46 | ||
|
|
59460035c5 | ||
|
|
c13f7e5f78 | ||
|
|
9318173dc8 | ||
|
|
dc4d4e70e4 | ||
|
|
dacf2cace2 | ||
|
|
49f017c0dd | ||
|
|
d5888fea89 | ||
|
|
fc09d63eb1 | ||
|
|
f21cffd9b8 | ||
|
|
43db9d4526 | ||
|
|
e59f533f9d | ||
|
|
9db0e03f05 | ||
|
|
7f3070f793 | ||
|
|
e94e90baec | ||
|
|
26f6bb7d0d | ||
|
|
0e25f5c730 | ||
|
|
0734d9f0de | ||
|
|
f89adc2873 | ||
|
|
d8017a04a8 | ||
|
|
6b98158d64 | ||
|
|
e58735f1d7 | ||
|
|
b4d5f9e7a6 | ||
|
|
edf431f541 | ||
|
|
2bd36e21f1 | ||
|
|
8314f7f93c | ||
|
|
d6585202fd | ||
|
|
68512ce2cd | ||
|
|
52d7a5693f | ||
|
|
fa131e3290 | ||
|
|
9041d6bdfc | ||
|
|
2130f01ec7 | ||
|
|
81c3c2d15f | ||
|
|
44d109ca92 | ||
|
|
976f758da1 | ||
|
|
336b7de6d4 | ||
|
|
27f65be860 | ||
|
|
cc66feac09 | ||
|
|
9167d3ef17 | ||
|
|
d04848ac19 | ||
|
|
451477593f | ||
|
|
22ae0c5bca | ||
|
|
2c716dbf6c | ||
|
|
132d02e8ca | ||
|
|
78d2514087 | ||
|
|
be88ba6f05 | ||
|
|
fdcb69f5d4 | ||
|
|
3083eaa27b | ||
|
|
41eb36148d | ||
|
|
f5725ec11e | ||
|
|
754c31850b | ||
|
|
041315b65f | ||
|
|
8dcf9fd963 | ||
|
|
776ace2d3f | ||
|
|
87d8bfd7a7 | ||
|
|
452c4115d3 | ||
|
|
accd2399ed | ||
|
|
907a4b0e5e | ||
|
|
daa6f8276f | ||
|
|
7830e40cce | ||
|
|
8be69e2f41 | ||
|
|
418c7e4a96 | ||
|
|
3991cf9dc5 | ||
|
|
f323a54d8d | ||
|
|
57e79db136 | ||
|
|
8f2a4fc0c4 | ||
|
|
6c5d1c96d2 | ||
|
|
d9d68db5df | ||
|
|
08fe1d59e6 | ||
|
|
69ade32cb9 | ||
|
|
c016e8a4cf | ||
|
|
093b3cba25 | ||
|
|
0acd1b8dc8 | ||
|
|
b962fff7f1 | ||
|
|
ecf3e166ff | ||
|
|
660b5531e5 | ||
|
|
b5a9467b5c | ||
|
|
2d2779d6f3 | ||
|
|
aaa523ce7c | ||
|
|
3b0125e8cd | ||
|
|
c7f88c93b2 | ||
|
|
16ac877227 | ||
|
|
2f11b41ae6 | ||
|
|
b52a41ac6f | ||
|
|
c750ff3f50 | ||
|
|
4bdf00b148 | ||
|
|
94b0f92b75 | ||
|
|
d0f1523363 | ||
|
|
b03fea26c2 | ||
|
|
cbed62cafc | ||
|
|
694fbe053d | ||
|
|
44bf4ae883 | ||
|
|
ebdfa0be73 | ||
|
|
b19852b6e7 | ||
|
|
fd5a89dccd | ||
|
|
f19ba277d6 | ||
|
|
d805e2d71e | ||
|
|
1fd386e57e | ||
|
|
7f2abd1a46 | ||
|
|
dfba381b57 | ||
|
|
5643b14987 | ||
|
|
cc6602926f | ||
|
|
730a8afc6b | ||
|
|
b3755f4ca1 | ||
|
|
e145d73852 | ||
|
|
0b1704d829 | ||
|
|
8f48247b8f | ||
|
|
71188bb67b | ||
|
|
a558f666bc | ||
|
|
e4c7e70aba | ||
|
|
488676e0e9 | ||
|
|
fb6bf5c34f | ||
|
|
b35a1f3d15 | ||
|
|
41333cd6e1 | ||
|
|
eb28365d82 | ||
|
|
85e748df4f | ||
|
|
b0c3f5381b | ||
|
|
cf5296ebb5 | ||
|
|
75991e1f87 | ||
|
|
8b9c8eb0bf | ||
|
|
c46abd8f89 | ||
|
|
23583b7d48 | ||
|
|
4007027617 | ||
|
|
5e58d814d7 | ||
|
|
88416db6a3 | ||
|
|
073504abb4 | ||
|
|
1b643ff55f | ||
|
|
e81ac925d7 | ||
|
|
0b8c054dc1 | ||
|
|
d942325c51 | ||
|
|
b670c45381 | ||
|
|
a02055414d | ||
|
|
e5843ffcf6 | ||
|
|
474255cbe7 | ||
|
|
43500b2e24 | ||
|
|
95fb908b9b | ||
|
|
fd57a20022 | ||
|
|
5c5562af2d | ||
|
|
4c2393b61a | ||
|
|
8f0266f06a | ||
|
|
ce7d8a3041 | ||
|
|
bbea66c91f | ||
|
|
5f10a12be9 | ||
|
|
ed1ba03c19 | ||
|
|
0972087e34 | ||
|
|
71fee12b5b | ||
|
|
83572948ca | ||
|
|
704c5ff919 | ||
|
|
5177e656b6 | ||
|
|
63ff39ad65 | ||
|
|
60b6652006 | ||
|
|
03383c48eb | ||
|
|
1a8afb95d3 | ||
|
|
c310156dde | ||
|
|
a3776e361b | ||
|
|
91561e2c5b | ||
|
|
5bca951c21 | ||
|
|
1b24cfd618 | ||
|
|
d4c91f7b0c | ||
|
|
523cb458a6 | ||
|
|
ca5a78dfc7 | ||
|
|
5dbaea7a83 | ||
|
|
1e354a797e | ||
|
|
5e8129788a | ||
|
|
96a2178a25 | ||
|
|
4bc2f63608 | ||
|
|
8159c5f567 | ||
|
|
fe829699be | ||
|
|
0037b0db7e | ||
|
|
3a95cd470a | ||
|
|
55cf470436 | ||
|
|
0be2192eab | ||
|
|
0bdcf2910a | ||
|
|
4905cfc7a5 | ||
|
|
dbef785a94 | ||
|
|
de91142880 | ||
|
|
e010d3dabc | ||
|
|
20ff7e702a | ||
|
|
c5b75b0b16 | ||
|
|
382b52a0b8 | ||
|
|
cef350ad66 | ||
|
|
a0a7f9feda | ||
|
|
3f13c2bd3e | ||
|
|
e3fcc0e091 | ||
|
|
47d5262cd9 | ||
|
|
aa1bf00274 | ||
|
|
11e88fbd12 | ||
|
|
d232437105 | ||
|
|
b127c7b069 | ||
|
|
4cef4012e5 | ||
|
|
1533108ca6 | ||
|
|
b02867fe37 | ||
|
|
4e0d00098c | ||
|
|
aa909c0506 | ||
|
|
2643975c3c | ||
|
|
4bddcd4c1a | ||
|
|
c32d80c7bc | ||
|
|
dc29ad430e | ||
|
|
d44f14063a | ||
|
|
ba7f7163e5 | ||
|
|
9961ab383b | ||
|
|
0d8bc405e0 | ||
|
|
ec1476da2e | ||
|
|
e6655cf66d | ||
|
|
2c5898b9f7 | ||
|
|
c7cd51a7d5 | ||
|
|
616c842f2f | ||
|
|
b4e2b00437 | ||
|
|
2dbc2b2c07 | ||
|
|
5358ac60c8 | ||
|
|
1b6ce1b7f4 | ||
|
|
a4d8f3a012 | ||
|
|
7b8490b6c0 | ||
|
|
db06eeded5 | ||
|
|
d845ecd7fc | ||
|
|
1672399252 | ||
|
|
b673c39b62 | ||
|
|
9b264c7514 | ||
|
|
be3f61af62 | ||
|
|
e19efcf8a8 | ||
|
|
b3cd31a808 | ||
|
|
63dae3a885 | ||
|
|
d114e64b05 | ||
|
|
26664ba644 | ||
|
|
a346644c71 | ||
|
|
f806eefba6 | ||
|
|
f31f254d9b | ||
|
|
63dee327c9 | ||
|
|
8d3a8f9eda | ||
|
|
bf4e968c67 | ||
|
|
b3869fe42b | ||
|
|
58b738ca5b | ||
|
|
9eaa0d0968 | ||
|
|
93f5e30a00 | ||
|
|
ddf7f202d8 | ||
|
|
086010d81e | ||
|
|
6dbac1c047 | ||
|
|
e9b5c355d2 | ||
|
|
956baed76b | ||
|
|
f10b9f1172 | ||
|
|
97cc90b49f | ||
|
|
345d048f43 | ||
|
|
f61aaa9053 | ||
|
|
4652843b38 | ||
|
|
5d490a4e22 | ||
|
|
534a85cf8f | ||
|
|
75b894a186 | ||
|
|
220ac021f0 | ||
|
|
24424a0486 | ||
|
|
db267ae195 | ||
|
|
d5d85bd9c7 | ||
|
|
f8d66f3fe1 | ||
|
|
0f1ba4739c | ||
|
|
4c7c38efcb | ||
|
|
b7c3e7b959 | ||
|
|
b21b4377a8 | ||
|
|
10b00da1ae | ||
|
|
b337cfe4c6 | ||
|
|
3dbf3f9e0a | ||
|
|
d02b63a847 | ||
|
|
f2d7391974 | ||
|
|
4dd23c530a | ||
|
|
e459ac52cc | ||
|
|
5cf1dce89e | ||
|
|
94d394001e | ||
|
|
e2c375b874 | ||
|
|
fd82587213 | ||
|
|
a5610fd6da | ||
|
|
85f6b3c6df | ||
|
|
08c8a5f7dd | ||
|
|
b0012fd410 | ||
|
|
894ec7d66a | ||
|
|
42b5ee831e | ||
|
|
3ba63128da | ||
|
|
425a6d33cf | ||
|
|
2f59abaf13 | ||
|
|
3de1299650 | ||
|
|
0d59a1cba8 | ||
|
|
9ca06ecfa2 | ||
|
|
ef78f69822 | ||
|
|
c72da37916 | ||
|
|
1cc6a6669b | ||
|
|
14237c2c62 | ||
|
|
a5c078516b | ||
|
|
c3c52220f6 | ||
|
|
0350d19bd3 | ||
|
|
c64d9520ff | ||
|
|
59cdbd780c | ||
|
|
703b0043db | ||
|
|
6dbdea0ee3 | ||
|
|
b1334bcc22 | ||
|
|
dfe98d1053 | ||
|
|
a024c14dd6 | ||
|
|
e7b84d4089 | ||
|
|
0119cf510f | ||
|
|
a545b919f7 | ||
|
|
b43d8b13d8 | ||
|
|
70699988ed | ||
|
|
8dc9f0562a | ||
|
|
f1ba14b496 | ||
|
|
9bf749643a | ||
|
|
219c2f8ae8 | ||
|
|
f17131f6c2 | ||
|
|
84a269f36a | ||
|
|
8033931bae | ||
|
|
9b066ec50a | ||
|
|
b1e9ff059a | ||
|
|
e8830a631e | ||
|
|
425fcdf8e4 | ||
|
|
d2e103ecc1 | ||
|
|
167faafff2 | ||
|
|
8369c74f74 | ||
|
|
6f690c442e | ||
|
|
efcea65596 | ||
|
|
8ecc3a3bb0 | ||
|
|
ea1921defd | ||
|
|
36a2f4a15a | ||
|
|
fc32858e5c | ||
|
|
1956158096 | ||
|
|
33ad0ab1fc | ||
|
|
fc5349e1dc | ||
|
|
d3b727d0c7 | ||
|
|
6736f6a3f2 | ||
|
|
5828bbafe9 | ||
|
|
84dacc9bc8 | ||
|
|
8a87b5d357 | ||
|
|
ad401e035f | ||
|
|
1f67353a40 | ||
|
|
62d2018695 | ||
|
|
163bc2e12e | ||
|
|
1a9bc64776 | ||
|
|
231f1d90ce | ||
|
|
17bb9fc21c | ||
|
|
90bbe4d1ef | ||
|
|
364e13f4c2 | ||
|
|
a79b07bd94 | ||
|
|
c4eabcd663 | ||
|
|
599f582c20 | ||
|
|
3e8a394217 | ||
|
|
480c4e878e | ||
|
|
fdd302e4f7 | ||
|
|
e16d89a548 | ||
|
|
9b7b97d626 | ||
|
|
ab27612139 | ||
|
|
863e194073 | ||
|
|
5a8b7910e0 | ||
|
|
67473c6db1 | ||
|
|
68d8900c6c | ||
|
|
ddd343c89c | ||
|
|
67e895b6c7 | ||
|
|
b57027f800 | ||
|
|
fc15e85811 | ||
|
|
3be9a9b051 | ||
|
|
645a1512dd | ||
|
|
4532176e7b | ||
|
|
80a59720de | ||
|
|
9df5a89037 | ||
|
|
839d2b1cbe | ||
|
|
0120061456 | ||
|
|
108e722c85 | ||
|
|
3b4fe97dbc | ||
|
|
b6349437f7 | ||
|
|
b5cd082e43 | ||
|
|
154af84714 | ||
|
|
10e8b78695 | ||
|
|
150ca90517 | ||
|
|
5a2d909607 | ||
|
|
171a0f201b | ||
|
|
7f27c183be | ||
|
|
0e50760b70 | ||
|
|
c08078841f | ||
|
|
1fcce12870 | ||
|
|
00747be9d3 | ||
|
|
261c44bea9 | ||
|
|
c2eabb13b0 | ||
|
|
31eed6c9a6 | ||
|
|
34d4c08374 | ||
|
|
ebc013ac2a | ||
|
|
d93732a6b3 | ||
|
|
c4fc5c0c43 | ||
|
|
57a72a7120 | ||
|
|
1e982a9a84 | ||
|
|
e60f698615 | ||
|
|
df53ccf426 | ||
|
|
4204579c06 | ||
|
|
1e5e6a63a5 | ||
|
|
4a4856c176 | ||
|
|
90b3927906 | ||
|
|
2ff6dbd482 | ||
|
|
f85ca19cef | ||
|
|
da2ff6f3cb | ||
|
|
cf4b89efe3 | ||
|
|
525d3ee4c9 | ||
|
|
6b94dc5279 | ||
|
|
2fa6c952c2 | ||
|
|
83f7cf84a9 | ||
|
|
37750b9e30 | ||
|
|
a82b0d007d | ||
|
|
e98a05e53d | ||
|
|
d887623377 | ||
|
|
e86a79740a | ||
|
|
aa4cb2927d | ||
|
|
2117b2afc6 | ||
|
|
5310c60d58 | ||
|
|
a0e5a3e8ee | ||
|
|
5a73ad0c19 | ||
|
|
def41e70bf | ||
|
|
fd75f77108 | ||
|
|
5bacbc9d38 | ||
|
|
de0686c50a | ||
|
|
b0880df695 | ||
|
|
94178c558a | ||
|
|
463e85ff5d | ||
|
|
2459f14f6f | ||
|
|
015de0e6db | ||
|
|
ace7877010 | ||
|
|
d3e85ad982 | ||
|
|
5fb23f1373 | ||
|
|
8001099661 | ||
|
|
708d0d9c27 | ||
|
|
d3f7d9319a | ||
|
|
6ec8bbaca5 | ||
|
|
e38169433e | ||
|
|
dfada850e0 | ||
|
|
a7b10a090f | ||
|
|
fc4c7bd2e4 | ||
|
|
402aa66756 | ||
|
|
b55e22b5c3 | ||
|
|
fa65f345ac | ||
|
|
57ddd8e95e | ||
|
|
728f06e797 | ||
|
|
7102459c81 | ||
|
|
622938e3d3 | ||
|
|
c776958388 | ||
|
|
05fe68ccab | ||
|
|
c907572557 | ||
|
|
4a909aa028 | ||
|
|
f41acc8fb5 | ||
|
|
9ec6e6da80 | ||
|
|
d60d4d756c | ||
|
|
0132bea42b | ||
|
|
8b14145a4d | ||
|
|
78e159cb27 | ||
|
|
472071c047 | ||
|
|
85eea17b18 | ||
|
|
e780efb3d9 | ||
|
|
4d141f489f | ||
|
|
f0c58b58dd | ||
|
|
36803cba06 | ||
|
|
f1874ff44f | ||
|
|
c8bbef0ab0 | ||
|
|
5085844550 | ||
|
|
9582162927 | ||
|
|
d8e58b5886 | ||
|
|
592ace18d4 | ||
|
|
e767f7c0b8 | ||
|
|
1bf036d1ba | ||
|
|
131f345007 | ||
|
|
dc59ed4d73 | ||
|
|
e22ef776f9 | ||
|
|
b5a70dbdec | ||
|
|
6c2fe3417e | ||
|
|
f1c0781a4c | ||
|
|
7daf1cb239 | ||
|
|
1698c60124 | ||
|
|
0ddf1316f7 | ||
|
|
a14685be3d | ||
|
|
f52f3db1f2 | ||
|
|
e7619477cd | ||
|
|
018d7a87be | ||
|
|
4204a8de9a | ||
|
|
122f0a7edc | ||
|
|
9cece08b2b | ||
|
|
172d0c3ca2 | ||
|
|
4c8b1be19c | ||
|
|
3d0721afea | ||
|
|
27cbe618f0 | ||
|
|
c0b6aef774 | ||
|
|
d0eda3336c | ||
|
|
1cd64481de | ||
|
|
87e9888167 | ||
|
|
c5c145320c | ||
|
|
4ff9d585ea | ||
|
|
1e5c67f152 | ||
|
|
54c1cd7c05 | ||
|
|
1814b672d7 | ||
|
|
6b550defae | ||
|
|
cdde1d7dfc | ||
|
|
11b258568d | ||
|
|
5b3ffa2419 | ||
|
|
b0bd8170e0 | ||
|
|
81345eb17e | ||
|
|
b1f8a70c02 | ||
|
|
0be0884a5b | ||
|
|
3879b8301f | ||
|
|
3c8e616eb9 | ||
|
|
b501677c0e | ||
|
|
5b891ecaca | ||
|
|
4dc54f881c | ||
|
|
5c599879f8 | ||
|
|
b59dc8e89b | ||
|
|
fed2cdad4e | ||
|
|
7b5d2ace24 | ||
|
|
989e3b7291 | ||
|
|
b1dd649278 | ||
|
|
e48e063c0f | ||
|
|
a56a14fb70 | ||
|
|
e92c493b07 | ||
|
|
6ca541d359 | ||
|
|
70956aaeca | ||
|
|
9c99c22f1b | ||
|
|
6d592c7c75 | ||
|
|
0d19d1bcf7 | ||
|
|
1b89d880f5 | ||
|
|
8c80f99a32 | ||
|
|
c1dadeff6f | ||
|
|
27fdf4903a | ||
|
|
c7dcaff025 | ||
|
|
f7b517f3aa | ||
|
|
48735315f8 | ||
|
|
d20872d576 | ||
|
|
c76221c14e | ||
|
|
cbc4ec6531 | ||
|
|
63c9e6a444 | ||
|
|
f5d299d8c7 | ||
|
|
b5eea81e2e | ||
|
|
4851a3d442 | ||
|
|
e12dce9d55 | ||
|
|
cfae36c5c8 | ||
|
|
4e61a6123e | ||
|
|
f326fa28a6 | ||
|
|
534dbfc4c2 | ||
|
|
091353a773 | ||
|
|
2a269e9cd9 | ||
|
|
1b48dc8749 | ||
|
|
ddf86600d1 | ||
|
|
6f3fa9dca6 | ||
|
|
a969fe021d | ||
|
|
6452c8f883 | ||
|
|
b8dd71a343 | ||
|
|
460308f388 | ||
|
|
6a26bc23ab | ||
|
|
48de8b145b | ||
|
|
0788054dd3 | ||
|
|
b2d763f993 | ||
|
|
35f0b26f4a | ||
|
|
ba9c782824 | ||
|
|
fa69786b0f | ||
|
|
e10da78a1a | ||
|
|
92abf4bdf8 | ||
|
|
27e82ce6c8 | ||
|
|
f1129460d8 | ||
|
|
c54c637ccc | ||
|
|
e300b2e30d | ||
|
|
11c03d79cd | ||
|
|
6b98c48985 | ||
|
|
b858b6ac75 | ||
|
|
a8d2dbfdfb | ||
|
|
0553094494 | ||
|
|
61519e6383 | ||
|
|
45dea54e3c | ||
|
|
a345b02729 | ||
|
|
6d879bbca3 | ||
|
|
0f13d9325b | ||
|
|
ad763685e5 | ||
|
|
120d2e12b0 | ||
|
|
ece32e930c | ||
|
|
8d15bbdded | ||
|
|
aaf35536a7 | ||
|
|
0ee7e40e69 | ||
|
|
ad7920dda1 | ||
|
|
93b92f4aab | ||
|
|
61f32b3e9b | ||
|
|
14ba20670b | ||
|
|
29b25206f6 | ||
|
|
58f031630c | ||
|
|
2c0c2e220e | ||
|
|
28caf45707 | ||
|
|
ee0eabc202 | ||
|
|
43898ebb71 | ||
|
|
0252f5fdbf | ||
|
|
aaab05793e | ||
|
|
ddb6743b26 | ||
|
|
269676318b | ||
|
|
6f904759b5 | ||
|
|
f8db4b8147 | ||
|
|
14df72a7a1 | ||
|
|
e590bf26ad | ||
|
|
40cc354030 | ||
|
|
c0b41d8c62 | ||
|
|
1f048a38f8 | ||
|
|
e187cda292 | ||
|
|
daaf7a62c8 | ||
|
|
341708f543 | ||
|
|
ad181ec7eb | ||
|
|
069d7b26a2 | ||
|
|
cc88451003 | ||
|
|
7ca9a007f8 | ||
|
|
b78d5f57aa | ||
|
|
98d1fca220 | ||
|
|
beb970d7d5 | ||
|
|
37b431f72f | ||
|
|
9a1cf2b03a | ||
|
|
e0dee14df4 | ||
|
|
f2e2748c59 | ||
|
|
03e59051dc | ||
|
|
91ad91cc7b | ||
|
|
b650ec75f3 | ||
|
|
c00dccfbb2 | ||
|
|
8941b5dc96 | ||
|
|
8e417970c3 | ||
|
|
1a957b6c10 | ||
|
|
8eb483d66b | ||
|
|
dd292b0781 | ||
|
|
54eae77328 | ||
|
|
81ba49e79b | ||
|
|
a9f5d45c34 | ||
|
|
e24d2e1b8c | ||
|
|
10985c3505 | ||
|
|
5ef40829aa | ||
|
|
b60f673468 | ||
|
|
8ab2772dd9 | ||
|
|
f17238d3d4 | ||
|
|
b5dc4ea040 | ||
|
|
7fc99f3d80 | ||
|
|
f54c416ddd | ||
|
|
f6ad556f34 | ||
|
|
b94fcf2c3c | ||
|
|
315725a3ac | ||
|
|
002346a125 | ||
|
|
b619d835e6 | ||
|
|
3f98bf372e | ||
|
|
9be5992a9a | ||
|
|
62426380e5 | ||
|
|
a1f7eed5a7 | ||
|
|
d7999577dd | ||
|
|
54131e9d3e | ||
|
|
aab5411317 | ||
|
|
183049ef2e | ||
|
|
42c27ddbc0 | ||
|
|
c762340a0c | ||
|
|
9b372de4a9 | ||
|
|
4dbc4ba93f | ||
|
|
dd83a40df4 | ||
|
|
677cfc9410 | ||
|
|
c91344cdf5 | ||
|
|
137badc77f | ||
|
|
ad338e7a17 | ||
|
|
0cabedfeef | ||
|
|
cd53318c7f | ||
|
|
f855d5f349 | ||
|
|
e3bfe73442 | ||
|
|
7e2c67a7e4 | ||
|
|
12940eb542 | ||
|
|
1a1a8ba26f | ||
|
|
1a67794293 | ||
|
|
aaf09dc573 | ||
|
|
f49dba6e38 | ||
|
|
c236046a73 | ||
|
|
17fc6622bb | ||
|
|
d992caf8fc | ||
|
|
947dcd556b | ||
|
|
bc54eb8671 | ||
|
|
222627b08d | ||
|
|
a728704cce | ||
|
|
f8f8699ab8 | ||
|
|
5d13d0073c | ||
|
|
f6ef657952 | ||
|
|
25005ded8a | ||
|
|
a93bf184aa | ||
|
|
f59a147589 | ||
|
|
866b299fef | ||
|
|
a74a9c8a21 | ||
|
|
88fc186402 | ||
|
|
ce1b82616d | ||
|
|
dd0e230a32 | ||
|
|
e35d284282 | ||
|
|
9fde38d96a | ||
|
|
e62ba57291 | ||
|
|
2775f2b2ee | ||
|
|
7edfdaa271 | ||
|
|
54ae6a63ee | ||
|
|
2b440bc8db | ||
|
|
27a34d5499 | ||
|
|
aa2e5a35d6 | ||
|
|
ae512f451e | ||
|
|
c88393ccfd | ||
|
|
d9655f5eb9 | ||
|
|
3cb756699f | ||
|
|
785f948bc7 | ||
|
|
38ac2c6598 | ||
|
|
a960658617 | ||
|
|
28ec7b4698 | ||
|
|
d1e88c5e8d | ||
|
|
3f21ac6b6a | ||
|
|
7a67af24f0 | ||
|
|
f36a7444d7 | ||
|
|
229733f1b0 | ||
|
|
0d66647918 | ||
|
|
14c1332017 | ||
|
|
1a2a57d59e | ||
|
|
418934644b | ||
|
|
8b435ec88f | ||
|
|
756aa3e16f | ||
|
|
1f94e0fee6 | ||
|
|
37050c49fc | ||
|
|
a36c0fcd4c | ||
|
|
d3c6ebcf15 | ||
|
|
012e124eaf | ||
|
|
9fadc78e4d | ||
|
|
6f620a6a9e | ||
|
|
21dcf73e38 | ||
|
|
18eace37f8 | ||
|
|
244d2753df | ||
|
|
452e03f9af | ||
|
|
db0a54b03f | ||
|
|
392fb3e1d7 | ||
|
|
021c94eece | ||
|
|
8398fe3bdd | ||
|
|
99a4765e75 | ||
|
|
69f729dbe5 | ||
|
|
41ecc0ad3d | ||
|
|
f9876823b8 | ||
|
|
7975bd8796 | ||
|
|
8837abc208 | ||
|
|
ad822b72c7 | ||
|
|
ec43aab999 | ||
|
|
3b5b49daac | ||
|
|
57cad14714 | ||
|
|
778ad5df3a | ||
|
|
d936be450b | ||
|
|
178d0dfa58 | ||
|
|
564a589bc6 | ||
|
|
9ceb43ec44 | ||
|
|
98596d439f | ||
|
|
f99a070735 | ||
|
|
21dfcf1e1b | ||
|
|
2f0b976bca | ||
|
|
9a5839650c | ||
|
|
deb3c31f2f | ||
|
|
2d91ff3f5d | ||
|
|
4fb026708b | ||
|
|
5b2013c037 | ||
|
|
b98bafaefe | ||
|
|
8806aac362 | ||
|
|
903d437943 | ||
|
|
024549e3b0 | ||
|
|
842c69dfdd | ||
|
|
2777e4113e | ||
|
|
8aec5244de | ||
|
|
15c8a937f4 | ||
|
|
489c913e58 | ||
|
|
d594798db8 | ||
|
|
aeaa20c3b7 | ||
|
|
46dbfa2fce | ||
|
|
530fe5e933 | ||
|
|
f499fd85d0 | ||
|
|
d3a7b2e4ca | ||
|
|
d496ea2d59 | ||
|
|
32562c6878 | ||
|
|
9e8c781871 | ||
|
|
640f758605 | ||
|
|
1903792239 | ||
|
|
329030e913 | ||
|
|
205f37fe09 | ||
|
|
8edfa4281e | ||
|
|
f5227ef982 | ||
|
|
844473e47a | ||
|
|
71608af486 | ||
|
|
8e0ef128c9 | ||
|
|
07552dddfe | ||
|
|
e1f2259e98 | ||
|
|
4925091ede | ||
|
|
09c77cfa83 | ||
|
|
c21ae0b651 | ||
|
|
049955dfd5 | ||
|
|
5359463d79 | ||
|
|
6ca39dd851 | ||
|
|
6c8e073dc8 | ||
|
|
3164ee06eb | ||
|
|
9f443d026a | ||
|
|
e783310eb4 | ||
|
|
a7dfdd48e0 | ||
|
|
9ee74253e4 | ||
|
|
b805f903c9 | ||
|
|
f7cf33b596 | ||
|
|
2a0a2d926e | ||
|
|
7439586334 | ||
|
|
0195cb31bb | ||
|
|
8f1b074595 | ||
|
|
94d49b4801 | ||
|
|
1b13b0c385 | ||
|
|
c098d0de37 | ||
|
|
69061c5629 | ||
|
|
f93eef848c | ||
|
|
f55242ad93 | ||
|
|
2d19708a41 | ||
|
|
6c97a4a6e0 | ||
|
|
9442fd4b75 | ||
|
|
78bbbb968f | ||
|
|
66640df541 | ||
|
|
f5e6091ff6 | ||
|
|
987bab9960 | ||
|
|
ba678e29fb | ||
|
|
10214a8b83 | ||
|
|
0233c96d48 | ||
|
|
6ae94d6f49 | ||
|
|
e8ddd9397d | ||
|
|
e603d9a2d0 | ||
|
|
a6443231e5 | ||
|
|
941eac848e | ||
|
|
3433a1ec7a | ||
|
|
fa2340b61e | ||
|
|
f4c46ec1c5 | ||
|
|
3bc55e0405 | ||
|
|
0b2e39e4a4 | ||
|
|
29c51c288b | ||
|
|
6f1e830aba | ||
|
|
253f3b2cd7 | ||
|
|
55e3645131 | ||
|
|
91b72ef292 | ||
|
|
695712e50c | ||
|
|
96ddfd5b65 | ||
|
|
74f4642a2c | ||
|
|
a2772db9da | ||
|
|
44a6617184 | ||
|
|
1770570921 | ||
|
|
343a091aee | ||
|
|
853280feeb | ||
|
|
6037fd74cd | ||
|
|
b18c1254a4 | ||
|
|
c3e615dfa3 | ||
|
|
d91400c3be | ||
|
|
d375ddebea | ||
|
|
894a2a4e7b | ||
|
|
63ce7d6e02 |
17
.appveyor.yml
Normal file
17
.appveyor.yml
Normal file
@@ -0,0 +1,17 @@
|
||||
shallow_clone: true
|
||||
version: '{branch}-{build}'
|
||||
cache:
|
||||
- C:\projects\qutebrowser\.cache
|
||||
build: off
|
||||
environment:
|
||||
PYTHON: 'C:\Python34'
|
||||
PYTHONUNBUFFERED: 1
|
||||
|
||||
install:
|
||||
- C:\Python27\python -u scripts\dev\ci_install.py
|
||||
|
||||
test_script:
|
||||
- C:\Python34\Scripts\tox -e py34
|
||||
- C:\Python34\Scripts\tox -e py34-integration
|
||||
- C:\Python34\Scripts\tox -e unittests-frozen
|
||||
- C:\Python34\Scripts\tox -e pylint
|
||||
@@ -3,6 +3,7 @@ branch = true
|
||||
omit =
|
||||
qutebrowser/__main__.py
|
||||
*/__init__.py
|
||||
qutebrowser/resources.py
|
||||
|
||||
[report]
|
||||
exclude_lines =
|
||||
@@ -11,3 +12,6 @@ exclude_lines =
|
||||
raise AssertionError
|
||||
raise NotImplementedError
|
||||
if __name__ == ["']__main__["']:
|
||||
|
||||
[xml]
|
||||
output=coverage.xml
|
||||
|
||||
49
.eslintrc
Normal file
49
.eslintrc
Normal file
@@ -0,0 +1,49 @@
|
||||
# vim: ft=yaml
|
||||
|
||||
env:
|
||||
browser: true
|
||||
|
||||
rules:
|
||||
block-scoped-var: 2
|
||||
dot-location: 2
|
||||
default-case: 2
|
||||
guard-for-in: 2
|
||||
no-div-regex: 2
|
||||
no-param-reassign: 2
|
||||
no-eq-null: 2
|
||||
no-floating-decimal: 2
|
||||
no-self-compare: 2
|
||||
no-throw-literal: 2
|
||||
no-void: 2
|
||||
radix: 2
|
||||
wrap-iife: [2, "inside"]
|
||||
brace-style: [2, "1tbs", {"allowSingleLine": true}]
|
||||
comma-style: [2, "last"]
|
||||
consistent-this: [2, "self"]
|
||||
func-style: [2, "declaration"]
|
||||
indent: [2, 4, {"SwitchCase": 1}]
|
||||
linebreak-style: [2, "unix"]
|
||||
max-nested-callbacks: [2, 3]
|
||||
no-lonely-if: 2
|
||||
no-multiple-empty-lines: [2, {"max": 2}]
|
||||
no-nested-ternary: 2
|
||||
no-unneeded-ternary: 2
|
||||
operator-assignment: [2, "always"]
|
||||
operator-linebreak: [2, "after"]
|
||||
space-after-keywords: [2, "always"]
|
||||
space-before-blocks: [2, "always"]
|
||||
space-before-function-paren: [2, {"anonymous": "never", "named": "never"}]
|
||||
object-curly-spacing: [2, "never"]
|
||||
array-bracket-spacing: [2, "never"]
|
||||
computed-property-spacing: [2, "never"]
|
||||
space-in-parens: [2, "never"]
|
||||
space-unary-ops: [2, {"words": true, "nonwords": false}]
|
||||
spaced-comment: [2, "always"]
|
||||
max-depth: [2, 5]
|
||||
max-len: [2, 79, 4]
|
||||
max-params: [2, 5]
|
||||
max-statements: [2, 30]
|
||||
no-bitwise: 2
|
||||
quote-props: [2, "always"]
|
||||
global-strict: 0
|
||||
quotes: 0
|
||||
13
.flake8
13
.flake8
@@ -1,13 +0,0 @@
|
||||
# vim: ft=dosini fileencoding=utf-8:
|
||||
|
||||
[flake8]
|
||||
# E265: Block comment should start with '#'
|
||||
# E501: Line too long
|
||||
# F841: unused variable
|
||||
# F401: Unused import
|
||||
# E402: module level import not at top of file
|
||||
# E266: too many leading '#' for block comment
|
||||
# W503: line break before binary operator
|
||||
ignore=E265,E501,F841,F401,E402,E266,W503
|
||||
max_complexity = 12
|
||||
exclude=resources.py
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -1,5 +1,6 @@
|
||||
__pycache__
|
||||
*.pyc
|
||||
*.swp
|
||||
/build
|
||||
/dist
|
||||
/qutebrowser.egg-info
|
||||
@@ -20,4 +21,10 @@ __pycache__
|
||||
/.venv
|
||||
/.coverage
|
||||
/htmlcov
|
||||
/.coverage.xml
|
||||
/.tox
|
||||
/testresults.html
|
||||
/.cache
|
||||
/.testmondata
|
||||
/.hypothesis
|
||||
TODO
|
||||
|
||||
10
.pylintrc
10
.pylintrc
@@ -4,7 +4,6 @@
|
||||
ignore=resources.py
|
||||
extension-pkg-whitelist=PyQt5,sip
|
||||
load-plugins=pylint_checkers.config,
|
||||
pylint_checkers.crlf,
|
||||
pylint_checkers.modeline,
|
||||
pylint_checkers.openencoding,
|
||||
pylint_checkers.settrace
|
||||
@@ -28,19 +27,20 @@ disable=no-self-use,
|
||||
broad-except,
|
||||
bare-except,
|
||||
eval-used,
|
||||
exec-used
|
||||
exec-used,
|
||||
file-ignored
|
||||
|
||||
[BASIC]
|
||||
module-rgx=(__)?[a-z][a-z0-9_]*(__)?$
|
||||
function-rgx=([a-z_][a-z0-9_]{2,30}|setUpModule|tearDownModule)$
|
||||
function-rgx=([a-z_][a-z0-9_]{2,50}|setUpModule|tearDownModule)$
|
||||
const-rgx=[A-Za-z_][A-Za-z0-9_]{0,30}$
|
||||
method-rgx=[a-z_][A-Za-z0-9_]{2,40}$
|
||||
method-rgx=[a-z_][A-Za-z0-9_]{2,50}$
|
||||
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}$
|
||||
class-attribute-rgx=[A-Za-z_][A-Za-z0-9_]{1,30}$
|
||||
inlinevar-rgx=[a-z_][a-z0-9_]*$
|
||||
docstring-min-length=2
|
||||
docstring-min-length=3
|
||||
|
||||
[FORMAT]
|
||||
max-line-length=79
|
||||
|
||||
33
.travis.yml
Normal file
33
.travis.yml
Normal file
@@ -0,0 +1,33 @@
|
||||
# So we get Ubuntu Trusty - using "dist: trusty" breaks OS X.
|
||||
services: docker
|
||||
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
|
||||
# Not really, but this is here so we can do stuff by hand.
|
||||
language: c
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.cache/pip
|
||||
- $HOME/build/The-Compiler/qutebrowser/.cache
|
||||
|
||||
env:
|
||||
- PATH=/home/travis/bin:/home/travis/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
|
||||
|
||||
install:
|
||||
- python scripts/dev/ci_install.py
|
||||
|
||||
script:
|
||||
- xvfb-run -s "-screen 0 640x480x16" tox -e py34,py34-integration
|
||||
- '[[ $TRAVIS_OS_NAME == linux ]] && tox -e unittests-nodisp || true'
|
||||
- '[[ $TRAVIS_OS_NAME == linux ]] && tox -e misc || true'
|
||||
- '[[ $TRAVIS_OS_NAME == linux ]] && tox -e pep257 || true'
|
||||
- '[[ $TRAVIS_OS_NAME == linux ]] && tox -e pyflakes || true'
|
||||
- '[[ $TRAVIS_OS_NAME == linux ]] && tox -e pep8 || true'
|
||||
- '[[ $TRAVIS_OS_NAME == linux ]] && tox -e mccabe || true'
|
||||
- '[[ $TRAVIS_OS_NAME == linux ]] && tox -e pyroma || true'
|
||||
- '[[ $TRAVIS_OS_NAME == linux ]] && tox -e check-manifest || true'
|
||||
- '[[ $TRAVIS_OS_NAME == linux ]] && tox -e pylint || true'
|
||||
@@ -14,6 +14,218 @@ 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.5.0 (unreleased)
|
||||
-------------------
|
||||
|
||||
Added
|
||||
~~~~~
|
||||
|
||||
- New setting `ui -> hide-wayland-decoration` to hide the window decoration
|
||||
when using wayland.
|
||||
- New userscripts in `misc/userscripts`:
|
||||
- `open_download` to easily open a file in your downloads folder.
|
||||
- `view_in_mpv` to open a video in mpv and remove it from the page.
|
||||
- New setting `content -> host-blocking-whitelist` to whitelist certain domains
|
||||
from the adblocker.
|
||||
|
||||
Changed
|
||||
~~~~~~~
|
||||
|
||||
- The `colors -> tabs.bg/fg.selected` option got split into
|
||||
`tabs.bg/fg.selected.odd/even`.
|
||||
|
||||
v0.4.1
|
||||
------
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
|
||||
- Adjusted AppArmor config for the IPC changes in v0.4.0.
|
||||
- Fixed atime update frequency for IPC file.
|
||||
- Worked around a Qt issue where middle-clicking caused scrolling with a
|
||||
touchpad to restart at the beginning of the page.
|
||||
- The `completion -> web-history-max-items` setting is now also respected for
|
||||
items added after starting qutebrowser.
|
||||
- Search terms are now shared between different tabs again
|
||||
- Tests (a reduced subset of them) now run correctly again when DISPLAY is not
|
||||
set.
|
||||
- Fixed an issue causing qutebrowser to crash with Python 3.5 as soon as an ad
|
||||
was blocked.
|
||||
- Fixed an issue causing qutebrowser to not start with more recent Python 3.4
|
||||
versions (e.g. on Debian experimental).
|
||||
- Fixed various `PendingDeprecationWarnings` shown with Python 3.5.
|
||||
|
||||
v0.4.0
|
||||
------
|
||||
|
||||
Added
|
||||
~~~~~
|
||||
|
||||
- New bookmark functionality (similar to quickmarks without a name).
|
||||
* New command `:bookmark-add` to bookmark the current page (bound to `M`).
|
||||
* New command `:bookmark-load` to load a bookmark (bound to `gb`/`gB`/`wB`).
|
||||
- New (hidden) command `:completion-item-del` (bound to `<Ctrl-D>`) to delete
|
||||
the current item in the completion (for quickmarks/bookmarks).
|
||||
- New settings `tabs -> padding` and `tabs -> indicator-tabbing` to control the
|
||||
size/padding of the tabbar.
|
||||
- New setting `ui -> statusbar-padding` to control the size/padding of the
|
||||
status bar.
|
||||
- New setting `network -> referer-header` to configure when the referer should
|
||||
be sent (by default it's only sent while on the same domain).
|
||||
- New setting `tabs -> show` which supersedes the old `tabs -> hide-*` options
|
||||
and has an additional `switching` option which shows tab while switching
|
||||
them. There's also a new `show-switching` option to configure the timeout.
|
||||
- New setting `storage -> remember-download-directory` to remember the last
|
||||
used download directory.
|
||||
- New setting `storage -> prompt-download-directory` to download all downloads
|
||||
without asking.
|
||||
- Rapid hinting is now also possible for downloads.
|
||||
- Directory browsing via `file://` is now supported.
|
||||
|
||||
Changed
|
||||
~~~~~~~
|
||||
|
||||
- Some developer scripts got moved to `scripts/dev/`
|
||||
- When downloading to a FIFO or special file, a confirmation is displayed as
|
||||
this might cause qutebrowser to hang.
|
||||
- The `:yank-selected` command now works in all modes instead of just caret
|
||||
mode and is not hidden anymore.
|
||||
- `minimal_webkit_testbrowser.py` now has a `--webengine` switch to test
|
||||
QtWebEngine if it's installed.
|
||||
- The column width percentages for the completion view now depend on the
|
||||
completion model.
|
||||
- The values for `tabs -> position` and `ui -> downloads-position` got changed
|
||||
from `north`/`south`/`west/`east` to `top`/`bottom`/`left`/`right`. Existing
|
||||
configs should be adjusted automatically.
|
||||
- `:tab-focus`/`gt` now behaves like `:tab-next` if no count/index is given.
|
||||
- The completion widget doesn't show a border anymore.
|
||||
- The tabbar doesn't display ugly arrows anymore if there isn't enough space
|
||||
for all tabs.
|
||||
- Some insignificant Qt warnings which were printed on OS X are now hidden.
|
||||
- Better support for Qt 5.5 and Python 3.5.
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
|
||||
- Fixed a bug where cookies were saved despite qutebrowser being started in
|
||||
private browsing mode.
|
||||
- The local socket used for inter-process communication (opening new instances)
|
||||
is now ensured to only be accessible by the user on all operating systems.
|
||||
- Various corner cases for inter-process communication issues got fixed.
|
||||
- `link_pyqt.py` now should work better on untested distributions.
|
||||
- Fixed various corner-cases with crashes when reading invalid config values
|
||||
and the history file.
|
||||
- Fixed various corner-cases when setting text via an external editor.
|
||||
- Fixed potential crash when hinting a text field.
|
||||
- Fixed entering of insert mode when certain disabled text fields were clicked.
|
||||
- Fixed a crash when using `:set` with `-p` and `!` (invert value)
|
||||
- Downloads with unknown size are now handled correctly.
|
||||
- `:navigate increment/decrement` (`<Ctrl-A>`/`<Ctrl-X>`) now handles some
|
||||
corner-cases better.
|
||||
- Fixed a bug where the completion got affected by another window's completion
|
||||
if it was open in both windows.
|
||||
- Fixed a performance issue with large histories when opening previously
|
||||
unvisited websites.
|
||||
- The progress bar now doesn't cause the statusbar to change it's height
|
||||
anymore.
|
||||
- `~` is now always expanded when spawning a script.
|
||||
- Fixed various corner cases when opening links in an existing instance.
|
||||
- Fixed a race-condition causing an exception when starting qutebrowser.
|
||||
|
||||
Removed
|
||||
~~~~~~~
|
||||
|
||||
- The `tabs -> indicator-space` setting got removed as the new padding settings
|
||||
should be used instead.
|
||||
- The `tabs -> hide-always` and `tabs -> hide-auto` settings got merged into
|
||||
the new `tabs -> show` setting.
|
||||
|
||||
v0.3.0
|
||||
------
|
||||
|
||||
Added
|
||||
~~~~~
|
||||
|
||||
- New commands `:message-info`, `:message-error` and `:message-warning` to show messages in the statusbar, e.g. from a userscript.
|
||||
- New command `:scroll-px` which replaces `:scroll` for pixel-exact scrolling.
|
||||
- New command `:jseval` to run a javascript snippet on the current page.
|
||||
- New (hidden) command `:follow-selected` (bound to `Enter`/`Ctrl-Enter` by default) to follow the link which is currently selected (e.g. after searching via `/`).
|
||||
- New (hidden) command `:clear-keychain` to clear a partially entered keychain (bound to `<Escape>` by default, in addition to clearing search).
|
||||
- New setting `ui -> smooth-scrolling`.
|
||||
- New setting `content -> webgl` to enable/disable https://www.khronos.org/webgl/[WebGL].
|
||||
- New setting `content -> css-regions` to enable/disable support for http://dev.w3.org/csswg/css-regions/[CSS Regions].
|
||||
- New setting `content -> hyperlink-auditing` to enable/disable support for https://html.spec.whatwg.org/multipage/semantics.html#hyperlink-auditing[hyperlink auditing].
|
||||
- New setting `tabs -> mousewheel-tab-switching` to control mousewheel behavior on the tab bar.
|
||||
- New arguments `--datadir` and `--cachedir` to set the data/cache location.
|
||||
- New arguments `--basedir` and `--temp-basedir` (intended for debugging) to set a different base directory for all data, which allows multiple invocations.
|
||||
- New argument `--no-err-windows` to suppress all error windows.
|
||||
- New arguments `--top-navigate` and `--bottom-navigate` (`-t`/`-b`) for `:scroll-page` to specify a navigation action (e.g. automatically go to the next page when arriving at the bottom).
|
||||
- New flag `-d`/`--detach` for `:spawn` to detach the spawned process so it's not closed when qutebrowser is.
|
||||
- New flag `-v`/`--verbose` for `:spawn` to print informations when the process started/exited successfully.
|
||||
- Many new color settings (foreground setting for every background setting).
|
||||
- New setting `ui -> modal-js-dialog` to use the standard modal dialogs for javascript questions instead of using the statusbar.
|
||||
- New setting `colors -> webpage.bg` to set the background color to use for websites which don't set one.
|
||||
- New setting `completion -> auto-open` to only open the completion when tab is pressed (if set to false).
|
||||
- New visual/caret mode (bound to `v`) to select text by keyboard.
|
||||
- There are now some example userscripts in `misc/userscripts`.
|
||||
- Support for Qt 5.5 and tox 2.0
|
||||
|
||||
Changed
|
||||
~~~~~~~
|
||||
|
||||
- *Breaking change for userscripts:* `QUTE_HTML` and `QUTE_TEXT` for userscripts now don't store the contents directly, and instead contain a filename.
|
||||
- The `content -> geolocation` and `notifications` settings now support a `true` value to always allow those. However, this is *not recommended*.
|
||||
- New bindings `<Ctrl-R>` (rapid), `<Ctrl-F>` (foreground) and `<Ctrl-B>` (background) to switch hint modes while hinting.
|
||||
- `<Ctrl-M>` and numpad-enter are now bound by default for bindings where `<Return>` was bound.
|
||||
- `:hint tab` and `F` now respect the `background-tabs` setting. To enforce a foreground tab (what `F` did before), use `:hint tab-fg` or `;f`.
|
||||
- `:scroll` now takes a direction argument (`up`/`down`/`left`/`right`/`top`/`bottom`/`page-up`/`page-down`) instead of two pixel arguments (`dx`/`dy`). The old form still works but is deprecated.
|
||||
- The `ui -> user-stylesheet` setting now also takes file paths relative to the config directory.
|
||||
- The `content -> cookies-accept` setting now has new `no-3rdparty` (default) and `no-unknown-3rdparty` values to block third-party cookies. The `default` value got renamed to `all`.
|
||||
- Improved startup time by reading the webpage history while qutebrowser is open.
|
||||
- The way `:spawn` splits its commandline has been changed slightly to allow commands with flags.
|
||||
- The default for the `new-instance-open-target` setting has been changed to `tab`.
|
||||
- Sessions now store zoom/scroll-position separately for each entry.
|
||||
|
||||
Deprecated
|
||||
~~~~~~~~~~
|
||||
|
||||
- `:scroll` with two pixel-arguments is now deprecated - `:scroll-px` should be used instead.
|
||||
|
||||
Removed
|
||||
~~~~~~~
|
||||
|
||||
- The `--no-crash-dialog` argument which was intended for debugging only was removed as it's replaced by `--no-err-windows` which suppresses all error windows.
|
||||
- Support for Qt installations without SSL support was dropped.
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
|
||||
- Scrolling should now work more reliably on some pages where arrow keys worked but `hjkl` didn't.
|
||||
- Small improvements when checking if an input is an URL or not.
|
||||
- Fixed wrong cursor position when completing the first item in the completion.
|
||||
- Fixed exception when using search engines with {foo} in their name.
|
||||
- Fixed a bug where the same title was shown for all tabs on some systems.
|
||||
- Don't install the scripts package when installing qutebrowser.
|
||||
- Fixed searching for terms starting with a hyphen (e.g. `/-foo`)
|
||||
- Proxy authentication credentials are now remembered between different tabs.
|
||||
- Fixed updating of the tab title on pages without title.
|
||||
- Fixed AssertionError when closing many windows quickly.
|
||||
- Various fixes for deprecated key bindings and auto-migrations.
|
||||
- Workaround for qutebrowser not starting when there are NUL-bytes in the history (because of a currently unknown bug).
|
||||
- Fixed handling of keybindings containing Ctrl/Meta on OS X.
|
||||
- Fixed crash when downloading an URL without filename (e.g. magnet links) via "Save as...".
|
||||
- Fixed exception when starting qutebrowser with `:set` as argument.
|
||||
- Fixed horrible completion performance when the `shrink` option was set.
|
||||
- Sessions now store zoom/scroll-position correctly.
|
||||
|
||||
https://github.com/The-Compiler/qutebrowser/releases/tag/v0.2.1[v0.2.1]
|
||||
-----------------------------------------------------------------------
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
|
||||
- Added missing manpage (doc/qutebrowser.1.asciidoc) to archive.
|
||||
|
||||
https://github.com/The-Compiler/qutebrowser/releases/tag/v0.2.0[v0.2.0]
|
||||
-----------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -86,14 +86,15 @@ Useful utilities
|
||||
Checkers
|
||||
~~~~~~~~
|
||||
|
||||
qutbebrowser uses http://tox.readthedocs.org/en/latest/[tox] to run its
|
||||
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`:
|
||||
|
||||
* Unit tests using the Python
|
||||
https://docs.python.org/3.4/library/unittest.html[unittest] framework
|
||||
* https://pypi.python.org/pypi/flake8/[flake8]
|
||||
* 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]
|
||||
@@ -152,7 +153,7 @@ Useful websites
|
||||
|
||||
Some resources which might be handy:
|
||||
|
||||
* http://qt-project.org/doc/qt-5/classes.html[The Qt5 reference]
|
||||
* http://doc.qt.io/qt-5/classes.html[The Qt5 reference]
|
||||
* https://docs.python.org/3/library/index.html[The Python reference]
|
||||
* http://httpbin.org/[httpbin, a test service for HTTP requests/responses]
|
||||
* http://requestb.in/[RequestBin, a service to inspect HTTP requests]
|
||||
@@ -210,8 +211,7 @@ Other
|
||||
Languages] (http://www.rfc-editor.org/errata_search.php?rfc=5646[Errata])
|
||||
* http://www.w3.org/TR/CSS2/[Cascading Style Sheets Level 2 Revision 1 (CSS
|
||||
2.1) Specification]
|
||||
* http://qt-project.org/doc/qt-4.8/stylesheet-reference.html[Qt Style Sheets
|
||||
Reference]
|
||||
* http://doc.qt.io/qt-5/stylesheet-reference.html[Qt Style Sheets Reference]
|
||||
* http://mimesniff.spec.whatwg.org/[MIME Sniffing Standard]
|
||||
* http://spec.whatwg.org/[WHATWG specifications]
|
||||
* http://www.w3.org/html/wg/drafts/html/master/Overview.html[HTML 5.1 Nightly]
|
||||
@@ -237,9 +237,7 @@ There are some exceptions to that:
|
||||
|
||||
* `QThread` is used instead of Python threads because it provides signals and
|
||||
slots.
|
||||
* `QProcess` is used instead of Python's `subprocess` if certain actions (e.g.
|
||||
cleanup) when the process finished are desired, as it provides signals for
|
||||
that.
|
||||
* `QProcess` is used instead of Python's `subprocess`
|
||||
* `QUrl` is used instead of storing URLs as string, see the
|
||||
<<handling-urls,handling URLs>> section for details.
|
||||
|
||||
@@ -294,8 +292,8 @@ All objects can be printed by starting with the `--debug` flag and using the
|
||||
|
||||
The registry is mainly used for <<commands,command handlers>> but also can be
|
||||
useful in places where using Qt's
|
||||
http://qt-project.org/doc/qt-5/signalsandslots.html[signals and slots]
|
||||
mechanism would be difficult.
|
||||
http://doc.qt.io/qt-5/signalsandslots.html[signals and slots] mechanism would
|
||||
be difficult.
|
||||
|
||||
Logging
|
||||
~~~~~~~
|
||||
@@ -397,13 +395,12 @@ then automatically checked. Possible values:
|
||||
e.g. `('foo', 'bar')` or `(int, 'foo')`.
|
||||
* `flag`: The flag to be used, as 1-char string (default: First char of the
|
||||
long name).
|
||||
* `name`: The long name to be used, as string (default: Name of the parameter).
|
||||
* `special`: The string `count` or `win_id` if the parameter should be
|
||||
auto-filled (with the count given by the user and the window ID the command was
|
||||
executed in, respectively).
|
||||
* `nargs`: Gets passed to argparse, see
|
||||
https://docs.python.org/dev/library/argparse.html#nargs[its documentation].
|
||||
|
||||
The name of an argument will always be the parameter name, with any trailing
|
||||
underscores stripped.
|
||||
|
||||
[[handling-urls]]
|
||||
Handling URLs
|
||||
~~~~~~~~~~~~~
|
||||
@@ -541,7 +538,7 @@ New Qt release
|
||||
|
||||
* Run all tests and check nothing is broken.
|
||||
* Check the
|
||||
https://bugreports.qt-project.org/issues/?jql=reporter%20%3D%20%22The%20Compiler%22%20ORDER%20BY%20fixVersion%20ASC[Qt bugtracker]
|
||||
https://bugreports.qt.io/issues/?jql=reporter%20%3D%20%22The%20Compiler%22%20ORDER%20BY%20fixVersion%20ASC[Qt bugtracker]
|
||||
and make sure all bugs marked as resolved are actually fixed.
|
||||
* Update own PKGBUILDs based on upstream Archlinux updates and rebuild.
|
||||
* Update recommended Qt version in `README`
|
||||
|
||||
35
FAQ.asciidoc
35
FAQ.asciidoc
@@ -4,8 +4,8 @@ The Compiler <mail@qutebrowser.org>
|
||||
|
||||
[qanda]
|
||||
What is qutebrowser based on?::
|
||||
qutebrowser uses http://www.python.org/[Python], http://qt-project.org/[Qt]
|
||||
and http://www.riverbankcomputing.com/software/pyqt/intro[PyQt].
|
||||
qutebrowser uses http://www.python.org/[Python], http://qt.io/[Qt] and
|
||||
http://www.riverbankcomputing.com/software/pyqt/intro[PyQt].
|
||||
+
|
||||
The concept of it is largely inspired by http://portix.bitbucket.org/dwb/[dwb]
|
||||
and http://www.vimperator.org/vimperator[Vimperator]. Many actions and
|
||||
@@ -15,7 +15,7 @@ Why another browser?::
|
||||
It might be hard to believe, but I didn't find any browser which I was
|
||||
happy with, so I started to write my own. Also, I needed a project to get
|
||||
into writing GUI applications with Python and
|
||||
link:http://qt-project.org/[Qt]/link:http://www.riverbankcomputing.com/software/pyqt/intro[PyQt].
|
||||
link:http://qt.io/[Qt]/link:http://www.riverbankcomputing.com/software/pyqt/intro[PyQt].
|
||||
+
|
||||
Read the next few questions to find out why I was unhappy with existing
|
||||
software.
|
||||
@@ -32,12 +32,11 @@ API] seems to lack basic features like proxy support, and almost no projects
|
||||
seem to have started porting to WebKit2 (I only know of
|
||||
http://www.uzbl.org/[uzbl]).
|
||||
+
|
||||
qutebrowser uses http://qt-project.org/[Qt] and
|
||||
http://qt-project.org/wiki/QtWebKit[QtWebKit] instead, which suffers from far
|
||||
less such crashes. It might switch to
|
||||
http://qt-project.org/wiki/QtWebEngine[QtWebEngine] in the future, which is
|
||||
based on Google's https://en.wikipedia.org/wiki/Blink_(layout_engine)[Blink]
|
||||
rendering engine.
|
||||
qutebrowser uses http://qt.io/[Qt] and http://wiki.qt.io/QtWebKit[QtWebKit]
|
||||
instead, which suffers from far less such crashes. It might switch to
|
||||
http://wiki.qt.io/QtWebEngine[QtWebEngine] in the future, which is based on
|
||||
Google's https://en.wikipedia.org/wiki/Blink_(layout_engine)[Blink] rendering
|
||||
engine.
|
||||
|
||||
What's wrong with https://www.mozilla.org/en-US/firefox/new/[Firefox] and link:http://5digits.org/pentadactyl/[Pentadactyl]/link:http://www.vimperator.org/vimperator[Vimperator]?::
|
||||
Firefox likes to break compatibility with addons on each upgrade, gets
|
||||
@@ -54,10 +53,10 @@ What's wrong with http://www.chromium.org/Home[Chromium] and https://vimium.gith
|
||||
|
||||
Why Python?::
|
||||
I enjoy writing Python since 2011, which made it one of the possible
|
||||
choices. I wanted to use http://qt-project.org/[Qt] because of
|
||||
http://qt-project.org/wiki/QtWebKit[QtWebKit] so I didn't have
|
||||
http://qt-project.org/wiki/Category:LanguageBindings[many other choices]. I
|
||||
don't like C++ and can't write it very well, so that wasn't an alternative.
|
||||
choices. I wanted to use http://qt.io/[Qt] because of
|
||||
http://wiki.qt.io/QtWebKit[QtWebKit] so I didn't have
|
||||
http://wiki.qt.io/Category:LanguageBindings[many other choices]. I don't
|
||||
like C++ and can't write it very well, so that wasn't an alternative.
|
||||
|
||||
But isn't Python too slow for a browser?::
|
||||
http://www.infoworld.com/d/application-development/van-rossum-python-not-too-slow-188715[No.]
|
||||
@@ -83,6 +82,10 @@ How do I play Youtube videos with mpv?::
|
||||
:bind x spawn mpv {url}
|
||||
:bind ;x hint links spawn mpv {hint-url}
|
||||
----
|
||||
+
|
||||
Note that you might need an additional package (e.g.
|
||||
https://www.archlinux.org/packages/community/any/youtube-dl/[youtube-dl] on
|
||||
Archlinux) to play web videos with mpv.
|
||||
|
||||
== Troubleshooting
|
||||
|
||||
@@ -112,10 +115,10 @@ Experiencing segfaults (crashes) on Debian systems.::
|
||||
|
||||
Segfaults on Facebook, Medium, Amazon, ...::
|
||||
If you are on a Debian or Ubuntu based system, you might experience some crashes
|
||||
visting these sites. This is caused by a known bug in Qt which has been
|
||||
visting these sites. This is caused by various bugs in Qt which have been
|
||||
fixed in Qt 5.4. However Debian and Ubuntu are slow to adopt or upgrade
|
||||
some packages. There is currently no easy way to manually upgrade to Qt
|
||||
5.4 on those systems.
|
||||
some packages. On Debian Jessie, it's recommended to use the experimental
|
||||
repos as described in https://github.com/The-Compiler/qutebrowser/blob/master/INSTALL.asciidoc#on-debian--ubuntu[INSTALL].
|
||||
+
|
||||
Since Ubuntu Trusty (using Qt 5.2.1),
|
||||
https://bugreports.qt.io/browse/QTBUG-42417?jql=component%20%3D%20WebKit%20and%20resolution%20%3D%20Done%20and%20fixVersion%20in%20(5.3.0%2C%20%225.3.0%20Alpha%22%2C%20%225.3.0%20Beta1%22%2C%20%225.3.0%20RC1%22%2C%205.3.1%2C%205.3.2%2C%205.4.0%2C%20%225.4.0%20Alpha%22%2C%20%225.4.0%20Beta%22%2C%20%225.4.0%20RC%22)%20and%20priority%20in%20(%22P2%3A%20Important%22%2C%20%22P1%3A%20Critical%22%2C%20%22P0%3A%20Blocker%22)[over
|
||||
|
||||
152
INSTALL.asciidoc
152
INSTALL.asciidoc
@@ -10,10 +10,47 @@ qutebrowser should run on these systems:
|
||||
* Ubuntu Trusty (14.04 LTS) or newer
|
||||
* Any other distribution based on these (e.g. Linux Mint)
|
||||
|
||||
Unfortunately there is no Debian package yet, but installing qutebrowser is
|
||||
still relatively easy! If you want to help packaging it for Debian, please
|
||||
https://github.com/The-Compiler/qutebrowser/issues/582[get in touch]!
|
||||
|
||||
Install the dependencies via apt-get:
|
||||
|
||||
[NOTE]
|
||||
==========================
|
||||
On Debian, it's recommended to install the Qt packages from the
|
||||
https://wiki.debian.org/DebianExperimental[experimental] repository as those
|
||||
are a much newer version of Qt which is more stable.
|
||||
|
||||
Add the following line to your `/etc/apt/sources.list`:
|
||||
|
||||
----
|
||||
# apt-get install python3-pyqt5 python3-pyqt5.qtwebkit python-tox
|
||||
deb http://ftp.debian.org/debian experimental main
|
||||
----
|
||||
|
||||
Then install the packages like this:
|
||||
|
||||
----
|
||||
# apt-get update
|
||||
# apt-get install -t experimental python3-pyqt5 python3-pyqt5.qtwebkit python3-sip
|
||||
# apt-get install python-tox
|
||||
----
|
||||
|
||||
It's also recommended to pin those packages to receive updates by creating a
|
||||
file `/etc/apt/preferences.d/qutebrowser` with the following contents:
|
||||
|
||||
----
|
||||
Package: python3-pyqt5* libqt5*
|
||||
Pin: release a=experimental
|
||||
Pin-Priority: 800
|
||||
----
|
||||
==========================
|
||||
|
||||
For distributions other than Debian or if you prefer to not use the
|
||||
experimental repo:
|
||||
|
||||
----
|
||||
# apt-get install python3-pyqt5 python3-pyqt5.qtwebkit python-tox python3-sip
|
||||
----
|
||||
|
||||
To generate the documentation for the `:help` command, when using the git
|
||||
@@ -24,25 +61,33 @@ repository (rather than a release):
|
||||
$ python3 scripts/asciidoc2html.py
|
||||
----
|
||||
|
||||
Then run tox like this to set up a
|
||||
https://docs.python.org/3/library/venv.html[virtual environment]:
|
||||
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:
|
||||
|
||||
----
|
||||
$ tox -e mkvenv
|
||||
# dnf update
|
||||
# dnf install python3-qt5 python-tox python3-sip
|
||||
----
|
||||
|
||||
This installs all needed Python dependencies in a `.venv` subfolder. The
|
||||
system-wide Qt5/PyQt5 installations are symlinked into the virtual environment.
|
||||
|
||||
You can then create a simple wrapper script to start qutebrowser somewhere in
|
||||
your `$PATH` (e.g. `/usr/local/bin/qutebrowser` or `~/bin/qutebrowser`):
|
||||
To generate the documentation for the `:help` command, when using the git
|
||||
repository (rather than a release):
|
||||
|
||||
----
|
||||
#!/bin/bash
|
||||
~/path/to/qutebrowser/.venv/bin/python3 -m qutebrowser "$@"
|
||||
# dnf install asciidoc
|
||||
$ python3 scripts/asciidoc2html.py
|
||||
----
|
||||
|
||||
Please also read about <<updating,updating qutebrowser with tox>>.
|
||||
Then <<tox,install qutebrowser via tox>>.
|
||||
|
||||
On Archlinux
|
||||
------------
|
||||
@@ -51,13 +96,22 @@ 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 like this:
|
||||
You can install them (and the needed pypeg2 dependency) like this:
|
||||
|
||||
----
|
||||
$ mkdir qutebrowser
|
||||
$ cd qutebrowser
|
||||
$ wget https://aur.archlinux.org/packages/qu/qutebrowser-git/PKGBUILD
|
||||
$ 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
|
||||
|
||||
$ wget https://aur.archlinux.org/packages/qu/qutebrowser/qutebrowser.tar.gz
|
||||
$ tar xzf qutebrowser.tar.gz
|
||||
$ cd qutebrowser
|
||||
$ makepkg -si
|
||||
$ cd ..
|
||||
$ rm -r qutebrowser qutebrowser.tar.gz
|
||||
----
|
||||
|
||||
or you could use an AUR helper, e.g. `yaourt -S qutebrowser-git`.
|
||||
@@ -65,23 +119,16 @@ or you could use an AUR helper, e.g. `yaourt -S qutebrowser-git`.
|
||||
On Gentoo
|
||||
---------
|
||||
|
||||
A dedicated overlay is available on
|
||||
https://github.com/posativ/qutebrowser-overlay[GitHub]. To install it, add the
|
||||
overlay with http://wiki.gentoo.org/wiki/Layman[layman]:
|
||||
|
||||
----
|
||||
# layman -a qutebrowser
|
||||
----
|
||||
|
||||
Note, that Qt5 is available in the portage tree, but masked. You may need to do
|
||||
a lot of keywording to install qutebrowser. Also make sure you have `python3_4`
|
||||
in your `PYTHON_TARGETS` (`/etc/portage/make.conf`) and rebuild your system
|
||||
(`emerge -uDNav @world`). Afterwards, you can install qutebrowser:
|
||||
qutebrowser is available in the main repository and can be installed with:
|
||||
|
||||
----
|
||||
# emerge -av qutebrowser
|
||||
----
|
||||
|
||||
Make sure you have `python3_4` in your `PYTHON_TARGETS`
|
||||
(`/etc/portage/make.conf`) and rebuild your system (`emerge -uDNav @world`) if
|
||||
necessary.
|
||||
|
||||
On Void Linux
|
||||
-------------
|
||||
|
||||
@@ -92,6 +139,16 @@ with:
|
||||
# xbps-install qutebrowser
|
||||
----
|
||||
|
||||
On NixOS
|
||||
--------
|
||||
|
||||
Nixpkgs collection contains `pkgs.qutebrowser` since June 2015. You can install
|
||||
it with:
|
||||
|
||||
----
|
||||
$ nix-env -i qutebrowser
|
||||
----
|
||||
|
||||
On Windows
|
||||
----------
|
||||
|
||||
@@ -111,17 +168,7 @@ https://pip.pypa.io/en/latest/[pip]:
|
||||
$ pip install tox
|
||||
----
|
||||
|
||||
Then run tox like this to set up a
|
||||
https://docs.python.org/3/library/venv.html[virtual environment]:
|
||||
|
||||
----
|
||||
$ tox -e mkvenv
|
||||
----
|
||||
|
||||
This installs all needed Python dependencies in a `.venv` subfolder. The
|
||||
system-wide Qt5/PyQt5 installations are used in the virtual environment.
|
||||
|
||||
Please also read about <<updating,updating qutebrowser with tox>>.
|
||||
Then <<tox,install qutebrowser via tox>>.
|
||||
|
||||
On OS X
|
||||
-------
|
||||
@@ -157,9 +204,30 @@ standard location for your distro (`/usr/share/applications` and
|
||||
The normal `setup.py install` doesn't install these files, so you'll have to do
|
||||
it as part of the packaging process.
|
||||
|
||||
[[updating]]
|
||||
Updating qutebrowser with tox
|
||||
-----------------------------
|
||||
[[tox]]
|
||||
Installing qutebrowser with tox
|
||||
-------------------------------
|
||||
|
||||
Run tox like this to set up a
|
||||
https://docs.python.org/3/library/venv.html[virtual environment]:
|
||||
|
||||
----
|
||||
$ tox -e mkvenv
|
||||
----
|
||||
|
||||
This installs all needed Python dependencies in a `.venv` subfolder. The
|
||||
system-wide Qt5/PyQt5 installations are symlinked into the virtual environment.
|
||||
|
||||
You can then create a simple wrapper script to start qutebrowser somewhere in
|
||||
your `$PATH` (e.g. `/usr/local/bin/qutebrowser` or `~/bin/qutebrowser`):
|
||||
|
||||
----
|
||||
#!/bin/bash
|
||||
~/path/to/qutebrowser/.venv/bin/python3 -m qutebrowser "$@"
|
||||
----
|
||||
|
||||
Updating
|
||||
~~~~~~~~
|
||||
|
||||
When you updated your local copy of the code (e.g. by pulling the git repo, or
|
||||
extracting a new version), the virtualenv should automatically use the updated
|
||||
|
||||
21
MANIFEST.in
21
MANIFEST.in
@@ -1,11 +1,14 @@
|
||||
global-exclude __pycache__ *.pyc *.pyo
|
||||
|
||||
recursive-include qutebrowser *.py
|
||||
recursive-include qutebrowser/html *.html
|
||||
recursive-include qutebrowser/img *.svg
|
||||
recursive-include qutebrowser/test *.py
|
||||
recursive-include qutebrowser/javascript *.js
|
||||
graft icons
|
||||
graft scripts/pylint_checkers
|
||||
graft doc/img
|
||||
graft misc
|
||||
graft scripts
|
||||
include qutebrowser/utils/testfile
|
||||
include qutebrowser/git-commit-id
|
||||
include COPYING doc/* README.asciidoc CONTRIBUTING.asciidoc FAQ.asciidoc INSTALL.asciidoc CHANGELOG.asciidoc
|
||||
@@ -14,18 +17,18 @@ include requirements.txt
|
||||
include tox.ini
|
||||
include qutebrowser.py
|
||||
|
||||
exclude scripts/cleanup.py
|
||||
exclude scripts/minimal_webkit_testbrowser.py
|
||||
exclude scripts/run_profile.py
|
||||
exclude scripts/src2asciidoc.sh
|
||||
exclude scripts/gen_resources.sh
|
||||
exclude scripts/quit_segfault_test.sh
|
||||
exclude scripts/segfault_test.sh
|
||||
prune scripts/dev
|
||||
exclude scripts/asciidoc2html.py
|
||||
exclude doc/notes
|
||||
recursive-exclude doc *.asciidoc
|
||||
include doc/qutebrowser.1.asciidoc
|
||||
prune tests
|
||||
exclude pytest.ini
|
||||
exclude qutebrowser.rcc
|
||||
exclude .coveragerc
|
||||
exclude .flake8
|
||||
exclude .pylintrc
|
||||
exclude .eslintrc
|
||||
exclude doc/help
|
||||
exclude .appveyor.yml
|
||||
exclude .travis.yml
|
||||
exclude misc/appveyor_install.py
|
||||
|
||||
@@ -12,9 +12,10 @@ image:https://img.shields.io/pypi/l/qutebrowser.svg?style=flat["license badge",l
|
||||
image:https://img.shields.io/pypi/v/qutebrowser.svg?style=flat["version badge",link="https://pypi.python.org/pypi/qutebrowser/"]
|
||||
image:https://img.shields.io/github/issues/The-Compiler/qutebrowser.svg?style=flat["issues badge",link="https://github.com/The-Compiler/qutebrowser/issues"]
|
||||
image:https://requires.io/github/The-Compiler/qutebrowser/requirements.svg?branch=master["requirements badge",link="https://requires.io/github/The-Compiler/qutebrowser/requirements/?branch=master"]
|
||||
image:http://qutebrowser.org:8010/png?builder=archlinux["build badge",link="http://qutebrowser.org:8010/waterfall"]
|
||||
image:https://travis-ci.org/The-Compiler/qutebrowser.svg?branch=master["Build Status", link="https://travis-ci.org/The-Compiler/qutebrowser"]
|
||||
image:https://ci.appveyor.com/api/projects/status/9gmnuip6i1oq7046?svg=true["AppVeyor build status", link="https://ci.appveyor.com/project/The-Compiler/qutebrowser"]
|
||||
|
||||
qutebrowser is a keyboard-focused browser with with a minimal GUI. It's based
|
||||
qutebrowser is a keyboard-focused browser with a minimal GUI. It's based
|
||||
on Python, PyQt5 and QtWebKit and free software, licensed under the GPL.
|
||||
|
||||
It was inspired by other browsers/addons like dwb and Vimperator/Pentadactyl.
|
||||
@@ -23,7 +24,7 @@ Screenshots
|
||||
-----------
|
||||
|
||||
image:doc/img/main.png["screenshot 1",width=300,link="doc/img/main.png"]
|
||||
image:doc/img/downloads.png["screenshot 2",width=300,link="doc/img/downloads.png"]
|
||||
image:doc/img/downloads.png["screenshot 2",width=300j,link="doc/img/downloads.png"]
|
||||
image:doc/img/completion.png["screenshot 3",width=300,link="doc/img/completion.png"]
|
||||
image:doc/img/hints.png["screenshot 4",width=300,link="doc/img/hints.png"]
|
||||
|
||||
@@ -68,7 +69,7 @@ Contributions / Bugs
|
||||
--------------------
|
||||
|
||||
You want to contribute to qutebrowser? Awesome! Please read
|
||||
link:doc/CONTRIBUTING.asciidoc[the contribution guidelines] for details and
|
||||
link:CONTRIBUTING.asciidoc[the contribution guidelines] for details and
|
||||
useful hints.
|
||||
|
||||
If you found a bug or have a feature request, you can report it in several
|
||||
@@ -89,10 +90,10 @@ Requirements
|
||||
The following software and libraries are required to run qutebrowser:
|
||||
|
||||
* http://www.python.org/[Python] 3.4
|
||||
* http://qt-project.org/[Qt] 5.2.0 or newer (5.4.1 recommended)
|
||||
* http://qt.io/[Qt] 5.2.0 or newer (5.5.0 recommended)
|
||||
* QtWebKit
|
||||
* http://www.riverbankcomputing.com/software/pyqt/intro[PyQt] 5.2.0 or newer
|
||||
(5.4.1 recommended) for Python 3
|
||||
(5.5.0 recommended) for Python 3
|
||||
* https://pypi.python.org/pypi/setuptools/[pkg_resources/setuptools]
|
||||
* http://fdik.org/pyPEG/[pyPEG2]
|
||||
* http://jinja.pocoo.org/[jinja2]
|
||||
@@ -134,34 +135,50 @@ Contributors, sorted by the number of commits in descending order:
|
||||
|
||||
// QUTE_AUTHORS_START
|
||||
* Florian Bruhin
|
||||
* Antoni Boucher
|
||||
* Bruno Oliveira
|
||||
* Joel Torstensson
|
||||
* Martin Tournoij
|
||||
* Alexander Cogneau
|
||||
* Raphael Pierzina
|
||||
* Joel Torstensson
|
||||
* Claude
|
||||
* Lamar Pavel
|
||||
* Austin Anderson
|
||||
* Artur Shaik
|
||||
* ZDarian
|
||||
* Peter Vilim
|
||||
* John ShaggyTwoDope Jenkins
|
||||
* Daniel
|
||||
* Jimmy
|
||||
* Zach-Button
|
||||
* rikn00
|
||||
* Thorsten Wißmann
|
||||
* Patric Schmitz
|
||||
* Martin Zimmermann
|
||||
* Error 800
|
||||
* Brian Jackson
|
||||
* sbinix
|
||||
* jnphilipp
|
||||
* Tobias Patzl
|
||||
* Johannes Altmanninger
|
||||
* Samir Benmendil
|
||||
* Regina Hug
|
||||
* Mathias Fussenegger
|
||||
* Larry Hynes
|
||||
* Fritz V155 Reichwald
|
||||
* Franz Fellner
|
||||
* zwarag
|
||||
* neeasade
|
||||
* meles5
|
||||
* error800
|
||||
* Thorsten Wißmann
|
||||
* Tim Harder
|
||||
* Thiago Barroso Perrotta
|
||||
* Matthias Lisin
|
||||
* Helen Sherwood-Taylor
|
||||
* HalosGhost
|
||||
* Gregor Pohl
|
||||
* Franz Fellner
|
||||
* Eivind Uggedal
|
||||
* Arseniy Seroka
|
||||
* Andreas Fischer
|
||||
// QUTE_AUTHORS_END
|
||||
|
||||
@@ -170,8 +187,8 @@ The following people have contributed graphics:
|
||||
* WOFall (icon)
|
||||
* regines (key binding cheatsheet)
|
||||
|
||||
Thanks / Similiar projects
|
||||
--------------------------
|
||||
Thanks / Similar projects
|
||||
-------------------------
|
||||
|
||||
Many projects with a similar goal as qutebrowser exist:
|
||||
|
||||
@@ -214,7 +231,7 @@ Also, thanks to:
|
||||
|
||||
* Everyone who had the patience to test qutebrowser before v0.1.
|
||||
* Everyone triaging/fixing my bugs in the
|
||||
https://bugreports.qt-project.org/secure/Dashboard.jspa[Qt bugtracker]
|
||||
https://bugreports.qt.io/secure/Dashboard.jspa[Qt bugtracker]
|
||||
* Everyone answering my questions on http://stackoverflow.com/[Stack Overflow]
|
||||
and in IRC.
|
||||
* All the projects which were a great help while developing qutebrowser.
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
|<<adblock-update,adblock-update>>|Update the adblock block lists.
|
||||
|<<back,back>>|Go back in the history of the current tab.
|
||||
|<<bind,bind>>|Bind a key to a command.
|
||||
|<<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.
|
||||
|<<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.
|
||||
@@ -20,6 +23,7 @@
|
||||
|<<hint,hint>>|Start hinting.
|
||||
|<<home,home>>|Open main startpage in current tab.
|
||||
|<<inspector,inspector>>|Toggle the web inspector.
|
||||
|<<jseval,jseval>>|Evaluate a JavaScript string.
|
||||
|<<later,later>>|Execute a command after some time.
|
||||
|<<navigate,navigate>>|Open typical prev/next links or navigate using the URL path.
|
||||
|<<open,open>>|Open a URL in the current/[count]th tab.
|
||||
@@ -56,6 +60,7 @@
|
||||
|<<view-source,view-source>>|Show the source of the current page.
|
||||
|<<wq,wq>>|Save open pages and quit.
|
||||
|<<yank,yank>>|Yank the current URL/title to the clipboard or primary selection.
|
||||
|<<yank-selected,yank-selected>>|Yank the selected text to the clipboard or primary selection.
|
||||
|<<zoom,zoom>>|Set the zoom level for the current tab.
|
||||
|<<zoom-in,zoom-in>>|Increase the zoom level for the current tab.
|
||||
|<<zoom-out,zoom-out>>|Decrease the zoom level for the current tab.
|
||||
@@ -97,6 +102,41 @@ Bind a key to a command.
|
||||
* 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.
|
||||
|
||||
[[bookmark-add]]
|
||||
=== bookmark-add
|
||||
Save the current page as a bookmark.
|
||||
|
||||
[[bookmark-del]]
|
||||
=== bookmark-del
|
||||
Syntax: +:bookmark-del 'url'+
|
||||
|
||||
Delete a bookmark.
|
||||
|
||||
==== positional arguments
|
||||
* +'url'+: The URL of the bookmark to delete.
|
||||
|
||||
==== 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.
|
||||
|
||||
[[bookmark-load]]
|
||||
=== bookmark-load
|
||||
Syntax: +:bookmark-load [*--tab*] [*--bg*] [*--window*] 'url'+
|
||||
|
||||
Load a bookmark.
|
||||
|
||||
==== positional arguments
|
||||
* +'url'+: The url of the bookmark to load.
|
||||
|
||||
==== optional arguments
|
||||
* +*-t*+, +*--tab*+: Load the bookmark in a new tab.
|
||||
* +*-b*+, +*--bg*+: Load the bookmark in a new background tab.
|
||||
* +*-w*+, +*--window*+: Load the bookmark in a new window.
|
||||
|
||||
==== 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.
|
||||
|
||||
[[close]]
|
||||
=== close
|
||||
Close the current window.
|
||||
@@ -198,7 +238,9 @@ Start hinting.
|
||||
* +'target'+: What to do with the selected element.
|
||||
|
||||
- `normal`: Open the link in the current tab.
|
||||
- `tab`: Open the link in a new tab.
|
||||
- `tab`: Open the link in a new tab (honoring the
|
||||
background-tabs setting).
|
||||
- `tab-fg`: Open the link in a new foreground tab.
|
||||
- `tab-bg`: Open the link in a new background tab.
|
||||
- `window`: Open the link in a new window.
|
||||
- `hover` : Hover over the link.
|
||||
@@ -208,7 +250,7 @@ Start hinting.
|
||||
- `fill`: Fill the commandline with the command given as
|
||||
argument.
|
||||
- `download`: Download the link.
|
||||
- `userscript`: Call an userscript with `$QUTE_URL` set to the
|
||||
- `userscript`: Call a userscript with `$QUTE_URL` set to the
|
||||
link.
|
||||
- `spawn`: Spawn a command.
|
||||
|
||||
@@ -227,8 +269,8 @@ Start hinting.
|
||||
|
||||
|
||||
==== optional arguments
|
||||
* +*-r*+, +*--rapid*+: Whether to do rapid hinting. This is only possible with targets `tab-bg`, `window`, `run`, `hover`, `userscript` and
|
||||
`spawn`.
|
||||
* +*-r*+, +*--rapid*+: Whether to do rapid hinting. This is only possible with targets `tab` (with background-tabs=true), `tab-bg`,
|
||||
`window`, `run`, `hover`, `userscript` and `spawn`.
|
||||
|
||||
|
||||
[[home]]
|
||||
@@ -239,6 +281,24 @@ Open main startpage in current tab.
|
||||
=== inspector
|
||||
Toggle the web inspector.
|
||||
|
||||
Note: Due a bug in Qt, the inspector will show incorrect request headers in the network tab.
|
||||
|
||||
[[jseval]]
|
||||
=== jseval
|
||||
Syntax: +:jseval [*--quiet*] 'js-code'+
|
||||
|
||||
Evaluate a JavaScript string.
|
||||
|
||||
==== positional arguments
|
||||
* +'js-code'+: The string to evaluate.
|
||||
|
||||
==== optional arguments
|
||||
* +*-q*+, +*--quiet*+: Don't show resulting JS object.
|
||||
|
||||
==== 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.
|
||||
|
||||
[[later]]
|
||||
=== later
|
||||
Syntax: +:later 'ms' 'command'+
|
||||
@@ -510,17 +570,23 @@ Preset the statusbar to some text.
|
||||
|
||||
[[spawn]]
|
||||
=== spawn
|
||||
Syntax: +:spawn [*--userscript*] 'args' ['args' ...]+
|
||||
Syntax: +:spawn [*--userscript*] [*--verbose*] [*--detach*] 'cmdline'+
|
||||
|
||||
Spawn a command in a shell.
|
||||
|
||||
Note the {url} variable which gets replaced by the current URL might be useful here.
|
||||
|
||||
==== positional arguments
|
||||
* +'args'+: The commandline to execute.
|
||||
* +'cmdline'+: The commandline to execute.
|
||||
|
||||
==== optional arguments
|
||||
* +*-u*+, +*--userscript*+: Run the command as an userscript.
|
||||
* +*-u*+, +*--userscript*+: Run the command as a userscript.
|
||||
* +*-v*+, +*--verbose*+: Show notifications when the command started/exited.
|
||||
* +*-d*+, +*--detach*+: Whether the command should be detached from qutebrowser.
|
||||
|
||||
==== 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.
|
||||
|
||||
[[stop]]
|
||||
=== stop
|
||||
@@ -564,6 +630,8 @@ Syntax: +:tab-focus ['index']+
|
||||
|
||||
Select the tab given as argument/[count].
|
||||
|
||||
If neither count nor index are given, it behaves like tab-next.
|
||||
|
||||
==== positional arguments
|
||||
* +'index'+: The tab index to focus, starting with 1. The special value `last` focuses the last focused tab.
|
||||
|
||||
@@ -639,13 +707,24 @@ Save open pages and quit.
|
||||
|
||||
[[yank]]
|
||||
=== yank
|
||||
Syntax: +:yank [*--title*] [*--sel*]+
|
||||
Syntax: +:yank [*--title*] [*--sel*] [*--domain*]+
|
||||
|
||||
Yank the current URL/title to the clipboard or primary selection.
|
||||
|
||||
==== optional arguments
|
||||
* +*-t*+, +*--title*+: Yank the title instead of the URL.
|
||||
* +*-s*+, +*--sel*+: Use the primary selection instead of the clipboard.
|
||||
* +*-d*+, +*--domain*+: Yank only the scheme, domain, and port number.
|
||||
|
||||
[[yank-selected]]
|
||||
=== yank-selected
|
||||
Syntax: +:yank-selected [*--sel*] [*--keep*]+
|
||||
|
||||
Yank the selected text to the clipboard or primary selection.
|
||||
|
||||
==== optional arguments
|
||||
* +*-s*+, +*--sel*+: Use the primary selection instead of the clipboard.
|
||||
* +*-k*+, +*--keep*+: If given, stay in visual mode after yanking.
|
||||
|
||||
[[zoom]]
|
||||
=== zoom
|
||||
@@ -681,14 +760,36 @@ How many steps to zoom out.
|
||||
[options="header",width="75%",cols="25%,75%"]
|
||||
|==============
|
||||
|Command|Description
|
||||
|<<clear-keychain,clear-keychain>>|Clear the currently entered key chain.
|
||||
|<<command-accept,command-accept>>|Execute the command currently in the commandline.
|
||||
|<<command-history-next,command-history-next>>|Go forward in the commandline history.
|
||||
|<<command-history-prev,command-history-prev>>|Go back in the commandline history.
|
||||
|<<completion-item-del,completion-item-del>>|Delete the current completion item.
|
||||
|<<completion-item-next,completion-item-next>>|Select the next completion item.
|
||||
|<<completion-item-prev,completion-item-prev>>|Select the previous completion item.
|
||||
|<<drop-selection,drop-selection>>|Drop selection and keep selection mode enabled.
|
||||
|<<enter-mode,enter-mode>>|Enter a key mode.
|
||||
|<<follow-hint,follow-hint>>|Follow the currently selected hint.
|
||||
|<<follow-selected,follow-selected>>|Follow the selected text.
|
||||
|<<leave-mode,leave-mode>>|Leave the mode we're currently in.
|
||||
|<<message-error,message-error>>|Show an error message in the statusbar.
|
||||
|<<message-info,message-info>>|Show an info message in the statusbar.
|
||||
|<<message-warning,message-warning>>|Show a warning message in the statusbar.
|
||||
|<<move-to-end-of-document,move-to-end-of-document>>|Move the cursor or selection to the end of the document.
|
||||
|<<move-to-end-of-line,move-to-end-of-line>>|Move the cursor or selection to the end of line.
|
||||
|<<move-to-end-of-next-block,move-to-end-of-next-block>>|Move the cursor or selection to the end of next block.
|
||||
|<<move-to-end-of-prev-block,move-to-end-of-prev-block>>|Move the cursor or selection to the end of previous block.
|
||||
|<<move-to-end-of-word,move-to-end-of-word>>|Move the cursor or selection to the end of the word.
|
||||
|<<move-to-next-char,move-to-next-char>>|Move the cursor or selection to the next char.
|
||||
|<<move-to-next-line,move-to-next-line>>|Move the cursor or selection to the next line.
|
||||
|<<move-to-next-word,move-to-next-word>>|Move the cursor or selection to the next word.
|
||||
|<<move-to-prev-char,move-to-prev-char>>|Move the cursor or selection to the previous char.
|
||||
|<<move-to-prev-line,move-to-prev-line>>|Move the cursor or selection to the prev line.
|
||||
|<<move-to-prev-word,move-to-prev-word>>|Move the cursor or selection to the previous word.
|
||||
|<<move-to-start-of-document,move-to-start-of-document>>|Move the cursor or selection to the start of the document.
|
||||
|<<move-to-start-of-line,move-to-start-of-line>>|Move the cursor or selection to the start of the line.
|
||||
|<<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.
|
||||
|<<prompt-accept,prompt-accept>>|Accept the current prompt.
|
||||
|<<prompt-no,prompt-no>>|Answer no to a yes/no prompt.
|
||||
@@ -706,12 +807,18 @@ How many steps to zoom out.
|
||||
|<<rl-unix-line-discard,rl-unix-line-discard>>|Remove chars backward from the cursor to the beginning of the line.
|
||||
|<<rl-unix-word-rubout,rl-unix-word-rubout>>|Remove chars from the cursor to the beginning of the word.
|
||||
|<<rl-yank,rl-yank>>|Paste the most recently deleted text.
|
||||
|<<scroll,scroll>>|Scroll the current tab by 'count * dx/dy'.
|
||||
|<<scroll,scroll>>|Scroll the current tab in the given direction.
|
||||
|<<scroll-page,scroll-page>>|Scroll the frame page-wise.
|
||||
|<<scroll-perc,scroll-perc>>|Scroll to a specific percentage of the page.
|
||||
|<<scroll-px,scroll-px>>|Scroll the current tab by 'count * dx/dy' pixels.
|
||||
|<<search-next,search-next>>|Continue the search to the ([count]th) next term.
|
||||
|<<search-prev,search-prev>>|Continue the search to the ([count]th) previous term.
|
||||
|<<toggle-selection,toggle-selection>>|Toggle caret selection mode.
|
||||
|==============
|
||||
[[clear-keychain]]
|
||||
=== clear-keychain
|
||||
Clear the currently entered key chain.
|
||||
|
||||
[[command-accept]]
|
||||
=== command-accept
|
||||
Execute the command currently in the commandline.
|
||||
@@ -724,6 +831,10 @@ Go forward in the commandline history.
|
||||
=== command-history-prev
|
||||
Go back in the commandline history.
|
||||
|
||||
[[completion-item-del]]
|
||||
=== completion-item-del
|
||||
Delete the current completion item.
|
||||
|
||||
[[completion-item-next]]
|
||||
=== completion-item-next
|
||||
Select the next completion item.
|
||||
@@ -732,6 +843,10 @@ Select the next completion item.
|
||||
=== completion-item-prev
|
||||
Select the previous completion item.
|
||||
|
||||
[[drop-selection]]
|
||||
=== drop-selection
|
||||
Drop selection and keep selection mode enabled.
|
||||
|
||||
[[enter-mode]]
|
||||
=== enter-mode
|
||||
Syntax: +:enter-mode 'mode'+
|
||||
@@ -745,10 +860,139 @@ Enter a key mode.
|
||||
=== follow-hint
|
||||
Follow the currently selected hint.
|
||||
|
||||
[[follow-selected]]
|
||||
=== follow-selected
|
||||
Syntax: +:follow-selected [*--tab*]+
|
||||
|
||||
Follow the selected text.
|
||||
|
||||
==== optional arguments
|
||||
* +*-t*+, +*--tab*+: Load the selected link in a new tab.
|
||||
|
||||
[[leave-mode]]
|
||||
=== leave-mode
|
||||
Leave the mode we're currently in.
|
||||
|
||||
[[message-error]]
|
||||
=== message-error
|
||||
Syntax: +:message-error 'text'+
|
||||
|
||||
Show an error message in the statusbar.
|
||||
|
||||
==== positional arguments
|
||||
* +'text'+: The text to show.
|
||||
|
||||
[[message-info]]
|
||||
=== message-info
|
||||
Syntax: +:message-info 'text'+
|
||||
|
||||
Show an info message in the statusbar.
|
||||
|
||||
==== positional arguments
|
||||
* +'text'+: The text to show.
|
||||
|
||||
[[message-warning]]
|
||||
=== message-warning
|
||||
Syntax: +:message-warning 'text'+
|
||||
|
||||
Show a warning message in the statusbar.
|
||||
|
||||
==== positional arguments
|
||||
* +'text'+: The text to show.
|
||||
|
||||
[[move-to-end-of-document]]
|
||||
=== move-to-end-of-document
|
||||
Move the cursor or selection to the end of the document.
|
||||
|
||||
[[move-to-end-of-line]]
|
||||
=== move-to-end-of-line
|
||||
Move the cursor or selection to the end of line.
|
||||
|
||||
[[move-to-end-of-next-block]]
|
||||
=== move-to-end-of-next-block
|
||||
Move the cursor or selection to the end of next block.
|
||||
|
||||
==== count
|
||||
How many blocks to move.
|
||||
|
||||
[[move-to-end-of-prev-block]]
|
||||
=== move-to-end-of-prev-block
|
||||
Move the cursor or selection to the end of previous block.
|
||||
|
||||
==== count
|
||||
How many blocks to move.
|
||||
|
||||
[[move-to-end-of-word]]
|
||||
=== move-to-end-of-word
|
||||
Move the cursor or selection to the end of the word.
|
||||
|
||||
==== count
|
||||
How many words to move.
|
||||
|
||||
[[move-to-next-char]]
|
||||
=== move-to-next-char
|
||||
Move the cursor or selection to the next char.
|
||||
|
||||
==== count
|
||||
How many lines to move.
|
||||
|
||||
[[move-to-next-line]]
|
||||
=== move-to-next-line
|
||||
Move the cursor or selection to the next line.
|
||||
|
||||
==== count
|
||||
How many lines to move.
|
||||
|
||||
[[move-to-next-word]]
|
||||
=== move-to-next-word
|
||||
Move the cursor or selection to the next word.
|
||||
|
||||
==== count
|
||||
How many words to move.
|
||||
|
||||
[[move-to-prev-char]]
|
||||
=== move-to-prev-char
|
||||
Move the cursor or selection to the previous char.
|
||||
|
||||
==== count
|
||||
How many chars to move.
|
||||
|
||||
[[move-to-prev-line]]
|
||||
=== move-to-prev-line
|
||||
Move the cursor or selection to the prev line.
|
||||
|
||||
==== count
|
||||
How many lines to move.
|
||||
|
||||
[[move-to-prev-word]]
|
||||
=== move-to-prev-word
|
||||
Move the cursor or selection to the previous word.
|
||||
|
||||
==== count
|
||||
How many words to move.
|
||||
|
||||
[[move-to-start-of-document]]
|
||||
=== move-to-start-of-document
|
||||
Move the cursor or selection to the start of the document.
|
||||
|
||||
[[move-to-start-of-line]]
|
||||
=== move-to-start-of-line
|
||||
Move the cursor or selection to the start of the line.
|
||||
|
||||
[[move-to-start-of-next-block]]
|
||||
=== move-to-start-of-next-block
|
||||
Move the cursor or selection to the start of next block.
|
||||
|
||||
==== count
|
||||
How many blocks to move.
|
||||
|
||||
[[move-to-start-of-prev-block]]
|
||||
=== move-to-start-of-prev-block
|
||||
Move the cursor or selection to the start of previous block.
|
||||
|
||||
==== count
|
||||
How many blocks to move.
|
||||
|
||||
[[open-editor]]
|
||||
=== open-editor
|
||||
Open an external editor with the currently selected form field.
|
||||
@@ -847,20 +1091,20 @@ This acts like readline's yank.
|
||||
|
||||
[[scroll]]
|
||||
=== scroll
|
||||
Syntax: +:scroll 'dx' 'dy'+
|
||||
Syntax: +:scroll 'direction' ['dy']+
|
||||
|
||||
Scroll the current tab by 'count * dx/dy'.
|
||||
Scroll the current tab in the given direction.
|
||||
|
||||
==== positional arguments
|
||||
* +'dx'+: How much to scroll in x-direction.
|
||||
* +'dy'+: How much to scroll in x-direction.
|
||||
* +'direction'+: In which direction to scroll (up/down/left/right/top/bottom).
|
||||
|
||||
|
||||
==== count
|
||||
multiplier
|
||||
|
||||
[[scroll-page]]
|
||||
=== scroll-page
|
||||
Syntax: +:scroll-page 'x' 'y'+
|
||||
Syntax: +:scroll-page [*--top-navigate* 'ACTION'] [*--bottom-navigate* 'ACTION'] 'x' 'y'+
|
||||
|
||||
Scroll the frame page-wise.
|
||||
|
||||
@@ -868,6 +1112,12 @@ Scroll the frame page-wise.
|
||||
* +'x'+: How many pages to scroll to the right.
|
||||
* +'y'+: How many pages to scroll down.
|
||||
|
||||
==== optional arguments
|
||||
* +*-t*+, +*--top-navigate*+: :navigate action (prev, decrement) to run when scrolling up at the top of the page.
|
||||
|
||||
* +*-b*+, +*--bottom-navigate*+: :navigate action (next, increment) to run when scrolling down at the bottom of the page.
|
||||
|
||||
|
||||
==== count
|
||||
multiplier
|
||||
|
||||
@@ -888,6 +1138,19 @@ The percentage can be given either as argument or as count. If no percentage is
|
||||
==== count
|
||||
Percentage to scroll.
|
||||
|
||||
[[scroll-px]]
|
||||
=== scroll-px
|
||||
Syntax: +:scroll-px 'dx' 'dy'+
|
||||
|
||||
Scroll the current tab by 'count * dx/dy' pixels.
|
||||
|
||||
==== positional arguments
|
||||
* +'dx'+: How much to scroll in x-direction.
|
||||
* +'dy'+: How much to scroll in x-direction.
|
||||
|
||||
==== count
|
||||
multiplier
|
||||
|
||||
[[search-next]]
|
||||
=== search-next
|
||||
Continue the search to the ([count]th) next term.
|
||||
@@ -902,6 +1165,10 @@ Continue the search to the ([count]th) previous term.
|
||||
==== count
|
||||
How many elements to ignore.
|
||||
|
||||
[[toggle-selection]]
|
||||
=== toggle-selection
|
||||
Toggle caret selection mode.
|
||||
|
||||
|
||||
== Debugging commands
|
||||
These commands are mainly intended for debugging. They are hidden if qutebrowser was started without the `--debug`-flag.
|
||||
@@ -916,6 +1183,7 @@ These commands are mainly intended for debugging. They are hidden if qutebrowser
|
||||
|<<debug-crash,debug-crash>>|Crash for debugging purposes.
|
||||
|<<debug-pyeval,debug-pyeval>>|Evaluate a python string and display the results as a web page.
|
||||
|<<debug-trace,debug-trace>>|Trace executed code via hunter.
|
||||
|<<debug-webaction,debug-webaction>>|Execute a webaction.
|
||||
|==============
|
||||
[[debug-all-objects]]
|
||||
=== debug-all-objects
|
||||
@@ -964,3 +1232,17 @@ Trace executed code via hunter.
|
||||
* 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-webaction]]
|
||||
=== debug-webaction
|
||||
Syntax: +:debug-webaction 'action'+
|
||||
|
||||
Execute a webaction.
|
||||
|
||||
See http://doc.qt.io/qt-5/qwebpage.html#WebAction-enum for the available actions.
|
||||
|
||||
==== positional arguments
|
||||
* +'action'+: The action to execute, e.g. MoveToNextChar.
|
||||
|
||||
==== count
|
||||
How many times to repeat the action.
|
||||
|
||||
|
||||
@@ -38,12 +38,15 @@
|
||||
|<<ui-display-statusbar-messages,display-statusbar-messages>>|Whether to display javascript statusbar messages.
|
||||
|<<ui-zoom-text-only,zoom-text-only>>|Whether the zoom factor on a frame applies only to the text or to all content.
|
||||
|<<ui-frame-flattening,frame-flattening>>|Whether to expand each subframe to its contents.
|
||||
|<<ui-user-stylesheet,user-stylesheet>>|User stylesheet to use (absolute filename or CSS string). Will expand environment variables.
|
||||
|<<ui-user-stylesheet,user-stylesheet>>|User stylesheet to use (absolute filename, filename relative to the config directory or CSS string). Will expand environment variables.
|
||||
|<<ui-css-media-type,css-media-type>>|Set the CSS media type.
|
||||
|<<ui-smooth-scrolling,smooth-scrolling>>|Whether to enable smooth scrolling for webpages.
|
||||
|<<ui-remove-finished-downloads,remove-finished-downloads>>|Whether to remove finished downloads automatically.
|
||||
|<<ui-hide-statusbar,hide-statusbar>>|Whether to hide the statusbar unless a message is shown.
|
||||
|<<ui-statusbar-padding,statusbar-padding>>|Padding for statusbar (top, bottom, left, right).
|
||||
|<<ui-window-title-format,window-title-format>>|The format to use for the window title. The following placeholders are defined:
|
||||
|<<ui-hide-mouse-cursor,hide-mouse-cursor>>|Whether to hide the mouse cursor.
|
||||
|<<ui-modal-js-dialog,modal-js-dialog>>|Use standard JavaScript modal dialog for alert() and confirm()
|
||||
|==============
|
||||
|
||||
.Quick reference for section ``network''
|
||||
@@ -52,6 +55,7 @@
|
||||
|Setting|Description
|
||||
|<<network-do-not-track,do-not-track>>|Value to send in the `DNT` header.
|
||||
|<<network-accept-language,accept-language>>|Value to send in the `accept-language` header.
|
||||
|<<network-referer-header,referer-header>>|Send the Referer header
|
||||
|<<network-user-agent,user-agent>>|User agent to send. Empty to send the default.
|
||||
|<<network-proxy,proxy>>|The proxy to use.
|
||||
|<<network-proxy-dns-requests,proxy-dns-requests>>|Whether to send DNS requests over the configured proxy.
|
||||
@@ -63,6 +67,7 @@
|
||||
[options="header",width="75%",cols="25%,75%"]
|
||||
|==============
|
||||
|Setting|Description
|
||||
|<<completion-auto-open,auto-open>>|Automatically open completion when typing.
|
||||
|<<completion-download-path-suggestion,download-path-suggestion>>|What to display in the download filename input.
|
||||
|<<completion-timestamp-format,timestamp-format>>|How to format timestamps (e.g. for history)
|
||||
|<<completion-show,show>>|Whether to show the autocompletion window.
|
||||
@@ -83,7 +88,7 @@
|
||||
|<<input-auto-leave-insert-mode,auto-leave-insert-mode>>|Whether to leave insert mode if a non-editable element is clicked.
|
||||
|<<input-auto-insert-mode,auto-insert-mode>>|Whether to automatically enter insert mode if an editable element is focused after page load.
|
||||
|<<input-forward-unbound-keys,forward-unbound-keys>>|Whether to forward unbound keys to the webview in normal mode.
|
||||
|<<input-spatial-navigation,spatial-navigation>>|Enables or disables the Spatial Navigation feature
|
||||
|<<input-spatial-navigation,spatial-navigation>>|Enables or disables the Spatial Navigation feature.
|
||||
|<<input-links-included-in-focus-chain,links-included-in-focus-chain>>|Whether hyperlinks should be included in the keyboard focus chain.
|
||||
|<<input-rocker-gestures,rocker-gestures>>|Whether to enable Opera-like mouse rocker gestures. This disables the context menu.
|
||||
|<<input-mouse-zoom-divider,mouse-zoom-divider>>|How much to divide the mouse wheel movements to translate them into zoom increments.
|
||||
@@ -97,9 +102,9 @@
|
||||
|<<tabs-select-on-remove,select-on-remove>>|Which tab to select when the focused tab is removed.
|
||||
|<<tabs-new-tab-position,new-tab-position>>|How new tabs are positioned.
|
||||
|<<tabs-new-tab-position-explicit,new-tab-position-explicit>>|How new tabs opened explicitly are positioned.
|
||||
|<<tabs-last-close,last-close>>|Behaviour when the last tab is closed.
|
||||
|<<tabs-hide-auto,hide-auto>>|Hide the tab bar if only one tab is open.
|
||||
|<<tabs-hide-always,hide-always>>|Always hide the tab bar.
|
||||
|<<tabs-last-close,last-close>>|Behavior when the last tab is closed.
|
||||
|<<tabs-show,show>>|When to show the tab bar
|
||||
|<<tabs-show-switching-delay,show-switching-delay>>|Time to show the tab bar before hiding it when tabs->show is set to 'switching'.
|
||||
|<<tabs-wrap,wrap>>|Whether to wrap when changing tabs.
|
||||
|<<tabs-movable,movable>>|Whether tabs should be movable.
|
||||
|<<tabs-close-mouse-button,close-mouse-button>>|On which mouse button to close tabs.
|
||||
@@ -107,9 +112,11 @@
|
||||
|<<tabs-show-favicons,show-favicons>>|Whether to show favicons in the tab bar.
|
||||
|<<tabs-width,width>>|The width of the tab bar if it's vertical, in px or as percentage of the window.
|
||||
|<<tabs-indicator-width,indicator-width>>|Width of the progress indicator (0 to disable).
|
||||
|<<tabs-indicator-space,indicator-space>>|Spacing between tab edge and indicator.
|
||||
|<<tabs-tabs-are-windows,tabs-are-windows>>|Whether to open windows instead of tabs.
|
||||
|<<tabs-title-format,title-format>>|The format to use for the tab title. The following placeholders are defined:
|
||||
|<<tabs-mousewheel-tab-switching,mousewheel-tab-switching>>|Switch between tabs using the mouse wheel.
|
||||
|<<tabs-padding,padding>>|Padding for tabs (top, bottom, left, right).
|
||||
|<<tabs-indicator-padding,indicator-padding>>|Padding for indicators (top, bottom, left, right).
|
||||
|==============
|
||||
|
||||
.Quick reference for section ``storage''
|
||||
@@ -117,6 +124,8 @@
|
||||
|==============
|
||||
|Setting|Description
|
||||
|<<storage-download-directory,download-directory>>|The directory to save downloads to. An empty value selects a sensible os-specific default. Will expand environment variables.
|
||||
|<<storage-prompt-download-directory,prompt-download-directory>>|Whether to prompt the user for the download location.
|
||||
|<<storage-remember-download-directory,remember-download-directory>>|Whether to remember the last used download directory.
|
||||
|<<storage-maximum-pages-in-cache,maximum-pages-in-cache>>|The maximum number of pages to hold in the global memory page cache.
|
||||
|<<storage-object-cache-capacities,object-cache-capacities>>|The capacities for the global memory cache for dead objects such as stylesheets or scripts. Syntax: cacheMinDeadCapacity, cacheMaxDead, totalCapacity.
|
||||
|<<storage-offline-storage-default-quota,offline-storage-default-quota>>|Default quota for new offline storage databases.
|
||||
@@ -134,6 +143,9 @@
|
||||
|<<content-allow-images,allow-images>>|Whether images are automatically loaded in web pages.
|
||||
|<<content-allow-javascript,allow-javascript>>|Enables or disables the running of JavaScript programs.
|
||||
|<<content-allow-plugins,allow-plugins>>|Enables or disables plugins in Web pages.
|
||||
|<<content-webgl,webgl>>|Enables or disables WebGL.
|
||||
|<<content-css-regions,css-regions>>|Enable or disable support for CSS regions.
|
||||
|<<content-hyperlink-auditing,hyperlink-auditing>>|Enable or disable hyperlink auditing (<a ping>).
|
||||
|<<content-geolocation,geolocation>>|Allow websites to request geolocations.
|
||||
|<<content-notifications,notifications>>|Allow websites to show notifications.
|
||||
|<<content-javascript-can-open-windows,javascript-can-open-windows>>|Whether JavaScript programs can open new windows.
|
||||
@@ -143,7 +155,7 @@
|
||||
|<<content-ignore-javascript-alert,ignore-javascript-alert>>|Whether all javascript alerts should be ignored.
|
||||
|<<content-local-content-can-access-remote-urls,local-content-can-access-remote-urls>>|Whether locally loaded documents are allowed to access remote urls.
|
||||
|<<content-local-content-can-access-file-urls,local-content-can-access-file-urls>>|Whether locally loaded documents are allowed to access other local urls.
|
||||
|<<content-cookies-accept,cookies-accept>>|Whether to accept cookies.
|
||||
|<<content-cookies-accept,cookies-accept>>|Control which cookies to accept.
|
||||
|<<content-cookies-store,cookies-store>>|Whether to store cookies.
|
||||
|<<content-host-block-lists,host-block-lists>>|List of URLs of lists which contain hosts to block.
|
||||
|<<content-host-blocking-enabled,host-blocking-enabled>>|Whether host blocking is enabled.
|
||||
@@ -181,12 +193,22 @@
|
||||
|<<colors-completion.item.selected.border.top,completion.item.selected.border.top>>|Top border color of the completion widget category headers.
|
||||
|<<colors-completion.item.selected.border.bottom,completion.item.selected.border.bottom>>|Bottom border color of the selected completion item.
|
||||
|<<colors-completion.match.fg,completion.match.fg>>|Foreground color of the matched text in the completion.
|
||||
|<<colors-statusbar.bg,statusbar.bg>>|Foreground color of the statusbar.
|
||||
|<<colors-statusbar.fg,statusbar.fg>>|Foreground color of the statusbar.
|
||||
|<<colors-statusbar.bg,statusbar.bg>>|Foreground color of the statusbar.
|
||||
|<<colors-statusbar.fg.error,statusbar.fg.error>>|Foreground color of the statusbar if there was an error.
|
||||
|<<colors-statusbar.bg.error,statusbar.bg.error>>|Background color of the statusbar if there was an error.
|
||||
|<<colors-statusbar.fg.warning,statusbar.fg.warning>>|Foreground color of the statusbar if there is a warning.
|
||||
|<<colors-statusbar.bg.warning,statusbar.bg.warning>>|Background color of the statusbar if there is a warning.
|
||||
|<<colors-statusbar.fg.prompt,statusbar.fg.prompt>>|Foreground color of the statusbar if there is a prompt.
|
||||
|<<colors-statusbar.bg.prompt,statusbar.bg.prompt>>|Background color of the statusbar if there is a prompt.
|
||||
|<<colors-statusbar.fg.insert,statusbar.fg.insert>>|Foreground color of the statusbar in insert mode.
|
||||
|<<colors-statusbar.bg.insert,statusbar.bg.insert>>|Background color of the statusbar in insert mode.
|
||||
|<<colors-statusbar.fg.command,statusbar.fg.command>>|Foreground color of the statusbar in command mode.
|
||||
|<<colors-statusbar.bg.command,statusbar.bg.command>>|Background color of the statusbar in command mode.
|
||||
|<<colors-statusbar.fg.caret,statusbar.fg.caret>>|Foreground color of the statusbar in caret mode.
|
||||
|<<colors-statusbar.bg.caret,statusbar.bg.caret>>|Background color of the statusbar in caret mode.
|
||||
|<<colors-statusbar.fg.caret-selection,statusbar.fg.caret-selection>>|Foreground color of the statusbar in caret mode with a selection
|
||||
|<<colors-statusbar.bg.caret-selection,statusbar.bg.caret-selection>>|Background color of the statusbar in caret mode with a selection
|
||||
|<<colors-statusbar.progress.bg,statusbar.progress.bg>>|Background color of the progress bar.
|
||||
|<<colors-statusbar.url.fg,statusbar.url.fg>>|Default foreground color of the URL in the statusbar.
|
||||
|<<colors-statusbar.url.fg.success,statusbar.url.fg.success>>|Foreground color of the URL in the statusbar on successful load.
|
||||
@@ -194,10 +216,10 @@
|
||||
|<<colors-statusbar.url.fg.warn,statusbar.url.fg.warn>>|Foreground color of the URL in the statusbar when there's a warning.
|
||||
|<<colors-statusbar.url.fg.hover,statusbar.url.fg.hover>>|Foreground color of the URL in the statusbar for hovered links.
|
||||
|<<colors-tabs.fg.odd,tabs.fg.odd>>|Foreground color of unselected odd tabs.
|
||||
|<<colors-tabs.fg.even,tabs.fg.even>>|Foreground color of unselected even tabs.
|
||||
|<<colors-tabs.fg.selected,tabs.fg.selected>>|Foreground color of selected tabs.
|
||||
|<<colors-tabs.bg.odd,tabs.bg.odd>>|Background color of unselected odd tabs.
|
||||
|<<colors-tabs.fg.even,tabs.fg.even>>|Foreground color of unselected even tabs.
|
||||
|<<colors-tabs.bg.even,tabs.bg.even>>|Background color of unselected even tabs.
|
||||
|<<colors-tabs.fg.selected,tabs.fg.selected>>|Foreground color of selected tabs.
|
||||
|<<colors-tabs.bg.selected,tabs.bg.selected>>|Background color of selected tabs.
|
||||
|<<colors-tabs.bg.bar,tabs.bg.bar>>|Background color of the tab bar.
|
||||
|<<colors-tabs.indicator.start,tabs.indicator.start>>|Color gradient start for the tab indicator.
|
||||
@@ -205,14 +227,18 @@
|
||||
|<<colors-tabs.indicator.error,tabs.indicator.error>>|Color for the tab indicator on errors..
|
||||
|<<colors-tabs.indicator.system,tabs.indicator.system>>|Color gradient interpolation system for the tab indicator.
|
||||
|<<colors-hints.fg,hints.fg>>|Font color for hints.
|
||||
|<<colors-hints.fg.match,hints.fg.match>>|Font color for the matched part of hints.
|
||||
|<<colors-hints.bg,hints.bg>>|Background color for hints.
|
||||
|<<colors-downloads.fg,downloads.fg>>|Foreground color for downloads.
|
||||
|<<colors-hints.fg.match,hints.fg.match>>|Font color for the matched part of hints.
|
||||
|<<colors-downloads.bg.bar,downloads.bg.bar>>|Background color for the download bar.
|
||||
|<<colors-downloads.bg.start,downloads.bg.start>>|Color gradient start for downloads.
|
||||
|<<colors-downloads.bg.stop,downloads.bg.stop>>|Color gradient end for downloads.
|
||||
|<<colors-downloads.bg.system,downloads.bg.system>>|Color gradient interpolation system for downloads.
|
||||
|<<colors-downloads.fg.start,downloads.fg.start>>|Color gradient start for download text.
|
||||
|<<colors-downloads.bg.start,downloads.bg.start>>|Color gradient start for download backgrounds.
|
||||
|<<colors-downloads.fg.stop,downloads.fg.stop>>|Color gradient end for download text.
|
||||
|<<colors-downloads.bg.stop,downloads.bg.stop>>|Color gradient stop for download backgrounds.
|
||||
|<<colors-downloads.fg.system,downloads.fg.system>>|Color gradient interpolation system for download text.
|
||||
|<<colors-downloads.bg.system,downloads.bg.system>>|Color gradient interpolation system for download backgrounds.
|
||||
|<<colors-downloads.fg.error,downloads.fg.error>>|Foreground color for downloads with errors.
|
||||
|<<colors-downloads.bg.error,downloads.bg.error>>|Background color for downloads with errors.
|
||||
|<<colors-webpage.bg,webpage.bg>>|Background color for webpages if unset (or empty to use the theme's color)
|
||||
|==============
|
||||
|
||||
.Quick reference for section ``fonts''
|
||||
@@ -392,13 +418,13 @@ How to open links in an existing instance if a new one is launched.
|
||||
|
||||
Valid values:
|
||||
|
||||
* +tab+: Open a new tab in the existing window and activate it.
|
||||
* +tab-bg+: Open a new background tab in the existing window and activate it.
|
||||
* +tab-silent+: Open a new tab in the existing window without activating it.
|
||||
* +tab-bg-silent+: Open a new background tab in the existing window without activating it.
|
||||
* +tab+: Open a new tab in the existing window and activate the window.
|
||||
* +tab-bg+: Open a new background tab in the existing window and activate the window.
|
||||
* +tab-silent+: Open a new tab in the existing window without activating the window.
|
||||
* +tab-bg-silent+: Open a new background tab in the existing window without activating the window.
|
||||
* +window+: Open in a new window.
|
||||
|
||||
Default: +pass:[window]+
|
||||
Default: +pass:[tab]+
|
||||
|
||||
[[general-log-javascript-console]]
|
||||
=== log-javascript-console
|
||||
@@ -449,10 +475,10 @@ Where to show the downloaded files.
|
||||
|
||||
Valid values:
|
||||
|
||||
* +north+
|
||||
* +south+
|
||||
* +top+
|
||||
* +bottom+
|
||||
|
||||
Default: +pass:[north]+
|
||||
Default: +pass:[top]+
|
||||
|
||||
[[ui-message-timeout]]
|
||||
=== message-timeout
|
||||
@@ -521,7 +547,7 @@ Default: +pass:[false]+
|
||||
|
||||
[[ui-user-stylesheet]]
|
||||
=== user-stylesheet
|
||||
User stylesheet to use (absolute filename or CSS string). Will expand environment variables.
|
||||
User stylesheet to use (absolute filename, filename relative to the config directory or CSS string). Will expand environment variables.
|
||||
|
||||
Default: +pass:[::-webkit-scrollbar { width: 0px; height: 0px; }]+
|
||||
|
||||
@@ -531,6 +557,17 @@ Set the CSS media type.
|
||||
|
||||
Default: empty
|
||||
|
||||
[[ui-smooth-scrolling]]
|
||||
=== smooth-scrolling
|
||||
Whether to enable smooth scrolling for webpages.
|
||||
|
||||
Valid values:
|
||||
|
||||
* +true+
|
||||
* +false+
|
||||
|
||||
Default: +pass:[false]+
|
||||
|
||||
[[ui-remove-finished-downloads]]
|
||||
=== remove-finished-downloads
|
||||
Whether to remove finished downloads automatically.
|
||||
@@ -553,6 +590,12 @@ Valid values:
|
||||
|
||||
Default: +pass:[false]+
|
||||
|
||||
[[ui-statusbar-padding]]
|
||||
=== statusbar-padding
|
||||
Padding for statusbar (top, bottom, left, right).
|
||||
|
||||
Default: +pass:[1,1,0,0]+
|
||||
|
||||
[[ui-window-title-format]]
|
||||
=== window-title-format
|
||||
The format to use for the window title. The following placeholders are defined:
|
||||
@@ -576,6 +619,17 @@ Valid values:
|
||||
|
||||
Default: +pass:[false]+
|
||||
|
||||
[[ui-modal-js-dialog]]
|
||||
=== modal-js-dialog
|
||||
Use standard JavaScript modal dialog for alert() and confirm()
|
||||
|
||||
Valid values:
|
||||
|
||||
* +true+
|
||||
* +false+
|
||||
|
||||
Default: +pass:[false]+
|
||||
|
||||
== network
|
||||
Settings related to the network.
|
||||
|
||||
@@ -596,6 +650,18 @@ Value to send in the `accept-language` header.
|
||||
|
||||
Default: +pass:[en-US,en]+
|
||||
|
||||
[[network-referer-header]]
|
||||
=== referer-header
|
||||
Send the Referer header
|
||||
|
||||
Valid values:
|
||||
|
||||
* +always+: Always send.
|
||||
* +never+: Never send; this is not recommended, as some sites may break.
|
||||
* +same-domain+: Only send for the same domain. This will still protect your privacy, but shouldn't break any sites.
|
||||
|
||||
Default: +pass:[same-domain]+
|
||||
|
||||
[[network-user-agent]]
|
||||
=== user-agent
|
||||
User agent to send. Empty to send the default.
|
||||
@@ -652,6 +718,17 @@ Default: +pass:[true]+
|
||||
== completion
|
||||
Options related to completion and command history.
|
||||
|
||||
[[completion-auto-open]]
|
||||
=== auto-open
|
||||
Automatically open completion when typing.
|
||||
|
||||
Valid values:
|
||||
|
||||
* +true+
|
||||
* +false+
|
||||
|
||||
Default: +pass:[true]+
|
||||
|
||||
[[completion-download-path-suggestion]]
|
||||
=== download-path-suggestion
|
||||
What to display in the download filename input.
|
||||
@@ -787,7 +864,7 @@ Default: +pass:[auto]+
|
||||
|
||||
[[input-spatial-navigation]]
|
||||
=== spatial-navigation
|
||||
Enables or disables the Spatial Navigation feature
|
||||
Enables or disables the Spatial Navigation feature.
|
||||
|
||||
Spatial navigation consists in the ability to navigate between focusable elements in a Web page, such as hyperlinks and form controls, by using Left, Right, Up and Down arrow keys. For example, if a user presses the Right key, heuristics determine whether there is an element he might be trying to reach towards the right and which element he probably wants.
|
||||
|
||||
@@ -880,7 +957,7 @@ Default: +pass:[last]+
|
||||
|
||||
[[tabs-last-close]]
|
||||
=== last-close
|
||||
Behaviour when the last tab is closed.
|
||||
Behavior when the last tab is closed.
|
||||
|
||||
Valid values:
|
||||
|
||||
@@ -892,27 +969,24 @@ Valid values:
|
||||
|
||||
Default: +pass:[ignore]+
|
||||
|
||||
[[tabs-hide-auto]]
|
||||
=== hide-auto
|
||||
Hide the tab bar if only one tab is open.
|
||||
[[tabs-show]]
|
||||
=== show
|
||||
When to show the tab bar
|
||||
|
||||
Valid values:
|
||||
|
||||
* +true+
|
||||
* +false+
|
||||
* +always+: Always show the tab bar.
|
||||
* +never+: Always hide the tab bar.
|
||||
* +multiple+: Hide the tab bar if only one tab is open.
|
||||
* +switching+: Show the tab bar when switching tabs.
|
||||
|
||||
Default: +pass:[false]+
|
||||
Default: +pass:[always]+
|
||||
|
||||
[[tabs-hide-always]]
|
||||
=== hide-always
|
||||
Always hide the tab bar.
|
||||
[[tabs-show-switching-delay]]
|
||||
=== show-switching-delay
|
||||
Time to show the tab bar before hiding it when tabs->show is set to 'switching'.
|
||||
|
||||
Valid values:
|
||||
|
||||
* +true+
|
||||
* +false+
|
||||
|
||||
Default: +pass:[false]+
|
||||
Default: +pass:[800]+
|
||||
|
||||
[[tabs-wrap]]
|
||||
=== wrap
|
||||
@@ -954,12 +1028,12 @@ The position of the tab bar.
|
||||
|
||||
Valid values:
|
||||
|
||||
* +north+
|
||||
* +south+
|
||||
* +east+
|
||||
* +west+
|
||||
* +top+
|
||||
* +bottom+
|
||||
* +left+
|
||||
* +right+
|
||||
|
||||
Default: +pass:[north]+
|
||||
Default: +pass:[top]+
|
||||
|
||||
[[tabs-show-favicons]]
|
||||
=== show-favicons
|
||||
@@ -984,12 +1058,6 @@ Width of the progress indicator (0 to disable).
|
||||
|
||||
Default: +pass:[3]+
|
||||
|
||||
[[tabs-indicator-space]]
|
||||
=== indicator-space
|
||||
Spacing between tab edge and indicator.
|
||||
|
||||
Default: +pass:[3]+
|
||||
|
||||
[[tabs-tabs-are-windows]]
|
||||
=== tabs-are-windows
|
||||
Whether to open windows instead of tabs.
|
||||
@@ -1014,6 +1082,29 @@ The format to use for the tab title. The following placeholders are defined:
|
||||
|
||||
Default: +pass:[{index}: {title}]+
|
||||
|
||||
[[tabs-mousewheel-tab-switching]]
|
||||
=== mousewheel-tab-switching
|
||||
Switch between tabs using the mouse wheel.
|
||||
|
||||
Valid values:
|
||||
|
||||
* +true+
|
||||
* +false+
|
||||
|
||||
Default: +pass:[true]+
|
||||
|
||||
[[tabs-padding]]
|
||||
=== padding
|
||||
Padding for tabs (top, bottom, left, right).
|
||||
|
||||
Default: +pass:[0,0,5,5]+
|
||||
|
||||
[[tabs-indicator-padding]]
|
||||
=== indicator-padding
|
||||
Padding for indicators (top, bottom, left, right).
|
||||
|
||||
Default: +pass:[2,2,0,4]+
|
||||
|
||||
== storage
|
||||
Settings related to cache and storage.
|
||||
|
||||
@@ -1023,6 +1114,29 @@ The directory to save downloads to. An empty value selects a sensible os-specifi
|
||||
|
||||
Default: empty
|
||||
|
||||
[[storage-prompt-download-directory]]
|
||||
=== prompt-download-directory
|
||||
Whether to prompt the user for the download location.
|
||||
If set to false, 'download-directory' will be used.
|
||||
|
||||
Valid values:
|
||||
|
||||
* +true+
|
||||
* +false+
|
||||
|
||||
Default: +pass:[true]+
|
||||
|
||||
[[storage-remember-download-directory]]
|
||||
=== remember-download-directory
|
||||
Whether to remember the last used download directory.
|
||||
|
||||
Valid values:
|
||||
|
||||
* +true+
|
||||
* +false+
|
||||
|
||||
Default: +pass:[true]+
|
||||
|
||||
[[storage-maximum-pages-in-cache]]
|
||||
=== maximum-pages-in-cache
|
||||
The maximum number of pages to hold in the global memory page cache.
|
||||
@@ -1138,12 +1252,46 @@ Valid values:
|
||||
|
||||
Default: +pass:[false]+
|
||||
|
||||
[[content-webgl]]
|
||||
=== webgl
|
||||
Enables or disables WebGL.
|
||||
|
||||
Valid values:
|
||||
|
||||
* +true+
|
||||
* +false+
|
||||
|
||||
Default: +pass:[true]+
|
||||
|
||||
[[content-css-regions]]
|
||||
=== css-regions
|
||||
Enable or disable support for CSS regions.
|
||||
|
||||
Valid values:
|
||||
|
||||
* +true+
|
||||
* +false+
|
||||
|
||||
Default: +pass:[true]+
|
||||
|
||||
[[content-hyperlink-auditing]]
|
||||
=== hyperlink-auditing
|
||||
Enable or disable hyperlink auditing (<a ping>).
|
||||
|
||||
Valid values:
|
||||
|
||||
* +true+
|
||||
* +false+
|
||||
|
||||
Default: +pass:[false]+
|
||||
|
||||
[[content-geolocation]]
|
||||
=== geolocation
|
||||
Allow websites to request geolocations.
|
||||
|
||||
Valid values:
|
||||
|
||||
* +true+
|
||||
* +false+
|
||||
* +ask+
|
||||
|
||||
@@ -1155,6 +1303,7 @@ Allow websites to show notifications.
|
||||
|
||||
Valid values:
|
||||
|
||||
* +true+
|
||||
* +false+
|
||||
* +ask+
|
||||
|
||||
@@ -1239,14 +1388,16 @@ Default: +pass:[true]+
|
||||
|
||||
[[content-cookies-accept]]
|
||||
=== cookies-accept
|
||||
Whether to accept cookies.
|
||||
Control which cookies to accept.
|
||||
|
||||
Valid values:
|
||||
|
||||
* +default+: Default QtWebKit behavior.
|
||||
* +all+: Accept all cookies.
|
||||
* +no-3rdparty+: Accept cookies from the same origin only.
|
||||
* +no-unknown-3rdparty+: Accept cookies from the same origin only, unless a cookie is already set for the domain.
|
||||
* +never+: Don't accept cookies at all.
|
||||
|
||||
Default: +pass:[default]+
|
||||
Default: +pass:[no-3rdparty]+
|
||||
|
||||
[[content-cookies-store]]
|
||||
=== cookies-store
|
||||
@@ -1357,7 +1508,7 @@ Default: +pass:[true]+
|
||||
=== next-regexes
|
||||
A comma-separated list of regexes to use for 'next' links.
|
||||
|
||||
Default: +pass:[\bnext\b,\bmore\b,\bnewer\b,\b[>→≫]\b,\b(>>|»)\b]+
|
||||
Default: +pass:[\bnext\b,\bmore\b,\bnewer\b,\b[>→≫]\b,\b(>>|»)\b,\bcontinue\b]+
|
||||
|
||||
[[hints-prev-regexes]]
|
||||
=== prev-regexes
|
||||
@@ -1384,7 +1535,9 @@ A value can be in one of the following format:
|
||||
* transparent (no color)
|
||||
* `rgb(r, g, b)` / `rgba(r, g, b, a)` (values 0-255 or percentages)
|
||||
* `hsv(h, s, v)` / `hsva(h, s, v, a)` (values 0-255, hue 0-359)
|
||||
* A gradient as explained in http://qt-project.org/doc/qt-4.8/stylesheet-reference.html#list-of-property-types[the Qt documentation] under ``Gradient''.
|
||||
* A gradient as explained in http://doc.qt.io/qt-5/stylesheet-reference.html#list-of-property-types[the Qt documentation] under ``Gradient''.
|
||||
|
||||
A *.system value determines the color system to use for color interpolation between similarly-named *.start and *.stop entries, regardless of how they are defined in the options. Valid values are 'rgb', 'hsv', and 'hsl'.
|
||||
|
||||
The `hints.*` values are a special case as they're real CSS colors, not Qt-CSS colors. There, for a gradient, you need to use `-webkit-gradient`, see https://www.webkit.org/blog/175/introducing-css-gradients/[the WebKit documentation].
|
||||
|
||||
@@ -1460,17 +1613,23 @@ Foreground color of the matched text in the completion.
|
||||
|
||||
Default: +pass:[#ff4444]+
|
||||
|
||||
[[colors-statusbar.fg]]
|
||||
=== statusbar.fg
|
||||
Foreground color of the statusbar.
|
||||
|
||||
Default: +pass:[white]+
|
||||
|
||||
[[colors-statusbar.bg]]
|
||||
=== statusbar.bg
|
||||
Foreground color of the statusbar.
|
||||
|
||||
Default: +pass:[black]+
|
||||
|
||||
[[colors-statusbar.fg]]
|
||||
=== statusbar.fg
|
||||
Foreground color of the statusbar.
|
||||
[[colors-statusbar.fg.error]]
|
||||
=== statusbar.fg.error
|
||||
Foreground color of the statusbar if there was an error.
|
||||
|
||||
Default: +pass:[white]+
|
||||
Default: +pass:[${statusbar.fg}]+
|
||||
|
||||
[[colors-statusbar.bg.error]]
|
||||
=== statusbar.bg.error
|
||||
@@ -1478,24 +1637,78 @@ Background color of the statusbar if there was an error.
|
||||
|
||||
Default: +pass:[red]+
|
||||
|
||||
[[colors-statusbar.fg.warning]]
|
||||
=== statusbar.fg.warning
|
||||
Foreground color of the statusbar if there is a warning.
|
||||
|
||||
Default: +pass:[${statusbar.fg}]+
|
||||
|
||||
[[colors-statusbar.bg.warning]]
|
||||
=== statusbar.bg.warning
|
||||
Background color of the statusbar if there is a warning.
|
||||
|
||||
Default: +pass:[darkorange]+
|
||||
|
||||
[[colors-statusbar.fg.prompt]]
|
||||
=== statusbar.fg.prompt
|
||||
Foreground color of the statusbar if there is a prompt.
|
||||
|
||||
Default: +pass:[${statusbar.fg}]+
|
||||
|
||||
[[colors-statusbar.bg.prompt]]
|
||||
=== statusbar.bg.prompt
|
||||
Background color of the statusbar if there is a prompt.
|
||||
|
||||
Default: +pass:[darkblue]+
|
||||
|
||||
[[colors-statusbar.fg.insert]]
|
||||
=== statusbar.fg.insert
|
||||
Foreground color of the statusbar in insert mode.
|
||||
|
||||
Default: +pass:[${statusbar.fg}]+
|
||||
|
||||
[[colors-statusbar.bg.insert]]
|
||||
=== statusbar.bg.insert
|
||||
Background color of the statusbar in insert mode.
|
||||
|
||||
Default: +pass:[darkgreen]+
|
||||
|
||||
[[colors-statusbar.fg.command]]
|
||||
=== statusbar.fg.command
|
||||
Foreground color of the statusbar in command mode.
|
||||
|
||||
Default: +pass:[${statusbar.fg}]+
|
||||
|
||||
[[colors-statusbar.bg.command]]
|
||||
=== statusbar.bg.command
|
||||
Background color of the statusbar in command mode.
|
||||
|
||||
Default: +pass:[${statusbar.bg}]+
|
||||
|
||||
[[colors-statusbar.fg.caret]]
|
||||
=== statusbar.fg.caret
|
||||
Foreground color of the statusbar in caret mode.
|
||||
|
||||
Default: +pass:[${statusbar.fg}]+
|
||||
|
||||
[[colors-statusbar.bg.caret]]
|
||||
=== statusbar.bg.caret
|
||||
Background color of the statusbar in caret mode.
|
||||
|
||||
Default: +pass:[purple]+
|
||||
|
||||
[[colors-statusbar.fg.caret-selection]]
|
||||
=== statusbar.fg.caret-selection
|
||||
Foreground color of the statusbar in caret mode with a selection
|
||||
|
||||
Default: +pass:[${statusbar.fg}]+
|
||||
|
||||
[[colors-statusbar.bg.caret-selection]]
|
||||
=== statusbar.bg.caret-selection
|
||||
Background color of the statusbar in caret mode with a selection
|
||||
|
||||
Default: +pass:[#a12dff]+
|
||||
|
||||
[[colors-statusbar.progress.bg]]
|
||||
=== statusbar.progress.bg
|
||||
Background color of the progress bar.
|
||||
@@ -1538,30 +1751,30 @@ Foreground color of unselected odd tabs.
|
||||
|
||||
Default: +pass:[white]+
|
||||
|
||||
[[colors-tabs.fg.even]]
|
||||
=== tabs.fg.even
|
||||
Foreground color of unselected even tabs.
|
||||
|
||||
Default: +pass:[white]+
|
||||
|
||||
[[colors-tabs.fg.selected]]
|
||||
=== tabs.fg.selected
|
||||
Foreground color of selected tabs.
|
||||
|
||||
Default: +pass:[white]+
|
||||
|
||||
[[colors-tabs.bg.odd]]
|
||||
=== tabs.bg.odd
|
||||
Background color of unselected odd tabs.
|
||||
|
||||
Default: +pass:[grey]+
|
||||
|
||||
[[colors-tabs.fg.even]]
|
||||
=== tabs.fg.even
|
||||
Foreground color of unselected even tabs.
|
||||
|
||||
Default: +pass:[white]+
|
||||
|
||||
[[colors-tabs.bg.even]]
|
||||
=== tabs.bg.even
|
||||
Background color of unselected even tabs.
|
||||
|
||||
Default: +pass:[darkgrey]+
|
||||
|
||||
[[colors-tabs.fg.selected]]
|
||||
=== tabs.fg.selected
|
||||
Foreground color of selected tabs.
|
||||
|
||||
Default: +pass:[white]+
|
||||
|
||||
[[colors-tabs.bg.selected]]
|
||||
=== tabs.bg.selected
|
||||
Background color of selected tabs.
|
||||
@@ -1610,23 +1823,17 @@ Font color for hints.
|
||||
|
||||
Default: +pass:[black]+
|
||||
|
||||
[[colors-hints.fg.match]]
|
||||
=== hints.fg.match
|
||||
Font color for the matched part of hints.
|
||||
|
||||
Default: +pass:[green]+
|
||||
|
||||
[[colors-hints.bg]]
|
||||
=== hints.bg
|
||||
Background color for hints.
|
||||
|
||||
Default: +pass:[-webkit-gradient(linear, left top, left bottom, color-stop(0%,#FFF785), color-stop(100%,#FFC542))]+
|
||||
|
||||
[[colors-downloads.fg]]
|
||||
=== downloads.fg
|
||||
Foreground color for downloads.
|
||||
[[colors-hints.fg.match]]
|
||||
=== hints.fg.match
|
||||
Font color for the matched part of hints.
|
||||
|
||||
Default: +pass:[#ffffff]+
|
||||
Default: +pass:[green]+
|
||||
|
||||
[[colors-downloads.bg.bar]]
|
||||
=== downloads.bg.bar
|
||||
@@ -1634,21 +1841,33 @@ Background color for the download bar.
|
||||
|
||||
Default: +pass:[black]+
|
||||
|
||||
[[colors-downloads.fg.start]]
|
||||
=== downloads.fg.start
|
||||
Color gradient start for download text.
|
||||
|
||||
Default: +pass:[white]+
|
||||
|
||||
[[colors-downloads.bg.start]]
|
||||
=== downloads.bg.start
|
||||
Color gradient start for downloads.
|
||||
Color gradient start for download backgrounds.
|
||||
|
||||
Default: +pass:[#0000aa]+
|
||||
|
||||
[[colors-downloads.fg.stop]]
|
||||
=== downloads.fg.stop
|
||||
Color gradient end for download text.
|
||||
|
||||
Default: +pass:[${downloads.fg.start}]+
|
||||
|
||||
[[colors-downloads.bg.stop]]
|
||||
=== downloads.bg.stop
|
||||
Color gradient end for downloads.
|
||||
Color gradient stop for download backgrounds.
|
||||
|
||||
Default: +pass:[#00aa00]+
|
||||
|
||||
[[colors-downloads.bg.system]]
|
||||
=== downloads.bg.system
|
||||
Color gradient interpolation system for downloads.
|
||||
[[colors-downloads.fg.system]]
|
||||
=== downloads.fg.system
|
||||
Color gradient interpolation system for download text.
|
||||
|
||||
Valid values:
|
||||
|
||||
@@ -1658,12 +1877,36 @@ Valid values:
|
||||
|
||||
Default: +pass:[rgb]+
|
||||
|
||||
[[colors-downloads.bg.system]]
|
||||
=== downloads.bg.system
|
||||
Color gradient interpolation system for download backgrounds.
|
||||
|
||||
Valid values:
|
||||
|
||||
* +rgb+: Interpolate in the RGB color system.
|
||||
* +hsv+: Interpolate in the HSV color system.
|
||||
* +hsl+: Interpolate in the HSL color system.
|
||||
|
||||
Default: +pass:[rgb]+
|
||||
|
||||
[[colors-downloads.fg.error]]
|
||||
=== downloads.fg.error
|
||||
Foreground color for downloads with errors.
|
||||
|
||||
Default: +pass:[white]+
|
||||
|
||||
[[colors-downloads.bg.error]]
|
||||
=== downloads.bg.error
|
||||
Background color for downloads with errors.
|
||||
|
||||
Default: +pass:[red]+
|
||||
|
||||
[[colors-webpage.bg]]
|
||||
=== webpage.bg
|
||||
Background color for webpages if unset (or empty to use the theme's color)
|
||||
|
||||
Default: +pass:[white]+
|
||||
|
||||
== fonts
|
||||
Fonts used for the UI, with optional style/weight/size.
|
||||
|
||||
@@ -1705,7 +1948,7 @@ Default: +pass:[8pt ${_monospace}]+
|
||||
=== hints
|
||||
Font used for the hints.
|
||||
|
||||
Default: +pass:[bold 12px Monospace]+
|
||||
Default: +pass:[bold 13px Monospace]+
|
||||
|
||||
[[fonts-debug-console]]
|
||||
=== debug-console
|
||||
|
||||
@@ -11,6 +11,7 @@ What to do now
|
||||
* View the 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.
|
||||
* If you just cloned the repository, you'll need to run
|
||||
`scripts/asciidoc2html.py` to generate the documentation.
|
||||
* Go to the link:qute://settings[settings page] to set up qutebrowser the way you want it.
|
||||
|
||||
@@ -16,7 +16,7 @@ qutebrowser - a keyboard-driven, vim-like browser based on PyQt5 and QtWebKit.
|
||||
*qutebrowser* ['-OPTION' ['...']] [':COMMAND' ['...']] ['URL' ['...']]
|
||||
|
||||
== DESCRIPTION
|
||||
qutebrowser is a keyboard-focused browser with with a minimal GUI. It's based
|
||||
qutebrowser is a keyboard-focused browser with a minimal GUI. It's based
|
||||
on Python, PyQt5 and QtWebKit and free software, licensed under the GPL.
|
||||
|
||||
It was inspired by other browsers/addons like dwb and Vimperator/Pentadactyl.
|
||||
@@ -41,6 +41,15 @@ show it.
|
||||
*-c* 'CONFDIR', *--confdir* 'CONFDIR'::
|
||||
Set config directory (empty for no config storage).
|
||||
|
||||
*--datadir* 'DATADIR'::
|
||||
Set data directory (empty for no data storage).
|
||||
|
||||
*--cachedir* 'CACHEDIR'::
|
||||
Set cache directory (empty for no cache storage).
|
||||
|
||||
*--basedir* 'BASEDIR'::
|
||||
Base directory for all storage. Other --*dir arguments are ignored if this is given.
|
||||
|
||||
*-V*, *--version*::
|
||||
Show version and quit.
|
||||
|
||||
@@ -81,12 +90,15 @@ show it.
|
||||
*--debug-exit*::
|
||||
Turn on debugging of late exit.
|
||||
|
||||
*--no-crash-dialog*::
|
||||
Don't show a crash dialog.
|
||||
|
||||
*--pdb-postmortem*::
|
||||
Drop into pdb on exceptions.
|
||||
|
||||
*--temp-basedir*::
|
||||
Use a temporary basedir.
|
||||
|
||||
*--no-err-windows*::
|
||||
Don't show any error windows (used for tests/smoke.py).
|
||||
|
||||
*--qt-name* 'NAME'::
|
||||
Set the window name.
|
||||
|
||||
|
||||
@@ -5,9 +5,9 @@ The Compiler <mail@qutebrowser.org>
|
||||
qutebrowser is extensible by writing userscripts which can be called via the
|
||||
`:spawn --userscript` command, or via a key binding.
|
||||
|
||||
These userscripts are similiar to the (non-javascript) dwb userscripts. They
|
||||
can be written in any language which can read environment variables and write
|
||||
to a FIFO. Note they are *not* related to Greasemonkey userscripts.
|
||||
These userscripts are similar to the (non-javascript) dwb userscripts. They can
|
||||
be written in any language which can read environment variables and write to a
|
||||
FIFO. Note they are *not* related to Greasemonkey userscripts.
|
||||
|
||||
Note for simple things such as opening the current page with another browser or
|
||||
mpv, a simple key binding to something like `:spawn mpv {url}` should suffice.
|
||||
@@ -18,14 +18,14 @@ qutebrowser to run them.
|
||||
Getting information
|
||||
-------------------
|
||||
|
||||
The following environment variables will be set when an userscript is launched:
|
||||
The following environment variables will be set when a userscript is launched:
|
||||
|
||||
- `QUTE_MODE`: Either `hints` (started via hints) or `command` (started via
|
||||
command or key binding).
|
||||
- `QUTE_USER_AGENT`: The currently set user agent.
|
||||
- `QUTE_FIFO`: The FIFO or file to write commands to.
|
||||
- `QUTE_HTML`: The HTML source of the current page.
|
||||
- `QUTE_TEXT`: The plaintext of the current page.
|
||||
- `QUTE_HTML`: Path of a file containing the HTML source of the current page.
|
||||
- `QUTE_TEXT`: Path of a file containing the plaintext of the current page.
|
||||
|
||||
In `command` mode:
|
||||
|
||||
|
||||
@@ -29,6 +29,8 @@ profile qutebrowser /usr/{local/,}bin/qutebrowser {
|
||||
|
||||
/proc/*/mounts r,
|
||||
owner /tmp/** rwkl,
|
||||
owner /run/user/*/ rw,
|
||||
owner /run/user/*/** krw,
|
||||
|
||||
@{HOME}/.config/qutebrowser/** krw,
|
||||
@{HOME}/.local/share/qutebrowser/** krw,
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
height="640"
|
||||
id="svg2"
|
||||
sodipodi:version="0.32"
|
||||
inkscape:version="0.91 r13725"
|
||||
inkscape:version="0.48.5 r10040"
|
||||
version="1.0"
|
||||
sodipodi:docname="cheatsheet.svg"
|
||||
inkscape:output_extension="org.inkscape.output.svg.inkscape"
|
||||
@@ -33,16 +33,16 @@
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.8791156"
|
||||
inkscape:cx="327.65084"
|
||||
inkscape:cy="233.0095"
|
||||
inkscape:cx="768.67127"
|
||||
inkscape:cy="133.80749"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
width="1024px"
|
||||
height="640px"
|
||||
showgrid="false"
|
||||
inkscape:window-width="1366"
|
||||
inkscape:window-height="768"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-width="636"
|
||||
inkscape:window-height="536"
|
||||
inkscape:window-x="2560"
|
||||
inkscape:window-y="0"
|
||||
showguides="true"
|
||||
inkscape:guide-bbox="true"
|
||||
@@ -1939,7 +1939,7 @@
|
||||
x="542.06946"
|
||||
sodipodi:role="line"
|
||||
id="tspan4938"
|
||||
style="font-size:8px">scoll</tspan><tspan
|
||||
style="font-size:8px">scroll</tspan><tspan
|
||||
y="276.1955"
|
||||
x="542.06946"
|
||||
sodipodi:role="line"
|
||||
@@ -2999,6 +2999,8 @@
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara3626-73">;b - open hint in background tab</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara4051">;f - open hint in foreground tab</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara3788">;h - hover over hint (mouse-over)</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara3790">;i - hint images</flowPara><flowPara
|
||||
@@ -3324,27 +3326,15 @@
|
||||
style="font-size:8px">tab</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:8px;line-height:89.99999762%;font-family:TlwgTypewriter;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none"
|
||||
x="267.67316"
|
||||
y="326.20523"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:8px;line-height:89.99999762%;font-family:TlwgTypewriter;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#ff0000;fill-opacity:1;stroke:none"
|
||||
x="274.21381"
|
||||
y="343.17578"
|
||||
id="text10547-23-6-7"
|
||||
sodipodi:linespacing="89.999998%"><tspan
|
||||
sodipodi:role="line"
|
||||
x="267.67316"
|
||||
y="326.20523"
|
||||
id="tspan10560-1-3-1" /><tspan
|
||||
sodipodi:role="line"
|
||||
x="267.67316"
|
||||
y="333.40524"
|
||||
id="tspan5325">co: close</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="267.67316"
|
||||
y="340.60522"
|
||||
id="tspan10562-12-5-98">other tabs</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="267.67316"
|
||||
y="347.80524"
|
||||
id="tspan4045">cd: clea</tspan></text>
|
||||
x="274.21381"
|
||||
y="343.17578"
|
||||
id="tspan4052">(10)</tspan></text>
|
||||
<text
|
||||
sodipodi:linespacing="89.999998%"
|
||||
id="text10564-6-7-8-0"
|
||||
@@ -3469,5 +3459,20 @@
|
||||
y="177.63554"
|
||||
style="font-size:8px"
|
||||
id="tspan3719">cache)</tspan></text>
|
||||
<text
|
||||
sodipodi:linespacing="89.999998%"
|
||||
id="text9514-60-7-7-0-8"
|
||||
y="338.04874"
|
||||
x="342.42523"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:8px;line-height:89.99999762%;font-family:TlwgTypewriter;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none"
|
||||
xml:space="preserve"><tspan
|
||||
y="338.04874"
|
||||
x="342.42523"
|
||||
sodipodi:role="line"
|
||||
id="tspan5689-6">visual</tspan><tspan
|
||||
y="345.24875"
|
||||
x="342.42523"
|
||||
sodipodi:role="line"
|
||||
id="tspan4112">mode</tspan></text>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 137 KiB |
48
misc/userscripts/dmenu_qutebrowser
Executable file
48
misc/userscripts/dmenu_qutebrowser
Executable file
@@ -0,0 +1,48 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Copyright 2015 Zach-Button <zachrey.button@gmail.com>
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
# Pipes history, quickmarks, and URL into dmenu.
|
||||
#
|
||||
# If run from qutebrowser as a userscript, it runs :open on the URL
|
||||
# If not, it opens a new qutebrowser window at the URL
|
||||
#
|
||||
# Ideal for use with tabs-are-windows. Set a hotkey to launch this script, then:
|
||||
# :bind o spawn --userscript dmenu_qutebrowser
|
||||
#
|
||||
# Use the hotkey to open in new tab/window, press 'o' to open URL in current tab/window
|
||||
# You can simulate "go" by pressing "o<tab>", as the current URL is always first in the list
|
||||
#
|
||||
# I personally use "<Mod4>o" to launch this script. For me, my workflow is:
|
||||
# Default keys Keys with this script
|
||||
# O <Mod4>o
|
||||
# o o
|
||||
# go o<Tab>
|
||||
# gO gC, then o<Tab>
|
||||
# (This is unnecessarily long. I use this rarely, feel free to make this script accept parameters.)
|
||||
#
|
||||
|
||||
[ -z "$QUTE_URL" ] && QUTE_URL='http://google.com'
|
||||
|
||||
url=$(echo "$QUTE_URL" | cat - ~/.config/qutebrowser/quickmarks ~/.local/share/qutebrowser/history | dmenu -l 15 -p qutebrowser)
|
||||
url=$(echo $url | sed -E 's/[^ ]+ +//g' | egrep "https?:" || echo $url)
|
||||
|
||||
[ -z "${url// }" ] && exit
|
||||
|
||||
echo "open $url" >> "$QUTE_FIFO" || qutebrowser "$url"
|
||||
|
||||
38
misc/userscripts/openfeeds
Executable file
38
misc/userscripts/openfeeds
Executable file
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2015 jnphilipp <me@jnphilipp.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/>.
|
||||
|
||||
# Opens all links to feeds defined in the head of a site
|
||||
#
|
||||
# Ideal for use with tabs-are-windows. Set a hotkey to launch this script, then:
|
||||
# :bind gF spawn --userscript openfeeds
|
||||
#
|
||||
# Use the hotkey to open the feeds in new tab/window, press 'gF' to open
|
||||
#
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
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'))
|
||||
32
misc/userscripts/qutebrowser_viewsource
Executable file
32
misc/userscripts/qutebrowser_viewsource
Executable file
@@ -0,0 +1,32 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Copyright 2015 Zach-Button <zachrey.button@gmail.com>
|
||||
#
|
||||
# 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 script fetches the unprocessed HTML source for a page and opens it in vim.
|
||||
# :bind gf spawn --userscript qutebrowser_viewsource
|
||||
#
|
||||
# Caveat: Does not use authentication of any kind. Add it in if you want it to.
|
||||
#
|
||||
|
||||
path=/tmp/qutebrowser_$(mktemp XXXXXXXX).html
|
||||
|
||||
curl "$QUTE_URL" > $path
|
||||
urxvt -e vim "$path"
|
||||
|
||||
rm "$path"
|
||||
32
pytest.ini
Normal file
32
pytest.ini
Normal file
@@ -0,0 +1,32 @@
|
||||
[pytest]
|
||||
norecursedirs = .tox .venv
|
||||
markers =
|
||||
gui: Tests using the GUI (e.g. spawning widgets)
|
||||
posix: Tests which only can run on a POSIX OS.
|
||||
windows: Tests which only can run on Windows.
|
||||
linux: Tests which only can run on Linux.
|
||||
osx: Tests which only can run on OS X.
|
||||
not_frozen: Tests which can't be run if sys.frozen is True.
|
||||
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
|
||||
qt_log_level_fail = WARNING
|
||||
qt_log_ignore =
|
||||
^SpellCheck: .*
|
||||
^SetProcessDpiAwareness failed: .*
|
||||
^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
|
||||
^virtual void QSslSocketBackendPrivate::transmit\(\) SSLRead failed with: -9805
|
||||
@@ -28,7 +28,7 @@ __copyright__ = "Copyright 2014-2015 Florian Bruhin (The Compiler)"
|
||||
__license__ = "GPL"
|
||||
__maintainer__ = __author__
|
||||
__email__ = "mail@qutebrowser.org"
|
||||
__version_info__ = (0, 2, 0)
|
||||
__version_info__ = (0, 4, 1)
|
||||
__version__ = '.'.join(map(str, __version_info__))
|
||||
__description__ = "A keyboard-driven, vim-like browser based on PyQt5 and QtWebKit."
|
||||
|
||||
|
||||
1205
qutebrowser/app.py
1205
qutebrowser/app.py
File diff suppressed because it is too large
Load Diff
@@ -27,7 +27,7 @@ import zipfile
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import objreg, standarddir, log, message
|
||||
from qutebrowser.commands import cmdutils
|
||||
from qutebrowser.commands import cmdutils, cmdexc
|
||||
|
||||
|
||||
def guess_zip_filename(zf):
|
||||
@@ -90,12 +90,18 @@ class HostBlocker:
|
||||
self.blocked_hosts = set()
|
||||
self._in_progress = []
|
||||
self._done_count = 0
|
||||
self._hosts_file = os.path.join(standarddir.data(), 'blocked-hosts')
|
||||
data_dir = standarddir.data()
|
||||
if data_dir is None:
|
||||
self._hosts_file = None
|
||||
else:
|
||||
self._hosts_file = os.path.join(data_dir, 'blocked-hosts')
|
||||
objreg.get('config').changed.connect(self.on_config_changed)
|
||||
|
||||
def read_hosts(self):
|
||||
"""Read hosts from the existing blocked-hosts file."""
|
||||
self.blocked_hosts = set()
|
||||
if self._hosts_file is None:
|
||||
return
|
||||
if os.path.exists(self._hosts_file):
|
||||
try:
|
||||
with open(self._hosts_file, 'r', encoding='utf-8') as f:
|
||||
@@ -104,13 +110,17 @@ class HostBlocker:
|
||||
except OSError:
|
||||
log.misc.exception("Failed to read host blocklist!")
|
||||
else:
|
||||
if config.get('content', 'host-block-lists') is not None:
|
||||
args = objreg.get('args')
|
||||
if (config.get('content', 'host-block-lists') is not None and
|
||||
args.basedir is None):
|
||||
message.info('current',
|
||||
"Run :adblock-update to get adblock lists.")
|
||||
|
||||
@cmdutils.register(instance='host-blocker')
|
||||
def adblock_update(self, win_id: {'special': 'win_id'}):
|
||||
@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:
|
||||
raise cmdexc.CommandError("No data storage is configured!")
|
||||
self.blocked_hosts = set()
|
||||
self._done_count = 0
|
||||
urls = config.get('content', 'host-block-lists')
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
import os.path
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot
|
||||
from PyQt5.QtNetwork import QNetworkDiskCache, QNetworkCacheMetaData
|
||||
|
||||
from qutebrowser.config import config
|
||||
@@ -29,23 +30,41 @@ from qutebrowser.utils import utils, standarddir, objreg
|
||||
|
||||
class DiskCache(QNetworkDiskCache):
|
||||
|
||||
"""Disk cache which sets correct cache dir and size."""
|
||||
"""Disk cache which sets correct cache dir and size.
|
||||
|
||||
Attributes:
|
||||
_activated: Whether the cache should be used.
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setCacheDirectory(os.path.join(standarddir.cache(), 'http'))
|
||||
cache_dir = standarddir.cache()
|
||||
if config.get('general', 'private-browsing') or cache_dir is None:
|
||||
self._activated = False
|
||||
else:
|
||||
self._activated = True
|
||||
self.setCacheDirectory(os.path.join(standarddir.cache(), 'http'))
|
||||
self.setMaximumCacheSize(config.get('storage', 'cache-size'))
|
||||
objreg.get('config').changed.connect(self.cache_size_changed)
|
||||
objreg.get('config').changed.connect(self.on_config_changed)
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self, size=self.cacheSize(),
|
||||
maxsize=self.maximumCacheSize(),
|
||||
path=self.cacheDirectory())
|
||||
|
||||
@config.change_filter('storage', 'cache-size')
|
||||
def cache_size_changed(self):
|
||||
"""Update cache size if the config was changed."""
|
||||
self.setMaximumCacheSize(config.get('storage', 'cache-size'))
|
||||
@pyqtSlot(str, str)
|
||||
def on_config_changed(self, section, option):
|
||||
"""Update cache size/activated if the config was changed."""
|
||||
if (section, option) == ('storage', 'cache-size'):
|
||||
self.setMaximumCacheSize(config.get('storage', 'cache-size'))
|
||||
elif (section, option) == ('general', 'private-browsing'):
|
||||
if (config.get('general', 'private-browsing') or
|
||||
standarddir.cache() is None):
|
||||
self._activated = False
|
||||
else:
|
||||
self._activated = True
|
||||
self.setCacheDirectory(
|
||||
os.path.join(standarddir.cache(), 'http'))
|
||||
|
||||
def cacheSize(self):
|
||||
"""Return the current size taken up by the cache.
|
||||
@@ -53,10 +72,10 @@ class DiskCache(QNetworkDiskCache):
|
||||
Return:
|
||||
An int.
|
||||
"""
|
||||
if config.get('general', 'private-browsing'):
|
||||
return 0
|
||||
else:
|
||||
if self._activated:
|
||||
return super().cacheSize()
|
||||
else:
|
||||
return 0
|
||||
|
||||
def fileMetaData(self, filename):
|
||||
"""Return the QNetworkCacheMetaData for the cache file filename.
|
||||
@@ -67,10 +86,10 @@ class DiskCache(QNetworkDiskCache):
|
||||
Return:
|
||||
A QNetworkCacheMetaData object.
|
||||
"""
|
||||
if config.get('general', 'private-browsing'):
|
||||
return QNetworkCacheMetaData()
|
||||
else:
|
||||
if self._activated:
|
||||
return super().fileMetaData(filename)
|
||||
else:
|
||||
return QNetworkCacheMetaData()
|
||||
|
||||
def data(self, url):
|
||||
"""Return the data associated with url.
|
||||
@@ -81,10 +100,10 @@ class DiskCache(QNetworkDiskCache):
|
||||
return:
|
||||
A QIODevice or None.
|
||||
"""
|
||||
if config.get('general', 'private-browsing'):
|
||||
return None
|
||||
else:
|
||||
if self._activated:
|
||||
return super().data(url)
|
||||
else:
|
||||
return None
|
||||
|
||||
def insert(self, device):
|
||||
"""Insert the data in device and the prepared meta data into the cache.
|
||||
@@ -92,10 +111,10 @@ class DiskCache(QNetworkDiskCache):
|
||||
Args:
|
||||
device: A QIODevice.
|
||||
"""
|
||||
if config.get('general', 'private-browsing'):
|
||||
return
|
||||
else:
|
||||
if self._activated:
|
||||
super().insert(device)
|
||||
else:
|
||||
return None
|
||||
|
||||
def metaData(self, url):
|
||||
"""Return the meta data for the url url.
|
||||
@@ -106,10 +125,10 @@ class DiskCache(QNetworkDiskCache):
|
||||
Return:
|
||||
A QNetworkCacheMetaData object.
|
||||
"""
|
||||
if config.get('general', 'private-browsing'):
|
||||
return QNetworkCacheMetaData()
|
||||
else:
|
||||
if self._activated:
|
||||
return super().metaData(url)
|
||||
else:
|
||||
return QNetworkCacheMetaData()
|
||||
|
||||
def prepare(self, meta_data):
|
||||
"""Return the device that should be populated with the data.
|
||||
@@ -120,10 +139,10 @@ class DiskCache(QNetworkDiskCache):
|
||||
Return:
|
||||
A QIODevice or None.
|
||||
"""
|
||||
if config.get('general', 'private-browsing'):
|
||||
return None
|
||||
else:
|
||||
if self._activated:
|
||||
return super().prepare(meta_data)
|
||||
else:
|
||||
return None
|
||||
|
||||
def remove(self, url):
|
||||
"""Remove the cache entry for url.
|
||||
@@ -131,10 +150,10 @@ class DiskCache(QNetworkDiskCache):
|
||||
Return:
|
||||
True on success, False otherwise.
|
||||
"""
|
||||
if config.get('general', 'private-browsing'):
|
||||
return False
|
||||
else:
|
||||
if self._activated:
|
||||
return super().remove(url)
|
||||
else:
|
||||
return False
|
||||
|
||||
def updateMetaData(self, meta_data):
|
||||
"""Update the cache meta date for the meta_data's url to meta_data.
|
||||
@@ -142,14 +161,14 @@ class DiskCache(QNetworkDiskCache):
|
||||
Args:
|
||||
meta_data: A QNetworkCacheMetaData object.
|
||||
"""
|
||||
if config.get('general', 'private-browsing'):
|
||||
return
|
||||
else:
|
||||
if self._activated:
|
||||
super().updateMetaData(meta_data)
|
||||
else:
|
||||
return
|
||||
|
||||
def clear(self):
|
||||
"""Remove all items from the cache."""
|
||||
if config.get('general', 'private-browsing'):
|
||||
return
|
||||
else:
|
||||
if self._activated:
|
||||
super().clear()
|
||||
else:
|
||||
return
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -84,7 +84,7 @@ class CookieJar(RAMCookieJar):
|
||||
def purge_old_cookies(self):
|
||||
"""Purge expired cookies from the cookie jar."""
|
||||
# Based on:
|
||||
# http://qt-project.org/doc/qt-5/qtwebkitexamples-webkitwidgets-browser-cookiejar-cpp.html
|
||||
# http://doc.qt.io/qt-5/qtwebkitexamples-webkitwidgets-browser-cookiejar-cpp.html
|
||||
now = QDateTime.currentDateTime()
|
||||
cookies = [c for c in self.allCookies()
|
||||
if c.isSessionCookie() or c.expirationDate() >= now]
|
||||
|
||||
@@ -48,13 +48,26 @@ ModelRole = usertypes.enum('ModelRole', ['item'], start=Qt.UserRole,
|
||||
|
||||
RetryInfo = collections.namedtuple('RetryInfo', ['request', 'manager'])
|
||||
|
||||
# Remember the last used directory
|
||||
_last_used_directory = None
|
||||
|
||||
|
||||
# All REFRESH_INTERVAL milliseconds, speeds will be recalculated and downloads
|
||||
# redrawn.
|
||||
REFRESH_INTERVAL = 500
|
||||
|
||||
|
||||
def _download_dir():
|
||||
"""Get the download directory to use."""
|
||||
directory = config.get('storage', 'download-directory')
|
||||
if directory is None:
|
||||
directory = standarddir.download()
|
||||
return directory
|
||||
remember_dir = config.get('storage', 'remember-download-directory')
|
||||
|
||||
if remember_dir and _last_used_directory is not None:
|
||||
return _last_used_directory
|
||||
elif directory is None:
|
||||
return standarddir.download()
|
||||
else:
|
||||
return directory
|
||||
|
||||
|
||||
def _path_suggestion(filename):
|
||||
@@ -80,7 +93,6 @@ class DownloadItemStats(QObject):
|
||||
"""Statistics (bytes done, total bytes, time, etc.) about a download.
|
||||
|
||||
Class attributes:
|
||||
SPEED_REFRESH_INTERVAL: How often to refresh the speed, in msec.
|
||||
SPEED_AVG_WINDOW: How many seconds of speed data to average to
|
||||
estimate the remaining time.
|
||||
|
||||
@@ -93,42 +105,40 @@ class DownloadItemStats(QObject):
|
||||
the speed the last time.
|
||||
"""
|
||||
|
||||
SPEED_REFRESH_INTERVAL = 500
|
||||
SPEED_AVG_WINDOW = 30
|
||||
|
||||
updated = pyqtSignal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.total = None
|
||||
self.done = 0
|
||||
self.speed = 0
|
||||
self._last_done = 0
|
||||
samples = int(self.SPEED_AVG_WINDOW *
|
||||
(1000 / self.SPEED_REFRESH_INTERVAL))
|
||||
samples = int(self.SPEED_AVG_WINDOW * (1000 / REFRESH_INTERVAL))
|
||||
self._speed_avg = collections.deque(maxlen=samples)
|
||||
self.timer = usertypes.Timer(self, 'speed_refresh')
|
||||
self.timer.timeout.connect(self._update_speed)
|
||||
self.timer.setInterval(self.SPEED_REFRESH_INTERVAL)
|
||||
self.timer.start()
|
||||
|
||||
@pyqtSlot()
|
||||
def _update_speed(self):
|
||||
"""Recalculate the current download speed."""
|
||||
def update_speed(self):
|
||||
"""Recalculate the current download speed.
|
||||
|
||||
The caller needs to guarantee this is called all REFRESH_INTERVAL ms.
|
||||
"""
|
||||
if self.done is None:
|
||||
# this can happen for very fast downloads, e.g. when actually
|
||||
# opening a file
|
||||
return
|
||||
delta = self.done - self._last_done
|
||||
self.speed = delta * 1000 / self.SPEED_REFRESH_INTERVAL
|
||||
self.speed = delta * 1000 / REFRESH_INTERVAL
|
||||
self._speed_avg.append(self.speed)
|
||||
self._last_done = self.done
|
||||
self.updated.emit()
|
||||
|
||||
def finish(self):
|
||||
"""Set the download stats as finished."""
|
||||
self.timer.stop()
|
||||
self.done = self.total
|
||||
|
||||
def percentage(self):
|
||||
"""The current download percentage, or None if unknown."""
|
||||
if self.total == 0 or self.total is None:
|
||||
if self.done == self.total:
|
||||
return 100
|
||||
elif self.total == 0 or self.total is None:
|
||||
return None
|
||||
else:
|
||||
return 100 * self.done / self.total
|
||||
@@ -148,7 +158,7 @@ class DownloadItemStats(QObject):
|
||||
|
||||
@pyqtSlot(int, int)
|
||||
def on_download_progress(self, bytes_done, bytes_total):
|
||||
"""Upload local variables when the download progress changed.
|
||||
"""Update local variables when the download progress changed.
|
||||
|
||||
Args:
|
||||
bytes_done: How many bytes are downloaded.
|
||||
@@ -158,7 +168,6 @@ class DownloadItemStats(QObject):
|
||||
bytes_total = None
|
||||
self.done = bytes_done
|
||||
self.total = bytes_total
|
||||
self.updated.emit()
|
||||
|
||||
|
||||
class DownloadItem(QObject):
|
||||
@@ -231,7 +240,6 @@ class DownloadItem(QObject):
|
||||
self.retry_info = None
|
||||
self.done = False
|
||||
self.stats = DownloadItemStats(self)
|
||||
self.stats.updated.connect(self.data_changed)
|
||||
self.index = 0
|
||||
self.autoclose = True
|
||||
self.reply = None
|
||||
@@ -297,10 +305,10 @@ class DownloadItem(QObject):
|
||||
else:
|
||||
self.set_fileobj(fileobj)
|
||||
|
||||
def _ask_overwrite_question(self):
|
||||
def _ask_confirm_question(self, msg):
|
||||
"""Create a Question object to be asked."""
|
||||
q = usertypes.Question(self)
|
||||
q.text = self._filename + " already exists. Overwrite? (y/n)"
|
||||
q.text = msg
|
||||
q.mode = usertypes.PromptMode.yesno
|
||||
q.answered_yes.connect(self._create_fileobj)
|
||||
q.answered_no.connect(functools.partial(self.cancel, False))
|
||||
@@ -356,12 +364,19 @@ class DownloadItem(QObject):
|
||||
if reply.error() != QNetworkReply.NoError:
|
||||
QTimer.singleShot(0, lambda: self.error.emit(reply.errorString()))
|
||||
|
||||
def bg_color(self):
|
||||
"""Background color to be shown."""
|
||||
start = config.get('colors', 'downloads.bg.start')
|
||||
stop = config.get('colors', 'downloads.bg.stop')
|
||||
system = config.get('colors', 'downloads.bg.system')
|
||||
error = config.get('colors', 'downloads.bg.error')
|
||||
def get_status_color(self, position):
|
||||
"""Choose an appropriate color for presenting the download's status.
|
||||
|
||||
Args:
|
||||
position: The color type requested, can be 'fg' or 'bg'.
|
||||
"""
|
||||
# pylint: disable=bad-config-call
|
||||
# WORKAROUND for https://bitbucket.org/logilab/astroid/issue/104/
|
||||
assert position in ("fg", "bg")
|
||||
start = config.get('colors', 'downloads.{}.start'.format(position))
|
||||
stop = config.get('colors', 'downloads.{}.stop'.format(position))
|
||||
system = config.get('colors', 'downloads.{}.system'.format(position))
|
||||
error = config.get('colors', 'downloads.{}.error'.format(position))
|
||||
if self.error_msg is not None:
|
||||
assert not self.successful
|
||||
return error
|
||||
@@ -427,6 +442,7 @@ class DownloadItem(QObject):
|
||||
filename: The full filename to save the download to.
|
||||
None: special value to stop the download.
|
||||
"""
|
||||
global _last_used_directory
|
||||
if self.fileobj is not None:
|
||||
raise ValueError("fileobj was already set! filename: {}, "
|
||||
"existing: {}, fileobj {}".format(
|
||||
@@ -442,11 +458,20 @@ class DownloadItem(QObject):
|
||||
# try again.
|
||||
self._create_full_filename(os.path.join(_download_dir(), filename))
|
||||
|
||||
_last_used_directory = os.path.dirname(self._filename)
|
||||
|
||||
log.downloads.debug("Setting filename to {}".format(filename))
|
||||
if os.path.isfile(self._filename):
|
||||
# The file already exists, so ask the user if it should be
|
||||
# overwritten.
|
||||
self._ask_overwrite_question()
|
||||
txt = self._filename + " already exists. Overwrite?"
|
||||
self._ask_confirm_question(txt)
|
||||
# FIFO, device node, etc. Make sure we want to do this
|
||||
elif (os.path.exists(self._filename) and not
|
||||
os.path.isdir(self._filename)):
|
||||
txt = (self._filename + " already exists and is a special file. "
|
||||
"Write to this?")
|
||||
self._ask_confirm_question(txt)
|
||||
else:
|
||||
self._create_fileobj()
|
||||
|
||||
@@ -617,6 +642,9 @@ class DownloadManager(QAbstractListModel):
|
||||
self.questions = []
|
||||
self._networkmanager = networkmanager.NetworkManager(
|
||||
win_id, None, self)
|
||||
self._update_timer = usertypes.Timer(self, 'download-update')
|
||||
self._update_timer.timeout.connect(self.update_gui)
|
||||
self._update_timer.setInterval(REFRESH_INTERVAL)
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self, downloads=len(self.downloads))
|
||||
@@ -631,18 +659,21 @@ class DownloadManager(QAbstractListModel):
|
||||
self.questions.append(q)
|
||||
return q
|
||||
|
||||
@pyqtSlot()
|
||||
def update_gui(self):
|
||||
"""Periodical GUI update of all items."""
|
||||
assert self.downloads
|
||||
for dl in self.downloads:
|
||||
dl.stats.update_speed()
|
||||
self.dataChanged.emit(self.index(0), self.last_index())
|
||||
|
||||
@pyqtSlot('QUrl', 'QWebPage')
|
||||
def get(self, url, page=None, fileobj=None, filename=None,
|
||||
auto_remove=False):
|
||||
def get(self, url, **kwargs):
|
||||
"""Start a download with a link URL.
|
||||
|
||||
Args:
|
||||
url: The URL to get, as QUrl
|
||||
page: The QWebPage to get the download from.
|
||||
fileobj: The file object to write the answer to.
|
||||
filename: A path to write the data to.
|
||||
auto_remove: Whether to remove the download even if
|
||||
ui -> remove-finished-downloads is set to false.
|
||||
**kwargs: passed to get_request().
|
||||
|
||||
Return:
|
||||
If the download could start immediately, (fileobj/filename given),
|
||||
@@ -650,25 +681,24 @@ class DownloadManager(QAbstractListModel):
|
||||
|
||||
If not, None.
|
||||
"""
|
||||
if fileobj is not None and filename is not None:
|
||||
raise TypeError("Only one of fileobj/filename may be given!")
|
||||
if not url.isValid():
|
||||
urlutils.invalid_url_error(self._win_id, url, "start download")
|
||||
return
|
||||
req = QNetworkRequest(url)
|
||||
return self.get_request(req, page, fileobj, filename, auto_remove)
|
||||
return self.get_request(req, **kwargs)
|
||||
|
||||
def get_request(self, request, page=None, fileobj=None, filename=None,
|
||||
auto_remove=False):
|
||||
def get_request(self, request, *, fileobj=None, filename=None,
|
||||
prompt_download_directory=None, **kwargs):
|
||||
"""Start a download with a QNetworkRequest.
|
||||
|
||||
Args:
|
||||
request: The QNetworkRequest to download.
|
||||
page: The QWebPage to use.
|
||||
fileobj: The file object to write the answer to.
|
||||
filename: A path to write the data to.
|
||||
auto_remove: Whether to remove the download even if
|
||||
ui -> remove-finished-downloads is set to false.
|
||||
prompt_download_directory: Whether to prompt for the download dir
|
||||
or automatically download. If None, the
|
||||
config is used.
|
||||
**kwargs: Passed to fetch_request.
|
||||
|
||||
Return:
|
||||
If the download could start immediately, (fileobj/filename given),
|
||||
@@ -679,37 +709,47 @@ class DownloadManager(QAbstractListModel):
|
||||
if fileobj is not None and filename is not None:
|
||||
raise TypeError("Only one of fileobj/filename may be given!")
|
||||
# WORKAROUND for Qt corrupting data loaded from cache:
|
||||
# https://bugreports.qt-project.org/browse/QTBUG-42757
|
||||
# https://bugreports.qt.io/browse/QTBUG-42757
|
||||
request.setAttribute(QNetworkRequest.CacheLoadControlAttribute,
|
||||
QNetworkRequest.AlwaysNetwork)
|
||||
suggested_fn = urlutils.filename_from_url(request.url())
|
||||
|
||||
if prompt_download_directory is None:
|
||||
prompt_download_directory = config.get(
|
||||
'storage', 'prompt-download-directory')
|
||||
if not prompt_download_directory and not fileobj:
|
||||
filename = config.get('storage', 'download-directory')
|
||||
|
||||
if fileobj is not None or filename is not None:
|
||||
return self.fetch_request(request, page, fileobj, filename,
|
||||
auto_remove, suggested_fn)
|
||||
encoding = sys.getfilesystemencoding()
|
||||
suggested_fn = utils.force_encoding(suggested_fn, encoding)
|
||||
return self.fetch_request(request,
|
||||
fileobj=fileobj,
|
||||
filename=filename,
|
||||
suggested_filename=suggested_fn,
|
||||
**kwargs)
|
||||
if suggested_fn is None:
|
||||
suggested_fn = 'qutebrowser-download'
|
||||
else:
|
||||
encoding = sys.getfilesystemencoding()
|
||||
suggested_fn = utils.force_encoding(suggested_fn, encoding)
|
||||
q = self._prepare_question()
|
||||
q.default = _path_suggestion(suggested_fn)
|
||||
message_bridge = objreg.get('message-bridge', scope='window',
|
||||
window=self._win_id)
|
||||
q.answered.connect(
|
||||
lambda fn: self.fetch_request(request, page, filename=fn,
|
||||
auto_remove=auto_remove,
|
||||
suggested_filename=suggested_fn))
|
||||
lambda fn: self.fetch_request(request,
|
||||
filename=fn,
|
||||
suggested_filename=suggested_fn,
|
||||
**kwargs))
|
||||
message_bridge.ask(q, blocking=False)
|
||||
return None
|
||||
|
||||
def fetch_request(self, request, page=None, fileobj=None, filename=None,
|
||||
auto_remove=False, suggested_filename=None):
|
||||
def fetch_request(self, request, *, page=None, **kwargs):
|
||||
"""Download a QNetworkRequest to disk.
|
||||
|
||||
Args:
|
||||
request: The QNetworkRequest to download.
|
||||
page: The QWebPage to use.
|
||||
fileobj: The file object to write the answer to.
|
||||
filename: A path to write the data to.
|
||||
auto_remove: Whether to remove the download even if
|
||||
ui -> remove-finished-downloads is set to false.
|
||||
**kwargs: passed to fetch().
|
||||
|
||||
Return:
|
||||
The created DownloadItem.
|
||||
@@ -719,12 +759,11 @@ class DownloadManager(QAbstractListModel):
|
||||
else:
|
||||
nam = page.networkAccessManager()
|
||||
reply = nam.get(request)
|
||||
return self.fetch(reply, fileobj, filename, auto_remove,
|
||||
suggested_filename)
|
||||
return self.fetch(reply, **kwargs)
|
||||
|
||||
@pyqtSlot('QNetworkReply')
|
||||
def fetch(self, reply, fileobj=None, filename=None, auto_remove=False,
|
||||
suggested_filename=None):
|
||||
def fetch(self, reply, *, fileobj=None, filename=None, auto_remove=False,
|
||||
suggested_filename=None, prompt_download_directory=None):
|
||||
"""Download a QNetworkReply to disk.
|
||||
|
||||
Args:
|
||||
@@ -766,6 +805,14 @@ class DownloadManager(QAbstractListModel):
|
||||
self.downloads.append(download)
|
||||
self.endInsertRows()
|
||||
|
||||
if not self._update_timer.isActive():
|
||||
self._update_timer.start()
|
||||
|
||||
prompt_download_directory = config.get('storage',
|
||||
'prompt-download-directory')
|
||||
if not prompt_download_directory and not fileobj:
|
||||
filename = config.get('storage', 'download-directory')
|
||||
|
||||
if filename is not None:
|
||||
download.set_filename(filename)
|
||||
elif fileobj is not None:
|
||||
@@ -794,8 +841,9 @@ class DownloadManager(QAbstractListModel):
|
||||
raise cmdexc.CommandError("There's no download!")
|
||||
raise cmdexc.CommandError("There's no download {}!".format(count))
|
||||
|
||||
@cmdutils.register(instance='download-manager', scope='window')
|
||||
def download_cancel(self, count: {'special': 'count'}=0):
|
||||
@cmdutils.register(instance='download-manager', scope='window',
|
||||
count='count')
|
||||
def download_cancel(self, count=0):
|
||||
"""Cancel the last/[count]th download.
|
||||
|
||||
Args:
|
||||
@@ -812,8 +860,9 @@ class DownloadManager(QAbstractListModel):
|
||||
.format(count))
|
||||
download.cancel()
|
||||
|
||||
@cmdutils.register(instance='download-manager', scope='window')
|
||||
def download_delete(self, count: {'special': 'count'}=0):
|
||||
@cmdutils.register(instance='download-manager', scope='window',
|
||||
count='count')
|
||||
def download_delete(self, count=0):
|
||||
"""Delete the last/[count]th download from disk.
|
||||
|
||||
Args:
|
||||
@@ -831,8 +880,9 @@ class DownloadManager(QAbstractListModel):
|
||||
self.remove_item(download)
|
||||
|
||||
@cmdutils.register(instance='download-manager', scope='window',
|
||||
deprecated="Use :download-cancel instead.")
|
||||
def cancel_download(self, count: {'special': 'count'}=1):
|
||||
deprecated="Use :download-cancel instead.",
|
||||
count='count')
|
||||
def cancel_download(self, count=1):
|
||||
"""Cancel the first/[count]th download.
|
||||
|
||||
Args:
|
||||
@@ -840,8 +890,9 @@ class DownloadManager(QAbstractListModel):
|
||||
"""
|
||||
self.download_cancel(count)
|
||||
|
||||
@cmdutils.register(instance='download-manager', scope='window')
|
||||
def download_open(self, count: {'special': 'count'}=0):
|
||||
@cmdutils.register(instance='download-manager', scope='window',
|
||||
count='count')
|
||||
def download_open(self, count=0):
|
||||
"""Open the last/[count]th download.
|
||||
|
||||
Args:
|
||||
@@ -912,9 +963,9 @@ class DownloadManager(QAbstractListModel):
|
||||
"""Check if there are finished downloads to clear."""
|
||||
return any(download.done for download in self.downloads)
|
||||
|
||||
@cmdutils.register(instance='download-manager', scope='window')
|
||||
def download_remove(self, all_: {'name': 'all'}=False,
|
||||
count: {'special': 'count'}=0):
|
||||
@cmdutils.register(instance='download-manager', scope='window',
|
||||
count='count')
|
||||
def download_remove(self, all_=False, count=0):
|
||||
"""Remove the last/[count]th download from the list.
|
||||
|
||||
Args:
|
||||
@@ -957,6 +1008,8 @@ class DownloadManager(QAbstractListModel):
|
||||
self.endRemoveRows()
|
||||
download.deleteLater()
|
||||
self.update_indexes()
|
||||
if not self.downloads:
|
||||
self._update_timer.stop()
|
||||
|
||||
def remove_items(self, downloads):
|
||||
"""Remove an iterable of downloads."""
|
||||
@@ -985,6 +1038,8 @@ class DownloadManager(QAbstractListModel):
|
||||
else:
|
||||
download.deleteLater()
|
||||
self.endRemoveRows()
|
||||
if not self.downloads:
|
||||
self._update_timer.stop()
|
||||
|
||||
def update_indexes(self):
|
||||
"""Update indexes of all DownloadItems."""
|
||||
@@ -1016,9 +1071,9 @@ class DownloadManager(QAbstractListModel):
|
||||
if role == Qt.DisplayRole:
|
||||
data = str(item)
|
||||
elif role == Qt.ForegroundRole:
|
||||
data = config.get('colors', 'downloads.fg')
|
||||
data = item.get_status_color('fg')
|
||||
elif role == Qt.BackgroundRole:
|
||||
data = item.bg_color()
|
||||
data = item.get_status_color('bg')
|
||||
elif role == ModelRole.item:
|
||||
data = item
|
||||
elif role == Qt.ToolTipRole:
|
||||
@@ -1034,7 +1089,7 @@ class DownloadManager(QAbstractListModel):
|
||||
"""Override flags so items aren't selectable.
|
||||
|
||||
The default would be Qt.ItemIsEnabled | Qt.ItemIsSelectable."""
|
||||
return Qt.ItemIsEnabled
|
||||
return Qt.ItemIsEnabled | Qt.ItemNeverHasChildren
|
||||
|
||||
def rowCount(self, parent=QModelIndex()):
|
||||
"""Get count of active downloads."""
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
|
||||
import math
|
||||
import functools
|
||||
import subprocess
|
||||
import collections
|
||||
|
||||
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, QObject, QEvent, Qt, QUrl,
|
||||
@@ -36,15 +35,16 @@ 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.misc import guiprocess
|
||||
|
||||
|
||||
ElemTuple = collections.namedtuple('ElemTuple', ['elem', 'label'])
|
||||
|
||||
|
||||
Target = usertypes.enum('Target', ['normal', 'tab', 'tab_bg', 'window', 'yank',
|
||||
'yank_primary', 'run', 'fill', 'hover',
|
||||
'rapid', 'rapid_win', 'download',
|
||||
'userscript', 'spawn'])
|
||||
Target = usertypes.enum('Target', ['normal', 'tab', 'tab_fg', 'tab_bg',
|
||||
'window', 'yank', 'yank_primary', 'run',
|
||||
'fill', 'hover', 'rapid', 'rapid_win',
|
||||
'download', 'userscript', 'spawn'])
|
||||
|
||||
|
||||
@pyqtSlot(usertypes.KeyMode)
|
||||
@@ -65,7 +65,7 @@ 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_bg/window: Get passed to BrowserTab.
|
||||
normal/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.
|
||||
@@ -117,13 +117,14 @@ class HintManager(QObject):
|
||||
mouse_event: Mouse event to be posted in the web view.
|
||||
arg: A QMouseEvent
|
||||
start_hinting: Emitted when hinting starts, before a link is clicked.
|
||||
arg: The hinting target name.
|
||||
arg: The ClickTarget to use.
|
||||
stop_hinting: Emitted after a link was clicked.
|
||||
"""
|
||||
|
||||
HINT_TEXTS = {
|
||||
Target.normal: "Follow hint",
|
||||
Target.tab: "Follow hint in new tab",
|
||||
Target.tab_fg: "Follow hint in foreground tab",
|
||||
Target.tab_bg: "Follow hint in background tab",
|
||||
Target.window: "Follow hint in new window",
|
||||
Target.yank: "Yank hint to clipboard",
|
||||
@@ -137,7 +138,7 @@ class HintManager(QObject):
|
||||
}
|
||||
|
||||
mouse_event = pyqtSignal('QMouseEvent')
|
||||
start_hinting = pyqtSignal(str)
|
||||
start_hinting = pyqtSignal(usertypes.ClickTarget)
|
||||
stop_hinting = pyqtSignal()
|
||||
|
||||
def __init__(self, win_id, tab_id, parent=None):
|
||||
@@ -413,22 +414,30 @@ class HintManager(QObject):
|
||||
elem: The QWebElement to click.
|
||||
context: The HintContext to use.
|
||||
"""
|
||||
if context.target == Target.rapid:
|
||||
target = Target.tab_bg
|
||||
elif context.target == Target.rapid_win:
|
||||
target = Target.window
|
||||
target_mapping = {
|
||||
Target.rapid: usertypes.ClickTarget.tab_bg,
|
||||
Target.rapid_win: usertypes.ClickTarget.window,
|
||||
Target.normal: usertypes.ClickTarget.normal,
|
||||
Target.tab_fg: usertypes.ClickTarget.tab,
|
||||
Target.tab_bg: usertypes.ClickTarget.tab_bg,
|
||||
Target.window: usertypes.ClickTarget.window,
|
||||
Target.hover: usertypes.ClickTarget.normal,
|
||||
}
|
||||
if config.get('tabs', 'background-tabs'):
|
||||
target_mapping[Target.tab] = usertypes.ClickTarget.tab_bg
|
||||
else:
|
||||
target = context.target
|
||||
target_mapping[Target.tab] = usertypes.ClickTarget.tab
|
||||
# FIXME Instead of clicking the center, we could have nicer heuristics.
|
||||
# e.g. parse (-webkit-)border-radius correctly and click text fields at
|
||||
# the bottom right, and everything else on the top left or so.
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/70
|
||||
pos = elem.rect_on_view().center()
|
||||
action = "Hovering" if target == Target.hover else "Clicking"
|
||||
action = "Hovering" if context.target == Target.hover else "Clicking"
|
||||
log.hints.debug("{} on '{}' at {}/{}".format(
|
||||
action, elem, pos.x(), pos.y()))
|
||||
self.start_hinting.emit(target.name)
|
||||
if target in (Target.tab, Target.tab_bg, Target.window):
|
||||
self.start_hinting.emit(target_mapping[context.target])
|
||||
if context.target in [Target.tab, Target.tab_fg, Target.tab_bg,
|
||||
Target.window, Target.rapid, Target.rapid_win]:
|
||||
modifiers = Qt.ControlModifier
|
||||
else:
|
||||
modifiers = Qt.NoModifier
|
||||
@@ -436,7 +445,7 @@ class HintManager(QObject):
|
||||
QMouseEvent(QEvent.MouseMove, pos, Qt.NoButton, Qt.NoButton,
|
||||
Qt.NoModifier),
|
||||
]
|
||||
if target != Target.hover:
|
||||
if context.target != Target.hover:
|
||||
events += [
|
||||
QMouseEvent(QEvent.MouseButtonPress, pos, Qt.LeftButton,
|
||||
Qt.LeftButton, modifiers),
|
||||
@@ -505,12 +514,18 @@ class HintManager(QObject):
|
||||
if url is None:
|
||||
self._show_url_error()
|
||||
return
|
||||
if context.rapid:
|
||||
prompt = False
|
||||
else:
|
||||
prompt = None
|
||||
|
||||
download_manager = objreg.get('download-manager', scope='window',
|
||||
window=self._win_id)
|
||||
download_manager.get(url, elem.webFrame().page())
|
||||
download_manager.get(url, page=elem.webFrame().page(),
|
||||
prompt_download_directory=prompt)
|
||||
|
||||
def _call_userscript(self, elem, context):
|
||||
"""Call an userscript from a hint.
|
||||
"""Call a userscript from a hint.
|
||||
|
||||
Args:
|
||||
elem: The QWebElement to use in the userscript.
|
||||
@@ -523,12 +538,11 @@ class HintManager(QObject):
|
||||
'QUTE_MODE': 'hints',
|
||||
'QUTE_SELECTED_TEXT': str(elem),
|
||||
'QUTE_SELECTED_HTML': elem.toOuterXml(),
|
||||
'QUTE_HTML': frame.toHtml(),
|
||||
'QUTE_TEXT': frame.toPlainText(),
|
||||
}
|
||||
url = self._resolve_url(elem, context.baseurl)
|
||||
if url is not None:
|
||||
env['QUTE_URL'] = url.toString(QUrl.FullyEncoded)
|
||||
env.update(userscripts.store_source(frame))
|
||||
userscripts.run(cmd, *args, win_id=self._win_id, env=env)
|
||||
|
||||
def _spawn(self, url, context):
|
||||
@@ -540,11 +554,9 @@ class HintManager(QObject):
|
||||
"""
|
||||
urlstr = url.toString(QUrl.FullyEncoded | QUrl.RemovePassword)
|
||||
args = context.get_args(urlstr)
|
||||
try:
|
||||
subprocess.Popen(args)
|
||||
except OSError as e:
|
||||
msg = "Error while spawning command: {}".format(e)
|
||||
message.error(self._win_id, msg, immediately=True)
|
||||
cmd, *args = args
|
||||
proc = guiprocess.GUIProcess(self._win_id, what='command', parent=self)
|
||||
proc.start(cmd, args)
|
||||
|
||||
def _resolve_url(self, elem, baseurl):
|
||||
"""Resolve a URL and check if we want to keep it.
|
||||
@@ -590,13 +602,16 @@ class HintManager(QObject):
|
||||
# Then check for regular links/buttons.
|
||||
elems = frame.findAllElements(
|
||||
webelem.SELECTORS[webelem.Group.prevnext])
|
||||
elems = [webelem.WebElementWrapper(e) for e in elems]
|
||||
filterfunc = webelem.FILTERS[webelem.Group.prevnext]
|
||||
elems = [e for e in elems if filterfunc(e)]
|
||||
|
||||
option = 'prev-regexes' if prev else 'next-regexes'
|
||||
if not elems:
|
||||
return None
|
||||
for regex in config.get('hints', option):
|
||||
log.hints.vdebug("== Checking regex '{}'.".format(regex.pattern))
|
||||
for e in elems:
|
||||
e = webelem.WebElementWrapper(e)
|
||||
text = str(e)
|
||||
if not text:
|
||||
continue
|
||||
@@ -694,15 +709,16 @@ class HintManager(QObject):
|
||||
tab=self._tab_id)
|
||||
webview.openurl(url)
|
||||
|
||||
@cmdutils.register(instance='hintmanager', scope='tab', name='hint')
|
||||
@cmdutils.register(instance='hintmanager', scope='tab', name='hint',
|
||||
win_id='win_id')
|
||||
def start(self, rapid=False, group=webelem.Group.all, target=Target.normal,
|
||||
*args: {'nargs': '*'}, win_id: {'special': 'win_id'}):
|
||||
*args: {'nargs': '*'}, win_id):
|
||||
"""Start hinting.
|
||||
|
||||
Args:
|
||||
rapid: Whether to do rapid hinting. This is only possible with
|
||||
targets `tab-bg`, `window`, `run`, `hover`, `userscript` and
|
||||
`spawn`.
|
||||
targets `tab` (with background-tabs=true), `tab-bg`,
|
||||
`window`, `run`, `hover`, `userscript` and `spawn`.
|
||||
group: The hinting mode to use.
|
||||
|
||||
- `all`: All clickable elements.
|
||||
@@ -712,7 +728,9 @@ class HintManager(QObject):
|
||||
target: What to do with the selected element.
|
||||
|
||||
- `normal`: Open the link in the current tab.
|
||||
- `tab`: Open the link in a new tab.
|
||||
- `tab`: Open the link in a new tab (honoring the
|
||||
background-tabs setting).
|
||||
- `tab-fg`: Open the link in a new foreground tab.
|
||||
- `tab-bg`: Open the link in a new background tab.
|
||||
- `window`: Open the link in a new window.
|
||||
- `hover` : Hover over the link.
|
||||
@@ -722,7 +740,7 @@ class HintManager(QObject):
|
||||
- `fill`: Fill the commandline with the command given as
|
||||
argument.
|
||||
- `download`: Download the link.
|
||||
- `userscript`: Call an userscript with `$QUTE_URL` set to the
|
||||
- `userscript`: Call a userscript with `$QUTE_URL` set to the
|
||||
link.
|
||||
- `spawn`: Spawn a command.
|
||||
|
||||
@@ -748,14 +766,20 @@ class HintManager(QObject):
|
||||
mode_manager = objreg.get('mode-manager', scope='window',
|
||||
window=self._win_id)
|
||||
if mode_manager.mode == usertypes.KeyMode.hint:
|
||||
raise cmdexc.CommandError("Already hinting!")
|
||||
modeman.leave(win_id, usertypes.KeyMode.hint, 're-hinting')
|
||||
|
||||
if rapid and target not in (Target.tab_bg, Target.window, Target.run,
|
||||
Target.hover, Target.userscript,
|
||||
Target.spawn):
|
||||
name = target.name.replace('_', '-')
|
||||
raise cmdexc.CommandError("Rapid hinting makes no sense with "
|
||||
"target {}!".format(name))
|
||||
if rapid:
|
||||
if target in [Target.tab_bg, Target.window, Target.run,
|
||||
Target.hover, Target.userscript, Target.spawn,
|
||||
Target.download]:
|
||||
pass
|
||||
elif (target == Target.tab and
|
||||
config.get('tabs', 'background-tabs')):
|
||||
pass
|
||||
else:
|
||||
name = target.name.replace('_', '-')
|
||||
raise cmdexc.CommandError("Rapid hinting makes no sense with "
|
||||
"target {}!".format(name))
|
||||
|
||||
self._check_args(target, *args)
|
||||
self._context = HintContext()
|
||||
@@ -874,6 +898,7 @@ class HintManager(QObject):
|
||||
elem_handlers = {
|
||||
Target.normal: self._click,
|
||||
Target.tab: self._click,
|
||||
Target.tab_fg: self._click,
|
||||
Target.tab_bg: self._click,
|
||||
Target.window: self._click,
|
||||
Target.hover: self._click,
|
||||
|
||||
@@ -67,41 +67,30 @@ class WebHistory(QWebHistoryInterface):
|
||||
_history_dict: An OrderedDict of URLs read from the on-disk history.
|
||||
_new_history: A list of HistoryEntry items of the current session.
|
||||
_saved_count: How many HistoryEntries have been written to disk.
|
||||
_initial_read_started: Whether async_read was called.
|
||||
_initial_read_done: Whether async_read has completed.
|
||||
_temp_history: OrderedDict of temporary history entries before
|
||||
async_read was called.
|
||||
|
||||
Signals:
|
||||
item_about_to_be_added: Emitted before a new HistoryEntry is added.
|
||||
arg: The new HistoryEntry.
|
||||
add_completion_item: Emitted before a new HistoryEntry is added.
|
||||
arg: The new HistoryEntry.
|
||||
item_added: Emitted after a new HistoryEntry is added.
|
||||
arg: The new HistoryEntry.
|
||||
"""
|
||||
|
||||
item_about_to_be_added = pyqtSignal(HistoryEntry)
|
||||
add_completion_item = pyqtSignal(HistoryEntry)
|
||||
item_added = pyqtSignal(HistoryEntry)
|
||||
async_read_done = pyqtSignal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._initial_read_started = False
|
||||
self._initial_read_done = False
|
||||
self._lineparser = lineparser.AppendLineParser(
|
||||
standarddir.data(), 'history', parent=self)
|
||||
self._history_dict = collections.OrderedDict()
|
||||
with self._lineparser.open():
|
||||
for line in self._lineparser:
|
||||
data = line.rstrip().split(maxsplit=1)
|
||||
if not data:
|
||||
# empty line
|
||||
continue
|
||||
elif len(data) != 2:
|
||||
# other malformed line
|
||||
log.init.warning("Invalid history entry {!r}!".format(
|
||||
line))
|
||||
continue
|
||||
atime, url = data
|
||||
# This de-duplicates history entries; only the latest
|
||||
# entry for each URL is kept. If you want to keep
|
||||
# information about previous hits change the items in
|
||||
# old_urls to be lists or change HistoryEntry to have a
|
||||
# list of atimes.
|
||||
self._history_dict[url] = HistoryEntry(atime, url)
|
||||
self._history_dict.move_to_end(url)
|
||||
self._temp_history = collections.OrderedDict()
|
||||
self._new_history = []
|
||||
self._saved_count = 0
|
||||
objreg.get('save-manager').add_saveable(
|
||||
@@ -119,6 +108,60 @@ class WebHistory(QWebHistoryInterface):
|
||||
def __len__(self):
|
||||
return len(self._history_dict)
|
||||
|
||||
def async_read(self):
|
||||
"""Read the initial history."""
|
||||
if self._initial_read_started:
|
||||
log.init.debug("Ignoring async_read() because reading is started.")
|
||||
return
|
||||
self._initial_read_started = True
|
||||
|
||||
if standarddir.data() is None:
|
||||
self._initial_read_done = True
|
||||
self.async_read_done.emit()
|
||||
return
|
||||
|
||||
with self._lineparser.open():
|
||||
for line in self._lineparser:
|
||||
yield
|
||||
data = line.rstrip().split(maxsplit=1)
|
||||
if not data:
|
||||
# empty line
|
||||
continue
|
||||
elif len(data) != 2:
|
||||
# other malformed line
|
||||
log.init.warning("Invalid history entry {!r}!".format(
|
||||
line))
|
||||
continue
|
||||
atime, url = data
|
||||
if atime.startswith('\0'):
|
||||
log.init.warning(
|
||||
"Removing NUL bytes from entry {!r} - see "
|
||||
"https://github.com/The-Compiler/qutebrowser/issues/"
|
||||
"670".format(data))
|
||||
atime = atime.lstrip('\0')
|
||||
# This de-duplicates history entries; only the latest
|
||||
# entry for each URL is kept. If you want to keep
|
||||
# information about previous hits change the items in
|
||||
# old_urls to be lists or change HistoryEntry to have a
|
||||
# list of atimes.
|
||||
entry = HistoryEntry(atime, url)
|
||||
self._add_entry(entry)
|
||||
|
||||
self._initial_read_done = True
|
||||
self.async_read_done.emit()
|
||||
|
||||
for url, entry in self._temp_history.items():
|
||||
self._new_history.append(entry)
|
||||
self._add_entry(entry)
|
||||
self.add_completion_item.emit(entry)
|
||||
|
||||
def _add_entry(self, entry, target=None):
|
||||
"""Add an entry to self._history_dict or another given OrderedDict."""
|
||||
if target is None:
|
||||
target = self._history_dict
|
||||
target[entry.url_string] = entry
|
||||
target.move_to_end(entry.url_string)
|
||||
|
||||
def get_recent(self):
|
||||
"""Get the most recent history entries."""
|
||||
old = self._lineparser.get_recent()
|
||||
@@ -139,13 +182,16 @@ class WebHistory(QWebHistoryInterface):
|
||||
"""
|
||||
if not url_string:
|
||||
return
|
||||
if not config.get('general', 'private-browsing'):
|
||||
entry = HistoryEntry(time.time(), url_string)
|
||||
self.item_about_to_be_added.emit(entry)
|
||||
if config.get('general', 'private-browsing'):
|
||||
return
|
||||
entry = HistoryEntry(time.time(), url_string)
|
||||
if self._initial_read_done:
|
||||
self.add_completion_item.emit(entry)
|
||||
self._new_history.append(entry)
|
||||
self._history_dict[url_string] = entry
|
||||
self._history_dict.move_to_end(url_string)
|
||||
self._add_entry(entry)
|
||||
self.item_added.emit(entry)
|
||||
else:
|
||||
self._add_entry(entry, target=self._temp_history)
|
||||
|
||||
def historyContains(self, url_string):
|
||||
"""Called by WebKit to determine if an URL is contained in the history.
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Other utilities which don't fit anywhere else."""
|
||||
"""Parsing functions for various HTTP headers."""
|
||||
|
||||
|
||||
import os.path
|
||||
@@ -46,16 +46,19 @@ def parse_content_disposition(reply):
|
||||
# We use the unsafe variant of the filename as we sanitize it via
|
||||
# os.path.basename later.
|
||||
try:
|
||||
content_disposition = rfc6266.parse_headers(
|
||||
bytes(reply.rawHeader(content_disposition_header)))
|
||||
value = bytes(reply.rawHeader(content_disposition_header))
|
||||
log.rfc6266.debug("Parsing Content-Disposition: {}".format(value))
|
||||
content_disposition = rfc6266.parse_headers(value)
|
||||
filename = content_disposition.filename()
|
||||
except UnicodeDecodeError:
|
||||
log.rfc6266.exception("Error while decoding filename")
|
||||
except (SyntaxError, UnicodeDecodeError, rfc6266.Error):
|
||||
log.rfc6266.exception("Error while parsing filename")
|
||||
else:
|
||||
is_inline = content_disposition.is_inline()
|
||||
# Then try to get filename from url
|
||||
if not filename:
|
||||
filename = reply.url().path()
|
||||
path = reply.url().path()
|
||||
if path is not None:
|
||||
filename = path.rstrip('/')
|
||||
# If that fails as well, use a fallback
|
||||
if not filename:
|
||||
filename = 'qutebrowser-download'
|
||||
|
||||
61
qutebrowser/browser/inspector.py
Normal file
61
qutebrowser/browser/inspector.py
Normal file
@@ -0,0 +1,61 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2015 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/>.
|
||||
|
||||
"""Customized QWebInspector."""
|
||||
|
||||
import base64
|
||||
import binascii
|
||||
|
||||
from PyQt5.QtWebKitWidgets import QWebInspector
|
||||
|
||||
from qutebrowser.utils import log, objreg
|
||||
|
||||
|
||||
class WebInspector(QWebInspector):
|
||||
|
||||
"""A customized WebInspector which stores its geometry."""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._load_state_geometry()
|
||||
|
||||
def closeEvent(self, e):
|
||||
"""Save the geometry when closed."""
|
||||
state_config = objreg.get('state-config')
|
||||
data = bytes(self.saveGeometry())
|
||||
geom = base64.b64encode(data).decode('ASCII')
|
||||
state_config['geometry']['inspector'] = geom
|
||||
super().closeEvent(e)
|
||||
|
||||
def _load_state_geometry(self):
|
||||
"""Load the geometry from the state file."""
|
||||
state_config = objreg.get('state-config')
|
||||
try:
|
||||
data = state_config['geometry']['inspector']
|
||||
geom = base64.b64decode(data, validate=True)
|
||||
except KeyError:
|
||||
# First start
|
||||
pass
|
||||
except binascii.Error:
|
||||
log.misc.exception("Error while reading geometry")
|
||||
else:
|
||||
log.init.debug("Loading geometry from {}".format(geom))
|
||||
ok = self.restoreGeometry(geom)
|
||||
if not ok:
|
||||
log.init.warning("Error while loading geometry.")
|
||||
128
qutebrowser/browser/network/filescheme.py
Normal file
128
qutebrowser/browser/network/filescheme.py
Normal file
@@ -0,0 +1,128 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2015 Antoni Boucher (antoyo) <bouanto@zoho.com>
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
# pylint complains when using .render() on jinja templates, so we make it shut
|
||||
# up for this whole module.
|
||||
|
||||
"""Handler functions for file:... pages."""
|
||||
|
||||
import os
|
||||
|
||||
from PyQt5.QtCore import QUrl
|
||||
|
||||
from qutebrowser.browser.network import schemehandler, networkreply
|
||||
from qutebrowser.utils import utils, jinja
|
||||
|
||||
|
||||
def get_file_list(basedir, all_files, filterfunc):
|
||||
"""Get a list of files filtered by a filter function and sorted by name.
|
||||
|
||||
Args:
|
||||
basedir: The parent directory of all files.
|
||||
all_files: The list of files to filter and sort.
|
||||
filterfunc: The filter function.
|
||||
|
||||
Return:
|
||||
A list of dicts. Each dict contains the name and absname keys.
|
||||
"""
|
||||
items = []
|
||||
for filename in all_files:
|
||||
absname = os.path.join(basedir, filename)
|
||||
if filterfunc(absname):
|
||||
items.append({'name': filename, 'absname': absname})
|
||||
return sorted(items, key=lambda v: v['name'].lower())
|
||||
|
||||
|
||||
def is_root(directory):
|
||||
"""Check if the directory is the root directory.
|
||||
|
||||
Args:
|
||||
directory: The directory to check.
|
||||
|
||||
Return:
|
||||
Whether the directory is a root directory or not.
|
||||
"""
|
||||
return os.path.dirname(directory) == directory
|
||||
|
||||
|
||||
def dirbrowser_html(path):
|
||||
"""Get the directory browser web page.
|
||||
|
||||
Args:
|
||||
path: The directory path.
|
||||
|
||||
Return:
|
||||
The HTML of the web page.
|
||||
"""
|
||||
title = "Browse directory: {}".format(path)
|
||||
template = jinja.env.get_template('dirbrowser.html')
|
||||
# pylint: disable=no-member
|
||||
# https://bitbucket.org/logilab/pylint/issue/490/
|
||||
|
||||
folder_icon = utils.resource_filename('img/folder.svg')
|
||||
file_icon = utils.resource_filename('img/file.svg')
|
||||
|
||||
folder_url = QUrl.fromLocalFile(folder_icon).toString(QUrl.FullyEncoded)
|
||||
file_url = QUrl.fromLocalFile(file_icon).toString(QUrl.FullyEncoded)
|
||||
|
||||
if is_root(path):
|
||||
parent = None
|
||||
else:
|
||||
parent = os.path.dirname(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='')
|
||||
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, folder_url=folder_url,
|
||||
file_url=file_url)
|
||||
return html.encode('UTF-8', errors='xmlcharrefreplace')
|
||||
|
||||
|
||||
class FileSchemeHandler(schemehandler.SchemeHandler):
|
||||
|
||||
"""Scheme handler for file: URLs."""
|
||||
|
||||
def createRequest(self, _op, request, _outgoing_data):
|
||||
"""Create a new request.
|
||||
|
||||
Args:
|
||||
request: const QNetworkRequest & req
|
||||
_op: Operation op
|
||||
_outgoing_data: QIODevice * outgoingData
|
||||
|
||||
Return:
|
||||
A QNetworkReply for directories, None for files.
|
||||
"""
|
||||
path = request.url().toLocalFile()
|
||||
if os.path.isdir(path):
|
||||
data = dirbrowser_html(path)
|
||||
return networkreply.FixedDataNetworkReply(
|
||||
request, data, 'text/html', self.parent())
|
||||
@@ -22,35 +22,31 @@
|
||||
import collections
|
||||
|
||||
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, PYQT_VERSION, QCoreApplication,
|
||||
QUrl)
|
||||
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkReply, QSslError
|
||||
|
||||
try:
|
||||
from PyQt5.QtNetwork import QSslSocket
|
||||
except ImportError:
|
||||
SSL_AVAILABLE = False
|
||||
else:
|
||||
SSL_AVAILABLE = QSslSocket.supportsSsl()
|
||||
QUrl, QByteArray)
|
||||
from PyQt5.QtNetwork import (QNetworkAccessManager, QNetworkReply, QSslError,
|
||||
QSslSocket)
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import (message, log, usertypes, utils, objreg, qtutils,
|
||||
urlutils)
|
||||
from qutebrowser.browser import cookies
|
||||
from qutebrowser.browser.network import qutescheme, networkreply
|
||||
from qutebrowser.browser.network import filescheme
|
||||
|
||||
|
||||
HOSTBLOCK_ERROR_STRING = '%HOSTBLOCK%'
|
||||
ProxyId = collections.namedtuple('ProxyId', 'type, hostname, port')
|
||||
_proxy_auth_cache = {}
|
||||
|
||||
|
||||
def init():
|
||||
"""Disable insecure SSL ciphers on old Qt versions."""
|
||||
if SSL_AVAILABLE:
|
||||
if not qtutils.version_check('5.3.0'):
|
||||
# Disable weak SSL ciphers.
|
||||
# See https://codereview.qt-project.org/#/c/75943/
|
||||
good_ciphers = [c for c in QSslSocket.supportedCiphers()
|
||||
if c.usedBits() >= 128]
|
||||
QSslSocket.setDefaultCiphers(good_ciphers)
|
||||
if not qtutils.version_check('5.3.0'):
|
||||
# Disable weak SSL ciphers.
|
||||
# See https://codereview.qt-project.org/#/c/75943/
|
||||
good_ciphers = [c for c in QSslSocket.supportedCiphers()
|
||||
if c.usedBits() >= 128]
|
||||
QSslSocket.setDefaultCiphers(good_ciphers)
|
||||
|
||||
|
||||
class SslError(QSslError):
|
||||
@@ -102,13 +98,13 @@ class NetworkManager(QNetworkAccessManager):
|
||||
self._requests = []
|
||||
self._scheme_handlers = {
|
||||
'qute': qutescheme.QuteSchemeHandler(win_id),
|
||||
'file': filescheme.FileSchemeHandler(win_id),
|
||||
}
|
||||
self._set_cookiejar()
|
||||
self._set_cookiejar(private=config.get('general', 'private-browsing'))
|
||||
self._set_cache()
|
||||
if SSL_AVAILABLE:
|
||||
self.sslErrors.connect(self.on_ssl_errors)
|
||||
self._rejected_ssl_errors = collections.defaultdict(list)
|
||||
self._accepted_ssl_errors = collections.defaultdict(list)
|
||||
self.sslErrors.connect(self.on_ssl_errors)
|
||||
self._rejected_ssl_errors = collections.defaultdict(list)
|
||||
self._accepted_ssl_errors = collections.defaultdict(list)
|
||||
self.authenticationRequired.connect(self.on_authentication_required)
|
||||
self.proxyAuthenticationRequired.connect(
|
||||
self.on_proxy_authentication_required)
|
||||
@@ -171,16 +167,6 @@ class NetworkManager(QNetworkAccessManager):
|
||||
q.deleteLater()
|
||||
return q.answer
|
||||
|
||||
def _fill_authenticator(self, authenticator, answer):
|
||||
"""Fill a given QAuthenticator object with an answer."""
|
||||
if answer is not None:
|
||||
# Since the answer could be something else than (user, password)
|
||||
# pylint seems to think we're unpacking a non-sequence. However we
|
||||
# *did* explicitly ask for a tuple, so it *will* always be one.
|
||||
user, password = answer
|
||||
authenticator.setUser(user)
|
||||
authenticator.setPassword(password)
|
||||
|
||||
def shutdown(self):
|
||||
"""Abort all running requests."""
|
||||
self.setNetworkAccessible(QNetworkAccessManager.NotAccessible)
|
||||
@@ -189,64 +175,67 @@ class NetworkManager(QNetworkAccessManager):
|
||||
request.deleteLater()
|
||||
self.shutting_down.emit()
|
||||
|
||||
if SSL_AVAILABLE: # noqa
|
||||
@pyqtSlot('QNetworkReply*', 'QList<QSslError>')
|
||||
def on_ssl_errors(self, reply, errors):
|
||||
"""Decide if SSL errors should be ignored or not.
|
||||
@pyqtSlot('QNetworkReply*', 'QList<QSslError>')
|
||||
def on_ssl_errors(self, reply, errors): # pragma: no mccabe
|
||||
"""Decide if SSL errors should be ignored or not.
|
||||
|
||||
This slot is called on SSL/TLS errors by the self.sslErrors signal.
|
||||
This slot is called on SSL/TLS errors by the self.sslErrors signal.
|
||||
|
||||
Args:
|
||||
reply: The QNetworkReply that is encountering the errors.
|
||||
errors: A list of errors.
|
||||
"""
|
||||
errors = [SslError(e) for e in errors]
|
||||
ssl_strict = config.get('network', 'ssl-strict')
|
||||
if ssl_strict == 'ask':
|
||||
Args:
|
||||
reply: The QNetworkReply that is encountering the errors.
|
||||
errors: A list of errors.
|
||||
"""
|
||||
errors = [SslError(e) for e in errors]
|
||||
ssl_strict = config.get('network', 'ssl-strict')
|
||||
if ssl_strict == 'ask':
|
||||
try:
|
||||
host_tpl = urlutils.host_tuple(reply.url())
|
||||
if set(errors).issubset(self._accepted_ssl_errors[host_tpl]):
|
||||
reply.ignoreSslErrors()
|
||||
elif set(errors).issubset(self._rejected_ssl_errors[host_tpl]):
|
||||
pass
|
||||
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()
|
||||
self._accepted_ssl_errors[host_tpl] += errors
|
||||
else:
|
||||
self._rejected_ssl_errors[host_tpl] += errors
|
||||
elif ssl_strict:
|
||||
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:
|
||||
reply.ignoreSslErrors()
|
||||
elif is_rejected:
|
||||
pass
|
||||
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()))
|
||||
reply.ignoreSslErrors()
|
||||
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
|
||||
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()))
|
||||
reply.ignoreSslErrors()
|
||||
|
||||
@pyqtSlot(QUrl)
|
||||
def clear_rejected_ssl_errors(self, url):
|
||||
"""Clear the rejected SSL errors on a reload.
|
||||
@pyqtSlot(QUrl)
|
||||
def clear_rejected_ssl_errors(self, url):
|
||||
"""Clear the rejected SSL errors on a reload.
|
||||
|
||||
Args:
|
||||
url: The URL to remove.
|
||||
"""
|
||||
try:
|
||||
del self._rejected_ssl_errors[url]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
@pyqtSlot(QUrl)
|
||||
def clear_rejected_ssl_errors(self, _url):
|
||||
"""Clear the rejected SSL errors on a reload.
|
||||
|
||||
Does nothing because SSL is unavailable.
|
||||
"""
|
||||
Args:
|
||||
url: The URL to remove.
|
||||
"""
|
||||
try:
|
||||
del self._rejected_ssl_errors[url]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
@pyqtSlot('QNetworkReply', 'QAuthenticator')
|
||||
@@ -255,14 +244,25 @@ class NetworkManager(QNetworkAccessManager):
|
||||
answer = self._ask("Username ({}):".format(authenticator.realm()),
|
||||
mode=usertypes.PromptMode.user_pwd,
|
||||
owner=reply)
|
||||
self._fill_authenticator(authenticator, answer)
|
||||
if answer is not None:
|
||||
authenticator.setUser(answer.user)
|
||||
authenticator.setPassword(answer.password)
|
||||
|
||||
@pyqtSlot('QNetworkProxy', 'QAuthenticator')
|
||||
def on_proxy_authentication_required(self, _proxy, authenticator):
|
||||
def on_proxy_authentication_required(self, proxy, authenticator):
|
||||
"""Called when a proxy needs authentication."""
|
||||
answer = self._ask("Proxy username ({}):".format(
|
||||
authenticator.realm()), mode=usertypes.PromptMode.user_pwd)
|
||||
self._fill_authenticator(authenticator, answer)
|
||||
proxy_id = ProxyId(proxy.type(), proxy.hostName(), proxy.port())
|
||||
if proxy_id in _proxy_auth_cache:
|
||||
user, password = _proxy_auth_cache[proxy_id]
|
||||
authenticator.setUser(user)
|
||||
authenticator.setPassword(password)
|
||||
else:
|
||||
answer = self._ask("Proxy username ({}):".format(
|
||||
authenticator.realm()), mode=usertypes.PromptMode.user_pwd)
|
||||
if answer is not None:
|
||||
authenticator.setUser(answer.user)
|
||||
authenticator.setPassword(answer.password)
|
||||
_proxy_auth_cache[proxy_id] = answer
|
||||
|
||||
@config.change_filter('general', 'private-browsing')
|
||||
def on_config_changed(self):
|
||||
@@ -297,6 +297,25 @@ class NetworkManager(QNetworkAccessManager):
|
||||
download.destroyed.connect(self.on_adopted_download_destroyed)
|
||||
download.do_retry.connect(self.adopt_download)
|
||||
|
||||
def set_referer(self, req, current_url):
|
||||
"""Set the referer header."""
|
||||
referer_header_conf = config.get('network', 'referer-header')
|
||||
|
||||
try:
|
||||
if referer_header_conf == 'never':
|
||||
# Note: using ''.encode('ascii') sends a header with no value,
|
||||
# instead of no header at all
|
||||
req.setRawHeader('Referer'.encode('ascii'), QByteArray())
|
||||
elif (referer_header_conf == 'same-domain' and
|
||||
not urlutils.same_domain(req.url(), current_url)):
|
||||
req.setRawHeader('Referer'.encode('ascii'), QByteArray())
|
||||
# If refer_header_conf is set to 'always', we leave the header
|
||||
# alone as QtWebKit did set it.
|
||||
except urlutils.InvalidUrlError:
|
||||
# req.url() or current_url can be invalid - this happens on
|
||||
# https://www.playstation.com/ for example.
|
||||
pass
|
||||
|
||||
# WORKAROUND for:
|
||||
# http://www.riverbankcomputing.com/pipermail/pyqt/2014-September/034806.html
|
||||
#
|
||||
@@ -319,13 +338,11 @@ class NetworkManager(QNetworkAccessManager):
|
||||
A QNetworkReply.
|
||||
"""
|
||||
scheme = req.url().scheme()
|
||||
if scheme == 'https' and not SSL_AVAILABLE:
|
||||
return networkreply.ErrorNetworkReply(
|
||||
req, "SSL is not supported by the installed Qt library!",
|
||||
QNetworkReply.ProtocolUnknownError, self)
|
||||
elif scheme in self._scheme_handlers:
|
||||
return self._scheme_handlers[scheme].createRequest(
|
||||
if scheme in self._scheme_handlers:
|
||||
result = self._scheme_handlers[scheme].createRequest(
|
||||
op, req, outgoing_data)
|
||||
if result is not None:
|
||||
return result
|
||||
|
||||
host_blocker = objreg.get('host-blocker')
|
||||
if (op == QNetworkAccessManager.GetOperation and
|
||||
@@ -343,6 +360,16 @@ class NetworkManager(QNetworkAccessManager):
|
||||
dnt = '0'.encode('ascii')
|
||||
req.setRawHeader('DNT'.encode('ascii'), dnt)
|
||||
req.setRawHeader('X-Do-Not-Track'.encode('ascii'), dnt)
|
||||
|
||||
if self._tab_id is None:
|
||||
current_url = QUrl() # generic NetworkManager, e.g. for downloads
|
||||
else:
|
||||
webview = objreg.get('webview', scope='tab', window=self._win_id,
|
||||
tab=self._tab_id)
|
||||
current_url = webview.url()
|
||||
|
||||
self.set_referer(req, current_url)
|
||||
|
||||
accept_language = config.get('network', 'accept-language')
|
||||
if accept_language is not None:
|
||||
req.setRawHeader('Accept-Language'.encode('ascii'),
|
||||
|
||||
@@ -87,9 +87,11 @@ class FixedDataNetworkReply(QNetworkReply):
|
||||
return buf
|
||||
|
||||
def isFinished(self):
|
||||
"""Check if the reply is finished."""
|
||||
return True
|
||||
|
||||
def isRunning(self):
|
||||
return False
|
||||
|
||||
|
||||
class ErrorNetworkReply(QNetworkReply):
|
||||
|
||||
@@ -125,6 +127,12 @@ class ErrorNetworkReply(QNetworkReply):
|
||||
"""We always have 0 bytes available."""
|
||||
return 0
|
||||
|
||||
def readData(self):
|
||||
def readData(self, _maxlen):
|
||||
"""No data available."""
|
||||
return bytes()
|
||||
|
||||
def isFinished(self):
|
||||
return True
|
||||
|
||||
def isRunning(self):
|
||||
return False
|
||||
|
||||
@@ -96,6 +96,12 @@ class JSBridge(QObject):
|
||||
@pyqtSlot(int, str, str, str)
|
||||
def set(self, win_id, sectname, optname, value):
|
||||
"""Slot to set a setting from qute:settings."""
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/727
|
||||
if ((sectname, optname) == ('content', 'allow-javascript') and
|
||||
value == 'false'):
|
||||
message.error(win_id, "Refusing to disable javascript via "
|
||||
"qute:settings as it needs javascript support.")
|
||||
return
|
||||
try:
|
||||
objreg.get('config').set('conf', sectname, optname, value)
|
||||
except (configexc.Error, configparser.Error) as e:
|
||||
@@ -147,13 +153,13 @@ def qute_help(win_id, request):
|
||||
"""Handler for qute:help. Return HTML content as bytes."""
|
||||
try:
|
||||
utils.read_file('html/doc/index.html')
|
||||
except FileNotFoundError:
|
||||
except OSError:
|
||||
html = jinja.env.get_template('error.html').render(
|
||||
title="Error while loading documentation",
|
||||
url=request.url().toDisplayString(),
|
||||
error="This most likely means the documentation was not generated "
|
||||
"properly. If you are running qutebrowser from the git "
|
||||
"repository, please run scripts/asciidoc2html.py."
|
||||
"repository, please run scripts/asciidoc2html.py. "
|
||||
"If you're running a released version this is a bug, please "
|
||||
"use :report to report it.",
|
||||
icon='')
|
||||
|
||||
@@ -1,169 +0,0 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014-2015 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/>.
|
||||
|
||||
"""Manager for quickmarks.
|
||||
|
||||
Note we violate our general QUrl rule by storing url strings in the marks
|
||||
OrderedDict. This is because we read them from a file at start and write them
|
||||
to a file on shutdown, so it makes sense to keep them as strings here.
|
||||
"""
|
||||
|
||||
import os.path
|
||||
import functools
|
||||
import collections
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, QUrl, QObject
|
||||
|
||||
from qutebrowser.utils import message, usertypes, urlutils, standarddir, objreg
|
||||
from qutebrowser.commands import cmdexc, cmdutils
|
||||
from qutebrowser.misc import lineparser
|
||||
|
||||
|
||||
class QuickmarkManager(QObject):
|
||||
|
||||
"""Manager for quickmarks.
|
||||
|
||||
Attributes:
|
||||
marks: An OrderedDict of all quickmarks.
|
||||
_lineparser: The LineParser used for the quickmarks, or None
|
||||
(when qutebrowser is started with -c '').
|
||||
|
||||
Signals:
|
||||
changed: Emitted when anything changed.
|
||||
added: Emitted when a new quickmark was added.
|
||||
arg 0: The name of the quickmark.
|
||||
arg 1: The URL of the quickmark, as string.
|
||||
removed: Emitted when an existing quickmark was removed.
|
||||
arg 0: The name of the quickmark.
|
||||
"""
|
||||
|
||||
changed = pyqtSignal()
|
||||
added = pyqtSignal(str, str)
|
||||
removed = pyqtSignal(str)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
"""Initialize and read quickmarks."""
|
||||
super().__init__(parent)
|
||||
|
||||
self.marks = collections.OrderedDict()
|
||||
|
||||
if standarddir.config() is None:
|
||||
self._lineparser = None
|
||||
else:
|
||||
self._lineparser = lineparser.LineParser(
|
||||
standarddir.config(), 'quickmarks', parent=self)
|
||||
for line in self._lineparser:
|
||||
if not line.strip():
|
||||
# Ignore empty or whitespace-only lines.
|
||||
continue
|
||||
try:
|
||||
key, url = line.rsplit(maxsplit=1)
|
||||
except ValueError:
|
||||
message.error(0, "Invalid quickmark '{}'".format(line))
|
||||
else:
|
||||
self.marks[key] = url
|
||||
filename = os.path.join(standarddir.config(), 'quickmarks')
|
||||
objreg.get('save-manager').add_saveable(
|
||||
'quickmark-manager', self.save, self.changed,
|
||||
filename=filename)
|
||||
|
||||
def save(self):
|
||||
"""Save the quickmarks to disk."""
|
||||
if self._lineparser is not None:
|
||||
self._lineparser.data = [' '.join(tpl)
|
||||
for tpl in self.marks.items()]
|
||||
self._lineparser.save()
|
||||
|
||||
def prompt_save(self, win_id, url):
|
||||
"""Prompt for a new quickmark name to be added and add it.
|
||||
|
||||
Args:
|
||||
win_id: The current window ID.
|
||||
url: The quickmark url as a QUrl.
|
||||
"""
|
||||
if not url.isValid():
|
||||
urlutils.invalid_url_error(win_id, url, "save quickmark")
|
||||
return
|
||||
urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded)
|
||||
message.ask_async(
|
||||
win_id, "Add quickmark:", usertypes.PromptMode.text,
|
||||
functools.partial(self.quickmark_add, win_id, urlstr))
|
||||
|
||||
@cmdutils.register(instance='quickmark-manager')
|
||||
def quickmark_add(self, win_id: {'special': 'win_id'}, url, name):
|
||||
"""Add a new quickmark.
|
||||
|
||||
Args:
|
||||
win_id: The window ID to display the errors in.
|
||||
url: The url to add as quickmark.
|
||||
name: The name for the new quickmark.
|
||||
"""
|
||||
# We don't raise cmdexc.CommandError here as this can be called async
|
||||
# via prompt_save.
|
||||
if not name:
|
||||
message.error(win_id, "Can't set mark with empty name!")
|
||||
return
|
||||
if not url:
|
||||
message.error(win_id, "Can't set mark with empty URL!")
|
||||
return
|
||||
|
||||
def set_mark():
|
||||
"""Really set the quickmark."""
|
||||
self.marks[name] = url
|
||||
self.changed.emit()
|
||||
self.added.emit(name, url)
|
||||
|
||||
if name in self.marks:
|
||||
message.confirm_async(
|
||||
win_id, "Override existing quickmark?", set_mark, default=True)
|
||||
else:
|
||||
set_mark()
|
||||
|
||||
@cmdutils.register(instance='quickmark-manager', maxsplit=0,
|
||||
completion=[usertypes.Completion.quickmark_by_name])
|
||||
def quickmark_del(self, name):
|
||||
"""Delete a quickmark.
|
||||
|
||||
Args:
|
||||
name: The name of the quickmark to delete.
|
||||
"""
|
||||
try:
|
||||
del self.marks[name]
|
||||
except KeyError:
|
||||
raise cmdexc.CommandError("Quickmark '{}' not found!".format(name))
|
||||
else:
|
||||
self.changed.emit()
|
||||
self.removed.emit(name)
|
||||
|
||||
def get(self, name):
|
||||
"""Get the URL of the quickmark named name as a QUrl."""
|
||||
if name not in self.marks:
|
||||
raise cmdexc.CommandError(
|
||||
"Quickmark '{}' does not exist!".format(name))
|
||||
urlstr = self.marks[name]
|
||||
try:
|
||||
url = urlutils.fuzzy_url(urlstr, do_search=False)
|
||||
except urlutils.FuzzyUrlError as e:
|
||||
if e.url is None or not e.url.errorString():
|
||||
errstr = ''
|
||||
else:
|
||||
errstr = ' ({})'.format(e.url.errorString())
|
||||
raise cmdexc.CommandError("Invalid URL for quickmark {}: "
|
||||
"{}{}".format(name, urlstr, errstr))
|
||||
return url
|
||||
@@ -26,7 +26,7 @@ import re
|
||||
|
||||
import pypeg2 as peg
|
||||
|
||||
from qutebrowser.utils import log, utils
|
||||
from qutebrowser.utils import utils
|
||||
|
||||
|
||||
class UniqueNamespace(peg.Namespace):
|
||||
@@ -215,17 +215,22 @@ class ContentDispositionValue:
|
||||
LangTagged = collections.namedtuple('LangTagged', ['string', 'langtag'])
|
||||
|
||||
|
||||
class DuplicateParamError(Exception):
|
||||
class Error(Exception):
|
||||
|
||||
"""Base class for RFC6266 errors."""
|
||||
|
||||
|
||||
class DuplicateParamError(Error):
|
||||
|
||||
"""Exception raised when a parameter has been given twice."""
|
||||
|
||||
|
||||
class InvalidISO8859Error(Exception):
|
||||
class InvalidISO8859Error(Error):
|
||||
|
||||
"""Exception raised when a byte is invalid in ISO-8859-1."""
|
||||
|
||||
|
||||
class ContentDisposition:
|
||||
class _ContentDisposition:
|
||||
|
||||
"""Records various indications and hints about content disposition.
|
||||
|
||||
@@ -234,24 +239,15 @@ class ContentDisposition:
|
||||
in the download case.
|
||||
"""
|
||||
|
||||
def __init__(self, disposition='inline', assocs=None):
|
||||
"""Used internally after parsing the header.
|
||||
|
||||
Instances should generally be created from a factory
|
||||
function, such as parse_headers and its variants.
|
||||
"""
|
||||
if len(disposition) != 1:
|
||||
self.disposition = 'inline'
|
||||
else:
|
||||
self.disposition = disposition[0]
|
||||
if assocs is None:
|
||||
self.assocs = {}
|
||||
else:
|
||||
self.assocs = dict(assocs) # So we can change values
|
||||
if 'filename*' in self.assocs:
|
||||
param = self.assocs['filename*']
|
||||
assert isinstance(param, ExtDispositionParm)
|
||||
self.assocs['filename*'] = parse_ext_value(param.value).string
|
||||
def __init__(self, disposition, assocs):
|
||||
"""Used internally after parsing the header."""
|
||||
assert len(disposition) == 1
|
||||
self.disposition = disposition[0]
|
||||
self.assocs = dict(assocs) # So we can change values
|
||||
if 'filename*' in self.assocs:
|
||||
param = self.assocs['filename*']
|
||||
assert isinstance(param, ExtDispositionParm)
|
||||
self.assocs['filename*'] = parse_ext_value(param.value).string
|
||||
|
||||
def filename(self):
|
||||
"""The filename from the Content-Disposition header or None.
|
||||
@@ -291,7 +287,7 @@ def normalize_ws(text):
|
||||
|
||||
|
||||
def parse_headers(content_disposition):
|
||||
"""Build a ContentDisposition from header values."""
|
||||
"""Build a _ContentDisposition from header values."""
|
||||
# https://bitbucket.org/logilab/pylint/issue/492/
|
||||
# pylint: disable=no-member
|
||||
|
||||
@@ -302,8 +298,6 @@ def parse_headers(content_disposition):
|
||||
# filename parameter. But it does mean we occasionally give
|
||||
# less-than-certain values for some legacy senders.
|
||||
content_disposition = content_disposition.decode('iso-8859-1')
|
||||
log.rfc6266.debug("Parsing Content-Disposition: {}".format(
|
||||
content_disposition))
|
||||
# Our parsing is relaxed in these regards:
|
||||
# - The grammar allows a final ';' in the header;
|
||||
# - We do LWS-folding, and possibly normalise other broken
|
||||
@@ -311,14 +305,8 @@ def parse_headers(content_disposition):
|
||||
# XXX Would prefer to accept only the quoted whitespace
|
||||
# case, rather than normalising everything.
|
||||
content_disposition = normalize_ws(content_disposition)
|
||||
try:
|
||||
parsed = peg.parse(content_disposition, ContentDispositionValue)
|
||||
except (SyntaxError, DuplicateParamError, InvalidISO8859Error):
|
||||
log.rfc6266.exception("Error while parsing Content-Disposition")
|
||||
return ContentDisposition()
|
||||
else:
|
||||
return ContentDisposition(disposition=parsed.dtype,
|
||||
assocs=parsed.params)
|
||||
parsed = peg.parse(content_disposition, ContentDispositionValue)
|
||||
return _ContentDisposition(disposition=parsed.dtype, assocs=parsed.params)
|
||||
|
||||
|
||||
def parse_ext_value(val):
|
||||
|
||||
@@ -24,7 +24,6 @@ import functools
|
||||
from PyQt5.QtCore import QObject
|
||||
|
||||
from qutebrowser.utils import debug, log, objreg
|
||||
from qutebrowser.browser import webview
|
||||
|
||||
|
||||
class SignalFilter(QObject):
|
||||
@@ -58,9 +57,6 @@ class SignalFilter(QObject):
|
||||
Return:
|
||||
A partial functon calling _filter_signals with a signal.
|
||||
"""
|
||||
if not isinstance(tab, webview.WebView):
|
||||
raise ValueError("Tried to create filter for {} which is no "
|
||||
"WebView!".format(tab))
|
||||
return functools.partial(self._filter_signals, signal, tab)
|
||||
|
||||
def _filter_signals(self, signal, tab, *args):
|
||||
|
||||
@@ -35,14 +35,19 @@ class TabHistoryItem:
|
||||
|
||||
Attributes:
|
||||
url: The QUrl of this item.
|
||||
original_url: The QUrl of this item which was originally requested.
|
||||
title: The title as string of this item.
|
||||
active: Whether this item is the item currently navigated to.
|
||||
user_data: The user data for this item.
|
||||
"""
|
||||
|
||||
def __init__(self, url, original_url, title, active=False, user_data=None):
|
||||
def __init__(self, url, title, *, original_url=None, active=False,
|
||||
user_data=None):
|
||||
self.url = url
|
||||
self.original_url = original_url
|
||||
if original_url is None:
|
||||
self.original_url = url
|
||||
else:
|
||||
self.original_url = original_url
|
||||
self.title = title
|
||||
self.active = active
|
||||
self.user_data = user_data
|
||||
|
||||
296
qutebrowser/browser/urlmarks.py
Normal file
296
qutebrowser/browser/urlmarks.py
Normal file
@@ -0,0 +1,296 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2015 Antoni Boucher <bouanto@zoho.com>
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
"""Managers for bookmarks and quickmarks.
|
||||
|
||||
Note we violate our general QUrl rule by storing url strings in the marks
|
||||
OrderedDict. This is because we read them from a file at start and write them
|
||||
to a file on shutdown, so it makes sense to keep them as strings here.
|
||||
"""
|
||||
|
||||
import os
|
||||
import os.path
|
||||
import functools
|
||||
import collections
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, QUrl, QObject
|
||||
|
||||
from qutebrowser.utils import message, usertypes, urlutils, standarddir, objreg
|
||||
from qutebrowser.commands import cmdexc, cmdutils
|
||||
from qutebrowser.misc import lineparser
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
|
||||
"""Base class for all errors in this module."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class InvalidUrlError(Error):
|
||||
|
||||
"""Exception emitted when a URL is invalid."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class DoesNotExistError(Error):
|
||||
|
||||
"""Exception emitted when a given URL does not exist."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class AlreadyExistsError(Error):
|
||||
|
||||
"""Exception emitted when a given URL does already exist."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class UrlMarkManager(QObject):
|
||||
|
||||
"""Base class for BookmarkManager and QuickmarkManager.
|
||||
|
||||
Attributes:
|
||||
marks: An OrderedDict of all quickmarks/bookmarks.
|
||||
_lineparser: The LineParser used for the marks, or None
|
||||
(when qutebrowser is started with -c '').
|
||||
|
||||
Signals:
|
||||
changed: Emitted when anything changed.
|
||||
added: Emitted when a new quickmark/bookmark was added.
|
||||
removed: Emitted when an existing quickmark/bookmark was removed.
|
||||
"""
|
||||
|
||||
changed = pyqtSignal()
|
||||
added = pyqtSignal(str, str)
|
||||
removed = pyqtSignal(str)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
"""Initialize and read quickmarks."""
|
||||
super().__init__(parent)
|
||||
|
||||
self.marks = collections.OrderedDict()
|
||||
self._lineparser = None
|
||||
|
||||
if standarddir.config() is None:
|
||||
return
|
||||
|
||||
self._init_lineparser()
|
||||
for line in self._lineparser:
|
||||
if not line.strip():
|
||||
# Ignore empty or whitespace-only lines.
|
||||
continue
|
||||
self._parse_line(line)
|
||||
self._init_savemanager(objreg.get('save-manager'))
|
||||
|
||||
def _init_lineparser(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def _parse_line(self, line):
|
||||
raise NotImplementedError
|
||||
|
||||
def _init_savemanager(self, _save_manager):
|
||||
raise NotImplementedError
|
||||
|
||||
def save(self):
|
||||
"""Save the marks to disk."""
|
||||
if self._lineparser is not None:
|
||||
self._lineparser.data = [' '.join(tpl)
|
||||
for tpl in self.marks.items()]
|
||||
self._lineparser.save()
|
||||
|
||||
def delete(self, key):
|
||||
"""Delete a quickmark/bookmark.
|
||||
|
||||
Args:
|
||||
key: The key to delete (name for quickmarks, URL for bookmarks.)
|
||||
"""
|
||||
del self.marks[key]
|
||||
self.changed.emit()
|
||||
self.removed.emit(key)
|
||||
|
||||
|
||||
class QuickmarkManager(UrlMarkManager):
|
||||
|
||||
"""Manager for quickmarks.
|
||||
|
||||
The primary key for quickmarks is their *name*, this means:
|
||||
|
||||
- self.marks maps names to URLs.
|
||||
- changed gets emitted with the name as first argument and the URL as
|
||||
second argument.
|
||||
- removed gets emitted with the name as argument.
|
||||
"""
|
||||
|
||||
def _init_lineparser(self):
|
||||
self._lineparser = lineparser.LineParser(
|
||||
standarddir.config(), 'quickmarks', parent=self)
|
||||
|
||||
def _init_savemanager(self, save_manager):
|
||||
filename = os.path.join(standarddir.config(), 'quickmarks')
|
||||
save_manager.add_saveable('quickmark-manager', self.save, self.changed,
|
||||
filename=filename)
|
||||
|
||||
def _parse_line(self, line):
|
||||
try:
|
||||
key, url = line.rsplit(maxsplit=1)
|
||||
except ValueError:
|
||||
message.error('current', "Invalid quickmark '{}'".format(
|
||||
line))
|
||||
else:
|
||||
self.marks[key] = url
|
||||
|
||||
def prompt_save(self, win_id, url):
|
||||
"""Prompt for a new quickmark name to be added and add it.
|
||||
|
||||
Args:
|
||||
win_id: The current window ID.
|
||||
url: The quickmark url as a QUrl.
|
||||
"""
|
||||
if not url.isValid():
|
||||
urlutils.invalid_url_error(win_id, url, "save quickmark")
|
||||
return
|
||||
urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded)
|
||||
message.ask_async(
|
||||
win_id, "Add quickmark:", usertypes.PromptMode.text,
|
||||
functools.partial(self.quickmark_add, win_id, urlstr))
|
||||
|
||||
@cmdutils.register(instance='quickmark-manager', win_id='win_id')
|
||||
def quickmark_add(self, win_id, url, name):
|
||||
"""Add a new quickmark.
|
||||
|
||||
Args:
|
||||
win_id: The window ID to display the errors in.
|
||||
url: The url to add as quickmark.
|
||||
name: The name for the new quickmark.
|
||||
"""
|
||||
# We don't raise cmdexc.CommandError here as this can be called async
|
||||
# via prompt_save.
|
||||
if not name:
|
||||
message.error(win_id, "Can't set mark with empty name!")
|
||||
return
|
||||
if not url:
|
||||
message.error(win_id, "Can't set mark with empty URL!")
|
||||
return
|
||||
|
||||
def set_mark():
|
||||
"""Really set the quickmark."""
|
||||
self.marks[name] = url
|
||||
self.changed.emit()
|
||||
self.added.emit(name, url)
|
||||
|
||||
if name in self.marks:
|
||||
message.confirm_async(
|
||||
win_id, "Override existing quickmark?", set_mark, default=True)
|
||||
else:
|
||||
set_mark()
|
||||
|
||||
@cmdutils.register(instance='quickmark-manager', maxsplit=0,
|
||||
completion=[usertypes.Completion.quickmark_by_name])
|
||||
def quickmark_del(self, name):
|
||||
"""Delete a quickmark.
|
||||
|
||||
Args:
|
||||
name: The name of the quickmark to delete.
|
||||
"""
|
||||
try:
|
||||
self.delete(name)
|
||||
except KeyError:
|
||||
raise cmdexc.CommandError("Quickmark '{}' not found!".format(name))
|
||||
|
||||
def get(self, name):
|
||||
"""Get the URL of the quickmark named name as a QUrl."""
|
||||
if name not in self.marks:
|
||||
raise DoesNotExistError(
|
||||
"Quickmark '{}' does not exist!".format(name))
|
||||
urlstr = self.marks[name]
|
||||
try:
|
||||
url = urlutils.fuzzy_url(urlstr, do_search=False)
|
||||
except urlutils.InvalidUrlError as e:
|
||||
raise InvalidUrlError(
|
||||
"Invalid URL for quickmark {}: {}".format(name, str(e)))
|
||||
return url
|
||||
|
||||
|
||||
class BookmarkManager(UrlMarkManager):
|
||||
|
||||
"""Manager for bookmarks.
|
||||
|
||||
The primary key for bookmarks is their *url*, this means:
|
||||
|
||||
- self.marks maps URLs to titles.
|
||||
- changed gets emitted with the URL as first argument and the title as
|
||||
second argument.
|
||||
- removed gets emitted with the URL as argument.
|
||||
"""
|
||||
|
||||
def _init_lineparser(self):
|
||||
bookmarks_directory = os.path.join(standarddir.config(), 'bookmarks')
|
||||
if not os.path.isdir(bookmarks_directory):
|
||||
os.makedirs(bookmarks_directory)
|
||||
self._lineparser = lineparser.LineParser(
|
||||
standarddir.config(), 'bookmarks/urls', parent=self)
|
||||
|
||||
def _init_savemanager(self, save_manager):
|
||||
filename = os.path.join(standarddir.config(), 'bookmarks/urls')
|
||||
save_manager.add_saveable('bookmark-manager', self.save, self.changed,
|
||||
filename=filename)
|
||||
|
||||
def _parse_line(self, line):
|
||||
parts = line.split(maxsplit=1)
|
||||
if len(parts) == 2:
|
||||
self.marks[parts[0]] = parts[1]
|
||||
elif len(parts) == 1:
|
||||
self.marks[parts[0]] = ''
|
||||
|
||||
def add(self, url, title):
|
||||
"""Add a new bookmark.
|
||||
|
||||
Args:
|
||||
url: The url to add as bookmark.
|
||||
title: The title for the new bookmark.
|
||||
"""
|
||||
if not url.isValid():
|
||||
errstr = urlutils.get_errstring(url)
|
||||
raise InvalidUrlError(errstr)
|
||||
|
||||
urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded)
|
||||
|
||||
if urlstr in self.marks:
|
||||
raise AlreadyExistsError("Bookmark already exists!")
|
||||
else:
|
||||
self.marks[urlstr] = title
|
||||
self.changed.emit()
|
||||
self.added.emit(title, urlstr)
|
||||
|
||||
@cmdutils.register(instance='bookmark-manager', maxsplit=0,
|
||||
completion=[usertypes.Completion.bookmark_by_url])
|
||||
def bookmark_del(self, url):
|
||||
"""Delete a bookmark.
|
||||
|
||||
Args:
|
||||
url: The URL of the bookmark to delete.
|
||||
"""
|
||||
try:
|
||||
self.delete(url)
|
||||
except KeyError:
|
||||
raise cmdexc.CommandError("Bookmark '{}' not found!".format(url))
|
||||
@@ -43,18 +43,23 @@ Group = usertypes.enum('Group', ['all', 'links', 'images', 'url', 'prevnext',
|
||||
|
||||
SELECTORS = {
|
||||
Group.all: ('a, area, textarea, select, input:not([type=hidden]), button, '
|
||||
'frame, iframe, [onclick], [onmousedown], [role=link], '
|
||||
'frame, iframe, link, [onclick], [onmousedown], [role=link], '
|
||||
'[role=option], [role=button], img'),
|
||||
Group.links: 'a, area, link, [role=link]',
|
||||
Group.images: 'img',
|
||||
Group.url: '[src], [href]',
|
||||
Group.prevnext: 'a, area, button, [role=button]',
|
||||
Group.prevnext: 'a, area, button, link, [role=button]',
|
||||
Group.focus: '*:focus',
|
||||
}
|
||||
|
||||
|
||||
def filter_links(elem):
|
||||
return 'href' in elem and QUrl(elem['href']).scheme() != 'javascript'
|
||||
|
||||
|
||||
FILTERS = {
|
||||
Group.links: (lambda e: 'href' in e and
|
||||
QUrl(e['href']).scheme() != 'javascript'),
|
||||
Group.links: filter_links,
|
||||
Group.prevnext: filter_links,
|
||||
}
|
||||
|
||||
|
||||
@@ -136,7 +141,7 @@ class WebElementWrapper(collections.abc.MutableMapping):
|
||||
self._check_vanished()
|
||||
if key not in self:
|
||||
raise KeyError(key)
|
||||
self.removeAttribute(key)
|
||||
self._elem.removeAttribute(key)
|
||||
|
||||
def __contains__(self, key):
|
||||
self._check_vanished()
|
||||
@@ -179,8 +184,6 @@ class WebElementWrapper(collections.abc.MutableMapping):
|
||||
def is_content_editable(self):
|
||||
"""Check if an element has a contenteditable attribute.
|
||||
|
||||
FIXME: Add tests.
|
||||
|
||||
Args:
|
||||
elem: The QWebElement to check.
|
||||
|
||||
@@ -240,12 +243,11 @@ class WebElementWrapper(collections.abc.MutableMapping):
|
||||
for klass in self._elem.classes():
|
||||
if any([klass.startswith(e) for e in div_classes]):
|
||||
return True
|
||||
return False
|
||||
|
||||
def is_editable(self, strict=False):
|
||||
"""Check whether we should switch to insert mode for this element.
|
||||
|
||||
FIXME: add tests
|
||||
|
||||
Args:
|
||||
strict: Whether to do stricter checking so only fields where we can
|
||||
get the value match, for use with the :editor command.
|
||||
@@ -261,7 +263,7 @@ class WebElementWrapper(collections.abc.MutableMapping):
|
||||
tag = self._elem.tagName().lower()
|
||||
if self.is_content_editable() and self.is_writable():
|
||||
return True
|
||||
elif self.get('role', None) in roles:
|
||||
elif self.get('role', None) in roles and self.is_writable():
|
||||
return True
|
||||
elif tag == 'input':
|
||||
return self._is_editable_input()
|
||||
@@ -279,6 +281,7 @@ class WebElementWrapper(collections.abc.MutableMapping):
|
||||
|
||||
def is_text_input(self):
|
||||
"""Check if this element is some kind of text box."""
|
||||
self._check_vanished()
|
||||
roles = ('combobox', 'textbox')
|
||||
tag = self._elem.tagName().lower()
|
||||
return self.get('role', None) in roles or tag in ('input', 'textarea')
|
||||
@@ -303,6 +306,8 @@ def javascript_escape(text):
|
||||
("'", r"\'"), # Then escape ' and " as \' and \".
|
||||
('"', r'\"'), # (note it won't hurt when we escape the wrong one).
|
||||
('\n', r'\n'), # We also need to escape newlines for some reason.
|
||||
('\r', r'\r'),
|
||||
('\x00', r'\x00'),
|
||||
)
|
||||
for orig, repl in replacements:
|
||||
text = text.replace(orig, repl)
|
||||
@@ -312,7 +317,7 @@ def javascript_escape(text):
|
||||
def get_child_frames(startframe):
|
||||
"""Get all children recursively of a given QWebFrame.
|
||||
|
||||
Loosly based on http://blog.nextgenetics.net/?e=64
|
||||
Loosely based on http://blog.nextgenetics.net/?e=64
|
||||
|
||||
Args:
|
||||
startframe: The QWebFrame to start with.
|
||||
@@ -334,8 +339,6 @@ def get_child_frames(startframe):
|
||||
def focus_elem(frame):
|
||||
"""Get the focused element in a web frame.
|
||||
|
||||
FIXME: Add tests.
|
||||
|
||||
Args:
|
||||
frame: The QWebFrame to search in.
|
||||
"""
|
||||
|
||||
@@ -46,7 +46,7 @@ class BrowserPage(QWebPage):
|
||||
("normal", "tab", "tab_bg")
|
||||
_hint_target: Override for open_target while hinting, or None.
|
||||
_extension_handlers: Mapping of QWebPage extensions to their handlers.
|
||||
_networkmnager: The NetworkManager used.
|
||||
_networkmanager: The NetworkManager used.
|
||||
_win_id: The window ID this BrowserPage is associated with.
|
||||
_ignore_load_started: Whether to ignore the next loadStarted signal.
|
||||
_is_shutting_down: Whether the page is currently shutting down.
|
||||
@@ -109,7 +109,7 @@ class BrowserPage(QWebPage):
|
||||
def _handle_errorpage(self, info, errpage):
|
||||
"""Display an error page if needed.
|
||||
|
||||
Loosly based on Helpviewer/HelpBrowserWV.py from eric5
|
||||
Loosely based on Helpviewer/HelpBrowserWV.py from eric5
|
||||
(line 260 @ 5d937eb378dd)
|
||||
|
||||
Args:
|
||||
@@ -178,7 +178,7 @@ class BrowserPage(QWebPage):
|
||||
def _handle_multiple_files(self, info, files):
|
||||
"""Handle uploading of multiple files.
|
||||
|
||||
Loosly based on Helpviewer/HelpBrowserWV.py from eric5.
|
||||
Loosely based on Helpviewer/HelpBrowserWV.py from eric5.
|
||||
|
||||
Args:
|
||||
info: The ChooseMultipleFilesExtensionOption instance.
|
||||
@@ -241,7 +241,7 @@ class BrowserPage(QWebPage):
|
||||
if cur_data is not None:
|
||||
frame = self.mainFrame()
|
||||
if 'zoom' in cur_data:
|
||||
frame.setZoomFactor(cur_data['zoom'])
|
||||
frame.page().view().zoom_perc(cur_data['zoom'] * 100)
|
||||
if ('scroll-pos' in cur_data and
|
||||
frame.scrollPosition() == QPoint(0, 0)):
|
||||
QTimer.singleShot(0, functools.partial(
|
||||
@@ -325,7 +325,8 @@ class BrowserPage(QWebPage):
|
||||
QWebPage.Notifications: ('content', 'notifications'),
|
||||
QWebPage.Geolocation: ('content', 'geolocation'),
|
||||
}
|
||||
if config.get(*options[feature]) == 'ask':
|
||||
config_val = config.get(*options[feature])
|
||||
if config_val == 'ask':
|
||||
bridge = objreg.get('message-bridge', scope='window',
|
||||
window=self._win_id)
|
||||
q = usertypes.Question(bridge)
|
||||
@@ -361,6 +362,9 @@ class BrowserPage(QWebPage):
|
||||
self.loadStarted.connect(q.abort)
|
||||
|
||||
bridge.ask(q, blocking=False)
|
||||
elif config_val:
|
||||
self.setFeaturePermission(frame, feature,
|
||||
QWebPage.PermissionGrantedByUser)
|
||||
else:
|
||||
self.setFeaturePermission(frame, feature,
|
||||
QWebPage.PermissionDeniedByUser)
|
||||
@@ -414,7 +418,7 @@ class BrowserPage(QWebPage):
|
||||
if data is None:
|
||||
return
|
||||
if 'zoom' in data:
|
||||
frame.setZoomFactor(data['zoom'])
|
||||
frame.page().view().zoom_perc(data['zoom'] * 100)
|
||||
if 'scroll-pos' in data and frame.scrollPosition() == QPoint(0, 0):
|
||||
frame.setScrollPosition(data['scroll-pos'])
|
||||
|
||||
@@ -423,14 +427,10 @@ class BrowserPage(QWebPage):
|
||||
"""Emitted before a hinting-click takes place.
|
||||
|
||||
Args:
|
||||
hint_target: A string to set self._hint_target to.
|
||||
hint_target: A ClickTarget member to set self._hint_target to.
|
||||
"""
|
||||
t = getattr(usertypes.ClickTarget, hint_target, None)
|
||||
if t is None:
|
||||
return
|
||||
log.webview.debug("Setting force target to {}/{}".format(
|
||||
hint_target, t))
|
||||
self._hint_target = t
|
||||
log.webview.debug("Setting force target to {}".format(hint_target))
|
||||
self._hint_target = hint_target
|
||||
|
||||
@pyqtSlot()
|
||||
def on_stop_hinting(self):
|
||||
@@ -478,17 +478,23 @@ class BrowserPage(QWebPage):
|
||||
return super().extension(ext, opt, out)
|
||||
return handler(opt, out)
|
||||
|
||||
def javaScriptAlert(self, _frame, msg):
|
||||
def javaScriptAlert(self, frame, msg):
|
||||
"""Override javaScriptAlert to use the statusbar."""
|
||||
log.js.debug("alert: {}".format(msg))
|
||||
if config.get('ui', 'modal-js-dialog'):
|
||||
return super().javaScriptAlert(frame, msg)
|
||||
|
||||
if (self._is_shutting_down or
|
||||
config.get('content', 'ignore-javascript-alert')):
|
||||
return
|
||||
self._ask("[js alert] {}".format(msg), usertypes.PromptMode.alert)
|
||||
|
||||
def javaScriptConfirm(self, _frame, msg):
|
||||
def javaScriptConfirm(self, frame, msg):
|
||||
"""Override javaScriptConfirm to use the statusbar."""
|
||||
log.js.debug("confirm: {}".format(msg))
|
||||
if config.get('ui', 'modal-js-dialog'):
|
||||
return super().javaScriptConfirm(frame, msg)
|
||||
|
||||
if self._is_shutting_down:
|
||||
return False
|
||||
ans = self._ask("[js confirm] {}".format(msg),
|
||||
|
||||
@@ -24,6 +24,7 @@ import itertools
|
||||
import functools
|
||||
|
||||
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
|
||||
@@ -32,7 +33,6 @@ from qutebrowser.config import config
|
||||
from qutebrowser.keyinput import modeman
|
||||
from qutebrowser.utils import message, log, usertypes, utils, qtutils, objreg
|
||||
from qutebrowser.browser import webpage, hints, webelem
|
||||
from qutebrowser.commands import cmdexc
|
||||
|
||||
|
||||
LoadStatus = usertypes.enum('LoadStatus', ['none', 'success', 'error', 'warn',
|
||||
@@ -71,6 +71,8 @@ class WebView(QWebView):
|
||||
_check_insertmode: If True, in mouseReleaseEvent we should check if we
|
||||
need to enter/leave insert mode.
|
||||
_default_zoom_changed: Whether the zoom was changed from the default.
|
||||
_ignore_wheel_event: Ignore the next wheel event.
|
||||
See https://github.com/The-Compiler/qutebrowser/issues/395
|
||||
|
||||
Signals:
|
||||
scroll_pos_changed: Scroll percentage of current tab changed.
|
||||
@@ -103,10 +105,13 @@ class WebView(QWebView):
|
||||
self._old_scroll_pos = (-1, -1)
|
||||
self._zoom = None
|
||||
self._has_ssl_errors = False
|
||||
self._ignore_wheel_event = False
|
||||
self.keep_icon = False
|
||||
self.search_text = None
|
||||
self.search_flags = 0
|
||||
self.selection_enabled = False
|
||||
self.init_neighborlist()
|
||||
self._set_bg_color()
|
||||
cfg = objreg.get('config')
|
||||
cfg.changed.connect(self.init_neighborlist)
|
||||
# For some reason, this signal doesn't get disconnected automatically
|
||||
@@ -160,7 +165,7 @@ class WebView(QWebView):
|
||||
return utils.get_repr(self, tab_id=self.tab_id, url=url)
|
||||
|
||||
def __del__(self):
|
||||
# Explicitely releasing the page here seems to prevent some segfaults
|
||||
# Explicitly releasing the page here seems to prevent some segfaults
|
||||
# when quitting.
|
||||
# Copied from:
|
||||
# https://code.google.com/p/webscraping/source/browse/webkit.py#325
|
||||
@@ -180,6 +185,15 @@ class WebView(QWebView):
|
||||
self.load_status = val
|
||||
self.load_status_changed.emit(val.name)
|
||||
|
||||
def _set_bg_color(self):
|
||||
"""Set the webpage background color as configured."""
|
||||
col = config.get('colors', 'webpage.bg')
|
||||
palette = self.palette()
|
||||
if col is None:
|
||||
col = self.style().standardPalette().color(QPalette.Base)
|
||||
palette.setColor(QPalette.Base, col)
|
||||
self.setPalette(palette)
|
||||
|
||||
@pyqtSlot(str, str)
|
||||
def on_config_changed(self, section, option):
|
||||
"""Reinitialize the zoom neighborlist if related config changed."""
|
||||
@@ -194,6 +208,8 @@ class WebView(QWebView):
|
||||
self.setContextMenuPolicy(Qt.PreventContextMenu)
|
||||
else:
|
||||
self.setContextMenuPolicy(Qt.DefaultContextMenu)
|
||||
elif section == 'colors' and option == 'webpage.bg':
|
||||
self._set_bg_color()
|
||||
|
||||
def init_neighborlist(self):
|
||||
"""Initialize the _zoom neighborlist."""
|
||||
@@ -355,9 +371,8 @@ class WebView(QWebView):
|
||||
if fuzzyval:
|
||||
self._zoom.fuzzyval = int(perc)
|
||||
if perc < 0:
|
||||
raise cmdexc.CommandError("Can't zoom {}%!".format(perc))
|
||||
raise ValueError("Can't zoom {}%!".format(perc))
|
||||
self.setZoomFactor(float(perc) / 100)
|
||||
message.info(self.win_id, "Zoom level: {}%".format(perc))
|
||||
self._default_zoom_changed = True
|
||||
|
||||
def zoom(self, offset):
|
||||
@@ -365,9 +380,13 @@ class WebView(QWebView):
|
||||
|
||||
Args:
|
||||
offset: The offset in the zoom level list.
|
||||
|
||||
Return:
|
||||
The new zoom percentage.
|
||||
"""
|
||||
level = self._zoom.getitem(offset)
|
||||
self.zoom_perc(level, fuzzyval=False)
|
||||
return level
|
||||
|
||||
@pyqtSlot('QUrl')
|
||||
def on_url_changed(self, url):
|
||||
@@ -378,6 +397,8 @@ class WebView(QWebView):
|
||||
if url.isValid():
|
||||
self.cur_url = url
|
||||
self.url_text_changed.emit(url.toDisplayString())
|
||||
if not self.title():
|
||||
self.titleChanged.emit(self.url().toDisplayString())
|
||||
|
||||
@pyqtSlot('QMouseEvent')
|
||||
def on_mouse_event(self, evt):
|
||||
@@ -396,7 +417,7 @@ class WebView(QWebView):
|
||||
|
||||
@pyqtSlot()
|
||||
def on_load_finished(self):
|
||||
"""Handle auto-insert-mode after loading finished.
|
||||
"""Handle a finished page load.
|
||||
|
||||
We don't take loadFinished's ok argument here as it always seems to be
|
||||
true when the QWebPage has an ErrorPageExtension implemented.
|
||||
@@ -409,6 +430,12 @@ class WebView(QWebView):
|
||||
self._set_load_status(LoadStatus.warn)
|
||||
else:
|
||||
self._set_load_status(LoadStatus.error)
|
||||
if not self.title():
|
||||
self.titleChanged.emit(self.url().toDisplayString())
|
||||
self._handle_auto_insert_mode(ok)
|
||||
|
||||
def _handle_auto_insert_mode(self, ok):
|
||||
"""Handle auto-insert-mode after loading finished."""
|
||||
if not config.get('input', 'auto-insert-mode'):
|
||||
return
|
||||
mode_manager = objreg.get('mode-manager', scope='window',
|
||||
@@ -435,6 +462,25 @@ class WebView(QWebView):
|
||||
log.webview.debug("Ignoring focus because mode {} was "
|
||||
"entered.".format(mode))
|
||||
self.setFocusPolicy(Qt.NoFocus)
|
||||
elif mode == usertypes.KeyMode.caret:
|
||||
settings = self.settings()
|
||||
settings.setAttribute(QWebSettings.CaretBrowsingEnabled, True)
|
||||
self.selection_enabled = bool(self.page().selectedText())
|
||||
|
||||
if self.isVisible():
|
||||
# Sometimes the caret isn't immediately visible, but unfocusing
|
||||
# and refocusing it fixes that.
|
||||
self.clearFocus()
|
||||
self.setFocus(Qt.OtherFocusReason)
|
||||
|
||||
# Move the caret to the first element in the viewport if there
|
||||
# isn't any text which is already selected.
|
||||
#
|
||||
# Note: We can't use hasSelection() here, as that's always
|
||||
# true in caret mode.
|
||||
if not self.page().selectedText():
|
||||
self.page().currentFrame().evaluateJavaScript(
|
||||
utils.read_file('javascript/position_caret.js'))
|
||||
|
||||
@pyqtSlot(usertypes.KeyMode)
|
||||
def on_mode_left(self, mode):
|
||||
@@ -443,6 +489,15 @@ class WebView(QWebView):
|
||||
usertypes.KeyMode.yesno):
|
||||
log.webview.debug("Restoring focus policy because mode {} was "
|
||||
"left.".format(mode))
|
||||
elif mode == usertypes.KeyMode.caret:
|
||||
settings = self.settings()
|
||||
if settings.testAttribute(QWebSettings.CaretBrowsingEnabled):
|
||||
if self.selection_enabled and self.hasSelection():
|
||||
# Remove selection if it exists
|
||||
self.triggerPageAction(QWebPage.MoveToNextChar)
|
||||
settings.setAttribute(QWebSettings.CaretBrowsingEnabled, False)
|
||||
self.selection_enabled = False
|
||||
|
||||
self.setFocusPolicy(Qt.WheelFocus)
|
||||
|
||||
def search(self, text, flags):
|
||||
@@ -457,12 +512,25 @@ class WebView(QWebView):
|
||||
old_scroll_pos = self.scroll_pos
|
||||
flags = QWebPage.FindFlags(flags)
|
||||
found = self.findText(text, flags)
|
||||
if not found and not flags & QWebPage.HighlightAllOccurrences and text:
|
||||
message.error(self.win_id, "Text '{}' not found on "
|
||||
"page!".format(text), immediately=True)
|
||||
else:
|
||||
backward = int(flags) & QWebPage.FindBackward
|
||||
backward = flags & QWebPage.FindBackward
|
||||
|
||||
if not found and not flags & QWebPage.HighlightAllOccurrences and text:
|
||||
# User disabled wrapping; but findText() just returns False. If we
|
||||
# have a selection, we know there's a match *somewhere* on the page
|
||||
if (not flags & QWebPage.FindWrapsAroundDocument and
|
||||
self.hasSelection()):
|
||||
if not backward:
|
||||
message.warning(self.win_id, "Search hit BOTTOM without "
|
||||
"match for: {}".format(text),
|
||||
immediately=True)
|
||||
else:
|
||||
message.warning(self.win_id, "Search hit TOP without "
|
||||
"match for: {}".format(text),
|
||||
immediately=True)
|
||||
else:
|
||||
message.error(self.win_id, "Text '{}' not found on "
|
||||
"page!".format(text), immediately=True)
|
||||
else:
|
||||
def check_scroll_pos():
|
||||
"""Check if the scroll position got smaller and show info."""
|
||||
if not backward and self.scroll_pos < old_scroll_pos:
|
||||
@@ -551,6 +619,7 @@ class WebView(QWebView):
|
||||
return
|
||||
self._mousepress_insertmode(e)
|
||||
self._mousepress_opentarget(e)
|
||||
self._ignore_wheel_event = True
|
||||
super().mousePressEvent(e)
|
||||
|
||||
def mouseReleaseEvent(self, e):
|
||||
@@ -564,6 +633,7 @@ class WebView(QWebView):
|
||||
"""Save a reference to the context menu so we can close it."""
|
||||
menu = self.page().createStandardContextMenu()
|
||||
self.shutting_down.connect(menu.close)
|
||||
modeman.instance(self.win_id).entered.connect(menu.close)
|
||||
menu.exec_(e.globalPos())
|
||||
|
||||
def wheelEvent(self, e):
|
||||
@@ -572,6 +642,10 @@ class WebView(QWebView):
|
||||
Args:
|
||||
e: The QWheelEvent.
|
||||
"""
|
||||
if self._ignore_wheel_event:
|
||||
self._ignore_wheel_event = False
|
||||
# See https://github.com/The-Compiler/qutebrowser/issues/395
|
||||
return
|
||||
if e.modifiers() & Qt.ControlModifier:
|
||||
e.accept()
|
||||
divider = config.get('input', 'mouse-zoom-divider')
|
||||
|
||||
@@ -81,7 +81,7 @@ class ArgumentParser(argparse.ArgumentParser):
|
||||
raise ArgumentParserExit(status, msg)
|
||||
|
||||
def error(self, msg):
|
||||
raise ArgumentParserError(msg[0].upper() + msg[1:])
|
||||
raise ArgumentParserError(msg.capitalize())
|
||||
|
||||
|
||||
def enum_getter(enum):
|
||||
@@ -101,11 +101,11 @@ def enum_getter(enum):
|
||||
return _get_enum_item
|
||||
|
||||
|
||||
def multitype_conv(tpl):
|
||||
def multitype_conv(types):
|
||||
"""Function factory to get a type converter for a choice of types."""
|
||||
def _convert(value):
|
||||
"""Convert a value according to an iterable of possible arg types."""
|
||||
for typ in set(tpl):
|
||||
for typ in set(types):
|
||||
if isinstance(typ, str):
|
||||
if value == typ:
|
||||
return value
|
||||
@@ -119,6 +119,8 @@ def multitype_conv(tpl):
|
||||
return typ(value)
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
else:
|
||||
raise ValueError("Unknown type {!r}!".format(typ))
|
||||
raise cmdexc.ArgumentTypeError('Invalid value {}.'.format(value))
|
||||
|
||||
return _convert
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
Module attributes:
|
||||
cmd_dict: A mapping from command-strings to command objects.
|
||||
aliases: A list of all aliases, needed for doc generation.
|
||||
"""
|
||||
|
||||
from qutebrowser.utils import qtutils, log
|
||||
|
||||
@@ -29,6 +29,11 @@ from qutebrowser.utils import log, utils, message, docutils, objreg, usertypes
|
||||
from qutebrowser.utils import debug as debug_utils
|
||||
|
||||
|
||||
def arg_name(name):
|
||||
"""Get the name an argument should have based on its Python name."""
|
||||
return name.rstrip('_').replace('_', '-')
|
||||
|
||||
|
||||
class Command:
|
||||
|
||||
"""Base skeleton for a command.
|
||||
@@ -44,12 +49,11 @@ class Command:
|
||||
completion: Completions to use for arguments, as a list of strings.
|
||||
debug: Whether this is a debugging command (only shown with --debug).
|
||||
parser: The ArgumentParser to use to parse this command.
|
||||
special_params: A dict with the names of the special parameters as
|
||||
values.
|
||||
count_arg: The name of the count parameter, or None.
|
||||
win_id_arg: The name of the win_id parameter, or None.
|
||||
flags_with_args: A list of flags which take an argument.
|
||||
no_cmd_split: If true, ';;' to split sub-commands is ignored.
|
||||
_type_conv: A mapping of conversion functions for arguments.
|
||||
_name_conv: A mapping of argument names to parameter names.
|
||||
_needs_js: Whether the command needs javascript enabled
|
||||
_modes: The modes the command can be executed in.
|
||||
_not_modes: The modes the command can not be executed in.
|
||||
@@ -62,13 +66,14 @@ class Command:
|
||||
"""
|
||||
|
||||
AnnotationInfo = collections.namedtuple('AnnotationInfo',
|
||||
['kwargs', 'type', 'name', 'flag',
|
||||
'special'])
|
||||
['kwargs', 'type', 'flag', 'hide',
|
||||
'metavar'])
|
||||
|
||||
def __init__(self, *, handler, name, instance=None, maxsplit=None,
|
||||
hide=False, completion=None, modes=None, not_modes=None,
|
||||
needs_js=False, debug=False, ignore_args=False,
|
||||
deprecated=False, no_cmd_split=False, scope='global'):
|
||||
deprecated=False, no_cmd_split=False, scope='global',
|
||||
count=None, win_id=None):
|
||||
# I really don't know how to solve this in a better way, I tried.
|
||||
# pylint: disable=too-many-arguments,too-many-locals
|
||||
if modes is not None and not_modes is not None:
|
||||
@@ -81,6 +86,9 @@ class Command:
|
||||
for m in not_modes:
|
||||
if not isinstance(m, usertypes.KeyMode):
|
||||
raise TypeError("Mode {} is no KeyMode member!".format(m))
|
||||
if scope != 'global' and instance is None:
|
||||
raise ValueError("Setting scope without setting instance makes "
|
||||
"no sense!")
|
||||
self.name = name
|
||||
self.maxsplit = maxsplit
|
||||
self.hide = hide
|
||||
@@ -95,6 +103,8 @@ class Command:
|
||||
self.ignore_args = ignore_args
|
||||
self.handler = handler
|
||||
self.no_cmd_split = no_cmd_split
|
||||
self.count_arg = count
|
||||
self.win_id_arg = win_id
|
||||
self.docparser = docutils.DocstringParser(handler)
|
||||
self.parser = argparser.ArgumentParser(
|
||||
name, description=self.docparser.short_desc,
|
||||
@@ -107,11 +117,9 @@ class Command:
|
||||
self.namespace = None
|
||||
self._count = None
|
||||
self.pos_args = []
|
||||
self.special_params = {'count': None, 'win_id': None}
|
||||
self.desc = None
|
||||
self.flags_with_args = []
|
||||
self._type_conv = {}
|
||||
self._name_conv = {}
|
||||
count = self._inspect_func()
|
||||
if self.completion is not None and len(self.completion) > count:
|
||||
raise ValueError("Got {} completions, but only {} "
|
||||
@@ -153,7 +161,8 @@ class Command:
|
||||
elif 'self' not in signature.parameters and self._instance is not None:
|
||||
raise TypeError("{} is not a class method, but instance was "
|
||||
"given!".format(self.name[0]))
|
||||
elif inspect.getfullargspec(self.handler).varkw is not None:
|
||||
elif any(param.kind == inspect.Parameter.VAR_KEYWORD
|
||||
for param in signature.parameters.values()):
|
||||
raise TypeError("{}: functions with varkw arguments are not "
|
||||
"supported!".format(self.name[0]))
|
||||
|
||||
@@ -173,52 +182,22 @@ class Command:
|
||||
type_conv[param.name] = argparser.multitype_conv(typ)
|
||||
return type_conv
|
||||
|
||||
def _get_nameconv(self, param, annotation_info):
|
||||
"""Get a dict with a name conversion for the parameter.
|
||||
|
||||
Args:
|
||||
param: The inspect.Parameter to handle.
|
||||
annotation_info: The AnnotationInfo tuple for the parameter.
|
||||
"""
|
||||
d = {}
|
||||
if annotation_info.name is not None:
|
||||
d[param.name] = annotation_info.name
|
||||
return d
|
||||
|
||||
def _inspect_special_param(self, param, annotation_info):
|
||||
def _inspect_special_param(self, param):
|
||||
"""Check if the given parameter is a special one.
|
||||
|
||||
Args:
|
||||
param: The inspect.Parameter to handle.
|
||||
annotation_info: The AnnotationInfo tuple for the parameter.
|
||||
|
||||
Return:
|
||||
True if the parameter is special, False otherwise.
|
||||
"""
|
||||
special = annotation_info.special
|
||||
if special == 'count':
|
||||
if self.special_params['count'] is not None:
|
||||
raise ValueError("Registered multiple parameters ({}/{}) as "
|
||||
"count!".format(self.special_params['count'],
|
||||
param.name))
|
||||
if param.name == self.count_arg:
|
||||
if param.default is inspect.Parameter.empty:
|
||||
raise TypeError("{}: handler has count parameter "
|
||||
"without default!".format(self.name))
|
||||
self.special_params['count'] = param.name
|
||||
return True
|
||||
elif special == 'win_id':
|
||||
if self.special_params['win_id'] is not None:
|
||||
raise ValueError("Registered multiple parameters ({}/{}) as "
|
||||
"win_id!".format(
|
||||
self.special_params['win_id'],
|
||||
param.name))
|
||||
self.special_params['win_id'] = param.name
|
||||
elif param.name == self.win_id_arg:
|
||||
return True
|
||||
elif special is None:
|
||||
return False
|
||||
else:
|
||||
raise ValueError("{}: Invalid value '{}' for 'special' "
|
||||
"annotation!".format(self.name, special))
|
||||
|
||||
def _inspect_func(self):
|
||||
"""Inspect the function to get useful informations from it.
|
||||
@@ -236,20 +215,28 @@ class Command:
|
||||
self.desc = doc.splitlines()[0].strip()
|
||||
else:
|
||||
self.desc = ""
|
||||
|
||||
if (self.count_arg is not None and
|
||||
self.count_arg not in signature.parameters):
|
||||
raise ValueError("count parameter {} does not exist!".format(
|
||||
self.count_arg))
|
||||
if (self.win_id_arg is not None and
|
||||
self.win_id_arg not in signature.parameters):
|
||||
raise ValueError("win_id parameter {} does not exist!".format(
|
||||
self.win_id_arg))
|
||||
|
||||
if not self.ignore_args:
|
||||
for param in signature.parameters.values():
|
||||
annotation_info = self._parse_annotation(param)
|
||||
if param.name == 'self':
|
||||
continue
|
||||
if self._inspect_special_param(param, annotation_info):
|
||||
if self._inspect_special_param(param):
|
||||
continue
|
||||
arg_count += 1
|
||||
typ = self._get_type(param, annotation_info)
|
||||
kwargs = self._param_to_argparse_kwargs(param, annotation_info)
|
||||
args = self._param_to_argparse_args(param, annotation_info)
|
||||
self._type_conv.update(self._get_typeconv(param, typ))
|
||||
self._name_conv.update(
|
||||
self._get_nameconv(param, annotation_info))
|
||||
callsig = debug_utils.format_call(
|
||||
self.parser.add_argument, args, kwargs,
|
||||
full=False)
|
||||
@@ -276,11 +263,13 @@ class Command:
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
kwargs['dest'] = param.name
|
||||
|
||||
if isinstance(typ, tuple):
|
||||
pass
|
||||
kwargs['metavar'] = annotation_info.metavar or param.name
|
||||
elif utils.is_enum(typ):
|
||||
kwargs['choices'] = [e.name.replace('_', '-') for e in typ]
|
||||
kwargs['metavar'] = param.name
|
||||
kwargs['choices'] = [arg_name(e.name) for e in typ]
|
||||
kwargs['metavar'] = annotation_info.metavar or param.name
|
||||
elif typ is bool:
|
||||
kwargs['action'] = 'store_true'
|
||||
elif typ is not None:
|
||||
@@ -307,8 +296,8 @@ class Command:
|
||||
A list of args.
|
||||
"""
|
||||
args = []
|
||||
name = annotation_info.name or param.name
|
||||
shortname = annotation_info.flag or param.name[0]
|
||||
name = arg_name(param.name)
|
||||
shortname = annotation_info.flag or name[0]
|
||||
if len(shortname) != 1:
|
||||
raise ValueError("Flag '{}' of parameter {} (command {}) must be "
|
||||
"exactly 1 char!".format(shortname, name,
|
||||
@@ -323,8 +312,8 @@ class Command:
|
||||
if typ is not bool:
|
||||
self.flags_with_args += [short_flag, long_flag]
|
||||
else:
|
||||
args.append(name)
|
||||
self.pos_args.append((param.name, name))
|
||||
if not annotation_info.hide:
|
||||
self.pos_args.append((param.name, name))
|
||||
return args
|
||||
|
||||
def _parse_annotation(self, param):
|
||||
@@ -341,12 +330,12 @@ class Command:
|
||||
flag: The short name/flag if overridden.
|
||||
name: The long name if overridden.
|
||||
"""
|
||||
info = {'kwargs': {}, 'type': None, 'flag': None, 'name': None,
|
||||
'special': None}
|
||||
info = {'kwargs': {}, 'type': None, 'flag': None, 'hide': False,
|
||||
'metavar': None}
|
||||
if param.annotation is not inspect.Parameter.empty:
|
||||
log.commands.vdebug("Parsing annotation {}".format(
|
||||
param.annotation))
|
||||
for field in ('type', 'flag', 'name', 'special'):
|
||||
for field in ('type', 'flag', 'name', 'hide', 'metavar'):
|
||||
if field in param.annotation:
|
||||
info[field] = param.annotation[field]
|
||||
if 'nargs' in param.annotation:
|
||||
@@ -426,19 +415,18 @@ class Command:
|
||||
raise TypeError("{}: invalid parameter type {} for argument "
|
||||
"{!r}!".format(self.name, param.kind, param.name))
|
||||
|
||||
def _get_param_name_and_value(self, param):
|
||||
"""Get the converted name and value for an inspect.Parameter."""
|
||||
name = self._name_conv.get(param.name, param.name)
|
||||
value = getattr(self.namespace, name)
|
||||
def _get_param_value(self, param):
|
||||
"""Get the converted value for an inspect.Parameter."""
|
||||
value = getattr(self.namespace, param.name)
|
||||
if param.name in self._type_conv:
|
||||
# We convert enum types after getting the values from
|
||||
# argparse, because argparse's choices argument is
|
||||
# processed after type conversation, which is not what we
|
||||
# want.
|
||||
value = self._type_conv[param.name](value)
|
||||
return name, value
|
||||
return value
|
||||
|
||||
def _get_call_args(self, win_id): # noqa
|
||||
def _get_call_args(self, win_id):
|
||||
"""Get arguments for a function call.
|
||||
|
||||
Args:
|
||||
@@ -462,22 +450,22 @@ class Command:
|
||||
# Special case for 'self'.
|
||||
self._get_self_arg(win_id, param, args)
|
||||
continue
|
||||
elif param.name == self.special_params['count']:
|
||||
elif param.name == self.count_arg:
|
||||
# Special case for count parameter.
|
||||
self._get_count_arg(param, args, kwargs)
|
||||
continue
|
||||
elif param.name == self.special_params['win_id']:
|
||||
elif param.name == self.win_id_arg:
|
||||
# Special case for win_id parameter.
|
||||
self._get_win_id_arg(win_id, param, args, kwargs)
|
||||
continue
|
||||
name, value = self._get_param_name_and_value(param)
|
||||
value = self._get_param_value(param)
|
||||
if param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD:
|
||||
args.append(value)
|
||||
elif param.kind == inspect.Parameter.VAR_POSITIONAL:
|
||||
if value is not None:
|
||||
args += value
|
||||
elif param.kind == inspect.Parameter.KEYWORD_ONLY:
|
||||
kwargs[name] = value
|
||||
kwargs[param.name] = value
|
||||
else:
|
||||
raise TypeError("{}: Invalid parameter type {} for argument "
|
||||
"'{}'!".format(
|
||||
|
||||
@@ -17,18 +17,18 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Functions to execute an userscript."""
|
||||
"""Functions to execute a userscript."""
|
||||
|
||||
import os
|
||||
import os.path
|
||||
import tempfile
|
||||
|
||||
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, QObject, QSocketNotifier,
|
||||
QProcessEnvironment, QProcess)
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QSocketNotifier
|
||||
|
||||
from qutebrowser.utils import message, log, objreg, standarddir
|
||||
from qutebrowser.commands import runners, cmdexc
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.misc import guiprocess
|
||||
|
||||
|
||||
class _QtFIFOReader(QObject):
|
||||
@@ -70,13 +70,9 @@ class _BaseUserscriptRunner(QObject):
|
||||
|
||||
Attributes:
|
||||
_filepath: The path of the file/FIFO which is being read.
|
||||
_proc: The QProcess which is being executed.
|
||||
_proc: The GUIProcess which is being executed.
|
||||
_win_id: The window ID this runner is associated with.
|
||||
|
||||
Class attributes:
|
||||
PROCESS_MESSAGES: A mapping of QProcess::ProcessError members to
|
||||
human-readable error strings.
|
||||
|
||||
Signals:
|
||||
got_cmd: Emitted when a new command arrived and should be executed.
|
||||
finished: Emitted when the userscript finished running.
|
||||
@@ -85,82 +81,75 @@ class _BaseUserscriptRunner(QObject):
|
||||
got_cmd = pyqtSignal(str)
|
||||
finished = pyqtSignal()
|
||||
|
||||
PROCESS_MESSAGES = {
|
||||
QProcess.FailedToStart: "The process failed to start.",
|
||||
QProcess.Crashed: "The process crashed.",
|
||||
QProcess.Timedout: "The last waitFor...() function timed out.",
|
||||
QProcess.WriteError: ("An error occurred when attempting to write to "
|
||||
"the process."),
|
||||
QProcess.ReadError: ("An error occurred when attempting to read from "
|
||||
"the process."),
|
||||
QProcess.UnknownError: "An unknown error occurred.",
|
||||
}
|
||||
|
||||
def __init__(self, win_id, parent=None):
|
||||
super().__init__(parent)
|
||||
self._win_id = win_id
|
||||
self._filepath = None
|
||||
self._proc = None
|
||||
self._env = None
|
||||
|
||||
def _run_process(self, cmd, *args, env):
|
||||
"""Start the given command via QProcess.
|
||||
def _run_process(self, cmd, *args, env, verbose):
|
||||
"""Start the given command.
|
||||
|
||||
Args:
|
||||
cmd: The command to be started.
|
||||
*args: The arguments to hand to the command
|
||||
env: A dictionary of environment variables to add.
|
||||
verbose: Show notifications when the command started/exited.
|
||||
"""
|
||||
self._proc = QProcess(self)
|
||||
procenv = QProcessEnvironment.systemEnvironment()
|
||||
procenv.insert('QUTE_FIFO', self._filepath)
|
||||
if env is not None:
|
||||
for k, v in env.items():
|
||||
procenv.insert(k, v)
|
||||
self._proc.setProcessEnvironment(procenv)
|
||||
self._env = {'QUTE_FIFO': self._filepath}
|
||||
self._env.update(env)
|
||||
self._proc = guiprocess.GUIProcess(self._win_id, 'userscript',
|
||||
additional_env=self._env,
|
||||
verbose=verbose, parent=self)
|
||||
self._proc.error.connect(self.on_proc_error)
|
||||
self._proc.finished.connect(self.on_proc_finished)
|
||||
self._proc.start(cmd, args)
|
||||
|
||||
def _cleanup(self):
|
||||
"""Clean up the temporary file."""
|
||||
log.procs.debug("Deleting temporary file {}.".format(self._filepath))
|
||||
try:
|
||||
os.remove(self._filepath)
|
||||
except OSError as e:
|
||||
# NOTE: Do not replace this with "raise CommandError" as it's
|
||||
# executed async.
|
||||
message.error(self._win_id,
|
||||
"Failed to delete tempfile... ({})".format(e))
|
||||
"""Clean up temporary files."""
|
||||
tempfiles = [self._filepath]
|
||||
if 'QUTE_HTML' in self._env:
|
||||
tempfiles.append(self._env['QUTE_HTML'])
|
||||
if 'QUTE_TEXT' in self._env:
|
||||
tempfiles.append(self._env['QUTE_TEXT'])
|
||||
for fn in tempfiles:
|
||||
log.procs.debug("Deleting temporary file {}.".format(fn))
|
||||
try:
|
||||
os.remove(fn)
|
||||
except OSError as e:
|
||||
# NOTE: Do not replace this with "raise CommandError" as it's
|
||||
# executed async.
|
||||
message.error(
|
||||
self._win_id, "Failed to delete tempfile {} ({})!".format(
|
||||
fn, e))
|
||||
self._filepath = None
|
||||
self._proc = None
|
||||
self._env = None
|
||||
|
||||
def run(self, cmd, *args, env=None):
|
||||
def run(self, cmd, *args, env=None, verbose=False):
|
||||
"""Run the userscript given.
|
||||
|
||||
Needs to be overridden by superclasses.
|
||||
Needs to be overridden by subclasses.
|
||||
|
||||
Args:
|
||||
cmd: The command to be started.
|
||||
*args: The arguments to hand to the command
|
||||
env: A dictionary of environment variables to add.
|
||||
verbose: Show notifications when the command started/exited.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def on_proc_finished(self):
|
||||
"""Called when the process has finished.
|
||||
|
||||
Needs to be overridden by superclasses.
|
||||
Needs to be overridden by subclasses.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def on_proc_error(self, error):
|
||||
"""Called when the process encountered an error."""
|
||||
msg = self.PROCESS_MESSAGES[error]
|
||||
# NOTE: Do not replace this with "raise CommandError" as it's
|
||||
# executed async.
|
||||
message.error(self._win_id,
|
||||
"Error while calling userscript: {}".format(msg))
|
||||
log.procs.debug("Userscript process error: {} - {}".format(error, msg))
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class _POSIXUserscriptRunner(_BaseUserscriptRunner):
|
||||
@@ -177,7 +166,7 @@ class _POSIXUserscriptRunner(_BaseUserscriptRunner):
|
||||
super().__init__(win_id, parent)
|
||||
self._reader = None
|
||||
|
||||
def run(self, cmd, *args, env=None):
|
||||
def run(self, cmd, *args, env=None, verbose=False):
|
||||
try:
|
||||
# tempfile.mktemp is deprecated and discouraged, but we use it here
|
||||
# to create a FIFO since the only other alternative would be to
|
||||
@@ -195,16 +184,14 @@ class _POSIXUserscriptRunner(_BaseUserscriptRunner):
|
||||
self._reader = _QtFIFOReader(self._filepath)
|
||||
self._reader.got_line.connect(self.got_cmd)
|
||||
|
||||
self._run_process(cmd, *args, env=env)
|
||||
self._run_process(cmd, *args, env=env, verbose=verbose)
|
||||
|
||||
def on_proc_finished(self):
|
||||
"""Interrupt the reader when the process finished."""
|
||||
log.procs.debug("Userscript process finished.")
|
||||
self.finish()
|
||||
|
||||
def on_proc_error(self, error):
|
||||
"""Interrupt the reader when the process had an error."""
|
||||
super().on_proc_error(error)
|
||||
self.finish()
|
||||
|
||||
def finish(self):
|
||||
@@ -249,7 +236,6 @@ class _WindowsUserscriptRunner(_BaseUserscriptRunner):
|
||||
|
||||
def on_proc_finished(self):
|
||||
"""Read back the commands when the process finished."""
|
||||
log.procs.debug("Userscript process finished.")
|
||||
try:
|
||||
with open(self._filepath, 'r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
@@ -261,18 +247,17 @@ class _WindowsUserscriptRunner(_BaseUserscriptRunner):
|
||||
|
||||
def on_proc_error(self, error):
|
||||
"""Clean up when the process had an error."""
|
||||
super().on_proc_error(error)
|
||||
self._cleanup()
|
||||
self.finished.emit()
|
||||
|
||||
def run(self, cmd, *args, env=None):
|
||||
def run(self, cmd, *args, env=None, verbose=False):
|
||||
try:
|
||||
self._oshandle, self._filepath = tempfile.mkstemp(text=True)
|
||||
except OSError as e:
|
||||
message.error(self._win_id, "Error while creating tempfile: "
|
||||
"{}".format(e))
|
||||
return
|
||||
self._run_process(cmd, *args, env=env)
|
||||
self._run_process(cmd, *args, env=env, verbose=verbose)
|
||||
|
||||
|
||||
class _DummyUserscriptRunner:
|
||||
@@ -288,8 +273,9 @@ class _DummyUserscriptRunner:
|
||||
|
||||
finished = pyqtSignal()
|
||||
|
||||
def run(self, _cmd, *_args, _env=None):
|
||||
def run(self, cmd, *args, env=None, verbose=False):
|
||||
"""Print an error as userscripts are not supported."""
|
||||
# pylint: disable=unused-argument,unused-variable
|
||||
self.finished.emit()
|
||||
raise cmdexc.CommandError(
|
||||
"Userscripts are not supported on this platform!")
|
||||
@@ -305,14 +291,46 @@ else:
|
||||
UserscriptRunner = _DummyUserscriptRunner
|
||||
|
||||
|
||||
def run(cmd, *args, win_id, env):
|
||||
"""Convenience method to run an userscript.
|
||||
def store_source(frame):
|
||||
"""Store HTML/plaintext in files.
|
||||
|
||||
This writes files containing the HTML/plaintext source of the page, and
|
||||
returns a dict with the paths as QUTE_HTML/QUTE_TEXT.
|
||||
|
||||
Args:
|
||||
frame: The QWebFrame to get the info from, or None to do nothing.
|
||||
|
||||
Return:
|
||||
A dictionary with the needed environment variables.
|
||||
|
||||
Warning:
|
||||
The caller is responsible to delete the files after using them!
|
||||
"""
|
||||
if frame is None:
|
||||
return {}
|
||||
env = {}
|
||||
with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8',
|
||||
suffix='.html',
|
||||
delete=False) as html_file:
|
||||
html_file.write(frame.toHtml())
|
||||
env['QUTE_HTML'] = html_file.name
|
||||
with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8',
|
||||
suffix='.txt',
|
||||
delete=False) as txt_file:
|
||||
txt_file.write(frame.toPlainText())
|
||||
env['QUTE_TEXT'] = txt_file.name
|
||||
return env
|
||||
|
||||
|
||||
def run(cmd, *args, win_id, env, verbose=False):
|
||||
"""Convenience method to run a userscript.
|
||||
|
||||
Args:
|
||||
cmd: The userscript binary to run.
|
||||
*args: The arguments to pass to the userscript.
|
||||
win_id: The window id the userscript is executed in.
|
||||
env: A dictionary of variables to add to the process environment.
|
||||
verbose: Show notifications when the command started/exited.
|
||||
"""
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=win_id)
|
||||
@@ -325,6 +343,7 @@ def run(cmd, *args, win_id, env):
|
||||
user_agent = config.get('network', 'user-agent')
|
||||
if user_agent is not None:
|
||||
env['QUTE_USER_AGENT'] = user_agent
|
||||
runner.run(cmd, *args, env=env)
|
||||
cmd = os.path.expanduser(cmd)
|
||||
runner.run(cmd, *args, env=env, verbose=verbose)
|
||||
runner.finished.connect(commandrunner.deleteLater)
|
||||
runner.finished.connect(runner.deleteLater)
|
||||
|
||||
@@ -19,12 +19,12 @@
|
||||
|
||||
"""Completer attached to a CompletionView."""
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot, QObject, QTimer
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QTimer
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.commands import cmdutils, runners
|
||||
from qutebrowser.commands import cmdexc, cmdutils, runners
|
||||
from qutebrowser.utils import usertypes, log, objreg, utils
|
||||
from qutebrowser.completion.models import instances
|
||||
from qutebrowser.completion.models import instances, sortfilter
|
||||
|
||||
|
||||
class Completer(QObject):
|
||||
@@ -40,14 +40,22 @@ class Completer(QObject):
|
||||
_last_cursor_pos: The old cursor position so we avoid double completion
|
||||
updates.
|
||||
_last_text: The old command text so we avoid double completion updates.
|
||||
_signals_connected: Whether the signals are connected to update the
|
||||
completion when the command widget requests that.
|
||||
|
||||
Signals:
|
||||
next_prev_item: Emitted to select the next/previous item in the
|
||||
completion.
|
||||
arg0: True for the previous item, False for the next.
|
||||
"""
|
||||
|
||||
next_prev_item = pyqtSignal(bool)
|
||||
|
||||
def __init__(self, cmd, win_id, parent=None):
|
||||
super().__init__(parent)
|
||||
self._win_id = win_id
|
||||
self._cmd = cmd
|
||||
self._cmd.update_completion.connect(self.schedule_completion_update)
|
||||
self._cmd.textEdited.connect(self.on_text_edited)
|
||||
self._signals_connected = False
|
||||
self._ignore_change = False
|
||||
self._empty_item_idx = None
|
||||
self._timer = QTimer()
|
||||
@@ -58,9 +66,63 @@ class Completer(QObject):
|
||||
self._last_cursor_pos = None
|
||||
self._last_text = None
|
||||
|
||||
objreg.get('config').changed.connect(self.on_auto_open_changed)
|
||||
self.handle_signal_connections()
|
||||
self._cmd.clear_completion_selection.connect(
|
||||
self.handle_signal_connections)
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self)
|
||||
|
||||
@config.change_filter('completion', 'auto-open')
|
||||
def on_auto_open_changed(self):
|
||||
self.handle_signal_connections()
|
||||
|
||||
@pyqtSlot()
|
||||
def handle_signal_connections(self):
|
||||
self._connect_signals(config.get('completion', 'auto-open'))
|
||||
|
||||
def _connect_signals(self, connect=True):
|
||||
"""Connect or disconnect the completion signals.
|
||||
|
||||
Args:
|
||||
connect: Whether to connect (True) or disconnect (False) the
|
||||
signals.
|
||||
|
||||
Return:
|
||||
True if the signals were connected (connect=True and aren't
|
||||
connected yet) - otherwise False.
|
||||
"""
|
||||
connections = [
|
||||
(self._cmd.update_completion, self.schedule_completion_update),
|
||||
(self._cmd.textChanged, self.on_text_edited),
|
||||
]
|
||||
|
||||
if connect and not self._signals_connected:
|
||||
for sender, receiver in connections:
|
||||
sender.connect(receiver)
|
||||
self._signals_connected = True
|
||||
return True
|
||||
elif not connect:
|
||||
for sender, receiver in connections:
|
||||
try:
|
||||
sender.disconnect(receiver)
|
||||
except TypeError:
|
||||
# Don't fail if not connected
|
||||
pass
|
||||
self._signals_connected = False
|
||||
return False
|
||||
|
||||
def _open_completion_if_needed(self):
|
||||
"""If auto-open is false, temporarily connect signals.
|
||||
|
||||
Also opens the completion.
|
||||
"""
|
||||
if not config.get('completion', 'auto-open'):
|
||||
connected = self._connect_signals(True)
|
||||
if connected:
|
||||
self.update_completion()
|
||||
|
||||
def _model(self):
|
||||
"""Convienience method to get the current completion model."""
|
||||
completion = objreg.get('completion', scope='window',
|
||||
@@ -71,12 +133,12 @@ class Completer(QObject):
|
||||
"""Get a completion model based on an enum member.
|
||||
|
||||
Args:
|
||||
completion: An usertypes.Completion member.
|
||||
completion: A usertypes.Completion member.
|
||||
parts: The parts currently in the commandline.
|
||||
cursor_part: The part the cursor is in.
|
||||
|
||||
Return:
|
||||
A completion model.
|
||||
A completion model or None.
|
||||
"""
|
||||
if completion == usertypes.Completion.option:
|
||||
section = parts[cursor_part - 1]
|
||||
@@ -91,7 +153,11 @@ class Completer(QObject):
|
||||
model = None
|
||||
else:
|
||||
model = instances.get(completion)
|
||||
return model
|
||||
|
||||
if model is None:
|
||||
return None
|
||||
else:
|
||||
return sortfilter.CompletionFilterModel(source=model, parent=self)
|
||||
|
||||
def _filter_cmdline_parts(self, parts, cursor_part):
|
||||
"""Filter a list of commandline parts to exclude flags.
|
||||
@@ -140,7 +206,8 @@ class Completer(QObject):
|
||||
"{}".format(parts, cursor_part))
|
||||
if cursor_part == 0:
|
||||
# '|' or 'set|'
|
||||
return instances.get(usertypes.Completion.command)
|
||||
model = instances.get(usertypes.Completion.command)
|
||||
return sortfilter.CompletionFilterModel(source=model, parent=self)
|
||||
# delegate completion to command
|
||||
try:
|
||||
completions = cmdutils.cmd_dict[parts[0]].completion
|
||||
@@ -272,7 +339,7 @@ class Completer(QObject):
|
||||
pattern = parts[self._cursor_part].strip()
|
||||
except IndexError:
|
||||
pattern = ''
|
||||
self._model().set_pattern(pattern)
|
||||
completion.set_pattern(pattern)
|
||||
|
||||
log.completion.debug(
|
||||
"New completion for {}: {}, with pattern '{}'".format(
|
||||
@@ -328,7 +395,7 @@ class Completer(QObject):
|
||||
cursor_pos))
|
||||
skip = 0
|
||||
for i, part in enumerate(parts):
|
||||
log.completion.vdebug("Checking part {}: {}".format(i, parts[i]))
|
||||
log.completion.vdebug("Checking part {}: {!r}".format(i, parts[i]))
|
||||
if not part:
|
||||
skip += 1
|
||||
continue
|
||||
@@ -350,7 +417,11 @@ class Completer(QObject):
|
||||
"Removing len({!r}) -> {} from cursor_pos -> {}".format(
|
||||
part, len(part), cursor_pos))
|
||||
else:
|
||||
self._cursor_part = i - skip
|
||||
if i == 0:
|
||||
# Initial `:` press without any text.
|
||||
self._cursor_part = 0
|
||||
else:
|
||||
self._cursor_part = i - skip
|
||||
if spaces:
|
||||
self._empty_item_idx = i - skip
|
||||
else:
|
||||
@@ -401,3 +472,30 @@ class Completer(QObject):
|
||||
# We also want to update the cursor part and emit update_completion
|
||||
# here, but that's already done for us by cursorPositionChanged
|
||||
# anyways, so we don't need to do it twice.
|
||||
|
||||
@cmdutils.register(instance='completer', hide=True,
|
||||
modes=[usertypes.KeyMode.command], scope='window')
|
||||
def completion_item_prev(self):
|
||||
"""Select the previous completion item."""
|
||||
self._open_completion_if_needed()
|
||||
self.next_prev_item.emit(True)
|
||||
|
||||
@cmdutils.register(instance='completer', hide=True,
|
||||
modes=[usertypes.KeyMode.command], scope='window')
|
||||
def completion_item_next(self):
|
||||
"""Select the next completion item."""
|
||||
self._open_completion_if_needed()
|
||||
self.next_prev_item.emit(False)
|
||||
|
||||
@cmdutils.register(instance='completion', hide=True,
|
||||
modes=[usertypes.KeyMode.command], scope='window')
|
||||
def completion_item_del(self):
|
||||
"""Delete the current completion item."""
|
||||
completion = objreg.get('completion', scope='window',
|
||||
window=self._win_id)
|
||||
if not completion.currentIndex().isValid():
|
||||
raise cmdexc.CommandError("No item selected!")
|
||||
try:
|
||||
self.model().srcmodel.delete_cur_item(completion)
|
||||
except NotImplementedError:
|
||||
raise cmdexc.CommandError("Cannot delete this item.")
|
||||
|
||||
@@ -145,7 +145,6 @@ class CompletionItemDelegate(QStyledItemDelegate):
|
||||
rect: The QRect to clip the drawing to.
|
||||
"""
|
||||
# We can't use drawContents because then the color would be ignored.
|
||||
# See: https://qt-project.org/forums/viewthread/21492
|
||||
clip = QRectF(0, 0, rect.width(), rect.height())
|
||||
self._painter.save()
|
||||
if self._opt.state & QStyle.State_Selected:
|
||||
@@ -196,7 +195,8 @@ class CompletionItemDelegate(QStyledItemDelegate):
|
||||
|
||||
if index.parent().isValid():
|
||||
pattern = index.model().pattern
|
||||
if index.column() == 0 and pattern:
|
||||
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)
|
||||
|
||||
@@ -26,10 +26,10 @@ subclasses to provide completions.
|
||||
from PyQt5.QtWidgets import QStyle, QTreeView, QSizePolicy
|
||||
from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QItemSelectionModel
|
||||
|
||||
from qutebrowser.commands import cmdutils
|
||||
from qutebrowser.config import config, style
|
||||
from qutebrowser.completion import completiondelegate, completer
|
||||
from qutebrowser.utils import usertypes, qtutils, objreg, utils
|
||||
from qutebrowser.completion.models import base
|
||||
from qutebrowser.utils import qtutils, objreg, utils
|
||||
|
||||
|
||||
class CompletionView(QTreeView):
|
||||
@@ -39,15 +39,13 @@ class CompletionView(QTreeView):
|
||||
Based on QTreeView but heavily customized so root elements show as category
|
||||
headers, and children show as flat list.
|
||||
|
||||
Class attributes:
|
||||
COLUMN_WIDTHS: A list of column widths, in percent.
|
||||
|
||||
Attributes:
|
||||
enabled: Whether showing the CompletionView is enabled.
|
||||
_win_id: The ID of the window this CompletionView is associated with.
|
||||
_height: The height to use for the CompletionView.
|
||||
_height_perc: Either None or a percentage if height should be relative.
|
||||
_delegate: The item delegate used.
|
||||
_column_widths: A list of column widths, in percent.
|
||||
|
||||
Signals:
|
||||
resize_completion: Emitted when the completion should be resized.
|
||||
@@ -61,6 +59,7 @@ class CompletionView(QTreeView):
|
||||
{{ color['completion.bg'] }}
|
||||
alternate-background-color: {{ color['completion.alternate-bg'] }};
|
||||
outline: 0;
|
||||
border: 0px;
|
||||
}
|
||||
|
||||
QTreeView::item:disabled {
|
||||
@@ -83,7 +82,6 @@ class CompletionView(QTreeView):
|
||||
border: 0px;
|
||||
}
|
||||
"""
|
||||
COLUMN_WIDTHS = (20, 70, 10)
|
||||
|
||||
# FIXME style scrollbar
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/117
|
||||
@@ -96,12 +94,15 @@ class CompletionView(QTreeView):
|
||||
objreg.register('completion', self, scope='window', window=win_id)
|
||||
cmd = objreg.get('status-command', scope='window', window=win_id)
|
||||
completer_obj = completer.Completer(cmd, win_id, self)
|
||||
completer_obj.next_prev_item.connect(self.on_next_prev_item)
|
||||
objreg.register('completer', completer_obj, scope='window',
|
||||
window=win_id)
|
||||
self.enabled = config.get('completion', 'show')
|
||||
objreg.get('config').changed.connect(self.set_enabled)
|
||||
# FIXME handle new aliases.
|
||||
#objreg.get('config').changed.connect(self.init_command_completion)
|
||||
# objreg.get('config').changed.connect(self.init_command_completion)
|
||||
|
||||
self._column_widths = base.BaseCompletionModel.COLUMN_WIDTHS
|
||||
|
||||
self._delegate = completiondelegate.CompletionItemDelegate(self)
|
||||
self.setItemDelegate(self._delegate)
|
||||
@@ -128,9 +129,9 @@ class CompletionView(QTreeView):
|
||||
return utils.get_repr(self)
|
||||
|
||||
def _resize_columns(self):
|
||||
"""Resize the completion columns based on COLUMN_WIDTHS."""
|
||||
"""Resize the completion columns based on column_widths."""
|
||||
width = self.size().width()
|
||||
pixel_widths = [(width * perc // 100) for perc in self.COLUMN_WIDTHS]
|
||||
pixel_widths = [(width * perc // 100) for perc in self._column_widths]
|
||||
if self.verticalScrollBar().isVisible():
|
||||
pixel_widths[-1] -= self.style().pixelMetric(
|
||||
QStyle.PM_ScrollBarExtent) + 5
|
||||
@@ -168,12 +169,15 @@ class CompletionView(QTreeView):
|
||||
# Item is a real item, not a category header -> success
|
||||
return idx
|
||||
|
||||
def _next_prev_item(self, prev):
|
||||
@pyqtSlot(bool)
|
||||
def on_next_prev_item(self, prev):
|
||||
"""Handle a tab press for the CompletionView.
|
||||
|
||||
Select the previous/next item and write the new text to the
|
||||
statusbar.
|
||||
|
||||
Called from the Completer's next_prev_item signal.
|
||||
|
||||
Args:
|
||||
prev: True for prev item, False for next one.
|
||||
"""
|
||||
@@ -194,15 +198,32 @@ class CompletionView(QTreeView):
|
||||
Args:
|
||||
model: The model to use.
|
||||
"""
|
||||
old_model = self.model()
|
||||
sel_model = self.selectionModel()
|
||||
|
||||
self.setModel(model)
|
||||
|
||||
if sel_model is not None:
|
||||
sel_model.deleteLater()
|
||||
if old_model is not None:
|
||||
old_model.deleteLater()
|
||||
|
||||
for i in range(model.rowCount()):
|
||||
self.expand(model.index(i, 0))
|
||||
|
||||
self._column_widths = model.srcmodel.COLUMN_WIDTHS
|
||||
self._resize_columns()
|
||||
model.rowsRemoved.connect(self.maybe_resize_completion)
|
||||
model.rowsInserted.connect(self.maybe_resize_completion)
|
||||
self.maybe_resize_completion()
|
||||
|
||||
def set_pattern(self, pattern):
|
||||
"""Set the completion pattern for the current model.
|
||||
|
||||
Called from on_update_completion().
|
||||
|
||||
Args:
|
||||
pattern: The filter pattern to set (what the user entered).
|
||||
"""
|
||||
self.model().set_pattern(pattern)
|
||||
self.maybe_resize_completion()
|
||||
|
||||
@pyqtSlot()
|
||||
@@ -224,18 +245,6 @@ class CompletionView(QTreeView):
|
||||
selmod.clearSelection()
|
||||
selmod.clearCurrentIndex()
|
||||
|
||||
@cmdutils.register(instance='completion', hide=True,
|
||||
modes=[usertypes.KeyMode.command], scope='window')
|
||||
def completion_item_prev(self):
|
||||
"""Select the previous completion item."""
|
||||
self._next_prev_item(prev=True)
|
||||
|
||||
@cmdutils.register(instance='completion', hide=True,
|
||||
modes=[usertypes.KeyMode.command], scope='window')
|
||||
def completion_item_next(self):
|
||||
"""Select the next completion item."""
|
||||
self._next_prev_item(prev=False)
|
||||
|
||||
def selectionChanged(self, selected, deselected):
|
||||
"""Extend selectionChanged to call completers selection_changed."""
|
||||
super().selectionChanged(selected, deselected)
|
||||
|
||||
@@ -39,11 +39,20 @@ class BaseCompletionModel(QStandardItemModel):
|
||||
|
||||
Used for showing completions later in the CompletionView. Supports setting
|
||||
marks and adding new categories/items easily.
|
||||
|
||||
Class Attributes:
|
||||
COLUMN_WIDTHS: The width percentages of the columns used in the
|
||||
completion view.
|
||||
DUMB_SORT: the dumb sorting used by the model
|
||||
"""
|
||||
|
||||
COLUMN_WIDTHS = (30, 70, 0)
|
||||
DUMB_SORT = None
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setColumnCount(3)
|
||||
self.columns_to_filter = [0]
|
||||
|
||||
def new_category(self, name, sort=None):
|
||||
"""Add a new category to the model.
|
||||
@@ -79,22 +88,25 @@ class BaseCompletionModel(QStandardItemModel):
|
||||
assert not isinstance(name, int)
|
||||
assert not isinstance(desc, int)
|
||||
assert not isinstance(misc, int)
|
||||
|
||||
nameitem = QStandardItem(name)
|
||||
descitem = QStandardItem(desc)
|
||||
if misc is None:
|
||||
miscitem = QStandardItem()
|
||||
else:
|
||||
miscitem = QStandardItem(misc)
|
||||
idx = cat.rowCount()
|
||||
cat.setChild(idx, 0, nameitem)
|
||||
cat.setChild(idx, 1, descitem)
|
||||
cat.setChild(idx, 2, miscitem)
|
||||
|
||||
cat.appendRow([nameitem, descitem, miscitem])
|
||||
if sort is not None:
|
||||
nameitem.setData(sort, Role.sort)
|
||||
if userdata is not None:
|
||||
nameitem.setData(userdata, Role.userdata)
|
||||
return nameitem, descitem, miscitem
|
||||
|
||||
def delete_cur_item(self, win_id):
|
||||
"""Delete the selected item."""
|
||||
raise NotImplementedError
|
||||
|
||||
def flags(self, index):
|
||||
"""Return the item flags for index.
|
||||
|
||||
@@ -109,7 +121,8 @@ class BaseCompletionModel(QStandardItemModel):
|
||||
qtutils.ensure_valid(index)
|
||||
if index.parent().isValid():
|
||||
# item
|
||||
return Qt.ItemIsEnabled | Qt.ItemIsSelectable
|
||||
return (Qt.ItemIsEnabled | Qt.ItemIsSelectable |
|
||||
Qt.ItemNeverHasChildren)
|
||||
else:
|
||||
# category
|
||||
return Qt.NoItemFlags
|
||||
@@ -120,3 +133,13 @@ class BaseCompletionModel(QStandardItemModel):
|
||||
Override QAbstractItemModel::sort.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def custom_filter(self, pattern, row, parent):
|
||||
"""Custom filter.
|
||||
|
||||
Args:
|
||||
pattern: The current filter pattern.
|
||||
row: The row to accept or reject in the filter.
|
||||
parent: The parent item QModelIndex.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -32,6 +32,8 @@ class SettingSectionCompletionModel(base.BaseCompletionModel):
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
|
||||
COLUMN_WIDTHS = (20, 70, 10)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
cat = self.new_category("Sections")
|
||||
@@ -51,6 +53,8 @@ class SettingOptionCompletionModel(base.BaseCompletionModel):
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
|
||||
COLUMN_WIDTHS = (20, 70, 10)
|
||||
|
||||
def __init__(self, section, parent=None):
|
||||
super().__init__(parent)
|
||||
cat = self.new_category(section)
|
||||
@@ -104,6 +108,8 @@ class SettingValueCompletionModel(base.BaseCompletionModel):
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
|
||||
COLUMN_WIDTHS = (20, 70, 10)
|
||||
|
||||
def __init__(self, section, option, parent=None):
|
||||
super().__init__(parent)
|
||||
self._section = section
|
||||
|
||||
@@ -27,10 +27,9 @@ Module attributes:
|
||||
|
||||
import functools
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot, Qt
|
||||
from PyQt5.QtCore import pyqtSlot
|
||||
|
||||
from qutebrowser.completion.models import miscmodels, urlmodel, configmodel
|
||||
from qutebrowser.completion.models.sortfilter import CompletionFilterModel
|
||||
from qutebrowser.utils import objreg, usertypes, log, debug
|
||||
from qutebrowser.config import configdata
|
||||
|
||||
@@ -38,31 +37,17 @@ from qutebrowser.config import configdata
|
||||
_instances = {}
|
||||
|
||||
|
||||
def _init_model(klass, *args, dumb_sort=None, **kwargs):
|
||||
"""Helper to initialize a model.
|
||||
|
||||
Args:
|
||||
klass: The class of the model to initialize.
|
||||
*args: Arguments to pass to the model.
|
||||
**kwargs: Keyword arguments to pass to the model.
|
||||
dumb_sort: Passed to CompletionFilterModel.
|
||||
"""
|
||||
app = objreg.get('app')
|
||||
return CompletionFilterModel(klass(*args, parent=app, **kwargs),
|
||||
dumb_sort=dumb_sort, parent=app)
|
||||
|
||||
|
||||
def _init_command_completion():
|
||||
"""Initialize the command completion model."""
|
||||
log.completion.debug("Initializing command completion.")
|
||||
model = _init_model(miscmodels.CommandCompletionModel)
|
||||
model = miscmodels.CommandCompletionModel()
|
||||
_instances[usertypes.Completion.command] = model
|
||||
|
||||
|
||||
def _init_helptopic_completion():
|
||||
"""Initialize the helptopic completion model."""
|
||||
log.completion.debug("Initializing helptopic completion.")
|
||||
model = _init_model(miscmodels.HelpCompletionModel)
|
||||
model = miscmodels.HelpCompletionModel()
|
||||
_instances[usertypes.Completion.helptopic] = model
|
||||
|
||||
|
||||
@@ -70,25 +55,23 @@ def _init_url_completion():
|
||||
"""Initialize the URL completion model."""
|
||||
log.completion.debug("Initializing URL completion.")
|
||||
with debug.log_time(log.completion, 'URL completion init'):
|
||||
model = _init_model(urlmodel.UrlCompletionModel,
|
||||
dumb_sort=Qt.DescendingOrder)
|
||||
model = urlmodel.UrlCompletionModel()
|
||||
_instances[usertypes.Completion.url] = model
|
||||
|
||||
|
||||
def _init_setting_completions():
|
||||
"""Initialize setting completion models."""
|
||||
log.completion.debug("Initializing setting completion.")
|
||||
_instances[usertypes.Completion.section] = _init_model(
|
||||
configmodel.SettingSectionCompletionModel)
|
||||
_instances[usertypes.Completion.section] = (
|
||||
configmodel.SettingSectionCompletionModel())
|
||||
_instances[usertypes.Completion.option] = {}
|
||||
_instances[usertypes.Completion.value] = {}
|
||||
for sectname in configdata.DATA:
|
||||
model = _init_model(configmodel.SettingOptionCompletionModel, sectname)
|
||||
model = configmodel.SettingOptionCompletionModel(sectname)
|
||||
_instances[usertypes.Completion.option][sectname] = model
|
||||
_instances[usertypes.Completion.value][sectname] = {}
|
||||
for opt in configdata.DATA[sectname].keys():
|
||||
model = _init_model(configmodel.SettingValueCompletionModel,
|
||||
sectname, opt)
|
||||
model = configmodel.SettingValueCompletionModel(sectname, opt)
|
||||
_instances[usertypes.Completion.value][sectname][opt] = model
|
||||
|
||||
|
||||
@@ -97,16 +80,25 @@ def init_quickmark_completions():
|
||||
"""Initialize quickmark completion models."""
|
||||
log.completion.debug("Initializing quickmark completion.")
|
||||
try:
|
||||
_instances[usertypes.Completion.quickmark_by_url].deleteLater()
|
||||
_instances[usertypes.Completion.quickmark_by_name].deleteLater()
|
||||
except KeyError:
|
||||
pass
|
||||
model = _init_model(miscmodels.QuickmarkCompletionModel, 'url')
|
||||
_instances[usertypes.Completion.quickmark_by_url] = model
|
||||
model = _init_model(miscmodels.QuickmarkCompletionModel, 'name')
|
||||
model = miscmodels.QuickmarkCompletionModel()
|
||||
_instances[usertypes.Completion.quickmark_by_name] = model
|
||||
|
||||
|
||||
@pyqtSlot()
|
||||
def init_bookmark_completions():
|
||||
"""Initialize bookmark completion models."""
|
||||
log.completion.debug("Initializing bookmark completion.")
|
||||
try:
|
||||
_instances[usertypes.Completion.bookmark_by_url].deleteLater()
|
||||
except KeyError:
|
||||
pass
|
||||
model = miscmodels.BookmarkCompletionModel()
|
||||
_instances[usertypes.Completion.bookmark_by_url] = model
|
||||
|
||||
|
||||
@pyqtSlot()
|
||||
def init_session_completion():
|
||||
"""Initialize session completion model."""
|
||||
@@ -115,7 +107,7 @@ def init_session_completion():
|
||||
_instances[usertypes.Completion.sessions].deleteLater()
|
||||
except KeyError:
|
||||
pass
|
||||
model = _init_model(miscmodels.SessionCompletionModel)
|
||||
model = miscmodels.SessionCompletionModel()
|
||||
_instances[usertypes.Completion.sessions] = model
|
||||
|
||||
|
||||
@@ -126,8 +118,8 @@ INITIALIZERS = {
|
||||
usertypes.Completion.section: _init_setting_completions,
|
||||
usertypes.Completion.option: _init_setting_completions,
|
||||
usertypes.Completion.value: _init_setting_completions,
|
||||
usertypes.Completion.quickmark_by_url: init_quickmark_completions,
|
||||
usertypes.Completion.quickmark_by_name: init_quickmark_completions,
|
||||
usertypes.Completion.bookmark_by_url: init_bookmark_completions,
|
||||
usertypes.Completion.sessions: init_session_completion,
|
||||
}
|
||||
|
||||
@@ -163,8 +155,16 @@ def init():
|
||||
"""Initialize completions. Note this only connects signals."""
|
||||
quickmark_manager = objreg.get('quickmark-manager')
|
||||
quickmark_manager.changed.connect(
|
||||
functools.partial(update, [usertypes.Completion.quickmark_by_url,
|
||||
usertypes.Completion.quickmark_by_name]))
|
||||
functools.partial(update, [usertypes.Completion.quickmark_by_name]))
|
||||
|
||||
bookmark_manager = objreg.get('bookmark-manager')
|
||||
bookmark_manager.changed.connect(
|
||||
functools.partial(update, [usertypes.Completion.bookmark_by_url]))
|
||||
|
||||
session_manager = objreg.get('session-manager')
|
||||
session_manager.update_completion.connect(
|
||||
functools.partial(update, [usertypes.Completion.sessions]))
|
||||
|
||||
history = objreg.get('web-history')
|
||||
history.async_read_done.connect(
|
||||
functools.partial(update, [usertypes.Completion.url]))
|
||||
|
||||
@@ -96,19 +96,26 @@ class QuickmarkCompletionModel(base.BaseCompletionModel):
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
|
||||
def __init__(self, match_field='url', parent=None):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
cat = self.new_category("Quickmarks")
|
||||
quickmarks = objreg.get('quickmark-manager').marks.items()
|
||||
if match_field == 'url':
|
||||
for qm_name, qm_url in quickmarks:
|
||||
self.new_item(cat, qm_url, qm_name)
|
||||
elif match_field == 'name':
|
||||
for qm_name, qm_url in quickmarks:
|
||||
self.new_item(cat, qm_name, qm_url)
|
||||
else:
|
||||
raise ValueError("Invalid value '{}' for match_field!".format(
|
||||
match_field))
|
||||
for qm_name, qm_url in quickmarks:
|
||||
self.new_item(cat, qm_name, qm_url)
|
||||
|
||||
|
||||
class BookmarkCompletionModel(base.BaseCompletionModel):
|
||||
|
||||
"""A CompletionModel filled with all bookmarks."""
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
cat = self.new_category("Bookmarks")
|
||||
bookmarks = objreg.get('bookmark-manager').marks.items()
|
||||
for bm_url, bm_title in bookmarks:
|
||||
self.new_item(cat, bm_url, bm_title)
|
||||
|
||||
|
||||
class SessionCompletionModel(base.BaseCompletionModel):
|
||||
|
||||
@@ -41,11 +41,13 @@ class CompletionFilterModel(QSortFilterProxyModel):
|
||||
_sort_order: The order to use for sorting if using dumb_sort.
|
||||
"""
|
||||
|
||||
def __init__(self, source, parent=None, *, dumb_sort=None):
|
||||
def __init__(self, source, parent=None):
|
||||
super().__init__(parent)
|
||||
super().setSourceModel(source)
|
||||
self.srcmodel = source
|
||||
self.pattern = ''
|
||||
|
||||
dumb_sort = self.srcmodel.DUMB_SORT
|
||||
if dumb_sort is None:
|
||||
# pylint: disable=invalid-name
|
||||
self.lessThan = self.intelligentLessThan
|
||||
@@ -130,19 +132,23 @@ class CompletionFilterModel(QSortFilterProxyModel):
|
||||
True if self.pattern is contained in item, or if it's a root item
|
||||
(category). False in all other cases
|
||||
"""
|
||||
if parent == QModelIndex():
|
||||
if parent == QModelIndex() or not self.pattern:
|
||||
return True
|
||||
idx = self.srcmodel.index(row, 0, parent)
|
||||
if not idx.isValid():
|
||||
# No entries in parent model
|
||||
|
||||
try:
|
||||
return self.srcmodel.custom_filter(self.pattern, row, parent)
|
||||
except NotImplementedError:
|
||||
for col in self.srcmodel.columns_to_filter:
|
||||
idx = self.srcmodel.index(row, col, parent)
|
||||
if not idx.isValid():
|
||||
# No entries in parent model
|
||||
continue
|
||||
data = self.srcmodel.data(idx)
|
||||
if not data:
|
||||
continue
|
||||
elif self.pattern.casefold() in data.casefold():
|
||||
return True
|
||||
return False
|
||||
data = self.srcmodel.data(idx)
|
||||
# TODO more sophisticated filtering
|
||||
if not self.pattern:
|
||||
return True
|
||||
if not data:
|
||||
return False
|
||||
return self.pattern.casefold() in data.casefold()
|
||||
|
||||
def intelligentLessThan(self, lindex, rindex):
|
||||
"""Custom sorting implementation.
|
||||
|
||||
@@ -23,38 +23,57 @@ import datetime
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot, Qt
|
||||
|
||||
from qutebrowser.utils import objreg, utils
|
||||
from qutebrowser.utils import objreg, utils, qtutils, log
|
||||
from qutebrowser.completion.models import base
|
||||
from qutebrowser.config import config
|
||||
|
||||
|
||||
class UrlCompletionModel(base.BaseCompletionModel):
|
||||
|
||||
"""A model which combines quickmarks and web history URLs.
|
||||
"""A model which combines bookmarks, quickmarks and web history URLs.
|
||||
|
||||
Used for the `open` command."""
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
|
||||
URL_COLUMN = 0
|
||||
TEXT_COLUMN = 1
|
||||
TIME_COLUMN = 2
|
||||
|
||||
COLUMN_WIDTHS = (40, 50, 10)
|
||||
DUMB_SORT = Qt.DescendingOrder
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.columns_to_filter = [self.URL_COLUMN, self.TEXT_COLUMN]
|
||||
|
||||
self._quickmark_cat = self.new_category("Quickmarks")
|
||||
self._bookmark_cat = self.new_category("Bookmarks")
|
||||
self._history_cat = self.new_category("History")
|
||||
|
||||
quickmark_manager = objreg.get('quickmark-manager')
|
||||
quickmarks = quickmark_manager.marks.items()
|
||||
for qm_name, qm_url in quickmarks:
|
||||
self._add_quickmark_entry(qm_name, qm_url)
|
||||
quickmark_manager.added.connect(self.on_quickmark_added)
|
||||
self.new_item(self._quickmark_cat, qm_url, qm_name)
|
||||
quickmark_manager.added.connect(
|
||||
lambda name, url: self.new_item(self._quickmark_cat, url, name))
|
||||
quickmark_manager.removed.connect(self.on_quickmark_removed)
|
||||
|
||||
bookmark_manager = objreg.get('bookmark-manager')
|
||||
bookmarks = bookmark_manager.marks.items()
|
||||
for bm_url, bm_title in bookmarks:
|
||||
self.new_item(self._bookmark_cat, bm_url, bm_title)
|
||||
bookmark_manager.added.connect(
|
||||
lambda name, url: self.new_item(self._bookmark_cat, url, name))
|
||||
bookmark_manager.removed.connect(self.on_bookmark_removed)
|
||||
|
||||
self._history = objreg.get('web-history')
|
||||
max_history = config.get('completion', 'web-history-max-items')
|
||||
history = utils.newest_slice(self._history, max_history)
|
||||
self._max_history = config.get('completion', 'web-history-max-items')
|
||||
history = utils.newest_slice(self._history, self._max_history)
|
||||
for entry in history:
|
||||
self._add_history_entry(entry)
|
||||
self._history.item_about_to_be_added.connect(
|
||||
self._history.add_completion_item.connect(
|
||||
self.on_history_item_added)
|
||||
|
||||
objreg.get('config').changed.connect(self.reformat_timestamps)
|
||||
@@ -64,7 +83,18 @@ class UrlCompletionModel(base.BaseCompletionModel):
|
||||
fmt = config.get('completion', 'timestamp-format')
|
||||
if fmt is None:
|
||||
return ''
|
||||
return datetime.datetime.fromtimestamp(atime).strftime(fmt)
|
||||
try:
|
||||
dt = datetime.datetime.fromtimestamp(atime)
|
||||
except (ValueError, OSError, OverflowError):
|
||||
# Different errors which can occur for too large values...
|
||||
log.misc.error("Got invalid timestamp {}!".format(atime))
|
||||
return '(invalid)'
|
||||
else:
|
||||
return dt.strftime(fmt)
|
||||
|
||||
def _remove_oldest_history(self):
|
||||
"""Remove the oldest history entry."""
|
||||
self._history_cat.removeRow(0)
|
||||
|
||||
def _add_history_entry(self, entry):
|
||||
"""Add a new history entry to the completion."""
|
||||
@@ -72,47 +102,45 @@ class UrlCompletionModel(base.BaseCompletionModel):
|
||||
self._fmt_atime(entry.atime), sort=int(entry.atime),
|
||||
userdata=entry.url)
|
||||
|
||||
def _add_quickmark_entry(self, name, url):
|
||||
"""Add a new quickmark entry to the completion.
|
||||
|
||||
Args:
|
||||
name: The name of the new quickmark.
|
||||
url: The URL of the new quickmark.
|
||||
"""
|
||||
self.new_item(self._quickmark_cat, url, name)
|
||||
if self._history_cat.rowCount() > self._max_history:
|
||||
self._remove_oldest_history()
|
||||
|
||||
@config.change_filter('completion', 'timestamp-format')
|
||||
def reformat_timestamps(self):
|
||||
"""Reformat the timestamps if the config option was changed."""
|
||||
for i in range(self._history_cat.rowCount()):
|
||||
name_item = self._history_cat.child(i, 0)
|
||||
atime_item = self._history_cat.child(i, 2)
|
||||
atime = name_item.data(base.Role.sort)
|
||||
url_item = self._history_cat.child(i, self.URL_COLUMN)
|
||||
atime_item = self._history_cat.child(i, self.TIME_COLUMN)
|
||||
atime = url_item.data(base.Role.sort)
|
||||
atime_item.setText(self._fmt_atime(atime))
|
||||
|
||||
@pyqtSlot(object)
|
||||
def on_history_item_added(self, entry):
|
||||
"""Slot called when a new history item was added."""
|
||||
for i in range(self._history_cat.rowCount()):
|
||||
name_item = self._history_cat.child(i, 0)
|
||||
atime_item = self._history_cat.child(i, 2)
|
||||
url = name_item.data(base.Role.userdata)
|
||||
url_item = self._history_cat.child(i, self.URL_COLUMN)
|
||||
atime_item = self._history_cat.child(i, self.TIME_COLUMN)
|
||||
url = url_item.data(base.Role.userdata)
|
||||
if url == entry.url:
|
||||
atime_item.setText(self._fmt_atime(entry.atime))
|
||||
name_item.setData(int(entry.atime), base.Role.sort)
|
||||
url_item.setData(int(entry.atime), base.Role.sort)
|
||||
break
|
||||
else:
|
||||
self._add_history_entry(entry)
|
||||
|
||||
@pyqtSlot(str, str)
|
||||
def on_quickmark_added(self, name, url):
|
||||
"""Called when a quickmark has been added by the user.
|
||||
def _remove_item(self, data, category, column):
|
||||
"""Helper function for on_quickmark_removed and on_bookmark_removed.
|
||||
|
||||
Args:
|
||||
name: The name of the new quickmark.
|
||||
url: The url of the new quickmark, as string.
|
||||
data: The item to search for.
|
||||
category: The category to search in.
|
||||
column: The column to use for matching.
|
||||
"""
|
||||
self._add_quickmark_entry(name, url)
|
||||
for i in range(category.rowCount()):
|
||||
item = category.child(i, column)
|
||||
if item.data(Qt.DisplayRole) == data:
|
||||
category.removeRow(i)
|
||||
break
|
||||
|
||||
@pyqtSlot(str)
|
||||
def on_quickmark_removed(self, name):
|
||||
@@ -121,8 +149,35 @@ class UrlCompletionModel(base.BaseCompletionModel):
|
||||
Args:
|
||||
name: The name of the quickmark which has been removed.
|
||||
"""
|
||||
for i in range(self._quickmark_cat.rowCount()):
|
||||
name_item = self._quickmark_cat.child(i, 1)
|
||||
if name_item.data(Qt.DisplayRole) == name:
|
||||
self._quickmark_cat.removeRow(i)
|
||||
break
|
||||
self._remove_item(name, self._quickmark_cat, self.TEXT_COLUMN)
|
||||
|
||||
@pyqtSlot(str)
|
||||
def on_bookmark_removed(self, url):
|
||||
"""Called when a bookmark has been removed by the user.
|
||||
|
||||
Args:
|
||||
url: The url of the bookmark which has been removed.
|
||||
"""
|
||||
self._remove_item(url, self._bookmark_cat, self.URL_COLUMN)
|
||||
|
||||
def delete_cur_item(self, completion):
|
||||
"""Delete the selected item.
|
||||
|
||||
Args:
|
||||
completion: The Completion object to use.
|
||||
"""
|
||||
index = completion.currentIndex()
|
||||
qtutils.ensure_valid(index)
|
||||
url = index.data()
|
||||
category = index.parent()
|
||||
qtutils.ensure_valid(category)
|
||||
|
||||
if category.data() == 'Bookmarks':
|
||||
bookmark_manager = objreg.get('bookmark-manager')
|
||||
bookmark_manager.delete(url)
|
||||
elif category.data() == 'Quickmarks':
|
||||
quickmark_manager = objreg.get('quickmark-manager')
|
||||
sibling = index.sibling(index.row(), self.TEXT_COLUMN)
|
||||
qtutils.ensure_valid(sibling)
|
||||
name = sibling.data()
|
||||
quickmark_manager.quickmark_del(name)
|
||||
|
||||
@@ -33,15 +33,18 @@ import collections
|
||||
import collections.abc
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QUrl, QSettings
|
||||
from PyQt5.QtWidgets import QMessageBox
|
||||
|
||||
from qutebrowser.config import configdata, configexc, textwrapper
|
||||
from qutebrowser.config.parsers import ini, keyconf
|
||||
from qutebrowser.commands import cmdexc, cmdutils
|
||||
from qutebrowser.utils import message, objreg, utils, standarddir, log, qtutils
|
||||
from qutebrowser.utils import (message, objreg, utils, standarddir, log,
|
||||
qtutils, error, usertypes)
|
||||
from qutebrowser.utils.usertypes import Completion
|
||||
|
||||
|
||||
UNSET = object()
|
||||
|
||||
|
||||
class change_filter: # pylint: disable=invalid-name
|
||||
|
||||
"""Decorator to filter calls based on a config section/option matching.
|
||||
@@ -52,9 +55,10 @@ class change_filter: # pylint: disable=invalid-name
|
||||
Attributes:
|
||||
_sectname: The section to be filtered.
|
||||
_optname: The option to be filtered.
|
||||
_function: Whether a function rather than a method is decorated.
|
||||
"""
|
||||
|
||||
def __init__(self, sectname, optname=None):
|
||||
def __init__(self, sectname, optname=None, function=False):
|
||||
"""Save decorator arguments.
|
||||
|
||||
Gets called on parse-time with the decorator arguments.
|
||||
@@ -62,6 +66,7 @@ class change_filter: # pylint: disable=invalid-name
|
||||
Args:
|
||||
sectname: The section to be filtered.
|
||||
optname: The option to be filtered.
|
||||
function: Whether a function rather than a method is decorated.
|
||||
"""
|
||||
if sectname not in configdata.DATA:
|
||||
raise configexc.NoSectionError(sectname)
|
||||
@@ -69,6 +74,7 @@ class change_filter: # pylint: disable=invalid-name
|
||||
raise configexc.NoOptionError(optname, sectname)
|
||||
self._sectname = sectname
|
||||
self._optname = optname
|
||||
self._function = function
|
||||
|
||||
def __call__(self, func):
|
||||
"""Filter calls to the decorated function.
|
||||
@@ -86,19 +92,34 @@ class change_filter: # pylint: disable=invalid-name
|
||||
Return:
|
||||
The decorated function.
|
||||
"""
|
||||
@pyqtSlot(str, str)
|
||||
@functools.wraps(func)
|
||||
def wrapper(wrapper_self, sectname=None, optname=None):
|
||||
# pylint: disable=missing-docstring
|
||||
if sectname is None and optname is None:
|
||||
# Called directly, not from a config change event.
|
||||
return func(wrapper_self)
|
||||
elif sectname != self._sectname:
|
||||
return
|
||||
elif self._optname is not None and optname != self._optname:
|
||||
return
|
||||
else:
|
||||
return func(wrapper_self)
|
||||
if self._function:
|
||||
@pyqtSlot(str, str)
|
||||
@functools.wraps(func)
|
||||
def wrapper(sectname=None, optname=None):
|
||||
# pylint: disable=missing-docstring
|
||||
if sectname is None and optname is None:
|
||||
# Called directly, not from a config change event.
|
||||
return func()
|
||||
elif sectname != self._sectname:
|
||||
return
|
||||
elif self._optname is not None and optname != self._optname:
|
||||
return
|
||||
else:
|
||||
return func()
|
||||
else:
|
||||
@pyqtSlot(str, str)
|
||||
@functools.wraps(func)
|
||||
def wrapper(wrapper_self, sectname=None, optname=None):
|
||||
# pylint: disable=missing-docstring
|
||||
if sectname is None and optname is None:
|
||||
# Called directly, not from a config change event.
|
||||
return func(wrapper_self)
|
||||
elif sectname != self._sectname:
|
||||
return
|
||||
elif self._optname is not None and optname != self._optname:
|
||||
return
|
||||
else:
|
||||
return func(wrapper_self)
|
||||
|
||||
return wrapper
|
||||
|
||||
@@ -119,8 +140,8 @@ def _init_main_config(parent=None):
|
||||
Args:
|
||||
parent: The parent to pass to ConfigManager.
|
||||
"""
|
||||
args = objreg.get('args')
|
||||
try:
|
||||
args = objreg.get('args')
|
||||
config_obj = ConfigManager(standarddir.config(), 'qutebrowser.conf',
|
||||
args.relaxed_config, parent=parent)
|
||||
except (configexc.Error, configparser.Error, UnicodeDecodeError) as e:
|
||||
@@ -131,12 +152,11 @@ def _init_main_config(parent=None):
|
||||
e.section, e.option) # pylint: disable=no-member
|
||||
except AttributeError:
|
||||
pass
|
||||
errstr += "\n{}".format(e)
|
||||
msgbox = QMessageBox(QMessageBox.Critical,
|
||||
"Error while reading config!", errstr)
|
||||
msgbox.exec_()
|
||||
errstr += "\n"
|
||||
error.handle_fatal_exc(e, args, "Error while reading config!",
|
||||
pre_text=errstr)
|
||||
# We didn't really initialize much so far, so we just quit hard.
|
||||
sys.exit(1)
|
||||
sys.exit(usertypes.Exit.err_config)
|
||||
else:
|
||||
objreg.register('config', config_obj)
|
||||
if standarddir.config() is not None:
|
||||
@@ -160,20 +180,20 @@ def _init_key_config(parent):
|
||||
Args:
|
||||
parent: The parent to use for the KeyConfigParser.
|
||||
"""
|
||||
args = objreg.get('args')
|
||||
try:
|
||||
key_config = keyconf.KeyConfigParser(standarddir.config(), 'keys.conf',
|
||||
args.relaxed_config,
|
||||
parent=parent)
|
||||
except (keyconf.KeyConfigError, UnicodeDecodeError) as e:
|
||||
log.init.exception(e)
|
||||
errstr = "Error while reading key config:\n"
|
||||
if e.lineno is not None:
|
||||
errstr += "In line {}: ".format(e.lineno)
|
||||
errstr += str(e)
|
||||
msgbox = QMessageBox(QMessageBox.Critical,
|
||||
"Error while reading key config!", errstr)
|
||||
msgbox.exec_()
|
||||
error.handle_fatal_exc(e, args, "Error while reading key config!",
|
||||
pre_text=errstr)
|
||||
# We didn't really initialize much so far, so we just quit hard.
|
||||
sys.exit(1)
|
||||
sys.exit(usertypes.Exit.err_key_config)
|
||||
else:
|
||||
objreg.register('key-config', key_config)
|
||||
if standarddir.config() is not None:
|
||||
@@ -235,6 +255,39 @@ def init(parent=None):
|
||||
_init_misc()
|
||||
|
||||
|
||||
def _get_value_transformer(old, new):
|
||||
"""Get a function which transforms a value for CHANGED_OPTIONS.
|
||||
|
||||
Args:
|
||||
old: The old value - if the supplied value doesn't match this, it's
|
||||
returned untransformed.
|
||||
new: The new value.
|
||||
|
||||
Return:
|
||||
A function which takes a value and transforms it.
|
||||
"""
|
||||
def transformer(val):
|
||||
if val == old:
|
||||
return new
|
||||
else:
|
||||
return val
|
||||
return transformer
|
||||
|
||||
|
||||
def _transform_position(val):
|
||||
"""Transformer for position values."""
|
||||
mapping = {
|
||||
'north': 'top',
|
||||
'south': 'bottom',
|
||||
'west': 'left',
|
||||
'east': 'right',
|
||||
}
|
||||
try:
|
||||
return mapping[val]
|
||||
except KeyError:
|
||||
return val
|
||||
|
||||
|
||||
class ConfigManager(QObject):
|
||||
|
||||
"""Configuration manager for qutebrowser.
|
||||
@@ -246,6 +299,10 @@ class ConfigManager(QObject):
|
||||
RENAMED_SECTIONS: A mapping of renamed sections, {'oldname': 'newname'}
|
||||
RENAMED_OPTIONS: A mapping of renamed options,
|
||||
{('section', 'oldname'): 'newname'}
|
||||
CHANGED_OPTIONS: A mapping of arbitrarily changed options,
|
||||
{('section', 'option'): callable}.
|
||||
The callable takes the old value and returns the new
|
||||
one.
|
||||
DELETED_OPTIONS: A (section, option) list of deleted options.
|
||||
|
||||
Attributes:
|
||||
@@ -281,12 +338,22 @@ class ConfigManager(QObject):
|
||||
('colors', 'tab.indicator.system'): 'tabs.indicator.system',
|
||||
('tabs', 'auto-hide'): 'hide-auto',
|
||||
('completion', 'history-length'): 'cmd-history-max-items',
|
||||
('colors', 'downloads.fg'): 'downloads.fg.start',
|
||||
}
|
||||
DELETED_OPTIONS = [
|
||||
('colors', 'tab.separator'),
|
||||
('colors', 'tabs.separator'),
|
||||
('colors', 'completion.item.bg'),
|
||||
('tabs', 'indicator-space'),
|
||||
('tabs', 'hide-auto'),
|
||||
('tabs', 'hide-always'),
|
||||
]
|
||||
CHANGED_OPTIONS = {
|
||||
('content', 'cookies-accept'):
|
||||
_get_value_transformer('default', 'no-3rdparty'),
|
||||
('tabs', 'position'): _transform_position,
|
||||
('ui', 'downloads-position'): _transform_position,
|
||||
}
|
||||
|
||||
changed = pyqtSignal(str, str)
|
||||
style_changed = pyqtSignal(str, str)
|
||||
@@ -346,13 +413,16 @@ class ConfigManager(QObject):
|
||||
lines = []
|
||||
if not getattr(sect, 'descriptions', None):
|
||||
return lines
|
||||
|
||||
for optname, option in sect.items():
|
||||
|
||||
lines.append('#')
|
||||
if option.typ.typestr is None:
|
||||
if option.typ.special:
|
||||
typestr = ''
|
||||
else:
|
||||
typestr = ' ({})'.format(option.typ.typestr)
|
||||
typestr = ' ({})'.format(option.typ.__class__.__name__)
|
||||
lines.append("# {}{}:".format(optname, typestr))
|
||||
|
||||
try:
|
||||
desc = self.sections[sectname].descriptions[optname]
|
||||
except KeyError:
|
||||
@@ -445,10 +515,15 @@ class ConfigManager(QObject):
|
||||
for k, v in cp[real_sectname].items():
|
||||
if k.startswith(self.ESCAPE_CHAR):
|
||||
k = k[1:]
|
||||
|
||||
if (sectname, k) in self.DELETED_OPTIONS:
|
||||
return
|
||||
elif (sectname, k) in self.RENAMED_OPTIONS:
|
||||
if (sectname, k) in self.RENAMED_OPTIONS:
|
||||
k = self.RENAMED_OPTIONS[sectname, k]
|
||||
if (sectname, k) in self.CHANGED_OPTIONS:
|
||||
func = self.CHANGED_OPTIONS[(sectname, k)]
|
||||
v = func(v)
|
||||
|
||||
try:
|
||||
self.set('conf', sectname, k, v, validate=False)
|
||||
except configexc.NoOptionError:
|
||||
@@ -547,9 +622,13 @@ class ConfigManager(QObject):
|
||||
return existed
|
||||
|
||||
@functools.lru_cache()
|
||||
def get(self, sectname, optname, raw=False, transformed=True):
|
||||
def get(self, sectname, optname, raw=False, transformed=True,
|
||||
fallback=UNSET):
|
||||
"""Get the value from a section/option.
|
||||
|
||||
We don't support the vars argument from configparser.get as it's not
|
||||
hashable.
|
||||
|
||||
Args:
|
||||
sectname: The section to get the option from.
|
||||
optname: The option name
|
||||
@@ -562,13 +641,18 @@ class ConfigManager(QObject):
|
||||
if not self._initialized:
|
||||
raise Exception("get got called before initialization was "
|
||||
"complete!")
|
||||
|
||||
try:
|
||||
sect = self.sections[sectname]
|
||||
except KeyError:
|
||||
if fallback is not UNSET:
|
||||
return fallback
|
||||
raise configexc.NoSectionError(sectname)
|
||||
try:
|
||||
val = sect[optname]
|
||||
except KeyError:
|
||||
if fallback is not UNSET:
|
||||
return fallback
|
||||
raise configexc.NoOptionError(optname, sectname)
|
||||
if raw:
|
||||
return val.value()
|
||||
@@ -579,13 +663,11 @@ class ConfigManager(QObject):
|
||||
newval = val.typ.transform(newval)
|
||||
return newval
|
||||
|
||||
@cmdutils.register(name='set', instance='config',
|
||||
@cmdutils.register(name='set', instance='config', win_id='win_id',
|
||||
completion=[Completion.section, Completion.option,
|
||||
Completion.value])
|
||||
def set_command(self, win_id: {'special': 'win_id'},
|
||||
sectname: {'name': 'section'}=None,
|
||||
optname: {'name': 'option'}=None, value=None, temp=False,
|
||||
print_val: {'name': 'print'}=False):
|
||||
def set_command(self, win_id, section_=None, option=None, value=None,
|
||||
temp=False, print_=False):
|
||||
"""Set an option.
|
||||
|
||||
If the option name ends with '?', the value of the option is shown
|
||||
@@ -598,38 +680,39 @@ class ConfigManager(QObject):
|
||||
Wrapper for self.set() to output exceptions in the status bar.
|
||||
|
||||
Args:
|
||||
sectname: The section where the option is in.
|
||||
optname: The name of the option.
|
||||
section_: The section where the option is in.
|
||||
option: The name of the option.
|
||||
value: The value to set.
|
||||
temp: Set value temporarily.
|
||||
print_val: Print the value after setting.
|
||||
print_: Print the value after setting.
|
||||
"""
|
||||
if sectname is not None and optname is None:
|
||||
if section_ is not None and option is None:
|
||||
raise cmdexc.CommandError(
|
||||
"set: Either both section and option have to be given, or "
|
||||
"neither!")
|
||||
if sectname is None and optname is None:
|
||||
if section_ is None and option is None:
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=win_id)
|
||||
tabbed_browser.openurl(QUrl('qute:settings'), newtab=False)
|
||||
return
|
||||
|
||||
if optname.endswith('?'):
|
||||
optname = optname[:-1]
|
||||
print_val = True
|
||||
if option.endswith('?'):
|
||||
option = option[:-1]
|
||||
print_ = True
|
||||
else:
|
||||
try:
|
||||
if optname.endswith('!') and value is None:
|
||||
val = self.get(sectname, optname[:-1])
|
||||
if option.endswith('!') and value is None:
|
||||
option = option[:-1]
|
||||
val = self.get(section_, option)
|
||||
layer = 'temp' if temp else 'conf'
|
||||
if isinstance(val, bool):
|
||||
self.set(layer, sectname, optname[:-1], str(not val))
|
||||
self.set(layer, section_, option, str(not val))
|
||||
else:
|
||||
raise cmdexc.CommandError(
|
||||
"set: Attempted inversion of non-boolean value.")
|
||||
elif value is not None:
|
||||
layer = 'temp' if temp else 'conf'
|
||||
self.set(layer, sectname, optname, value)
|
||||
self.set(layer, section_, option, value)
|
||||
else:
|
||||
raise cmdexc.CommandError("set: The following arguments "
|
||||
"are required: value")
|
||||
@@ -637,10 +720,10 @@ class ConfigManager(QObject):
|
||||
raise cmdexc.CommandError("set: {} - {}".format(
|
||||
e.__class__.__name__, e))
|
||||
|
||||
if print_val:
|
||||
val = self.get(sectname, optname, transformed=False)
|
||||
if print_:
|
||||
val = self.get(section_, option, transformed=False)
|
||||
message.info(win_id, "{} {} = {}".format(
|
||||
sectname, optname, val), immediately=True)
|
||||
section_, option, val), immediately=True)
|
||||
|
||||
def set(self, layer, sectname, optname, value, validate=True):
|
||||
"""Set an option.
|
||||
|
||||
@@ -100,9 +100,13 @@ SECTION_DESC = {
|
||||
" * `rgb(r, g, b)` / `rgba(r, g, b, a)` (values 0-255 or "
|
||||
"percentages)\n"
|
||||
" * `hsv(h, s, v)` / `hsva(h, s, v, a)` (values 0-255, hue 0-359)\n"
|
||||
" * A gradient as explained in http://qt-project.org/doc/qt-4.8/"
|
||||
" * A gradient as explained in http://doc.qt.io/qt-5/"
|
||||
"stylesheet-reference.html#list-of-property-types[the Qt "
|
||||
"documentation] under ``Gradient''.\n\n"
|
||||
"A *.system value determines the color system to use for color "
|
||||
"interpolation between similarly-named *.start and *.stop entries, "
|
||||
"regardless of how they are defined in the options. "
|
||||
"Valid values are 'rgb', 'hsv', and 'hsl'.\n\n"
|
||||
"The `hints.*` values are a special case as they're real CSS "
|
||||
"colors, not Qt-CSS colors. There, for a gradient, you need to use "
|
||||
"`-webkit-gradient`, see https://www.webkit.org/blog/175/introducing-"
|
||||
@@ -204,7 +208,7 @@ def data(readonly=False):
|
||||
"be used."),
|
||||
|
||||
('new-instance-open-target',
|
||||
SettingValue(typ.NewInstanceOpenTarget(), 'window'),
|
||||
SettingValue(typ.NewInstanceOpenTarget(), 'tab'),
|
||||
"How to open links in an existing instance if a new one is "
|
||||
"launched."),
|
||||
|
||||
@@ -236,7 +240,7 @@ def data(readonly=False):
|
||||
"The default zoom level."),
|
||||
|
||||
('downloads-position',
|
||||
SettingValue(typ.VerticalPosition(), 'north'),
|
||||
SettingValue(typ.VerticalPosition(), 'top'),
|
||||
"Where to show the downloaded files."),
|
||||
|
||||
('message-timeout',
|
||||
@@ -267,15 +271,20 @@ def data(readonly=False):
|
||||
"page."),
|
||||
|
||||
('user-stylesheet',
|
||||
SettingValue(typ.UserStyleSheet(),
|
||||
SettingValue(typ.UserStyleSheet(none_ok=True),
|
||||
'::-webkit-scrollbar { width: 0px; height: 0px; }'),
|
||||
"User stylesheet to use (absolute filename or CSS string). Will "
|
||||
"expand environment variables."),
|
||||
"User stylesheet to use (absolute filename, filename relative to "
|
||||
"the config directory or CSS string). Will expand environment "
|
||||
"variables."),
|
||||
|
||||
('css-media-type',
|
||||
SettingValue(typ.String(none_ok=True), ''),
|
||||
"Set the CSS media type."),
|
||||
|
||||
('smooth-scrolling',
|
||||
SettingValue(typ.Bool(), 'false'),
|
||||
"Whether to enable smooth scrolling for webpages."),
|
||||
|
||||
('remove-finished-downloads',
|
||||
SettingValue(typ.Bool(), 'false'),
|
||||
"Whether to remove finished downloads automatically."),
|
||||
@@ -284,6 +293,10 @@ def data(readonly=False):
|
||||
SettingValue(typ.Bool(), 'false'),
|
||||
"Whether to hide the statusbar unless a message is shown."),
|
||||
|
||||
('statusbar-padding',
|
||||
SettingValue(typ.Padding(), '1,1,0,0'),
|
||||
"Padding for statusbar (top, bottom, left, right)."),
|
||||
|
||||
('window-title-format',
|
||||
SettingValue(typ.FormatString(fields=['perc', 'perc_raw', 'title',
|
||||
'title_sep', 'id']),
|
||||
@@ -301,6 +314,10 @@ def data(readonly=False):
|
||||
SettingValue(typ.Bool(), 'false'),
|
||||
"Whether to hide the mouse cursor."),
|
||||
|
||||
('modal-js-dialog',
|
||||
SettingValue(typ.Bool(), 'false'),
|
||||
"Use standard JavaScript modal dialog for alert() and confirm()"),
|
||||
|
||||
readonly=readonly
|
||||
)),
|
||||
|
||||
@@ -313,6 +330,10 @@ def data(readonly=False):
|
||||
SettingValue(typ.String(none_ok=True), 'en-US,en'),
|
||||
"Value to send in the `accept-language` header."),
|
||||
|
||||
('referer-header',
|
||||
SettingValue(typ.Referer(), 'same-domain'),
|
||||
"Send the Referer header"),
|
||||
|
||||
('user-agent',
|
||||
SettingValue(typ.UserAgent(none_ok=True), ''),
|
||||
"User agent to send. Empty to send the default."),
|
||||
@@ -339,6 +360,10 @@ def data(readonly=False):
|
||||
)),
|
||||
|
||||
('completion', sect.KeyValue(
|
||||
('auto-open',
|
||||
SettingValue(typ.Bool(), 'true'),
|
||||
"Automatically open completion when typing."),
|
||||
|
||||
('download-path-suggestion',
|
||||
SettingValue(typ.DownloadPathSuggestion(), 'path'),
|
||||
"What to display in the download filename input."),
|
||||
@@ -410,7 +435,7 @@ def data(readonly=False):
|
||||
|
||||
('spatial-navigation',
|
||||
SettingValue(typ.Bool(), 'false'),
|
||||
"Enables or disables the Spatial Navigation feature\n\n"
|
||||
"Enables or disables the Spatial Navigation feature.\n\n"
|
||||
"Spatial navigation consists in the ability to navigate between "
|
||||
"focusable elements in a Web page, such as hyperlinks and form "
|
||||
"controls, by using Left, Right, Up and Down arrow keys. For "
|
||||
@@ -456,15 +481,16 @@ def data(readonly=False):
|
||||
|
||||
('last-close',
|
||||
SettingValue(typ.LastClose(), 'ignore'),
|
||||
"Behaviour when the last tab is closed."),
|
||||
"Behavior when the last tab is closed."),
|
||||
|
||||
('hide-auto',
|
||||
SettingValue(typ.Bool(), 'false'),
|
||||
"Hide the tab bar if only one tab is open."),
|
||||
('show',
|
||||
SettingValue(typ.TabBarShow(), 'always'),
|
||||
"When to show the tab bar"),
|
||||
|
||||
('hide-always',
|
||||
SettingValue(typ.Bool(), 'false'),
|
||||
"Always hide the tab bar."),
|
||||
('show-switching-delay',
|
||||
SettingValue(typ.Int(), '800'),
|
||||
"Time to show the tab bar before hiding it when tabs->show is "
|
||||
"set to 'switching'."),
|
||||
|
||||
('wrap',
|
||||
SettingValue(typ.Bool(), 'true'),
|
||||
@@ -479,7 +505,7 @@ def data(readonly=False):
|
||||
"On which mouse button to close tabs."),
|
||||
|
||||
('position',
|
||||
SettingValue(typ.Position(), 'north'),
|
||||
SettingValue(typ.Position(), 'top'),
|
||||
"The position of the tab bar."),
|
||||
|
||||
('show-favicons',
|
||||
@@ -496,10 +522,6 @@ def data(readonly=False):
|
||||
SettingValue(typ.Int(minval=0), '3'),
|
||||
"Width of the progress indicator (0 to disable)."),
|
||||
|
||||
('indicator-space',
|
||||
SettingValue(typ.Int(minval=0), '3'),
|
||||
"Spacing between tab edge and indicator."),
|
||||
|
||||
('tabs-are-windows',
|
||||
SettingValue(typ.Bool(), 'false'),
|
||||
"Whether to open windows instead of tabs."),
|
||||
@@ -518,6 +540,18 @@ def data(readonly=False):
|
||||
"* `{index}`: The index of this tab.\n"
|
||||
"* `{id}`: The internal tab ID of this tab."),
|
||||
|
||||
('mousewheel-tab-switching',
|
||||
SettingValue(typ.Bool(), 'true'),
|
||||
"Switch between tabs using the mouse wheel."),
|
||||
|
||||
('padding',
|
||||
SettingValue(typ.Padding(), '0,0,5,5'),
|
||||
"Padding for tabs (top, bottom, left, right)."),
|
||||
|
||||
('indicator-padding',
|
||||
SettingValue(typ.Padding(), '2,2,0,4'),
|
||||
"Padding for indicators (top, bottom, left, right)."),
|
||||
|
||||
readonly=readonly
|
||||
)),
|
||||
|
||||
@@ -528,6 +562,15 @@ def data(readonly=False):
|
||||
"sensible os-specific default. Will expand environment "
|
||||
"variables."),
|
||||
|
||||
('prompt-download-directory',
|
||||
SettingValue(typ.Bool(), 'true'),
|
||||
"Whether to prompt the user for the download location.\n"
|
||||
"If set to false, 'download-directory' will be used."),
|
||||
|
||||
('remember-download-directory',
|
||||
SettingValue(typ.Bool(), 'true'),
|
||||
"Whether to remember the last used download directory."),
|
||||
|
||||
('maximum-pages-in-cache',
|
||||
SettingValue(
|
||||
typ.Int(none_ok=True, minval=0, maxval=MAXVALS['int']), ''),
|
||||
@@ -541,7 +584,8 @@ def data(readonly=False):
|
||||
|
||||
('object-cache-capacities',
|
||||
SettingValue(
|
||||
typ.WebKitBytesList(length=3, maxsize=MAXVALS['int']), ''),
|
||||
typ.WebKitBytesList(length=3, maxsize=MAXVALS['int'],
|
||||
none_ok=True), ''),
|
||||
"The capacities for the global memory cache for dead objects "
|
||||
"such as stylesheets or scripts. Syntax: cacheMinDeadCapacity, "
|
||||
"cacheMaxDead, totalCapacity.\n\n"
|
||||
@@ -554,11 +598,13 @@ def data(readonly=False):
|
||||
"that the cache should consume *overall*."),
|
||||
|
||||
('offline-storage-default-quota',
|
||||
SettingValue(typ.WebKitBytes(maxsize=MAXVALS['int64']), ''),
|
||||
SettingValue(typ.WebKitBytes(maxsize=MAXVALS['int64'],
|
||||
none_ok=True), ''),
|
||||
"Default quota for new offline storage databases."),
|
||||
|
||||
('offline-web-application-cache-quota',
|
||||
SettingValue(typ.WebKitBytes(maxsize=MAXVALS['int64']), ''),
|
||||
SettingValue(typ.WebKitBytes(maxsize=MAXVALS['int64'],
|
||||
none_ok=True), ''),
|
||||
"Quota for the offline web application cache."),
|
||||
|
||||
('offline-storage-database',
|
||||
@@ -605,12 +651,24 @@ def data(readonly=False):
|
||||
'Qt plugins with a mimetype such as "application/x-qt-plugin" '
|
||||
"are not affected by this setting."),
|
||||
|
||||
('webgl',
|
||||
SettingValue(typ.Bool(), 'true'),
|
||||
"Enables or disables WebGL."),
|
||||
|
||||
('css-regions',
|
||||
SettingValue(typ.Bool(), 'true'),
|
||||
"Enable or disable support for CSS regions."),
|
||||
|
||||
('hyperlink-auditing',
|
||||
SettingValue(typ.Bool(), 'false'),
|
||||
"Enable or disable hyperlink auditing (<a ping>)."),
|
||||
|
||||
('geolocation',
|
||||
SettingValue(typ.NoAsk(), 'ask'),
|
||||
SettingValue(typ.BoolAsk(), 'ask'),
|
||||
"Allow websites to request geolocations."),
|
||||
|
||||
('notifications',
|
||||
SettingValue(typ.NoAsk(), 'ask'),
|
||||
SettingValue(typ.BoolAsk(), 'ask'),
|
||||
"Allow websites to show notifications."),
|
||||
|
||||
#('allow-java',
|
||||
@@ -650,8 +708,8 @@ def data(readonly=False):
|
||||
"local urls."),
|
||||
|
||||
('cookies-accept',
|
||||
SettingValue(typ.AcceptCookies(), 'default'),
|
||||
"Whether to accept cookies."),
|
||||
SettingValue(typ.AcceptCookies(), 'no-3rdparty'),
|
||||
"Control which cookies to accept."),
|
||||
|
||||
('cookies-store',
|
||||
SettingValue(typ.Bool(), 'true'),
|
||||
@@ -716,7 +774,8 @@ def data(readonly=False):
|
||||
|
||||
('next-regexes',
|
||||
SettingValue(typ.RegexList(flags=re.IGNORECASE),
|
||||
r'\bnext\b,\bmore\b,\bnewer\b,\b[>→≫]\b,\b(>>|»)\b'),
|
||||
r'\bnext\b,\bmore\b,\bnewer\b,\b[>→≫]\b,\b(>>|»)\b,'
|
||||
r'\bcontinue\b'),
|
||||
"A comma-separated list of regexes to use for 'next' links."),
|
||||
|
||||
('prev-regexes',
|
||||
@@ -792,30 +851,72 @@ def data(readonly=False):
|
||||
SettingValue(typ.QssColor(), '#ff4444'),
|
||||
"Foreground color of the matched text in the completion."),
|
||||
|
||||
('statusbar.fg',
|
||||
SettingValue(typ.QssColor(), 'white'),
|
||||
"Foreground color of the statusbar."),
|
||||
|
||||
('statusbar.bg',
|
||||
SettingValue(typ.QssColor(), 'black'),
|
||||
"Foreground color of the statusbar."),
|
||||
|
||||
('statusbar.fg',
|
||||
SettingValue(typ.QssColor(), 'white'),
|
||||
"Foreground color of the statusbar."),
|
||||
('statusbar.fg.error',
|
||||
SettingValue(typ.QssColor(), '${statusbar.fg}'),
|
||||
"Foreground color of the statusbar if there was an error."),
|
||||
|
||||
('statusbar.bg.error',
|
||||
SettingValue(typ.QssColor(), 'red'),
|
||||
"Background color of the statusbar if there was an error."),
|
||||
|
||||
('statusbar.fg.warning',
|
||||
SettingValue(typ.QssColor(), '${statusbar.fg}'),
|
||||
"Foreground color of the statusbar if there is a warning."),
|
||||
|
||||
('statusbar.bg.warning',
|
||||
SettingValue(typ.QssColor(), 'darkorange'),
|
||||
"Background color of the statusbar if there is a warning."),
|
||||
|
||||
('statusbar.fg.prompt',
|
||||
SettingValue(typ.QssColor(), '${statusbar.fg}'),
|
||||
"Foreground color of the statusbar if there is a prompt."),
|
||||
|
||||
('statusbar.bg.prompt',
|
||||
SettingValue(typ.QssColor(), 'darkblue'),
|
||||
"Background color of the statusbar if there is a prompt."),
|
||||
|
||||
('statusbar.fg.insert',
|
||||
SettingValue(typ.QssColor(), '${statusbar.fg}'),
|
||||
"Foreground color of the statusbar in insert mode."),
|
||||
|
||||
('statusbar.bg.insert',
|
||||
SettingValue(typ.QssColor(), 'darkgreen'),
|
||||
"Background color of the statusbar in insert mode."),
|
||||
|
||||
('statusbar.fg.command',
|
||||
SettingValue(typ.QssColor(), '${statusbar.fg}'),
|
||||
"Foreground color of the statusbar in command mode."),
|
||||
|
||||
('statusbar.bg.command',
|
||||
SettingValue(typ.QssColor(), '${statusbar.bg}'),
|
||||
"Background color of the statusbar in command mode."),
|
||||
|
||||
('statusbar.fg.caret',
|
||||
SettingValue(typ.QssColor(), '${statusbar.fg}'),
|
||||
"Foreground color of the statusbar in caret mode."),
|
||||
|
||||
('statusbar.bg.caret',
|
||||
SettingValue(typ.QssColor(), 'purple'),
|
||||
"Background color of the statusbar in caret mode."),
|
||||
|
||||
('statusbar.fg.caret-selection',
|
||||
SettingValue(typ.QssColor(), '${statusbar.fg}'),
|
||||
"Foreground color of the statusbar in caret mode with a "
|
||||
"selection"),
|
||||
|
||||
('statusbar.bg.caret-selection',
|
||||
SettingValue(typ.QssColor(), '#a12dff'),
|
||||
"Background color of the statusbar in caret mode with a "
|
||||
"selection"),
|
||||
|
||||
('statusbar.progress.bg',
|
||||
SettingValue(typ.QssColor(), 'white'),
|
||||
"Background color of the progress bar."),
|
||||
@@ -847,22 +948,22 @@ def data(readonly=False):
|
||||
SettingValue(typ.QtColor(), 'white'),
|
||||
"Foreground color of unselected odd tabs."),
|
||||
|
||||
('tabs.fg.even',
|
||||
SettingValue(typ.QtColor(), 'white'),
|
||||
"Foreground color of unselected even tabs."),
|
||||
|
||||
('tabs.fg.selected',
|
||||
SettingValue(typ.QtColor(), 'white'),
|
||||
"Foreground color of selected tabs."),
|
||||
|
||||
('tabs.bg.odd',
|
||||
SettingValue(typ.QtColor(), 'grey'),
|
||||
"Background color of unselected odd tabs."),
|
||||
|
||||
('tabs.fg.even',
|
||||
SettingValue(typ.QtColor(), 'white'),
|
||||
"Foreground color of unselected even tabs."),
|
||||
|
||||
('tabs.bg.even',
|
||||
SettingValue(typ.QtColor(), 'darkgrey'),
|
||||
"Background color of unselected even tabs."),
|
||||
|
||||
('tabs.fg.selected',
|
||||
SettingValue(typ.QtColor(), 'white'),
|
||||
"Foreground color of selected tabs."),
|
||||
|
||||
('tabs.bg.selected',
|
||||
SettingValue(typ.QtColor(), 'black'),
|
||||
"Background color of selected tabs."),
|
||||
@@ -891,10 +992,6 @@ def data(readonly=False):
|
||||
SettingValue(typ.CssColor(), 'black'),
|
||||
"Font color for hints."),
|
||||
|
||||
('hints.fg.match',
|
||||
SettingValue(typ.CssColor(), 'green'),
|
||||
"Font color for the matched part of hints."),
|
||||
|
||||
('hints.bg',
|
||||
SettingValue(
|
||||
typ.CssColor(), '-webkit-gradient(linear, left top, '
|
||||
@@ -902,30 +999,51 @@ def data(readonly=False):
|
||||
'color-stop(100%,#FFC542))'),
|
||||
"Background color for hints."),
|
||||
|
||||
('downloads.fg',
|
||||
SettingValue(typ.QtColor(), '#ffffff'),
|
||||
"Foreground color for downloads."),
|
||||
('hints.fg.match',
|
||||
SettingValue(typ.CssColor(), 'green'),
|
||||
"Font color for the matched part of hints."),
|
||||
|
||||
('downloads.bg.bar',
|
||||
SettingValue(typ.QssColor(), 'black'),
|
||||
"Background color for the download bar."),
|
||||
|
||||
('downloads.fg.start',
|
||||
SettingValue(typ.QtColor(), 'white'),
|
||||
"Color gradient start for download text."),
|
||||
|
||||
('downloads.bg.start',
|
||||
SettingValue(typ.QtColor(), '#0000aa'),
|
||||
"Color gradient start for downloads."),
|
||||
"Color gradient start for download backgrounds."),
|
||||
|
||||
('downloads.fg.stop',
|
||||
SettingValue(typ.QtColor(), '${downloads.fg.start}'),
|
||||
"Color gradient end for download text."),
|
||||
|
||||
('downloads.bg.stop',
|
||||
SettingValue(typ.QtColor(), '#00aa00'),
|
||||
"Color gradient end for downloads."),
|
||||
"Color gradient stop for download backgrounds."),
|
||||
|
||||
('downloads.fg.system',
|
||||
SettingValue(typ.ColorSystem(), 'rgb'),
|
||||
"Color gradient interpolation system for download text."),
|
||||
|
||||
('downloads.bg.system',
|
||||
SettingValue(typ.ColorSystem(), 'rgb'),
|
||||
"Color gradient interpolation system for downloads."),
|
||||
"Color gradient interpolation system for download backgrounds."),
|
||||
|
||||
('downloads.fg.error',
|
||||
SettingValue(typ.QtColor(), 'white'),
|
||||
"Foreground color for downloads with errors."),
|
||||
|
||||
('downloads.bg.error',
|
||||
SettingValue(typ.QtColor(), 'red'),
|
||||
"Background color for downloads with errors."),
|
||||
|
||||
('webpage.bg',
|
||||
SettingValue(typ.QtColor(none_ok=True), 'white'),
|
||||
"Background color for webpages if unset (or empty to use the "
|
||||
"theme's color)"),
|
||||
|
||||
readonly=readonly
|
||||
)),
|
||||
|
||||
@@ -955,7 +1073,7 @@ def data(readonly=False):
|
||||
"Font used for the downloadbar."),
|
||||
|
||||
('hints',
|
||||
SettingValue(typ.Font(), 'bold 12px Monospace'),
|
||||
SettingValue(typ.Font(), 'bold 13px Monospace'),
|
||||
"Font used for the hints."),
|
||||
|
||||
('debug-console',
|
||||
@@ -1088,16 +1206,24 @@ KEY_SECTION_DESC = {
|
||||
" * `prompt-accept`: Confirm the entered value.\n"
|
||||
" * `prompt-yes`: Answer yes to a yes/no question.\n"
|
||||
" * `prompt-no`: Answer no to a yes/no question."),
|
||||
'caret': (
|
||||
""),
|
||||
}
|
||||
|
||||
# Keys which are similar to Return and should be bound by default where Return
|
||||
# is bound.
|
||||
|
||||
RETURN_KEYS = ['<Return>', '<Ctrl-M>', '<Ctrl-J>', '<Shift-Return>', '<Enter>',
|
||||
'<Shift-Enter>']
|
||||
|
||||
|
||||
KEY_DATA = collections.OrderedDict([
|
||||
('!normal', collections.OrderedDict([
|
||||
('leave-mode', ['<Escape>', '<Ctrl-[>']),
|
||||
('clear-keychain ;; leave-mode', ['<Escape>', '<Ctrl-[>']),
|
||||
])),
|
||||
|
||||
('normal', collections.OrderedDict([
|
||||
('search ""', ['<Escape>']),
|
||||
('clear-keychain ;; search', ['<Escape>']),
|
||||
('set-cmd-text -s :open', ['o']),
|
||||
('set-cmd-text :open {url}', ['go']),
|
||||
('set-cmd-text -s :open -t', ['O']),
|
||||
@@ -1114,12 +1240,12 @@ KEY_DATA = collections.OrderedDict([
|
||||
('tab-move', ['gm']),
|
||||
('tab-move -', ['gl']),
|
||||
('tab-move +', ['gr']),
|
||||
('tab-next', ['J', 'gt']),
|
||||
('tab-focus', ['J', 'gt']),
|
||||
('tab-prev', ['K', 'gT']),
|
||||
('tab-clone', ['gC']),
|
||||
('reload', ['r']),
|
||||
('reload -f', ['R']),
|
||||
('back', ['H', '<Backspace>']),
|
||||
('back', ['H']),
|
||||
('back -t', ['th']),
|
||||
('back -w', ['wh']),
|
||||
('forward', ['L']),
|
||||
@@ -1130,6 +1256,7 @@ KEY_DATA = collections.OrderedDict([
|
||||
('hint all tab', ['F']),
|
||||
('hint all window', ['wf']),
|
||||
('hint all tab-bg', [';b']),
|
||||
('hint all tab-fg', [';f']),
|
||||
('hint all hover', [';h']),
|
||||
('hint images', [';i']),
|
||||
('hint images tab', [';I']),
|
||||
@@ -1139,23 +1266,26 @@ KEY_DATA = collections.OrderedDict([
|
||||
('hint links fill ":open -b {hint-url}"', ['.o']),
|
||||
('hint links yank', [';y']),
|
||||
('hint links yank-primary', [';Y']),
|
||||
('hint links rapid', [';r']),
|
||||
('hint links rapid-win', [';R']),
|
||||
('hint --rapid links tab-bg', [';r']),
|
||||
('hint --rapid links window', [';R']),
|
||||
('hint links download', [';d']),
|
||||
('scroll -50 0', ['h']),
|
||||
('scroll 0 50', ['j']),
|
||||
('scroll 0 -50', ['k']),
|
||||
('scroll 50 0', ['l']),
|
||||
('scroll left', ['h']),
|
||||
('scroll down', ['j']),
|
||||
('scroll up', ['k']),
|
||||
('scroll right', ['l']),
|
||||
('undo', ['u', '<Ctrl-Shift-T>']),
|
||||
('scroll-perc 0', ['gg']),
|
||||
('scroll-perc', ['G']),
|
||||
('search-next', ['n']),
|
||||
('search-prev', ['N']),
|
||||
('enter-mode insert', ['i']),
|
||||
('enter-mode caret', ['v']),
|
||||
('yank', ['yy']),
|
||||
('yank -s', ['yY']),
|
||||
('yank -t', ['yt']),
|
||||
('yank -ts', ['yT']),
|
||||
('yank -d', ['yd']),
|
||||
('yank -ds', ['yD']),
|
||||
('paste', ['pp']),
|
||||
('paste -s', ['pP']),
|
||||
('paste -t', ['Pp']),
|
||||
@@ -1166,6 +1296,10 @@ KEY_DATA = collections.OrderedDict([
|
||||
('set-cmd-text -s :quickmark-load', ['b']),
|
||||
('set-cmd-text -s :quickmark-load -t', ['B']),
|
||||
('set-cmd-text -s :quickmark-load -w', ['wb']),
|
||||
('bookmark-add', ['M']),
|
||||
('set-cmd-text -s :bookmark-load', ['gb']),
|
||||
('set-cmd-text -s :bookmark-load -t', ['gB']),
|
||||
('set-cmd-text -s :bookmark-load -w', ['wB']),
|
||||
('save', ['sf']),
|
||||
('set-cmd-text -s :set', ['ss']),
|
||||
('set-cmd-text -s :set -t', ['sl']),
|
||||
@@ -1206,6 +1340,8 @@ KEY_DATA = collections.OrderedDict([
|
||||
('stop', ['<Ctrl-s>']),
|
||||
('print', ['<Ctrl-Alt-p>']),
|
||||
('open qute:settings', ['Ss']),
|
||||
('follow-selected', RETURN_KEYS),
|
||||
('follow-selected -t', ['<Ctrl-Return>', '<Ctrl-Enter>']),
|
||||
])),
|
||||
|
||||
('insert', collections.OrderedDict([
|
||||
@@ -1213,7 +1349,10 @@ KEY_DATA = collections.OrderedDict([
|
||||
])),
|
||||
|
||||
('hint', collections.OrderedDict([
|
||||
('follow-hint', ['<Return>']),
|
||||
('follow-hint', RETURN_KEYS),
|
||||
('hint --rapid links tab-bg', ['<Ctrl-R>']),
|
||||
('hint links', ['<Ctrl-F>']),
|
||||
('hint all tab-bg', ['<Ctrl-B>']),
|
||||
])),
|
||||
|
||||
('passthrough', {}),
|
||||
@@ -1223,11 +1362,12 @@ KEY_DATA = collections.OrderedDict([
|
||||
('command-history-next', ['<Ctrl-N>']),
|
||||
('completion-item-prev', ['<Shift-Tab>', '<Up>']),
|
||||
('completion-item-next', ['<Tab>', '<Down>']),
|
||||
('command-accept', ['<Return>', '<Ctrl-J>', '<Shift-Return>']),
|
||||
('completion-item-del', ['<Ctrl-D>']),
|
||||
('command-accept', RETURN_KEYS),
|
||||
])),
|
||||
|
||||
('prompt', collections.OrderedDict([
|
||||
('prompt-accept', ['<Return>', '<Ctrl-J>', '<Shift-Return>']),
|
||||
('prompt-accept', RETURN_KEYS),
|
||||
('prompt-yes', ['y']),
|
||||
('prompt-no', ['n']),
|
||||
])),
|
||||
@@ -1242,11 +1382,38 @@ KEY_DATA = collections.OrderedDict([
|
||||
('rl-unix-line-discard', ['<Ctrl-U>']),
|
||||
('rl-kill-line', ['<Ctrl-K>']),
|
||||
('rl-kill-word', ['<Alt-D>']),
|
||||
('rl-unix-word-rubout', ['<Ctrl-W>']),
|
||||
('rl-unix-word-rubout', ['<Ctrl-W>', '<Alt-Backspace>']),
|
||||
('rl-yank', ['<Ctrl-Y>']),
|
||||
('rl-delete-char', ['<Ctrl-?>']),
|
||||
('rl-backward-delete-char', ['<Ctrl-H>']),
|
||||
])),
|
||||
|
||||
('caret', collections.OrderedDict([
|
||||
('toggle-selection', ['v', '<Space>']),
|
||||
('drop-selection', ['<Ctrl-Space>']),
|
||||
('enter-mode normal', ['c']),
|
||||
('move-to-next-line', ['j']),
|
||||
('move-to-prev-line', ['k']),
|
||||
('move-to-next-char', ['l']),
|
||||
('move-to-prev-char', ['h']),
|
||||
('move-to-end-of-word', ['e']),
|
||||
('move-to-next-word', ['w']),
|
||||
('move-to-prev-word', ['b']),
|
||||
('move-to-start-of-next-block', [']']),
|
||||
('move-to-start-of-prev-block', ['[']),
|
||||
('move-to-end-of-next-block', ['}']),
|
||||
('move-to-end-of-prev-block', ['{']),
|
||||
('move-to-start-of-line', ['0']),
|
||||
('move-to-end-of-line', ['$']),
|
||||
('move-to-start-of-document', ['gg']),
|
||||
('move-to-end-of-document', ['G']),
|
||||
('yank-selected -p', ['Y']),
|
||||
('yank-selected', ['y'] + RETURN_KEYS),
|
||||
('scroll left', ['H']),
|
||||
('scroll down', ['J']),
|
||||
('scroll up', ['K']),
|
||||
('scroll right', ['L']),
|
||||
])),
|
||||
])
|
||||
|
||||
|
||||
@@ -1254,10 +1421,25 @@ KEY_DATA = collections.OrderedDict([
|
||||
|
||||
CHANGED_KEY_COMMANDS = [
|
||||
(re.compile(r'^open -([twb]) about:blank$'), r'open -\1'),
|
||||
|
||||
(re.compile(r'^download-page$'), r'download'),
|
||||
(re.compile(r'^cancel-download$'), r'download-cancel'),
|
||||
(re.compile(r'^search ""$'), r'search'),
|
||||
(re.compile(r"^search ''$"), r'search'),
|
||||
|
||||
(re.compile(r"""^search (''|"")$"""), r'clear-keychain ;; search'),
|
||||
(re.compile(r'^search$'), r'clear-keychain ;; search'),
|
||||
|
||||
(re.compile(r"""^set-cmd-text ['"](.*) ['"]$"""), r'set-cmd-text -s \1'),
|
||||
(re.compile(r"""^set-cmd-text ['"](.*)['"]$"""), r'set-cmd-text \1'),
|
||||
|
||||
(re.compile(r"^hint links rapid$"), r'hint --rapid links tab-bg'),
|
||||
(re.compile(r"^hint links rapid-win$"), r'hint --rapid links window'),
|
||||
|
||||
(re.compile(r'^scroll -50 0$'), r'scroll left'),
|
||||
(re.compile(r'^scroll 0 50$'), r'scroll down'),
|
||||
(re.compile(r'^scroll 0 -50$'), r'scroll up'),
|
||||
(re.compile(r'^scroll 50 0$'), r'scroll right'),
|
||||
(re.compile(r'^scroll ([-\d]+ [-\d]+)$'), r'scroll-px \1'),
|
||||
|
||||
(re.compile(r'^search *;; *clear-keychain$'), r'clear-keychain ;; search'),
|
||||
(re.compile(r'^leave-mode$'), r'clear-keychain ;; leave-mode'),
|
||||
]
|
||||
|
||||
@@ -57,7 +57,7 @@ class NoOptionError(Error):
|
||||
"""Raised when an option was not found."""
|
||||
|
||||
def __init__(self, option, section):
|
||||
super().__init__("No option {!r} in section: {!r}".format(
|
||||
super().__init__("No option {!r} in section {!r}".format(
|
||||
option, section))
|
||||
self.option = option
|
||||
self.section = section
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -47,11 +47,15 @@ class ReadConfigParser(configparser.ConfigParser):
|
||||
self.optionxform = lambda opt: opt # be case-insensitive
|
||||
self._configdir = configdir
|
||||
self._fname = fname
|
||||
if self._configdir is None:
|
||||
self._configfile = None
|
||||
return
|
||||
self._configfile = os.path.join(self._configdir, fname)
|
||||
if not os.path.isfile(self._configfile):
|
||||
return
|
||||
log.init.debug("Reading config from {}".format(self._configfile))
|
||||
self.read(self._configfile, encoding='utf-8')
|
||||
if self._configfile is not None:
|
||||
self.read(self._configfile, encoding='utf-8')
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self, constructor=True,
|
||||
@@ -64,6 +68,8 @@ class ReadWriteConfigParser(ReadConfigParser):
|
||||
|
||||
def save(self):
|
||||
"""Save the config file."""
|
||||
if self._configdir is None:
|
||||
return
|
||||
if not os.path.exists(self._configdir):
|
||||
os.makedirs(self._configdir, 0o755)
|
||||
log.destroy.debug("Saving config to {}".format(self._configfile))
|
||||
|
||||
@@ -75,12 +75,13 @@ class KeyConfigParser(QObject):
|
||||
config_dirty = pyqtSignal()
|
||||
UNBOUND_COMMAND = '<unbound>'
|
||||
|
||||
def __init__(self, configdir, fname, parent=None):
|
||||
def __init__(self, configdir, fname, relaxed=False, parent=None):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
configdir: The directory to save the configs in.
|
||||
fname: The filename of the config.
|
||||
relaxed: If given, unknwon commands are ignored.
|
||||
"""
|
||||
super().__init__(parent)
|
||||
self.is_dirty = False
|
||||
@@ -95,7 +96,7 @@ class KeyConfigParser(QObject):
|
||||
if self._configfile is None or not os.path.exists(self._configfile):
|
||||
self._load_default()
|
||||
else:
|
||||
self._read()
|
||||
self._read(relaxed)
|
||||
self._load_default(only_new=True)
|
||||
log.init.debug("Loaded bindings: {}".format(self.keybindings))
|
||||
|
||||
@@ -236,27 +237,40 @@ class KeyConfigParser(QObject):
|
||||
only_new: If set, only keybindings which are completely unused
|
||||
(same command/key not bound) are added.
|
||||
"""
|
||||
# {'sectname': {'keychain1': 'command', 'keychain2': 'command'}, ...}
|
||||
bindings_to_add = collections.OrderedDict()
|
||||
|
||||
for sectname, sect in configdata.KEY_DATA.items():
|
||||
sectname = self._normalize_sectname(sectname)
|
||||
bindings_to_add[sectname] = collections.OrderedDict()
|
||||
for command, keychains in sect.items():
|
||||
for e in keychains:
|
||||
if not only_new or self._is_new(sectname, command, e):
|
||||
assert e not in bindings_to_add[sectname]
|
||||
bindings_to_add[sectname][e] = command
|
||||
|
||||
for sectname, sect in bindings_to_add.items():
|
||||
if not sect:
|
||||
if not only_new:
|
||||
self.keybindings[sectname] = collections.OrderedDict()
|
||||
self._mark_config_dirty()
|
||||
else:
|
||||
for command, keychains in sect.items():
|
||||
for e in keychains:
|
||||
if not only_new or self._is_new(sectname, command, e):
|
||||
self._add_binding(sectname, e, command)
|
||||
self._mark_config_dirty()
|
||||
for keychain, command in sect.items():
|
||||
self._add_binding(sectname, keychain, command)
|
||||
self.changed.emit(sectname)
|
||||
|
||||
if bindings_to_add:
|
||||
self._mark_config_dirty()
|
||||
|
||||
def _is_new(self, sectname, command, keychain):
|
||||
"""Check if a given binding is new.
|
||||
|
||||
A binding is considered new if both the command is not bound to any key
|
||||
yet, and the key isn't used anywhere else in the same section.
|
||||
"""
|
||||
bindings = self.keybindings[sectname]
|
||||
try:
|
||||
bindings = self.keybindings[sectname]
|
||||
except KeyError:
|
||||
return True
|
||||
if keychain in bindings:
|
||||
return False
|
||||
elif command in bindings.values():
|
||||
@@ -264,8 +278,12 @@ class KeyConfigParser(QObject):
|
||||
else:
|
||||
return True
|
||||
|
||||
def _read(self):
|
||||
"""Read the config file from disk and parse it."""
|
||||
def _read(self, relaxed=False):
|
||||
"""Read the config file from disk and parse it.
|
||||
|
||||
Args:
|
||||
relaxed: Ignore unknown commands.
|
||||
"""
|
||||
try:
|
||||
with open(self._configfile, 'r', encoding='utf-8') as f:
|
||||
for i, line in enumerate(f):
|
||||
@@ -284,8 +302,11 @@ class KeyConfigParser(QObject):
|
||||
line = line.strip()
|
||||
self._read_command(line)
|
||||
except KeyConfigError as e:
|
||||
e.lineno = i
|
||||
raise
|
||||
if relaxed:
|
||||
continue
|
||||
else:
|
||||
e.lineno = i
|
||||
raise
|
||||
except OSError:
|
||||
log.keyboard.exception("Failed to read key bindings!")
|
||||
for sectname in self.keybindings:
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"""Utilities related to the look&feel of qutebrowser."""
|
||||
|
||||
import functools
|
||||
import collections
|
||||
|
||||
import jinja2
|
||||
import sip
|
||||
@@ -42,8 +43,7 @@ def get_stylesheet(template_str):
|
||||
colordict = ColorDict(config.section('colors'))
|
||||
fontdict = FontDict(config.section('fonts'))
|
||||
template = jinja2.Template(template_str)
|
||||
return template.render(color=colordict, font=fontdict,
|
||||
config=objreg.get('config'))
|
||||
return template.render(color=colordict, font=fontdict)
|
||||
|
||||
|
||||
def set_register_stylesheet(obj):
|
||||
@@ -69,7 +69,7 @@ def update_stylesheet(obj):
|
||||
obj.setStyleSheet(get_stylesheet(obj.STYLESHEET))
|
||||
|
||||
|
||||
class ColorDict(dict):
|
||||
class ColorDict(collections.UserDict):
|
||||
|
||||
"""A dict aimed at Qt stylesheet colors."""
|
||||
|
||||
@@ -89,9 +89,9 @@ class ColorDict(dict):
|
||||
In all other cases, return the plain value.
|
||||
"""
|
||||
try:
|
||||
val = super().__getitem__(key)
|
||||
val = self.data[key]
|
||||
except KeyError:
|
||||
log.config.exception("No color defined for {}!")
|
||||
log.config.exception("No color defined for {}!".format(key))
|
||||
return ''
|
||||
if isinstance(val, QColor):
|
||||
# This could happen when accidentally declaring something as
|
||||
@@ -106,7 +106,7 @@ class ColorDict(dict):
|
||||
return val
|
||||
|
||||
|
||||
class FontDict(dict):
|
||||
class FontDict(collections.UserDict):
|
||||
|
||||
"""A dict aimed at Qt stylesheet fonts."""
|
||||
|
||||
@@ -123,7 +123,7 @@ class FontDict(dict):
|
||||
In all other cases, return font: <value>.
|
||||
"""
|
||||
try:
|
||||
val = super().__getitem__(key)
|
||||
val = self.data[key]
|
||||
except KeyError:
|
||||
return ''
|
||||
else:
|
||||
|
||||
@@ -26,7 +26,7 @@ class TextWrapper(textwrap.TextWrapper):
|
||||
|
||||
"""Text wrapper customized to be used in configs."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
def __init__(self, **kwargs):
|
||||
kw = {
|
||||
'width': 72,
|
||||
'replace_whitespace': False,
|
||||
@@ -36,4 +36,4 @@ class TextWrapper(textwrap.TextWrapper):
|
||||
'subsequent_indent': '# ',
|
||||
}
|
||||
kw.update(kwargs)
|
||||
super().__init__(*args, **kw)
|
||||
super().__init__(**kw)
|
||||
|
||||
@@ -84,8 +84,8 @@ class Base:
|
||||
qws: The QWebSettings instance to use, or None to use the global
|
||||
instance.
|
||||
"""
|
||||
log.config.vdebug("Restoring default {!r}.".format(self._default))
|
||||
if self._default is not UNSET:
|
||||
log.config.vdebug("Restoring default {!r}.".format(self._default))
|
||||
self._set(self._default, qws=qws)
|
||||
|
||||
def get(self, qws=None):
|
||||
@@ -238,6 +238,25 @@ class GlobalSetter(Setter):
|
||||
self._setter(*args)
|
||||
|
||||
|
||||
class CookiePolicy(Base):
|
||||
|
||||
"""The ThirdPartyCookiePolicy setting is different from other settings."""
|
||||
|
||||
MAPPING = {
|
||||
'all': QWebSettings.AlwaysAllowThirdPartyCookies,
|
||||
'no-3rdparty': QWebSettings.AlwaysBlockThirdPartyCookies,
|
||||
'never': QWebSettings.AlwaysBlockThirdPartyCookies,
|
||||
'no-unknown-3rdparty': QWebSettings.AllowThirdPartyWithExistingCookies,
|
||||
}
|
||||
|
||||
def get(self, qws=None):
|
||||
return config.get('content', 'cookies-accept')
|
||||
|
||||
def _set(self, value, qws=None):
|
||||
QWebSettings.globalSettings().setThirdPartyCookiePolicy(
|
||||
self.MAPPING[value])
|
||||
|
||||
|
||||
MAPPINGS = {
|
||||
'content': {
|
||||
'allow-images':
|
||||
@@ -254,10 +273,18 @@ MAPPINGS = {
|
||||
# Attribute(QWebSettings.JavaEnabled),
|
||||
'allow-plugins':
|
||||
Attribute(QWebSettings.PluginsEnabled),
|
||||
'webgl':
|
||||
Attribute(QWebSettings.WebGLEnabled),
|
||||
'css-regions':
|
||||
Attribute(QWebSettings.CSSRegionsEnabled),
|
||||
'hyperlink-auditing':
|
||||
Attribute(QWebSettings.HyperlinkAuditingEnabled),
|
||||
'local-content-can-access-remote-urls':
|
||||
Attribute(QWebSettings.LocalContentCanAccessRemoteUrls),
|
||||
'local-content-can-access-file-urls':
|
||||
Attribute(QWebSettings.LocalContentCanAccessFileUrls),
|
||||
'cookies-accept':
|
||||
CookiePolicy(),
|
||||
},
|
||||
'network': {
|
||||
'dns-prefetch':
|
||||
@@ -322,6 +349,8 @@ MAPPINGS = {
|
||||
'css-media-type':
|
||||
NullStringSetter(getter=QWebSettings.cssMediaType,
|
||||
setter=QWebSettings.setCSSMediaType),
|
||||
'smooth-scrolling':
|
||||
Attribute(QWebSettings.ScrollAnimatorEnabled),
|
||||
#'accelerated-compositing':
|
||||
# Attribute(QWebSettings.AcceleratedCompositingEnabled),
|
||||
#'tiled-backing-store':
|
||||
@@ -369,16 +398,20 @@ MAPPINGS = {
|
||||
|
||||
def init():
|
||||
"""Initialize the global QWebSettings."""
|
||||
if config.get('general', 'private-browsing'):
|
||||
cache_path = standarddir.cache()
|
||||
data_path = standarddir.data()
|
||||
if config.get('general', 'private-browsing') or cache_path is None:
|
||||
QWebSettings.setIconDatabasePath('')
|
||||
else:
|
||||
QWebSettings.setIconDatabasePath(standarddir.cache())
|
||||
QWebSettings.setOfflineWebApplicationCachePath(
|
||||
os.path.join(standarddir.cache(), 'application-cache'))
|
||||
QWebSettings.globalSettings().setLocalStoragePath(
|
||||
os.path.join(standarddir.data(), 'local-storage'))
|
||||
QWebSettings.setOfflineStoragePath(
|
||||
os.path.join(standarddir.data(), 'offline-storage'))
|
||||
QWebSettings.setIconDatabasePath(cache_path)
|
||||
if cache_path is not None:
|
||||
QWebSettings.setOfflineWebApplicationCachePath(
|
||||
os.path.join(cache_path, 'application-cache'))
|
||||
if data_path is not None:
|
||||
QWebSettings.globalSettings().setLocalStoragePath(
|
||||
os.path.join(data_path, 'local-storage'))
|
||||
QWebSettings.setOfflineStoragePath(
|
||||
os.path.join(data_path, 'offline-storage'))
|
||||
|
||||
for sectname, section in MAPPINGS.items():
|
||||
for optname, mapping in section.items():
|
||||
@@ -394,11 +427,12 @@ def init():
|
||||
|
||||
def update_settings(section, option):
|
||||
"""Update global settings when qwebsettings changed."""
|
||||
cache_path = standarddir.cache()
|
||||
if (section, option) == ('general', 'private-browsing'):
|
||||
if config.get('general', 'private-browsing'):
|
||||
if config.get('general', 'private-browsing') or cache_path is None:
|
||||
QWebSettings.setIconDatabasePath('')
|
||||
else:
|
||||
QWebSettings.setIconDatabasePath(standarddir.cache())
|
||||
QWebSettings.setIconDatabasePath(cache_path)
|
||||
else:
|
||||
try:
|
||||
mapping = MAPPINGS[section][option]
|
||||
|
||||
67
qutebrowser/html/dirbrowser.html
Normal file
67
qutebrowser/html/dirbrowser.html
Normal file
@@ -0,0 +1,67 @@
|
||||
{% extends "base.html" %}
|
||||
{% block style %}
|
||||
{{ super() }}
|
||||
#dirbrowserContainer {
|
||||
background: #fff;
|
||||
min-width: 35em;
|
||||
max-width: 35em;
|
||||
position: absolute;
|
||||
top: 2em;
|
||||
left: 1em;
|
||||
padding: 10px;
|
||||
border: 2px solid #eee;
|
||||
-webkit-border-radius: 5px;
|
||||
}
|
||||
|
||||
#dirbrowserTitleText {
|
||||
font-size: 118%;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ul > li {
|
||||
background-repeat: no-repeat;
|
||||
background-size: 22px;
|
||||
line-height: 22px;
|
||||
padding-left: 25px;
|
||||
}
|
||||
|
||||
ul > li {
|
||||
background-image: url('{{folder_url}}');
|
||||
}
|
||||
|
||||
ul.files > li {
|
||||
background-image: url('{{file_url}}');
|
||||
}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="dirbrowserContainer">
|
||||
<div id="dirbrowserTitle">
|
||||
<p id="dirbrowserTitleText">Browse directory: {{url}}</p>
|
||||
</div>
|
||||
|
||||
{% if parent %}
|
||||
<ul class="parent">
|
||||
<li><a href="{{parent}}">..</a></li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
<ul class="folders">
|
||||
{% for item in directories %}
|
||||
<li><a href="file://{{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>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -14,10 +14,12 @@ pre { margin: 2px; }
|
||||
th, td { border: 1px solid grey; padding: 0px 5px; }
|
||||
th { background: lightgrey; }
|
||||
th pre { color: grey; text-align: left; }
|
||||
.noscript, .noscript-text { color:red; }
|
||||
.noscript-text { margin-bottom: 5cm; }
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<noscript><h1>View Only</h1><p>Changing settings requires javascript to be enabled</p></noscript>
|
||||
<noscript><h1 class="noscript">View Only</h1><p class="noscript-text">Changing settings requires javascript to be enabled!</p></noscript>
|
||||
<header><h1>{{ title }}</h1></header>
|
||||
<table>
|
||||
{% for section in config.DATA %}
|
||||
|
||||
@@ -21,6 +21,6 @@ GNU General Public License for more details.
|
||||
<p>
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <a href="http://www.gnu.org/licenses/">
|
||||
http://www.gnu.org/licenses/</a> or open <a href="qute:gpl">qute:gpl</a>.
|
||||
http://www.gnu.org/licenses/</a> or open <a href="qute://gpl">qute://gpl</a>.
|
||||
</p>
|
||||
{% endblock %}
|
||||
|
||||
548
qutebrowser/img/file.svg
Normal file
548
qutebrowser/img/file.svg
Normal file
@@ -0,0 +1,548 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
inkscape:export-ydpi="240.00000"
|
||||
inkscape:export-xdpi="240.00000"
|
||||
inkscape:export-filename="/home/jimmac/gfx/novell/pdes/trunk/docs/BIGmime-text.png"
|
||||
sodipodi:docname="text-x-generic.svg"
|
||||
sodipodi:docbase="/home/jimmac/src/cvs/tango-icon-theme/scalable/mimetypes"
|
||||
inkscape:version="0.46"
|
||||
sodipodi:version="0.32"
|
||||
id="svg249"
|
||||
height="48.000000px"
|
||||
width="48.000000px"
|
||||
inkscape:output_extension="org.inkscape.output.svg.inkscape">
|
||||
<defs
|
||||
id="defs3">
|
||||
<inkscape:perspective
|
||||
sodipodi:type="inkscape:persp3d"
|
||||
inkscape:vp_x="0 : 24 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_z="48 : 24 : 1"
|
||||
inkscape:persp3d-origin="24 : 16 : 1"
|
||||
id="perspective78" />
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient5060"
|
||||
id="radialGradient6719"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(-2.774389,0,0,1.969706,112.7623,-872.8854)"
|
||||
cx="605.71429"
|
||||
cy="486.64789"
|
||||
fx="605.71429"
|
||||
fy="486.64789"
|
||||
r="117.14286" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient5060">
|
||||
<stop
|
||||
style="stop-color:black;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop5062" />
|
||||
<stop
|
||||
style="stop-color:black;stop-opacity:0;"
|
||||
offset="1"
|
||||
id="stop5064" />
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient5060"
|
||||
id="radialGradient6717"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(2.774389,0,0,1.969706,-1891.633,-872.8854)"
|
||||
cx="605.71429"
|
||||
cy="486.64789"
|
||||
fx="605.71429"
|
||||
fy="486.64789"
|
||||
r="117.14286" />
|
||||
<linearGradient
|
||||
id="linearGradient5048">
|
||||
<stop
|
||||
style="stop-color:black;stop-opacity:0;"
|
||||
offset="0"
|
||||
id="stop5050" />
|
||||
<stop
|
||||
id="stop5056"
|
||||
offset="0.5"
|
||||
style="stop-color:black;stop-opacity:1;" />
|
||||
<stop
|
||||
style="stop-color:black;stop-opacity:0;"
|
||||
offset="1"
|
||||
id="stop5052" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient5048"
|
||||
id="linearGradient6715"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(2.774389,0,0,1.969706,-1892.179,-872.8854)"
|
||||
x1="302.85715"
|
||||
y1="366.64789"
|
||||
x2="302.85715"
|
||||
y2="609.50507" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient4542">
|
||||
<stop
|
||||
style="stop-color:#000000;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop4544" />
|
||||
<stop
|
||||
style="stop-color:#000000;stop-opacity:0;"
|
||||
offset="1"
|
||||
id="stop4546" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient15662">
|
||||
<stop
|
||||
id="stop15664"
|
||||
offset="0.0000000"
|
||||
style="stop-color:#ffffff;stop-opacity:1.0000000;" />
|
||||
<stop
|
||||
id="stop15666"
|
||||
offset="1.0000000"
|
||||
style="stop-color:#f8f8f8;stop-opacity:1.0000000;" />
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
id="aigrd3"
|
||||
cx="20.8921"
|
||||
cy="64.5679"
|
||||
r="5.257"
|
||||
fx="20.8921"
|
||||
fy="64.5679"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop
|
||||
offset="0"
|
||||
style="stop-color:#F0F0F0"
|
||||
id="stop15573" />
|
||||
<stop
|
||||
offset="1.0000000"
|
||||
style="stop-color:#9a9a9a;stop-opacity:1.0000000;"
|
||||
id="stop15575" />
|
||||
</radialGradient>
|
||||
<radialGradient
|
||||
id="aigrd2"
|
||||
cx="20.8921"
|
||||
cy="114.5684"
|
||||
r="5.256"
|
||||
fx="20.8921"
|
||||
fy="114.5684"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop
|
||||
offset="0"
|
||||
style="stop-color:#F0F0F0"
|
||||
id="stop15566" />
|
||||
<stop
|
||||
offset="1.0000000"
|
||||
style="stop-color:#9a9a9a;stop-opacity:1.0000000;"
|
||||
id="stop15568" />
|
||||
</radialGradient>
|
||||
<linearGradient
|
||||
id="linearGradient269">
|
||||
<stop
|
||||
id="stop270"
|
||||
offset="0.0000000"
|
||||
style="stop-color:#a3a3a3;stop-opacity:1.0000000;" />
|
||||
<stop
|
||||
id="stop271"
|
||||
offset="1.0000000"
|
||||
style="stop-color:#4c4c4c;stop-opacity:1.0000000;" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient259">
|
||||
<stop
|
||||
id="stop260"
|
||||
offset="0.0000000"
|
||||
style="stop-color:#fafafa;stop-opacity:1.0000000;" />
|
||||
<stop
|
||||
id="stop261"
|
||||
offset="1.0000000"
|
||||
style="stop-color:#bbbbbb;stop-opacity:1.0000000;" />
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient269"
|
||||
id="radialGradient15656"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(0.968273,0.000000,0.000000,1.032767,3.353553,0.646447)"
|
||||
cx="8.8244190"
|
||||
cy="3.7561285"
|
||||
fx="8.8244190"
|
||||
fy="3.7561285"
|
||||
r="37.751713" />
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient259"
|
||||
id="radialGradient15658"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="scale(0.960493,1.041132)"
|
||||
cx="33.966679"
|
||||
cy="35.736916"
|
||||
fx="33.966679"
|
||||
fy="35.736916"
|
||||
r="86.708450" />
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient15662"
|
||||
id="radialGradient15668"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(0.968273,0.000000,0.000000,1.032767,3.353553,0.646447)"
|
||||
cx="8.1435566"
|
||||
cy="7.2678967"
|
||||
fx="8.1435566"
|
||||
fy="7.2678967"
|
||||
r="38.158695" />
|
||||
<radialGradient
|
||||
r="5.256"
|
||||
fy="114.5684"
|
||||
fx="20.8921"
|
||||
cy="114.5684"
|
||||
cx="20.8921"
|
||||
gradientTransform="matrix(0.229703,0.000000,0.000000,0.229703,4.613529,3.979808)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
id="radialGradient2283"
|
||||
xlink:href="#aigrd2"
|
||||
inkscape:collect="always" />
|
||||
<radialGradient
|
||||
r="5.257"
|
||||
fy="64.5679"
|
||||
fx="20.8921"
|
||||
cy="64.5679"
|
||||
cx="20.8921"
|
||||
gradientTransform="matrix(0.229703,0.000000,0.000000,0.229703,4.613529,3.979808)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
id="radialGradient2285"
|
||||
xlink:href="#aigrd3"
|
||||
inkscape:collect="always" />
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient4542"
|
||||
id="radialGradient4548"
|
||||
cx="24.306795"
|
||||
cy="42.07798"
|
||||
fx="24.306795"
|
||||
fy="42.07798"
|
||||
r="15.821514"
|
||||
gradientTransform="matrix(1.000000,0.000000,0.000000,0.284916,0.000000,30.08928)"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
inkscape:window-y="160"
|
||||
inkscape:window-x="343"
|
||||
inkscape:window-height="688"
|
||||
inkscape:window-width="872"
|
||||
inkscape:document-units="px"
|
||||
inkscape:grid-bbox="true"
|
||||
showgrid="false"
|
||||
inkscape:current-layer="layer6"
|
||||
inkscape:cy="24.318443"
|
||||
inkscape:cx="25.938708"
|
||||
inkscape:zoom="5.6568542"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
borderopacity="0.25490196"
|
||||
bordercolor="#666666"
|
||||
pagecolor="#ffffff"
|
||||
id="base"
|
||||
inkscape:showpageshadow="false" />
|
||||
<metadata
|
||||
id="metadata4">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title>Generic Text</dc:title>
|
||||
<dc:subject>
|
||||
<rdf:Bag>
|
||||
<rdf:li>text</rdf:li>
|
||||
<rdf:li>plaintext</rdf:li>
|
||||
<rdf:li>regular</rdf:li>
|
||||
<rdf:li>document</rdf:li>
|
||||
</rdf:Bag>
|
||||
</dc:subject>
|
||||
<cc:license
|
||||
rdf:resource="http://creativecommons.org/licenses/publicdomain/" />
|
||||
<dc:creator>
|
||||
<cc:Agent>
|
||||
<dc:title>Jakub Steiner</dc:title>
|
||||
</cc:Agent>
|
||||
</dc:creator>
|
||||
<dc:source>http://jimmac.musichall.cz</dc:source>
|
||||
</cc:Work>
|
||||
<cc:License
|
||||
rdf:about="http://creativecommons.org/licenses/publicdomain/">
|
||||
<cc:permits
|
||||
rdf:resource="http://creativecommons.org/ns#Reproduction" />
|
||||
<cc:permits
|
||||
rdf:resource="http://creativecommons.org/ns#Distribution" />
|
||||
<cc:permits
|
||||
rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
|
||||
</cc:License>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer6"
|
||||
inkscape:label="Shadow">
|
||||
<g
|
||||
style="display:inline"
|
||||
transform="matrix(2.105461e-2,0,0,2.086758e-2,42.85172,41.1536)"
|
||||
id="g6707">
|
||||
<rect
|
||||
style="opacity:0.40206185;color:black;fill:url(#linearGradient6715);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
|
||||
id="rect6709"
|
||||
width="1339.6335"
|
||||
height="478.35718"
|
||||
x="-1559.2523"
|
||||
y="-150.69685" />
|
||||
<path
|
||||
style="opacity:0.40206185;color:black;fill:url(#radialGradient6717);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
|
||||
d="M -219.61876,-150.68038 C -219.61876,-150.68038 -219.61876,327.65041 -219.61876,327.65041 C -76.744594,328.55086 125.78146,220.48075 125.78138,88.454235 C 125.78138,-43.572302 -33.655436,-150.68036 -219.61876,-150.68038 z "
|
||||
id="path6711"
|
||||
sodipodi:nodetypes="cccc" />
|
||||
<path
|
||||
sodipodi:nodetypes="cccc"
|
||||
id="path6713"
|
||||
d="M -1559.2523,-150.68038 C -1559.2523,-150.68038 -1559.2523,327.65041 -1559.2523,327.65041 C -1702.1265,328.55086 -1904.6525,220.48075 -1904.6525,88.454235 C -1904.6525,-43.572302 -1745.2157,-150.68036 -1559.2523,-150.68038 z "
|
||||
style="opacity:0.40206185;color:black;fill:url(#radialGradient6719);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible" />
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
style="display:inline"
|
||||
inkscape:groupmode="layer"
|
||||
inkscape:label="Base"
|
||||
id="layer1">
|
||||
<rect
|
||||
style="color:#000000;fill:url(#radialGradient15658);fill-opacity:1.0000000;fill-rule:nonzero;stroke:url(#radialGradient15656);stroke-width:1.0000000;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.0000000;stroke-dashoffset:0.0000000;stroke-opacity:1.0000000;marker:none;marker-start:none;marker-mid:none;marker-end:none;visibility:visible;display:block;overflow:visible"
|
||||
id="rect15391"
|
||||
width="34.875000"
|
||||
height="40.920494"
|
||||
x="6.6035528"
|
||||
y="3.6464462"
|
||||
ry="1.1490486" />
|
||||
<rect
|
||||
style="color:#000000;fill:none;fill-opacity:1.0000000;fill-rule:nonzero;stroke:url(#radialGradient15668);stroke-width:1.0000000;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.0000000;stroke-dashoffset:0.0000000;stroke-opacity:1.0000000;marker:none;marker-start:none;marker-mid:none;marker-end:none;visibility:visible;display:block;overflow:visible"
|
||||
id="rect15660"
|
||||
width="32.775887"
|
||||
height="38.946384"
|
||||
x="7.6660538"
|
||||
y="4.5839462"
|
||||
ry="0.14904857"
|
||||
rx="0.14904857" />
|
||||
<g
|
||||
transform="translate(0.646447,-3.798933e-2)"
|
||||
id="g2270">
|
||||
<g
|
||||
id="g1440"
|
||||
style="fill:#ffffff;fill-opacity:1.0000000;fill-rule:nonzero;stroke:#000000;stroke-miterlimit:4.0000000"
|
||||
transform="matrix(0.229703,0.000000,0.000000,0.229703,4.967081,4.244972)">
|
||||
<radialGradient
|
||||
id="radialGradient1442"
|
||||
cx="20.892099"
|
||||
cy="114.56840"
|
||||
r="5.2560000"
|
||||
fx="20.892099"
|
||||
fy="114.56840"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop
|
||||
offset="0"
|
||||
style="stop-color:#F0F0F0"
|
||||
id="stop1444" />
|
||||
<stop
|
||||
offset="1"
|
||||
style="stop-color:#474747"
|
||||
id="stop1446" />
|
||||
</radialGradient>
|
||||
<path
|
||||
style="stroke:none"
|
||||
d="M 23.428000,113.07000 C 23.428000,115.04300 21.828000,116.64200 19.855000,116.64200 C 17.881000,116.64200 16.282000,115.04200 16.282000,113.07000 C 16.282000,111.09600 17.882000,109.49700 19.855000,109.49700 C 21.828000,109.49700 23.428000,111.09700 23.428000,113.07000 z "
|
||||
id="path1448" />
|
||||
<radialGradient
|
||||
id="radialGradient1450"
|
||||
cx="20.892099"
|
||||
cy="64.567902"
|
||||
r="5.2570000"
|
||||
fx="20.892099"
|
||||
fy="64.567902"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop
|
||||
offset="0"
|
||||
style="stop-color:#F0F0F0"
|
||||
id="stop1452" />
|
||||
<stop
|
||||
offset="1"
|
||||
style="stop-color:#474747"
|
||||
id="stop1454" />
|
||||
</radialGradient>
|
||||
<path
|
||||
style="stroke:none"
|
||||
d="M 23.428000,63.070000 C 23.428000,65.043000 21.828000,66.643000 19.855000,66.643000 C 17.881000,66.643000 16.282000,65.043000 16.282000,63.070000 C 16.282000,61.096000 17.882000,59.497000 19.855000,59.497000 C 21.828000,59.497000 23.428000,61.097000 23.428000,63.070000 z "
|
||||
id="path1456" />
|
||||
</g>
|
||||
<path
|
||||
style="fill:url(#radialGradient2283);fill-rule:nonzero;stroke:none;stroke-miterlimit:4.0000000"
|
||||
d="M 9.9950109,29.952326 C 9.9950109,30.405530 9.6274861,30.772825 9.1742821,30.772825 C 8.7208483,30.772825 8.3535532,30.405301 8.3535532,29.952326 C 8.3535532,29.498892 8.7210780,29.131597 9.1742821,29.131597 C 9.6274861,29.131597 9.9950109,29.499122 9.9950109,29.952326 z "
|
||||
id="path15570" />
|
||||
<path
|
||||
style="fill:url(#radialGradient2285);fill-rule:nonzero;stroke:none;stroke-miterlimit:4.0000000"
|
||||
d="M 9.9950109,18.467176 C 9.9950109,18.920380 9.6274861,19.287905 9.1742821,19.287905 C 8.7208483,19.287905 8.3535532,18.920380 8.3535532,18.467176 C 8.3535532,18.013742 8.7210780,17.646447 9.1742821,17.646447 C 9.6274861,17.646447 9.9950109,18.013972 9.9950109,18.467176 z "
|
||||
id="path15577" />
|
||||
</g>
|
||||
<path
|
||||
style="fill:none;fill-opacity:0.75000000;fill-rule:evenodd;stroke:#000000;stroke-width:0.98855311;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4.0000000;stroke-opacity:0.017543854"
|
||||
d="M 11.505723,5.4942766 L 11.505723,43.400869"
|
||||
id="path15672"
|
||||
sodipodi:nodetypes="cc" />
|
||||
<path
|
||||
style="fill:none;fill-opacity:0.75000000;fill-rule:evenodd;stroke:#ffffff;stroke-width:1.0000000;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4.0000000;stroke-opacity:0.20467831"
|
||||
d="M 12.500000,5.0205154 L 12.500000,43.038228"
|
||||
id="path15674"
|
||||
sodipodi:nodetypes="cc" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer5"
|
||||
inkscape:label="Text"
|
||||
style="display:inline">
|
||||
<g
|
||||
transform="matrix(0.909091,0.000000,0.000000,1.000000,2.363628,0.000000)"
|
||||
id="g2253">
|
||||
<rect
|
||||
style="color:#000000;fill:#9b9b9b;fill-opacity:0.54970759;fill-rule:nonzero;stroke:none;stroke-width:1.0000000;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.0000000;stroke-dashoffset:0.0000000;stroke-opacity:0.081871338;marker:none;marker-start:none;marker-mid:none;marker-end:none;visibility:visible;display:block;overflow:visible"
|
||||
id="rect15686"
|
||||
width="22.000004"
|
||||
height="1.0000000"
|
||||
x="15.000002"
|
||||
y="9.0000000"
|
||||
rx="0.15156493"
|
||||
ry="0.065390877" />
|
||||
<rect
|
||||
style="color:#000000;fill:#9b9b9b;fill-opacity:0.54970759;fill-rule:nonzero;stroke:none;stroke-width:1.0000000;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.0000000;stroke-dashoffset:0.0000000;stroke-opacity:0.081871338;marker:none;marker-start:none;marker-mid:none;marker-end:none;visibility:visible;display:block;overflow:visible"
|
||||
id="rect15688"
|
||||
width="22.000004"
|
||||
height="1.0000000"
|
||||
x="15.000002"
|
||||
y="11.000000"
|
||||
rx="0.15156493"
|
||||
ry="0.065390877" />
|
||||
<rect
|
||||
style="color:#000000;fill:#9b9b9b;fill-opacity:0.54970759;fill-rule:nonzero;stroke:none;stroke-width:1.0000000;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.0000000;stroke-dashoffset:0.0000000;stroke-opacity:0.081871338;marker:none;marker-start:none;marker-mid:none;marker-end:none;visibility:visible;display:block;overflow:visible"
|
||||
id="rect15690"
|
||||
width="22.000004"
|
||||
height="1.0000000"
|
||||
x="15.000002"
|
||||
y="13.000000"
|
||||
rx="0.15156493"
|
||||
ry="0.065390877" />
|
||||
<rect
|
||||
style="color:#000000;fill:#9b9b9b;fill-opacity:0.54970759;fill-rule:nonzero;stroke:none;stroke-width:1.0000000;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.0000000;stroke-dashoffset:0.0000000;stroke-opacity:0.081871338;marker:none;marker-start:none;marker-mid:none;marker-end:none;visibility:visible;display:block;overflow:visible"
|
||||
id="rect15692"
|
||||
width="22.000004"
|
||||
height="1.0000000"
|
||||
x="15.000002"
|
||||
y="15.000000"
|
||||
rx="0.15156493"
|
||||
ry="0.065390877" />
|
||||
<rect
|
||||
style="color:#000000;fill:#9b9b9b;fill-opacity:0.54970759;fill-rule:nonzero;stroke:none;stroke-width:1.0000000;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.0000000;stroke-dashoffset:0.0000000;stroke-opacity:0.081871338;marker:none;marker-start:none;marker-mid:none;marker-end:none;visibility:visible;display:block;overflow:visible"
|
||||
id="rect15694"
|
||||
width="22.000004"
|
||||
height="1.0000000"
|
||||
x="15.000002"
|
||||
y="17.000000"
|
||||
rx="0.15156493"
|
||||
ry="0.065390877" />
|
||||
<rect
|
||||
style="color:#000000;fill:#9b9b9b;fill-opacity:0.54970759;fill-rule:nonzero;stroke:none;stroke-width:1.0000000;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.0000000;stroke-dashoffset:0.0000000;stroke-opacity:0.081871338;marker:none;marker-start:none;marker-mid:none;marker-end:none;visibility:visible;display:block;overflow:visible"
|
||||
id="rect15696"
|
||||
width="22.000004"
|
||||
height="1.0000000"
|
||||
x="15.000002"
|
||||
y="19.000000"
|
||||
rx="0.15156493"
|
||||
ry="0.065390877" />
|
||||
<rect
|
||||
style="color:#000000;fill:#9b9b9b;fill-opacity:0.54970759;fill-rule:nonzero;stroke:none;stroke-width:1.0000000;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.0000000;stroke-dashoffset:0.0000000;stroke-opacity:0.081871338;marker:none;marker-start:none;marker-mid:none;marker-end:none;visibility:visible;display:block;overflow:visible"
|
||||
id="rect15698"
|
||||
width="22.000004"
|
||||
height="1.0000000"
|
||||
x="15.000002"
|
||||
y="21.000000"
|
||||
rx="0.15156493"
|
||||
ry="0.065390877" />
|
||||
<rect
|
||||
style="color:#000000;fill:#9b9b9b;fill-opacity:0.54970759;fill-rule:nonzero;stroke:none;stroke-width:1.0000000;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.0000000;stroke-dashoffset:0.0000000;stroke-opacity:0.081871338;marker:none;marker-start:none;marker-mid:none;marker-end:none;visibility:visible;display:block;overflow:visible"
|
||||
id="rect15700"
|
||||
width="22.000004"
|
||||
height="1.0000000"
|
||||
x="15.000002"
|
||||
y="23.000000"
|
||||
rx="0.15156493"
|
||||
ry="0.065390877" />
|
||||
<rect
|
||||
style="color:#000000;fill:#9b9b9b;fill-opacity:0.54970759;fill-rule:nonzero;stroke:none;stroke-width:1.0000000;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.0000000;stroke-dashoffset:0.0000000;stroke-opacity:0.081871338;marker:none;marker-start:none;marker-mid:none;marker-end:none;visibility:visible;display:block;overflow:visible"
|
||||
id="rect15732"
|
||||
width="9.9000053"
|
||||
height="1.0000000"
|
||||
x="14.999992"
|
||||
y="25.000000"
|
||||
rx="0.068204239"
|
||||
ry="0.065390877" />
|
||||
<rect
|
||||
style="color:#000000;fill:#9b9b9b;fill-opacity:0.54970759;fill-rule:nonzero;stroke:none;stroke-width:1.0000000;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.0000000;stroke-dashoffset:0.0000000;stroke-opacity:0.081871338;marker:none;marker-start:none;marker-mid:none;marker-end:none;visibility:visible;display:block;overflow:visible"
|
||||
id="rect15736"
|
||||
width="22.000004"
|
||||
height="1.0000000"
|
||||
x="14.999992"
|
||||
y="29.000000"
|
||||
rx="0.15156493"
|
||||
ry="0.065390877" />
|
||||
<rect
|
||||
style="color:#000000;fill:#9b9b9b;fill-opacity:0.54970759;fill-rule:nonzero;stroke:none;stroke-width:1.0000000;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.0000000;stroke-dashoffset:0.0000000;stroke-opacity:0.081871338;marker:none;marker-start:none;marker-mid:none;marker-end:none;visibility:visible;display:block;overflow:visible"
|
||||
id="rect15738"
|
||||
width="22.000004"
|
||||
height="1.0000000"
|
||||
x="14.999992"
|
||||
y="31.000000"
|
||||
rx="0.15156493"
|
||||
ry="0.065390877" />
|
||||
<rect
|
||||
style="color:#000000;fill:#9b9b9b;fill-opacity:0.54970759;fill-rule:nonzero;stroke:none;stroke-width:1.0000000;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.0000000;stroke-dashoffset:0.0000000;stroke-opacity:0.081871338;marker:none;marker-start:none;marker-mid:none;marker-end:none;visibility:visible;display:block;overflow:visible"
|
||||
id="rect15740"
|
||||
width="22.000004"
|
||||
height="1.0000000"
|
||||
x="14.999992"
|
||||
y="33.000000"
|
||||
rx="0.15156493"
|
||||
ry="0.065390877" />
|
||||
<rect
|
||||
style="color:#000000;fill:#9b9b9b;fill-opacity:0.54970759;fill-rule:nonzero;stroke:none;stroke-width:1.0000000;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.0000000;stroke-dashoffset:0.0000000;stroke-opacity:0.081871338;marker:none;marker-start:none;marker-mid:none;marker-end:none;visibility:visible;display:block;overflow:visible"
|
||||
id="rect15742"
|
||||
width="22.000004"
|
||||
height="1.0000000"
|
||||
x="14.999992"
|
||||
y="35.000000"
|
||||
rx="0.15156493"
|
||||
ry="0.065390877" />
|
||||
<rect
|
||||
style="color:#000000;fill:#9b9b9b;fill-opacity:0.54970759;fill-rule:nonzero;stroke:none;stroke-width:1.0000000;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.0000000;stroke-dashoffset:0.0000000;stroke-opacity:0.081871338;marker:none;marker-start:none;marker-mid:none;marker-end:none;visibility:visible;display:block;overflow:visible"
|
||||
id="rect15744"
|
||||
width="15.400014"
|
||||
height="1.0000000"
|
||||
x="14.999992"
|
||||
y="37.000000"
|
||||
rx="0.10609552"
|
||||
ry="0.065390877" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 23 KiB |
424
qutebrowser/img/folder.svg
Normal file
424
qutebrowser/img/folder.svg
Normal file
@@ -0,0 +1,424 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="48.000000px"
|
||||
height="48.000000px"
|
||||
id="svg97"
|
||||
sodipodi:version="0.32"
|
||||
inkscape:version="0.46"
|
||||
sodipodi:docbase="/home/jimmac/src/cvs/tango-icon-theme/scalable/places"
|
||||
sodipodi:docname="folder.svg"
|
||||
inkscape:export-filename="/home/jimmac/Desktop/horlander-style3.png"
|
||||
inkscape:export-xdpi="90.000000"
|
||||
inkscape:export-ydpi="90.000000"
|
||||
inkscape:output_extension="org.inkscape.output.svg.inkscape">
|
||||
<defs
|
||||
id="defs3">
|
||||
<inkscape:perspective
|
||||
sodipodi:type="inkscape:persp3d"
|
||||
inkscape:vp_x="0 : 24 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_z="48 : 24 : 1"
|
||||
inkscape:persp3d-origin="24 : 16 : 1"
|
||||
id="perspective68" />
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient5060"
|
||||
id="radialGradient6719"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(-2.774389,0,0,1.969706,112.7623,-872.8854)"
|
||||
cx="605.71429"
|
||||
cy="486.64789"
|
||||
fx="605.71429"
|
||||
fy="486.64789"
|
||||
r="117.14286" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient5060">
|
||||
<stop
|
||||
style="stop-color:black;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop5062" />
|
||||
<stop
|
||||
style="stop-color:black;stop-opacity:0;"
|
||||
offset="1"
|
||||
id="stop5064" />
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient5060"
|
||||
id="radialGradient6717"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(2.774389,0,0,1.969706,-1891.633,-872.8854)"
|
||||
cx="605.71429"
|
||||
cy="486.64789"
|
||||
fx="605.71429"
|
||||
fy="486.64789"
|
||||
r="117.14286" />
|
||||
<linearGradient
|
||||
id="linearGradient5048">
|
||||
<stop
|
||||
style="stop-color:black;stop-opacity:0;"
|
||||
offset="0"
|
||||
id="stop5050" />
|
||||
<stop
|
||||
id="stop5056"
|
||||
offset="0.5"
|
||||
style="stop-color:black;stop-opacity:1;" />
|
||||
<stop
|
||||
style="stop-color:black;stop-opacity:0;"
|
||||
offset="1"
|
||||
id="stop5052" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient5048"
|
||||
id="linearGradient6715"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(2.774389,0,0,1.969706,-1892.179,-872.8854)"
|
||||
x1="302.85715"
|
||||
y1="366.64789"
|
||||
x2="302.85715"
|
||||
y2="609.50507" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient9806">
|
||||
<stop
|
||||
style="stop-color:#000000;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop9808" />
|
||||
<stop
|
||||
style="stop-color:#000000;stop-opacity:0;"
|
||||
offset="1"
|
||||
id="stop9810" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient9766">
|
||||
<stop
|
||||
style="stop-color:#6194cb;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop9768" />
|
||||
<stop
|
||||
style="stop-color:#729fcf;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop9770" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient3096">
|
||||
<stop
|
||||
id="stop3098"
|
||||
offset="0"
|
||||
style="stop-color:#424242;stop-opacity:1;" />
|
||||
<stop
|
||||
id="stop3100"
|
||||
offset="1.0000000"
|
||||
style="stop-color:#777777;stop-opacity:1.0000000;" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient319"
|
||||
inkscape:collect="always">
|
||||
<stop
|
||||
id="stop320"
|
||||
offset="0"
|
||||
style="stop-color:#ffffff;stop-opacity:1;" />
|
||||
<stop
|
||||
id="stop321"
|
||||
offset="1"
|
||||
style="stop-color:#ffffff;stop-opacity:0;" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient1789">
|
||||
<stop
|
||||
style="stop-color:#202020;stop-opacity:1.0000000;"
|
||||
offset="0.0000000"
|
||||
id="stop1790" />
|
||||
<stop
|
||||
style="stop-color:#b9b9b9;stop-opacity:1.0000000;"
|
||||
offset="1.0000000"
|
||||
id="stop1791" />
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient1789"
|
||||
id="radialGradient238"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(1.055022,-2.734504e-2,0.177703,1.190929,-3.572177,-7.125301)"
|
||||
cx="20.706017"
|
||||
cy="37.517986"
|
||||
fx="20.706017"
|
||||
fy="37.517986"
|
||||
r="30.905205" />
|
||||
<linearGradient
|
||||
id="linearGradient3983">
|
||||
<stop
|
||||
style="stop-color:#ffffff;stop-opacity:0.87628865;"
|
||||
offset="0.0000000"
|
||||
id="stop3984" />
|
||||
<stop
|
||||
style="stop-color:#fffffe;stop-opacity:0.0000000;"
|
||||
offset="1.0000000"
|
||||
id="stop3985" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3983"
|
||||
id="linearGradient491"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(1.516844,0.000000,0.000000,0.708978,-0.879573,-1.318166)"
|
||||
x1="6.2297964"
|
||||
y1="13.773066"
|
||||
x2="9.8980894"
|
||||
y2="66.834053" />
|
||||
<linearGradient
|
||||
gradientUnits="userSpaceOnUse"
|
||||
y2="46.689312"
|
||||
x2="12.853771"
|
||||
y1="32.567184"
|
||||
x1="13.035696"
|
||||
gradientTransform="matrix(1.317489,0.000000,0.000000,0.816256,-0.879573,-1.318166)"
|
||||
id="linearGradient322"
|
||||
xlink:href="#linearGradient319"
|
||||
inkscape:collect="always" />
|
||||
<linearGradient
|
||||
gradientUnits="userSpaceOnUse"
|
||||
y2="6.1802502"
|
||||
x2="15.514889"
|
||||
y1="31.367750"
|
||||
x1="18.112709"
|
||||
id="linearGradient3104"
|
||||
xlink:href="#linearGradient3096"
|
||||
inkscape:collect="always" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient9766"
|
||||
id="linearGradient9772"
|
||||
x1="22.175976"
|
||||
y1="36.987999"
|
||||
x2="22.065331"
|
||||
y2="32.050499"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient9806"
|
||||
id="radialGradient9812"
|
||||
cx="24.35099"
|
||||
cy="41.591846"
|
||||
fx="24.35099"
|
||||
fy="41.591846"
|
||||
r="19.136078"
|
||||
gradientTransform="matrix(1.000000,0.000000,0.000000,0.242494,1.565588e-16,31.50606)"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
fill="#729fcf"
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="0.10196078"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="4"
|
||||
inkscape:cx="54.359127"
|
||||
inkscape:cy="-13.803699"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
inkscape:grid-bbox="true"
|
||||
inkscape:document-units="px"
|
||||
inkscape:window-width="1026"
|
||||
inkscape:window-height="818"
|
||||
inkscape:window-x="169"
|
||||
inkscape:window-y="30"
|
||||
inkscape:showpageshadow="false"
|
||||
stroke="#3465a4" />
|
||||
<metadata
|
||||
id="metadata4">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title>Folder Icon</dc:title>
|
||||
<dc:date />
|
||||
<dc:creator>
|
||||
<cc:Agent>
|
||||
<dc:title>Jakub Steiner</dc:title>
|
||||
</cc:Agent>
|
||||
</dc:creator>
|
||||
<cc:license
|
||||
rdf:resource="http://creativecommons.org/licenses/publicdomain/" />
|
||||
<dc:source>http://jimmac.musichall.cz</dc:source>
|
||||
<dc:subject>
|
||||
<rdf:Bag>
|
||||
<rdf:li>folder</rdf:li>
|
||||
<rdf:li>directory</rdf:li>
|
||||
</rdf:Bag>
|
||||
</dc:subject>
|
||||
</cc:Work>
|
||||
<cc:License
|
||||
rdf:about="http://creativecommons.org/licenses/publicdomain/">
|
||||
<cc:permits
|
||||
rdf:resource="http://creativecommons.org/ns#Reproduction" />
|
||||
<cc:permits
|
||||
rdf:resource="http://creativecommons.org/ns#Distribution" />
|
||||
<cc:permits
|
||||
rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
|
||||
</cc:License>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
id="layer1"
|
||||
inkscape:label="Folder"
|
||||
inkscape:groupmode="layer">
|
||||
<g
|
||||
style="display:inline"
|
||||
transform="matrix(2.262383e-2,0,0,2.086758e-2,43.38343,36.36962)"
|
||||
id="g6707">
|
||||
<rect
|
||||
style="opacity:0.40206185;color:black;fill:url(#linearGradient6715);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
|
||||
id="rect6709"
|
||||
width="1339.6335"
|
||||
height="478.35718"
|
||||
x="-1559.2523"
|
||||
y="-150.69685" />
|
||||
<path
|
||||
style="opacity:0.40206185;color:black;fill:url(#radialGradient6717);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
|
||||
d="M -219.61876,-150.68038 C -219.61876,-150.68038 -219.61876,327.65041 -219.61876,327.65041 C -76.744594,328.55086 125.78146,220.48075 125.78138,88.454235 C 125.78138,-43.572302 -33.655436,-150.68036 -219.61876,-150.68038 z "
|
||||
id="path6711"
|
||||
sodipodi:nodetypes="cccc" />
|
||||
<path
|
||||
sodipodi:nodetypes="cccc"
|
||||
id="path6713"
|
||||
d="M -1559.2523,-150.68038 C -1559.2523,-150.68038 -1559.2523,327.65041 -1559.2523,327.65041 C -1702.1265,328.55086 -1904.6525,220.48075 -1904.6525,88.454235 C -1904.6525,-43.572302 -1745.2157,-150.68036 -1559.2523,-150.68038 z "
|
||||
style="opacity:0.40206185;color:black;fill:url(#radialGradient6719);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible" />
|
||||
</g>
|
||||
<path
|
||||
d="M 4.5217805,38.687417 C 4.5435766,39.103721 4.9816854,39.520026 5.3979900,39.520026 L 36.725011,39.520026 C 37.141313,39.520026 37.535823,39.103721 37.514027,38.687417 L 36.577584,11.460682 C 36.555788,11.044379 36.117687,10.628066 35.701383,10.628066 L 22.430510,10.628066 C 21.945453,10.628066 21.196037,10.312477 21.028866,9.5214338 L 20.417475,6.6283628 C 20.262006,5.8926895 19.535261,5.5904766 19.118957,5.5904766 L 4.3400975,5.5904766 C 3.9237847,5.5904766 3.5292767,6.0067807 3.5510726,6.4230849 L 4.5217805,38.687417 z "
|
||||
id="path216"
|
||||
style="fill:url(#radialGradient238);fill-opacity:1.0000000;fill-rule:nonzero;stroke:url(#linearGradient3104);stroke-width:1.0000000;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.0000000;stroke-dasharray:none;stroke-opacity:1.0000000"
|
||||
sodipodi:nodetypes="ccccccssssccc" />
|
||||
<path
|
||||
sodipodi:nodetypes="cc"
|
||||
id="path9788"
|
||||
d="M 5.2265927,22.5625 L 35.492173,22.5625"
|
||||
style="opacity:0.11363633;color:#000000;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.00000024;stroke-linecap:round;stroke-linejoin:round;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible" />
|
||||
<path
|
||||
sodipodi:nodetypes="cc"
|
||||
id="path9784"
|
||||
d="M 5.0421736,18.5625 L 35.489104,18.5625"
|
||||
style="opacity:0.11363633;color:#000000;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.00000036;stroke-linecap:round;stroke-linejoin:round;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible" />
|
||||
<path
|
||||
style="opacity:0.11363633;color:#000000;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
|
||||
d="M 4.9806965,12.5625 L 35.488057,12.5625"
|
||||
id="path9778"
|
||||
sodipodi:nodetypes="cc" />
|
||||
<path
|
||||
style="opacity:0.11363633;color:#000000;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.00000036;stroke-linecap:round;stroke-linejoin:round;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
|
||||
d="M 5.3861577,32.5625 L 35.494881,32.5625"
|
||||
id="path9798"
|
||||
sodipodi:nodetypes="cc" />
|
||||
<path
|
||||
sodipodi:nodetypes="cc"
|
||||
id="path9800"
|
||||
d="M 5.5091398,34.5625 L 35.496893,34.5625"
|
||||
style="opacity:0.11363633;color:#000000;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.00000024;stroke-linecap:round;stroke-linejoin:round;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible" />
|
||||
<path
|
||||
style="opacity:0.11363633;color:#000000;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.00000036;stroke-linecap:round;stroke-linejoin:round;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
|
||||
d="M 5.0421736,16.5625 L 35.489104,16.5625"
|
||||
id="path9782"
|
||||
sodipodi:nodetypes="cc" />
|
||||
<path
|
||||
sodipodi:nodetypes="cc"
|
||||
id="path9780"
|
||||
d="M 5.0114345,14.5625 L 35.48858,14.5625"
|
||||
style="opacity:0.11363633;color:#000000;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.00000024;stroke-linecap:round;stroke-linejoin:round;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible" />
|
||||
<path
|
||||
sodipodi:nodetypes="cc"
|
||||
id="path9776"
|
||||
d="M 4.9220969,10.5625 L 20.202912,10.5625"
|
||||
style="opacity:0.11363633;color:#000000;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.00000024;stroke-linecap:round;stroke-linejoin:round;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible" />
|
||||
<path
|
||||
style="opacity:0.11363633;color:#000000;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.99999982;stroke-linecap:round;stroke-linejoin:round;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
|
||||
d="M 4.8737534,8.5624999 L 19.657487,8.5624999"
|
||||
id="path9774"
|
||||
sodipodi:nodetypes="cc" />
|
||||
<path
|
||||
style="opacity:0.11363633;color:#000000;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.00000036;stroke-linecap:round;stroke-linejoin:round;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
|
||||
d="M 5.3246666,28.5625 L 35.493876,28.5625"
|
||||
id="path9794"
|
||||
sodipodi:nodetypes="cc" />
|
||||
<path
|
||||
sodipodi:nodetypes="cc"
|
||||
id="path9792"
|
||||
d="M 5.2880638,26.5625 L 35.493184,26.5625"
|
||||
style="opacity:0.11363633;color:#000000;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible" />
|
||||
<path
|
||||
style="opacity:0.11363633;color:#000000;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.00000024;stroke-linecap:round;stroke-linejoin:round;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
|
||||
d="M 5.2265927,24.5625 L 35.492173,24.5625"
|
||||
id="path9790"
|
||||
sodipodi:nodetypes="cc" />
|
||||
<path
|
||||
style="opacity:0.11363633;color:#000000;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.00000012;stroke-linecap:round;stroke-linejoin:round;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
|
||||
d="M 5.1958537,20.5625 L 35.491649,20.5625"
|
||||
id="path9786"
|
||||
sodipodi:nodetypes="cc" />
|
||||
<path
|
||||
sodipodi:nodetypes="cc"
|
||||
id="path9796"
|
||||
d="M 5.3246666,30.5625 L 35.493876,30.5625"
|
||||
style="opacity:0.11363633;color:#000000;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.00000036;stroke-linecap:round;stroke-linejoin:round;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible" />
|
||||
<path
|
||||
style="opacity:0.11363633;color:#000000;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.00000024;stroke-linecap:round;stroke-linejoin:round;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
|
||||
d="M 5.5091398,36.5625 L 35.496893,36.5625"
|
||||
id="path9802"
|
||||
sodipodi:nodetypes="cc" />
|
||||
<path
|
||||
style="color:#000000;fill:url(#linearGradient491);fill-opacity:1.0000000;fill-rule:nonzero;stroke:none;stroke-width:1.2138050;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4.0000000;stroke-dashoffset:0.0000000;stroke-opacity:1.0000000;visibility:visible;display:block;overflow:visible;opacity:0.45142857"
|
||||
d="M 6.0683430,38.864023 C 6.0846856,39.176251 5.8874317,39.384402 5.5697582,39.280326 L 5.5697582,39.280326 C 5.2520766,39.176251 5.0330270,38.968099 5.0166756,38.655870 L 4.0689560,6.5913839 C 4.0526131,6.2791558 4.2341418,6.0906134 4.5463699,6.0906134 L 18.968420,6.0429196 C 19.280648,6.0429196 19.900363,6.3433923 20.101356,7.3651014 L 20.674845,10.180636 C 20.247791,9.7153790 20.255652,9.7010175 20.037287,9.0239299 L 19.631192,7.7647478 C 19.412142,7.0371009 18.932991,6.9328477 18.620763,6.9328477 L 5.7329889,6.9328477 C 5.4207613,6.9328477 5.2235075,7.1409999 5.2398583,7.4532364 L 6.1778636,38.968099 L 6.0683430,38.864023 z "
|
||||
id="path219"
|
||||
sodipodi:nodetypes="cccccccccscccccc" />
|
||||
<g
|
||||
style="stroke-miterlimit:4.0000000;stroke-width:0.99946535;stroke:none;fill-rule:nonzero;fill-opacity:0.75706214;fill:#ffffff"
|
||||
id="g220"
|
||||
transform="matrix(1.040764,0.000000,5.449252e-2,1.040764,-8.670199,2.670594)"
|
||||
inkscape:export-filename="/home/jimmac/ximian_art/icons/nautilus/suse93/gnome-fs-directory.png"
|
||||
inkscape:export-xdpi="74.800003"
|
||||
inkscape:export-ydpi="74.800003">
|
||||
<path
|
||||
style="fill-opacity:0.50847459;fill:#ffffff"
|
||||
d="M 42.417183,8.5151772 C 42.422267,8.4180642 42.289022,8.2681890 42.182066,8.2681716 L 29.150665,8.2660527 C 29.150665,8.2660527 30.062379,8.8540072 31.352477,8.8622963 L 42.405974,8.9333167 C 42.417060,8.7215889 42.408695,8.6772845 42.417183,8.5151772 z "
|
||||
id="path221"
|
||||
sodipodi:nodetypes="cscscs" />
|
||||
</g>
|
||||
<path
|
||||
style="color:#000000;fill:url(#linearGradient9772);fill-opacity:1.0;fill-rule:nonzero;stroke:#3465a4;stroke-width:1.0000000;stroke-linecap:butt;stroke-linejoin:round;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4.0000000;stroke-dasharray:none;stroke-dashoffset:0.0000000;stroke-opacity:1;visibility:visible;display:block"
|
||||
d="M 39.783532,39.510620 C 40.927426,39.466556 41.746608,38.414321 41.830567,37.189615 C 42.622354,25.640928 43.489927,15.957666 43.489927,15.957666 C 43.562082,15.710182 43.322016,15.462699 43.009787,15.462699 L 8.6386304,15.462699 C 8.6386304,15.462699 6.7883113,37.329591 6.7883113,37.329591 C 6.6737562,38.311657 6.3223038,39.134309 5.2384755,39.513304 L 39.783532,39.510620 z "
|
||||
id="path233"
|
||||
sodipodi:nodetypes="cscccscc"
|
||||
inkscape:export-filename="/home/jimmac/ximian_art/icons/nautilus/suse93/gnome-fs-directory.png"
|
||||
inkscape:export-xdpi="74.800003"
|
||||
inkscape:export-ydpi="74.800003" />
|
||||
<path
|
||||
sodipodi:nodetypes="ccsscsc"
|
||||
id="path304"
|
||||
d="M 9.6202444,16.463921 L 42.411343,16.528735 L 40.837297,36.530714 C 40.752975,37.602225 40.386619,37.958929 38.964641,37.958929 C 37.093139,37.958929 10.286673,37.926522 7.569899,37.926522 C 7.8034973,37.605711 7.9036547,36.937899 7.9049953,36.92191 L 9.6202444,16.463921 z "
|
||||
style="opacity:0.46590909;fill:none;fill-opacity:1.0000000;fill-rule:evenodd;stroke:url(#linearGradient322);stroke-width:0.99999970px;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1.0000000" />
|
||||
<path
|
||||
sodipodi:nodetypes="ccccc"
|
||||
id="path323"
|
||||
d="M 9.6202481,16.223182 L 8.4536014,31.866453 C 8.4536014,31.866453 16.749756,27.718375 27.119949,27.718375 C 37.490142,27.718375 42.675239,16.223182 42.675239,16.223182 L 9.6202481,16.223182 z "
|
||||
style="fill:#ffffff;fill-opacity:0.089285679;fill-rule:evenodd;stroke:none;stroke-width:1.0000000px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1.0000000" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer2"
|
||||
inkscape:label="pattern" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 22 KiB |
110
qutebrowser/javascript/position_caret.js
Normal file
110
qutebrowser/javascript/position_caret.js
Normal file
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* Copyright 2015 Artur Shaik <ashaihullin@gmail.com>
|
||||
* Copyright 2015 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/>.
|
||||
*/
|
||||
|
||||
/* eslint-disable max-len */
|
||||
|
||||
/**
|
||||
* Snippet to position caret at top of the page when caret mode is enabled.
|
||||
* Some code was borrowed from:
|
||||
*
|
||||
* https://github.com/1995eaton/chromium-vim/blob/master/content_scripts/dom.js
|
||||
* https://github.com/1995eaton/chromium-vim/blob/master/content_scripts/visual.js
|
||||
*/
|
||||
|
||||
/* eslint-enable max-len */
|
||||
|
||||
"use strict";
|
||||
|
||||
function isElementInViewport(node) {
|
||||
var i;
|
||||
var boundingRect = (node.getClientRects()[0] ||
|
||||
node.getBoundingClientRect());
|
||||
if (boundingRect.width <= 1 && boundingRect.height <= 1) {
|
||||
var rects = node.getClientRects();
|
||||
for (i = 0; i < rects.length; i++) {
|
||||
if (rects[i].width > rects[0].height &&
|
||||
rects[i].height > rects[0].height) {
|
||||
boundingRect = rects[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (boundingRect === undefined) {
|
||||
return null;
|
||||
}
|
||||
if (boundingRect.top > innerHeight || boundingRect.left > innerWidth) {
|
||||
return null;
|
||||
}
|
||||
if (boundingRect.width <= 1 || boundingRect.height <= 1) {
|
||||
var children = node.children;
|
||||
var visibleChildNode = false;
|
||||
var l = children.length;
|
||||
for (i = 0; i < l; ++i) {
|
||||
boundingRect = (children[i].getClientRects()[0] ||
|
||||
children[i].getBoundingClientRect());
|
||||
if (boundingRect.width > 1 && boundingRect.height > 1) {
|
||||
visibleChildNode = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (visibleChildNode === false) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (boundingRect.top + boundingRect.height < 10 ||
|
||||
boundingRect.left + boundingRect.width < -10) {
|
||||
return null;
|
||||
}
|
||||
var computedStyle = window.getComputedStyle(node, null);
|
||||
if (computedStyle.visibility !== 'visible' ||
|
||||
computedStyle.display === 'none' ||
|
||||
node.hasAttribute('disabled') ||
|
||||
parseInt(computedStyle.width, 10) === 0 ||
|
||||
parseInt(computedStyle.height, 10) === 0) {
|
||||
return null;
|
||||
}
|
||||
return boundingRect.top >= -20;
|
||||
}
|
||||
|
||||
(function() {
|
||||
var walker = document.createTreeWalker(document.body, 4, null);
|
||||
var node;
|
||||
var textNodes = [];
|
||||
var el;
|
||||
while ((node = walker.nextNode())) {
|
||||
if (node.nodeType === 3 && node.data.trim() !== '') {
|
||||
textNodes.push(node);
|
||||
}
|
||||
}
|
||||
for (var i = 0; i < textNodes.length; i++) {
|
||||
var element = textNodes[i].parentElement;
|
||||
if (isElementInViewport(element.parentElement)) {
|
||||
el = element;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (el !== undefined) {
|
||||
var range = document.createRange();
|
||||
range.setStart(el, 0);
|
||||
range.setEnd(el, 0);
|
||||
var sel = window.getSelection();
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(range);
|
||||
}
|
||||
})();
|
||||
@@ -23,7 +23,7 @@ import re
|
||||
import functools
|
||||
import unicodedata
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QObject
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import usertypes, log, utils, objreg
|
||||
@@ -49,6 +49,8 @@ class BaseKeyParser(QObject):
|
||||
special: execute() was called via a special key binding
|
||||
|
||||
do_log: Whether to log keypresses or not.
|
||||
passthrough: Whether unbound keys should be passed through with this
|
||||
handler.
|
||||
|
||||
Attributes:
|
||||
bindings: Bound key bindings
|
||||
@@ -69,6 +71,7 @@ class BaseKeyParser(QObject):
|
||||
|
||||
keystring_updated = pyqtSignal(str)
|
||||
do_log = True
|
||||
passthrough = False
|
||||
|
||||
Match = usertypes.enum('Match', ['partial', 'definitive', 'ambiguous',
|
||||
'other', 'none'])
|
||||
@@ -137,6 +140,9 @@ class BaseKeyParser(QObject):
|
||||
(countstr, cmd_input) = re.match(r'^(\d*)(.*)',
|
||||
self._keystring).groups()
|
||||
count = int(countstr) if countstr else None
|
||||
if count == 0 and not cmd_input:
|
||||
cmd_input = self._keystring
|
||||
count = None
|
||||
else:
|
||||
cmd_input = self._keystring
|
||||
count = None
|
||||
@@ -159,12 +165,6 @@ class BaseKeyParser(QObject):
|
||||
key = e.key()
|
||||
self._debug_log("Got key: 0x{:x} / text: '{}'".format(key, txt))
|
||||
|
||||
if key == Qt.Key_Escape:
|
||||
self._debug_log("Escape pressed, discarding '{}'.".format(
|
||||
self._keystring))
|
||||
self._keystring = ''
|
||||
return self.Match.none
|
||||
|
||||
if len(txt) == 1:
|
||||
category = unicodedata.category(txt)
|
||||
is_control_char = (category == 'Cc')
|
||||
@@ -186,16 +186,13 @@ class BaseKeyParser(QObject):
|
||||
|
||||
match, binding = self._match_key(cmd_input)
|
||||
|
||||
if not isinstance(match, self.Match):
|
||||
raise TypeError("Value {} is no Match member!".format(match))
|
||||
|
||||
if match == self.Match.definitive:
|
||||
self._debug_log("Definitive match for '{}'.".format(
|
||||
self._keystring))
|
||||
self._keystring = ''
|
||||
self.execute(binding, self.Type.chain, count)
|
||||
elif match == self.Match.ambiguous:
|
||||
self._debug_log("Ambigious match for '{}'.".format(
|
||||
self._debug_log("Ambiguous match for '{}'.".format(
|
||||
self._keystring))
|
||||
self._handle_ambiguous_match(binding, count)
|
||||
elif match == self.Match.partial:
|
||||
@@ -205,6 +202,8 @@ class BaseKeyParser(QObject):
|
||||
self._debug_log("Giving up with '{}', no matches".format(
|
||||
self._keystring))
|
||||
self._keystring = ''
|
||||
else:
|
||||
raise AssertionError("Invalid match value {!r}".format(match))
|
||||
return match
|
||||
|
||||
def _match_key(self, cmd_input):
|
||||
@@ -300,6 +299,7 @@ class BaseKeyParser(QObject):
|
||||
True if the event was handled, False otherwise.
|
||||
"""
|
||||
handled = self._handle_special_key(e)
|
||||
|
||||
if handled or not self._supports_chains:
|
||||
return handled
|
||||
match = self._handle_single_key(e)
|
||||
@@ -326,17 +326,21 @@ class BaseKeyParser(QObject):
|
||||
self.special_bindings = {}
|
||||
keyconfparser = objreg.get('key-config')
|
||||
for (key, cmd) in keyconfparser.get_bindings_for(modename).items():
|
||||
if not cmd:
|
||||
continue
|
||||
elif key.startswith('<') and key.endswith('>'):
|
||||
keystr = utils.normalize_keystr(key[1:-1])
|
||||
self.special_bindings[keystr] = cmd
|
||||
elif self._supports_chains:
|
||||
self.bindings[key] = cmd
|
||||
elif self._warn_on_keychains:
|
||||
log.keyboard.warning(
|
||||
"Ignoring keychain '{}' in mode '{}' because "
|
||||
"keychains are not supported there.".format(key, modename))
|
||||
assert cmd
|
||||
self._parse_key_command(modename, key, cmd)
|
||||
|
||||
def _parse_key_command(self, modename, key, cmd):
|
||||
"""Parse the keys and their command and store them in the object."""
|
||||
if key.startswith('<') and key.endswith('>'):
|
||||
keystr = utils.normalize_keystr(key[1:-1])
|
||||
self.special_bindings[keystr] = cmd
|
||||
elif self._supports_chains:
|
||||
self.bindings[key] = cmd
|
||||
elif self._warn_on_keychains:
|
||||
log.keyboard.warning(
|
||||
"Ignoring keychain '{}' in mode '{}' because "
|
||||
"keychains are not supported there."
|
||||
.format(key, modename))
|
||||
|
||||
def execute(self, cmdstr, keytype, count=None):
|
||||
"""Handle a completed keychain.
|
||||
@@ -352,7 +356,13 @@ class BaseKeyParser(QObject):
|
||||
def on_keyconfig_changed(self, mode):
|
||||
"""Re-read the config if a key binding was changed."""
|
||||
if self._modename is None:
|
||||
raise AttributeError("on_keyconfig_changed called but no section "
|
||||
raise AssertionError("on_keyconfig_changed called but no section "
|
||||
"defined!")
|
||||
if mode == self._modename:
|
||||
self.read_config()
|
||||
|
||||
def clear_keystring(self):
|
||||
"""Clear the currently entered key sequence."""
|
||||
self._debug_log("discarding keystring '{}'.".format(self._keystring))
|
||||
self._keystring = ''
|
||||
self.keystring_updated.emit(self._keystring)
|
||||
|
||||
@@ -55,6 +55,7 @@ class PassthroughKeyParser(CommandKeyParser):
|
||||
"""
|
||||
|
||||
do_log = False
|
||||
passthrough = True
|
||||
|
||||
def __init__(self, win_id, mode, parent=None, warn=True):
|
||||
"""Constructor.
|
||||
|
||||
@@ -78,42 +78,36 @@ def init(win_id, parent):
|
||||
KM.prompt: keyparser.PassthroughKeyParser(win_id, 'prompt', modeman,
|
||||
warn=False),
|
||||
KM.yesno: modeparsers.PromptKeyParser(win_id, modeman),
|
||||
KM.caret: modeparsers.CaretKeyParser(win_id, modeman),
|
||||
}
|
||||
objreg.register('keyparsers', keyparsers, scope='window', window=win_id)
|
||||
modeman.destroyed.connect(
|
||||
functools.partial(objreg.delete, 'keyparsers', scope='window',
|
||||
window=win_id))
|
||||
modeman.register(KM.normal, keyparsers[KM.normal].handle)
|
||||
modeman.register(KM.hint, keyparsers[KM.hint].handle)
|
||||
modeman.register(KM.insert, keyparsers[KM.insert].handle, passthrough=True)
|
||||
modeman.register(KM.passthrough, keyparsers[KM.passthrough].handle,
|
||||
passthrough=True)
|
||||
modeman.register(KM.command, keyparsers[KM.command].handle,
|
||||
passthrough=True)
|
||||
modeman.register(KM.prompt, keyparsers[KM.prompt].handle, passthrough=True)
|
||||
modeman.register(KM.yesno, keyparsers[KM.yesno].handle)
|
||||
for mode, parser in keyparsers.items():
|
||||
modeman.register(mode, parser)
|
||||
return modeman
|
||||
|
||||
|
||||
def _get_modeman(win_id):
|
||||
def instance(win_id):
|
||||
"""Get a modemanager object."""
|
||||
return objreg.get('mode-manager', scope='window', window=win_id)
|
||||
|
||||
|
||||
def enter(win_id, mode, reason=None, only_if_normal=False):
|
||||
"""Enter the mode 'mode'."""
|
||||
_get_modeman(win_id).enter(mode, reason, only_if_normal)
|
||||
instance(win_id).enter(mode, reason, only_if_normal)
|
||||
|
||||
|
||||
def leave(win_id, mode, reason=None):
|
||||
"""Leave the mode 'mode'."""
|
||||
_get_modeman(win_id).leave(mode, reason)
|
||||
instance(win_id).leave(mode, reason)
|
||||
|
||||
|
||||
def maybe_leave(win_id, mode, reason=None):
|
||||
"""Convenience method to leave 'mode' without exceptions."""
|
||||
try:
|
||||
_get_modeman(win_id).leave(mode, reason)
|
||||
instance(win_id).leave(mode, reason)
|
||||
except NotInModeError as e:
|
||||
# This is rather likely to happen, so we only log to debug log.
|
||||
log.modes.debug("{} (leave reason: {})".format(e, reason))
|
||||
@@ -124,10 +118,9 @@ class ModeManager(QObject):
|
||||
"""Manager for keyboard modes.
|
||||
|
||||
Attributes:
|
||||
passthrough: A list of modes in which to pass through events.
|
||||
mode: The mode we're currently in.
|
||||
_win_id: The window ID of this ModeManager
|
||||
_handlers: A dictionary of modes and their handlers.
|
||||
_parsers: A dictionary of modes and their keyparsers.
|
||||
_forward_unbound_keys: If we should forward unbound keys.
|
||||
_releaseevents_to_pass: A set of KeyEvents where the keyPressEvent was
|
||||
passed through, so the release event should as
|
||||
@@ -149,8 +142,7 @@ class ModeManager(QObject):
|
||||
def __init__(self, win_id, parent=None):
|
||||
super().__init__(parent)
|
||||
self._win_id = win_id
|
||||
self._handlers = {}
|
||||
self.passthrough = []
|
||||
self._parsers = {}
|
||||
self.mode = usertypes.KeyMode.normal
|
||||
self._releaseevents_to_pass = set()
|
||||
self._forward_unbound_keys = config.get(
|
||||
@@ -158,8 +150,7 @@ class ModeManager(QObject):
|
||||
objreg.get('config').changed.connect(self.set_forward_unbound_keys)
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self, mode=self.mode,
|
||||
passthrough=self.passthrough)
|
||||
return utils.get_repr(self, mode=self.mode)
|
||||
|
||||
def _eventFilter_keypress(self, event):
|
||||
"""Handle filtering of KeyPress events.
|
||||
@@ -171,11 +162,11 @@ class ModeManager(QObject):
|
||||
True if event should be filtered, False otherwise.
|
||||
"""
|
||||
curmode = self.mode
|
||||
handler = self._handlers[curmode]
|
||||
parser = self._parsers[curmode]
|
||||
if curmode != usertypes.KeyMode.insert:
|
||||
log.modes.debug("got keypress in mode {} - calling handler "
|
||||
"{}".format(curmode, utils.qualname(handler)))
|
||||
handled = handler(event) if handler is not None else False
|
||||
log.modes.debug("got keypress in mode {} - delegating to "
|
||||
"{}".format(curmode, utils.qualname(parser)))
|
||||
handled = parser.handle(event)
|
||||
|
||||
is_non_alnum = bool(event.modifiers()) or not event.text().strip()
|
||||
focus_widget = QApplication.instance().focusWidget()
|
||||
@@ -185,7 +176,7 @@ class ModeManager(QObject):
|
||||
filter_this = True
|
||||
elif is_tab and not isinstance(focus_widget, QWebView):
|
||||
filter_this = True
|
||||
elif (curmode in self.passthrough or
|
||||
elif (parser.passthrough or
|
||||
self._forward_unbound_keys == 'all' or
|
||||
(self._forward_unbound_keys == 'auto' and is_non_alnum)):
|
||||
filter_this = False
|
||||
@@ -200,8 +191,8 @@ class ModeManager(QObject):
|
||||
"passthrough: {}, is_non_alnum: {}, is_tab {} --> "
|
||||
"filter: {} (focused: {!r})".format(
|
||||
handled, self._forward_unbound_keys,
|
||||
curmode in self.passthrough, is_non_alnum,
|
||||
is_tab, filter_this, focus_widget))
|
||||
parser.passthrough, is_non_alnum, is_tab,
|
||||
filter_this, focus_widget))
|
||||
return filter_this
|
||||
|
||||
def _eventFilter_keyrelease(self, event):
|
||||
@@ -224,20 +215,16 @@ class ModeManager(QObject):
|
||||
log.modes.debug("filter: {}".format(filter_this))
|
||||
return filter_this
|
||||
|
||||
def register(self, mode, handler, passthrough=False):
|
||||
def register(self, mode, parser):
|
||||
"""Register a new mode.
|
||||
|
||||
Args:
|
||||
mode: The name of the mode.
|
||||
handler: Handler for keyPressEvents.
|
||||
passthrough: Whether to pass key bindings in this mode through to
|
||||
the widgets.
|
||||
parser: The KeyParser which should be used.
|
||||
"""
|
||||
if not isinstance(mode, usertypes.KeyMode):
|
||||
raise TypeError("Mode {} is no KeyMode member!".format(mode))
|
||||
self._handlers[mode] = handler
|
||||
if passthrough:
|
||||
self.passthrough.append(mode)
|
||||
assert isinstance(mode, usertypes.KeyMode)
|
||||
assert parser is not None
|
||||
self._parsers[mode] = parser
|
||||
|
||||
def enter(self, mode, reason=None, only_if_normal=False):
|
||||
"""Enter a new mode.
|
||||
@@ -251,8 +238,8 @@ class ModeManager(QObject):
|
||||
raise TypeError("Mode {} is no KeyMode member!".format(mode))
|
||||
log.modes.debug("Entering mode {}{}".format(
|
||||
mode, '' if reason is None else ' (reason: {})'.format(reason)))
|
||||
if mode not in self._handlers:
|
||||
raise ValueError("No handler for mode {}".format(mode))
|
||||
if mode not in self._parsers:
|
||||
raise ValueError("No keyparser for mode {}".format(mode))
|
||||
prompt_modes = (usertypes.KeyMode.prompt, usertypes.KeyMode.yesno)
|
||||
if self.mode == mode or (self.mode in prompt_modes and
|
||||
mode in prompt_modes):
|
||||
@@ -330,3 +317,8 @@ class ModeManager(QObject):
|
||||
return self._eventFilter_keypress(event)
|
||||
else:
|
||||
return self._eventFilter_keyrelease(event)
|
||||
|
||||
@cmdutils.register(instance='mode-manager', scope='window', hide=True)
|
||||
def clear_keychain(self):
|
||||
"""Clear the currently entered key chain."""
|
||||
self._parsers[self.mode].clear_keystring()
|
||||
|
||||
@@ -218,3 +218,15 @@ class HintKeyParser(keyparser.CommandKeyParser):
|
||||
hintmanager = objreg.get('hintmanager', scope='tab',
|
||||
window=self._win_id, tab='current')
|
||||
hintmanager.handle_partial_key(keystr)
|
||||
|
||||
|
||||
class CaretKeyParser(keyparser.CommandKeyParser):
|
||||
|
||||
"""KeyParser for caret mode."""
|
||||
|
||||
passthrough = True
|
||||
|
||||
def __init__(self, win_id, parent=None):
|
||||
super().__init__(win_id, parent, supports_count=True,
|
||||
supports_chains=True)
|
||||
self.read_config('caret')
|
||||
|
||||
@@ -22,9 +22,10 @@
|
||||
import binascii
|
||||
import base64
|
||||
import itertools
|
||||
import functools
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot, QRect, QPoint, QTimer, Qt
|
||||
from PyQt5.QtWidgets import QWidget, QVBoxLayout
|
||||
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication
|
||||
|
||||
from qutebrowser.commands import runners, cmdutils
|
||||
from qutebrowser.config import config
|
||||
@@ -33,12 +34,54 @@ from qutebrowser.mainwindow import tabbedbrowser
|
||||
from qutebrowser.mainwindow.statusbar import bar
|
||||
from qutebrowser.completion import completionwidget
|
||||
from qutebrowser.keyinput import modeman
|
||||
from qutebrowser.browser import hints, downloads, downloadview
|
||||
from qutebrowser.browser import hints, downloads, downloadview, commands
|
||||
from qutebrowser.misc import crashsignal
|
||||
|
||||
|
||||
win_id_gen = itertools.count(0)
|
||||
|
||||
|
||||
def get_window(via_ipc, force_window=False, force_tab=False):
|
||||
"""Helper function for app.py to get a window id.
|
||||
|
||||
Args:
|
||||
via_ipc: Whether the request was made via IPC.
|
||||
force_window: Whether to force opening in a window.
|
||||
force_tab: Whether to force opening in a tab.
|
||||
"""
|
||||
if force_window and force_tab:
|
||||
raise ValueError("force_window and force_tab are mutually exclusive!")
|
||||
if not via_ipc:
|
||||
# Initial main window
|
||||
return 0
|
||||
window_to_raise = None
|
||||
open_target = config.get('general', 'new-instance-open-target')
|
||||
if (open_target == 'window' or force_window) and not force_tab:
|
||||
window = MainWindow()
|
||||
window.show()
|
||||
win_id = window.win_id
|
||||
window_to_raise = window
|
||||
else:
|
||||
try:
|
||||
window = objreg.last_window()
|
||||
except objreg.NoWindow:
|
||||
# There is no window left, so we open a new one
|
||||
window = MainWindow()
|
||||
window.show()
|
||||
win_id = window.win_id
|
||||
window_to_raise = window
|
||||
win_id = window.win_id
|
||||
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.raise_()
|
||||
window_to_raise.activateWindow()
|
||||
QApplication.instance().alert(window_to_raise)
|
||||
return win_id
|
||||
|
||||
|
||||
class MainWindow(QWidget):
|
||||
|
||||
"""The main window of qutebrowser.
|
||||
@@ -48,8 +91,8 @@ class MainWindow(QWidget):
|
||||
|
||||
Attributes:
|
||||
status: The StatusBar widget.
|
||||
tabbed_browser: The TabbedBrowser widget.
|
||||
_downloadview: The DownloadView widget.
|
||||
_tabbed_browser: The TabbedBrowser widget.
|
||||
_vbox: The main QVBoxLayout.
|
||||
_commandrunner: The main CommandRunner instance.
|
||||
"""
|
||||
@@ -78,14 +121,6 @@ class MainWindow(QWidget):
|
||||
window=self.win_id)
|
||||
|
||||
self.setWindowTitle('qutebrowser')
|
||||
if geometry is not None:
|
||||
self._load_geometry(geometry)
|
||||
elif self.win_id == 0:
|
||||
self._load_state_geometry()
|
||||
else:
|
||||
self._set_default_geometry()
|
||||
log.init.debug("Initial main window geometry: {}".format(
|
||||
self.geometry()))
|
||||
self._vbox = QVBoxLayout(self)
|
||||
self._vbox.setContentsMargins(0, 0, 0, 0)
|
||||
self._vbox.setSpacing(0)
|
||||
@@ -97,9 +132,16 @@ class MainWindow(QWidget):
|
||||
|
||||
self._downloadview = downloadview.DownloadView(self.win_id)
|
||||
|
||||
self._tabbed_browser = tabbedbrowser.TabbedBrowser(self.win_id)
|
||||
objreg.register('tabbed-browser', self._tabbed_browser, scope='window',
|
||||
self.tabbed_browser = tabbedbrowser.TabbedBrowser(self.win_id)
|
||||
objreg.register('tabbed-browser', self.tabbed_browser, scope='window',
|
||||
window=self.win_id)
|
||||
dispatcher = commands.CommandDispatcher(self.win_id,
|
||||
self.tabbed_browser)
|
||||
objreg.register('command-dispatcher', dispatcher, scope='window',
|
||||
window=self.win_id)
|
||||
self.tabbed_browser.destroyed.connect(
|
||||
functools.partial(objreg.delete, 'command-dispatcher',
|
||||
scope='window', window=self.win_id))
|
||||
|
||||
# We need to set an explicit parent for StatusBar because it does some
|
||||
# show/hide magic immediately which would mean it'd show up as a
|
||||
@@ -116,6 +158,15 @@ class MainWindow(QWidget):
|
||||
log.init.debug("Initializing modes...")
|
||||
modeman.init(self.win_id, self)
|
||||
|
||||
if geometry is not None:
|
||||
self._load_geometry(geometry)
|
||||
elif self.win_id == 0:
|
||||
self._load_state_geometry()
|
||||
else:
|
||||
self._set_default_geometry()
|
||||
log.init.debug("Initial main window geometry: {}".format(
|
||||
self.geometry()))
|
||||
|
||||
self._connect_signals()
|
||||
|
||||
# When we're here the statusbar might not even really exist yet, so
|
||||
@@ -139,20 +190,22 @@ class MainWindow(QWidget):
|
||||
"""Resize the completion if related config options changed."""
|
||||
if section == 'completion' and option in ('height', 'shrink'):
|
||||
self.resize_completion()
|
||||
elif section == 'ui' and option == 'statusbar-padding':
|
||||
self.resize_completion()
|
||||
elif section == 'ui' and option == 'downloads-position':
|
||||
self._add_widgets()
|
||||
|
||||
def _add_widgets(self):
|
||||
"""Add or readd all widgets to the VBox."""
|
||||
self._vbox.removeWidget(self._tabbed_browser)
|
||||
self._vbox.removeWidget(self.tabbed_browser)
|
||||
self._vbox.removeWidget(self._downloadview)
|
||||
self._vbox.removeWidget(self.status)
|
||||
position = config.get('ui', 'downloads-position')
|
||||
if position == 'north':
|
||||
if position == 'top':
|
||||
self._vbox.addWidget(self._downloadview)
|
||||
self._vbox.addWidget(self._tabbed_browser)
|
||||
elif position == 'south':
|
||||
self._vbox.addWidget(self._tabbed_browser)
|
||||
self._vbox.addWidget(self.tabbed_browser)
|
||||
elif position == 'bottom':
|
||||
self._vbox.addWidget(self.tabbed_browser)
|
||||
self._vbox.addWidget(self._downloadview)
|
||||
else:
|
||||
raise ValueError("Invalid position {}!".format(position))
|
||||
@@ -173,6 +226,13 @@ class MainWindow(QWidget):
|
||||
else:
|
||||
self._load_geometry(geom)
|
||||
|
||||
def _save_geometry(self):
|
||||
"""Save the window geometry to the state config."""
|
||||
state_config = objreg.get('state-config')
|
||||
data = bytes(self.saveGeometry())
|
||||
geom = base64.b64encode(data).decode('ASCII')
|
||||
state_config['geometry']['mainwindow'] = geom
|
||||
|
||||
def _load_geometry(self, geom):
|
||||
"""Load geometry from a bytes object.
|
||||
|
||||
@@ -212,7 +272,7 @@ class MainWindow(QWidget):
|
||||
prompter = self._get_object('prompter')
|
||||
|
||||
# misc
|
||||
self._tabbed_browser.close_window.connect(self.close)
|
||||
self.tabbed_browser.close_window.connect(self.close)
|
||||
mode_manager.entered.connect(hints.on_mode_entered)
|
||||
|
||||
# status bar
|
||||
@@ -333,12 +393,22 @@ class MainWindow(QWidget):
|
||||
super().resizeEvent(e)
|
||||
self.resize_completion()
|
||||
self._downloadview.updateGeometry()
|
||||
self._tabbed_browser.tabBar().refresh()
|
||||
self.tabbed_browser.tabBar().refresh()
|
||||
|
||||
def _do_close(self):
|
||||
"""Helper function for closeEvent."""
|
||||
objreg.get('session-manager').save_last_window_session()
|
||||
self._save_geometry()
|
||||
log.destroy.debug("Closing window {}".format(self.win_id))
|
||||
self.tabbed_browser.shutdown()
|
||||
|
||||
def closeEvent(self, e):
|
||||
"""Override closeEvent to display a confirmation if needed."""
|
||||
if crashsignal.is_crashing:
|
||||
e.accept()
|
||||
return
|
||||
confirm_quit = config.get('ui', 'confirm-quit')
|
||||
tab_count = self._tabbed_browser.count()
|
||||
tab_count = self.tabbed_browser.count()
|
||||
download_manager = objreg.get('download-manager', scope='window',
|
||||
window=self.win_id)
|
||||
download_count = download_manager.rowCount()
|
||||
@@ -368,8 +438,4 @@ class MainWindow(QWidget):
|
||||
e.ignore()
|
||||
return
|
||||
e.accept()
|
||||
if len(objreg.window_registry) == 1:
|
||||
objreg.get('session-manager').save_last_window_session()
|
||||
objreg.get('app').geometry = bytes(self.saveGeometry())
|
||||
log.destroy.debug("Closing window {}".format(self.win_id))
|
||||
self._tabbed_browser.shutdown()
|
||||
self._do_close()
|
||||
|
||||
@@ -36,6 +36,7 @@ from qutebrowser.mainwindow.statusbar import text as textwidget
|
||||
PreviousWidget = usertypes.enum('PreviousWidget', ['none', 'prompt',
|
||||
'command'])
|
||||
Severity = usertypes.enum('Severity', ['normal', 'warning', 'error'])
|
||||
CaretMode = usertypes.enum('CaretMode', ['off', 'on', 'selection'])
|
||||
|
||||
|
||||
class StatusBar(QWidget):
|
||||
@@ -77,6 +78,16 @@ class StatusBar(QWidget):
|
||||
For some reason we need to have this as class attribute
|
||||
so pyqtProperty works correctly.
|
||||
|
||||
_command_active: If we're currently in command mode.
|
||||
|
||||
For some reason we need to have this as class
|
||||
attribute so pyqtProperty works correctly.
|
||||
|
||||
_caret_mode: The current caret mode (off/on/selection).
|
||||
|
||||
For some reason we need to have this as class attribute
|
||||
so pyqtProperty works correctly.
|
||||
|
||||
Signals:
|
||||
resized: Emitted when the statusbar has resized, so the completion
|
||||
widget can adjust its size to it.
|
||||
@@ -91,32 +102,68 @@ class StatusBar(QWidget):
|
||||
_severity = None
|
||||
_prompt_active = False
|
||||
_insert_active = False
|
||||
_command_active = False
|
||||
_caret_mode = CaretMode.off
|
||||
|
||||
STYLESHEET = """
|
||||
QWidget#StatusBar {
|
||||
|
||||
QWidget#StatusBar,
|
||||
QWidget#StatusBar QLabel,
|
||||
QWidget#StatusBar QLineEdit {
|
||||
{{ font['statusbar'] }}
|
||||
{{ color['statusbar.bg'] }}
|
||||
{{ color['statusbar.fg'] }}
|
||||
}
|
||||
|
||||
QWidget#StatusBar[insert_active="true"] {
|
||||
{{ color['statusbar.bg.insert'] }}
|
||||
QWidget#StatusBar[caret_mode="on"],
|
||||
QWidget#StatusBar[caret_mode="on"] QLabel,
|
||||
QWidget#StatusBar[caret_mode="on"] QLineEdit {
|
||||
{{ color['statusbar.fg.caret'] }}
|
||||
{{ color['statusbar.bg.caret'] }}
|
||||
}
|
||||
|
||||
QWidget#StatusBar[prompt_active="true"] {
|
||||
{{ color['statusbar.bg.prompt'] }}
|
||||
QWidget#StatusBar[caret_mode="selection"],
|
||||
QWidget#StatusBar[caret_mode="selection"] QLabel,
|
||||
QWidget#StatusBar[caret_mode="selection"] QLineEdit {
|
||||
{{ color['statusbar.fg.caret-selection'] }}
|
||||
{{ color['statusbar.bg.caret-selection'] }}
|
||||
}
|
||||
|
||||
QWidget#StatusBar[severity="error"] {
|
||||
QWidget#StatusBar[severity="error"],
|
||||
QWidget#StatusBar[severity="error"] QLabel,
|
||||
QWidget#StatusBar[severity="error"] QLineEdit {
|
||||
{{ color['statusbar.fg.error'] }}
|
||||
{{ color['statusbar.bg.error'] }}
|
||||
}
|
||||
|
||||
QWidget#StatusBar[severity="warning"] {
|
||||
QWidget#StatusBar[severity="warning"],
|
||||
QWidget#StatusBar[severity="warning"] QLabel,
|
||||
QWidget#StatusBar[severity="warning"] QLineEdit {
|
||||
{{ color['statusbar.fg.warning'] }}
|
||||
{{ color['statusbar.bg.warning'] }}
|
||||
}
|
||||
|
||||
QLabel, QLineEdit {
|
||||
{{ color['statusbar.fg'] }}
|
||||
{{ font['statusbar'] }}
|
||||
QWidget#StatusBar[prompt_active="true"],
|
||||
QWidget#StatusBar[prompt_active="true"] QLabel,
|
||||
QWidget#StatusBar[prompt_active="true"] QLineEdit {
|
||||
{{ color['statusbar.fg.prompt'] }}
|
||||
{{ color['statusbar.bg.prompt'] }}
|
||||
}
|
||||
|
||||
QWidget#StatusBar[insert_active="true"],
|
||||
QWidget#StatusBar[insert_active="true"] QLabel,
|
||||
QWidget#StatusBar[insert_active="true"] QLineEdit {
|
||||
{{ color['statusbar.fg.insert'] }}
|
||||
{{ color['statusbar.bg.insert'] }}
|
||||
}
|
||||
|
||||
QWidget#StatusBar[command_active="true"],
|
||||
QWidget#StatusBar[command_active="true"] QLabel,
|
||||
QWidget#StatusBar[command_active="true"] QLineEdit {
|
||||
{{ color['statusbar.fg.command'] }}
|
||||
{{ color['statusbar.bg.command'] }}
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, win_id, parent=None):
|
||||
@@ -133,7 +180,8 @@ class StatusBar(QWidget):
|
||||
self._stopwatch = QTime()
|
||||
|
||||
self._hbox = QHBoxLayout(self)
|
||||
self._hbox.setContentsMargins(0, 0, 0, 0)
|
||||
self.set_hbox_padding()
|
||||
objreg.get('config').changed.connect(self.set_hbox_padding)
|
||||
self._hbox.setSpacing(5)
|
||||
|
||||
self._stack = QStackedLayout()
|
||||
@@ -199,6 +247,11 @@ class StatusBar(QWidget):
|
||||
else:
|
||||
self.show()
|
||||
|
||||
@config.change_filter('ui', 'statusbar-padding')
|
||||
def set_hbox_padding(self):
|
||||
padding = config.get('ui', 'statusbar-padding')
|
||||
self._hbox.setContentsMargins(padding.left, 0, padding.right, 0)
|
||||
|
||||
@pyqtProperty(str)
|
||||
def severity(self):
|
||||
"""Getter for self.severity, so it can be used as Qt property.
|
||||
@@ -248,19 +301,47 @@ class StatusBar(QWidget):
|
||||
self._prompt_active = val
|
||||
self.setStyleSheet(style.get_stylesheet(self.STYLESHEET))
|
||||
|
||||
@pyqtProperty(bool)
|
||||
def command_active(self):
|
||||
"""Getter for self.command_active, so it can be used as Qt property."""
|
||||
return self._command_active
|
||||
|
||||
@pyqtProperty(bool)
|
||||
def insert_active(self):
|
||||
"""Getter for self.insert_active, so it can be used as Qt property."""
|
||||
return self._insert_active
|
||||
|
||||
def _set_insert_active(self, val):
|
||||
"""Setter for self.insert_active.
|
||||
@pyqtProperty(str)
|
||||
def caret_mode(self):
|
||||
"""Getter for self._caret_mode, so it can be used as Qt property."""
|
||||
return self._caret_mode.name
|
||||
|
||||
def set_mode_active(self, mode, val):
|
||||
"""Setter for self.{insert,command,caret}_active.
|
||||
|
||||
Re-set the stylesheet after setting the value, so everything gets
|
||||
updated by Qt properly.
|
||||
"""
|
||||
log.statusbar.debug("Setting insert_active to {}".format(val))
|
||||
self._insert_active = val
|
||||
if mode == usertypes.KeyMode.insert:
|
||||
log.statusbar.debug("Setting insert_active to {}".format(val))
|
||||
self._insert_active = val
|
||||
if mode == usertypes.KeyMode.command:
|
||||
log.statusbar.debug("Setting command_active to {}".format(val))
|
||||
self._command_active = val
|
||||
elif mode == usertypes.KeyMode.caret:
|
||||
webview = objreg.get('tabbed-browser', scope='window',
|
||||
window=self._win_id).currentWidget()
|
||||
log.statusbar.debug("Setting caret_mode - val {}, selection "
|
||||
"{}".format(val, webview.selection_enabled))
|
||||
if val:
|
||||
if webview.selection_enabled:
|
||||
self._set_mode_text("{} selection".format(mode.name))
|
||||
self._caret_mode = CaretMode.selection
|
||||
else:
|
||||
self._set_mode_text(mode.name)
|
||||
self._caret_mode = CaretMode.on
|
||||
else:
|
||||
self._caret_mode = CaretMode.off
|
||||
self.setStyleSheet(style.get_stylesheet(self.STYLESHEET))
|
||||
|
||||
def _set_mode_text(self, mode):
|
||||
@@ -434,25 +515,29 @@ class StatusBar(QWidget):
|
||||
@pyqtSlot(usertypes.KeyMode)
|
||||
def on_mode_entered(self, mode):
|
||||
"""Mark certain modes in the commandline."""
|
||||
mode_manager = objreg.get('mode-manager', scope='window',
|
||||
window=self._win_id)
|
||||
if mode in mode_manager.passthrough:
|
||||
keyparsers = objreg.get('keyparsers', scope='window',
|
||||
window=self._win_id)
|
||||
if keyparsers[mode].passthrough:
|
||||
self._set_mode_text(mode.name)
|
||||
if mode == usertypes.KeyMode.insert:
|
||||
self._set_insert_active(True)
|
||||
if mode in (usertypes.KeyMode.insert,
|
||||
usertypes.KeyMode.command,
|
||||
usertypes.KeyMode.caret):
|
||||
self.set_mode_active(mode, True)
|
||||
|
||||
@pyqtSlot(usertypes.KeyMode, usertypes.KeyMode)
|
||||
def on_mode_left(self, old_mode, new_mode):
|
||||
"""Clear marked mode."""
|
||||
mode_manager = objreg.get('mode-manager', scope='window',
|
||||
window=self._win_id)
|
||||
if old_mode in mode_manager.passthrough:
|
||||
if new_mode in mode_manager.passthrough:
|
||||
keyparsers = objreg.get('keyparsers', scope='window',
|
||||
window=self._win_id)
|
||||
if keyparsers[old_mode].passthrough:
|
||||
if keyparsers[new_mode].passthrough:
|
||||
self._set_mode_text(new_mode.name)
|
||||
else:
|
||||
self.txt.set_text(self.txt.Text.normal, '')
|
||||
if old_mode == usertypes.KeyMode.insert:
|
||||
self._set_insert_active(False)
|
||||
if old_mode in (usertypes.KeyMode.insert,
|
||||
usertypes.KeyMode.command,
|
||||
usertypes.KeyMode.caret):
|
||||
self.set_mode_active(old_mode, False)
|
||||
|
||||
@config.change_filter('ui', 'message-timeout')
|
||||
def set_pop_timer_interval(self):
|
||||
@@ -479,6 +564,7 @@ class StatusBar(QWidget):
|
||||
|
||||
def minimumSizeHint(self):
|
||||
"""Set the minimum height to the text height plus some padding."""
|
||||
padding = config.get('ui', 'statusbar-padding')
|
||||
width = super().minimumSizeHint().width()
|
||||
height = self.fontMetrics().height() + 3
|
||||
height = self.fontMetrics().height() + padding.top + padding.bottom
|
||||
return QSize(width, height)
|
||||
|
||||
@@ -163,8 +163,8 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
|
||||
"""Execute the command currently in the commandline."""
|
||||
prefixes = {
|
||||
':': '',
|
||||
'/': 'search ',
|
||||
'?': 'search -r ',
|
||||
'/': 'search -- ',
|
||||
'?': 'search -r -- ',
|
||||
}
|
||||
text = self.text()
|
||||
self.history.append(text)
|
||||
|
||||
@@ -34,11 +34,11 @@ class Percentage(textbase.TextBase):
|
||||
self.set_perc(0, 0)
|
||||
|
||||
@pyqtSlot(int, int)
|
||||
def set_perc(self, _, y):
|
||||
def set_perc(self, x, y): # pylint: disable=unused-argument
|
||||
"""Setter to be used as a Qt slot.
|
||||
|
||||
Args:
|
||||
_: The x percentage (int), currently ignored.
|
||||
x: The x percentage (int), currently ignored.
|
||||
y: The y percentage (int)
|
||||
"""
|
||||
if y == 0:
|
||||
@@ -48,7 +48,7 @@ class Percentage(textbase.TextBase):
|
||||
else:
|
||||
self.setText('[{:2}%]'.format(y))
|
||||
|
||||
@pyqtSlot(int)
|
||||
@pyqtSlot(object)
|
||||
def on_tab_changed(self, tab):
|
||||
"""Update scroll position when tab changed."""
|
||||
self.set_perc(*tab.scroll_pos)
|
||||
|
||||
@@ -66,10 +66,10 @@ class Progress(QProgressBar):
|
||||
@pyqtSlot(int)
|
||||
def on_tab_changed(self, tab):
|
||||
"""Set the correct value when the current tab changed."""
|
||||
if self is None:
|
||||
if self is None: # pragma: no branch
|
||||
# This should never happen, but for some weird reason it does
|
||||
# sometimes.
|
||||
return
|
||||
return # pragma: no cover
|
||||
self.setValue(tab.progress)
|
||||
if tab.load_status == webview.LoadStatus.loading:
|
||||
self.show()
|
||||
@@ -77,7 +77,10 @@ class Progress(QProgressBar):
|
||||
self.hide()
|
||||
|
||||
def sizeHint(self):
|
||||
"""Set the height to the text height plus some padding."""
|
||||
"""Set the height to the text height."""
|
||||
width = super().sizeHint().width()
|
||||
height = self.fontMetrics().height() + 3
|
||||
height = self.fontMetrics().height()
|
||||
return QSize(width, height)
|
||||
|
||||
def minimumSizeHint(self):
|
||||
return self.sizeHint()
|
||||
|
||||
@@ -33,6 +33,7 @@ from qutebrowser.utils import usertypes, log, qtutils, objreg, utils
|
||||
PromptContext = collections.namedtuple('PromptContext',
|
||||
['question', 'text', 'input_text',
|
||||
'echo_mode', 'input_visible'])
|
||||
AuthTuple = collections.namedtuple('AuthTuple', ['user', 'password'])
|
||||
|
||||
|
||||
class Prompter(QObject):
|
||||
@@ -229,7 +230,7 @@ class Prompter(QObject):
|
||||
prompt = objreg.get('prompt', scope='window', window=self._win_id)
|
||||
if (self._question.mode == usertypes.PromptMode.user_pwd and
|
||||
self._question.user is None):
|
||||
# User just entered an username
|
||||
# User just entered a username
|
||||
self._question.user = prompt.lineedit.text()
|
||||
prompt.txt.setText("Password:")
|
||||
prompt.lineedit.clear()
|
||||
@@ -237,7 +238,7 @@ class Prompter(QObject):
|
||||
elif self._question.mode == usertypes.PromptMode.user_pwd:
|
||||
# User just entered a password
|
||||
password = prompt.lineedit.text()
|
||||
self._question.answer = (self._question.user, password)
|
||||
self._question.answer = AuthTuple(self._question.user, password)
|
||||
modeman.maybe_leave(self._win_id, usertypes.KeyMode.prompt,
|
||||
'prompt accept')
|
||||
self._question.done()
|
||||
|
||||
@@ -55,9 +55,11 @@ class TextBase(QLabel):
|
||||
Args:
|
||||
width: The maximal width the text should take.
|
||||
"""
|
||||
if self.text is not None:
|
||||
if self.text():
|
||||
self._elided_text = self.fontMetrics().elidedText(
|
||||
self.text(), self._elidemode, width, Qt.TextShowMnemonic)
|
||||
else:
|
||||
self._elided_text = ''
|
||||
|
||||
def setText(self, txt):
|
||||
"""Extend QLabel::setText.
|
||||
@@ -70,7 +72,7 @@ class TextBase(QLabel):
|
||||
More info:
|
||||
|
||||
http://stackoverflow.com/q/21890462/2085149
|
||||
https://bugreports.qt-project.org/browse/QTBUG-36945
|
||||
https://bugreports.qt.io/browse/QTBUG-36945
|
||||
https://codereview.qt-project.org/#/c/79181/
|
||||
|
||||
Args:
|
||||
|
||||
@@ -23,13 +23,13 @@ import functools
|
||||
import collections
|
||||
|
||||
from PyQt5.QtWidgets import QSizePolicy
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QSize, QTimer, QUrl
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QTimer, QUrl
|
||||
from PyQt5.QtGui import QIcon
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.keyinput import modeman
|
||||
from qutebrowser.mainwindow import tabwidget
|
||||
from qutebrowser.browser import signalfilter, commands, webview
|
||||
from qutebrowser.browser import signalfilter, webview
|
||||
from qutebrowser.utils import log, usertypes, utils, qtutils, objreg, urlutils
|
||||
|
||||
|
||||
@@ -54,6 +54,8 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
emitted if the signal occurred in the current tab.
|
||||
|
||||
Attributes:
|
||||
search_text/search_flags: Search parameters which are shared between
|
||||
all tabs.
|
||||
_win_id: The window ID this tabbedbrowser is associated with.
|
||||
_filter: A SignalFilter instance.
|
||||
_now_focused: The tab which is focused now.
|
||||
@@ -107,16 +109,9 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
self._undo_stack = []
|
||||
self._filter = signalfilter.SignalFilter(win_id, self)
|
||||
dispatcher = commands.CommandDispatcher(win_id)
|
||||
objreg.register('command-dispatcher', dispatcher, scope='window',
|
||||
window=win_id)
|
||||
self.destroyed.connect(
|
||||
functools.partial(objreg.delete, 'command-dispatcher',
|
||||
scope='window', window=win_id))
|
||||
self._now_focused = None
|
||||
# FIXME adjust this to font size
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/119
|
||||
self.setIconSize(QSize(12, 12))
|
||||
self.search_text = None
|
||||
self.search_flags = 0
|
||||
objreg.get('config').changed.connect(self.update_favicons)
|
||||
objreg.get('config').changed.connect(self.update_window_title)
|
||||
objreg.get('config').changed.connect(self.update_tab_titles)
|
||||
@@ -240,17 +235,24 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
tab: The QWebView to be closed.
|
||||
"""
|
||||
last_close = config.get('tabs', 'last-close')
|
||||
if self.count() > 1:
|
||||
self._remove_tab(tab)
|
||||
elif last_close == 'close':
|
||||
self._remove_tab(tab)
|
||||
self.close_window.emit()
|
||||
elif last_close == 'blank':
|
||||
tab.openurl(QUrl('about:blank'))
|
||||
elif last_close == 'startpage':
|
||||
tab.openurl(QUrl(config.get('general', 'startpage')[0]))
|
||||
elif last_close == 'default-page':
|
||||
tab.openurl(config.get('general', 'default-page'))
|
||||
count = self.count()
|
||||
|
||||
if last_close == 'ignore' and count == 1:
|
||||
return
|
||||
|
||||
self._remove_tab(tab)
|
||||
|
||||
if count == 1: # We just closed the last tab above.
|
||||
if last_close == 'close':
|
||||
self.close_window.emit()
|
||||
elif last_close == 'blank':
|
||||
self.openurl(QUrl('about:blank'), newtab=True)
|
||||
elif last_close == 'startpage':
|
||||
url = QUrl(config.get('general', 'startpage')[0])
|
||||
self.openurl(url, newtab=True)
|
||||
elif last_close == 'default-page':
|
||||
url = config.get('general', 'default-page')
|
||||
self.openurl(url, newtab=True)
|
||||
|
||||
def _remove_tab(self, tab):
|
||||
"""Remove a tab from the tab list and delete it properly.
|
||||
@@ -302,7 +304,7 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
newtab: True to open URL in a new tab, False otherwise.
|
||||
"""
|
||||
qtutils.ensure_valid(url)
|
||||
if newtab:
|
||||
if newtab or self.currentWidget() is None:
|
||||
self.tabopen(url, background=False)
|
||||
else:
|
||||
self.currentWidget().openurl(url)
|
||||
@@ -338,7 +340,7 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
the default settings we handle it like Chromium does:
|
||||
- Tabs from clicked links etc. are to the right of
|
||||
the current.
|
||||
- Explicitely opened tabs are at the very right.
|
||||
- Explicitly opened tabs are at the very right.
|
||||
|
||||
Return:
|
||||
The opened WebView instance.
|
||||
@@ -518,7 +520,8 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
tab = self.widget(idx)
|
||||
log.modes.debug("Current tab changed, focusing {!r}".format(tab))
|
||||
tab.setFocus()
|
||||
for mode in (usertypes.KeyMode.hint, usertypes.KeyMode.insert):
|
||||
for mode in (usertypes.KeyMode.hint, usertypes.KeyMode.insert,
|
||||
usertypes.KeyMode.caret):
|
||||
modeman.maybe_leave(self._win_id, mode, 'tab changed')
|
||||
if self._now_focused is not None:
|
||||
objreg.register('last-focused-tab', self._now_focused, update=True,
|
||||
@@ -582,3 +585,14 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
"""
|
||||
super().resizeEvent(e)
|
||||
self.resized.emit(self.geometry())
|
||||
|
||||
def wheelEvent(self, e):
|
||||
"""Override wheelEvent of QWidget to forward it to the focused tab.
|
||||
|
||||
Args:
|
||||
e: The QWheelEvent
|
||||
"""
|
||||
if self._now_focused is not None:
|
||||
self._now_focused.wheelEvent(e)
|
||||
else:
|
||||
e.ignore()
|
||||
|
||||
@@ -17,26 +17,23 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""The tab widget used for TabbedBrowser from browser.py.
|
||||
|
||||
Module attributes:
|
||||
PM_TabBarPadding: The PixelMetric value for TabBarStyle to get the padding
|
||||
between items.
|
||||
"""
|
||||
"""The tab widget used for TabbedBrowser from browser.py."""
|
||||
|
||||
import collections
|
||||
import functools
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QSize, QRect, QPoint, QTimer
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QSize, QRect, QTimer
|
||||
from PyQt5.QtWidgets import (QTabWidget, QTabBar, QSizePolicy, QCommonStyle,
|
||||
QStyle, QStylePainter, QStyleOptionTab)
|
||||
from PyQt5.QtGui import QIcon, QPalette, QColor
|
||||
|
||||
from qutebrowser.utils import qtutils, objreg, utils
|
||||
from qutebrowser.utils import qtutils, objreg, utils, usertypes
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.browser import webview
|
||||
|
||||
|
||||
PM_TabBarPadding = QStyle.PM_CustomBase
|
||||
PixelMetrics = usertypes.enum('PixelMetrics', ['icon_padding'],
|
||||
start=QStyle.PM_CustomBase, is_int=True)
|
||||
|
||||
|
||||
class TabWidget(QTabWidget):
|
||||
@@ -196,6 +193,7 @@ class TabWidget(QTabWidget):
|
||||
@pyqtSlot(int)
|
||||
def emit_tab_index_changed(self, index):
|
||||
"""Emit the tab_index_changed signal if the current tab changed."""
|
||||
self.tabBar().on_change()
|
||||
self.tab_index_changed.emit(index, self.count())
|
||||
|
||||
|
||||
@@ -222,32 +220,47 @@ class TabBar(QTabBar):
|
||||
config_obj = objreg.get('config')
|
||||
config_obj.changed.connect(self.set_font)
|
||||
self.vertical = False
|
||||
self._auto_hide_timer = QTimer()
|
||||
self._auto_hide_timer.setSingleShot(True)
|
||||
self._auto_hide_timer.setInterval(
|
||||
config.get('tabs', 'show-switching-delay'))
|
||||
self._auto_hide_timer.timeout.connect(self._tabhide)
|
||||
self.setAutoFillBackground(True)
|
||||
self.set_colors()
|
||||
config_obj.changed.connect(self.set_colors)
|
||||
QTimer.singleShot(0, self._tabhide)
|
||||
config_obj.changed.connect(self.autohide)
|
||||
config_obj.changed.connect(self.alwayshide)
|
||||
config_obj.changed.connect(self.on_tab_colors_changed)
|
||||
config_obj.changed.connect(self.on_show_switching_delay_changed)
|
||||
config_obj.changed.connect(self.tabs_show)
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self, count=self.count())
|
||||
|
||||
@config.change_filter('tabs', 'hide-auto')
|
||||
def autohide(self):
|
||||
"""Hide tab bar if needed when tabs->hide-auto got changed."""
|
||||
@config.change_filter('tabs', 'show')
|
||||
def tabs_show(self):
|
||||
"""Hide or show tab bar if needed when tabs->show got changed."""
|
||||
self._tabhide()
|
||||
|
||||
@config.change_filter('tabs', 'hide-always')
|
||||
def alwayshide(self):
|
||||
"""Hide tab bar if needed when tabs->hide-always got changed."""
|
||||
self._tabhide()
|
||||
@config.change_filter('tabs', 'show-switching-delay')
|
||||
def on_show_switching_delay_changed(self):
|
||||
"""Set timer interval when tabs->show-switching-delay got changed."""
|
||||
self._auto_hide_timer.setInterval(
|
||||
config.get('tabs', 'show-switching-delay'))
|
||||
|
||||
def on_change(self):
|
||||
"""Show tab bar when current tab got changed."""
|
||||
show = config.get('tabs', 'show')
|
||||
if show == 'switching':
|
||||
self.show()
|
||||
self._auto_hide_timer.start()
|
||||
|
||||
def _tabhide(self):
|
||||
"""Hide the tab bar if needed."""
|
||||
hide_auto = config.get('tabs', 'hide-auto')
|
||||
hide_always = config.get('tabs', 'hide-always')
|
||||
if hide_always or (hide_auto and self.count() == 1):
|
||||
show = config.get('tabs', 'show')
|
||||
show_never = show == 'never'
|
||||
switching = show == 'switching'
|
||||
multiple = show == 'multiple'
|
||||
if show_never or (multiple and self.count() == 1) or switching:
|
||||
self.hide()
|
||||
else:
|
||||
self.show()
|
||||
@@ -295,6 +308,8 @@ class TabBar(QTabBar):
|
||||
def set_font(self):
|
||||
"""Set the tab bar font."""
|
||||
self.setFont(config.get('fonts', 'tabbar'))
|
||||
size = self.fontMetrics().height() - 2
|
||||
self.setIconSize(QSize(size, size))
|
||||
|
||||
@config.change_filter('colors', 'tabs.bg.bar')
|
||||
def set_colors(self):
|
||||
@@ -331,22 +346,21 @@ class TabBar(QTabBar):
|
||||
A QSize.
|
||||
"""
|
||||
icon = self.tabIcon(index)
|
||||
padding_count = 2
|
||||
padding = config.get('tabs', 'padding')
|
||||
padding_h = padding.left + padding.right
|
||||
padding_v = padding.top + padding.bottom
|
||||
if icon.isNull():
|
||||
icon_size = QSize(0, 0)
|
||||
else:
|
||||
extent = self.style().pixelMetric(QStyle.PM_TabBarIconSize, None,
|
||||
self)
|
||||
icon_size = icon.actualSize(QSize(extent, extent))
|
||||
padding_count += 1
|
||||
padding_h += self.style().pixelMetric(
|
||||
PixelMetrics.icon_padding, None, self)
|
||||
indicator_width = config.get('tabs', 'indicator-width')
|
||||
if indicator_width != 0:
|
||||
indicator_width += config.get('tabs', 'indicator-space')
|
||||
padding_width = self.style().pixelMetric(PM_TabBarPadding, None, self)
|
||||
height = self.fontMetrics().height()
|
||||
width = (self.fontMetrics().width('\u2026') +
|
||||
icon_size.width() + padding_count * padding_width +
|
||||
indicator_width)
|
||||
height = self.fontMetrics().height() + padding_v
|
||||
width = (self.fontMetrics().width('\u2026') + icon_size.width() +
|
||||
padding_h + indicator_width)
|
||||
return QSize(width, height)
|
||||
|
||||
def tabSizeHint(self, index):
|
||||
@@ -361,7 +375,7 @@ class TabBar(QTabBar):
|
||||
A QSize.
|
||||
"""
|
||||
minimum_size = self.minimumTabSizeHint(index)
|
||||
height = self.fontMetrics().height()
|
||||
height = minimum_size.height()
|
||||
if self.vertical:
|
||||
confwidth = str(config.get('tabs', 'width'))
|
||||
if confwidth.endswith('%'):
|
||||
@@ -391,9 +405,9 @@ class TabBar(QTabBar):
|
||||
def paintEvent(self, _e):
|
||||
"""Override paintEvent to draw the tabs like we want to."""
|
||||
p = QStylePainter(self)
|
||||
tab = QStyleOptionTab()
|
||||
selected = self.currentIndex()
|
||||
for idx in range(self.count()):
|
||||
tab = QStyleOptionTab()
|
||||
self.initStyleOption(tab, idx)
|
||||
if idx == selected:
|
||||
bg_color = config.get('colors', 'tabs.bg.selected')
|
||||
@@ -480,6 +494,23 @@ class TabBar(QTabBar):
|
||||
new_idx = super().insertTab(idx, icon, '')
|
||||
self.set_page_title(new_idx, text)
|
||||
|
||||
def wheelEvent(self, e):
|
||||
"""Override wheelEvent to make the action configurable.
|
||||
|
||||
Args:
|
||||
e: The QWheelEvent
|
||||
"""
|
||||
if config.get('tabs', 'mousewheel-tab-switching'):
|
||||
super().wheelEvent(e)
|
||||
else:
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=self._win_id)
|
||||
tabbed_browser.wheelEvent(e)
|
||||
|
||||
|
||||
# Used by TabBarStyle._tab_layout().
|
||||
Layouts = collections.namedtuple('Layouts', ['text', 'icon', 'indicator'])
|
||||
|
||||
|
||||
class TabBarStyle(QCommonStyle):
|
||||
|
||||
@@ -520,6 +551,36 @@ class TabBarStyle(QCommonStyle):
|
||||
setattr(self, method, functools.partial(target))
|
||||
super().__init__()
|
||||
|
||||
def _draw_indicator(self, layouts, opt, p):
|
||||
"""Draw the tab indicator.
|
||||
|
||||
Args:
|
||||
layouts: The layouts from _tab_layout.
|
||||
opt: QStyleOption from drawControl.
|
||||
p: QPainter from drawControl.
|
||||
"""
|
||||
color = opt.palette.base().color()
|
||||
rect = layouts.indicator
|
||||
indicator_width = config.get('tabs', 'indicator-width')
|
||||
if color.isValid() and indicator_width != 0:
|
||||
p.fillRect(rect, color)
|
||||
|
||||
def _draw_icon(self, layouts, opt, p):
|
||||
"""Draw the tab icon.
|
||||
|
||||
Args:
|
||||
layouts: The layouts from _tab_layout.
|
||||
opt: QStyleOption
|
||||
p: QPainter
|
||||
"""
|
||||
qtutils.ensure_valid(layouts.icon)
|
||||
icon_mode = (QIcon.Normal if opt.state & QStyle.State_Enabled
|
||||
else QIcon.Disabled)
|
||||
icon_state = (QIcon.On if opt.state & QStyle.State_Selected
|
||||
else QIcon.Off)
|
||||
icon = opt.icon.pixmap(opt.iconSize, icon_mode, icon_state)
|
||||
p.drawPixmap(layouts.icon.x(), layouts.icon.y(), icon)
|
||||
|
||||
def drawControl(self, element, opt, p, widget=None):
|
||||
"""Override drawControl to draw odd tabs in a different color.
|
||||
|
||||
@@ -528,38 +589,26 @@ class TabBarStyle(QCommonStyle):
|
||||
|
||||
Args:
|
||||
element: ControlElement
|
||||
option: const QStyleOption *
|
||||
painter: QPainter *
|
||||
widget: const QWidget *
|
||||
opt: QStyleOption
|
||||
p: QPainter
|
||||
widget: QWidget
|
||||
"""
|
||||
layouts = self._tab_layout(opt)
|
||||
if element == QStyle.CE_TabBarTab:
|
||||
# We override this so we can control TabBarTabShape/TabBarTabLabel.
|
||||
self.drawControl(QStyle.CE_TabBarTabShape, opt, p, widget)
|
||||
self.drawControl(QStyle.CE_TabBarTabLabel, opt, p, widget)
|
||||
elif element == QStyle.CE_TabBarTabShape:
|
||||
p.fillRect(opt.rect, opt.palette.window())
|
||||
indicator_color = opt.palette.base().color()
|
||||
indicator_width = config.get('tabs', 'indicator-width')
|
||||
if indicator_color.isValid() and indicator_width != 0:
|
||||
topleft = opt.rect.topLeft()
|
||||
topleft += QPoint(config.get('tabs', 'indicator-space'), 2)
|
||||
p.fillRect(topleft.x(), topleft.y(), indicator_width,
|
||||
opt.rect.height() - 4, indicator_color)
|
||||
self._draw_indicator(layouts, opt, p)
|
||||
# We use super() rather than self._style here because we don't want
|
||||
# any sophisticated drawing.
|
||||
super().drawControl(QStyle.CE_TabBarTabShape, opt, p, widget)
|
||||
elif element == QStyle.CE_TabBarTabLabel:
|
||||
text_rect, icon_rect = self._tab_layout(opt)
|
||||
if not opt.icon.isNull():
|
||||
qtutils.ensure_valid(icon_rect)
|
||||
icon_mode = (QIcon.Normal if opt.state & QStyle.State_Enabled
|
||||
else QIcon.Disabled)
|
||||
icon_state = (QIcon.On if opt.state & QStyle.State_Selected
|
||||
else QIcon.Off)
|
||||
icon = opt.icon.pixmap(opt.iconSize, icon_mode, icon_state)
|
||||
p.drawPixmap(icon_rect.x(), icon_rect.y(), icon)
|
||||
self._draw_icon(layouts, opt, p)
|
||||
alignment = Qt.AlignLeft | Qt.AlignVCenter | Qt.TextHideMnemonic
|
||||
self._style.drawItemText(p, text_rect, alignment, opt.palette,
|
||||
self._style.drawItemText(p, layouts.text, alignment, opt.palette,
|
||||
opt.state & QStyle.State_Enabled,
|
||||
opt.text, QPalette.WindowText)
|
||||
else:
|
||||
@@ -578,12 +627,13 @@ class TabBarStyle(QCommonStyle):
|
||||
Return:
|
||||
An int.
|
||||
"""
|
||||
if (metric == QStyle.PM_TabBarTabShiftHorizontal or
|
||||
metric == QStyle.PM_TabBarTabShiftVertical or
|
||||
metric == QStyle.PM_TabBarTabHSpace or
|
||||
metric == QStyle.PM_TabBarTabVSpace):
|
||||
if metric in [QStyle.PM_TabBarTabShiftHorizontal,
|
||||
QStyle.PM_TabBarTabShiftVertical,
|
||||
QStyle.PM_TabBarTabHSpace,
|
||||
QStyle.PM_TabBarTabVSpace,
|
||||
QStyle.PM_TabBarScrollButtonWidth]:
|
||||
return 0
|
||||
elif metric == PM_TabBarPadding:
|
||||
elif metric == PixelMetrics.icon_padding:
|
||||
return 4
|
||||
else:
|
||||
return self._style.pixelMetric(metric, option, widget)
|
||||
@@ -600,8 +650,8 @@ class TabBarStyle(QCommonStyle):
|
||||
A QRect.
|
||||
"""
|
||||
if sr == QStyle.SE_TabBarTabText:
|
||||
text_rect, _icon_rect = self._tab_layout(opt)
|
||||
return text_rect
|
||||
layouts = self._tab_layout(opt)
|
||||
return layouts.text
|
||||
else:
|
||||
return self._style.subElementRect(sr, opt, widget)
|
||||
|
||||
@@ -616,22 +666,42 @@ class TabBarStyle(QCommonStyle):
|
||||
opt: QStyleOptionTab
|
||||
|
||||
Return:
|
||||
A (text_rect, icon_rect) tuple (both QRects).
|
||||
A Layout namedtuple with two QRects.
|
||||
"""
|
||||
padding = self.pixelMetric(PM_TabBarPadding, opt)
|
||||
icon_rect = QRect()
|
||||
padding = config.get('tabs', 'padding')
|
||||
indicator_padding = config.get('tabs', 'indicator-padding')
|
||||
|
||||
text_rect = QRect(opt.rect)
|
||||
indicator_rect = QRect(opt.rect)
|
||||
|
||||
qtutils.ensure_valid(text_rect)
|
||||
text_rect.adjust(padding.left, padding.top, -padding.right,
|
||||
-padding.bottom)
|
||||
|
||||
indicator_width = config.get('tabs', 'indicator-width')
|
||||
text_rect.adjust(padding, 0, 0, 0)
|
||||
if indicator_width != 0:
|
||||
text_rect.adjust(indicator_width +
|
||||
config.get('tabs', 'indicator-space'), 0, 0, 0)
|
||||
if not opt.icon.isNull():
|
||||
if indicator_width == 0:
|
||||
indicator_rect = 0
|
||||
else:
|
||||
qtutils.ensure_valid(indicator_rect)
|
||||
indicator_rect.adjust(padding.left + indicator_padding.left,
|
||||
padding.top + indicator_padding.top,
|
||||
0,
|
||||
-(padding.bottom + indicator_padding.bottom))
|
||||
indicator_rect.setWidth(indicator_width)
|
||||
|
||||
text_rect.adjust(indicator_width + indicator_padding.left +
|
||||
indicator_padding.right, 0, 0, 0)
|
||||
|
||||
if opt.icon.isNull():
|
||||
icon_rect = QRect()
|
||||
else:
|
||||
icon_padding = self.pixelMetric(PixelMetrics.icon_padding, opt)
|
||||
icon_rect = self._get_icon_rect(opt, text_rect)
|
||||
text_rect.adjust(icon_rect.width() + padding, 0, 0, 0)
|
||||
text_rect.adjust(icon_rect.width() + icon_padding, 0, 0, 0)
|
||||
|
||||
text_rect = self._style.visualRect(opt.direction, opt.rect, text_rect)
|
||||
return (text_rect, icon_rect)
|
||||
return Layouts(text=text_rect, icon=icon_rect,
|
||||
indicator=indicator_rect)
|
||||
|
||||
def _get_icon_rect(self, opt, text_rect):
|
||||
"""Get a QRect for the icon to draw.
|
||||
@@ -654,8 +724,7 @@ class TabBarStyle(QCommonStyle):
|
||||
tab_icon_size = opt.icon.actualSize(icon_size, icon_mode, icon_state)
|
||||
tab_icon_size = QSize(min(tab_icon_size.width(), icon_size.width()),
|
||||
min(tab_icon_size.height(), icon_size.height()))
|
||||
icon_rect = QRect(text_rect.left(),
|
||||
text_rect.center().y() - tab_icon_size.height() / 2,
|
||||
icon_rect = QRect(text_rect.left(), text_rect.top() + 1,
|
||||
tab_icon_size.width(), tab_icon_size.height())
|
||||
icon_rect = self._style.visualRect(opt.direction, opt.rect, icon_rect)
|
||||
qtutils.ensure_valid(icon_rect)
|
||||
|
||||
@@ -49,9 +49,12 @@ class PyPIVersionClient(QObject):
|
||||
success = pyqtSignal(str)
|
||||
error = pyqtSignal(str)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
def __init__(self, parent=None, client=None):
|
||||
super().__init__(parent)
|
||||
self._client = httpclient.HTTPClient(self)
|
||||
if client is None:
|
||||
self._client = httpclient.HTTPClient(self)
|
||||
else:
|
||||
self._client = client
|
||||
self._client.error.connect(self.error)
|
||||
self._client.success.connect(self.on_client_success)
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ import sys
|
||||
try:
|
||||
# Python3
|
||||
from tkinter import Tk, messagebox
|
||||
except ImportError:
|
||||
except ImportError: # pragma: no coverage
|
||||
try:
|
||||
# Python2
|
||||
# pylint: disable=import-error
|
||||
@@ -34,6 +34,7 @@ except ImportError:
|
||||
except ImportError:
|
||||
# Some Python without Tk
|
||||
Tk = None
|
||||
messagebox = None
|
||||
|
||||
|
||||
# First we check the version of Python. This code should run fine with python2
|
||||
@@ -47,7 +48,7 @@ def check_python_version():
|
||||
version_str = '.'.join(map(str, sys.version_info[:3]))
|
||||
text = ("At least Python 3.4 is required to run qutebrowser, but " +
|
||||
version_str + " is installed!\n")
|
||||
if Tk:
|
||||
if Tk and '--no-err-windows' not in sys.argv: # pragma: no coverage
|
||||
root = Tk()
|
||||
root.withdraw()
|
||||
messagebox.showerror("qutebrowser: Fatal error!", text)
|
||||
@@ -55,3 +56,7 @@ def check_python_version():
|
||||
sys.stderr.write(text)
|
||||
sys.stderr.flush()
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
check_python_version()
|
||||
|
||||
@@ -24,13 +24,12 @@ import sys
|
||||
import html
|
||||
import getpass
|
||||
import traceback
|
||||
import distutils.version # pylint: disable=no-name-in-module,import-error
|
||||
# https://bitbucket.org/logilab/pylint/issue/73/
|
||||
|
||||
import pkg_resources
|
||||
from PyQt5.QtCore import pyqtSlot, Qt, QSize, qVersion
|
||||
from PyQt5.QtWidgets import (QDialog, QLabel, QTextEdit, QPushButton,
|
||||
QVBoxLayout, QHBoxLayout, QCheckBox,
|
||||
QDialogButtonBox, QMessageBox)
|
||||
QDialogButtonBox, QMessageBox, QApplication)
|
||||
|
||||
import qutebrowser
|
||||
from qutebrowser.utils import version, log, utils, objreg, qtutils
|
||||
@@ -183,7 +182,7 @@ class _CrashDialog(QDialog):
|
||||
def _init_text(self):
|
||||
"""Initialize the main text to be displayed on an exception.
|
||||
|
||||
Should be extended by superclass to set the actual text."""
|
||||
Should be extended by subclasses to set the actual text."""
|
||||
self._lbl = QLabel(wordWrap=True, openExternalLinks=True,
|
||||
textInteractionFlags=Qt.LinksAccessibleByMouse)
|
||||
self._vbox.addWidget(self._lbl)
|
||||
@@ -220,6 +219,12 @@ class _CrashDialog(QDialog):
|
||||
cmdhist: A list with the command history (as strings)
|
||||
exc: An exception tuple (type, value, traceback)
|
||||
"""
|
||||
try:
|
||||
application = QApplication.instance()
|
||||
launch_time = application.launch_time.ctime()
|
||||
self._crash_info.append(('Launch time', launch_time))
|
||||
except Exception:
|
||||
self._crash_info.append(("Launch time", traceback.format_exc()))
|
||||
try:
|
||||
self._crash_info.append(("Version info", version.version()))
|
||||
except Exception:
|
||||
@@ -328,8 +333,8 @@ class _CrashDialog(QDialog):
|
||||
"""
|
||||
# pylint: disable=no-member
|
||||
# https://bitbucket.org/logilab/pylint/issue/73/
|
||||
new_version = distutils.version.StrictVersion(newest)
|
||||
cur_version = distutils.version.StrictVersion(qutebrowser.__version__)
|
||||
new_version = pkg_resources.parse_version(newest)
|
||||
cur_version = pkg_resources.parse_version(qutebrowser.__version__)
|
||||
lines = ['The report has been sent successfully. Thanks!']
|
||||
if new_version > cur_version:
|
||||
lines.append("<b>Note:</b> The newest available version is v{}, "
|
||||
@@ -584,3 +589,38 @@ class ReportErrorDialog(QDialog):
|
||||
btn.clicked.connect(self.close)
|
||||
hbox.addWidget(btn)
|
||||
vbox.addLayout(hbox)
|
||||
|
||||
|
||||
def dump_exception_info(exc, pages, cmdhist, objects):
|
||||
"""Dump exception info to stderr.
|
||||
|
||||
Args:
|
||||
exc: An exception tuple (type, value, traceback)
|
||||
pages: A list of lists of the open pages (URLs as strings)
|
||||
cmdhist: A list with the command history (as strings)
|
||||
objects: A list of all QObjects as string.
|
||||
"""
|
||||
print(file=sys.stderr)
|
||||
print("\n\n===== Handling exception with --no-err-windows... =====\n\n",
|
||||
file=sys.stderr)
|
||||
print("\n---- Exceptions ----", file=sys.stderr)
|
||||
print(''.join(traceback.format_exception(*exc)), file=sys.stderr)
|
||||
print("\n---- Version info ----", file=sys.stderr)
|
||||
try:
|
||||
print(version.version(), file=sys.stderr)
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
print("\n---- Config ----", file=sys.stderr)
|
||||
try:
|
||||
conf = objreg.get('config')
|
||||
print(conf.dump_userconfig(), file=sys.stderr)
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
print("\n---- Commandline args ----", file=sys.stderr)
|
||||
print(' '.join(sys.argv[1:]), file=sys.stderr)
|
||||
print("\n---- Open pages ----", file=sys.stderr)
|
||||
print('\n\n'.join('\n'.join(e) for e in pages), file=sys.stderr)
|
||||
print("\n---- Command history ----", file=sys.stderr)
|
||||
print('\n'.join(cmdhist), file=sys.stderr)
|
||||
print("\n---- Objects ----", file=sys.stderr)
|
||||
print(objects, file=sys.stderr)
|
||||
|
||||
394
qutebrowser/misc/crashsignal.py
Normal file
394
qutebrowser/misc/crashsignal.py
Normal file
@@ -0,0 +1,394 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014-2015 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/>.
|
||||
|
||||
"""Handlers for crashes and OS signals."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import bdb
|
||||
import pdb
|
||||
import signal
|
||||
import functools
|
||||
import faulthandler
|
||||
import os.path
|
||||
import collections
|
||||
|
||||
from PyQt5.QtCore import (pyqtSlot, qInstallMessageHandler, QObject,
|
||||
QSocketNotifier, QTimer, QUrl)
|
||||
from PyQt5.QtWidgets import QApplication, QDialog
|
||||
|
||||
from qutebrowser.commands import cmdutils
|
||||
from qutebrowser.misc import earlyinit, crashdialog
|
||||
from qutebrowser.utils import usertypes, standarddir, log, objreg, debug
|
||||
|
||||
|
||||
ExceptionInfo = collections.namedtuple('ExceptionInfo',
|
||||
'pages, cmd_history, objects')
|
||||
|
||||
|
||||
# Used by mainwindow.py to skip confirm questions on crashes
|
||||
is_crashing = False
|
||||
|
||||
|
||||
class CrashHandler(QObject):
|
||||
|
||||
"""Handler for crashes, reports and exceptions.
|
||||
|
||||
Attributes:
|
||||
_app: The QApplication instance.
|
||||
_quitter: The Quitter instance.
|
||||
_args: The argparse namespace.
|
||||
_crash_dialog: The CrashDialog currently being shown.
|
||||
_crash_log_file: The file handle for the faulthandler crash log.
|
||||
"""
|
||||
|
||||
def __init__(self, *, app, quitter, args, parent=None):
|
||||
super().__init__(parent)
|
||||
self._app = app
|
||||
self._quitter = quitter
|
||||
self._args = args
|
||||
self._crash_log_file = None
|
||||
self._crash_dialog = None
|
||||
|
||||
def activate(self):
|
||||
"""Activate the exception hook."""
|
||||
sys.excepthook = self.exception_hook
|
||||
|
||||
def handle_segfault(self):
|
||||
"""Handle a segfault from a previous run."""
|
||||
data_dir = None
|
||||
if data_dir is None:
|
||||
return
|
||||
logname = os.path.join(data_dir, 'crash.log')
|
||||
try:
|
||||
# First check if an old logfile exists.
|
||||
if os.path.exists(logname):
|
||||
with open(logname, 'r', encoding='ascii') as f:
|
||||
data = f.read()
|
||||
os.remove(logname)
|
||||
self._init_crashlogfile()
|
||||
if data:
|
||||
# Crashlog exists and has data in it, so something crashed
|
||||
# previously.
|
||||
self._crash_dialog = crashdialog.get_fatal_crash_dialog(
|
||||
self._args.debug, data)
|
||||
self._crash_dialog.show()
|
||||
else:
|
||||
# There's no log file, so we can use this to display crashes to
|
||||
# the user on the next start.
|
||||
self._init_crashlogfile()
|
||||
except OSError:
|
||||
log.init.exception("Error while handling crash log file!")
|
||||
self._init_crashlogfile()
|
||||
|
||||
def _recover_pages(self, forgiving=False):
|
||||
"""Try to recover all open pages.
|
||||
|
||||
Called from exception_hook, so as forgiving as possible.
|
||||
|
||||
Args:
|
||||
forgiving: Whether to ignore exceptions.
|
||||
|
||||
Return:
|
||||
A list containing a list for each window, which in turn contain the
|
||||
opened URLs.
|
||||
"""
|
||||
pages = []
|
||||
for win_id in objreg.window_registry:
|
||||
win_pages = []
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=win_id)
|
||||
for tab in tabbed_browser.widgets():
|
||||
try:
|
||||
urlstr = tab.cur_url.toString(
|
||||
QUrl.RemovePassword | QUrl.FullyEncoded)
|
||||
if urlstr:
|
||||
win_pages.append(urlstr)
|
||||
except Exception:
|
||||
if forgiving:
|
||||
log.destroy.exception("Error while recovering tab")
|
||||
else:
|
||||
raise
|
||||
pages.append(win_pages)
|
||||
return pages
|
||||
|
||||
def _init_crashlogfile(self):
|
||||
"""Start a new logfile and redirect faulthandler to it."""
|
||||
assert not self._args.no_err_windows
|
||||
data_dir = standarddir.data()
|
||||
if data_dir is None:
|
||||
return
|
||||
logname = os.path.join(data_dir, 'crash.log')
|
||||
try:
|
||||
self._crash_log_file = open(logname, 'w', encoding='ascii')
|
||||
except OSError:
|
||||
log.init.exception("Error while opening crash log file!")
|
||||
else:
|
||||
earlyinit.init_faulthandler(self._crash_log_file)
|
||||
|
||||
@cmdutils.register(instance='crash-handler')
|
||||
def report(self):
|
||||
"""Report a bug in qutebrowser."""
|
||||
pages = self._recover_pages()
|
||||
cmd_history = objreg.get('command-history')[-5:]
|
||||
objects = debug.get_all_objects()
|
||||
self._crash_dialog = crashdialog.ReportDialog(pages, cmd_history,
|
||||
objects)
|
||||
self._crash_dialog.show()
|
||||
|
||||
def destroy_crashlogfile(self):
|
||||
"""Clean up the crash log file and delete it."""
|
||||
if self._crash_log_file is None:
|
||||
return
|
||||
# We use sys.__stderr__ instead of sys.stderr here so this will still
|
||||
# work when sys.stderr got replaced, e.g. by "Python Tools for Visual
|
||||
# Studio".
|
||||
if sys.__stderr__ is not None:
|
||||
faulthandler.enable(sys.__stderr__)
|
||||
else:
|
||||
faulthandler.disable()
|
||||
try:
|
||||
self._crash_log_file.close()
|
||||
os.remove(self._crash_log_file.name)
|
||||
except OSError:
|
||||
log.destroy.exception("Could not remove crash log!")
|
||||
|
||||
def _get_exception_info(self):
|
||||
"""Get info needed for the exception hook/dialog.
|
||||
|
||||
Return:
|
||||
An ExceptionInfo namedtuple.
|
||||
"""
|
||||
try:
|
||||
pages = self._recover_pages(forgiving=True)
|
||||
except Exception:
|
||||
log.destroy.exception("Error while recovering pages")
|
||||
pages = []
|
||||
|
||||
try:
|
||||
cmd_history = objreg.get('command-history')[-5:]
|
||||
except Exception:
|
||||
log.destroy.exception("Error while getting history: {}")
|
||||
cmd_history = []
|
||||
|
||||
try:
|
||||
objects = debug.get_all_objects()
|
||||
except Exception:
|
||||
log.destroy.exception("Error while getting objects")
|
||||
objects = ""
|
||||
return ExceptionInfo(pages, cmd_history, objects)
|
||||
|
||||
def exception_hook(self, exctype, excvalue, tb):
|
||||
"""Handle uncaught python exceptions.
|
||||
|
||||
It'll try very hard to write all open tabs to a file, and then exit
|
||||
gracefully.
|
||||
"""
|
||||
exc = (exctype, excvalue, tb)
|
||||
qapp = QApplication.instance()
|
||||
|
||||
if not self._quitter.quit_status['crash']:
|
||||
log.misc.error("ARGH, there was an exception while the crash "
|
||||
"dialog is already shown:", exc_info=exc)
|
||||
return
|
||||
|
||||
log.misc.error("Uncaught exception", exc_info=exc)
|
||||
|
||||
is_ignored_exception = (exctype is bdb.BdbQuit or
|
||||
not issubclass(exctype, Exception))
|
||||
|
||||
if self._args.pdb_postmortem:
|
||||
pdb.post_mortem(tb)
|
||||
|
||||
if is_ignored_exception or self._args.pdb_postmortem:
|
||||
# pdb exit, KeyboardInterrupt, ...
|
||||
status = 0 if is_ignored_exception else 2
|
||||
try:
|
||||
self._quitter.shutdown(status)
|
||||
return
|
||||
except Exception:
|
||||
log.init.exception("Error while shutting down")
|
||||
qapp.quit()
|
||||
return
|
||||
|
||||
self._quitter.quit_status['crash'] = False
|
||||
info = self._get_exception_info()
|
||||
|
||||
try:
|
||||
objreg.get('ipc-server').ignored = True
|
||||
except Exception:
|
||||
log.destroy.exception("Error while ignoring ipc")
|
||||
|
||||
try:
|
||||
self._app.lastWindowClosed.disconnect(
|
||||
self._quitter.on_last_window_closed)
|
||||
except TypeError:
|
||||
log.destroy.exception("Error while preventing shutdown")
|
||||
|
||||
global is_crashing
|
||||
is_crashing = True
|
||||
|
||||
self._app.closeAllWindows()
|
||||
if self._args.no_err_windows:
|
||||
crashdialog.dump_exception_info(exc, info.pages, info.cmd_history,
|
||||
info.objects)
|
||||
else:
|
||||
self._crash_dialog = crashdialog.ExceptionCrashDialog(
|
||||
self._args.debug, info.pages, info.cmd_history, exc,
|
||||
info.objects)
|
||||
ret = self._crash_dialog.exec_()
|
||||
if ret == QDialog.Accepted: # restore
|
||||
self._quitter.restart(info.pages)
|
||||
|
||||
# We might risk a segfault here, but that's better than continuing to
|
||||
# run in some undefined state, so we only do the most needed shutdown
|
||||
# here.
|
||||
qInstallMessageHandler(None)
|
||||
self.destroy_crashlogfile()
|
||||
sys.exit(usertypes.Exit.exception)
|
||||
|
||||
def raise_crashdlg(self):
|
||||
"""Raise the crash dialog if one exists."""
|
||||
if self._crash_dialog is not None:
|
||||
self._crash_dialog.raise_()
|
||||
|
||||
|
||||
class SignalHandler(QObject):
|
||||
|
||||
"""Handler responsible for handling OS signals (SIGINT, SIGTERM, etc.).
|
||||
|
||||
Attributes:
|
||||
_app: The QApplication instance.
|
||||
_quitter: The Quitter instance.
|
||||
_activated: Whether activate() was called.
|
||||
_notifier: A QSocketNotifier used for signals on Unix.
|
||||
_timer: A QTimer used to poll for signals on Windows.
|
||||
_orig_handlers: A {signal: handler} dict of original signal handlers.
|
||||
_orig_wakeup_fd: The original wakeup filedescriptor.
|
||||
"""
|
||||
|
||||
def __init__(self, *, app, quitter, parent=None):
|
||||
super().__init__(parent)
|
||||
self._app = app
|
||||
self._quitter = quitter
|
||||
self._notifier = None
|
||||
self._timer = usertypes.Timer(self, 'python_hacks')
|
||||
self._orig_handlers = {}
|
||||
self._activated = False
|
||||
self._orig_wakeup_fd = None
|
||||
|
||||
def activate(self):
|
||||
"""Set up signal handlers.
|
||||
|
||||
On Windows this uses a QTimer to periodically hand control over to
|
||||
Python so it can handle signals.
|
||||
|
||||
On Unix, it uses a QSocketNotifier with os.set_wakeup_fd to get
|
||||
notified.
|
||||
"""
|
||||
self._orig_handlers[signal.SIGINT] = signal.signal(
|
||||
signal.SIGINT, self.interrupt)
|
||||
self._orig_handlers[signal.SIGTERM] = signal.signal(
|
||||
signal.SIGTERM, self.interrupt)
|
||||
|
||||
if os.name == 'posix' and hasattr(signal, 'set_wakeup_fd'):
|
||||
# pylint: disable=import-error,no-member
|
||||
import fcntl
|
||||
read_fd, write_fd = os.pipe()
|
||||
for fd in (read_fd, write_fd):
|
||||
flags = fcntl.fcntl(fd, fcntl.F_GETFL)
|
||||
fcntl.fcntl(fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
|
||||
self._notifier = QSocketNotifier(
|
||||
read_fd, QSocketNotifier.Read, self)
|
||||
self._notifier.activated.connect(self.handle_signal_wakeup)
|
||||
self._orig_wakeup_fd = signal.set_wakeup_fd(write_fd)
|
||||
else:
|
||||
self._timer.start(1000)
|
||||
self._timer.timeout.connect(lambda: None)
|
||||
self._activated = True
|
||||
|
||||
def deactivate(self):
|
||||
"""Deactivate all signal handlers."""
|
||||
if not self._activated:
|
||||
return
|
||||
if self._notifier is not None:
|
||||
self._notifier.setEnabled(False)
|
||||
rfd = self._notifier.socket()
|
||||
wfd = signal.set_wakeup_fd(self._orig_wakeup_fd)
|
||||
os.close(rfd)
|
||||
os.close(wfd)
|
||||
for sig, handler in self._orig_handlers.items():
|
||||
signal.signal(sig, handler)
|
||||
self._timer.stop()
|
||||
self._activated = False
|
||||
|
||||
@pyqtSlot()
|
||||
def handle_signal_wakeup(self):
|
||||
"""Handle a newly arrived signal.
|
||||
|
||||
This gets called via self._notifier when there's a signal.
|
||||
|
||||
Python will get control here, so the signal will get handled.
|
||||
"""
|
||||
log.destroy.debug("Handling signal wakeup!")
|
||||
self._notifier.setEnabled(False)
|
||||
read_fd = self._notifier.socket()
|
||||
try:
|
||||
os.read(read_fd, 1)
|
||||
except OSError:
|
||||
log.destroy.exception("Failed to read wakeup fd.")
|
||||
self._notifier.setEnabled(True)
|
||||
|
||||
def interrupt(self, signum, _frame):
|
||||
"""Handler for signals to gracefully shutdown (SIGINT/SIGTERM).
|
||||
|
||||
This calls shutdown and remaps the signal to call
|
||||
interrupt_forcefully the next time.
|
||||
"""
|
||||
log.destroy.info("SIGINT/SIGTERM received, shutting down!")
|
||||
log.destroy.info("Do the same again to forcefully quit.")
|
||||
signal.signal(signal.SIGINT, self.interrupt_forcefully)
|
||||
signal.signal(signal.SIGTERM, self.interrupt_forcefully)
|
||||
# If we call shutdown directly here, we get a segfault.
|
||||
QTimer.singleShot(0, functools.partial(
|
||||
self._quitter.shutdown, 128 + signum))
|
||||
|
||||
def interrupt_forcefully(self, signum, _frame):
|
||||
"""Interrupt forcefully on the second SIGINT/SIGTERM request.
|
||||
|
||||
This skips our shutdown routine and calls QApplication:exit instead.
|
||||
It then remaps the signals to call self.interrupt_really_forcefully the
|
||||
next time.
|
||||
"""
|
||||
log.destroy.info("Forceful quit requested, goodbye cruel world!")
|
||||
log.destroy.info("Do the same again to quit with even more force.")
|
||||
signal.signal(signal.SIGINT, self.interrupt_really_forcefully)
|
||||
signal.signal(signal.SIGTERM, self.interrupt_really_forcefully)
|
||||
# This *should* work without a QTimer, but because of the trouble in
|
||||
# self.interrupt we're better safe than sorry.
|
||||
QTimer.singleShot(0, functools.partial(self._app.exit, 128 + signum))
|
||||
|
||||
def interrupt_really_forcefully(self, signum, _frame):
|
||||
"""Interrupt with even more force on the third SIGINT/SIGTERM request.
|
||||
|
||||
This doesn't run *any* Qt cleanup and simply exits via Python.
|
||||
It will most likely lead to a segfault.
|
||||
"""
|
||||
log.destroy.info("WHY ARE YOU DOING THIS TO ME? :(")
|
||||
sys.exit(128 + signum)
|
||||
@@ -80,16 +80,21 @@ def _die(message, exception=None):
|
||||
"""
|
||||
from PyQt5.QtWidgets import QApplication, QMessageBox
|
||||
from PyQt5.QtCore import Qt
|
||||
if '--debug' in sys.argv and exception is not None:
|
||||
if (('--debug' in sys.argv or '--no-err-windows' in sys.argv) and
|
||||
exception is not None):
|
||||
print(file=sys.stderr)
|
||||
traceback.print_exc()
|
||||
app = QApplication(sys.argv)
|
||||
message += '<br/><br/><br/><b>Error:</b><br/>{}'.format(exception)
|
||||
msgbox = QMessageBox(QMessageBox.Critical, "qutebrowser: Fatal error!",
|
||||
message)
|
||||
msgbox.setTextFormat(Qt.RichText)
|
||||
msgbox.resize(msgbox.sizeHint())
|
||||
msgbox.exec_()
|
||||
if '--no-err-windows' in sys.argv:
|
||||
print(message, file=sys.stderr)
|
||||
print("Exiting because of --no-err-windows.", file=sys.stderr)
|
||||
else:
|
||||
message += '<br/><br/><br/><b>Error:</b><br/>{}'.format(exception)
|
||||
msgbox = QMessageBox(QMessageBox.Critical, "qutebrowser: Fatal error!",
|
||||
message)
|
||||
msgbox.setTextFormat(Qt.RichText)
|
||||
msgbox.resize(msgbox.sizeHint())
|
||||
msgbox.exec_()
|
||||
app.quit()
|
||||
sys.exit(1)
|
||||
|
||||
@@ -132,10 +137,10 @@ def fix_harfbuzz(args):
|
||||
- On Qt 5.2 (and probably earlier) the new engine probably has more
|
||||
crashes and is also experimental.
|
||||
|
||||
e.g. https://bugreports.qt-project.org/browse/QTBUG-36099
|
||||
e.g. https://bugreports.qt.io/browse/QTBUG-36099
|
||||
|
||||
- On Qt 5.3.0 there's a bug that affects a lot of websites:
|
||||
https://bugreports.qt-project.org/browse/QTBUG-39278
|
||||
https://bugreports.qt.io/browse/QTBUG-39278
|
||||
So the new engine will be more stable.
|
||||
|
||||
- On Qt 5.3.1 this bug is fixed and the old engine will be the more stable
|
||||
@@ -186,13 +191,13 @@ def check_pyqt_core():
|
||||
text = text.replace('</b>', '')
|
||||
text = text.replace('<br />', '\n')
|
||||
text += '\n\nError: {}'.format(e)
|
||||
if tkinter:
|
||||
if tkinter and '--no-err-windows' not in sys.argv:
|
||||
root = tkinter.Tk()
|
||||
root.withdraw()
|
||||
tkinter.messagebox.showerror("qutebrowser: Fatal error!", text)
|
||||
else:
|
||||
print(text, file=sys.stderr)
|
||||
if '--debug' in sys.argv:
|
||||
if '--debug' in sys.argv or '--no-err-windows' in sys.argv:
|
||||
print(file=sys.stderr)
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
@@ -208,6 +213,19 @@ def check_qt_version():
|
||||
_die(text)
|
||||
|
||||
|
||||
def check_ssl_support():
|
||||
"""Check if SSL support is available."""
|
||||
try:
|
||||
from PyQt5.QtNetwork import QSslSocket
|
||||
except ImportError:
|
||||
ok = False
|
||||
else:
|
||||
ok = QSslSocket.supportsSsl()
|
||||
if not ok:
|
||||
text = "Fatal error: Your Qt is built without SSL support."
|
||||
_die(text)
|
||||
|
||||
|
||||
def check_libraries():
|
||||
"""Check if all needed Python libraries are installed."""
|
||||
modules = {
|
||||
@@ -283,6 +301,7 @@ def earlyinit(args):
|
||||
# Now we can be sure QtCore is available, so we can print dialogs on
|
||||
# errors, so people only using the GUI notice them as well.
|
||||
check_qt_version()
|
||||
check_ssl_support()
|
||||
remove_inputhook()
|
||||
check_libraries()
|
||||
init_log(args)
|
||||
|
||||
@@ -22,10 +22,11 @@
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, QProcess, QObject
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QProcess
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import message, log
|
||||
from qutebrowser.misc import guiprocess
|
||||
|
||||
|
||||
class ExternalEditor(QObject):
|
||||
@@ -36,7 +37,7 @@ class ExternalEditor(QObject):
|
||||
_text: The current text before the editor is opened.
|
||||
_oshandle: The OS level handle to the tmpfile.
|
||||
_filehandle: The file handle to the tmpfile.
|
||||
_proc: The QProcess of the editor.
|
||||
_proc: The GUIProcess of the editor.
|
||||
_win_id: The window ID the ExternalEditor is associated with.
|
||||
"""
|
||||
|
||||
@@ -52,6 +53,9 @@ class ExternalEditor(QObject):
|
||||
|
||||
def _cleanup(self):
|
||||
"""Clean up temporary files after the editor closed."""
|
||||
if self._oshandle is None or self._filename is None:
|
||||
# Could not create initial file.
|
||||
return
|
||||
try:
|
||||
os.close(self._oshandle)
|
||||
os.remove(self._filename)
|
||||
@@ -61,6 +65,7 @@ class ExternalEditor(QObject):
|
||||
message.error(self._win_id,
|
||||
"Failed to delete tempfile... ({})".format(e))
|
||||
|
||||
@pyqtSlot(int, QProcess.ExitStatus)
|
||||
def on_proc_closed(self, exitcode, exitstatus):
|
||||
"""Write the editor text into the form field and clean up tempfile.
|
||||
|
||||
@@ -69,20 +74,15 @@ class ExternalEditor(QObject):
|
||||
log.procs.debug("Editor closed")
|
||||
if exitstatus != QProcess.NormalExit:
|
||||
# No error/cleanup here, since we already handle this in
|
||||
# on_proc_error
|
||||
# on_proc_error.
|
||||
return
|
||||
try:
|
||||
if exitcode != 0:
|
||||
# NOTE: Do not replace this with "raise CommandError" as it's
|
||||
# executed async.
|
||||
message.error(
|
||||
self._win_id, "Editor did quit abnormally (status "
|
||||
"{})!".format(exitcode))
|
||||
return
|
||||
encoding = config.get('general', 'editor-encoding')
|
||||
try:
|
||||
with open(self._filename, 'r', encoding=encoding) as f:
|
||||
text = ''.join(f.readlines())
|
||||
text = ''.join(f.readlines()) # pragma: no branch
|
||||
except OSError as e:
|
||||
# NOTE: Do not replace this with "raise CommandError" as it's
|
||||
# executed async.
|
||||
@@ -94,22 +94,8 @@ class ExternalEditor(QObject):
|
||||
finally:
|
||||
self._cleanup()
|
||||
|
||||
def on_proc_error(self, error):
|
||||
"""Display an error message and clean up when editor crashed."""
|
||||
messages = {
|
||||
QProcess.FailedToStart: "The process failed to start.",
|
||||
QProcess.Crashed: "The process crashed.",
|
||||
QProcess.Timedout: "The last waitFor...() function timed out.",
|
||||
QProcess.WriteError: ("An error occurred when attempting to write "
|
||||
"to the process."),
|
||||
QProcess.ReadError: ("An error occurred when attempting to read "
|
||||
"from the process."),
|
||||
QProcess.UnknownError: "An unknown error occurred.",
|
||||
}
|
||||
# NOTE: Do not replace this with "raise CommandError" as it's
|
||||
# executed async.
|
||||
message.error(self._win_id,
|
||||
"Error while calling editor: {}".format(messages[error]))
|
||||
@pyqtSlot(QProcess.ProcessError)
|
||||
def on_proc_error(self, _err):
|
||||
self._cleanup()
|
||||
|
||||
def edit(self, text):
|
||||
@@ -122,16 +108,18 @@ class ExternalEditor(QObject):
|
||||
raise ValueError("Already editing a file!")
|
||||
self._text = text
|
||||
try:
|
||||
self._oshandle, self._filename = tempfile.mkstemp(text=True)
|
||||
self._oshandle, self._filename = tempfile.mkstemp(
|
||||
text=True, prefix='qutebrowser-editor-')
|
||||
if text:
|
||||
encoding = config.get('general', 'editor-encoding')
|
||||
with open(self._filename, 'w', encoding=encoding) as f:
|
||||
f.write(text)
|
||||
f.write(text) # pragma: no branch
|
||||
except OSError as e:
|
||||
message.error(self._win_id, "Failed to create initial file: "
|
||||
"{}".format(e))
|
||||
return
|
||||
self._proc = QProcess(self)
|
||||
self._proc = guiprocess.GUIProcess(self._win_id, what='editor',
|
||||
parent=self)
|
||||
self._proc.finished.connect(self.on_proc_closed)
|
||||
self._proc.error.connect(self.on_proc_error)
|
||||
editor = config.get('general', 'editor')
|
||||
|
||||
153
qutebrowser/misc/guiprocess.py
Normal file
153
qutebrowser/misc/guiprocess.py
Normal file
@@ -0,0 +1,153 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2015 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/>.
|
||||
|
||||
"""A QProcess which shows notifications in the GUI."""
|
||||
|
||||
import shlex
|
||||
|
||||
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, QObject, QProcess,
|
||||
QProcessEnvironment)
|
||||
|
||||
from qutebrowser.utils import message, log
|
||||
|
||||
# A mapping of QProcess::ErrorCode's to human-readable strings.
|
||||
|
||||
ERROR_STRINGS = {
|
||||
QProcess.FailedToStart: "The process failed to start.",
|
||||
QProcess.Crashed: "The process crashed.",
|
||||
QProcess.Timedout: "The last waitFor...() function timed out.",
|
||||
QProcess.WriteError: ("An error occurred when attempting to write to the "
|
||||
"process."),
|
||||
QProcess.ReadError: ("An error occurred when attempting to read from the "
|
||||
"process."),
|
||||
QProcess.UnknownError: "An unknown error occurred.",
|
||||
}
|
||||
|
||||
|
||||
class GUIProcess(QObject):
|
||||
|
||||
"""An external process which shows notifications in the GUI.
|
||||
|
||||
Args:
|
||||
cmd: The command which was started.
|
||||
args: A list of arguments which gets passed.
|
||||
verbose: Whether to show more messages.
|
||||
_started: Whether the underlying process is started.
|
||||
_proc: The underlying QProcess.
|
||||
_win_id: The window ID this process is used in.
|
||||
_what: What kind of thing is spawned (process/editor/userscript/...).
|
||||
Used in messages.
|
||||
|
||||
Signals:
|
||||
error/finished/started signals proxied from QProcess.
|
||||
"""
|
||||
|
||||
error = pyqtSignal(QProcess.ProcessError)
|
||||
finished = pyqtSignal(int, QProcess.ExitStatus)
|
||||
started = pyqtSignal()
|
||||
|
||||
def __init__(self, win_id, what, *, verbose=False, additional_env=None,
|
||||
parent=None):
|
||||
super().__init__(parent)
|
||||
self._win_id = win_id
|
||||
self._what = what
|
||||
self.verbose = verbose
|
||||
self._started = False
|
||||
self.cmd = None
|
||||
self.args = None
|
||||
|
||||
self._proc = QProcess(self)
|
||||
self._proc.error.connect(self.on_error)
|
||||
self._proc.error.connect(self.error)
|
||||
self._proc.finished.connect(self.on_finished)
|
||||
self._proc.finished.connect(self.finished)
|
||||
self._proc.started.connect(self.on_started)
|
||||
self._proc.started.connect(self.started)
|
||||
|
||||
if additional_env is not None:
|
||||
procenv = QProcessEnvironment.systemEnvironment()
|
||||
for k, v in additional_env.items():
|
||||
procenv.insert(k, v)
|
||||
self._proc.setProcessEnvironment(procenv)
|
||||
|
||||
@pyqtSlot(QProcess.ProcessError)
|
||||
def on_error(self, error):
|
||||
"""Show a message if there was an error while spawning."""
|
||||
msg = ERROR_STRINGS[error]
|
||||
message.error(self._win_id, "Error while spawning {}: {}".format(
|
||||
self._what, msg), immediately=True)
|
||||
|
||||
@pyqtSlot(int, QProcess.ExitStatus)
|
||||
def on_finished(self, code, status):
|
||||
"""Show a message when the process finished."""
|
||||
self._started = False
|
||||
log.procs.debug("Process finished with code {}, status {}.".format(
|
||||
code, status))
|
||||
if status == QProcess.CrashExit:
|
||||
message.error(self._win_id,
|
||||
"{} crashed!".format(self._what.capitalize()),
|
||||
immediately=True)
|
||||
elif status == QProcess.NormalExit and code == 0:
|
||||
if self.verbose:
|
||||
message.info(self._win_id, "{} exited successfully.".format(
|
||||
self._what.capitalize()))
|
||||
else:
|
||||
assert status == QProcess.NormalExit
|
||||
message.error(self._win_id, "{} exited with status {}.".format(
|
||||
self._what.capitalize(), code))
|
||||
|
||||
@pyqtSlot()
|
||||
def on_started(self):
|
||||
"""Called when the process started successfully."""
|
||||
log.procs.debug("Process started.")
|
||||
assert not self._started
|
||||
self._started = True
|
||||
|
||||
def _pre_start(self, cmd, args):
|
||||
"""Prepare starting of a QProcess."""
|
||||
if self._started:
|
||||
raise ValueError("Trying to start a running QProcess!")
|
||||
self.cmd = cmd
|
||||
self.args = args
|
||||
fake_cmdline = ' '.join(shlex.quote(e) for e in [cmd] + list(args))
|
||||
log.procs.debug("Executing: {}".format(fake_cmdline))
|
||||
if self.verbose:
|
||||
message.info(self._win_id, 'Executing: ' + fake_cmdline)
|
||||
|
||||
def start(self, cmd, args, mode=None):
|
||||
"""Convenience wrapper around QProcess::start."""
|
||||
log.procs.debug("Starting process.")
|
||||
self._pre_start(cmd, args)
|
||||
if mode is None:
|
||||
self._proc.start(cmd, args)
|
||||
else:
|
||||
self._proc.start(cmd, args, mode)
|
||||
|
||||
def start_detached(self, cmd, args, cwd=None):
|
||||
"""Convenience wrapper around QProcess::startDetached."""
|
||||
log.procs.debug("Starting detached.")
|
||||
self._pre_start(cmd, args)
|
||||
ok, _pid = self._proc.startDetached(cmd, args, cwd)
|
||||
|
||||
if ok:
|
||||
log.procs.debug("Process started.")
|
||||
self._started = True
|
||||
else:
|
||||
message.error(self._win_id, "Error while spawning {}: {}.".format(
|
||||
self._what, self._proc.error()), immediately=True)
|
||||
@@ -20,26 +20,90 @@
|
||||
"""Utilities for IPC with existing instances."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import json
|
||||
import getpass
|
||||
import binascii
|
||||
import hashlib
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot, QObject
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, Qt
|
||||
from PyQt5.QtNetwork import QLocalSocket, QLocalServer, QAbstractSocket
|
||||
from PyQt5.QtWidgets import QMessageBox
|
||||
|
||||
from qutebrowser.utils import log, objreg, usertypes
|
||||
import qutebrowser
|
||||
from qutebrowser.utils import (log, usertypes, error, objreg, standarddir,
|
||||
qtutils)
|
||||
|
||||
|
||||
SOCKETNAME = 'qutebrowser-{}'.format(getpass.getuser())
|
||||
CONNECT_TIMEOUT = 100
|
||||
WRITE_TIMEOUT = 1000
|
||||
READ_TIMEOUT = 5000
|
||||
ATIME_INTERVAL = 60 * 60 * 6 * 1000 # 6 hours
|
||||
PROTOCOL_VERSION = 1
|
||||
|
||||
|
||||
def _get_socketname_legacy(basedir):
|
||||
"""Legacy implementation of _get_socketname."""
|
||||
parts = ['qutebrowser', getpass.getuser()]
|
||||
if basedir is not None:
|
||||
md5 = hashlib.md5(basedir.encode('utf-8')).hexdigest()
|
||||
parts.append(md5)
|
||||
return '-'.join(parts)
|
||||
|
||||
|
||||
def _get_socketname(basedir, legacy=False):
|
||||
"""Get a socketname to use."""
|
||||
if legacy or os.name == 'nt':
|
||||
return _get_socketname_legacy(basedir)
|
||||
|
||||
parts_to_hash = [getpass.getuser()]
|
||||
if basedir is not None:
|
||||
parts_to_hash.append(basedir)
|
||||
|
||||
data_to_hash = '-'.join(parts_to_hash).encode('utf-8')
|
||||
md5 = hashlib.md5(data_to_hash).hexdigest()
|
||||
|
||||
if sys.platform.startswith('linux'):
|
||||
target_dir = standarddir.runtime()
|
||||
else: # pragma: no cover
|
||||
# OS X or other Unix
|
||||
target_dir = standarddir.temp()
|
||||
|
||||
parts = ['ipc']
|
||||
parts.append(md5)
|
||||
return os.path.join(target_dir, '-'.join(parts))
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
|
||||
"""Exception raised when there was a problem with IPC."""
|
||||
"""Base class for IPC exceptions."""
|
||||
|
||||
|
||||
class SocketError(Error):
|
||||
|
||||
"""Exception raised when there was an error with a QLocalSocket.
|
||||
|
||||
Args:
|
||||
code: The error code.
|
||||
message: The error message.
|
||||
action: The action which was taken when the error happened.
|
||||
"""
|
||||
|
||||
def __init__(self, action, socket):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
action: The action which was taken when the error happened.
|
||||
socket: The QLocalSocket which has the error set.
|
||||
"""
|
||||
super().__init__()
|
||||
self.action = action
|
||||
self.code = socket.error()
|
||||
self.message = socket.errorString()
|
||||
|
||||
def __str__(self):
|
||||
return "Error while {}: {} (error {})".format(
|
||||
self.action, self.message, self.code)
|
||||
|
||||
|
||||
class ListenError(Error):
|
||||
@@ -80,40 +144,96 @@ class IPCServer(QObject):
|
||||
_timer: A timer to handle timeouts.
|
||||
_server: A QLocalServer to accept new connections.
|
||||
_socket: The QLocalSocket we're currently connected to.
|
||||
_socketname: The socketname to use.
|
||||
_socketopts_ok: Set if using setSocketOptions is working with this
|
||||
OS/Qt version.
|
||||
_atime_timer: Timer to update the atime of the socket regularily.
|
||||
|
||||
Signals:
|
||||
got_args: Emitted when there was an IPC connection and arguments were
|
||||
passed.
|
||||
got_args: Emitted with the raw data an IPC connection got.
|
||||
got_invalid_data: Emitted when there was invalid incoming data.
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
"""Start the IPC server and listen to commands."""
|
||||
got_args = pyqtSignal(list, str)
|
||||
got_raw = pyqtSignal(bytes)
|
||||
got_invalid_data = pyqtSignal()
|
||||
|
||||
def __init__(self, socketname, parent=None):
|
||||
"""Start the IPC server and listen to commands.
|
||||
|
||||
Args:
|
||||
socketname: The socketname to use.
|
||||
parent: The parent to be used.
|
||||
"""
|
||||
super().__init__(parent)
|
||||
self.ignored = False
|
||||
self._remove_server()
|
||||
self._socketname = socketname
|
||||
|
||||
self._timer = usertypes.Timer(self, 'ipc-timeout')
|
||||
self._timer.setInterval(READ_TIMEOUT)
|
||||
self._timer.timeout.connect(self.on_timeout)
|
||||
|
||||
if os.name == 'nt': # pragma: no coverage
|
||||
self._atime_timer = None
|
||||
else:
|
||||
self._atime_timer = usertypes.Timer(self, 'ipc-atime')
|
||||
self._atime_timer.setInterval(ATIME_INTERVAL)
|
||||
self._atime_timer.timeout.connect(self.update_atime)
|
||||
self._atime_timer.setTimerType(Qt.VeryCoarseTimer)
|
||||
|
||||
self._server = QLocalServer(self)
|
||||
ok = self._server.listen(SOCKETNAME)
|
||||
self._server.newConnection.connect(self.handle_connection)
|
||||
|
||||
self._socket = None
|
||||
self._socketopts_ok = os.name == 'nt' or qtutils.version_check('5.4')
|
||||
if self._socketopts_ok: # pragma: no cover
|
||||
# If we use setSocketOptions on Unix with Qt < 5.4, we get a
|
||||
# NameError while listening...
|
||||
self._server.setSocketOptions(QLocalServer.UserAccessOption)
|
||||
|
||||
def _remove_server(self):
|
||||
"""Remove an existing server."""
|
||||
ok = QLocalServer.removeServer(self._socketname)
|
||||
if not ok:
|
||||
raise Error("Error while removing server {}!".format(
|
||||
self._socketname))
|
||||
|
||||
def listen(self):
|
||||
"""Start listening on self._socketname."""
|
||||
log.ipc.debug("Listening as {}".format(self._socketname))
|
||||
if self._atime_timer is not None: # pragma: no branch
|
||||
self._atime_timer.start()
|
||||
self._remove_server()
|
||||
ok = self._server.listen(self._socketname)
|
||||
if not ok:
|
||||
if self._server.serverError() == QAbstractSocket.AddressInUseError:
|
||||
raise AddressInUseError(self._server)
|
||||
else:
|
||||
raise ListenError(self._server)
|
||||
self._server.newConnection.connect(self.handle_connection)
|
||||
self._socket = None
|
||||
|
||||
def _remove_server(self):
|
||||
"""Remove an existing server."""
|
||||
ok = QLocalServer.removeServer(SOCKETNAME)
|
||||
if not ok:
|
||||
raise Error("Error while removing server {}!".format(SOCKETNAME))
|
||||
if not self._socketopts_ok: # pragma: no cover
|
||||
# If we use setSocketOptions on Unix with Qt < 5.4, we get a
|
||||
# NameError while listening...
|
||||
os.chmod(self._server.fullServerName(), 0o700)
|
||||
|
||||
@pyqtSlot(int)
|
||||
def on_error(self, error):
|
||||
"""Convenience method which calls _socket_error on an error."""
|
||||
def on_error(self, err):
|
||||
"""Raise SocketError on fatal errors."""
|
||||
if self._socket is None:
|
||||
# Sometimes this gets called from stale sockets.
|
||||
msg = "In on_error with None socket!"
|
||||
if os.name == 'nt': # pragma: no cover
|
||||
# This happens a lot on Windows, so we ignore it there.
|
||||
log.ipc.debug(msg)
|
||||
else:
|
||||
log.ipc.warn(msg)
|
||||
return
|
||||
self._timer.stop()
|
||||
log.ipc.debug("Socket error {}: {}".format(
|
||||
self._socket.error(), self._socket.errorString()))
|
||||
if error != QLocalSocket.PeerClosedError:
|
||||
_socket_error("handling IPC connection", self._socket)
|
||||
if err != QLocalSocket.PeerClosedError:
|
||||
raise SocketError("handling IPC connection", self._socket)
|
||||
|
||||
@pyqtSlot()
|
||||
def handle_connection(self):
|
||||
@@ -150,45 +270,74 @@ class IPCServer(QObject):
|
||||
"""Clean up socket when the client disconnected."""
|
||||
log.ipc.debug("Client disconnected.")
|
||||
self._timer.stop()
|
||||
self._socket.deleteLater()
|
||||
self._socket = None
|
||||
if self._socket is None:
|
||||
log.ipc.warn("In on_disconnected with None socket!")
|
||||
else:
|
||||
self._socket.deleteLater()
|
||||
self._socket = None
|
||||
# Maybe another connection is waiting.
|
||||
self.handle_connection()
|
||||
|
||||
def _handle_invalid_data(self):
|
||||
"""Handle invalid data we got from a QLocalSocket."""
|
||||
log.ipc.error("Ignoring invalid IPC data.")
|
||||
self.got_invalid_data.emit()
|
||||
self._socket.error.connect(self.on_error)
|
||||
self._socket.disconnectFromServer()
|
||||
|
||||
@pyqtSlot()
|
||||
def on_ready_read(self):
|
||||
"""Read json data from the client."""
|
||||
if self._socket is None:
|
||||
# this happened once and I don't know why
|
||||
# This happens when doing a connection while another one is already
|
||||
# active for some reason.
|
||||
log.ipc.warn("In on_ready_read with None socket!")
|
||||
return
|
||||
self._timer.start()
|
||||
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("Ignoring invalid IPC data.")
|
||||
log.ipc.debug("invalid data: {}".format(
|
||||
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("Ignoring invalid IPC data.")
|
||||
log.ipc.debug("invalid json: {}".format(decoded.strip()))
|
||||
log.ipc.error("invalid json: {}".format(decoded.strip()))
|
||||
self._handle_invalid_data()
|
||||
return
|
||||
|
||||
try:
|
||||
args = json_data['args']
|
||||
except KeyError:
|
||||
log.ipc.error("Ignoring invalid IPC data.")
|
||||
log.ipc.debug("no args: {}".format(decoded.strip()))
|
||||
log.ipc.error("no args: {}".format(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)
|
||||
app = objreg.get('app')
|
||||
app.process_pos_args(args, via_ipc=True, cwd=cwd)
|
||||
self.got_args.emit(args, cwd)
|
||||
|
||||
@pyqtSlot()
|
||||
def on_timeout(self):
|
||||
@@ -196,52 +345,104 @@ class IPCServer(QObject):
|
||||
log.ipc.error("IPC connection timed out.")
|
||||
self._socket.close()
|
||||
|
||||
@pyqtSlot()
|
||||
def update_atime(self):
|
||||
"""Update the atime of the socket file all few hours.
|
||||
|
||||
From the XDG basedir spec:
|
||||
|
||||
To ensure that your files are not removed, they should have their
|
||||
access time timestamp modified at least once every 6 hours of monotonic
|
||||
time or the 'sticky' bit should be set on the file.
|
||||
"""
|
||||
path = self._server.fullServerName()
|
||||
if not path:
|
||||
log.ipc.error("In update_atime with no server path!")
|
||||
return
|
||||
os.utime(path)
|
||||
|
||||
def shutdown(self):
|
||||
"""Shut down the IPC server cleanly."""
|
||||
if self._socket is not None:
|
||||
self._socket.deleteLater()
|
||||
self._socket = None
|
||||
self._timer.stop()
|
||||
if self._atime_timer is not None: # pragma: no branch
|
||||
self._atime_timer.stop()
|
||||
try:
|
||||
self._atime_timer.timeout.disconnect(self.update_atime)
|
||||
except TypeError:
|
||||
pass
|
||||
self._server.close()
|
||||
self._server.deleteLater()
|
||||
self._remove_server()
|
||||
|
||||
|
||||
def init():
|
||||
"""Initialize the global IPC server."""
|
||||
app = objreg.get('app')
|
||||
server = IPCServer(app)
|
||||
objreg.register('ipc-server', server)
|
||||
|
||||
|
||||
def _socket_error(action, socket):
|
||||
"""Raise an Error based on an action and a QLocalSocket.
|
||||
def _has_legacy_server(name):
|
||||
"""Check if there is a legacy server.
|
||||
|
||||
Args:
|
||||
action: A string like "writing to running instance".
|
||||
socket: A QLocalSocket.
|
||||
name: The name to try to connect to.
|
||||
|
||||
Return:
|
||||
True if there is a server with the given name, False otherwise.
|
||||
"""
|
||||
raise Error("Error while {}: {} (error {})".format(
|
||||
action, socket.errorString(), socket.error()))
|
||||
socket = QLocalSocket()
|
||||
log.ipc.debug("Trying to connect to {}".format(name))
|
||||
socket.connectToServer(name)
|
||||
|
||||
err = socket.error()
|
||||
|
||||
if err != QLocalSocket.UnknownSocketError:
|
||||
log.ipc.debug("Socket error: {} ({})".format(
|
||||
socket.errorString(), err))
|
||||
|
||||
os_x_fail = (sys.platform == 'darwin' and
|
||||
socket.errorString() == 'QLocalSocket::connectToServer: '
|
||||
'Unknown error 38')
|
||||
|
||||
if err not in [QLocalSocket.ServerNotFoundError,
|
||||
QLocalSocket.ConnectionRefusedError] and not os_x_fail:
|
||||
return True
|
||||
|
||||
socket.disconnectFromServer()
|
||||
if socket.state() != QLocalSocket.UnconnectedState:
|
||||
socket.waitForDisconnected(100)
|
||||
return False
|
||||
|
||||
|
||||
def send_to_running_instance(cmdlist):
|
||||
def send_to_running_instance(socketname, command, *, legacy_name=None,
|
||||
socket=None):
|
||||
"""Try to send a commandline to a running instance.
|
||||
|
||||
Blocks for CONNECT_TIMEOUT ms.
|
||||
|
||||
Args:
|
||||
cmdlist: A list to send (URLs/commands)
|
||||
socketname: The name which should be used for the socket.
|
||||
command: The command to send to the running instance.
|
||||
socket: The socket to read data from, or None.
|
||||
legacy_name: The legacy name to first try to connect to.
|
||||
|
||||
Return:
|
||||
True if connecting was successful, False if no connection was made.
|
||||
"""
|
||||
socket = QLocalSocket()
|
||||
socket.connectToServer(SOCKETNAME)
|
||||
if socket is None:
|
||||
socket = QLocalSocket()
|
||||
|
||||
if (legacy_name is not None and
|
||||
_has_legacy_server(legacy_name)):
|
||||
name_to_use = legacy_name
|
||||
else:
|
||||
name_to_use = socketname
|
||||
|
||||
log.ipc.debug("Connecting to {}".format(name_to_use))
|
||||
socket.connectToServer(name_to_use)
|
||||
|
||||
connected = socket.waitForConnected(100)
|
||||
if connected:
|
||||
log.ipc.info("Opening in existing instance")
|
||||
json_data = {'args': cmdlist}
|
||||
json_data = {'args': command, 'version': qutebrowser.__version__,
|
||||
'protocol_version': PROTOCOL_VERSION}
|
||||
try:
|
||||
cwd = os.getcwd()
|
||||
except OSError:
|
||||
@@ -254,22 +455,62 @@ def send_to_running_instance(cmdlist):
|
||||
socket.writeData(data)
|
||||
socket.waitForBytesWritten(WRITE_TIMEOUT)
|
||||
if socket.error() != QLocalSocket.UnknownSocketError:
|
||||
_socket_error("writing to running instance", socket)
|
||||
raise SocketError("writing to running instance", socket)
|
||||
else:
|
||||
socket.disconnectFromServer()
|
||||
if socket.state() != QLocalSocket.UnconnectedState:
|
||||
socket.waitForDisconnected(100)
|
||||
return True
|
||||
else:
|
||||
if socket.error() not in (QLocalSocket.ConnectionRefusedError,
|
||||
QLocalSocket.ServerNotFoundError):
|
||||
_socket_error("connecting to running instance", socket)
|
||||
raise SocketError("connecting to running instance", socket)
|
||||
else:
|
||||
log.ipc.debug("No existing instance present (error {})".format(
|
||||
socket.error()))
|
||||
return False
|
||||
|
||||
|
||||
def display_error(exc):
|
||||
def display_error(exc, args):
|
||||
"""Display a message box with an IPC error."""
|
||||
text = '{}\n\nMaybe another instance is running but frozen?'.format(exc)
|
||||
msgbox = QMessageBox(QMessageBox.Critical, "Error while connecting to "
|
||||
"running instance!", text)
|
||||
msgbox.exec_()
|
||||
error.handle_fatal_exc(
|
||||
exc, args, "Error while connecting to running instance!",
|
||||
post_text="Maybe another instance is running but frozen?")
|
||||
|
||||
|
||||
def send_or_listen(args):
|
||||
"""Send the args to a running instance or start a new IPCServer.
|
||||
|
||||
Args:
|
||||
args: The argparse namespace.
|
||||
|
||||
Return:
|
||||
The IPCServer instance if no running instance was detected.
|
||||
None if an instance was running and received our request.
|
||||
"""
|
||||
socketname = _get_socketname(args.basedir)
|
||||
legacy_socketname = _get_socketname(args.basedir, legacy=True)
|
||||
try:
|
||||
try:
|
||||
sent = send_to_running_instance(socketname, args.command,
|
||||
legacy_name=legacy_socketname)
|
||||
if sent:
|
||||
return None
|
||||
log.init.debug("Starting IPC server...")
|
||||
server = IPCServer(socketname)
|
||||
server.listen()
|
||||
objreg.register('ipc-server', server)
|
||||
return server
|
||||
except AddressInUseError as e:
|
||||
# This could be a race condition...
|
||||
log.init.debug("Got AddressInUseError, trying again.")
|
||||
time.sleep(0.5)
|
||||
sent = send_to_running_instance(socketname, args.command,
|
||||
legacy_name=legacy_socketname)
|
||||
if sent:
|
||||
return None
|
||||
else:
|
||||
raise
|
||||
except Error as e:
|
||||
display_error(e, args)
|
||||
raise
|
||||
|
||||
@@ -35,7 +35,7 @@ class BaseLineParser(QObject):
|
||||
"""A LineParser without any real data.
|
||||
|
||||
Attributes:
|
||||
_configdir: The directory to read the config from.
|
||||
_configdir: Directory to read the config from, or None.
|
||||
_configfile: The config file path.
|
||||
_fname: Filename of the config.
|
||||
_binary: Whether to open the file in binary mode.
|
||||
@@ -53,12 +53,17 @@ class BaseLineParser(QObject):
|
||||
configdir: Directory to read the config from.
|
||||
fname: Filename of the config file.
|
||||
binary: Whether to open the file in binary mode.
|
||||
_opened: Whether the underlying file is open
|
||||
"""
|
||||
super().__init__(parent)
|
||||
self._configdir = configdir
|
||||
self._configfile = os.path.join(self._configdir, fname)
|
||||
if self._configdir is None:
|
||||
self._configfile = None
|
||||
else:
|
||||
self._configfile = os.path.join(self._configdir, fname)
|
||||
self._fname = fname
|
||||
self._binary = binary
|
||||
self._opened = False
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self, constructor=True,
|
||||
@@ -66,21 +71,38 @@ class BaseLineParser(QObject):
|
||||
binary=self._binary)
|
||||
|
||||
def _prepare_save(self):
|
||||
"""Prepare saving of the file."""
|
||||
"""Prepare saving of the file.
|
||||
|
||||
Return:
|
||||
True if the file should be saved, False otherwise.
|
||||
"""
|
||||
if self._configdir is None:
|
||||
return False
|
||||
log.destroy.debug("Saving to {}".format(self._configfile))
|
||||
if not os.path.exists(self._configdir):
|
||||
os.makedirs(self._configdir, 0o755)
|
||||
return True
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _open(self, mode):
|
||||
"""Open self._configfile for reading.
|
||||
|
||||
Args:
|
||||
mode: The mode to use ('a'/'r'/'w')
|
||||
"""
|
||||
if self._binary:
|
||||
return open(self._configfile, mode + 'b')
|
||||
else:
|
||||
return open(self._configfile, mode, encoding='utf-8')
|
||||
assert self._configfile is not None
|
||||
if self._opened:
|
||||
raise IOError("Refusing to double-open AppendLineParser.")
|
||||
self._opened = True
|
||||
try:
|
||||
if self._binary:
|
||||
with open(self._configfile, mode + 'b') as f:
|
||||
yield f
|
||||
else:
|
||||
with open(self._configfile, mode, encoding='utf-8') as f:
|
||||
yield f
|
||||
finally:
|
||||
self._opened = False
|
||||
|
||||
def _write(self, fp, data):
|
||||
"""Write the data to a file.
|
||||
@@ -150,7 +172,9 @@ class AppendLineParser(BaseLineParser):
|
||||
return data
|
||||
|
||||
def save(self):
|
||||
self._prepare_save()
|
||||
do_save = self._prepare_save()
|
||||
if not do_save:
|
||||
return
|
||||
with self._open('a') as f:
|
||||
self._write(f, self.new_data)
|
||||
self.new_data = []
|
||||
@@ -173,7 +197,7 @@ class LineParser(BaseLineParser):
|
||||
binary: Whether to open the file in binary mode.
|
||||
"""
|
||||
super().__init__(configdir, fname, binary=binary, parent=parent)
|
||||
if not os.path.isfile(self._configfile):
|
||||
if configdir is None or not os.path.isfile(self._configfile):
|
||||
self.data = []
|
||||
else:
|
||||
log.init.debug("Reading {}".format(self._configfile))
|
||||
@@ -195,9 +219,18 @@ class LineParser(BaseLineParser):
|
||||
|
||||
def save(self):
|
||||
"""Save the config file."""
|
||||
self._prepare_save()
|
||||
with qtutils.savefile_open(self._configfile, self._binary) as f:
|
||||
self._write(f, self.data)
|
||||
if self._opened:
|
||||
raise IOError("Refusing to double-open AppendLineParser.")
|
||||
do_save = self._prepare_save()
|
||||
if not do_save:
|
||||
return
|
||||
self._opened = True
|
||||
try:
|
||||
assert self._configfile is not None
|
||||
with qtutils.savefile_open(self._configfile, self._binary) as f:
|
||||
self._write(f, self.data)
|
||||
finally:
|
||||
self._opened = False
|
||||
|
||||
|
||||
class LimitLineParser(LineParser):
|
||||
@@ -213,14 +246,14 @@ class LimitLineParser(LineParser):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
configdir: Directory to read the config from.
|
||||
configdir: Directory to read the config from, or None.
|
||||
fname: Filename of the config file.
|
||||
limit: Config tuple (section, option) which contains a limit.
|
||||
binary: Whether to open the file in binary mode.
|
||||
"""
|
||||
super().__init__(configdir, fname, binary=binary, parent=parent)
|
||||
self._limit = limit
|
||||
if limit is not None:
|
||||
if limit is not None and configdir is not None:
|
||||
objreg.get('config').changed.connect(self.cleanup_file)
|
||||
|
||||
def __repr__(self):
|
||||
@@ -231,6 +264,7 @@ class LimitLineParser(LineParser):
|
||||
@pyqtSlot(str, str)
|
||||
def cleanup_file(self, section, option):
|
||||
"""Delete the file if the limit was changed to 0."""
|
||||
assert self._configfile is not None
|
||||
if (section, option) != self._limit:
|
||||
return
|
||||
value = config.get(section, option)
|
||||
@@ -243,6 +277,9 @@ class LimitLineParser(LineParser):
|
||||
limit = config.get(*self._limit)
|
||||
if limit == 0:
|
||||
return
|
||||
self._prepare_save()
|
||||
do_save = self._prepare_save()
|
||||
if not do_save:
|
||||
return
|
||||
assert self._configfile is not None
|
||||
with qtutils.savefile_open(self._configfile, self._binary) as f:
|
||||
self._write(f, self.data[-limit:])
|
||||
|
||||
@@ -77,7 +77,7 @@ class CommandLineEdit(QLineEdit):
|
||||
def __on_cursor_position_changed(self, _old, new):
|
||||
"""Prevent the cursor moving to the prompt.
|
||||
|
||||
We use __ here to avoid accidentally overriding it in superclasses.
|
||||
We use __ here to avoid accidentally overriding it in subclasses.
|
||||
"""
|
||||
if new < self._promptlen:
|
||||
self.setCursorPosition(self._promptlen)
|
||||
|
||||
@@ -184,9 +184,8 @@ class SaveManager(QObject):
|
||||
message.error('current', "Failed to auto-save {}: "
|
||||
"{}".format(key, e))
|
||||
|
||||
@cmdutils.register(instance='save-manager', name='save')
|
||||
def save_command(self, win_id: {'special': 'win_id'},
|
||||
*what: {'nargs': '*'}):
|
||||
@cmdutils.register(instance='save-manager', name='save', win_id='win_id')
|
||||
def save_command(self, win_id, *what: {'nargs': '*'}):
|
||||
"""Save configs and state.
|
||||
|
||||
Args:
|
||||
|
||||
@@ -27,7 +27,7 @@ from PyQt5.QtWidgets import QApplication
|
||||
import yaml
|
||||
try:
|
||||
from yaml import CSafeLoader as YamlLoader, CSafeDumper as YamlDumper
|
||||
except ImportError:
|
||||
except ImportError: # pragma: no cover
|
||||
from yaml import SafeLoader as YamlLoader, SafeDumper as YamlDumper
|
||||
|
||||
from qutebrowser.browser import tabhistory
|
||||
@@ -47,7 +47,17 @@ def init(parent=None):
|
||||
Args:
|
||||
parent: The parent to use for the SessionManager.
|
||||
"""
|
||||
session_manager = SessionManager(parent)
|
||||
data_dir = standarddir.data()
|
||||
if data_dir is None:
|
||||
base_path = None
|
||||
else:
|
||||
base_path = os.path.join(standarddir.data(), 'sessions')
|
||||
try:
|
||||
os.mkdir(base_path)
|
||||
except FileExistsError:
|
||||
pass
|
||||
|
||||
session_manager = SessionManager(base_path, parent)
|
||||
objreg.register('session-manager', session_manager)
|
||||
|
||||
|
||||
@@ -79,14 +89,12 @@ class SessionManager(QObject):
|
||||
|
||||
update_completion = pyqtSignal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
def __init__(self, base_path, parent=None):
|
||||
super().__init__(parent)
|
||||
self._current = None
|
||||
self._base_path = os.path.join(standarddir.data(), 'sessions')
|
||||
self._base_path = base_path
|
||||
self._last_window_session = None
|
||||
self.did_load = False
|
||||
if not os.path.exists(self._base_path):
|
||||
os.mkdir(self._base_path)
|
||||
|
||||
def _get_session_path(self, name, check_exists=False):
|
||||
"""Get the session path based on a session name or absolute path.
|
||||
@@ -100,6 +108,11 @@ class SessionManager(QObject):
|
||||
if os.path.isabs(path) and ((not check_exists) or
|
||||
os.path.exists(path)):
|
||||
return path
|
||||
elif self._base_path is None:
|
||||
if check_exists:
|
||||
raise SessionNotFoundError(name)
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
path = os.path.join(self._base_path, name + '.yml')
|
||||
if check_exists and not os.path.exists(path):
|
||||
@@ -136,21 +149,23 @@ class SessionManager(QObject):
|
||||
if item.originalUrl() != item.url():
|
||||
encoded = item.originalUrl().toEncoded()
|
||||
item_data['original-url'] = bytes(encoded).decode('ascii')
|
||||
user_data = item.userData()
|
||||
|
||||
if history.currentItemIndex() == idx:
|
||||
item_data['active'] = True
|
||||
if user_data is None:
|
||||
pos = tab.page().mainFrame().scrollPosition()
|
||||
data['zoom'] = tab.zoomFactor()
|
||||
data['scroll-pos'] = {'x': pos.x(), 'y': pos.y()}
|
||||
data['history'].append(item_data)
|
||||
|
||||
if user_data is not None:
|
||||
user_data = item.userData()
|
||||
if history.currentItemIndex() == idx:
|
||||
pos = tab.page().mainFrame().scrollPosition()
|
||||
item_data['zoom'] = tab.zoomFactor()
|
||||
item_data['scroll-pos'] = {'x': pos.x(), 'y': pos.y()}
|
||||
elif user_data is not None:
|
||||
if 'zoom' in user_data:
|
||||
data['zoom'] = user_data['zoom']
|
||||
item_data['zoom'] = user_data['zoom']
|
||||
if 'scroll-pos' in user_data:
|
||||
pos = user_data['scroll-pos']
|
||||
data['scroll-pos'] = {'x': pos.x(), 'y': pos.y()}
|
||||
item_data['scroll-pos'] = {'x': pos.x(), 'y': pos.y()}
|
||||
|
||||
data['history'].append(item_data)
|
||||
return data
|
||||
|
||||
def _save_all(self):
|
||||
@@ -173,6 +188,22 @@ class SessionManager(QObject):
|
||||
data['windows'].append(win_data)
|
||||
return data
|
||||
|
||||
def _get_session_name(self, name):
|
||||
"""Helper for save to get the name to save the session to.
|
||||
|
||||
Args:
|
||||
name: The name of the session to save, or the 'default' sentinel
|
||||
object.
|
||||
"""
|
||||
if name is default:
|
||||
name = config.get('general', 'session-default-name')
|
||||
if name is None:
|
||||
if self._current is not None:
|
||||
name = self._current
|
||||
else:
|
||||
name = 'default'
|
||||
return name
|
||||
|
||||
def save(self, name, last_window=False, load_next_time=False):
|
||||
"""Save a named session.
|
||||
|
||||
@@ -186,14 +217,10 @@ class SessionManager(QObject):
|
||||
Return:
|
||||
The name of the saved session.
|
||||
"""
|
||||
if name is default:
|
||||
name = config.get('general', 'session-default-name')
|
||||
if name is None:
|
||||
if self._current is not None:
|
||||
name = self._current
|
||||
else:
|
||||
name = 'default'
|
||||
name = self._get_session_name(name)
|
||||
path = self._get_session_path(name)
|
||||
if path is None:
|
||||
raise SessionError("No data storage configured.")
|
||||
|
||||
log.sessions.debug("Saving session {} to {}...".format(name, path))
|
||||
if last_window:
|
||||
@@ -224,11 +251,25 @@ class SessionManager(QObject):
|
||||
entries = []
|
||||
for histentry in data['history']:
|
||||
user_data = {}
|
||||
|
||||
if 'zoom' in data:
|
||||
# The zoom was accidentally stored in 'data' instead of per-tab
|
||||
# earlier.
|
||||
# See https://github.com/The-Compiler/qutebrowser/issues/728
|
||||
user_data['zoom'] = data['zoom']
|
||||
elif 'zoom' in histentry:
|
||||
user_data['zoom'] = histentry['zoom']
|
||||
|
||||
if 'scroll-pos' in data:
|
||||
# The scroll position was accidentally stored in 'data' instead
|
||||
# of per-tab earlier.
|
||||
# See https://github.com/The-Compiler/qutebrowser/issues/728
|
||||
pos = data['scroll-pos']
|
||||
user_data['scroll-pos'] = QPoint(pos['x'], pos['y'])
|
||||
elif 'scroll-pos' in histentry:
|
||||
pos = histentry['scroll-pos']
|
||||
user_data['scroll-pos'] = QPoint(pos['x'], pos['y'])
|
||||
|
||||
active = histentry.get('active', False)
|
||||
url = QUrl.fromEncoded(histentry['url'].encode('ascii'))
|
||||
if 'original-url' in histentry:
|
||||
@@ -289,6 +330,8 @@ class SessionManager(QObject):
|
||||
def list_sessions(self):
|
||||
"""Get a list of all session names."""
|
||||
sessions = []
|
||||
if self._base_path is None:
|
||||
return sessions
|
||||
for filename in os.listdir(self._base_path):
|
||||
base, ext = os.path.splitext(filename)
|
||||
if ext == '.yml':
|
||||
@@ -308,8 +351,8 @@ class SessionManager(QObject):
|
||||
underline).
|
||||
"""
|
||||
if name.startswith('_') and not force:
|
||||
raise cmdexc.CommandError("{!r} is an internal session, use "
|
||||
"--force to load anyways.".format(name))
|
||||
raise cmdexc.CommandError("{} is an internal session, use --force "
|
||||
"to load anyways.".format(name))
|
||||
old_windows = list(objreg.window_registry.values())
|
||||
try:
|
||||
self.load(name, temp=temp)
|
||||
@@ -323,12 +366,11 @@ class SessionManager(QObject):
|
||||
for win in old_windows:
|
||||
win.close()
|
||||
|
||||
@cmdutils.register(name=['session-save', 'w'],
|
||||
@cmdutils.register(name=['session-save', 'w'], win_id='win_id',
|
||||
completion=[usertypes.Completion.sessions],
|
||||
instance='session-manager')
|
||||
def session_save(self, win_id: {'special': 'win_id'},
|
||||
name: {'type': str}=default, current=False, quiet=False,
|
||||
force=False):
|
||||
def session_save(self, win_id, name: {'type': str}=default, current=False,
|
||||
quiet=False, force=False):
|
||||
"""Save a session.
|
||||
|
||||
Args:
|
||||
@@ -342,8 +384,8 @@ class SessionManager(QObject):
|
||||
if (name is not default and
|
||||
name.startswith('_') and # pylint: disable=no-member
|
||||
not force):
|
||||
raise cmdexc.CommandError("{!r} is an internal session, use "
|
||||
"--force to save anyways.".format(name))
|
||||
raise cmdexc.CommandError("{} is an internal session, use --force "
|
||||
"to save anyways.".format(name))
|
||||
if current:
|
||||
if self._current is None:
|
||||
raise cmdexc.CommandError("No session loaded currently!")
|
||||
@@ -356,7 +398,7 @@ class SessionManager(QObject):
|
||||
.format(e))
|
||||
else:
|
||||
if not quiet:
|
||||
message.info(win_id, "Saved session {!r}.".format(name),
|
||||
message.info(win_id, "Saved session {}.".format(name),
|
||||
immediately=True)
|
||||
|
||||
@cmdutils.register(completion=[usertypes.Completion.sessions],
|
||||
@@ -370,14 +412,12 @@ class SessionManager(QObject):
|
||||
underline).
|
||||
"""
|
||||
if name.startswith('_') and not force:
|
||||
raise cmdexc.CommandError("{!r} is an internal session, use "
|
||||
"--force to delete anyways.".format(
|
||||
name))
|
||||
raise cmdexc.CommandError("{} is an internal session, use --force "
|
||||
"to delete anyways.".format(name))
|
||||
try:
|
||||
self.delete(name)
|
||||
except SessionNotFoundError as e:
|
||||
log.sessions.exception("Session not found!")
|
||||
raise cmdexc.CommandError("Session {} not found".format(e))
|
||||
except SessionNotFoundError:
|
||||
raise cmdexc.CommandError("Session {} not found!".format(name))
|
||||
except (OSError, SessionError) as e:
|
||||
log.sessions.exception("Error while deleting session!")
|
||||
raise cmdexc.CommandError("Error while deleting session: {}"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user