Compare commits
796 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ffd3edb5d | ||
|
|
49b9144cf0 | ||
|
|
0a882739c8 | ||
|
|
14205ae14f | ||
|
|
727b418d8b | ||
|
|
b3adbec23e | ||
|
|
689f87d596 | ||
|
|
048d8ef794 | ||
|
|
53afa4275c | ||
|
|
e24c723700 | ||
|
|
3b59c35b35 | ||
|
|
a5c1903247 | ||
|
|
40057c26f5 | ||
|
|
94f2b6bdb8 | ||
|
|
df3e41db9b | ||
|
|
d817818fb8 | ||
|
|
45da910d23 | ||
|
|
d1093d1c1d | ||
|
|
ea026b5ecb | ||
|
|
e102551f9f | ||
|
|
b9e3d3cab9 | ||
|
|
274b66ec46 | ||
|
|
ae32b79d54 | ||
|
|
e80e695a56 | ||
|
|
0a31e19eda | ||
|
|
0f037fb415 | ||
|
|
85cc1e4f84 | ||
|
|
42a3622906 | ||
|
|
a0f36c5cbf | ||
|
|
dfafab4cff | ||
|
|
8cf22c85e0 | ||
|
|
4d1e56a8c6 | ||
|
|
8c11c516b4 | ||
|
|
857288b283 | ||
|
|
24e93fe023 | ||
|
|
8115e109db | ||
|
|
6abe8f2c97 | ||
|
|
7a9183d0b7 | ||
|
|
ba362de2c0 | ||
|
|
d03b03f7cb | ||
|
|
7e8c741937 | ||
|
|
641f7eb3c5 | ||
|
|
38791a2386 | ||
|
|
b8fb5d4590 | ||
|
|
f65f3db747 | ||
|
|
5add2cd930 | ||
|
|
0af8eec73a | ||
|
|
49be92e047 | ||
|
|
ec0bbe67f8 | ||
|
|
e6e28c846f | ||
|
|
efc4eb9069 | ||
|
|
26d6cf8ef6 | ||
|
|
05531ddcf0 | ||
|
|
1c8917b10e | ||
|
|
c33c019075 | ||
|
|
564cd3732b | ||
|
|
1d91a3ac66 | ||
|
|
f6f713bbfe | ||
|
|
6ca11ed95b | ||
|
|
41303ecfcf | ||
|
|
9a14574c9f | ||
|
|
d861c097b1 | ||
|
|
77fe2e1c85 | ||
|
|
ad19833e34 | ||
|
|
eca08f064b | ||
|
|
c3455d9082 | ||
|
|
f7ae7e7d40 | ||
|
|
e857400c2c | ||
|
|
dbd4ce48e6 | ||
|
|
a7af5195d1 | ||
|
|
e9c78b29ed | ||
|
|
96defc5dc2 | ||
|
|
a804300dc0 | ||
|
|
2f612aa6df | ||
|
|
aed964d9f5 | ||
|
|
8519aa940f | ||
|
|
ea4ee6f00b | ||
|
|
1536843f33 | ||
|
|
61da5d0c7c | ||
|
|
3312c221c4 | ||
|
|
85a9f6a08a | ||
|
|
c43d173197 | ||
|
|
ee2c765859 | ||
|
|
6f1232e621 | ||
|
|
324966cfe7 | ||
|
|
521268a1f7 | ||
|
|
54ca9b34e5 | ||
|
|
1bc3d444b6 | ||
|
|
876aa5a9b1 | ||
|
|
87778277e0 | ||
|
|
81b3ef937e | ||
|
|
8a4bba11ed | ||
|
|
3f923b41e0 | ||
|
|
158ccd7d54 | ||
|
|
228ca732d5 | ||
|
|
b3790f7a7e | ||
|
|
d6554a131e | ||
|
|
1f19db0785 | ||
|
|
e1bc5389a2 | ||
|
|
da0a6305df | ||
|
|
6c9e23af4a | ||
|
|
13f765a000 | ||
|
|
fc19262eaa | ||
|
|
f2f481d991 | ||
|
|
f5e69b2174 | ||
|
|
e6e844b039 | ||
|
|
a02c25dfb1 | ||
|
|
d2254ca48b | ||
|
|
66fc3a30dd | ||
|
|
0864ad4069 | ||
|
|
9a5439e5d0 | ||
|
|
7a7e04a054 | ||
|
|
d961eab1d2 | ||
|
|
5a7869f2fe | ||
|
|
62d8b5b574 | ||
|
|
c87757a913 | ||
|
|
9f5ca475c9 | ||
|
|
e7a300865c | ||
|
|
4887385bdd | ||
|
|
1000a1eac2 | ||
|
|
e5405f0ae9 | ||
|
|
a73a778b9d | ||
|
|
7c4eaa80b0 | ||
|
|
c3b76d1d01 | ||
|
|
fa0e8c1b51 | ||
|
|
da8f76d082 | ||
|
|
91c0aae05b | ||
|
|
3399f2df96 | ||
|
|
2029f52fdc | ||
|
|
663d1a4d2f | ||
|
|
7b7e0c93f5 | ||
|
|
e2ef39e872 | ||
|
|
7654467f36 | ||
|
|
b1b06fcb43 | ||
|
|
746c2986f0 | ||
|
|
f4386fa9ea | ||
|
|
07cf2f5b60 | ||
|
|
4dddc07753 | ||
|
|
11fce30ed0 | ||
|
|
1335fccba1 | ||
|
|
e4e982c0a7 | ||
|
|
7592186181 | ||
|
|
868cd115be | ||
|
|
cec63ea449 | ||
|
|
e5b6552568 | ||
|
|
a0adee55c9 | ||
|
|
389c1d11a0 | ||
|
|
b67733b781 | ||
|
|
bcc4bd22ee | ||
|
|
cb2881e0d7 | ||
|
|
1541088e76 | ||
|
|
5435609552 | ||
|
|
c37134861e | ||
|
|
772654bcae | ||
|
|
68a7387b6b | ||
|
|
fd4ae2fabe | ||
|
|
a1a5885367 | ||
|
|
a6db700886 | ||
|
|
d5dcec4320 | ||
|
|
088c7b235d | ||
|
|
6faff11243 | ||
|
|
2f97a597a4 | ||
|
|
e066f83a7c | ||
|
|
5b5657b0d5 | ||
|
|
3b0c8e46a3 | ||
|
|
35a1e118f8 | ||
|
|
093f07f552 | ||
|
|
b61c99687d | ||
|
|
ad7e080827 | ||
|
|
67c67db230 | ||
|
|
c328d54ebe | ||
|
|
a6b314ae91 | ||
|
|
d42934af08 | ||
|
|
e7659cea63 | ||
|
|
141afff0c6 | ||
|
|
ddfbe255e7 | ||
|
|
69abc9a1a1 | ||
|
|
3bf89bcea4 | ||
|
|
c5b7ed350e | ||
|
|
a5447e0e94 | ||
|
|
4d026bde6f | ||
|
|
6943677a90 | ||
|
|
37d3cf1e9c | ||
|
|
21dba1ffda | ||
|
|
82a0c94573 | ||
|
|
72f0820204 | ||
|
|
33ba47797d | ||
|
|
b7c1d7fe37 | ||
|
|
263d298449 | ||
|
|
e5fbb9f68a | ||
|
|
54011782c8 | ||
|
|
b3749df009 | ||
|
|
4186577928 | ||
|
|
c020160f75 | ||
|
|
4ea957b68b | ||
|
|
05e73872b6 | ||
|
|
be95d6f505 | ||
|
|
b954fd4b15 | ||
|
|
6e90465cdd | ||
|
|
98c82859b5 | ||
|
|
8964845c18 | ||
|
|
c7f57bc111 | ||
|
|
09a5ef8140 | ||
|
|
9f5a5a12ba | ||
|
|
f052eff038 | ||
|
|
6e23a6b958 | ||
|
|
cbd9e36e0f | ||
|
|
12a7aaed95 | ||
|
|
2b0e89ab14 | ||
|
|
b63e06561d | ||
|
|
49b6a512c2 | ||
|
|
8949afc2bf | ||
|
|
e6dd05a2c7 | ||
|
|
b5d1614c48 | ||
|
|
bfae6b357a | ||
|
|
4dcba2343d | ||
|
|
e36b6b13e0 | ||
|
|
2934f4a1ca | ||
|
|
8376278961 | ||
|
|
a1fcdbcfd1 | ||
|
|
1c8d470bd7 | ||
|
|
b7929ef747 | ||
|
|
c08c740793 | ||
|
|
649c9f37cd | ||
|
|
3999802c71 | ||
|
|
cf8dbd8bfd | ||
|
|
7f69920158 | ||
|
|
addd2e74ce | ||
|
|
f034abe6a1 | ||
|
|
1ba2e3e24b | ||
|
|
315ed519ee | ||
|
|
20763a87c0 | ||
|
|
c4add62301 | ||
|
|
50fa7743ba | ||
|
|
3bffe1ccf8 | ||
|
|
bd26498f6e | ||
|
|
829a6855a3 | ||
|
|
ca8d3903e3 | ||
|
|
11d8df0e3e | ||
|
|
00199ae60e | ||
|
|
de127497a2 | ||
|
|
292d92d02e | ||
|
|
c9fddfe458 | ||
|
|
7fdeeb25b7 | ||
|
|
6022fd2b26 | ||
|
|
1d19a2ebbb | ||
|
|
7d11c3aa43 | ||
|
|
96739d0013 | ||
|
|
dcb1191f93 | ||
|
|
cf3c200fcb | ||
|
|
a663c857cc | ||
|
|
c3771ca2b2 | ||
|
|
c73cb7efad | ||
|
|
22f05e3ca5 | ||
|
|
9161aa6e17 | ||
|
|
313cc42d8a | ||
|
|
d486a056e7 | ||
|
|
7ce7176475 | ||
|
|
1b48b67443 | ||
|
|
486a92a710 | ||
|
|
ec88c15390 | ||
|
|
40b1be6925 | ||
|
|
41b5fca9b7 | ||
|
|
b0325e17b7 | ||
|
|
7949335a2b | ||
|
|
6e954a1596 | ||
|
|
88f2873a79 | ||
|
|
5d38d28fee | ||
|
|
d32d541ac0 | ||
|
|
cc497bf2ea | ||
|
|
9725d9ce33 | ||
|
|
1531961aeb | ||
|
|
4e6fede40d | ||
|
|
91b4106dcf | ||
|
|
a13618fe2a | ||
|
|
8bf7cb539a | ||
|
|
d96a98a058 | ||
|
|
5e6002fcdc | ||
|
|
11b957f24b | ||
|
|
4614ad5063 | ||
|
|
6e48648033 | ||
|
|
d541634a7c | ||
|
|
b1506274c5 | ||
|
|
c6a1d729f4 | ||
|
|
596041c40e | ||
|
|
9b795c5257 | ||
|
|
89f4333df1 | ||
|
|
59e5a2a6f1 | ||
|
|
0c0d204fd4 | ||
|
|
999513d5d8 | ||
|
|
d059197bc9 | ||
|
|
0e9159e8e8 | ||
|
|
3d53d0d2c5 | ||
|
|
eb6478dd3e | ||
|
|
8cc3804119 | ||
|
|
513591348c | ||
|
|
456fdc55cc | ||
|
|
7e31897dcc | ||
|
|
5147fc832c | ||
|
|
ec6c5ebb69 | ||
|
|
34d054e8a5 | ||
|
|
e0213e7447 | ||
|
|
cee88cd7ca | ||
|
|
5635639ed4 | ||
|
|
982a42d453 | ||
|
|
dc203d46de | ||
|
|
62e86e1482 | ||
|
|
9f9a5d576b | ||
|
|
46329b0f03 | ||
|
|
454d026fdc | ||
|
|
e30e08a04e | ||
|
|
7858bb97d1 | ||
|
|
8c1080de97 | ||
|
|
d37b2713cf | ||
|
|
41a092cd80 | ||
|
|
0fcbc209bb | ||
|
|
1578a4836c | ||
|
|
f34d1b6ce1 | ||
|
|
4cc2b919fa | ||
|
|
5e99da5459 | ||
|
|
5543e75abd | ||
|
|
6fc3546923 | ||
|
|
52c44d3da6 | ||
|
|
b3c95c1668 | ||
|
|
f78410ef2b | ||
|
|
28fce9a7cb | ||
|
|
3392d82f50 | ||
|
|
c33a887b2d | ||
|
|
cd56b97e7d | ||
|
|
12e0edbcd0 | ||
|
|
40e391e199 | ||
|
|
17cfb0d39c | ||
|
|
7162f15348 | ||
|
|
74ea696a5c | ||
|
|
2f76ef1e53 | ||
|
|
26e37739e2 | ||
|
|
e895ee8bdc | ||
|
|
29ad252278 | ||
|
|
db1287cb82 | ||
|
|
71ad8bdb47 | ||
|
|
bc37af7c95 | ||
|
|
3bf02aa62f | ||
|
|
ae06c2cdb7 | ||
|
|
89f019b710 | ||
|
|
228c86c799 | ||
|
|
c551e29f4c | ||
|
|
7596dbdd43 | ||
|
|
616371a3c1 | ||
|
|
725a133b9a | ||
|
|
f60c42b7fb | ||
|
|
9dc9ec3435 | ||
|
|
e7a779ef90 | ||
|
|
5ec3f1949a | ||
|
|
5df4128e8d | ||
|
|
47446baf29 | ||
|
|
749b29e599 | ||
|
|
9a85796ac3 | ||
|
|
a31542269a | ||
|
|
36ddf3a328 | ||
|
|
d162e01422 | ||
|
|
6573a4d616 | ||
|
|
b0d1a137da | ||
|
|
13249329f7 | ||
|
|
19242b6cb2 | ||
|
|
c7a9792b67 | ||
|
|
23bfe6daa2 | ||
|
|
ab50ad735b | ||
|
|
7130f863cb | ||
|
|
547edb3c4e | ||
|
|
77c8575a88 | ||
|
|
4966a70709 | ||
|
|
046c0a7ea2 | ||
|
|
d208c48532 | ||
|
|
dacca0d2ed | ||
|
|
6ccd69dad2 | ||
|
|
d8569a36f9 | ||
|
|
44d26f77a5 | ||
|
|
6eadff4a10 | ||
|
|
6784356d9d | ||
|
|
b0105b0157 | ||
|
|
553a91c012 | ||
|
|
b0f83615b6 | ||
|
|
0c674c7110 | ||
|
|
71d55e9213 | ||
|
|
95093b82c9 | ||
|
|
538b4fafdb | ||
|
|
563afb277d | ||
|
|
4bb048cb6a | ||
|
|
ae04da7b78 | ||
|
|
0642695c7d | ||
|
|
7a297e2e3f | ||
|
|
bc9f178a08 | ||
|
|
262cea8e75 | ||
|
|
dc7f39514d | ||
|
|
72c2962908 | ||
|
|
cbf95d76bd | ||
|
|
0e756d2f68 | ||
|
|
48c44e1b4d | ||
|
|
20efaeff19 | ||
|
|
d194a8ddb0 | ||
|
|
c8d539a4c6 | ||
|
|
8531f89ca3 | ||
|
|
4fee9b9637 | ||
|
|
89a1c43b4c | ||
|
|
a39858c436 | ||
|
|
b718c8b43a | ||
|
|
b28bfbe8f5 | ||
|
|
70c7a42a63 | ||
|
|
648b1219a2 | ||
|
|
d005088b6b | ||
|
|
906da44d70 | ||
|
|
ca48f9f100 | ||
|
|
d0b2360745 | ||
|
|
f1b481dcc7 | ||
|
|
431a52da6c | ||
|
|
b109fc569f | ||
|
|
fcd98443f9 | ||
|
|
e455d8c13a | ||
|
|
d4e5a2eb7f | ||
|
|
a4424f2863 | ||
|
|
9e33f1a275 | ||
|
|
470c392ee0 | ||
|
|
b9fc068af5 | ||
|
|
7762017f00 | ||
|
|
2ba40acf74 | ||
|
|
388c155ebb | ||
|
|
adf2f9860d | ||
|
|
f528a5dd21 | ||
|
|
f0f1a4a1d1 | ||
|
|
ae295a7f65 | ||
|
|
2663feea2f | ||
|
|
68794cc2e2 | ||
|
|
acdf0a1c60 | ||
|
|
626abd3c83 | ||
|
|
979b7cfaba | ||
|
|
1e5b325ea5 | ||
|
|
2b6b4e82a7 | ||
|
|
80778a9ff3 | ||
|
|
6eb8284fe0 | ||
|
|
49bdcd5a97 | ||
|
|
106e591a36 | ||
|
|
a70f864ff5 | ||
|
|
4932cc4d24 | ||
|
|
2b5f133726 | ||
|
|
5436e27e41 | ||
|
|
4adf10a2f2 | ||
|
|
b80fa7a197 | ||
|
|
3cc790afb3 | ||
|
|
91aa9f6c0c | ||
|
|
d6cacdb42f | ||
|
|
19554ba4a1 | ||
|
|
d16d9e403a | ||
|
|
432d666d25 | ||
|
|
1c3ee0db20 | ||
|
|
bdc0c0ddc1 | ||
|
|
b9c8a79f10 | ||
|
|
801e9e0334 | ||
|
|
199eac2db8 | ||
|
|
cfa5ee2835 | ||
|
|
9c6437b3b9 | ||
|
|
486488e2cd | ||
|
|
b4f877d991 | ||
|
|
92aedf84f5 | ||
|
|
6825dfb8d8 | ||
|
|
d6c6014b85 | ||
|
|
c1ac1d702f | ||
|
|
9e50b7afcc | ||
|
|
1388880e7b | ||
|
|
30d60ea740 | ||
|
|
28cac01a1f | ||
|
|
1689cb09f8 | ||
|
|
286c71a48a | ||
|
|
c073234a8d | ||
|
|
8c286412cb | ||
|
|
b3cef948b0 | ||
|
|
fa41af63b6 | ||
|
|
e789296b7f | ||
|
|
bc9a8dd63f | ||
|
|
6640768860 | ||
|
|
3f1041eb0a | ||
|
|
c94ea5f8d4 | ||
|
|
e2d249541d | ||
|
|
2de6428830 | ||
|
|
c2472d88f1 | ||
|
|
8c16d81d89 | ||
|
|
1d2dd5bf55 | ||
|
|
537aa22d64 | ||
|
|
30d3612a17 | ||
|
|
178eeaed0d | ||
|
|
f1967718b7 | ||
|
|
cbb246fd0b | ||
|
|
646e92707a | ||
|
|
1021c3a330 | ||
|
|
ec57e58530 | ||
|
|
06a8a68fcb | ||
|
|
4a78519b63 | ||
|
|
d2207f66f1 | ||
|
|
23d4d72f3b | ||
|
|
4a93389356 | ||
|
|
3704e3ddd5 | ||
|
|
563d9bd097 | ||
|
|
643bf4bc20 | ||
|
|
b89b38fd3c | ||
|
|
c5334fb683 | ||
|
|
0829511221 | ||
|
|
48b865073c | ||
|
|
3f9099613b | ||
|
|
d705e600e2 | ||
|
|
77fa0730c8 | ||
|
|
46e4aeb3e9 | ||
|
|
ed76d689b0 | ||
|
|
ca311f8588 | ||
|
|
849e427231 | ||
|
|
9e628901e9 | ||
|
|
28126055da | ||
|
|
3d75d86123 | ||
|
|
03ea07e99f | ||
|
|
780ced8a52 | ||
|
|
fc33b065c2 | ||
|
|
03b7459b00 | ||
|
|
f964bf1b67 | ||
|
|
ef2a2702f5 | ||
|
|
6374b6dd4c | ||
|
|
945bc44550 | ||
|
|
69d642cab8 | ||
|
|
62aa9bdbb3 | ||
|
|
e35c91043e | ||
|
|
fac546e9b4 | ||
|
|
b74ddc3493 | ||
|
|
b7964d9baf | ||
|
|
76dbfa7305 | ||
|
|
feb2f99ea9 | ||
|
|
d0d5ad2eda | ||
|
|
eb18f0a2ac | ||
|
|
39c08cb582 | ||
|
|
164ea98a5b | ||
|
|
3b2c0823af | ||
|
|
9ad6cef369 | ||
|
|
423192e9c9 | ||
|
|
79823a9a0b | ||
|
|
1c0616f3ec | ||
|
|
bca07eebba | ||
|
|
599a3d75a4 | ||
|
|
707fc1176d | ||
|
|
cb8a75577e | ||
|
|
3d5d679561 | ||
|
|
6151d077e5 | ||
|
|
d438aa15fa | ||
|
|
7f5a79cdfd | ||
|
|
5bc794f85a | ||
|
|
d7455bcdba | ||
|
|
0b667e4701 | ||
|
|
d606cd5550 | ||
|
|
7c1de99876 | ||
|
|
2789bec1e7 | ||
|
|
1ccb464d1c | ||
|
|
758ea8b171 | ||
|
|
32568a6da4 | ||
|
|
14792472db | ||
|
|
005fa8b675 | ||
|
|
c7e5033eaa | ||
|
|
a828851640 | ||
|
|
b873cfb18a | ||
|
|
046a3dc159 | ||
|
|
68b707c749 | ||
|
|
9cff0e7367 | ||
|
|
7e66c3cf46 | ||
|
|
bf4aab79ac | ||
|
|
a6f6fdf19b | ||
|
|
e5ffcbd49f | ||
|
|
730347e449 | ||
|
|
55112b52e0 | ||
|
|
2249a88e3a | ||
|
|
6dbd6d1ddf | ||
|
|
6ecea8ef17 | ||
|
|
021bb25622 | ||
|
|
22e887045b | ||
|
|
4896765fcc | ||
|
|
d98590d712 | ||
|
|
496e6fc624 | ||
|
|
55a818b156 | ||
|
|
85be0f2801 | ||
|
|
b7b4cc7f31 | ||
|
|
056a901da0 | ||
|
|
ba9b166962 | ||
|
|
edf2652431 | ||
|
|
6755e17630 | ||
|
|
d4899240de | ||
|
|
ddbb6b5198 | ||
|
|
1087ce075d | ||
|
|
f237a87ad0 | ||
|
|
cecb79cf05 | ||
|
|
d4ea1df232 | ||
|
|
91ca7d0911 | ||
|
|
68e3ad6cba | ||
|
|
12a405965a | ||
|
|
a85ac1725f | ||
|
|
eb5684e5f7 | ||
|
|
b9bcad9c14 | ||
|
|
64b01cc076 | ||
|
|
1380fef600 | ||
|
|
8809ef02a1 | ||
|
|
3f37fcf8fa | ||
|
|
c8db9e1c76 | ||
|
|
01d8314dd8 | ||
|
|
fa21d280fa | ||
|
|
e211801e16 | ||
|
|
6db1ab0a58 | ||
|
|
aa70395925 | ||
|
|
880b33fff5 | ||
|
|
f1789effdc | ||
|
|
e095f64eb6 | ||
|
|
06df88075e | ||
|
|
00bdb60627 | ||
|
|
1fc0abb064 | ||
|
|
a1776087e0 | ||
|
|
948866f4f2 | ||
|
|
e2250d65e9 | ||
|
|
bb0c79b5a2 | ||
|
|
ff299c87a8 | ||
|
|
2d2bdad2ca | ||
|
|
07d043fe81 | ||
|
|
477da6002a | ||
|
|
d2c01d7ee6 | ||
|
|
b67a031151 | ||
|
|
a9a7f5da45 | ||
|
|
b169a1c802 | ||
|
|
764e79e505 | ||
|
|
7b7faa9f66 | ||
|
|
a6b92dbbd3 | ||
|
|
4fb940241c | ||
|
|
51f9464eb2 | ||
|
|
0645865d22 | ||
|
|
991ba54499 | ||
|
|
1cf3d66a22 | ||
|
|
300d873b18 | ||
|
|
a8bbd5fa4d | ||
|
|
2d655a7230 | ||
|
|
4d7f8e4878 | ||
|
|
bc885cc9ee | ||
|
|
a5dc8a3025 | ||
|
|
d6463d5ade | ||
|
|
7601e58c81 | ||
|
|
16bda94e2b | ||
|
|
5a20052bce | ||
|
|
e316f1768e | ||
|
|
11696f0073 | ||
|
|
f9d976880e | ||
|
|
a5f1022330 | ||
|
|
85d3d4baba | ||
|
|
561295238d | ||
|
|
0e670a597e | ||
|
|
0fd3674d9e | ||
|
|
0ee9d73fe2 | ||
|
|
81827a3150 | ||
|
|
32145d579b | ||
|
|
f230fd3abb | ||
|
|
51318d66c8 | ||
|
|
59602ec5b5 | ||
|
|
a374698693 | ||
|
|
b56988f0a4 | ||
|
|
32df91fbae | ||
|
|
f6c00babbe | ||
|
|
7eaad59be3 | ||
|
|
ea1e52ea7c | ||
|
|
b588f54a53 | ||
|
|
9031b3e535 | ||
|
|
1162e640c5 | ||
|
|
0ea7a1457d | ||
|
|
f5d7605ae0 | ||
|
|
e50068021d | ||
|
|
460bd86579 | ||
|
|
e43f0a61b9 | ||
|
|
8eb4d15805 | ||
|
|
3b7e1b3fe2 | ||
|
|
650aa532cd | ||
|
|
1f3fc756db | ||
|
|
2d5d485daf | ||
|
|
b77e43d74f | ||
|
|
5a26858e07 | ||
|
|
99ea4b98e8 | ||
|
|
da8b6fb50a | ||
|
|
33066af51d | ||
|
|
6a971e2846 | ||
|
|
f28a39571c | ||
|
|
bee04a1eec | ||
|
|
39d25c1127 | ||
|
|
07e831cee5 | ||
|
|
b3342d8f70 | ||
|
|
6465d64738 | ||
|
|
232fd19422 | ||
|
|
a4530797ea | ||
|
|
1b84bbd61d | ||
|
|
f2864c6253 | ||
|
|
8ae3047f2a | ||
|
|
b154846bdc | ||
|
|
8a3d9c0c01 | ||
|
|
62d30fe589 | ||
|
|
a52d18b700 | ||
|
|
b7159d780a | ||
|
|
f9e702bae5 | ||
|
|
f57e47c742 | ||
|
|
18146e2fbc | ||
|
|
f7074b80d0 | ||
|
|
fa282d574d | ||
|
|
3b0b4ffe66 | ||
|
|
a6ce188e0d | ||
|
|
01845faac5 | ||
|
|
69a013bc82 | ||
|
|
6f8eb419ae | ||
|
|
1e4b80d1ac | ||
|
|
5dbda3016b | ||
|
|
1d25b212d5 | ||
|
|
f538fc8b74 | ||
|
|
c2b995edde | ||
|
|
1d562d919e | ||
|
|
a60bae30b7 | ||
|
|
523502785a | ||
|
|
84c7c37e8e | ||
|
|
d232b3ea57 | ||
|
|
7a861b7119 | ||
|
|
a22f973c99 | ||
|
|
724e531087 | ||
|
|
5c73910a33 | ||
|
|
84bae210ab | ||
|
|
e5edc0f940 | ||
|
|
0d21265005 | ||
|
|
c0fdf19756 | ||
|
|
102b2be361 | ||
|
|
fac0f66e52 | ||
|
|
2563ecf6d8 | ||
|
|
64530375ab | ||
|
|
c9f6cd507b | ||
|
|
7278b7c2e5 | ||
|
|
35beff98a9 | ||
|
|
a6e94cf30c | ||
|
|
8e01353a94 | ||
|
|
8b9c6ccee2 | ||
|
|
b88ac51d25 | ||
|
|
73517f0a51 | ||
|
|
40364ce774 | ||
|
|
27966c94a6 | ||
|
|
a7b6d179d4 | ||
|
|
1c9598d2c0 | ||
|
|
dcd6bcd2f4 | ||
|
|
c590648077 | ||
|
|
80843c0b53 | ||
|
|
14d6e737fa | ||
|
|
9c613fb700 | ||
|
|
01aa1f755d | ||
|
|
3855d49821 | ||
|
|
55c24cad9a | ||
|
|
38bb3673db | ||
|
|
455f6b8a70 | ||
|
|
8c5b7bcd03 | ||
|
|
a6885a0d41 | ||
|
|
9941812127 | ||
|
|
990c0707f4 | ||
|
|
c03ef10d54 | ||
|
|
d72691ee49 | ||
|
|
29eadf7141 | ||
|
|
27c2650245 | ||
|
|
b0bf02e23a | ||
|
|
30ab1d0218 | ||
|
|
7f68affa30 | ||
|
|
b6e29d8eae | ||
|
|
591883656e | ||
|
|
f0a649e101 | ||
|
|
d0342bffc4 | ||
|
|
75ab8f077d | ||
|
|
d9f7d401c6 | ||
|
|
996561b50e | ||
|
|
4cf0311d7f | ||
|
|
cf4e472461 | ||
|
|
0ce94dae1c | ||
|
|
7e3c966afe | ||
|
|
1672995639 | ||
|
|
4a78b0519d | ||
|
|
46533c3367 | ||
|
|
7c2802e843 | ||
|
|
5992688926 | ||
|
|
a730290d40 | ||
|
|
f7bcdfc818 | ||
|
|
16218a9900 | ||
|
|
e169e2165d | ||
|
|
42ac3dcda0 | ||
|
|
872cff2ae1 | ||
|
|
620a966d1e | ||
|
|
b791dd3eb4 | ||
|
|
8a3049f09b | ||
|
|
9b8a182a78 | ||
|
|
f94e12008a | ||
|
|
e38df261cb | ||
|
|
fbb78f1133 | ||
|
|
6214c38d7e | ||
|
|
d04fc29163 |
@@ -5,15 +5,15 @@ cache:
|
||||
build: off
|
||||
environment:
|
||||
PYTHONUNBUFFERED: 1
|
||||
PYTHON: C:\Python36\python.exe
|
||||
PYTHON: C:\Python36-x64\python.exe
|
||||
matrix:
|
||||
- TESTENV: py36-pyqt510
|
||||
- TESTENV: py36-pyqt511
|
||||
- TESTENV: pylint
|
||||
|
||||
install:
|
||||
- '%PYTHON% -m pip install -U pip'
|
||||
- '%PYTHON% -m pip install -r misc\requirements\requirements-tox.txt'
|
||||
- 'set PATH=%PATH%;C:\Python36'
|
||||
- 'set PATH=C:\Python36-x64;%PATH'
|
||||
|
||||
test_script:
|
||||
- '%PYTHON% -m tox -e %TESTENV%'
|
||||
|
||||
12
.flake8
@@ -32,7 +32,7 @@ exclude = .*,__pycache__,resources.py
|
||||
# D403: First word of the first line should be properly capitalized
|
||||
# (false-positives)
|
||||
# D413: Missing blank line after last section (not in pep257?)
|
||||
# A003: Builtin name for class attribute (needed for attrs)
|
||||
# A003: Builtin name for class attribute (needed for overridden methods)
|
||||
ignore =
|
||||
B001,B008,B305,
|
||||
E128,E226,E265,E501,E402,E266,E722,E731,
|
||||
@@ -44,11 +44,11 @@ ignore =
|
||||
min-version = 3.4.0
|
||||
max-complexity = 12
|
||||
per-file-ignores =
|
||||
tests/*/test_*.py : D100,D101,D401
|
||||
tests/unit/browser/test_history.py : N806
|
||||
tests/helpers/fixtures.py : N806
|
||||
tests/unit/browser/webkit/http/test_content_disposition.py : D400
|
||||
scripts/dev/ci/appveyor_install.py : FI53
|
||||
/tests/**/*.py : D100,D101,D401
|
||||
/tests/unit/browser/test_history.py : N806
|
||||
/tests/helpers/fixtures.py : N806
|
||||
/tests/unit/browser/webkit/http/test_content_disposition.py : D400
|
||||
/scripts/dev/ci/appveyor_install.py : FI53
|
||||
copyright-check = True
|
||||
copyright-regexp = # Copyright [\d-]+ .*
|
||||
copyright-min-file-size = 110
|
||||
|
||||
5
.github/CONTRIBUTING.asciidoc
vendored
@@ -1,3 +1,8 @@
|
||||
IMPORTANT: I'm currently (July 2018) more busy than usual until September,
|
||||
because of exams coming up. Review of non-trivial pull requests will thus be
|
||||
delayed until then. If you're reading this note after mid-September, please
|
||||
open an issue.
|
||||
|
||||
- Before you start to work on something, please leave a comment on the relevant
|
||||
issue (or open one). This makes sure there is no duplicate work done.
|
||||
|
||||
|
||||
1
.gitignore
vendored
@@ -41,3 +41,4 @@ TODO
|
||||
/scripts/testbrowser/cpp/webengine/testbrowser
|
||||
/scripts/testbrowser/cpp/webengine/.qmake.stash
|
||||
/scripts/dev/pylint_checkers/qute_pylint.egg-info
|
||||
/misc/file_version_info.txt
|
||||
|
||||
23
.travis.yml
@@ -18,20 +18,19 @@ matrix:
|
||||
python: 3.5
|
||||
env: TESTENV=py35-pyqt571
|
||||
- os: linux
|
||||
env: TESTENV=py36-pyqt59-cov
|
||||
env: TESTENV=py36-pyqt59
|
||||
- os: linux
|
||||
env: TESTENV=py36-pyqt510
|
||||
# We need a newer Xvfb as a WORKAROUND for:
|
||||
# https://bugreports.qt.io/browse/QTBUG-64928
|
||||
- os: linux
|
||||
env: TESTENV=py36-pyqt511-cov
|
||||
# https://github.com/travis-ci/travis-ci/issues/9069
|
||||
- os: linux
|
||||
python: 3.7
|
||||
sudo: required
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- sourceline: "deb http://us.archive.ubuntu.com/ubuntu/ xenial main universe"
|
||||
packages:
|
||||
- xvfb
|
||||
dist: xenial
|
||||
env: TESTENV=py37-pyqt511
|
||||
- os: osx
|
||||
env: TESTENV=py36 OSX=sierra
|
||||
env: TESTENV=py37 OSX=sierra
|
||||
osx_image: xcode9.2
|
||||
language: generic
|
||||
# https://github.com/qutebrowser/qutebrowser/issues/2013
|
||||
@@ -66,6 +65,10 @@ matrix:
|
||||
env: TESTENV=shellcheck
|
||||
services: docker
|
||||
fast_finish: true
|
||||
allow_failures:
|
||||
# https://github.com/qutebrowser/qutebrowser/issues/4055
|
||||
- os: linux
|
||||
env: TESTENV=py36-pyqt510
|
||||
|
||||
cache:
|
||||
directories:
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
recursive-include qutebrowser *.py
|
||||
recursive-include qutebrowser/img *.svg *.png
|
||||
recursive-include qutebrowser/test *.py
|
||||
recursive-include qutebrowser/javascript *.js
|
||||
graft qutebrowser/html
|
||||
graft qutebrowser/3rdparty
|
||||
@@ -8,6 +7,7 @@ graft icons
|
||||
graft doc/img
|
||||
graft misc/apparmor
|
||||
graft misc/userscripts
|
||||
graft misc/requirements
|
||||
recursive-include scripts *.py *.sh *.js
|
||||
include qutebrowser/utils/testfile
|
||||
include qutebrowser/git-commit-id
|
||||
@@ -26,22 +26,18 @@ prune scripts/dev
|
||||
prune scripts/testbrowser/cpp
|
||||
prune .github
|
||||
exclude scripts/asciidoc2html.py
|
||||
exclude doc/notes
|
||||
recursive-exclude doc *.asciidoc
|
||||
include doc/qutebrowser.1.asciidoc
|
||||
include doc/changelog.asciidoc
|
||||
prune tests
|
||||
prune qutebrowser/3rdparty
|
||||
prune misc/requirements
|
||||
prune misc/docker
|
||||
exclude pytest.ini
|
||||
exclude qutebrowser.rcc
|
||||
exclude qutebrowser/javascript/.eslintrc.yaml
|
||||
exclude qutebrowser/javascript/.eslintignore
|
||||
exclude doc/help
|
||||
exclude .*
|
||||
exclude misc/appveyor_install.py
|
||||
exclude misc/qutebrowser.spec
|
||||
exclude misc/qutebrowser.nsi
|
||||
exclude misc/qutebrowser.rcc
|
||||
|
||||
global-exclude __pycache__ *.pyc *.pyo
|
||||
|
||||
@@ -9,8 +9,6 @@ qutebrowser
|
||||
// QUTE_WEB_HIDE
|
||||
image:icons/qutebrowser-64x64.png[qutebrowser logo] *A keyboard-driven, vim-like browser based on PyQt5 and Qt.*
|
||||
|
||||
image:https://img.shields.io/pypi/l/qutebrowser.svg?style=flat["license badge",link="https://github.com/qutebrowser/qutebrowser/blob/master/LICENSE"]
|
||||
image:https://img.shields.io/pypi/v/qutebrowser.svg?style=flat["version badge",link="https://pypi.python.org/pypi/qutebrowser/"]
|
||||
image:https://travis-ci.org/qutebrowser/qutebrowser.svg?branch=master["Build Status", link="https://travis-ci.org/qutebrowser/qutebrowser"]
|
||||
image:https://ci.appveyor.com/api/projects/status/5pyauww2k68bbow2/branch/master?svg=true["AppVeyor build status", link="https://ci.appveyor.com/project/qutebrowser/qutebrowser"]
|
||||
image:https://codecov.io/github/qutebrowser/qutebrowser/coverage.svg?branch=master["coverage badge",link="https://codecov.io/github/qutebrowser/qutebrowser?branch=master"]
|
||||
@@ -99,7 +97,7 @@ Requirements
|
||||
The following software and libraries are required to run qutebrowser:
|
||||
|
||||
* http://www.python.org/[Python] 3.5 or newer (3.6 recommended)
|
||||
* http://qt.io/[Qt] 5.7.1 or newer with the following modules:
|
||||
* http://qt.io/[Qt] 5.7.1 or newer (5.11.1 recommended) with the following modules:
|
||||
- QtCore / qtbase
|
||||
- QtQuick (part of qtbase in some distributions)
|
||||
- QtSQL (part of qtbase in some distributions)
|
||||
@@ -109,7 +107,7 @@ The following software and libraries are required to run qutebrowser:
|
||||
link:https://github.com/annulen/webkit/wiki[updated fork] (5.212) is
|
||||
supported
|
||||
* http://www.riverbankcomputing.com/software/pyqt/intro[PyQt] 5.7.0 or newer
|
||||
(5.9.2 recommended) for Python 3
|
||||
(5.11.2 recommended) for Python 3
|
||||
* https://pypi.python.org/pypi/setuptools/[pkg_resources/setuptools]
|
||||
* http://fdik.org/pyPEG/[pyPEG2]
|
||||
* http://jinja.pocoo.org/[jinja2]
|
||||
|
||||
@@ -15,6 +15,300 @@ breaking changes (such as renamed commands) can happen in minor releases.
|
||||
// `Fixed` for any bug fixes.
|
||||
// `Security` to invite users to upgrade in case of vulnerabilities.
|
||||
|
||||
v1.5.0 (unreleased)
|
||||
-------------------
|
||||
|
||||
Added
|
||||
~~~~~
|
||||
|
||||
- The qute-pass userscript now has optional OTP support.
|
||||
|
||||
v1.4.1 (unreleased)
|
||||
-------------------
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
|
||||
- Rare crash when an error occurs in downloads.
|
||||
- Newlines are now stripped from the :version pastebin URL.
|
||||
- There's a new `mkvenv-pypi-old` environment in `tox.ini` which installs an
|
||||
older Qt, which is needed on Ubuntu 16.04.
|
||||
- Worked around a Qt issue which redirects to a `chrome-error://` page when
|
||||
trying to use U2F.
|
||||
- The `link_pyqt.py` script now works correctly with PyQt 5.11.
|
||||
|
||||
v1.4.0
|
||||
------
|
||||
|
||||
Added
|
||||
~~~~~
|
||||
|
||||
- Support for the bundled `sip` module in PyQt 5.11 and other changes in
|
||||
Qt/PyQt 5.11.x.
|
||||
- New `--debug-flag log-requests` to log requests to the debug log for
|
||||
debugging.
|
||||
- New `--first` flag for `:hint` (bound to `gi` for inputs) which automatically
|
||||
selects the first hint.
|
||||
- New `input.escape_quits_reporter` setting which can be used to avoid
|
||||
accidentally quitting the crash reporter when pressing escape.
|
||||
- New `qute-lastpass` userscript which uses the LastPass CLI to fill passwords.
|
||||
- The Makefile now installs a `/usr/share/metainfo/qutebrowser.appdata.xml` file.
|
||||
- QtWebEngine: Support for printing from webpages via `window.print`.
|
||||
- QtWebEngine: Support for muting tabs:
|
||||
* New `{audio}` field for `window.title_format` and `tabs.title.format` which
|
||||
displays `[M]`/`[A]` for muted/recently audible tabs.
|
||||
* New `:tab-mute` command (bound to `<Alt-m>`) to mute/unmute a tab.
|
||||
- QtWebEngine: Support for `content.cookies.accept` with third-party cookies
|
||||
blocked by default (requires Qt 5.11).
|
||||
- QtWebEngine: New settings:
|
||||
* Support for requesting persistent storage via
|
||||
`navigator.webkitPersistentStorage.requestQuota` with a new
|
||||
`content.persistent_storage` setting (requires Qt 5.11).
|
||||
This setting also supports URL patterns.
|
||||
* Support for registering custom protocol handlers via
|
||||
`navigator.registerProtocolHandler` with a new
|
||||
`content.register_protocol_handler` setting (requires Qt 5.11).
|
||||
This setting also supports URL patterns.
|
||||
* Support for WebRTC screen sharing with a new `content.desktop_capture`
|
||||
setting (requires Qt 5.10).
|
||||
This setting also supports URL patterns.
|
||||
* New `content.autoplay` setting to enable/disable automatic video playback
|
||||
(requires Qt 5.10).
|
||||
* New `content.webrtc_public_interfaces_only` setting to only expose public
|
||||
interfaces over WebRTC (requires Qt 5.9.2 or 5.11).
|
||||
* New `content.canvas_reading` setting to disable reading from canvas
|
||||
elements.
|
||||
|
||||
Changed
|
||||
~~~~~~~
|
||||
|
||||
- The following settings now support URL patterns:
|
||||
* `content.headers.do_not_track`
|
||||
* `content.headers.custom`
|
||||
* `content.headers.accept_language`
|
||||
* `content.headers.user_agent`
|
||||
* `content.ssl_strict`
|
||||
* `content.geolocation`
|
||||
* `content.notifications`
|
||||
* `content.media_capture`
|
||||
- The Windows/macOS releases now bundle Qt 5.11.1 which is based on
|
||||
Chromium 65.0.3325.151 with security fixes up to Chromium 67.0.3396.87.
|
||||
- New short flags for commandline arguments: `-B` and `-T` for `--basedir` and
|
||||
`--temp-basedir`; `-d` and `-D` for `--debug` and `--debug-flag`.
|
||||
- Deleting history items via `:history-clear` or `:completion-item-del` now
|
||||
also removes that URL from QtWebEngine's visited links.
|
||||
- There's now completion for commands taking a variable count of arguments
|
||||
(like `:config-cycle`).
|
||||
- QtWebEngine: On Qt 5.11.1, no reloads are needed anymore when switching
|
||||
between pages with changed settings (e.g. `content.javascript.enabled`).
|
||||
- The `qt.force_software_rendering` setting changed from a boolean to taking
|
||||
different values (`software-opengl`, `qt-quick` and `chromium`) for different
|
||||
kinds of software rendering workarounds.
|
||||
- On Qt 5.11, using wayland with QtWebEngine is now possible when using
|
||||
software rendering.
|
||||
- GreaseMonkey scripts now get their own global scope (based on the page's
|
||||
one), which allows scripts like OneeChan to work.
|
||||
- Rapid hinting is now supported with the `yank` and `yank-primary` targets,
|
||||
copying newline-separated links.
|
||||
- QtWebEngine: On Qt 5.11, the developer tools (inspector) can now be used
|
||||
securely and without requiring the `--enable-webengine-inspector` option.
|
||||
- The `<Enter>` key (`:follow-selected`) now follows the currently focused
|
||||
element if there's no selection.
|
||||
- The `--logfilter` argument now can be prepended with an exclamation mark
|
||||
(e.g. `--logfilter '!init,destroy'`) to invert the filter.
|
||||
- `:view-source` now has a `--pygments` flag which uses the "old" way of
|
||||
rendering sources even with QtWebEngine.
|
||||
- Improved error messages when a setting needs a newer Qt version.
|
||||
- QtWebEngine: Various improvements to make the cursor more visible in caret
|
||||
browsing.
|
||||
- When a prompt is opened in insert/passthrough mode, the mode is restored
|
||||
after closing the prompt.
|
||||
- On Qt 5.10 or newer, dictionaries are now read from the qutebrowser data
|
||||
directory (e.g. `~/.local/share/qutebrowser`) instead of `/usr/share/qt`.
|
||||
Existing dictionaries are copied over.
|
||||
- If an error while parsing `~/.netrc` occurs, the cause of the error is now
|
||||
logged.
|
||||
- On Qt 5.9 or newer, certificate errors now show Chromium's detailed error
|
||||
page.
|
||||
- Greasemonkey scripts now support a "@qute-js-world" tag to run them in a
|
||||
different JavaScript context.
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
|
||||
- Various subtle keyboard focus issues.
|
||||
- The security fix in v1.3.3 caused URLs with ampersands
|
||||
(`www.example.com?one=1&two=2`) to send the wrong arguments when clicked on
|
||||
the `qute://history` page.
|
||||
- Crash when opening a PDF page with PDF.js enabled (on QtWebKit), but no
|
||||
PDF.js installed.
|
||||
- Crash when closing a tab shortly after opening it.
|
||||
|
||||
Removed
|
||||
~~~~~~~
|
||||
|
||||
- No prebuilt binaries for 32-bit Windows are supplied anymore. This is due to
|
||||
Qt removing QtWebEngine support for those upstream. It might be possible to
|
||||
distribute 32-bit binaries again with Qt 5.12 in December, but that will only
|
||||
happen if it turns out enough people actually need 32-bit support.
|
||||
- `:tab-detach` which has been deprecated in v1.1.0 has been removed.
|
||||
- The `content.developer_extras` setting got removed. On QtWebKit, developer
|
||||
extras are now automatically enabled when opening the inspector.
|
||||
|
||||
v1.3.3
|
||||
------
|
||||
|
||||
Security
|
||||
~~~~~~~~
|
||||
|
||||
- An XSS vulnerability on the `qute://history` page allowed websites to inject
|
||||
HTML into the page via a crafted title tag. This could allow them to steal
|
||||
your browsing history. If you're currently unable to upgrade, avoid using
|
||||
`:history`. A CVE request for this issue is pending, see
|
||||
https://github.com/qutebrowser/qutebrowser/issues/4011[#4011] for updates.
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
|
||||
- Crash in a workaround for a Qt 5.11 bug in rare circumstances.
|
||||
- Workaround for a Qt bug which preserves searches between page loads.
|
||||
- In v1.3.2 a dependency on the `PyQt5.QtQuickWidgets` module was accidentally
|
||||
introduced. Since that module isn't packaged everywhere, it's been removed
|
||||
again.
|
||||
|
||||
v1.3.2
|
||||
------
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
|
||||
- QtWebEngine: Improved workaround for a bug in Qt 5.11 where only the
|
||||
top/bottom half of the window is used.
|
||||
- QtWebEngine: Work around a bug in Qt 5.11 where an endless loading-loop is
|
||||
triggered when clicking a link with an unknown scheme.
|
||||
- QtWebEngine: When switching between pages with changed settings, less
|
||||
unnecessary reloads are done now.
|
||||
- QtWebEngine: It's now possible to open external links such as `magnet://` or
|
||||
`mailto:` via hints.
|
||||
|
||||
v1.3.1
|
||||
------
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
|
||||
- Work around a bug in Qt 5.11 where only the top/bottom half of the window is used.
|
||||
This workaround is incomplete, but fixes the majority of the cases where this happens.
|
||||
- Work around keyboard focus issues with Qt 5.11.
|
||||
- Work around an issue in Qt 5.11 where e.g. activating JavaScript per-domain
|
||||
needed a manual reload in some cases.
|
||||
- Don't crash when a ² key is pressed (e.g. on AZERTY keyboards).
|
||||
- Don't crash when a tab is opened and quickly closed again.
|
||||
|
||||
|
||||
v1.3.0
|
||||
------
|
||||
|
||||
Added
|
||||
~~~~~
|
||||
|
||||
- New `:scroll-to-anchor` command to scroll to an anchor in the document.
|
||||
- New `url.open_base_url` option to open the base URL of a searchengine when no
|
||||
search term is given.
|
||||
- New `tabs.min_width` setting to configure the minimal width for tabs.
|
||||
- New userscripts:
|
||||
* `getbib` to download bibtex information for DOIs on a page.
|
||||
* `qute-keepass` to get passwords from KeePassX.
|
||||
|
||||
Changed
|
||||
~~~~~~~
|
||||
|
||||
- QtWebEngine: Support for JavaScript Shared Web Workers have been disabled on
|
||||
Qt versions older than 5.11 because of security issues in in Chromium.
|
||||
You can get the same effect in earlier versions via
|
||||
`:set qt.args ['disable-shared-workers']`. An equivalent workaround is also
|
||||
contained in Qt 5.9.5 and 5.10.1.
|
||||
- The file dialog for downloads now has basic tab completion based on the
|
||||
entered text.
|
||||
- `:version` now shows OS information for POSIX OS other than Linux/macOS.
|
||||
- When there's an error inserting the text from an external editor, a backup
|
||||
file is now saved.
|
||||
- The `window.hide_wayland_decoration` setting got renamed to
|
||||
`window.hide_decoration` and now also works outside of wayland.
|
||||
- The `tabs.favicons.show` setting now can take three values: `'always'` (was
|
||||
`True`), `'never'` (was `False`) and `'pinned'` (to only show favicons for
|
||||
pinned tabs).
|
||||
- Hover tooltips on tabs now always show the webpage's title.
|
||||
- The default value for `content.host_blocking.lists` was changed to only
|
||||
include https://github.com/StevenBlack/hosts[Steven Black's hosts-list] which
|
||||
combines various sources.
|
||||
- Error messages when trying to wrap when `tabs.wrap` is `False` are now logged
|
||||
to debug instead of messages.
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
|
||||
- Using hints before a page is fully loaded is now possible again.
|
||||
- Selecting hints with the number keypad now works again.
|
||||
- Tab titles for tabs loaded from sessions should now really be correct instead
|
||||
of showing the URL.
|
||||
- Loading URLs with customized settings from a session now avoids an additional
|
||||
reload.
|
||||
- The window icon and title now get set correctly again.
|
||||
- The `tabs.switching_delay` setting now has a correct maximum value limit set.
|
||||
- The `taskadd` script now works properly when there's multi-line output.
|
||||
- QtWebEngine: Worked around issues with GreaseMonkey/stylesheets not being
|
||||
loaded correctly in some situations.
|
||||
- The statusbar now more closely reflects the caret mode state.
|
||||
- The icon on Windows should now be displayed in a higher resolution.
|
||||
- The QtWebEngine development tools (inspector) now also work when JavaScript is
|
||||
disabled globally.
|
||||
- Building `.exe` files now works when `upx` is installed on the system.
|
||||
- The keyhint widget now shows the correct text for chained modifiers.
|
||||
- Loading GreaseMonkey scripts now also works with Jinja2 2.8 (e.g. on Debian
|
||||
Stable).
|
||||
- Adding styles with GreaseMonkey on fast sites now works properly.
|
||||
- Window ID 0 is now excluded properly from `:tab-take` completion.
|
||||
- A rare crash when cancelling a download has been fixed.
|
||||
- The Makefile (intended for packagers) now supports `PREFIX` properly.
|
||||
- The workaround for a black window with Nvidia graphics is now enabled on
|
||||
non-Linux systems (like FreeBSD) as well.
|
||||
- Initial support for Qt 5.11.
|
||||
- Checking for a new version after sending a crash report now works properly
|
||||
again.
|
||||
- `@match` in Greasemonkey scripts now more closely matches the proper pattern
|
||||
syntax.
|
||||
- Searching via `/` or `?` now doesn't handle any characters in a special way.
|
||||
- Fixed crash when trying to retry some failed downloads on QtWebEngine.
|
||||
- An invalid spellcheck dictionary filename now doesn't crash anymore.
|
||||
- When no spellcheck dictionaries are configured, it's now disabled internally.
|
||||
This works around an issue with entering special characters on Facebook
|
||||
messenger.
|
||||
- The macOS release now should work again on macOS 10.11 and newer.
|
||||
|
||||
v1.2.1
|
||||
------
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
|
||||
- qutebrowser now starts properly when the PyQt5 QOpenGLFunctions package wasn't
|
||||
found.
|
||||
- The keybinding cheatsheet on the quickstart page is now loaded from a local
|
||||
`qute://` URL again.
|
||||
- With "tox -e mkvenv-pypi", PyQt 5.10.0 is used again instead of Qt 5.10.1,
|
||||
because of an issue with Qt 5.10.1 which causes qutebrowser to fail to start
|
||||
("Could not find QtWebEngineProcess").
|
||||
- Unbinding keys which were bound in older qutebrowser versions now doesn't
|
||||
crash anymore.
|
||||
- Fixed a crash when reloading a page which wasn't fully loaded with v1.2.0
|
||||
- Keys on the numeric keypad now fall back to the same bindings without `Num+`
|
||||
if no `Num+` binding was found.
|
||||
- Fixed hinting on some pages with Qt < 5.10.
|
||||
- Titles are now displayed correctly again for tabs which are cloned or loaded
|
||||
from sessions.
|
||||
- Shortcuts now correctly use `Ctrl` instead of `Command` on macOS again.
|
||||
|
||||
v1.2.0
|
||||
------
|
||||
|
||||
|
||||
@@ -5,6 +5,11 @@ The Compiler <mail@qutebrowser.org>
|
||||
:data-uri:
|
||||
:toc:
|
||||
|
||||
IMPORTANT: I'm currently (July 2018) more busy than usual until September,
|
||||
because of exams coming up. Review of non-trivial pull requests will thus be
|
||||
delayed until then. If you're reading this note after mid-September, please
|
||||
open an issue.
|
||||
|
||||
I `<3` footnote:[Of course, that says `<3` in HTML.] contributors!
|
||||
|
||||
This document contains guidelines for contributing to qutebrowser, as well as
|
||||
@@ -85,6 +90,16 @@ git format-patch origin/master <1>
|
||||
<1> Replace `master` by the branch your work was based on, e.g.,
|
||||
`origin/develop`.
|
||||
|
||||
Running qutebrowser
|
||||
-------------------
|
||||
|
||||
After link:install.asciidoc#tox[installing qutebrowser via tox], you can run
|
||||
`.venv/bin/qutebrowser --debug --temp-basedir` to test your changes with debug
|
||||
logging enabled and without affecting existing running instances.
|
||||
|
||||
Alternatively, you can install qutebrowser's dependencies system-wide and run
|
||||
`python3 -m qutebrowser --debug --temp-basedir`.
|
||||
|
||||
Useful utilities
|
||||
----------------
|
||||
|
||||
@@ -188,8 +203,8 @@ There are some useful functions for debugging in the `qutebrowser.utils.debug`
|
||||
module.
|
||||
|
||||
When starting qutebrowser with the `--debug` flag, you also get useful debug
|
||||
logs. You can add +--logfilter _category[,category,...]_+ to restrict logging
|
||||
to the given categories.
|
||||
logs. You can add +--logfilter _[!]category[,category,...]_+ to restrict
|
||||
logging to the given categories.
|
||||
|
||||
With `--debug` there are also some additional +debug-_*_+ commands available,
|
||||
for example `:debug-all-objects` and `:debug-all-widgets` which print a list of
|
||||
@@ -566,6 +581,23 @@ can be useful for debugging:
|
||||
- chrome://gpuclean/ (crashes the current renderer process!)
|
||||
- chrome://ppapiflashcrash/
|
||||
- chrome://ppapiflashhang/
|
||||
- chrome://quota-internals/ (Qt 5.11)
|
||||
- chrome://taskscheduler-internals/ (Qt 5.11)
|
||||
- chrome://sandbox/ (Qt 5.11, Linux only)
|
||||
|
||||
QtWebEngine internals
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This is mostly useful for qutebrowser maintainers to work around issues in Qt - if you don't understand it, don't worry, just ignore it.
|
||||
|
||||
The hierarchy of widgets when QtWebEngine is involved looks like this:
|
||||
|
||||
- qutebrowser has a `WebEngineTab` object, which is its abstraction over QtWebKit/QtWebEngine.
|
||||
- The `WebEngineTab` has a `_widget` attribute, which is the https://doc.qt.io/qt-5/qwebengineview.html[QWebEngineView]
|
||||
- That view has a https://doc.qt.io/qt-5/qwebenginepage.html[QWebEnginePage] for everything which doesn't require rendering.
|
||||
- The view also has a layout with exactly one element (which also is its `focusProxy()`)
|
||||
- That element is the http://code.qt.io/cgit/qt/qtwebengine.git/tree/src/webenginewidgets/render_widget_host_view_qt_delegate_widget.cpp[RenderWidgetHostViewQtDelegateWidget] (it inherits https://doc.qt.io/qt-5/qquickwidget.html[QQuickWidget]) - also often referred to as RWHV or RWHVQDW. It can be obtained via `sip.cast(tab._widget.focusProxy(), QQuickWidget)`.
|
||||
- Calling `rootObject()` on that gives us the https://doc.qt.io/qt-5/qquickitem.html[QQuickItem] where Chromium renders into (?). With it, we can do things like `.setRotation(20)`.
|
||||
|
||||
Style conventions
|
||||
-----------------
|
||||
@@ -662,18 +694,17 @@ New PyQt release
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
* See above.
|
||||
* Install new PyQt in Windows VM (32- and 64-bit).
|
||||
* Download new installer and update PyQt installer path in `ci_install.py`.
|
||||
* Update `tox.ini`/`.travis.yml`/`.appveyor.yml` to test new versions.
|
||||
|
||||
qutebrowser release
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
* Make sure there are no unstaged changes and the tests are green.
|
||||
* Make sure all issues with the related milestone are closed.
|
||||
* Run `x=... y=...` to set the respective shell variables.
|
||||
|
||||
* Adjust `__version_info__` in `qutebrowser/__init__.py`.
|
||||
* Update changelog (remove *(unreleased)*).
|
||||
* Adjust `__version_info__` in `qutebrowser/__init__.py`.
|
||||
* Commit.
|
||||
|
||||
* Create annotated git tag (`git tag -s "v1.$x.$y" -m "Release v1.$x.$y"`).
|
||||
@@ -683,9 +714,11 @@ qutebrowser release
|
||||
* Mark the milestone at https://github.com/qutebrowser/qutebrowser/milestones
|
||||
as closed.
|
||||
|
||||
* Linux: Run `git checkout v1.$x.$y && python3 scripts/dev/build_release.py --upload v1.$x.$y`.
|
||||
* Windows: Run `git checkout v1.X.Y; C:\Python36-32\python scripts\dev\build_release.py --asciidoc C:\Python27\python C:\asciidoc-8.6.9\asciidoc.py --upload v1.X.Y` (replace X/Y by hand).
|
||||
* macOS: Run `git checkout v1.X.Y && python3 scripts/dev/build_release.py --upload v1.X.Y` (replace X/Y by hand).
|
||||
* On server: Run `python3 scripts/dev/download_release.py v1.X.Y` (replace X/Y by hand).
|
||||
* Linux: Run `git checkout v1.$x.$y && ./.venv/bin/python3 scripts/dev/build_release.py --upload v1.$x.$y`.
|
||||
* Windows: Run `git checkout v1.X.Y; py -3.6 scripts\dev\build_release.py --asciidoc C:\Python27\python C:\asciidoc-8.6.9\asciidoc.py --upload v1.X.Y` (replace X/Y by hand).
|
||||
* macOS: Run `pyenv shell 3.6.6 && git checkout v1.X.Y && python3 scripts/dev/build_release.py --upload v1.X.Y` (replace X/Y by hand).
|
||||
* On server:
|
||||
- Run `python3 scripts/dev/download_release.py v1.X.Y` (replace X/Y by hand).
|
||||
- Run `git pull github master && sudo python3 scripts/asciidoc2html.py --website /srv/http/qutebrowser`
|
||||
* Update `qutebrowser-git` PKGBUILD if dependencies/install changed.
|
||||
* Announce to qutebrowser and qutebrowser-announce mailinglist.
|
||||
|
||||
@@ -212,6 +212,37 @@ Why takes it longer to open an URL in qutebrowser than in chromium?::
|
||||
qutebrowser if it is not running already. Also check if you want
|
||||
to use webengine as backend in line 17 and change it to your
|
||||
needs.
|
||||
|
||||
How do I make qutebrowser use greasemonkey scripts?::
|
||||
There is currently no UI elements to handle managing greasemonkey scripts.
|
||||
All management of what scripts are installed or disabled is done in the
|
||||
filesystem by you. qutebrowser reads all files that have an extension of
|
||||
`.js` from the `<data>/greasemonkey/` folder and attempts to load them.
|
||||
Where `<data>` is the qutebrowser data directory shown in the `Paths`
|
||||
section of the page displayed by `:version`. If you want to disable a
|
||||
script just rename it, for example, to have `.disabled` on the end, after
|
||||
the `.js` extension. To reload scripts from that directory run the command
|
||||
`:greasemonkey-reload`.
|
||||
+
|
||||
Troubleshooting: to check that your script is being loaded when
|
||||
`:greasemonkey-reload` runs you can start qutebrowser with the arguments
|
||||
`--debug --logfilter greasemonkey,js` and check the messages on the
|
||||
program's standard output for errors parsing or loading your script.
|
||||
You may also see javascript errors if your script is expecting an environment
|
||||
that we fail to provide.
|
||||
+
|
||||
Note that there are some missing features which you may run into:
|
||||
|
||||
. Some scripts expect `GM_xmlhttpRequest` to ignore Cross Origin Resource
|
||||
Sharing restrictions, this is currently not supported, so scripts making
|
||||
requests to third party sites will often fail to function correctly.
|
||||
. If your backend is a QtWebEngine version 5.8, 5.9 or 5.10 then regular
|
||||
expressions are not supported in `@include` or `@exclude` rules. If your
|
||||
script uses them you can re-write them to use glob expressions or convert
|
||||
them to `@match` rules.
|
||||
See https://wiki.greasespot.net/Metadata_Block[the wiki] for more info.
|
||||
. Any greasemonkey API function to do with adding UI elements is not currently
|
||||
supported. That means context menu extentensions and background pages.
|
||||
|
||||
== Troubleshooting
|
||||
|
||||
|
||||
@@ -93,6 +93,7 @@ It is possible to run or bind multiple commands by separating them with `;;`.
|
||||
|<<scroll,scroll>>|Scroll the current tab in the given direction.
|
||||
|<<scroll-page,scroll-page>>|Scroll the frame page-wise.
|
||||
|<<scroll-px,scroll-px>>|Scroll the current tab by 'count * dx/dy' pixels.
|
||||
|<<scroll-to-anchor,scroll-to-anchor>>|Scroll to the given anchor in the document.
|
||||
|<<scroll-to-perc,scroll-to-perc>>|Scroll to a specific percentage of the page.
|
||||
|<<search,search>>|Search for a text on the current page. With no text, clear results.
|
||||
|<<search-next,search-next>>|Continue the search to the ([count]th) next term.
|
||||
@@ -110,6 +111,7 @@ It is possible to run or bind multiple commands by separating them with `;;`.
|
||||
|<<tab-focus,tab-focus>>|Select the tab given as argument/[count].
|
||||
|<<tab-give,tab-give>>|Give the current tab to a new or existing window if win_id given.
|
||||
|<<tab-move,tab-move>>|Move the current tab according to the argument and [count].
|
||||
|<<tab-mute,tab-mute>>|Mute/Unmute the current/[count]th tab.
|
||||
|<<tab-next,tab-next>>|Switch to the next tab, or switch [count] tabs forward.
|
||||
|<<tab-only,tab-only>>|Close all tabs except for the current one.
|
||||
|<<tab-pin,tab-pin>>|Pin/Unpin the current/[count]th tab.
|
||||
@@ -529,7 +531,7 @@ Show help about a command or setting.
|
||||
|
||||
[[hint]]
|
||||
=== hint
|
||||
Syntax: +:hint [*--mode* 'mode'] [*--add-history*] [*--rapid*]
|
||||
Syntax: +:hint [*--mode* 'mode'] [*--add-history*] [*--rapid*] [*--first*]
|
||||
['group'] ['target'] ['args' ['args' ...]]+
|
||||
|
||||
Start hinting.
|
||||
@@ -599,6 +601,7 @@ Start hinting.
|
||||
`tab` (with `tabs.background_tabs=true`), `tab-bg`,
|
||||
`window`, `run`, `hover`, `userscript` and `spawn`.
|
||||
|
||||
* +*-f*+, +*--first*+: Click the first hinted element without prompting.
|
||||
|
||||
==== note
|
||||
* This command does not split arguments after the last argument and handles quotes literally.
|
||||
@@ -1024,6 +1027,15 @@ Scroll the current tab by 'count * dx/dy' pixels.
|
||||
==== count
|
||||
multiplier
|
||||
|
||||
[[scroll-to-anchor]]
|
||||
=== scroll-to-anchor
|
||||
Syntax: +:scroll-to-anchor 'name'+
|
||||
|
||||
Scroll to the given anchor in the document.
|
||||
|
||||
==== positional arguments
|
||||
* +'name'+: The anchor to scroll to.
|
||||
|
||||
[[scroll-to-perc]]
|
||||
=== scroll-to-perc
|
||||
Syntax: +:scroll-to-perc [*--horizontal*] ['perc']+
|
||||
@@ -1273,6 +1285,13 @@ If moving relatively: Offset. If moving absolutely: New position (default: 0). T
|
||||
overrides the index argument, if given.
|
||||
|
||||
|
||||
[[tab-mute]]
|
||||
=== tab-mute
|
||||
Mute/Unmute the current/[count]th tab.
|
||||
|
||||
==== count
|
||||
The tab index to mute or unmute
|
||||
|
||||
[[tab-next]]
|
||||
=== tab-next
|
||||
Switch to the next tab, or switch [count] tabs forward.
|
||||
@@ -1346,12 +1365,16 @@ Show version information.
|
||||
|
||||
[[view-source]]
|
||||
=== view-source
|
||||
Syntax: +:view-source [*--edit*]+
|
||||
Syntax: +:view-source [*--edit*] [*--pygments*]+
|
||||
|
||||
Show the source of the current page in a new tab.
|
||||
|
||||
==== optional arguments
|
||||
* +*-e*+, +*--edit*+: Edit the source in the editor instead of opening a tab.
|
||||
* +*-p*+, +*--pygments*+: Use pygments to generate the view. This is always the case for QtWebKit. For QtWebEngine it may display
|
||||
slightly different source.
|
||||
Some JavaScript processing may be applied.
|
||||
|
||||
|
||||
[[window-only]]
|
||||
=== window-only
|
||||
|
||||
@@ -3,44 +3,28 @@ Configuring qutebrowser
|
||||
|
||||
IMPORTANT: qutebrowser's configuration system was completely rewritten in
|
||||
September 2017. This information is not applicable to older releases, and older
|
||||
information elsewhere might be outdated. **If you had an old configuration
|
||||
around and upgraded, this page will automatically open once**. To view it at a
|
||||
later time, use the `:help` command.
|
||||
information elsewhere might be outdated.
|
||||
|
||||
Migrating older configurations
|
||||
------------------------------
|
||||
qutebrowser's config files
|
||||
--------------------------
|
||||
|
||||
qutebrowser does no automatic migration for the new configuration. However,
|
||||
there's a special link:qute://configdiff/old[configdiff] page
|
||||
(`qute://configdiff/old`) in qutebrowser, which will show you the changes you
|
||||
did in your old configuration, compared to the old defaults.
|
||||
qutebrowser releases before v1.0.0 had a `qutebrowser.conf` and `keys.conf`
|
||||
file. Those are not used anymore since that release - see
|
||||
<<migrating,"Migrating older configurations">> for information on how to
|
||||
migrate to the new config.
|
||||
|
||||
Other changes in default settings:
|
||||
When using `:set` and `:bind`, changes are saved to an `autoconfig.yml` file
|
||||
automatically. If you don't want to have a config file which is curated by
|
||||
hand, you can simply use those - see
|
||||
<<autoconfig,"Configuring qutebrowser via the user interface">> for details.
|
||||
|
||||
- In v1.1.x and newer, `<Up>` and `<Down>` navigate through command history
|
||||
if no text was entered yet.
|
||||
With v1.0.x, they always navigate through command history instead of selecting
|
||||
completion items. Use `<Tab>`/`<Shift-Tab>` to cycle through the completion
|
||||
instead.
|
||||
You can get back the old behavior by doing:
|
||||
+
|
||||
----
|
||||
:bind -m command <Up> completion-item-focus prev
|
||||
:bind -m command <Down> completion-item-focus next
|
||||
----
|
||||
+
|
||||
or always navigate through command history with
|
||||
+
|
||||
----
|
||||
:bind -m command <Up> command-history-prev
|
||||
:bind -m command <Down> command-history-next
|
||||
----
|
||||
|
||||
- The default for `completion.web_history_max_items` is now set to `-1`, showing
|
||||
an unlimited number of items in the completion for `:open` as the new
|
||||
sqlite-based completion is much faster. If the `:open` completion is too slow
|
||||
on your machine, set an appropriate limit again.
|
||||
For more advanced configuration, you can write a `config.py` file - see
|
||||
<<configpy,"Configuring qutebrowser via config.py">>. As soon as a `config.py`
|
||||
exists, the `autoconfig.yml` file **is not read anymore** by default. You need
|
||||
to <<configpy-autoconfig,load it by hand>> if you want settings done via
|
||||
`:set`/`:bind` to still persist.
|
||||
|
||||
[[autoconfig]]
|
||||
Configuring qutebrowser via the user interface
|
||||
----------------------------------------------
|
||||
|
||||
@@ -88,6 +72,7 @@ link:commands.html#config-clear[`:config-clear`] to reset the entire configurati
|
||||
and link:commands.html#config-cycle[`:config-cycle`] to cycle a setting between
|
||||
different values.
|
||||
|
||||
[[configpy]]
|
||||
Configuring qutebrowser via config.py
|
||||
-------------------------------------
|
||||
|
||||
@@ -239,13 +224,14 @@ config.bind(',v', 'spawn mpv {url}')
|
||||
To suppress loading of any default keybindings, you can set
|
||||
`c.bindings.default = {}`.
|
||||
|
||||
[[configpy-autoconfig]]
|
||||
Loading `autoconfig.yml`
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
By default, all customization done via `:set`, `:bind` and `:unbind` is
|
||||
temporary as soon as a `config.py` exists. The settings done that way are always
|
||||
saved in the `autoconfig.yml` file, but you'll need to explicitly load it in
|
||||
your `config.py` by doing:
|
||||
All customization done via the UI (`:set`, `:bind` and `:unbind`) is
|
||||
stored in the `autoconfig.yml` file, which is not loaded automatically as soon
|
||||
as a `config.py` exists. If you want those settings to be loaded, you'll need to
|
||||
explicitly load the `autoconfig.yml` file in your `config.py` by doing:
|
||||
|
||||
.config.py:
|
||||
[source,python]
|
||||
@@ -404,6 +390,12 @@ xresources = read_xresources('*')
|
||||
c.colors.statusbar.normal.bg = xresources['*.background']
|
||||
----
|
||||
|
||||
Pre-built colorschemes
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
- A collection of https://github.com/chriskempson/base16[base16] color-schemes can be found in https://github.com/theova/base16-qutebrowser[base16-qutebrowser] and used with https://github.com/AuditeMarlow/base16-manager[base16-manager].
|
||||
- Two implementations of the https://github.com/arcticicestudio/nord[Nord] colorscheme for qutebrowser exist: https://github.com/Linuus/nord-qutebrowser[Linuus], https://github.com/KnownAsDon/QuteBrowser-Nord-Theme[KnownAsDon]
|
||||
|
||||
Avoiding flake8 errors
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
@@ -429,3 +421,38 @@ from qutebrowser.config.config import ConfigContainer # noqa: F401
|
||||
config = config # type: ConfigAPI # noqa: F821 pylint: disable=E0602,C0103
|
||||
c = c # type: ConfigContainer # noqa: F821 pylint: disable=E0602,C0103
|
||||
----
|
||||
|
||||
[[migrating]]
|
||||
Migrating older configurations
|
||||
------------------------------
|
||||
|
||||
qutebrowser does no automatic migration for the new configuration. However,
|
||||
there's a special link:qute://configdiff/old[configdiff] page
|
||||
(`qute://configdiff/old`) in qutebrowser, which will show you the changes you
|
||||
did in your old configuration, compared to the old defaults.
|
||||
|
||||
Other changes in default settings:
|
||||
|
||||
- In v1.1.x and newer, `<Up>` and `<Down>` navigate through command history
|
||||
if no text was entered yet.
|
||||
With v1.0.x, they always navigate through command history instead of selecting
|
||||
completion items. Use `<Tab>`/`<Shift-Tab>` to cycle through the completion
|
||||
instead.
|
||||
You can get back the old behavior by doing:
|
||||
+
|
||||
----
|
||||
:bind -m command <Up> completion-item-focus prev
|
||||
:bind -m command <Down> completion-item-focus next
|
||||
----
|
||||
+
|
||||
or always navigate through command history with
|
||||
+
|
||||
----
|
||||
:bind -m command <Up> command-history-prev
|
||||
:bind -m command <Down> command-history-next
|
||||
----
|
||||
|
||||
- The default for `completion.web_history_max_items` is now set to `-1`, showing
|
||||
an unlimited number of items in the completion for `:open` as the new
|
||||
sqlite-based completion is much faster. If the `:open` completion is too slow
|
||||
on your machine, set an appropriate limit again.
|
||||
|
||||
@@ -109,13 +109,15 @@
|
||||
|<<completion.use_best_match,completion.use_best_match>>|Execute the best-matching command on a partial match.
|
||||
|<<completion.web_history_max_items,completion.web_history_max_items>>|Number of URLs to show in the web history.
|
||||
|<<confirm_quit,confirm_quit>>|Require a confirmation before quitting the application.
|
||||
|<<content.autoplay,content.autoplay>>|Automatically start playing `<video>` elements.
|
||||
|<<content.cache.appcache,content.cache.appcache>>|Enable support for the HTML 5 web application cache feature.
|
||||
|<<content.cache.maximum_pages,content.cache.maximum_pages>>|Maximum number of pages to hold in the global memory page cache.
|
||||
|<<content.cache.size,content.cache.size>>|Size (in bytes) of the HTTP network cache. Null to use the default value.
|
||||
|<<content.canvas_reading,content.canvas_reading>>|Allow websites to read canvas elements.
|
||||
|<<content.cookies.accept,content.cookies.accept>>|Which cookies to accept.
|
||||
|<<content.cookies.store,content.cookies.store>>|Store cookies.
|
||||
|<<content.default_encoding,content.default_encoding>>|Default encoding to use for websites.
|
||||
|<<content.developer_extras,content.developer_extras>>|Enable extra tools for Web developers.
|
||||
|<<content.desktop_capture,content.desktop_capture>>|Allow websites to share screen content.
|
||||
|<<content.dns_prefetch,content.dns_prefetch>>|Try to pre-fetch DNS entries to speed up browsing.
|
||||
|<<content.frame_flattening,content.frame_flattening>>|Expand each subframe to its contents.
|
||||
|<<content.geolocation,content.geolocation>>|Allow websites to request geolocations.
|
||||
@@ -144,14 +146,17 @@
|
||||
|<<content.netrc_file,content.netrc_file>>|Netrc-file for HTTP authentication.
|
||||
|<<content.notifications,content.notifications>>|Allow websites to show notifications.
|
||||
|<<content.pdfjs,content.pdfjs>>|Allow pdf.js to view PDF files in the browser.
|
||||
|<<content.persistent_storage,content.persistent_storage>>|Allow websites to request persistent storage quota via `navigator.webkitPersistentStorage.requestQuota`.
|
||||
|<<content.plugins,content.plugins>>|Enable plugins in Web pages.
|
||||
|<<content.print_element_backgrounds,content.print_element_backgrounds>>|Draw the background color and images also when the page is printed.
|
||||
|<<content.private_browsing,content.private_browsing>>|Open new windows in private browsing mode which does not record visited pages.
|
||||
|<<content.proxy,content.proxy>>|Proxy to use.
|
||||
|<<content.proxy_dns_requests,content.proxy_dns_requests>>|Send DNS requests over the configured proxy.
|
||||
|<<content.register_protocol_handler,content.register_protocol_handler>>|Allow websites to register protocol handlers via `navigator.registerProtocolHandler`.
|
||||
|<<content.ssl_strict,content.ssl_strict>>|Validate SSL handshakes.
|
||||
|<<content.user_stylesheets,content.user_stylesheets>>|List of user stylesheet filenames to use.
|
||||
|<<content.webgl,content.webgl>>|Enable WebGL.
|
||||
|<<content.webrtc_public_interfaces_only,content.webrtc_public_interfaces_only>>|Only expose public interfaces via WebRTC.
|
||||
|<<content.windowed_fullscreen,content.windowed_fullscreen>>|Limit fullscreen to the browser window (does not expand to fill the screen).
|
||||
|<<content.xss_auditing,content.xss_auditing>>|Monitor load requests for cross-site scripting attempts.
|
||||
|<<downloads.location.directory,downloads.location.directory>>|Directory to save downloads to.
|
||||
@@ -200,6 +205,7 @@
|
||||
|<<hints.scatter,hints.scatter>>|Scatter hint key chains (like Vimium) or not (like dwb).
|
||||
|<<hints.uppercase,hints.uppercase>>|Make characters in hint strings uppercase.
|
||||
|<<history_gap_interval,history_gap_interval>>|Maximum time (in minutes) between two history items for them to be considered being from the same browsing session.
|
||||
|<<input.escape_quits_reporter,input.escape_quits_reporter>>|Allow Escape to quit the crash reporter.
|
||||
|<<input.forward_unbound_keys,input.forward_unbound_keys>>|Which unbound keys to forward to the webview in normal mode.
|
||||
|<<input.insert_mode.auto_enter,input.insert_mode.auto_enter>>|Enter insert mode if an editable element is clicked.
|
||||
|<<input.insert_mode.auto_leave,input.insert_mode.auto_leave>>|Leave insert mode if a non-editable element is clicked.
|
||||
@@ -236,10 +242,11 @@
|
||||
|<<tabs.close_mouse_button,tabs.close_mouse_button>>|Mouse button with which to close tabs.
|
||||
|<<tabs.close_mouse_button_on_bar,tabs.close_mouse_button_on_bar>>|How to behave when the close mouse button is pressed on the tab bar.
|
||||
|<<tabs.favicons.scale,tabs.favicons.scale>>|Scaling factor for favicons in the tab bar.
|
||||
|<<tabs.favicons.show,tabs.favicons.show>>|Show favicons in the tab bar.
|
||||
|<<tabs.favicons.show,tabs.favicons.show>>|When to show favicons in the tab bar.
|
||||
|<<tabs.indicator.padding,tabs.indicator.padding>>|Padding (in pixels) for tab indicators.
|
||||
|<<tabs.indicator.width,tabs.indicator.width>>|Width (in pixels) of the progress indicator (0 to disable).
|
||||
|<<tabs.last_close,tabs.last_close>>|How to behave when the last tab is closed.
|
||||
|<<tabs.min_width,tabs.min_width>>|Minimum width (in pixels) of tabs (-1 for the default minimum size behavior).
|
||||
|<<tabs.mode_on_change,tabs.mode_on_change>>|When switching tabs, what input mode is applied.
|
||||
|<<tabs.mousewheel_switching,tabs.mousewheel_switching>>|Switch between tabs using the mouse wheel.
|
||||
|<<tabs.new_position.related,tabs.new_position.related>>|Position of new tabs opened from another tab.
|
||||
@@ -259,10 +266,11 @@
|
||||
|<<url.auto_search,url.auto_search>>|What search to start when something else than a URL is entered.
|
||||
|<<url.default_page,url.default_page>>|Page to open if :open -t/-b/-w is used without URL.
|
||||
|<<url.incdec_segments,url.incdec_segments>>|URL segments where `:navigate increment/decrement` will search for a number.
|
||||
|<<url.open_base_url,url.open_base_url>>|Open base URL of the searchengine if a searchengine shortcut is invoked without parameters.
|
||||
|<<url.searchengines,url.searchengines>>|Search engines which can be used via the address bar.
|
||||
|<<url.start_pages,url.start_pages>>|Page(s) to open at the start.
|
||||
|<<url.yank_ignored_parameters,url.yank_ignored_parameters>>|URL parameters to strip with `:yank url`.
|
||||
|<<window.hide_wayland_decoration,window.hide_wayland_decoration>>|Hide the window decoration when using wayland.
|
||||
|<<window.hide_decoration,window.hide_decoration>>|Hide the window decoration.
|
||||
|<<window.title_format,window.title_format>>|Format to use for the window title. The same placeholders like for
|
||||
|<<zoom.default,zoom.default>>|Default zoom level.
|
||||
|<<zoom.levels,zoom.levels>>|Available zoom levels.
|
||||
@@ -485,6 +493,7 @@ Default:
|
||||
* +pass:[<Alt-7>]+: +pass:[tab-focus 7]+
|
||||
* +pass:[<Alt-8>]+: +pass:[tab-focus 8]+
|
||||
* +pass:[<Alt-9>]+: +pass:[tab-focus -1]+
|
||||
* +pass:[<Alt-m>]+: +pass:[tab-mute]+
|
||||
* +pass:[<Ctrl-A>]+: +pass:[navigate increment]+
|
||||
* +pass:[<Ctrl-Alt-p>]+: +pass:[print]+
|
||||
* +pass:[<Ctrl-B>]+: +pass:[scroll-page 0 -1]+
|
||||
@@ -498,6 +507,7 @@ Default:
|
||||
* +pass:[<Ctrl-Return>]+: +pass:[follow-selected -t]+
|
||||
* +pass:[<Ctrl-Shift-N>]+: +pass:[open -p]+
|
||||
* +pass:[<Ctrl-Shift-T>]+: +pass:[undo]+
|
||||
* +pass:[<Ctrl-Shift-Tab>]+: +pass:[nop]+
|
||||
* +pass:[<Ctrl-Shift-W>]+: +pass:[close]+
|
||||
* +pass:[<Ctrl-T>]+: +pass:[open -t]+
|
||||
* +pass:[<Ctrl-Tab>]+: +pass:[tab-focus last]+
|
||||
@@ -560,6 +570,7 @@ Default:
|
||||
* +pass:[gd]+: +pass:[download]+
|
||||
* +pass:[gf]+: +pass:[view-source]+
|
||||
* +pass:[gg]+: +pass:[scroll-to-perc 0]+
|
||||
* +pass:[gi]+: +pass:[hint inputs --first]+
|
||||
* +pass:[gl]+: +pass:[tab-move -]+
|
||||
* +pass:[gm]+: +pass:[tab-move]+
|
||||
* +pass:[go]+: +pass:[set-cmd-text :open {url:pretty}]+
|
||||
@@ -1460,6 +1471,19 @@ Default:
|
||||
|
||||
- +pass:[never]+
|
||||
|
||||
[[content.autoplay]]
|
||||
=== content.autoplay
|
||||
Automatically start playing `<video>` elements.
|
||||
Note this option needs a restart with QtWebEngine on Qt < 5.11.
|
||||
|
||||
Type: <<types,Bool>>
|
||||
|
||||
Default: +pass:[true]+
|
||||
|
||||
On QtWebEngine, this setting requires Qt 5.10 or newer.
|
||||
|
||||
On QtWebKit, this setting is unavailable.
|
||||
|
||||
[[content.cache.appcache]]
|
||||
=== content.cache.appcache
|
||||
Enable support for the HTML 5 web application cache feature.
|
||||
@@ -1494,6 +1518,18 @@ Type: <<types,Int>>
|
||||
|
||||
Default: empty
|
||||
|
||||
[[content.canvas_reading]]
|
||||
=== content.canvas_reading
|
||||
Allow websites to read canvas elements.
|
||||
Note this is needed for some websites to work properly.
|
||||
This setting requires a restart.
|
||||
|
||||
Type: <<types,Bool>>
|
||||
|
||||
Default: +pass:[true]+
|
||||
|
||||
This setting is only available with the QtWebEngine backend.
|
||||
|
||||
[[content.cookies.accept]]
|
||||
=== content.cookies.accept
|
||||
Which cookies to accept.
|
||||
@@ -1504,12 +1540,12 @@ Valid values:
|
||||
|
||||
* +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.
|
||||
* +no-unknown-3rdparty+: Accept cookies from the same origin only, unless a cookie is already set for the domain. On QtWebEngine, this is the same as no-3rdparty.
|
||||
* +never+: Don't accept cookies at all.
|
||||
|
||||
Default: +pass:[no-3rdparty]+
|
||||
|
||||
This setting is only available with the QtWebKit backend.
|
||||
On QtWebEngine, this setting requires Qt 5.11 or newer.
|
||||
|
||||
[[content.cookies.store]]
|
||||
=== content.cookies.store
|
||||
@@ -1529,16 +1565,22 @@ Type: <<types,String>>
|
||||
|
||||
Default: +pass:[iso-8859-1]+
|
||||
|
||||
[[content.developer_extras]]
|
||||
=== content.developer_extras
|
||||
Enable extra tools for Web developers.
|
||||
This needs to be enabled for `:inspector` to work and also adds an _Inspect_ entry to the context menu. For QtWebEngine, see `--enable-webengine-inspector` in `qutebrowser --help` instead.
|
||||
[[content.desktop_capture]]
|
||||
=== content.desktop_capture
|
||||
Allow websites to share screen content.
|
||||
On Qt < 5.10, a dialog box is always displayed, even if this is set to "true".
|
||||
|
||||
Type: <<types,Bool>>
|
||||
This setting supports URL patterns.
|
||||
|
||||
Default: +pass:[false]+
|
||||
Type: <<types,BoolAsk>>
|
||||
|
||||
This setting is only available with the QtWebKit backend.
|
||||
Valid values:
|
||||
|
||||
* +true+
|
||||
* +false+
|
||||
* +ask+
|
||||
|
||||
Default: +pass:[ask]+
|
||||
|
||||
[[content.dns_prefetch]]
|
||||
=== content.dns_prefetch
|
||||
@@ -1569,6 +1611,8 @@ This setting is only available with the QtWebKit backend.
|
||||
=== content.geolocation
|
||||
Allow websites to request geolocations.
|
||||
|
||||
This setting supports URL patterns.
|
||||
|
||||
Type: <<types,BoolAsk>>
|
||||
|
||||
Valid values:
|
||||
@@ -1582,6 +1626,9 @@ Default: +pass:[ask]+
|
||||
[[content.headers.accept_language]]
|
||||
=== content.headers.accept_language
|
||||
Value to send in the `Accept-Language` header.
|
||||
Note that the value read from JavaScript is always the global value.
|
||||
|
||||
This setting supports URL patterns.
|
||||
|
||||
Type: <<types,String>>
|
||||
|
||||
@@ -1591,6 +1638,8 @@ Default: +pass:[en-US,en]+
|
||||
=== content.headers.custom
|
||||
Custom headers for qutebrowser HTTP requests.
|
||||
|
||||
This setting supports URL patterns.
|
||||
|
||||
Type: <<types,Dict>>
|
||||
|
||||
Default: empty
|
||||
@@ -1600,6 +1649,8 @@ Default: empty
|
||||
Value to send in the `DNT` header.
|
||||
When this is set to true, qutebrowser asks websites to not track your identity. If set to null, the DNT header is not sent at all.
|
||||
|
||||
This setting supports URL patterns.
|
||||
|
||||
Type: <<types,Bool>>
|
||||
|
||||
Default: +pass:[true]+
|
||||
@@ -1624,6 +1675,9 @@ This setting is only available with the QtWebKit backend.
|
||||
[[content.headers.user_agent]]
|
||||
=== content.headers.user_agent
|
||||
User agent to send. Unset to send the default.
|
||||
Note that the value read from JavaScript is always the global value.
|
||||
|
||||
This setting supports URL patterns.
|
||||
|
||||
Type: <<types,String>>
|
||||
|
||||
@@ -1653,11 +1707,7 @@ Type: <<types,List of Url>>
|
||||
|
||||
Default:
|
||||
|
||||
- +pass:[https://www.malwaredomainlist.com/hostslist/hosts.txt]+
|
||||
- +pass:[http://someonewhocares.org/hosts/hosts]+
|
||||
- +pass:[http://winhelp2002.mvps.org/hosts.zip]+
|
||||
- +pass:[http://malwaredomains.lehigh.edu/files/justdomains.zip]+
|
||||
- +pass:[https://pgl.yoyo.org/adservers/serverlist.php?hostformat=hosts&mimetype=plaintext]+
|
||||
- +pass:[https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts]+
|
||||
|
||||
[[content.host_blocking.whitelist]]
|
||||
=== content.host_blocking.whitelist
|
||||
@@ -1807,6 +1857,8 @@ Default: +pass:[true]+
|
||||
=== content.media_capture
|
||||
Allow websites to record audio/video.
|
||||
|
||||
This setting supports URL patterns.
|
||||
|
||||
Type: <<types,BoolAsk>>
|
||||
|
||||
Valid values:
|
||||
@@ -1832,6 +1884,8 @@ Default: empty
|
||||
=== content.notifications
|
||||
Allow websites to show notifications.
|
||||
|
||||
This setting supports URL patterns.
|
||||
|
||||
Type: <<types,BoolAsk>>
|
||||
|
||||
Valid values:
|
||||
@@ -1855,6 +1909,26 @@ Default: +pass:[false]+
|
||||
|
||||
This setting is only available with the QtWebKit backend.
|
||||
|
||||
[[content.persistent_storage]]
|
||||
=== content.persistent_storage
|
||||
Allow websites to request persistent storage quota via `navigator.webkitPersistentStorage.requestQuota`.
|
||||
|
||||
This setting supports URL patterns.
|
||||
|
||||
Type: <<types,BoolAsk>>
|
||||
|
||||
Valid values:
|
||||
|
||||
* +true+
|
||||
* +false+
|
||||
* +ask+
|
||||
|
||||
Default: +pass:[ask]+
|
||||
|
||||
On QtWebEngine, this setting requires Qt 5.11 or newer.
|
||||
|
||||
On QtWebKit, this setting is unavailable.
|
||||
|
||||
[[content.plugins]]
|
||||
=== content.plugins
|
||||
Enable plugins in Web pages.
|
||||
@@ -1909,10 +1983,32 @@ Default: +pass:[true]+
|
||||
|
||||
This setting is only available with the QtWebKit backend.
|
||||
|
||||
[[content.register_protocol_handler]]
|
||||
=== content.register_protocol_handler
|
||||
Allow websites to register protocol handlers via `navigator.registerProtocolHandler`.
|
||||
|
||||
This setting supports URL patterns.
|
||||
|
||||
Type: <<types,BoolAsk>>
|
||||
|
||||
Valid values:
|
||||
|
||||
* +true+
|
||||
* +false+
|
||||
* +ask+
|
||||
|
||||
Default: +pass:[ask]+
|
||||
|
||||
On QtWebEngine, this setting requires Qt 5.11 or newer.
|
||||
|
||||
On QtWebKit, this setting is unavailable.
|
||||
|
||||
[[content.ssl_strict]]
|
||||
=== content.ssl_strict
|
||||
Validate SSL handshakes.
|
||||
|
||||
This setting supports URL patterns.
|
||||
|
||||
Type: <<types,BoolAsk>>
|
||||
|
||||
Valid values:
|
||||
@@ -1941,6 +2037,19 @@ Type: <<types,Bool>>
|
||||
|
||||
Default: +pass:[true]+
|
||||
|
||||
[[content.webrtc_public_interfaces_only]]
|
||||
=== content.webrtc_public_interfaces_only
|
||||
Only expose public interfaces via WebRTC.
|
||||
On Qt 5.9, this option requires a restart. On Qt 5.10, this option doesn't work at all because of a Qt bug. On Qt >= 5.11, no restart is required.
|
||||
|
||||
Type: <<types,Bool>>
|
||||
|
||||
Default: +pass:[false]+
|
||||
|
||||
On QtWebEngine, this setting requires Qt 5.9.2 or newer.
|
||||
|
||||
On QtWebKit, this setting is unavailable.
|
||||
|
||||
[[content.windowed_fullscreen]]
|
||||
=== content.windowed_fullscreen
|
||||
Limit fullscreen to the browser window (does not expand to fill the screen).
|
||||
@@ -2387,6 +2496,14 @@ Type: <<types,Int>>
|
||||
|
||||
Default: +pass:[30]+
|
||||
|
||||
[[input.escape_quits_reporter]]
|
||||
=== input.escape_quits_reporter
|
||||
Allow Escape to quit the crash reporter.
|
||||
|
||||
Type: <<types,Bool>>
|
||||
|
||||
Default: +pass:[true]+
|
||||
|
||||
[[input.forward_unbound_keys]]
|
||||
=== input.forward_unbound_keys
|
||||
Which unbound keys to forward to the webview in normal mode.
|
||||
@@ -2579,12 +2696,19 @@ Default: empty
|
||||
[[qt.force_software_rendering]]
|
||||
=== qt.force_software_rendering
|
||||
Force software rendering for QtWebEngine.
|
||||
This is needed for QtWebEngine to work with Nouveau drivers.
|
||||
This is needed for QtWebEngine to work with Nouveau drivers and can be useful in other scenarios related to graphic issues.
|
||||
This setting requires a restart.
|
||||
|
||||
Type: <<types,Bool>>
|
||||
Type: <<types,String>>
|
||||
|
||||
Default: +pass:[false]+
|
||||
Valid values:
|
||||
|
||||
* +software-opengl+: Tell LibGL to use a software implementation of GL (`LIBGL_ALWAYS_SOFTWARE` / `QT_XCB_FORCE_SOFTWARE_OPENGL`)
|
||||
* +qt-quick+: Tell Qt Quick to use a software renderer instead of OpenGL. (`QT_QUICK_BACKEND=software`)
|
||||
* +chromium+: Tell Chromium to disable GPU support and use Skia software rendering instead. (`--disable-gpu`)
|
||||
* +none+: Don't force software rendering.
|
||||
|
||||
Default: +pass:[none]+
|
||||
|
||||
This setting is only available with the QtWebEngine backend.
|
||||
|
||||
@@ -2823,11 +2947,17 @@ Default: +pass:[1.0]+
|
||||
|
||||
[[tabs.favicons.show]]
|
||||
=== tabs.favicons.show
|
||||
Show favicons in the tab bar.
|
||||
When to show favicons in the tab bar.
|
||||
|
||||
Type: <<types,Bool>>
|
||||
Type: <<types,String>>
|
||||
|
||||
Default: +pass:[true]+
|
||||
Valid values:
|
||||
|
||||
* +always+: Always show favicons.
|
||||
* +never+: Always hide favicons.
|
||||
* +pinned+: Show favicons only on pinned tabs.
|
||||
|
||||
Default: +pass:[always]+
|
||||
|
||||
[[tabs.indicator.padding]]
|
||||
=== tabs.indicator.padding
|
||||
@@ -2866,6 +2996,16 @@ Valid values:
|
||||
|
||||
Default: +pass:[ignore]+
|
||||
|
||||
[[tabs.min_width]]
|
||||
=== tabs.min_width
|
||||
Minimum width (in pixels) of tabs (-1 for the default minimum size behavior).
|
||||
This setting only applies when tabs are horizontal.
|
||||
This setting does not apply to pinned tabs, unless `tabs.pinned.shrink` is False.
|
||||
|
||||
Type: <<types,Int>>
|
||||
|
||||
Default: +pass:[-1]+
|
||||
|
||||
[[tabs.mode_on_change]]
|
||||
=== tabs.mode_on_change
|
||||
When switching tabs, what input mode is applied.
|
||||
@@ -3030,11 +3170,12 @@ The following placeholders are defined:
|
||||
* `{private}`: Indicates when private mode is enabled.
|
||||
* `{current_url}`: URL of the current web page.
|
||||
* `{protocol}`: Protocol (http/https/...) of the current web page.
|
||||
* `{audio}`: Indicator for audio/mute status.
|
||||
|
||||
|
||||
Type: <<types,FormatString>>
|
||||
|
||||
Default: +pass:[{index}: {title}]+
|
||||
Default: +pass:[{audio}{index}: {title}]+
|
||||
|
||||
[[tabs.title.format_pinned]]
|
||||
=== tabs.title.format_pinned
|
||||
@@ -3102,6 +3243,14 @@ Default:
|
||||
- +pass:[path]+
|
||||
- +pass:[query]+
|
||||
|
||||
[[url.open_base_url]]
|
||||
=== url.open_base_url
|
||||
Open base URL of the searchengine if a searchengine shortcut is invoked without parameters.
|
||||
|
||||
Type: <<types,Bool>>
|
||||
|
||||
Default: +pass:[false]+
|
||||
|
||||
[[url.searchengines]]
|
||||
=== url.searchengines
|
||||
Search engines which can be used via the address bar.
|
||||
@@ -3137,10 +3286,12 @@ Default:
|
||||
- +pass:[utm_term]+
|
||||
- +pass:[utm_content]+
|
||||
|
||||
[[window.hide_wayland_decoration]]
|
||||
=== window.hide_wayland_decoration
|
||||
Hide the window decoration when using wayland.
|
||||
This setting requires a restart.
|
||||
[[window.hide_decoration]]
|
||||
=== window.hide_decoration
|
||||
Hide the window decoration.
|
||||
|
||||
This setting requires a restart on Wayland.
|
||||
|
||||
|
||||
Type: <<types,Bool>>
|
||||
|
||||
@@ -3273,7 +3424,7 @@ See the setting's valid values for more information on allowed values.
|
||||
|TextAlignment|Alignment of text.
|
||||
|TimestampTemplate|An strftime-like template for timestamps.
|
||||
|
||||
See https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior for reference.
|
||||
See https://sqlite.org/lang_datefunc.html for reference.
|
||||
|UniqueCharString|A string which may not contain duplicate chars.
|
||||
|Url|A URL as a string.
|
||||
|VerticalPosition|The position of the download bar.
|
||||
|
||||
|
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
@@ -47,17 +47,27 @@ Debian Stretch / Ubuntu 17.04 and 17.10
|
||||
Those versions come with QtWebEngine in the repositories. This makes it possible
|
||||
to install qutebrowser via the Debian package.
|
||||
|
||||
Download the https://packages.debian.org/sid/all/qutebrowser/download[qutebrowser] and
|
||||
https://packages.debian.org/sid/all/python3-pypeg2/download[PyPEG2]
|
||||
package from the Debian repositories.
|
||||
You'll need to download three packages:
|
||||
|
||||
Install the packages:
|
||||
- https://packages.debian.org/sid/all/python3-pypeg2/download[PyPEG2] (a library
|
||||
used by qutebrowser which is not in the earlier repositories)
|
||||
- https://packages.debian.org/sid/all/qutebrowser/download[qutebrowser] itself
|
||||
- Either https://packages.debian.org/sid/all/qutebrowser-qtwebengine/download[qutebrowser-qtwebengine]
|
||||
or https://packages.debian.org/sid/all/qutebrowser-qtwebkit/download[qutebrowser-qtwebkit]
|
||||
(or both) depending on the backend you want to use. QtWebEngine is the
|
||||
default/recommended choice.
|
||||
|
||||
After downloading, install the packages (make sure to install all the
|
||||
downloaded qutebrowser deb files in one apt command):
|
||||
|
||||
----
|
||||
# apt install ./python3-pypeg2_*_all.deb
|
||||
# apt install ./qutebrowser_*_all.deb
|
||||
# apt install ./qutebrowser*.deb
|
||||
----
|
||||
|
||||
For an update after the initial install, you only need to download/install the
|
||||
qutebrowser package.
|
||||
|
||||
Debian Testing / Ubuntu 18.04
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -372,8 +382,8 @@ $ git clone https://github.com/qutebrowser/qutebrowser.git
|
||||
$ cd qutebrowser
|
||||
----
|
||||
|
||||
Installing depdendencies (including Qt)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Installing dependencies (including Qt)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Then run tox inside the qutebrowser repository to set up a
|
||||
https://docs.python.org/3/library/venv.html[virtual environment]:
|
||||
@@ -382,6 +392,10 @@ https://docs.python.org/3/library/venv.html[virtual environment]:
|
||||
$ tox -e mkvenv-pypi
|
||||
----
|
||||
|
||||
If your system comes with Python 3.5.3 or older (such as Ubuntu 16.04 LTS), use
|
||||
`tox -e mkvenv-pypi-old` instead. This installs an older Qt version (5.10) due
|
||||
to bugs in newer versions.
|
||||
|
||||
This installs all needed Python dependencies in a `.venv` subfolder.
|
||||
|
||||
This comes with an up-to-date Qt/PyQt including QtWebEngine, but has a few
|
||||
@@ -417,7 +431,11 @@ Creating a wrapper script
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Running `tox` does not install a system-wide `qutebrowser` script. You can
|
||||
launch qutebrowser by doing `.venv/bin/python3 -m qutebrowser`.
|
||||
launch qutebrowser by doing:
|
||||
|
||||
----
|
||||
.venv/bin/python3 -m qutebrowser
|
||||
----
|
||||
|
||||
You can create a simple wrapper script to start qutebrowser somewhere in your
|
||||
`$PATH` (e.g. `/usr/local/bin/qutebrowser` or `~/bin/qutebrowser`):
|
||||
|
||||
@@ -22,9 +22,9 @@ Basic keybindings to get you started
|
||||
What to do now
|
||||
--------------
|
||||
|
||||
* View the link:https://qutebrowser.org/img/cheatsheet-big.png[key binding cheatsheet]
|
||||
* View the link:https://raw.githubusercontent.com/qutebrowser/qutebrowser/master/doc/img/cheatsheet-big.png[key binding cheatsheet]
|
||||
to make yourself familiar with the key bindings: +
|
||||
image:https://qutebrowser.org/img/cheatsheet-small.png["qutebrowser key binding cheatsheet",link="https://qutebrowser.org/img/cheatsheet-big.png"]
|
||||
image:https://raw.githubusercontent.com/qutebrowser/qutebrowser/master/doc/img/cheatsheet-small.png["qutebrowser key binding cheatsheet",link="https://raw.githubusercontent.com/qutebrowser/qutebrowser/master/doc/img/cheatsheet-big.png"]
|
||||
* There's also a https://www.shortcutfoo.com/app/dojos/qutebrowser[free training
|
||||
course] on shortcutfoo for the keybindings - note that you need to be in
|
||||
insert mode (i) for it to work.
|
||||
|
||||
@@ -38,7 +38,7 @@ show it.
|
||||
*-h*, *--help*::
|
||||
show this help message and exit
|
||||
|
||||
*--basedir* 'BASEDIR'::
|
||||
*-B* 'BASEDIR', *--basedir* 'BASEDIR'::
|
||||
Base directory for all storage.
|
||||
|
||||
*-V*, *--version*::
|
||||
@@ -60,7 +60,7 @@ show it.
|
||||
Which backend to use.
|
||||
|
||||
*--enable-webengine-inspector*::
|
||||
Enable the web inspector for QtWebEngine. Note that this is a SECURITY RISK and you should not visit untrusted websites with the inspector turned on. See https://bugreports.qt.io/browse/QTBUG-50725 for more details.
|
||||
Enable the web inspector for QtWebEngine. Note that this is a SECURITY RISK and you should not visit untrusted websites with the inspector turned on. See https://bugreports.qt.io/browse/QTBUG-50725 for more details. This is not needed anymore since Qt 5.11 where the inspector is always enabled and secure.
|
||||
|
||||
=== debug arguments
|
||||
*-l* '{critical,error,warning,info,debug,vdebug}', *--loglevel* '{critical,error,warning,info,debug,vdebug}'::
|
||||
@@ -72,7 +72,7 @@ show it.
|
||||
*--loglines* 'LOGLINES'::
|
||||
How many lines of the debug log to keep in RAM (-1: unlimited).
|
||||
|
||||
*--debug*::
|
||||
*-d*, *--debug*::
|
||||
Turn on debugging options.
|
||||
|
||||
*--json-logging*::
|
||||
@@ -87,7 +87,7 @@ show it.
|
||||
*--nowindow*::
|
||||
Don't show the main window.
|
||||
|
||||
*--temp-basedir*::
|
||||
*-T*, *--temp-basedir*::
|
||||
Use a temporary basedir.
|
||||
|
||||
*--no-err-windows*::
|
||||
@@ -99,7 +99,7 @@ show it.
|
||||
*--qt-flag* 'QT_FLAG'::
|
||||
Pass an argument to Qt as flag.
|
||||
|
||||
*--debug-flag* 'DEBUG_FLAGS'::
|
||||
*-D* 'DEBUG_FLAGS', *--debug-flag* 'DEBUG_FLAGS'::
|
||||
Pass name of debugging feature to be turned on.
|
||||
// QUTE_OPTIONS_END
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 113 KiB |
@@ -1,25 +1,33 @@
|
||||
PYTHON = python3
|
||||
DESTDIR = /
|
||||
PREFIX = /usr/local
|
||||
DESTDIR =
|
||||
ICONSIZES = 16 24 32 48 64 128 256 512
|
||||
|
||||
SETUPTOOLSOPTIONS =
|
||||
ifdef DESTDIR
|
||||
SETUPTOOLSOPTS = --root="$(DESTDIR)"
|
||||
endif
|
||||
|
||||
.PHONY: install
|
||||
|
||||
doc/qutebrowser.1.html:
|
||||
a2x -f manpage doc/qutebrowser.1.asciidoc
|
||||
|
||||
install: doc/qutebrowser.1.html
|
||||
$(PYTHON) setup.py install --root="$(DESTDIR)" --optimize=1
|
||||
$(PYTHON) setup.py install --prefix="$(PREFIX)" --optimize=1 $(SETUPTOOLSOPTS)
|
||||
install -Dm644 misc/qutebrowser.appdata.xml \
|
||||
"$(DESTDIR)$(PREFIX)/share/metainfo/qutebrowser.appdata.xml"
|
||||
install -Dm644 doc/qutebrowser.1 \
|
||||
"$(DESTDIR)/usr/share/man/man1/qutebrowser.1"
|
||||
"$(DESTDIR)$(PREFIX)/share/man/man1/qutebrowser.1"
|
||||
install -Dm644 misc/qutebrowser.desktop \
|
||||
"$(DESTDIR)/usr/share/applications/qutebrowser.desktop"
|
||||
"$(DESTDIR)$(PREFIX)/share/applications/qutebrowser.desktop"
|
||||
$(foreach i,$(ICONSIZES),install -Dm644 "icons/qutebrowser-$(i)x$(i).png" \
|
||||
"$(DESTDIR)/usr/share/icons/hicolor/$(i)x$(i)/apps/qutebrowser.png";)
|
||||
"$(DESTDIR)$(PREFIX)/share/icons/hicolor/$(i)x$(i)/apps/qutebrowser.png";)
|
||||
install -Dm644 icons/qutebrowser.svg \
|
||||
"$(DESTDIR)/usr/share/icons/hicolor/scalable/apps/qutebrowser.svg"
|
||||
install -Dm755 -t "$(DESTDIR)/usr/share/qutebrowser/userscripts/" \
|
||||
"$(DESTDIR)$(PREFIX)/share/icons/hicolor/scalable/apps/qutebrowser.svg"
|
||||
install -Dm755 -t "$(DESTDIR)$(PREFIX)/share/qutebrowser/userscripts/" \
|
||||
$(wildcard misc/userscripts/*)
|
||||
install -Dm755 -t "$(DESTDIR)/usr/share/qutebrowser/scripts/" \
|
||||
install -Dm755 -t "$(DESTDIR)$(PREFIX)/share/qutebrowser/scripts/" \
|
||||
$(filter-out scripts/__init__.py scripts/__pycache__ scripts/dev \
|
||||
scripts/testbrowser scripts/asciidoc2html.py scripts/setupcommon.py \
|
||||
scripts/link_pyqt.py,$(wildcard scripts/*))
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="1.7536248"
|
||||
inkscape:cx="376.55567"
|
||||
inkscape:cx="430.72917"
|
||||
inkscape:cy="268.64059"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
@@ -3710,7 +3710,7 @@
|
||||
style="font-weight:bold;font-size:10.66666698px;line-height:1.25;font-family:sans-serif;-inkscape-font-specification:'Sans Bold';fill:#000000;stroke-width:1.06666672"
|
||||
id="flowPara5701-9-2"><flowSpan
|
||||
style="font-weight:bold;font-family:sans-serif;-inkscape-font-specification:'Sans Bold';fill:#ff0000;stroke-width:1.06666672"
|
||||
id="flowSpan5705-5-1">(10)</flowSpan> toggling settings:</flowPara><flowPara
|
||||
id="flowSpan5705-5-1">(12)</flowSpan> toggling settings:</flowPara><flowPara
|
||||
style="font-size:10.66666698px;line-height:1.25;font-family:sans-serif;fill:#000000;stroke-width:1.06666672"
|
||||
id="flowPara6196">tsh - toggle scripts for the current host (temporarily)</flowPara><flowPara
|
||||
style="font-size:10.66666698px;line-height:1.25;font-family:sans-serif;fill:#000000;stroke-width:1.06666672"
|
||||
|
||||
|
Before Width: | Height: | Size: 181 KiB After Width: | Height: | Size: 181 KiB |
@@ -9,9 +9,9 @@
|
||||
<description>
|
||||
<p>
|
||||
qutebrowser is a keyboard-focused browser with a minimal GUI.
|
||||
It was inspired by other browsers/addons like dwb and Vimperator/Pentadactyl,
|
||||
It was inspired by other browsers/addons like dwb and Vimperator/Pentadactyl,
|
||||
and is based on Python and PyQt5.
|
||||
</p>
|
||||
</p>
|
||||
</description>
|
||||
<categories>
|
||||
<category>Network</category>
|
||||
@@ -40,4 +40,9 @@
|
||||
<url type="help">https://qutebrowser.org/doc/help/</url>
|
||||
<url type="bugtracker">https://github.com/qutebrowser/qutebrowser/issues/</url>
|
||||
<url type="donation">https://github.com/qutebrowser/qutebrowser#donating</url>
|
||||
<releases>
|
||||
<release version="1.3.0" date="2018-05-04"/>
|
||||
<release version="1.2.1" date="2018-03-14"/>
|
||||
<release version="1.2.0" date="2018-03-09"/>
|
||||
</releases>
|
||||
</component>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[Desktop Entry]
|
||||
Name=qutebrowser
|
||||
GenericName=Web Browser
|
||||
Comment=A keyboard-driven, vim-like browser based on PyQt5
|
||||
Icon=qutebrowser
|
||||
Type=Application
|
||||
Categories=Network;WebBrowser;
|
||||
|
||||
@@ -15,7 +15,7 @@ def get_data_files():
|
||||
('../qutebrowser/img', 'img'),
|
||||
('../qutebrowser/javascript', 'javascript'),
|
||||
('../qutebrowser/html/doc', 'html/doc'),
|
||||
('../qutebrowser/git-commit-id', ''),
|
||||
('../qutebrowser/git-commit-id', '.'),
|
||||
('../qutebrowser/config/configdata.yml', 'config'),
|
||||
]
|
||||
|
||||
@@ -58,14 +58,15 @@ exe = EXE(pyz,
|
||||
icon=icon,
|
||||
debug=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
console=False )
|
||||
upx=False,
|
||||
console=False,
|
||||
version='misc/file_version_info.txt')
|
||||
coll = COLLECT(exe,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx=False,
|
||||
name='qutebrowser')
|
||||
|
||||
app = BUNDLE(coll,
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
check-manifest==0.36
|
||||
check-manifest==0.37
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
certifi==2018.1.18
|
||||
certifi==2018.4.16
|
||||
chardet==3.0.4
|
||||
codecov==2.0.15
|
||||
coverage==4.5.1
|
||||
idna==2.6
|
||||
requests==2.18.4
|
||||
idna==2.7
|
||||
requests==2.19.1
|
||||
urllib3==1.22
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
attrs==17.4.0
|
||||
attrs==18.1.0
|
||||
flake8==3.5.0
|
||||
flake8-bugbear==18.2.0
|
||||
flake8-builtins==1.0.post0
|
||||
flake8-builtins==1.4.1 # rq.filter: != 1.4.0
|
||||
flake8-comprehensions==1.4.1
|
||||
flake8-copyright==0.2.0
|
||||
flake8-debugger==3.1.0
|
||||
@@ -11,15 +11,17 @@ flake8-deprecated==1.3
|
||||
flake8-docstrings==1.3.0
|
||||
flake8-future-import==0.4.4
|
||||
flake8-mock==0.3
|
||||
flake8-per-file-ignores==0.4
|
||||
flake8-per-file-ignores==0.6
|
||||
flake8-polyfill==1.0.2
|
||||
flake8-string-format==0.2.3
|
||||
flake8-tidy-imports==1.1.0
|
||||
flake8-tuple==0.2.13
|
||||
mccabe==0.6.1
|
||||
pep8-naming==0.5.0
|
||||
pycodestyle==2.3.1
|
||||
pathmatch==0.2.1
|
||||
pep8-naming==0.7.0
|
||||
pycodestyle==2.3.1 # rq.filter: < 2.4.0
|
||||
pydocstyle==2.1.1
|
||||
pyflakes==1.6.0
|
||||
pyflakes==2.0.0
|
||||
six==1.11.0
|
||||
snowballstemmer==1.2.1
|
||||
typing==3.6.4
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
flake8
|
||||
flake8-bugbear
|
||||
flake8-builtins
|
||||
flake8-builtins!=1.4.0
|
||||
flake8-comprehensions
|
||||
flake8-copyright
|
||||
flake8-debugger
|
||||
@@ -15,3 +15,9 @@ flake8-tuple
|
||||
pep8-naming
|
||||
pydocstyle
|
||||
pyflakes
|
||||
|
||||
# https://github.com/PyCQA/pycodestyle/issues/741
|
||||
#@ filter: pycodestyle < 2.4.0
|
||||
|
||||
# https://github.com/gforcada/flake8-builtins/issues/36
|
||||
#@ filter: flake8-builtins != 1.4.0
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
appdirs==1.4.3
|
||||
packaging==17.1
|
||||
pyparsing==2.2.0
|
||||
setuptools==38.5.1
|
||||
setuptools==40.0.0
|
||||
six==1.11.0
|
||||
wheel==0.30.0
|
||||
wheel==0.31.1
|
||||
|
||||
@@ -4,4 +4,4 @@ altgraph==0.15
|
||||
future==0.16.0
|
||||
macholib==1.9
|
||||
pefile==2017.11.5
|
||||
-e git+https://github.com/pyinstaller/pyinstaller.git@develop#egg=PyInstaller
|
||||
PyInstaller==3.3.1
|
||||
|
||||
@@ -1,4 +1 @@
|
||||
-e git+https://github.com/pyinstaller/pyinstaller.git@develop#egg=PyInstaller
|
||||
|
||||
# remove @commit-id for scm installs
|
||||
#@ replace: @.*# @develop#
|
||||
PyInstaller
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
-e git+https://github.com/PyCQA/astroid.git#egg=astroid
|
||||
certifi==2018.1.18
|
||||
certifi==2018.4.16
|
||||
chardet==3.0.4
|
||||
github3.py==0.9.6
|
||||
idna==2.6
|
||||
github3.py==1.1.0
|
||||
idna==2.7
|
||||
isort==4.3.4
|
||||
lazy-object-proxy==1.3.1
|
||||
mccabe==0.6.1
|
||||
-e git+https://github.com/PyCQA/pylint.git#egg=pylint
|
||||
python-dateutil==2.7.3
|
||||
./scripts/dev/pylint_checkers
|
||||
requests==2.18.4
|
||||
requests==2.19.1
|
||||
six==1.11.0
|
||||
uritemplate==3.0.0
|
||||
uritemplate.py==3.0.2
|
||||
urllib3==1.22
|
||||
wrapt==1.10.11
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
astroid==1.6.1
|
||||
certifi==2018.1.18
|
||||
astroid==1.6.5
|
||||
certifi==2018.4.16
|
||||
chardet==3.0.4
|
||||
github3.py==0.9.6
|
||||
idna==2.6
|
||||
github3.py==1.1.0
|
||||
idna==2.7
|
||||
isort==4.3.4
|
||||
lazy-object-proxy==1.3.1
|
||||
mccabe==0.6.1
|
||||
pylint==1.8.2
|
||||
pylint==1.9.2
|
||||
python-dateutil==2.7.3
|
||||
./scripts/dev/pylint_checkers
|
||||
requests==2.18.4
|
||||
requests==2.19.1
|
||||
six==1.11.0
|
||||
uritemplate==3.0.0
|
||||
uritemplate.py==3.0.2
|
||||
urllib3==1.22
|
||||
wrapt==1.10.11
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
PyQt5==5.10.1
|
||||
sip==4.19.8
|
||||
PyQt5==5.11.2
|
||||
PyQt5-sip==4.19.11
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
docutils==0.14
|
||||
pyroma==2.3
|
||||
pyroma==2.3.1
|
||||
|
||||
@@ -35,8 +35,4 @@ git+https://github.com/pallets/markupsafe.git
|
||||
hg+http://bitbucket.org/birkenfeld/pygments-main
|
||||
hg+https://bitbucket.org/fdik/pypeg
|
||||
git+https://github.com/python-attrs/attrs.git
|
||||
|
||||
# Fails to build:
|
||||
# gcc: error: ext/_yaml.c: No such file or directory
|
||||
# hg+https://bitbucket.org/xi/pyyaml
|
||||
PyYAML==3.12
|
||||
git+https://github.com/yaml/pyyaml.git
|
||||
|
||||
@@ -1,40 +1,40 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
attrs==17.4.0
|
||||
attrs==18.1.0
|
||||
beautifulsoup4==4.6.0
|
||||
cheroot==6.0.0
|
||||
cheroot==6.3.2.post0
|
||||
click==6.7
|
||||
# colorama==0.3.9
|
||||
coverage==4.5.1
|
||||
EasyProcess==0.2.3
|
||||
fields==5.0.0
|
||||
Flask==0.12.2
|
||||
Flask==1.0.2
|
||||
glob2==0.6
|
||||
hunter==2.0.2
|
||||
hypothesis==3.48.0
|
||||
hypothesis==3.66.1
|
||||
itsdangerous==0.24
|
||||
# Jinja2==2.10
|
||||
Mako==1.0.7
|
||||
# MarkupSafe==1.0
|
||||
more-itertools==4.1.0
|
||||
parse==1.8.2
|
||||
more-itertools==4.2.0
|
||||
parse==1.8.4
|
||||
parse-type==0.4.2
|
||||
pluggy==0.6.0
|
||||
py==1.5.2
|
||||
py-cpuinfo==3.3.0
|
||||
pytest==3.4.1
|
||||
pytest-bdd==2.20.0
|
||||
py==1.5.4
|
||||
py-cpuinfo==4.0.0
|
||||
pytest==3.6.3
|
||||
pytest-bdd==2.21.0
|
||||
pytest-benchmark==3.1.1
|
||||
pytest-cov==2.5.1
|
||||
pytest-faulthandler==1.4.1
|
||||
pytest-instafail==0.3.0
|
||||
pytest-mock==1.7.1
|
||||
pytest-qt==2.3.1
|
||||
pytest-faulthandler==1.5.0
|
||||
pytest-instafail==0.4.0
|
||||
pytest-mock==1.10.0
|
||||
pytest-qt==2.4.1
|
||||
pytest-repeat==0.4.1
|
||||
pytest-rerunfailures==4.0
|
||||
pytest-rerunfailures==4.1
|
||||
pytest-travis-fold==1.3.0
|
||||
pytest-xvfb==1.1.0
|
||||
PyVirtualDisplay==0.2.1
|
||||
six==1.11.0
|
||||
vulture==0.26
|
||||
vulture==0.28
|
||||
Werkzeug==0.14.1
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
pluggy==0.6.0
|
||||
py==1.5.2
|
||||
py==1.5.4
|
||||
six==1.11.0
|
||||
tox==2.9.1
|
||||
virtualenv==15.1.0
|
||||
tox==3.1.1
|
||||
virtualenv==16.0.0
|
||||
|
||||
@@ -1,4 +1 @@
|
||||
tox
|
||||
|
||||
# The latest tox release still depends on pluggy < 0.4...
|
||||
pluggy==0.4.0
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
vulture==0.26
|
||||
vulture==0.28
|
||||
|
||||
69
misc/userscripts/getbib
Executable file
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Qutebrowser userscript scraping the current web page for DOIs and downloading
|
||||
corresponding bibtex information.
|
||||
|
||||
Set the environment variable 'QUTE_BIB_FILEPATH' to indicate the path to
|
||||
download to. Otherwise, bibtex information is downloaded to '/tmp' and hence
|
||||
deleted at reboot.
|
||||
|
||||
Installation: see qute://help/userscripts.html
|
||||
|
||||
Inspired by
|
||||
https://ocefpaf.github.io/python4oceanographers/blog/2014/05/19/doi2bibtex/
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
import re
|
||||
from collections import Counter
|
||||
from urllib import parse as url_parse
|
||||
from urllib import request as url_request
|
||||
|
||||
|
||||
FIFO_PATH = os.getenv("QUTE_FIFO")
|
||||
|
||||
def message_fifo(message, level="warning"):
|
||||
"""Send message to qutebrowser FIFO. The level must be one of 'info',
|
||||
'warning' (default) or 'error'."""
|
||||
with open(FIFO_PATH, "w") as fifo:
|
||||
fifo.write("message-{} '{}'".format(level, message))
|
||||
|
||||
|
||||
source = os.getenv("QUTE_TEXT")
|
||||
with open(source) as f:
|
||||
text = f.read()
|
||||
|
||||
# find DOIs on page using regex
|
||||
dval = re.compile(r'(10\.(\d)+/([^(\s\>\"\<)])+)')
|
||||
# https://stackoverflow.com/a/10324802/3865876, too strict
|
||||
# dval = re.compile(r'\b(10[.][0-9]{4,}(?:[.][0-9]+)*/(?:(?!["&\'<>])\S)+)\b')
|
||||
dois = dval.findall(text)
|
||||
dois = Counter(e[0] for e in dois)
|
||||
try:
|
||||
doi = dois.most_common(1)[0][0]
|
||||
except IndexError:
|
||||
message_fifo("No DOIs found on page")
|
||||
sys.exit()
|
||||
message_fifo("Found {} DOIs on page, selecting {}".format(len(dois), doi),
|
||||
level="info")
|
||||
|
||||
# get bibtex data corresponding to DOI
|
||||
url = "http://dx.doi.org/" + url_parse.quote(doi)
|
||||
headers = dict(Accept='text/bibliography; style=bibtex')
|
||||
request = url_request.Request(url, headers=headers)
|
||||
response = url_request.urlopen(request)
|
||||
status_code = response.getcode()
|
||||
if status_code >= 400:
|
||||
message_fifo("Request returned {}".format(status_code))
|
||||
sys.exit()
|
||||
|
||||
# obtain content and format it
|
||||
bibtex = response.read().decode("utf-8").strip()
|
||||
bibtex = bibtex.replace(" ", "\n ", 1).\
|
||||
replace("}, ", "},\n ").replace("}}", "}\n}")
|
||||
|
||||
# append to file
|
||||
bib_filepath = os.getenv("QUTE_BIB_FILEPATH", "/tmp/qute.bib")
|
||||
with open(bib_filepath, "a") as f:
|
||||
f.write(bibtex + "\n\n")
|
||||
261
misc/userscripts/qute-keepass
Executable file
@@ -0,0 +1,261 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright 2018 Jay Kamat <jaygkamat@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 userscript allows for insertion of usernames and passwords from keepass
|
||||
databases using pykeepass. Since it is a userscript, it must be run from
|
||||
qutebrowser.
|
||||
|
||||
A sample invocation of this script is:
|
||||
|
||||
:spawn --userscript qute-keepass -p ~/KeePassFiles/MainDatabase.kdbx
|
||||
|
||||
And a sample binding
|
||||
|
||||
:bind --mode=insert <ctrl-i> spawn --userscript qute-keepass -p ~/KeePassFiles/MainDatabase.kdbx
|
||||
|
||||
-p or --path is a required argument.
|
||||
|
||||
--keyfile-path allows you to specify a keepass keyfile. If you only use a
|
||||
keyfile, also add --no-password as well. Specifying --no-password without
|
||||
--keyfile-path will lead to an error.
|
||||
|
||||
login information is inserted using :insert-text and :fake-key <Tab>, which
|
||||
means you must have a cursor in position before initiating this userscript. If
|
||||
you do not do this, you will get 'element not editable' errors.
|
||||
|
||||
If keepass takes a while to open the DB, you might want to consider reducing
|
||||
the number of transform rounds in your database settings.
|
||||
|
||||
Dependencies: pykeepass (in python3), PyQt5. Without pykeepass, you will get an
|
||||
exit code of 100.
|
||||
|
||||
********************!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!******************
|
||||
|
||||
WARNING: The login details are viewable as plaintext in qutebrowser's debug log
|
||||
(qute://log) and could be compromised if you decide to submit a crash report!
|
||||
|
||||
********************!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!******************
|
||||
|
||||
"""
|
||||
|
||||
# pylint: disable=bad-builtin
|
||||
|
||||
import argparse
|
||||
import enum
|
||||
import functools
|
||||
import os
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from PyQt5.QtCore import QUrl
|
||||
from PyQt5.QtWidgets import QApplication, QInputDialog, QLineEdit
|
||||
|
||||
try:
|
||||
import pykeepass
|
||||
except ImportError as e:
|
||||
print("pykeepass not found: {}".format(str(e)), file=sys.stderr)
|
||||
|
||||
# Since this is a common error, try to print it to the FIFO if we can.
|
||||
if 'QUTE_FIFO' in os.environ:
|
||||
with open(os.environ['QUTE_FIFO'], 'w') as fifo:
|
||||
fifo.write('message-error "pykeepass failed to be imported."\n')
|
||||
fifo.flush()
|
||||
sys.exit(100)
|
||||
|
||||
argument_parser = argparse.ArgumentParser(
|
||||
description="Fill passwords using keepass.",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog=__doc__)
|
||||
argument_parser.add_argument('url', nargs='?', default=os.getenv('QUTE_URL'))
|
||||
argument_parser.add_argument('--path', '-p', required=True,
|
||||
help='Path to the keepass db.')
|
||||
argument_parser.add_argument('--keyfile-path', '-k', default=None,
|
||||
help='Path to a keepass keyfile')
|
||||
argument_parser.add_argument(
|
||||
'--no-password', action='store_true',
|
||||
help='Supply if no password is required to unlock this database. '
|
||||
'Only allowed with --keyfile-path')
|
||||
argument_parser.add_argument(
|
||||
'--dmenu-invocation', '-d', default='dmenu',
|
||||
help='Invocation used to execute a dmenu-provider')
|
||||
argument_parser.add_argument(
|
||||
'--dmenu-format', '-f', default='{title}: {username}',
|
||||
help='Format string for keys to display in dmenu.'
|
||||
' Must generate a unique string.')
|
||||
argument_parser.add_argument(
|
||||
'--no-insert-mode', '-n', dest='insert_mode', action='store_false',
|
||||
help="Don't automatically enter insert mode")
|
||||
argument_parser.add_argument(
|
||||
'--io-encoding', '-i', default='UTF-8',
|
||||
help='Encoding used to communicate with subprocesses')
|
||||
group = argument_parser.add_mutually_exclusive_group()
|
||||
group.add_argument('--username-fill-only', '-e',
|
||||
action='store_true', help='Only insert username')
|
||||
group.add_argument('--password-fill-only', '-w',
|
||||
action='store_true', help='Only insert password')
|
||||
|
||||
CMD_DELAY = 50
|
||||
|
||||
|
||||
class ExitCodes(enum.IntEnum):
|
||||
"""Stores various exit codes groups to use."""
|
||||
SUCCESS = 0
|
||||
FAILURE = 1
|
||||
# 1 is automatically used if Python throws an exception
|
||||
NO_CANDIDATES = 2
|
||||
USER_QUIT = 3
|
||||
DB_OPEN_FAIL = 4
|
||||
|
||||
INTERNAL_ERROR = 10
|
||||
|
||||
|
||||
def qute_command(command):
|
||||
with open(os.environ['QUTE_FIFO'], 'w') as fifo:
|
||||
fifo.write(command + '\n')
|
||||
fifo.flush()
|
||||
|
||||
|
||||
def stderr(to_print):
|
||||
"""Extra functionality to echo out errors to qb ui."""
|
||||
print(to_print, file=sys.stderr)
|
||||
qute_command('message-error "{}"'.format(to_print))
|
||||
|
||||
|
||||
def dmenu(items, invocation, encoding):
|
||||
"""Runs dmenu with given arguments."""
|
||||
command = shlex.split(invocation)
|
||||
process = subprocess.run(command, input='\n'.join(items).encode(encoding),
|
||||
stdout=subprocess.PIPE)
|
||||
return process.stdout.decode(encoding).strip()
|
||||
|
||||
|
||||
def get_password():
|
||||
"""Get a keepass db password from user."""
|
||||
_app = QApplication(sys.argv)
|
||||
text, ok = QInputDialog.getText(
|
||||
None, "KeePass DB Password",
|
||||
"Please enter your KeePass Master Password",
|
||||
QLineEdit.Password)
|
||||
if not ok:
|
||||
stderr('Password Prompt Rejected.')
|
||||
sys.exit(ExitCodes.USER_QUIT)
|
||||
return text
|
||||
|
||||
|
||||
def find_candidates(args, host):
|
||||
"""Finds candidates that match host"""
|
||||
file_path = os.path.expanduser(args.path)
|
||||
|
||||
# TODO find a way to keep the db open, so we don't open (and query
|
||||
# password) it every time
|
||||
|
||||
pw = None
|
||||
if not args.no_password:
|
||||
pw = get_password()
|
||||
|
||||
kf = args.keyfile_path
|
||||
if kf:
|
||||
kf = os.path.expanduser(kf)
|
||||
|
||||
try:
|
||||
kp = pykeepass.PyKeePass(file_path, password=pw, keyfile=kf)
|
||||
except Exception as e:
|
||||
stderr("There was an error opening the DB: {}".format(str(e)))
|
||||
|
||||
return kp.find_entries(url="{}{}{}".format(".*", host, ".*"), regex=True)
|
||||
|
||||
|
||||
def candidate_to_str(args, candidate):
|
||||
"""Turns candidate into a human readable string for dmenu"""
|
||||
return args.dmenu_format.format(title=candidate.title,
|
||||
url=candidate.url,
|
||||
username=candidate.username,
|
||||
path=candidate.path,
|
||||
uuid=candidate.uuid)
|
||||
|
||||
|
||||
def candidate_to_secret(candidate):
|
||||
"""Turns candidate into a generic (user, password) tuple"""
|
||||
return (candidate.username, candidate.password)
|
||||
|
||||
|
||||
def run(args):
|
||||
"""Runs qute-keepass"""
|
||||
if not args.url:
|
||||
argument_parser.print_help()
|
||||
return ExitCodes.FAILURE
|
||||
|
||||
url_host = QUrl(args.url).host()
|
||||
|
||||
if not url_host:
|
||||
stderr('{} was not parsed as a valid URL!'.format(args.url))
|
||||
return ExitCodes.INTERNAL_ERROR
|
||||
|
||||
# Find candidates matching the host of the given URL
|
||||
candidates = find_candidates(args, url_host)
|
||||
if not candidates:
|
||||
stderr('No candidates for URL {!r} found!'.format(args.url))
|
||||
return ExitCodes.NO_CANDIDATES
|
||||
|
||||
# Create a map so we can get turn the resulting string from dmenu back into
|
||||
# a candidate
|
||||
candidates_strs = list(map(functools.partial(candidate_to_str, args),
|
||||
candidates))
|
||||
candidates_map = dict(zip(candidates_strs, candidates))
|
||||
|
||||
if len(candidates) == 1:
|
||||
selection = candidates.pop()
|
||||
else:
|
||||
selection = dmenu(candidates_strs,
|
||||
args.dmenu_invocation,
|
||||
args.io_encoding)
|
||||
|
||||
if selection not in candidates_map:
|
||||
stderr("'{}' was not a valid entry!").format(selection)
|
||||
return ExitCodes.USER_QUIT
|
||||
|
||||
selection = candidates_map[selection]
|
||||
|
||||
username, password = candidate_to_secret(selection)
|
||||
|
||||
insert_mode = ';; enter-mode insert' if args.insert_mode else ''
|
||||
if args.username_fill_only:
|
||||
qute_command('insert-text {}{}'.format(username, insert_mode))
|
||||
elif args.password_fill_only:
|
||||
qute_command('insert-text {}{}'.format(password, insert_mode))
|
||||
else:
|
||||
# Enter username and password using insert-key and fake-key <Tab>
|
||||
# (which supports more passwords than fake-key only), then switch back
|
||||
# into insert-mode, so the form can be directly submitted by hitting
|
||||
# enter afterwards. It dosen't matter when we go into insert mode, but
|
||||
# the other commands need to be be executed sequentially, so we add
|
||||
# delays with later.
|
||||
qute_command('insert-text {} ;;'
|
||||
'later {} fake-key <Tab> ;;'
|
||||
'later {} insert-text {}{}'
|
||||
.format(username, CMD_DELAY,
|
||||
CMD_DELAY * 2, password, insert_mode))
|
||||
|
||||
return ExitCodes.SUCCESS
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
arguments = argument_parser.parse_args()
|
||||
sys.exit(run(arguments))
|
||||
172
misc/userscripts/qute-lastpass
Executable file
@@ -0,0 +1,172 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright 2017 Chris Braun (cryzed) <cryzed@googlemail.com>
|
||||
# Adapted for LastPass by Wayne Cheng (welps) <waynethecheng@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 bjy
|
||||
# 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/>.
|
||||
|
||||
"""
|
||||
Insert login information using lastpass CLI and a dmenu-compatible application (e.g. dmenu, rofi -dmenu, ...).
|
||||
A short demonstration can be seen here: https://i.imgur.com/zA61NrF.gifv.
|
||||
"""
|
||||
|
||||
USAGE = """The domain of the site has to be in the name of the LastPass entry, for example: "github.com/cryzed" or
|
||||
"websites/github.com". The login information is inserted by emulating key events using qutebrowser's fake-key command in this manner:
|
||||
[USERNAME]<Tab>[PASSWORD], which is compatible with almost all login forms.
|
||||
|
||||
You must log into LastPass CLI using `lpass login <email>` prior to use of this script. The LastPass CLI agent only holds your master password for an hour by default. If you wish to change this, please see `man lpass`.
|
||||
|
||||
To use in qutebrowser, run: `spawn --userscript qute-lastpass`
|
||||
"""
|
||||
|
||||
EPILOG = """Dependencies: tldextract (Python 3 module), LastPass CLI (1.3 or newer)
|
||||
|
||||
WARNING: The login details are viewable as plaintext in qutebrowser's debug log (qute://log) and might be shared if
|
||||
you decide to submit a crash report!"""
|
||||
|
||||
import argparse
|
||||
import enum
|
||||
import fnmatch
|
||||
import functools
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
import json
|
||||
import tldextract
|
||||
|
||||
argument_parser = argparse.ArgumentParser(
|
||||
description=__doc__, usage=USAGE, epilog=EPILOG)
|
||||
argument_parser.add_argument('url', nargs='?', default=os.getenv('QUTE_URL'))
|
||||
argument_parser.add_argument('--dmenu-invocation', '-d', default='rofi -dmenu',
|
||||
help='Invocation used to execute a dmenu-provider')
|
||||
argument_parser.add_argument('--no-insert-mode', '-n', dest='insert_mode', action='store_false',
|
||||
help="Don't automatically enter insert mode")
|
||||
argument_parser.add_argument('--io-encoding', '-i', default='UTF-8',
|
||||
help='Encoding used to communicate with subprocesses')
|
||||
argument_parser.add_argument('--merge-candidates', '-m', action='store_true',
|
||||
help='Merge pass candidates for fully-qualified and registered domain name')
|
||||
group = argument_parser.add_mutually_exclusive_group()
|
||||
group.add_argument('--username-only', '-e',
|
||||
action='store_true', help='Only insert username')
|
||||
group.add_argument('--password-only', '-w',
|
||||
action='store_true', help='Only insert password')
|
||||
|
||||
stderr = functools.partial(print, file=sys.stderr)
|
||||
|
||||
class ExitCodes(enum.IntEnum):
|
||||
SUCCESS = 0
|
||||
FAILURE = 1
|
||||
# 1 is automatically used if Python throws an exception
|
||||
NO_PASS_CANDIDATES = 2
|
||||
COULD_NOT_MATCH_USERNAME = 3
|
||||
COULD_NOT_MATCH_PASSWORD = 4
|
||||
|
||||
def qute_command(command):
|
||||
with open(os.environ['QUTE_FIFO'], 'w') as fifo:
|
||||
fifo.write(command + '\n')
|
||||
fifo.flush()
|
||||
|
||||
def pass_(domain, encoding):
|
||||
args = ['lpass', 'show', '-x', '-j', '-G', '.*{:s}.*'.format(domain)]
|
||||
process = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
|
||||
err = process.stderr.decode(encoding).strip()
|
||||
if err:
|
||||
msg = "LastPass CLI returned for {:s} - {:s}".format(domain, err)
|
||||
stderr(msg)
|
||||
return '[]'
|
||||
|
||||
out = process.stdout.decode(encoding).strip()
|
||||
|
||||
return out
|
||||
|
||||
def dmenu(items, invocation, encoding):
|
||||
command = shlex.split(invocation)
|
||||
process = subprocess.run(command, input='\n'.join(
|
||||
items).encode(encoding), stdout=subprocess.PIPE)
|
||||
return process.stdout.decode(encoding).strip()
|
||||
|
||||
|
||||
def fake_key_raw(text):
|
||||
for character in text:
|
||||
# Escape all characters by default, space requires special handling
|
||||
sequence = '" "' if character == ' ' else '\{}'.format(character)
|
||||
qute_command('fake-key {}'.format(sequence))
|
||||
|
||||
|
||||
def main(arguments):
|
||||
if not arguments.url:
|
||||
argument_parser.print_help()
|
||||
return ExitCodes.FAILURE
|
||||
|
||||
extract_result = tldextract.extract(arguments.url)
|
||||
|
||||
# Try to find candidates using targets in the following order: fully-qualified domain name (includes subdomains),
|
||||
# the registered domain name and finally: the IPv4 address if that's what
|
||||
# the URL represents
|
||||
candidates = []
|
||||
for target in filter(None, [extract_result.fqdn, extract_result.registered_domain, extract_result.subdomain + extract_result.domain, extract_result.domain, extract_result.ipv4]):
|
||||
target_candidates = json.loads(pass_(target, arguments.io_encoding))
|
||||
if not target_candidates:
|
||||
continue
|
||||
|
||||
candidates = candidates + target_candidates
|
||||
if not arguments.merge_candidates:
|
||||
break
|
||||
else:
|
||||
if not candidates:
|
||||
stderr('No pass candidates for URL {!r} found!'.format(
|
||||
arguments.url))
|
||||
return ExitCodes.NO_PASS_CANDIDATES
|
||||
|
||||
if len(candidates) == 1:
|
||||
selection = candidates.pop()
|
||||
else:
|
||||
choices = ["{:s} | {:s} | {:s} | {:s}".format(c["id"], c["name"], c["url"], c["username"]) for c in candidates]
|
||||
choice = dmenu(choices, arguments.dmenu_invocation, arguments.io_encoding)
|
||||
choiceId = choice.split("|")[0].strip()
|
||||
selection = next((c for (i, c) in enumerate(candidates) if c["id"] == choiceId), None)
|
||||
|
||||
# Nothing was selected, simply return
|
||||
if not selection:
|
||||
return ExitCodes.SUCCESS
|
||||
|
||||
username = selection["username"]
|
||||
password = selection["password"]
|
||||
|
||||
if arguments.username_only:
|
||||
fake_key_raw(username)
|
||||
elif arguments.password_only:
|
||||
fake_key_raw(password)
|
||||
else:
|
||||
# Enter username and password using fake-key and <Tab> (which seems to work almost universally), then switch
|
||||
# back into insert-mode, so the form can be directly submitted by
|
||||
# hitting enter afterwards
|
||||
fake_key_raw(username)
|
||||
qute_command('fake-key <Tab>')
|
||||
fake_key_raw(password)
|
||||
|
||||
if arguments.insert_mode:
|
||||
qute_command('enter-mode insert')
|
||||
|
||||
return ExitCodes.SUCCESS
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
arguments = argument_parser.parse_args()
|
||||
sys.exit(main(arguments))
|
||||
@@ -25,9 +25,17 @@ demonstration can be seen here: https://i.imgur.com/KN3XuZP.gif.
|
||||
USAGE = """The domain of the site has to appear as a segment in the pass path, for example: "github.com/cryzed" or
|
||||
"websites/github.com". How the username and password are determined is freely configurable using the CLI arguments. The
|
||||
login information is inserted by emulating key events using qutebrowser's fake-key command in this manner:
|
||||
[USERNAME]<Tab>[PASSWORD], which is compatible with almost all login forms."""
|
||||
[USERNAME]<Tab>[PASSWORD], which is compatible with almost all login forms.
|
||||
|
||||
EPILOG = """Dependencies: tldextract (Python 3 module), pass.
|
||||
Suggested bindings similar to Uzbl's `formfiller` script:
|
||||
|
||||
config.bind('<z><l>', 'spawn --userscript qute-pass')
|
||||
config.bind('<z><u><l>', 'spawn --userscript qute-pass --username-only')
|
||||
config.bind('<z><p><l>', 'spawn --userscript qute-pass --password-only')
|
||||
config.bind('<z><o><l>', 'spawn --userscript qute-pass --otp-only')
|
||||
"""
|
||||
|
||||
EPILOG = """Dependencies: tldextract (Python 3 module), pass, pass-otp (optional).
|
||||
For issues and feedback please use: https://github.com/cryzed/qutebrowser-userscripts.
|
||||
|
||||
WARNING: The login details are viewable as plaintext in qutebrowser's debug log (qute://log) and might be shared if
|
||||
@@ -66,6 +74,7 @@ argument_parser.add_argument('--merge-candidates', '-m', action='store_true',
|
||||
group = argument_parser.add_mutually_exclusive_group()
|
||||
group.add_argument('--username-only', '-e', action='store_true', help='Only insert username')
|
||||
group.add_argument('--password-only', '-w', action='store_true', help='Only insert password')
|
||||
group.add_argument('--otp-only', '-o', action='store_true', help='Only insert OTP code')
|
||||
|
||||
stderr = functools.partial(print, file=sys.stderr)
|
||||
|
||||
@@ -98,17 +107,32 @@ def find_pass_candidates(domain, password_store_path):
|
||||
return candidates
|
||||
|
||||
|
||||
def pass_(path, encoding):
|
||||
process = subprocess.run(['pass', path], stdout=subprocess.PIPE)
|
||||
def _run_pass(command, encoding):
|
||||
process = subprocess.run(command, stdout=subprocess.PIPE)
|
||||
return process.stdout.decode(encoding).strip()
|
||||
|
||||
|
||||
def pass_(path, encoding):
|
||||
return _run_pass(['pass', path], encoding)
|
||||
|
||||
|
||||
def pass_otp(path, encoding):
|
||||
return _run_pass(['pass', 'otp', path], encoding)
|
||||
|
||||
|
||||
def dmenu(items, invocation, encoding):
|
||||
command = shlex.split(invocation)
|
||||
process = subprocess.run(command, input='\n'.join(items).encode(encoding), stdout=subprocess.PIPE)
|
||||
return process.stdout.decode(encoding).strip()
|
||||
|
||||
|
||||
def fake_key_raw(text):
|
||||
for character in text:
|
||||
# Escape all characters by default, space requires special handling
|
||||
sequence = '" "' if character == ' ' else '\{}'.format(character)
|
||||
qute_command('fake-key {}'.format(sequence))
|
||||
|
||||
|
||||
def main(arguments):
|
||||
if not arguments.url:
|
||||
argument_parser.print_help()
|
||||
@@ -158,15 +182,22 @@ def main(arguments):
|
||||
return ExitCodes.COULD_NOT_MATCH_PASSWORD
|
||||
password = match.group(1)
|
||||
|
||||
insert_mode = ';; enter-mode insert' if arguments.insert_mode else ''
|
||||
if arguments.username_only:
|
||||
qute_command('fake-key {}{}'.format(username, insert_mode))
|
||||
fake_key_raw(username)
|
||||
elif arguments.password_only:
|
||||
qute_command('fake-key {}{}'.format(password, insert_mode))
|
||||
fake_key_raw(password)
|
||||
elif arguments.otp_only:
|
||||
otp = pass_otp(selection, arguments.io_encoding)
|
||||
fake_key_raw(otp)
|
||||
else:
|
||||
# Enter username and password using fake-key and <Tab> (which seems to work almost universally), then switch
|
||||
# back into insert-mode, so the form can be directly submitted by hitting enter afterwards
|
||||
qute_command('fake-key {} ;; fake-key <Tab> ;; fake-key {}{}'.format(username, password, insert_mode))
|
||||
fake_key_raw(username)
|
||||
qute_command('fake-key <Tab>')
|
||||
fake_key_raw(password)
|
||||
|
||||
if arguments.insert_mode:
|
||||
qute_command('enter-mode insert')
|
||||
|
||||
return ExitCodes.SUCCESS
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
if msg="$(task add "$title" "$*" 2>&1)"; then
|
||||
# annotate the new task with the url, send the output back to the browser
|
||||
task +LATEST annotate "$QUTE_URL"
|
||||
echo "message-info '$msg'" >> "$QUTE_FIFO"
|
||||
echo "message-info '$(echo "$msg" | head -n 1)'" >> "$QUTE_FIFO"
|
||||
else
|
||||
echo "message-error '$msg'" >> "$QUTE_FIFO"
|
||||
echo "message-error '$(echo "$msg" | head -n 1)'" >> "$QUTE_FIFO"
|
||||
fi
|
||||
|
||||
@@ -49,7 +49,7 @@ msg() {
|
||||
|
||||
MPV_COMMAND=${MPV_COMMAND:-mpv}
|
||||
# Warning: spaces in single flags are not supported
|
||||
MPV_FLAGS=${MPV_FLAGS:- --force-window --no-terminal --keep-open=yes --ytdl --ytdl-raw-options=yes-playlist=}
|
||||
MPV_FLAGS=${MPV_FLAGS:- --force-window --no-terminal --keep-open=yes --ytdl}
|
||||
IFS=" " read -r -a video_command <<< "$MPV_COMMAND $MPV_FLAGS"
|
||||
|
||||
js() {
|
||||
|
||||
@@ -30,6 +30,7 @@ markers =
|
||||
qtbug60673: Tests which are broken if the conversion from orange selection to real selection is flaky
|
||||
fake_os: Fake utils.is_* to a fake operating system
|
||||
unicode_locale: Tests which need an unicode locale to work
|
||||
qtwebkit6021_skip: Tests which would fail on WebKit version 602.1
|
||||
qt_log_level_fail = WARNING
|
||||
qt_log_ignore =
|
||||
^SpellCheck: .*
|
||||
@@ -62,4 +63,8 @@ qt_log_ignore =
|
||||
^inotify_add_watch\(".*"\) failed: "No space left on device"
|
||||
^QSettings::value: Empty key passed
|
||||
^Icon theme ".*" not found
|
||||
^Error receiving trust for a CA certificate
|
||||
xfail_strict = true
|
||||
filterwarnings =
|
||||
# This happens in many qutebrowser dependencies...
|
||||
ignore:Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated, and in 3.8 it will stop working:DeprecationWarning
|
||||
|
||||
@@ -26,7 +26,7 @@ __copyright__ = "Copyright 2014-2018 Florian Bruhin (The Compiler)"
|
||||
__license__ = "GPL"
|
||||
__maintainer__ = __author__
|
||||
__email__ = "mail@qutebrowser.org"
|
||||
__version_info__ = (1, 2, 0)
|
||||
__version_info__ = (1, 4, 0)
|
||||
__version__ = '.'.join(str(e) for e in __version_info__)
|
||||
__description__ = "A keyboard-driven, vim-like browser based on PyQt5."
|
||||
|
||||
|
||||
@@ -340,7 +340,7 @@ def _open_startpage(win_id=None):
|
||||
for cur_win_id in list(window_ids): # Copying as the dict could change
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=cur_win_id)
|
||||
if tabbed_browser.count() == 0:
|
||||
if tabbed_browser.widget.count() == 0:
|
||||
log.init.debug("Opening start pages")
|
||||
for url in config.val.url.start_pages:
|
||||
tabbed_browser.tabopen(url)
|
||||
@@ -681,7 +681,6 @@ class Quitter:
|
||||
@cmdutils.argument('session', completion=miscmodels.session)
|
||||
def quit(self, save=False, session=None):
|
||||
"""Quit qutebrowser.
|
||||
|
||||
Args:
|
||||
save: When given, save the open windows even if auto_save.session
|
||||
is turned off.
|
||||
|
||||
@@ -94,14 +94,8 @@ class HostBlocker:
|
||||
_done_count: How many files have been read successfully.
|
||||
_local_hosts_file: The path to the blocked-hosts file.
|
||||
_config_hosts_file: The path to a blocked-hosts in ~/.config
|
||||
|
||||
Class attributes:
|
||||
WHITELISTED: Hosts which never should be blocked.
|
||||
"""
|
||||
|
||||
WHITELISTED = ('localhost', 'localhost.localdomain', 'broadcasthost',
|
||||
'local')
|
||||
|
||||
def __init__(self):
|
||||
self._blocked_hosts = set()
|
||||
self._config_blocked_hosts = set()
|
||||
@@ -234,16 +228,14 @@ class HostBlocker:
|
||||
parts = line.split()
|
||||
if len(parts) == 1:
|
||||
# "one host per line" format
|
||||
host = parts[0]
|
||||
elif len(parts) == 2:
|
||||
# /etc/hosts format
|
||||
host = parts[1]
|
||||
hosts = [parts[0]]
|
||||
else:
|
||||
log.misc.error("Failed to parse: {!r}".format(line))
|
||||
return False
|
||||
# /etc/hosts format
|
||||
hosts = parts[1:]
|
||||
|
||||
if host not in self.WHITELISTED:
|
||||
self._blocked_hosts.add(host)
|
||||
for host in hosts:
|
||||
if '.' in host and not host.endswith('.localdomain'):
|
||||
self._blocked_hosts.add(host)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -22,18 +22,22 @@
|
||||
import enum
|
||||
import itertools
|
||||
|
||||
import sip
|
||||
import attr
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QUrl, QObject, QSizeF, Qt
|
||||
from PyQt5.QtGui import QIcon
|
||||
from PyQt5.QtWidgets import QWidget, QApplication
|
||||
|
||||
import pygments
|
||||
import pygments.lexers
|
||||
import pygments.formatters
|
||||
|
||||
from qutebrowser.keyinput import modeman
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import (utils, objreg, usertypes, log, qtutils,
|
||||
urlutils, message)
|
||||
from qutebrowser.misc import miscwidgets, objects
|
||||
from qutebrowser.browser import mouse, hints
|
||||
from qutebrowser.qt import sip
|
||||
|
||||
|
||||
tab_id_gen = itertools.count(0)
|
||||
@@ -95,6 +99,8 @@ class TabData:
|
||||
keep_icon: Whether the (e.g. cloned) icon should not be cleared on page
|
||||
load.
|
||||
inspector: The QWebInspector used for this webview.
|
||||
viewing_source: Set if we're currently showing a source view.
|
||||
Only used when sources are shown via pygments.
|
||||
open_target: Where to open the next link.
|
||||
Only used for QtWebKit.
|
||||
override_target: Override for open_target for fake clicks (like hints).
|
||||
@@ -106,6 +112,7 @@ class TabData:
|
||||
"""
|
||||
|
||||
keep_icon = attr.ib(False)
|
||||
viewing_source = attr.ib(False)
|
||||
inspector = attr.ib(None)
|
||||
open_target = attr.ib(usertypes.ClickTarget.normal)
|
||||
override_target = attr.ib(None)
|
||||
@@ -114,6 +121,10 @@ class TabData:
|
||||
netrc_used = attr.ib(False)
|
||||
input_mode = attr.ib(usertypes.KeyMode.normal)
|
||||
|
||||
def should_show_icon(self):
|
||||
return (config.val.tabs.favicons.show == 'always' or
|
||||
config.val.tabs.favicons.show == 'pinned' and self.pinned)
|
||||
|
||||
|
||||
class AbstractAction:
|
||||
|
||||
@@ -146,10 +157,31 @@ class AbstractAction:
|
||||
raise WebTabError("{} is not a valid web action!".format(name))
|
||||
self._widget.triggerPageAction(member)
|
||||
|
||||
def show_source(self):
|
||||
def show_source(self,
|
||||
pygments=False): # pylint: disable=redefined-outer-name
|
||||
"""Show the source of the current page in a new tab."""
|
||||
raise NotImplementedError
|
||||
|
||||
def _show_source_pygments(self):
|
||||
|
||||
def show_source_cb(source):
|
||||
"""Show source as soon as it's ready."""
|
||||
# WORKAROUND for https://github.com/PyCQA/pylint/issues/491
|
||||
# pylint: disable=no-member
|
||||
lexer = pygments.lexers.HtmlLexer()
|
||||
formatter = pygments.formatters.HtmlFormatter(
|
||||
full=True, linenos='table')
|
||||
# pylint: enable=no-member
|
||||
highlighted = pygments.highlight(source, lexer, formatter)
|
||||
|
||||
tb = objreg.get('tabbed-browser', scope='window',
|
||||
window=self._tab.win_id)
|
||||
new_tab = tb.tabopen(background=False, related=True)
|
||||
new_tab.set_html(highlighted, self._tab.url())
|
||||
new_tab.data.viewing_source = True
|
||||
|
||||
self._tab.dump_async(show_source_cb)
|
||||
|
||||
|
||||
class AbstractPrinting:
|
||||
|
||||
@@ -333,7 +365,14 @@ class AbstractZoom(QObject):
|
||||
|
||||
class AbstractCaret(QObject):
|
||||
|
||||
"""Attribute of AbstractTab for caret browsing."""
|
||||
"""Attribute of AbstractTab for caret browsing.
|
||||
|
||||
Signals:
|
||||
selection_toggled: Emitted when the selection was toggled.
|
||||
arg: Whether the selection is now active.
|
||||
"""
|
||||
|
||||
selection_toggled = pyqtSignal(bool)
|
||||
|
||||
def __init__(self, tab, mode_manager, parent=None):
|
||||
super().__init__(parent)
|
||||
@@ -403,6 +442,13 @@ class AbstractCaret(QObject):
|
||||
def selection(self, callback):
|
||||
raise NotImplementedError
|
||||
|
||||
def _follow_enter(self, tab):
|
||||
"""Follow a link by faking an enter press."""
|
||||
if tab:
|
||||
self._tab.key_press(Qt.Key_Enter, modifier=Qt.ControlModifier)
|
||||
else:
|
||||
self._tab.key_press(Qt.Key_Enter)
|
||||
|
||||
def follow_selected(self, *, tab=False):
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -439,6 +485,9 @@ class AbstractScroller(QObject):
|
||||
def to_point(self, point):
|
||||
raise NotImplementedError
|
||||
|
||||
def to_anchor(self, name):
|
||||
raise NotImplementedError
|
||||
|
||||
def delta(self, x=0, y=0):
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -585,6 +634,33 @@ class AbstractElements:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class AbstractAudio(QObject):
|
||||
|
||||
"""Handling of audio/muting for this tab."""
|
||||
|
||||
muted_changed = pyqtSignal(bool)
|
||||
recently_audible_changed = pyqtSignal(bool)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._widget = None
|
||||
|
||||
def set_muted(self, muted: bool):
|
||||
"""Set this tab as muted or not."""
|
||||
raise NotImplementedError
|
||||
|
||||
def is_muted(self):
|
||||
"""Whether this tab is muted."""
|
||||
raise NotImplementedError
|
||||
|
||||
def toggle_muted(self):
|
||||
self.set_muted(not self.is_muted())
|
||||
|
||||
def is_recently_audible(self):
|
||||
"""Whether this tab has had audio playing recently."""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class AbstractTab(QWidget):
|
||||
|
||||
"""A wrapper over the given widget to hide its API and expose another one.
|
||||
@@ -665,8 +741,7 @@ class AbstractTab(QWidget):
|
||||
objreg.register('hintmanager', hintmanager, scope='tab',
|
||||
window=self.win_id, tab=self.tab_id)
|
||||
|
||||
self.predicted_navigation.connect(
|
||||
lambda url: self.title_changed.emit(url.toDisplayString()))
|
||||
self.predicted_navigation.connect(self._on_predicted_navigation)
|
||||
|
||||
def _set_widget(self, widget):
|
||||
# pylint: disable=protected-access
|
||||
@@ -680,6 +755,7 @@ class AbstractTab(QWidget):
|
||||
self.printing._widget = widget
|
||||
self.action._widget = widget
|
||||
self.elements._widget = widget
|
||||
self.audio._widget = widget
|
||||
self.settings._settings = widget.settings()
|
||||
|
||||
self._install_event_filter()
|
||||
@@ -711,10 +787,24 @@ class AbstractTab(QWidget):
|
||||
if getattr(evt, 'posted', False):
|
||||
raise utils.Unreachable("Can't re-use an event which was already "
|
||||
"posted!")
|
||||
|
||||
recipient = self.event_target()
|
||||
if recipient is None:
|
||||
# https://github.com/qutebrowser/qutebrowser/issues/3888
|
||||
log.webview.warning("Unable to find event target!")
|
||||
return
|
||||
|
||||
evt.posted = True
|
||||
QApplication.postEvent(recipient, evt)
|
||||
|
||||
@pyqtSlot(QUrl)
|
||||
def _on_predicted_navigation(self, url):
|
||||
"""Adjust the title if we are going to visit an URL soon."""
|
||||
qtutils.ensure_valid(url)
|
||||
url_string = url.toDisplayString()
|
||||
log.webview.debug("Predicted navigation: {}".format(url_string))
|
||||
self.title_changed.emit(url_string)
|
||||
|
||||
@pyqtSlot(QUrl)
|
||||
def _on_url_changed(self, url):
|
||||
"""Update title when URL has changed and no title is available."""
|
||||
@@ -726,6 +816,7 @@ class AbstractTab(QWidget):
|
||||
def _on_load_started(self):
|
||||
self._progress = 0
|
||||
self._has_ssl_errors = False
|
||||
self.data.viewing_source = False
|
||||
self._set_load_status(usertypes.LoadStatus.loading)
|
||||
self.load_started.emit()
|
||||
|
||||
@@ -802,10 +893,6 @@ class AbstractTab(QWidget):
|
||||
self._progress = perc
|
||||
self.load_progress.emit(perc)
|
||||
|
||||
@pyqtSlot()
|
||||
def _on_ssl_errors(self):
|
||||
self._has_ssl_errors = True
|
||||
|
||||
def url(self, requested=False):
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -815,11 +902,12 @@ class AbstractTab(QWidget):
|
||||
def load_status(self):
|
||||
return self._load_status
|
||||
|
||||
def _openurl_prepare(self, url):
|
||||
def _openurl_prepare(self, url, *, predict=True):
|
||||
qtutils.ensure_valid(url)
|
||||
self.predicted_navigation.emit(url)
|
||||
if predict:
|
||||
self.predicted_navigation.emit(url)
|
||||
|
||||
def openurl(self, url):
|
||||
def openurl(self, url, *, predict=True):
|
||||
raise NotImplementedError
|
||||
|
||||
def reload(self, *, force=False):
|
||||
|
||||
@@ -53,7 +53,6 @@ class CommandDispatcher:
|
||||
cmdutils.register() decorators are run, currentWidget() will return None.
|
||||
|
||||
Attributes:
|
||||
_editor: The ExternalEditor object.
|
||||
_win_id: The window ID the CommandDispatcher is associated with.
|
||||
_tabbed_browser: The TabbedBrowser used.
|
||||
"""
|
||||
@@ -73,16 +72,16 @@ class CommandDispatcher:
|
||||
|
||||
def _count(self):
|
||||
"""Convenience method to get the widget count."""
|
||||
return self._tabbed_browser.count()
|
||||
return self._tabbed_browser.widget.count()
|
||||
|
||||
def _set_current_index(self, idx):
|
||||
"""Convenience method to set the current widget index."""
|
||||
cmdutils.check_overflow(idx, 'int')
|
||||
self._tabbed_browser.setCurrentIndex(idx)
|
||||
self._tabbed_browser.widget.setCurrentIndex(idx)
|
||||
|
||||
def _current_index(self):
|
||||
"""Convenience method to get the current widget index."""
|
||||
return self._tabbed_browser.currentIndex()
|
||||
return self._tabbed_browser.widget.currentIndex()
|
||||
|
||||
def _current_url(self):
|
||||
"""Convenience method to get the current url."""
|
||||
@@ -101,7 +100,7 @@ class CommandDispatcher:
|
||||
|
||||
def _current_widget(self):
|
||||
"""Get the currently active widget from a command."""
|
||||
widget = self._tabbed_browser.currentWidget()
|
||||
widget = self._tabbed_browser.widget.currentWidget()
|
||||
if widget is None:
|
||||
raise cmdexc.CommandError("No WebView available yet!")
|
||||
return widget
|
||||
@@ -147,10 +146,10 @@ class CommandDispatcher:
|
||||
None if no widget was found.
|
||||
"""
|
||||
if count is None:
|
||||
return self._tabbed_browser.currentWidget()
|
||||
return self._tabbed_browser.widget.currentWidget()
|
||||
elif 1 <= count <= self._count():
|
||||
cmdutils.check_overflow(count + 1, 'int')
|
||||
return self._tabbed_browser.widget(count - 1)
|
||||
return self._tabbed_browser.widget.widget(count - 1)
|
||||
else:
|
||||
return None
|
||||
|
||||
@@ -163,7 +162,7 @@ class CommandDispatcher:
|
||||
if not show_error:
|
||||
return
|
||||
raise cmdexc.CommandError("No last focused tab!")
|
||||
idx = self._tabbed_browser.indexOf(tab)
|
||||
idx = self._tabbed_browser.widget.indexOf(tab)
|
||||
if idx == -1:
|
||||
raise cmdexc.CommandError("Last focused tab vanished!")
|
||||
self._set_current_index(idx)
|
||||
@@ -212,7 +211,7 @@ class CommandDispatcher:
|
||||
what's configured in 'tabs.select_on_remove'.
|
||||
count: The tab index to close, or None
|
||||
"""
|
||||
tabbar = self._tabbed_browser.tabBar()
|
||||
tabbar = self._tabbed_browser.widget.tabBar()
|
||||
selection_override = self._get_selection_override(prev, next_,
|
||||
opposite)
|
||||
|
||||
@@ -264,7 +263,7 @@ class CommandDispatcher:
|
||||
return
|
||||
|
||||
to_pin = not tab.data.pinned
|
||||
self._tabbed_browser.set_tab_pinned(tab, to_pin)
|
||||
self._tabbed_browser.widget.set_tab_pinned(tab, to_pin)
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', name='open',
|
||||
maxsplit=0, scope='window')
|
||||
@@ -483,7 +482,8 @@ class CommandDispatcher:
|
||||
"""
|
||||
cmdutils.check_exclusive((bg, window), 'bw')
|
||||
curtab = self._current_widget()
|
||||
cur_title = self._tabbed_browser.page_title(self._current_index())
|
||||
cur_title = self._tabbed_browser.widget.page_title(
|
||||
self._current_index())
|
||||
try:
|
||||
history = curtab.history.serialize()
|
||||
except browsertab.WebTabError as e:
|
||||
@@ -499,18 +499,18 @@ class CommandDispatcher:
|
||||
newtab = new_tabbed_browser.tabopen(background=bg)
|
||||
new_tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=newtab.win_id)
|
||||
idx = new_tabbed_browser.indexOf(newtab)
|
||||
idx = new_tabbed_browser.widget.indexOf(newtab)
|
||||
|
||||
new_tabbed_browser.set_page_title(idx, cur_title)
|
||||
if config.val.tabs.favicons.show:
|
||||
new_tabbed_browser.setTabIcon(idx, curtab.icon())
|
||||
new_tabbed_browser.widget.set_page_title(idx, cur_title)
|
||||
if curtab.data.should_show_icon():
|
||||
new_tabbed_browser.widget.setTabIcon(idx, curtab.icon())
|
||||
if config.val.tabs.tabs_are_windows:
|
||||
new_tabbed_browser.window().setWindowIcon(curtab.icon())
|
||||
new_tabbed_browser.widget.window().setWindowIcon(curtab.icon())
|
||||
|
||||
newtab.data.keep_icon = True
|
||||
newtab.history.deserialize(history)
|
||||
newtab.zoom.set_factor(curtab.zoom.factor())
|
||||
new_tabbed_browser.set_tab_pinned(newtab, curtab.data.pinned)
|
||||
new_tabbed_browser.widget.set_tab_pinned(newtab, curtab.data.pinned)
|
||||
return newtab
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window')
|
||||
@@ -566,12 +566,6 @@ class CommandDispatcher:
|
||||
tabbed_browser.tabopen(self._current_url())
|
||||
self._tabbed_browser.close_tab(self._current_widget(), add_undo=False)
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window',
|
||||
deprecated='Use :tab-give instead!')
|
||||
def tab_detach(self):
|
||||
"""Deprecated way to detach a tab."""
|
||||
self.tab_give()
|
||||
|
||||
def _back_forward(self, tab, bg, window, count, forward):
|
||||
"""Helper function for :back/:forward."""
|
||||
history = self._current_widget().history
|
||||
@@ -768,6 +762,15 @@ class CommandDispatcher:
|
||||
|
||||
self._current_widget().scroller.to_perc(x, y)
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window')
|
||||
def scroll_to_anchor(self, name):
|
||||
"""Scroll to the given anchor in the document.
|
||||
|
||||
Args:
|
||||
name: The anchor to scroll to.
|
||||
"""
|
||||
self._current_widget().scroller.to_anchor(name)
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window')
|
||||
@cmdutils.argument('count', count=True)
|
||||
@cmdutils.argument('top_navigate', metavar='ACTION',
|
||||
@@ -846,7 +849,7 @@ class CommandDispatcher:
|
||||
keep: Stay in visual mode after yanking the selection.
|
||||
"""
|
||||
if what == 'title':
|
||||
s = self._tabbed_browser.page_title(self._current_index())
|
||||
s = self._tabbed_browser.widget.page_title(self._current_index())
|
||||
elif what == 'domain':
|
||||
port = self._current_url().port()
|
||||
s = '{}://{}{}'.format(self._current_url().scheme(),
|
||||
@@ -958,7 +961,7 @@ class CommandDispatcher:
|
||||
force: Avoid confirmation for pinned tabs.
|
||||
"""
|
||||
cmdutils.check_exclusive((prev, next_), 'pn')
|
||||
cur_idx = self._tabbed_browser.currentIndex()
|
||||
cur_idx = self._tabbed_browser.widget.currentIndex()
|
||||
assert cur_idx != -1
|
||||
|
||||
def _to_close(i):
|
||||
@@ -1013,7 +1016,7 @@ class CommandDispatcher:
|
||||
elif config.val.tabs.wrap:
|
||||
self._set_current_index(newidx % self._count())
|
||||
else:
|
||||
raise cmdexc.CommandError("First tab")
|
||||
log.webview.debug("First tab")
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window')
|
||||
@cmdutils.argument('count', count=True)
|
||||
@@ -1033,7 +1036,7 @@ class CommandDispatcher:
|
||||
elif config.val.tabs.wrap:
|
||||
self._set_current_index(newidx % self._count())
|
||||
else:
|
||||
raise cmdexc.CommandError("Last tab")
|
||||
log.webview.debug("Last tab")
|
||||
|
||||
def _resolve_buffer_index(self, index):
|
||||
"""Resolve a buffer index to the tabbedbrowser and tab.
|
||||
@@ -1075,11 +1078,11 @@ class CommandDispatcher:
|
||||
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=win_id)
|
||||
if not 0 < idx <= tabbed_browser.count():
|
||||
if not 0 < idx <= tabbed_browser.widget.count():
|
||||
raise cmdexc.CommandError(
|
||||
"There's no tab with index {}!".format(idx))
|
||||
|
||||
return (tabbed_browser, tabbed_browser.widget(idx-1))
|
||||
return (tabbed_browser, tabbed_browser.widget.widget(idx-1))
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window',
|
||||
maxsplit=0)
|
||||
@@ -1107,10 +1110,10 @@ class CommandDispatcher:
|
||||
|
||||
tabbed_browser, tab = self._resolve_buffer_index(index)
|
||||
|
||||
window = tabbed_browser.window()
|
||||
window = tabbed_browser.widget.window()
|
||||
window.activateWindow()
|
||||
window.raise_()
|
||||
tabbed_browser.setCurrentWidget(tab)
|
||||
tabbed_browser.widget.setCurrentWidget(tab)
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window')
|
||||
@cmdutils.argument('index', choices=['last'])
|
||||
@@ -1194,7 +1197,7 @@ class CommandDispatcher:
|
||||
cur_idx = self._current_index()
|
||||
cmdutils.check_overflow(cur_idx, 'int')
|
||||
cmdutils.check_overflow(new_idx, 'int')
|
||||
self._tabbed_browser.tabBar().moveTab(cur_idx, new_idx)
|
||||
self._tabbed_browser.widget.tabBar().moveTab(cur_idx, new_idx)
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window',
|
||||
maxsplit=0, no_replace_variables=True)
|
||||
@@ -1278,10 +1281,10 @@ class CommandDispatcher:
|
||||
|
||||
idx = self._current_index()
|
||||
if idx != -1:
|
||||
env['QUTE_TITLE'] = self._tabbed_browser.page_title(idx)
|
||||
env['QUTE_TITLE'] = self._tabbed_browser.widget.page_title(idx)
|
||||
|
||||
# FIXME:qtwebengine: If tab is None, run_async will fail!
|
||||
tab = self._tabbed_browser.currentWidget()
|
||||
tab = self._tabbed_browser.widget.currentWidget()
|
||||
|
||||
try:
|
||||
url = self._tabbed_browser.current_url()
|
||||
@@ -1452,6 +1455,7 @@ class CommandDispatcher:
|
||||
if tab.data.inspector is None:
|
||||
tab.data.inspector = inspector.create()
|
||||
tab.data.inspector.inspect(page)
|
||||
tab.data.inspector.show()
|
||||
else:
|
||||
tab.data.inspector.toggle(page)
|
||||
except inspector.WebInspectorError as e:
|
||||
@@ -1512,11 +1516,15 @@ class CommandDispatcher:
|
||||
)
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window')
|
||||
def view_source(self, edit=False):
|
||||
def view_source(self, edit=False, pygments=False):
|
||||
"""Show the source of the current page in a new tab.
|
||||
|
||||
Args:
|
||||
edit: Edit the source in the editor instead of opening a tab.
|
||||
pygments: Use pygments to generate the view. This is always
|
||||
the case for QtWebKit. For QtWebEngine it may display
|
||||
slightly different source.
|
||||
Some JavaScript processing may be applied.
|
||||
"""
|
||||
tab = self._current_widget()
|
||||
try:
|
||||
@@ -1524,14 +1532,15 @@ class CommandDispatcher:
|
||||
except cmdexc.CommandError as e:
|
||||
message.error(str(e))
|
||||
return
|
||||
if current_url.scheme() == 'view-source':
|
||||
|
||||
if current_url.scheme() == 'view-source' or tab.data.viewing_source:
|
||||
raise cmdexc.CommandError("Already viewing source!")
|
||||
|
||||
if edit:
|
||||
ed = editor.ExternalEditor(self._tabbed_browser)
|
||||
tab.dump_async(ed.edit)
|
||||
else:
|
||||
tab.action.show_source()
|
||||
tab.action.show_source(pygments)
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window',
|
||||
debug=True)
|
||||
@@ -1639,7 +1648,7 @@ class CommandDispatcher:
|
||||
|
||||
ed = editor.ExternalEditor(watch=True, parent=self._tabbed_browser)
|
||||
ed.file_updated.connect(functools.partial(
|
||||
self.on_file_updated, elem))
|
||||
self.on_file_updated, ed, elem))
|
||||
ed.editing_finished.connect(lambda: mainwindow.raise_window(
|
||||
objreg.last_focused_window(), alert=False))
|
||||
ed.edit(text, caret_position)
|
||||
@@ -1654,7 +1663,7 @@ class CommandDispatcher:
|
||||
tab = self._current_widget()
|
||||
tab.elements.find_focused(self._open_editor_cb)
|
||||
|
||||
def on_file_updated(self, elem, text):
|
||||
def on_file_updated(self, ed, elem, text):
|
||||
"""Write the editor text into the form field and clean up tempfile.
|
||||
|
||||
Callback for GUIProcess when the edited text was updated.
|
||||
@@ -1665,10 +1674,12 @@ class CommandDispatcher:
|
||||
"""
|
||||
try:
|
||||
elem.set_value(text)
|
||||
except webelem.OrphanedError as e:
|
||||
except webelem.OrphanedError:
|
||||
message.error('Edited element vanished')
|
||||
ed.backup()
|
||||
except webelem.Error as e:
|
||||
raise cmdexc.CommandError(str(e))
|
||||
message.error(str(e))
|
||||
ed.backup()
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', maxsplit=0,
|
||||
scope='window')
|
||||
@@ -2217,5 +2228,22 @@ class CommandDispatcher:
|
||||
pass
|
||||
return
|
||||
|
||||
window = self._tabbed_browser.window()
|
||||
window = self._tabbed_browser.widget.window()
|
||||
window.setWindowState(window.windowState() ^ Qt.WindowFullScreen)
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window',
|
||||
name='tab-mute')
|
||||
@cmdutils.argument('count', count=True)
|
||||
def tab_mute(self, count=None):
|
||||
"""Mute/Unmute the current/[count]th tab.
|
||||
|
||||
Args:
|
||||
count: The tab index to mute or unmute, or None
|
||||
"""
|
||||
tab = self._cntwidget(count)
|
||||
if tab is None:
|
||||
return
|
||||
try:
|
||||
tab.audio.toggle_muted()
|
||||
except browsertab.WebTabError as e:
|
||||
raise cmdexc.CommandError(e)
|
||||
|
||||
@@ -29,7 +29,6 @@ import pathlib
|
||||
import tempfile
|
||||
import enum
|
||||
|
||||
import sip
|
||||
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, Qt, QObject, QModelIndex,
|
||||
QTimer, QAbstractListModel, QUrl)
|
||||
|
||||
@@ -37,6 +36,7 @@ from qutebrowser.commands import cmdexc, cmdutils
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import (usertypes, standarddir, utils, message, log,
|
||||
qtutils)
|
||||
from qutebrowser.qt import sip
|
||||
|
||||
|
||||
ModelRole = enum.IntEnum('ModelRole', ['item'], start=Qt.UserRole)
|
||||
|
||||
@@ -21,13 +21,13 @@
|
||||
|
||||
import functools
|
||||
|
||||
import sip
|
||||
from PyQt5.QtCore import pyqtSlot, QSize, Qt, QTimer
|
||||
from PyQt5.QtWidgets import QListView, QSizePolicy, QMenu, QStyleFactory
|
||||
|
||||
from qutebrowser.browser import downloads
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import qtutils, utils, objreg
|
||||
from qutebrowser.qt import sip
|
||||
|
||||
|
||||
def update_geometry(obj):
|
||||
|
||||
@@ -30,9 +30,11 @@ import textwrap
|
||||
import attr
|
||||
from PyQt5.QtCore import pyqtSignal, QObject, QUrl
|
||||
|
||||
from qutebrowser.utils import log, standarddir, jinja, objreg, utils
|
||||
from qutebrowser.utils import (log, standarddir, jinja, objreg, utils,
|
||||
javascript, urlmatch, version, usertypes)
|
||||
from qutebrowser.commands import cmdutils
|
||||
from qutebrowser.browser import downloads
|
||||
from qutebrowser.misc import objects
|
||||
|
||||
|
||||
def _scripts_dir():
|
||||
@@ -47,6 +49,7 @@ class GreasemonkeyScript:
|
||||
def __init__(self, properties, code):
|
||||
self._code = code
|
||||
self.includes = []
|
||||
self.matches = []
|
||||
self.excludes = []
|
||||
self.requires = []
|
||||
self.description = None
|
||||
@@ -55,6 +58,7 @@ class GreasemonkeyScript:
|
||||
self.run_at = None
|
||||
self.script_meta = None
|
||||
self.runs_on_sub_frames = True
|
||||
self.jsworld = "main"
|
||||
for name, value in properties:
|
||||
if name == 'name':
|
||||
self.name = value
|
||||
@@ -62,8 +66,10 @@ class GreasemonkeyScript:
|
||||
self.namespace = value
|
||||
elif name == 'description':
|
||||
self.description = value
|
||||
elif name in ['include', 'match']:
|
||||
elif name == 'include':
|
||||
self.includes.append(value)
|
||||
elif name == 'match':
|
||||
self.matches.append(value)
|
||||
elif name in ['exclude', 'exclude_match']:
|
||||
self.excludes.append(value)
|
||||
elif name == 'run-at':
|
||||
@@ -72,6 +78,8 @@ class GreasemonkeyScript:
|
||||
self.runs_on_sub_frames = False
|
||||
elif name == 'require':
|
||||
self.requires.append(value)
|
||||
elif name == 'qute-js-world':
|
||||
self.jsworld = value
|
||||
|
||||
HEADER_REGEX = r'// ==UserScript==|\n+// ==/UserScript==\n'
|
||||
PROPS_REGEX = r'// @(?P<prop>[^\s]+)\s*(?P<val>.*)'
|
||||
@@ -91,7 +99,7 @@ class GreasemonkeyScript:
|
||||
props = ""
|
||||
script = cls(re.findall(cls.PROPS_REGEX, props), source)
|
||||
script.script_meta = props
|
||||
if not props:
|
||||
if not script.includes and not script.matches:
|
||||
script.includes = ['*']
|
||||
return script
|
||||
|
||||
@@ -104,18 +112,24 @@ class GreasemonkeyScript:
|
||||
browser's debugger/inspector will not match up to the line
|
||||
numbers in the source script directly.
|
||||
"""
|
||||
return jinja.js_environment.get_template(
|
||||
'greasemonkey_wrapper.js').render(
|
||||
scriptName="/".join([self.namespace or '', self.name]),
|
||||
scriptInfo=self._meta_json(),
|
||||
scriptMeta=self.script_meta,
|
||||
scriptSource=self._code)
|
||||
# Don't use Proxy on this webkit version, the support isn't there.
|
||||
use_proxy = not (
|
||||
objects.backend == usertypes.Backend.QtWebKit and
|
||||
version.qWebKitVersion() == '602.1')
|
||||
template = jinja.js_environment.get_template('greasemonkey_wrapper.js')
|
||||
return template.render(
|
||||
scriptName=javascript.string_escape(
|
||||
"/".join([self.namespace or '', self.name])),
|
||||
scriptInfo=self._meta_json(),
|
||||
scriptMeta=javascript.string_escape(self.script_meta),
|
||||
scriptSource=self._code,
|
||||
use_proxy=use_proxy)
|
||||
|
||||
def _meta_json(self):
|
||||
return json.dumps({
|
||||
'name': self.name,
|
||||
'description': self.description,
|
||||
'matches': self.includes,
|
||||
'matches': self.matches,
|
||||
'includes': self.includes,
|
||||
'excludes': self.excludes,
|
||||
'run-at': self.run_at,
|
||||
@@ -141,6 +155,42 @@ class MatchingScripts(object):
|
||||
idle = attr.ib(default=attr.Factory(list))
|
||||
|
||||
|
||||
class GreasemonkeyMatcher:
|
||||
|
||||
"""Check whether scripts should be loaded for a given URL."""
|
||||
|
||||
# https://wiki.greasespot.net/Include_and_exclude_rules#Greaseable_schemes
|
||||
# Limit the schemes scripts can run on due to unreasonable levels of
|
||||
# exploitability
|
||||
GREASEABLE_SCHEMES = ['http', 'https', 'ftp', 'file']
|
||||
|
||||
def __init__(self, url):
|
||||
self._url = url
|
||||
self._url_string = url.toString(QUrl.FullyEncoded)
|
||||
self.is_greaseable = url.scheme() in self.GREASEABLE_SCHEMES
|
||||
|
||||
def _match_pattern(self, pattern):
|
||||
# For include and exclude rules if they start and end with '/' they
|
||||
# should be treated as a (ecma syntax) regular expression.
|
||||
if pattern.startswith('/') and pattern.endswith('/'):
|
||||
matches = re.search(pattern[1:-1], self._url_string, flags=re.I)
|
||||
return matches is not None
|
||||
|
||||
# Otherwise they are glob expressions.
|
||||
return fnmatch.fnmatch(self._url_string, pattern)
|
||||
|
||||
def matches(self, script):
|
||||
"""Check whether the URL matches filtering rules of the script."""
|
||||
assert self.is_greaseable
|
||||
matching_includes = any(self._match_pattern(pat)
|
||||
for pat in script.includes)
|
||||
matching_match = any(urlmatch.UrlPattern(pat).matches(self._url)
|
||||
for pat in script.matches)
|
||||
matching_excludes = any(self._match_pattern(pat)
|
||||
for pat in script.excludes)
|
||||
return (matching_includes or matching_match) and not matching_excludes
|
||||
|
||||
|
||||
class GreasemonkeyManager(QObject):
|
||||
|
||||
"""Manager of userscripts and a Greasemonkey compatible environment.
|
||||
@@ -152,10 +202,6 @@ class GreasemonkeyManager(QObject):
|
||||
"""
|
||||
|
||||
scripts_reloaded = pyqtSignal()
|
||||
# https://wiki.greasespot.net/Include_and_exclude_rules#Greaseable_schemes
|
||||
# Limit the schemes scripts can run on due to unreasonable levels of
|
||||
# exploitability
|
||||
greaseable_schemes = ['http', 'https', 'ftp', 'file']
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
@@ -307,30 +353,17 @@ class GreasemonkeyManager(QObject):
|
||||
returns a tuple of lists of scripts meant to run at (document-start,
|
||||
document-end, document-idle)
|
||||
"""
|
||||
if url.scheme() not in self.greaseable_schemes:
|
||||
matcher = GreasemonkeyMatcher(url)
|
||||
if not matcher.is_greaseable:
|
||||
return MatchingScripts(url, [], [], [])
|
||||
|
||||
string_url = url.toString(QUrl.FullyEncoded)
|
||||
|
||||
def _match(pattern):
|
||||
# For include and exclude rules if they start and end with '/' they
|
||||
# should be treated as a (ecma syntax) regular expression.
|
||||
if pattern.startswith('/') and pattern.endswith('/'):
|
||||
matches = re.search(pattern[1:-1], string_url, flags=re.I)
|
||||
return matches is not None
|
||||
|
||||
# Otherwise they are glob expressions.
|
||||
return fnmatch.fnmatch(string_url, pattern)
|
||||
|
||||
tester = (lambda script:
|
||||
any(_match(pat) for pat in script.includes) and
|
||||
not any(_match(pat) for pat in script.excludes))
|
||||
|
||||
return MatchingScripts(
|
||||
url,
|
||||
[script for script in self._run_start if tester(script)],
|
||||
[script for script in self._run_end if tester(script)],
|
||||
[script for script in self._run_idle if tester(script)]
|
||||
url=url,
|
||||
start=[script for script in self._run_start
|
||||
if matcher.matches(script)],
|
||||
end=[script for script in self._run_end
|
||||
if matcher.matches(script)],
|
||||
idle=[script for script in self._run_idle
|
||||
if matcher.matches(script)]
|
||||
)
|
||||
|
||||
def all_scripts(self):
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
import collections
|
||||
import functools
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import html
|
||||
import enum
|
||||
@@ -154,6 +155,7 @@ class HintContext:
|
||||
to_follow: The link to follow when enter is pressed.
|
||||
args: Custom arguments for userscript/spawn
|
||||
rapid: Whether to do rapid hinting.
|
||||
first_run: Whether the action is run for the 1st time in rapid hinting.
|
||||
add_history: Whether to add yanked or spawned link to the history.
|
||||
filterstr: Used to save the filter string for restoring in rapid mode.
|
||||
tab: The WebTab object we started hinting in.
|
||||
@@ -166,12 +168,14 @@ class HintContext:
|
||||
baseurl = attr.ib(None)
|
||||
to_follow = attr.ib(None)
|
||||
rapid = attr.ib(False)
|
||||
first_run = attr.ib(True)
|
||||
add_history = attr.ib(False)
|
||||
filterstr = attr.ib(None)
|
||||
args = attr.ib(attr.Factory(list))
|
||||
tab = attr.ib(None)
|
||||
group = attr.ib(None)
|
||||
hint_mode = attr.ib(None)
|
||||
first = attr.ib(False)
|
||||
|
||||
def get_args(self, urlstr):
|
||||
"""Get the arguments, with {hint-url} replaced by the given URL."""
|
||||
@@ -240,7 +244,18 @@ class HintActions:
|
||||
if url.scheme() == 'mailto':
|
||||
flags |= QUrl.RemoveScheme
|
||||
urlstr = url.toString(flags)
|
||||
utils.set_clipboard(urlstr, selection=sel)
|
||||
|
||||
new_content = urlstr
|
||||
|
||||
# only second and consecutive yanks are to append to the clipboard
|
||||
if context.rapid and not context.first_run:
|
||||
try:
|
||||
old_content = utils.get_clipboard(selection=sel)
|
||||
except utils.ClipboardEmptyError:
|
||||
pass
|
||||
else:
|
||||
new_content = os.linesep.join([old_content, new_content])
|
||||
utils.set_clipboard(new_content, selection=sel)
|
||||
|
||||
msg = "Yanked URL to {}: {}".format(
|
||||
"primary selection" if sel else "clipboard",
|
||||
@@ -612,6 +627,9 @@ class HintManager(QObject):
|
||||
modeman.enter(self._win_id, usertypes.KeyMode.hint,
|
||||
'HintManager.start')
|
||||
|
||||
if self._context.first:
|
||||
self._fire(strings[0])
|
||||
return
|
||||
# to make auto_follow == 'always' work
|
||||
self._handle_auto_follow()
|
||||
|
||||
@@ -620,7 +638,8 @@ class HintManager(QObject):
|
||||
@cmdutils.argument('win_id', win_id=True)
|
||||
def start(self, # pylint: disable=keyword-arg-before-vararg
|
||||
group=webelem.Group.all, target=Target.normal,
|
||||
*args, win_id, mode=None, add_history=False, rapid=False):
|
||||
*args, win_id, mode=None, add_history=False, rapid=False,
|
||||
first=False):
|
||||
"""Start hinting.
|
||||
|
||||
Args:
|
||||
@@ -631,6 +650,7 @@ class HintManager(QObject):
|
||||
`window`, `run`, `hover`, `userscript` and `spawn`.
|
||||
add_history: Whether to add the spawned or yanked link to the
|
||||
browsing history.
|
||||
first: Click the first hinted element without prompting.
|
||||
group: The element types to hint.
|
||||
|
||||
- `all`: All clickable elements.
|
||||
@@ -682,7 +702,7 @@ class HintManager(QObject):
|
||||
"""
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=self._win_id)
|
||||
tab = tabbed_browser.currentWidget()
|
||||
tab = tabbed_browser.widget.currentWidget()
|
||||
if tab is None:
|
||||
raise cmdexc.CommandError("No WebView available yet!")
|
||||
|
||||
@@ -694,7 +714,8 @@ class HintManager(QObject):
|
||||
if rapid:
|
||||
if target in [Target.tab_bg, Target.window, Target.run,
|
||||
Target.hover, Target.userscript, Target.spawn,
|
||||
Target.download, Target.normal, Target.current]:
|
||||
Target.download, Target.normal, Target.current,
|
||||
Target.yank, Target.yank_primary]:
|
||||
pass
|
||||
elif target == Target.tab and config.val.tabs.background:
|
||||
pass
|
||||
@@ -713,6 +734,7 @@ class HintManager(QObject):
|
||||
self._context.rapid = rapid
|
||||
self._context.hint_mode = mode
|
||||
self._context.add_history = add_history
|
||||
self._context.first = first
|
||||
try:
|
||||
self._context.baseurl = tabbed_browser.current_url()
|
||||
except qtutils.QtValueError:
|
||||
@@ -907,6 +929,9 @@ class HintManager(QObject):
|
||||
except HintingError as e:
|
||||
message.error(str(e))
|
||||
|
||||
if self._context is not None:
|
||||
self._context.first_run = False
|
||||
|
||||
@cmdutils.register(instance='hintmanager', scope='tab',
|
||||
modes=[usertypes.KeyMode.hint])
|
||||
def follow_hint(self, select=False, keystring=None):
|
||||
|
||||
@@ -23,7 +23,7 @@ import os
|
||||
import time
|
||||
import contextlib
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot, QUrl, QTimer
|
||||
from PyQt5.QtCore import pyqtSlot, QUrl, QTimer, pyqtSignal
|
||||
|
||||
from qutebrowser.commands import cmdutils, cmdexc
|
||||
from qutebrowser.utils import (utils, objreg, log, usertypes, message,
|
||||
@@ -52,6 +52,11 @@ class WebHistory(sql.SqlTable):
|
||||
|
||||
"""The global history of visited pages."""
|
||||
|
||||
# All web history cleared
|
||||
history_cleared = pyqtSignal()
|
||||
# one url cleared
|
||||
url_cleared = pyqtSignal(QUrl)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__("History", ['url', 'title', 'atime', 'redirect'],
|
||||
constraints={'url': 'NOT NULL',
|
||||
@@ -157,6 +162,7 @@ class WebHistory(sql.SqlTable):
|
||||
with self._handle_sql_errors():
|
||||
self.delete_all()
|
||||
self.completion.delete_all()
|
||||
self.history_cleared.emit()
|
||||
|
||||
def delete_url(self, url):
|
||||
"""Remove all history entries with the given url.
|
||||
@@ -168,6 +174,7 @@ class WebHistory(sql.SqlTable):
|
||||
qtutils.ensure_valid(qurl)
|
||||
self.delete('url', self._format_url(qurl))
|
||||
self.completion.delete('url', self._format_completion_url(qurl))
|
||||
self.url_cleared.emit(qurl)
|
||||
|
||||
@pyqtSlot(QUrl, QUrl, str)
|
||||
def add_from_tab(self, url, requested_url, title):
|
||||
|
||||
@@ -87,6 +87,8 @@ class AbstractWebInspector(QWidget):
|
||||
data = bytes(self.saveGeometry())
|
||||
geom = base64.b64encode(data).decode('ASCII')
|
||||
configfiles.state['geometry']['inspector'] = geom
|
||||
|
||||
self.inspect(None)
|
||||
super().closeEvent(e)
|
||||
|
||||
def inspect(self, page):
|
||||
@@ -99,3 +101,4 @@ class AbstractWebInspector(QWidget):
|
||||
self.hide()
|
||||
else:
|
||||
self.inspect(page)
|
||||
self.show()
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
from PyQt5.QtCore import QObject, QEvent, Qt, QTimer
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import message, log, usertypes
|
||||
from qutebrowser.utils import message, log, usertypes, qtutils, objreg
|
||||
from qutebrowser.keyinput import modeman
|
||||
|
||||
|
||||
@@ -40,11 +40,12 @@ class ChildEventFilter(QObject):
|
||||
_widget: The widget expected to send out childEvents.
|
||||
"""
|
||||
|
||||
def __init__(self, eventfilter, widget, parent=None):
|
||||
def __init__(self, eventfilter, widget, win_id, parent=None):
|
||||
super().__init__(parent)
|
||||
self._filter = eventfilter
|
||||
assert widget is not None
|
||||
self._widget = widget
|
||||
self._win_id = win_id
|
||||
|
||||
def eventFilter(self, obj, event):
|
||||
"""Act on ChildAdded events."""
|
||||
@@ -54,6 +55,29 @@ class ChildEventFilter(QObject):
|
||||
obj, child))
|
||||
assert obj is self._widget
|
||||
child.installEventFilter(self._filter)
|
||||
|
||||
if qtutils.version_check('5.11', compiled=False, exact=True):
|
||||
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-68076
|
||||
pass_modes = [usertypes.KeyMode.command,
|
||||
usertypes.KeyMode.prompt,
|
||||
usertypes.KeyMode.yesno]
|
||||
if modeman.instance(self._win_id).mode not in pass_modes:
|
||||
tabbed_browser = objreg.get('tabbed-browser',
|
||||
scope='window',
|
||||
window=self._win_id)
|
||||
current_index = tabbed_browser.widget.currentIndex()
|
||||
try:
|
||||
widget_index = tabbed_browser.widget.indexOf(
|
||||
self._widget.parent())
|
||||
except RuntimeError:
|
||||
widget_index = -1
|
||||
if current_index == widget_index:
|
||||
QTimer.singleShot(0, self._widget.setFocus)
|
||||
|
||||
elif event.type() == QEvent.ChildRemoved:
|
||||
child = event.child()
|
||||
log.mouse.debug("{}: removed child {}".format(obj, child))
|
||||
|
||||
return False
|
||||
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ from PyQt5.QtCore import pyqtSlot, pyqtSignal, QTimer
|
||||
from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import message, usertypes, log, urlutils, utils
|
||||
from qutebrowser.utils import message, usertypes, log, urlutils, utils, debug
|
||||
from qutebrowser.browser import downloads
|
||||
from qutebrowser.browser.webkit import http
|
||||
from qutebrowser.browser.webkit.network import networkmanager
|
||||
@@ -162,6 +162,7 @@ class DownloadItem(downloads.AbstractDownloadItem):
|
||||
QTimer.singleShot(0, lambda: self._die(reply.errorString()))
|
||||
|
||||
def _do_cancel(self):
|
||||
self._read_timer.stop()
|
||||
if self._reply is not None:
|
||||
self._reply.finished.disconnect(self._on_reply_finished)
|
||||
self._reply.abort()
|
||||
@@ -306,7 +307,14 @@ class DownloadItem(downloads.AbstractDownloadItem):
|
||||
"""Handle QNetworkReply errors."""
|
||||
if code == QNetworkReply.OperationCanceledError:
|
||||
return
|
||||
self._die(self._reply.errorString())
|
||||
|
||||
if self._reply is None:
|
||||
error = "Unknown error: {}".format(
|
||||
debug.qenum_key(QNetworkReply, code))
|
||||
else:
|
||||
error = self._reply.errorString()
|
||||
|
||||
self._die(error)
|
||||
|
||||
@pyqtSlot()
|
||||
def _on_read_timer_timeout(self):
|
||||
|
||||
@@ -24,6 +24,7 @@ Module attributes:
|
||||
_HANDLERS: The handlers registered via decorators.
|
||||
"""
|
||||
|
||||
import html
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
@@ -33,7 +34,6 @@ import urllib
|
||||
import collections
|
||||
|
||||
import pkg_resources
|
||||
import sip
|
||||
from PyQt5.QtCore import QUrlQuery, QUrl
|
||||
|
||||
import qutebrowser
|
||||
@@ -41,6 +41,7 @@ from qutebrowser.config import config, configdata, configexc, configdiff
|
||||
from qutebrowser.utils import (version, utils, jinja, log, message, docutils,
|
||||
objreg, urlutils)
|
||||
from qutebrowser.misc import objects
|
||||
from qutebrowser.qt import sip
|
||||
|
||||
|
||||
pyeval_output = ":pyeval was never called"
|
||||
@@ -123,12 +124,12 @@ class add_handler: # noqa: N801,N806 pylint: disable=invalid-name
|
||||
|
||||
def wrong_backend_handler(self, url):
|
||||
"""Show an error page about using the invalid backend."""
|
||||
html = jinja.render('error.html',
|
||||
title="Error while opening qute://url",
|
||||
url=url.toDisplayString(),
|
||||
error='{} is not available with this '
|
||||
'backend'.format(url.toDisplayString()))
|
||||
return 'text/html', html
|
||||
src = jinja.render('error.html',
|
||||
title="Error while opening qute://url",
|
||||
url=url.toDisplayString(),
|
||||
error='{} is not available with this '
|
||||
'backend'.format(url.toDisplayString()))
|
||||
return 'text/html', src
|
||||
|
||||
|
||||
def data_for_url(url):
|
||||
@@ -177,7 +178,7 @@ def data_for_url(url):
|
||||
except OSError as e:
|
||||
# FIXME:qtwebengine how to handle this?
|
||||
raise QuteSchemeOSError(e)
|
||||
except QuteSchemeError as e:
|
||||
except QuteSchemeError:
|
||||
raise
|
||||
|
||||
assert mimetype is not None, url
|
||||
@@ -196,11 +197,11 @@ def qute_bookmarks(_url):
|
||||
quickmarks = sorted(objreg.get('quickmark-manager').marks.items(),
|
||||
key=lambda x: x[0]) # Sort by name
|
||||
|
||||
html = jinja.render('bookmarks.html',
|
||||
title='Bookmarks',
|
||||
bookmarks=bookmarks,
|
||||
quickmarks=quickmarks)
|
||||
return 'text/html', html
|
||||
src = jinja.render('bookmarks.html',
|
||||
title='Bookmarks',
|
||||
bookmarks=bookmarks,
|
||||
quickmarks=quickmarks)
|
||||
return 'text/html', src
|
||||
|
||||
|
||||
@add_handler('tabs')
|
||||
@@ -218,10 +219,10 @@ def qute_tabs(_url):
|
||||
urlstr = tab.url().toDisplayString()
|
||||
tabs[str(win_id)].append((tab.title(), urlstr))
|
||||
|
||||
html = jinja.render('tabs.html',
|
||||
title='Tabs',
|
||||
tab_list_by_window=tabs)
|
||||
return 'text/html', html
|
||||
src = jinja.render('tabs.html',
|
||||
title='Tabs',
|
||||
tab_list_by_window=tabs)
|
||||
return 'text/html', src
|
||||
|
||||
|
||||
def history_data(start_time, offset=None):
|
||||
@@ -241,8 +242,9 @@ def history_data(start_time, offset=None):
|
||||
end_time = start_time - 24*60*60
|
||||
entries = hist.entries_between(end_time, start_time)
|
||||
|
||||
return [{"url": e.url, "title": e.title or e.url, "time": e.atime}
|
||||
for e in entries]
|
||||
return [{"url": e.url,
|
||||
"title": html.escape(e.title) or html.escape(e.url),
|
||||
"time": e.atime} for e in entries]
|
||||
|
||||
|
||||
@add_handler('history')
|
||||
@@ -287,25 +289,25 @@ def qute_javascript(url):
|
||||
@add_handler('pyeval')
|
||||
def qute_pyeval(_url):
|
||||
"""Handler for qute://pyeval."""
|
||||
html = jinja.render('pre.html', title='pyeval', content=pyeval_output)
|
||||
return 'text/html', html
|
||||
src = jinja.render('pre.html', title='pyeval', content=pyeval_output)
|
||||
return 'text/html', src
|
||||
|
||||
|
||||
@add_handler('spawn-output')
|
||||
def qute_spawn_output(_url):
|
||||
"""Handler for qute://spawn-output."""
|
||||
html = jinja.render('pre.html', title='spawn output', content=spawn_output)
|
||||
return 'text/html', html
|
||||
src = jinja.render('pre.html', title='spawn output', content=spawn_output)
|
||||
return 'text/html', src
|
||||
|
||||
|
||||
@add_handler('version')
|
||||
@add_handler('verizon')
|
||||
def qute_version(_url):
|
||||
"""Handler for qute://version."""
|
||||
html = jinja.render('version.html', title='Version info',
|
||||
version=version.version(),
|
||||
copyright=qutebrowser.__copyright__)
|
||||
return 'text/html', html
|
||||
src = jinja.render('version.html', title='Version info',
|
||||
version=version.version(),
|
||||
copyright=qutebrowser.__copyright__)
|
||||
return 'text/html', src
|
||||
|
||||
|
||||
@add_handler('plainlog')
|
||||
@@ -323,8 +325,8 @@ def qute_plainlog(url):
|
||||
if not level:
|
||||
level = 'vdebug'
|
||||
text = log.ram_handler.dump_log(html=False, level=level)
|
||||
html = jinja.render('pre.html', title='log', content=text)
|
||||
return 'text/html', html
|
||||
src = jinja.render('pre.html', title='log', content=text)
|
||||
return 'text/html', src
|
||||
|
||||
|
||||
@add_handler('log')
|
||||
@@ -343,8 +345,8 @@ def qute_log(url):
|
||||
level = 'vdebug'
|
||||
html_log = log.ram_handler.dump_log(html=True, level=level)
|
||||
|
||||
html = jinja.render('log.html', title='log', content=html_log)
|
||||
return 'text/html', html
|
||||
src = jinja.render('log.html', title='log', content=html_log)
|
||||
return 'text/html', src
|
||||
|
||||
|
||||
@add_handler('gpl')
|
||||
@@ -415,12 +417,12 @@ def qute_help(url):
|
||||
@add_handler('backend-warning')
|
||||
def qute_backend_warning(_url):
|
||||
"""Handler for qute://backend-warning."""
|
||||
html = jinja.render('backend-warning.html',
|
||||
distribution=version.distribution(),
|
||||
Distribution=version.Distribution,
|
||||
version=pkg_resources.parse_version,
|
||||
title="Legacy backend warning")
|
||||
return 'text/html', html
|
||||
src = jinja.render('backend-warning.html',
|
||||
distribution=version.distribution(),
|
||||
Distribution=version.Distribution,
|
||||
version=pkg_resources.parse_version,
|
||||
title="Legacy backend warning")
|
||||
return 'text/html', src
|
||||
|
||||
|
||||
def _qute_settings_set(url):
|
||||
@@ -450,10 +452,10 @@ def qute_settings(url):
|
||||
if url.path() == '/set':
|
||||
return _qute_settings_set(url)
|
||||
|
||||
html = jinja.render('settings.html', title='settings',
|
||||
configdata=configdata,
|
||||
confget=config.instance.get_str)
|
||||
return 'text/html', html
|
||||
src = jinja.render('settings.html', title='settings',
|
||||
configdata=configdata,
|
||||
confget=config.instance.get_str)
|
||||
return 'text/html', src
|
||||
|
||||
|
||||
@add_handler('bindings')
|
||||
@@ -467,9 +469,9 @@ def qute_bindings(_url):
|
||||
for mode in modes:
|
||||
bindings[mode] = config.key_instance.get_bindings_for(mode)
|
||||
|
||||
html = jinja.render('bindings.html', title='Bindings',
|
||||
bindings=bindings)
|
||||
return 'text/html', html
|
||||
src = jinja.render('bindings.html', title='Bindings',
|
||||
bindings=bindings)
|
||||
return 'text/html', src
|
||||
|
||||
|
||||
@add_handler('back')
|
||||
@@ -478,10 +480,10 @@ def qute_back(url):
|
||||
|
||||
Simple page to free ram / lazy load a site, goes back on focusing the tab.
|
||||
"""
|
||||
html = jinja.render(
|
||||
src = jinja.render(
|
||||
'back.html',
|
||||
title='Suspended: ' + urllib.parse.unquote(url.fragment()))
|
||||
return 'text/html', html
|
||||
return 'text/html', src
|
||||
|
||||
|
||||
@add_handler('configdiff')
|
||||
|
||||
@@ -34,21 +34,22 @@ class CallSuper(Exception):
|
||||
"""Raised when the caller should call the superclass instead."""
|
||||
|
||||
|
||||
def custom_headers():
|
||||
def custom_headers(url):
|
||||
"""Get the combined custom headers."""
|
||||
headers = {}
|
||||
|
||||
dnt_config = config.val.content.headers.do_not_track
|
||||
dnt_config = config.instance.get('content.headers.do_not_track', url=url)
|
||||
if dnt_config is not None:
|
||||
dnt = b'1' if dnt_config else b'0'
|
||||
headers[b'DNT'] = dnt
|
||||
headers[b'X-Do-Not-Track'] = dnt
|
||||
|
||||
conf_headers = config.val.content.headers.custom
|
||||
conf_headers = config.instance.get('content.headers.custom', url=url)
|
||||
for header, value in conf_headers.items():
|
||||
headers[header.encode('ascii')] = value.encode('ascii')
|
||||
|
||||
accept_language = config.val.content.headers.accept_language
|
||||
accept_language = config.instance.get('content.headers.accept_language',
|
||||
url=url)
|
||||
if accept_language is not None:
|
||||
headers[b'Accept-Language'] = accept_language.encode('ascii')
|
||||
|
||||
@@ -156,7 +157,7 @@ def ignore_certificate_errors(url, errors, abort_on):
|
||||
Return:
|
||||
True if the error should be ignored, False otherwise.
|
||||
"""
|
||||
ssl_strict = config.val.content.ssl_strict
|
||||
ssl_strict = config.instance.get('content.ssl_strict', url=url)
|
||||
log.webview.debug("Certificate errors {!r}, strict {}".format(
|
||||
errors, ssl_strict))
|
||||
|
||||
@@ -196,7 +197,8 @@ def ignore_certificate_errors(url, errors, abort_on):
|
||||
raise utils.Unreachable
|
||||
|
||||
|
||||
def feature_permission(url, option, msg, yes_action, no_action, abort_on):
|
||||
def feature_permission(url, option, msg, yes_action, no_action, abort_on,
|
||||
blocking=False):
|
||||
"""Handle a feature permission request.
|
||||
|
||||
Args:
|
||||
@@ -206,11 +208,13 @@ def feature_permission(url, option, msg, yes_action, no_action, abort_on):
|
||||
yes_action: A callable to call if the request was approved
|
||||
no_action: A callable to call if the request was denied
|
||||
abort_on: A list of signals which interrupt the question.
|
||||
blocking: If True, ask a blocking question.
|
||||
|
||||
Return:
|
||||
The Question object if a question was asked, None otherwise.
|
||||
The Question object if a question was asked (and blocking=False),
|
||||
None otherwise.
|
||||
"""
|
||||
config_val = config.instance.get(option)
|
||||
config_val = config.instance.get(option, url=url)
|
||||
if config_val == 'ask':
|
||||
if url.isValid():
|
||||
urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded)
|
||||
@@ -220,10 +224,20 @@ def feature_permission(url, option, msg, yes_action, no_action, abort_on):
|
||||
urlstr = None
|
||||
text = "Allow the website to {}?".format(msg)
|
||||
|
||||
return message.confirm_async(
|
||||
yes_action=yes_action, no_action=no_action,
|
||||
cancel_action=no_action, abort_on=abort_on,
|
||||
title='Permission request', text=text, url=urlstr)
|
||||
if blocking:
|
||||
answer = message.ask(abort_on=abort_on, title='Permission request',
|
||||
text=text, url=urlstr,
|
||||
mode=usertypes.PromptMode.yesno)
|
||||
if answer:
|
||||
yes_action()
|
||||
else:
|
||||
no_action()
|
||||
return None
|
||||
else:
|
||||
return message.confirm_async(
|
||||
yes_action=yes_action, no_action=no_action,
|
||||
cancel_action=no_action, abort_on=abort_on,
|
||||
title='Permission request', text=text, url=urlstr)
|
||||
elif config_val:
|
||||
yes_action()
|
||||
return None
|
||||
@@ -299,10 +313,10 @@ def netrc_authentication(url, authenticator):
|
||||
(user, _account, password) = authenticators
|
||||
except FileNotFoundError:
|
||||
log.misc.debug("No .netrc file found")
|
||||
except OSError:
|
||||
log.misc.exception("Unable to read the netrc file")
|
||||
except netrc.NetrcParseError:
|
||||
log.misc.exception("Error when parsing the netrc file")
|
||||
except OSError as e:
|
||||
log.misc.exception("Unable to read the netrc file: {}".format(e))
|
||||
except netrc.NetrcParseError as e:
|
||||
log.misc.exception("Error when parsing the netrc file: {}".format(e))
|
||||
|
||||
if user is None:
|
||||
return False
|
||||
|
||||
@@ -76,11 +76,11 @@ class SignalFilter(QObject):
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=self._win_id)
|
||||
try:
|
||||
tabidx = tabbed_browser.indexOf(tab)
|
||||
tabidx = tabbed_browser.widget.indexOf(tab)
|
||||
except RuntimeError:
|
||||
# The tab has been deleted already
|
||||
return
|
||||
if tabidx == tabbed_browser.currentIndex():
|
||||
if tabidx == tabbed_browser.widget.currentIndex():
|
||||
if log_signal:
|
||||
log.signals.debug("emitting: {} (tab {})".format(
|
||||
debug.dbg_signal(signal, args), tabidx))
|
||||
|
||||
@@ -307,6 +307,10 @@ class AbstractWebElement(collections.abc.MutableMapping):
|
||||
href_tags = ['a', 'area', 'link']
|
||||
return self.tag_name() in href_tags and 'href' in self
|
||||
|
||||
def _requires_user_interaction(self):
|
||||
"""Return True if clicking this element needs user interaction."""
|
||||
raise NotImplementedError
|
||||
|
||||
def _mouse_pos(self):
|
||||
"""Get the position to click/hover."""
|
||||
# Click the center of the largest square fitting into the top/left
|
||||
@@ -405,7 +409,7 @@ class AbstractWebElement(collections.abc.MutableMapping):
|
||||
return
|
||||
|
||||
if click_target == usertypes.ClickTarget.normal:
|
||||
if self.is_link():
|
||||
if self.is_link() and not self._requires_user_interaction():
|
||||
log.webelem.debug("Clicking via JS click()")
|
||||
self._click_js(click_target)
|
||||
elif self.is_editable(strict=True):
|
||||
|
||||
@@ -28,6 +28,10 @@ class CertificateErrorWrapper(usertypes.AbstractCertificateErrorWrapper):
|
||||
|
||||
"""A wrapper over a QWebEngineCertificateError."""
|
||||
|
||||
def __init__(self, error):
|
||||
super().__init__(error)
|
||||
self.ignore = False
|
||||
|
||||
def __str__(self):
|
||||
return self._error.errorDescription()
|
||||
|
||||
@@ -37,5 +41,8 @@ class CertificateErrorWrapper(usertypes.AbstractCertificateErrorWrapper):
|
||||
self._error.error()),
|
||||
string=str(self))
|
||||
|
||||
def url(self):
|
||||
return self._error.url()
|
||||
|
||||
def is_overridable(self):
|
||||
return self._error.isOverridable()
|
||||
|
||||
48
qutebrowser/browser/webengine/cookies.py
Normal file
@@ -0,0 +1,48 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2018 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/>.
|
||||
|
||||
"""Filter for QtWebEngine cookies."""
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import utils
|
||||
|
||||
|
||||
def _accept_cookie(request):
|
||||
"""Check whether the given cookie should be accepted."""
|
||||
accept = config.val.content.cookies.accept
|
||||
if accept == 'all':
|
||||
return True
|
||||
elif accept in ['no-3rdparty', 'no-unknown-3rdparty']:
|
||||
return not request.thirdParty
|
||||
elif accept == 'never':
|
||||
return False
|
||||
else:
|
||||
raise utils.Unreachable
|
||||
|
||||
|
||||
def install_filter(profile):
|
||||
"""Install the cookie filter on the given profile.
|
||||
|
||||
On Qt < 5.11, the filter isn't installed.
|
||||
"""
|
||||
store = profile.cookieStore()
|
||||
try:
|
||||
store.setCookieFilter(_accept_cookie)
|
||||
except AttributeError:
|
||||
pass
|
||||
@@ -19,20 +19,22 @@
|
||||
|
||||
"""A request interceptor taking care of adblocking and custom headers."""
|
||||
|
||||
from PyQt5.QtWebEngineCore import QWebEngineUrlRequestInterceptor
|
||||
from PyQt5.QtWebEngineCore import (QWebEngineUrlRequestInterceptor,
|
||||
QWebEngineUrlRequestInfo)
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.browser import shared
|
||||
from qutebrowser.utils import utils, log
|
||||
from qutebrowser.utils import utils, log, debug
|
||||
|
||||
|
||||
class RequestInterceptor(QWebEngineUrlRequestInterceptor):
|
||||
|
||||
"""Handle ad blocking and custom headers."""
|
||||
|
||||
def __init__(self, host_blocker, parent=None):
|
||||
def __init__(self, host_blocker, args, parent=None):
|
||||
super().__init__(parent)
|
||||
self._host_blocker = host_blocker
|
||||
self._args = args
|
||||
|
||||
def install(self, profile):
|
||||
"""Install the interceptor on the given QWebEngineProfile."""
|
||||
@@ -54,15 +56,29 @@ class RequestInterceptor(QWebEngineUrlRequestInterceptor):
|
||||
Args:
|
||||
info: QWebEngineUrlRequestInfo &info
|
||||
"""
|
||||
if 'log-requests' in self._args.debug_flags:
|
||||
resource_type = debug.qenum_key(QWebEngineUrlRequestInfo,
|
||||
info.resourceType())
|
||||
navigation_type = debug.qenum_key(QWebEngineUrlRequestInfo,
|
||||
info.navigationType())
|
||||
log.webview.debug("{} {}, first-party {}, resource {}, "
|
||||
"navigation {}".format(
|
||||
bytes(info.requestMethod()).decode('ascii'),
|
||||
info.requestUrl().toDisplayString(),
|
||||
info.firstPartyUrl().toDisplayString(),
|
||||
resource_type, navigation_type))
|
||||
|
||||
url = info.requestUrl()
|
||||
|
||||
# FIXME:qtwebengine only block ads for NavigationTypeOther?
|
||||
if self._host_blocker.is_blocked(info.requestUrl()):
|
||||
if self._host_blocker.is_blocked(url):
|
||||
log.webview.info("Request to {} blocked by host blocker.".format(
|
||||
info.requestUrl().host()))
|
||||
url.host()))
|
||||
info.block(True)
|
||||
|
||||
for header, value in shared.custom_headers():
|
||||
for header, value in shared.custom_headers(url=url):
|
||||
info.setHttpHeader(header, value)
|
||||
|
||||
user_agent = config.val.content.headers.user_agent
|
||||
user_agent = config.instance.get('content.headers.user_agent', url=url)
|
||||
if user_agent is not None:
|
||||
info.setHttpHeader(b'User-Agent', user_agent.encode('ascii'))
|
||||
|
||||
@@ -21,38 +21,53 @@
|
||||
|
||||
import glob
|
||||
import os
|
||||
import os.path
|
||||
import re
|
||||
import shutil
|
||||
|
||||
from PyQt5.QtCore import QLibraryInfo
|
||||
from qutebrowser.utils import log
|
||||
from qutebrowser.utils import log, message, standarddir, qtutils
|
||||
|
||||
dict_version_re = re.compile(r".+-(?P<version>[0-9]+-[0-9]+?)\.bdic")
|
||||
|
||||
|
||||
def version(filename):
|
||||
"""Extract the version number from the dictionary file name."""
|
||||
version_re = re.compile(r".+-(?P<version>[0-9]+-[0-9]+?)\.bdic")
|
||||
match = version_re.fullmatch(filename)
|
||||
match = dict_version_re.match(filename)
|
||||
if match is None:
|
||||
raise ValueError('the given dictionary file name is malformed: {}'
|
||||
.format(filename))
|
||||
message.warning(
|
||||
"Found a dictionary with a malformed name: {}".format(filename))
|
||||
return None
|
||||
return tuple(int(n) for n in match.group('version').split('-'))
|
||||
|
||||
|
||||
def dictionary_dir():
|
||||
def dictionary_dir(old=False):
|
||||
"""Return the path (str) to the QtWebEngine's dictionaries directory."""
|
||||
datapath = QLibraryInfo.location(QLibraryInfo.DataPath)
|
||||
if qtutils.version_check('5.10', compiled=False) and not old:
|
||||
datapath = standarddir.data()
|
||||
else:
|
||||
datapath = QLibraryInfo.location(QLibraryInfo.DataPath)
|
||||
return os.path.join(datapath, 'qtwebengine_dictionaries')
|
||||
|
||||
|
||||
def local_files(code):
|
||||
"""Return all installed dictionaries for the given code."""
|
||||
"""Return all installed dictionaries for the given code.
|
||||
|
||||
The returned dictionaries are sorted by version, therefore the latest will
|
||||
be the first element. The list will be empty if no dictionaries are found.
|
||||
"""
|
||||
pathname = os.path.join(dictionary_dir(), '{}*.bdic'.format(code))
|
||||
matching_dicts = glob.glob(pathname)
|
||||
files = []
|
||||
for matching_dict in sorted(matching_dicts, key=version, reverse=True):
|
||||
filename = os.path.basename(matching_dict)
|
||||
log.config.debug('Found file for dict {}: {}'.format(code, filename))
|
||||
files.append(filename)
|
||||
return files
|
||||
versioned_dicts = []
|
||||
for matching_dict in matching_dicts:
|
||||
parsed_version = version(matching_dict)
|
||||
if parsed_version is not None:
|
||||
filename = os.path.basename(matching_dict)
|
||||
log.config.debug('Found file for dict {}: {}'
|
||||
.format(code, filename))
|
||||
versioned_dicts.append((parsed_version, filename))
|
||||
return [filename for version, filename
|
||||
in sorted(versioned_dicts, reverse=True)]
|
||||
|
||||
|
||||
def local_filename(code):
|
||||
@@ -63,3 +78,16 @@ def local_filename(code):
|
||||
"""
|
||||
all_installed = local_files(code)
|
||||
return os.path.splitext(all_installed[0])[0] if all_installed else None
|
||||
|
||||
|
||||
def init():
|
||||
"""Initialize the dictionary path if supported."""
|
||||
if qtutils.version_check('5.10', compiled=False):
|
||||
new_dir = dictionary_dir()
|
||||
old_dir = dictionary_dir(old=True)
|
||||
os.environ['QTWEBENGINE_DICTIONARIES_PATH'] = new_dir
|
||||
try:
|
||||
if os.path.exists(old_dir) and not os.path.exists(new_dir):
|
||||
shutil.copytree(old_dir, new_dir)
|
||||
except OSError:
|
||||
log.misc.exception("Failed to copy old dictionaries")
|
||||
|
||||
@@ -101,7 +101,11 @@ class DownloadItem(downloads.AbstractDownloadItem):
|
||||
|
||||
def retry(self):
|
||||
state = self._qt_item.state()
|
||||
assert state == QWebEngineDownloadItem.DownloadInterrupted, state
|
||||
if state != QWebEngineDownloadItem.DownloadInterrupted:
|
||||
log.downloads.warning(
|
||||
"Trying to retry download in state {}".format(
|
||||
debug.qenum_key(QWebEngineDownloadItem, state)))
|
||||
return
|
||||
|
||||
try:
|
||||
self._qt_item.resume()
|
||||
|
||||
@@ -27,7 +27,7 @@ from PyQt5.QtGui import QMouseEvent
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PyQt5.QtWebEngineWidgets import QWebEngineSettings
|
||||
|
||||
from qutebrowser.utils import log, javascript
|
||||
from qutebrowser.utils import log, javascript, urlutils
|
||||
from qutebrowser.browser import webelem
|
||||
|
||||
|
||||
@@ -198,6 +198,13 @@ class WebEngineElement(webelem.AbstractWebElement):
|
||||
if self.is_text_input() and self.is_editable():
|
||||
self._js_call('move_cursor_to_end')
|
||||
|
||||
def _requires_user_interaction(self):
|
||||
baseurl = self._tab.url()
|
||||
url = self.resolve_url(baseurl)
|
||||
if url is None:
|
||||
return True
|
||||
return url.scheme() not in urlutils.WEBENGINE_SCHEMES
|
||||
|
||||
def _click_editable(self, click_target):
|
||||
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-58515
|
||||
ev = QMouseEvent(QMouseEvent.MouseButtonPress, QPoint(0, 0),
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
import os
|
||||
|
||||
from PyQt5.QtCore import QUrl
|
||||
from PyQt5.QtWebEngineWidgets import QWebEngineView
|
||||
from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEngineSettings
|
||||
|
||||
from qutebrowser.browser import inspector
|
||||
|
||||
@@ -35,10 +35,12 @@ class WebEngineInspector(inspector.AbstractWebInspector):
|
||||
super().__init__(parent)
|
||||
self.port = None
|
||||
view = QWebEngineView()
|
||||
settings = view.settings()
|
||||
settings.setAttribute(QWebEngineSettings.JavascriptEnabled, True)
|
||||
self._set_widget(view)
|
||||
|
||||
def inspect(self, _page):
|
||||
"""Set up the inspector."""
|
||||
def _inspect_old(self, page):
|
||||
"""Set up the inspector for Qt < 5.11."""
|
||||
try:
|
||||
port = int(os.environ['QTWEBENGINE_REMOTE_DEBUGGING'])
|
||||
except KeyError:
|
||||
@@ -46,5 +48,18 @@ class WebEngineInspector(inspector.AbstractWebInspector):
|
||||
"QtWebEngine inspector is not enabled. See "
|
||||
"'qutebrowser --help' for details.")
|
||||
url = QUrl('http://localhost:{}/'.format(port))
|
||||
self._widget.load(url)
|
||||
self.show()
|
||||
|
||||
if page is None:
|
||||
self._widget.load(QUrl('about:blank'))
|
||||
else:
|
||||
self._widget.load(url)
|
||||
|
||||
def _inspect_new(self, page):
|
||||
"""Set up the inspector for Qt >= 5.11."""
|
||||
self._widget.page().setInspectedPage(page)
|
||||
|
||||
def inspect(self, page):
|
||||
try:
|
||||
self._inspect_new(page)
|
||||
except AttributeError:
|
||||
self._inspect_old(page)
|
||||
|
||||
@@ -34,6 +34,10 @@ class QuteSchemeHandler(QWebEngineUrlSchemeHandler):
|
||||
def install(self, profile):
|
||||
"""Install the handler for qute:// URLs on the given profile."""
|
||||
profile.installUrlSchemeHandler(b'qute', self)
|
||||
if qtutils.version_check('5.11', compiled=False):
|
||||
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-63378
|
||||
profile.installUrlSchemeHandler(b'chrome-error', self)
|
||||
profile.installUrlSchemeHandler(b'chrome-extension', self)
|
||||
|
||||
def requestStarted(self, job):
|
||||
"""Handle a request for a qute: scheme.
|
||||
@@ -45,6 +49,12 @@ class QuteSchemeHandler(QWebEngineUrlSchemeHandler):
|
||||
job: QWebEngineUrlRequestJob
|
||||
"""
|
||||
url = job.requestUrl()
|
||||
|
||||
if url.scheme() in ['chrome-error', 'chrome-extension']:
|
||||
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-63378
|
||||
job.fail(QWebEngineUrlRequestJob.UrlInvalid)
|
||||
return
|
||||
|
||||
assert job.requestMethod() == b'GET'
|
||||
assert url.scheme() == 'qute'
|
||||
log.misc.debug("Got request for {}".format(url.toDisplayString()))
|
||||
|
||||
@@ -26,16 +26,14 @@ Module attributes:
|
||||
|
||||
import os
|
||||
|
||||
import sip
|
||||
from PyQt5.QtGui import QFont
|
||||
from PyQt5.QtWebEngineWidgets import (QWebEngineSettings, QWebEngineProfile,
|
||||
QWebEngineScript)
|
||||
QWebEnginePage)
|
||||
|
||||
from qutebrowser.browser import shared
|
||||
from qutebrowser.browser.webengine import spell
|
||||
from qutebrowser.config import config, websettings
|
||||
from qutebrowser.utils import (utils, standarddir, javascript, qtutils,
|
||||
message, log, objreg)
|
||||
from qutebrowser.config.websettings import AttributeInfo as Attr
|
||||
from qutebrowser.utils import utils, standarddir, qtutils, message, log
|
||||
|
||||
# The default QWebEngineProfile
|
||||
default_profile = None
|
||||
@@ -91,35 +89,40 @@ class WebEngineSettings(websettings.AbstractSettings):
|
||||
|
||||
_ATTRIBUTES = {
|
||||
'content.xss_auditing':
|
||||
[QWebEngineSettings.XSSAuditingEnabled],
|
||||
Attr(QWebEngineSettings.XSSAuditingEnabled),
|
||||
'content.images':
|
||||
[QWebEngineSettings.AutoLoadImages],
|
||||
Attr(QWebEngineSettings.AutoLoadImages),
|
||||
'content.javascript.enabled':
|
||||
[QWebEngineSettings.JavascriptEnabled],
|
||||
Attr(QWebEngineSettings.JavascriptEnabled),
|
||||
'content.javascript.can_open_tabs_automatically':
|
||||
[QWebEngineSettings.JavascriptCanOpenWindows],
|
||||
Attr(QWebEngineSettings.JavascriptCanOpenWindows),
|
||||
'content.javascript.can_access_clipboard':
|
||||
[QWebEngineSettings.JavascriptCanAccessClipboard],
|
||||
Attr(QWebEngineSettings.JavascriptCanAccessClipboard),
|
||||
'content.plugins':
|
||||
[QWebEngineSettings.PluginsEnabled],
|
||||
Attr(QWebEngineSettings.PluginsEnabled),
|
||||
'content.hyperlink_auditing':
|
||||
[QWebEngineSettings.HyperlinkAuditingEnabled],
|
||||
Attr(QWebEngineSettings.HyperlinkAuditingEnabled),
|
||||
'content.local_content_can_access_remote_urls':
|
||||
[QWebEngineSettings.LocalContentCanAccessRemoteUrls],
|
||||
Attr(QWebEngineSettings.LocalContentCanAccessRemoteUrls),
|
||||
'content.local_content_can_access_file_urls':
|
||||
[QWebEngineSettings.LocalContentCanAccessFileUrls],
|
||||
Attr(QWebEngineSettings.LocalContentCanAccessFileUrls),
|
||||
'content.webgl':
|
||||
[QWebEngineSettings.WebGLEnabled],
|
||||
Attr(QWebEngineSettings.WebGLEnabled),
|
||||
'content.local_storage':
|
||||
[QWebEngineSettings.LocalStorageEnabled],
|
||||
Attr(QWebEngineSettings.LocalStorageEnabled),
|
||||
'content.desktop_capture':
|
||||
Attr(QWebEngineSettings.ScreenCaptureEnabled,
|
||||
converter=lambda val: True if val == 'ask' else val),
|
||||
# 'ask' is handled via the permission system,
|
||||
# or a hardcoded dialog on Qt < 5.10
|
||||
|
||||
'input.spatial_navigation':
|
||||
[QWebEngineSettings.SpatialNavigationEnabled],
|
||||
Attr(QWebEngineSettings.SpatialNavigationEnabled),
|
||||
'input.links_included_in_focus_chain':
|
||||
[QWebEngineSettings.LinksIncludedInFocusChain],
|
||||
Attr(QWebEngineSettings.LinksIncludedInFocusChain),
|
||||
|
||||
'scrolling.smooth':
|
||||
[QWebEngineSettings.ScrollAnimatorEnabled],
|
||||
Attr(QWebEngineSettings.ScrollAnimatorEnabled),
|
||||
}
|
||||
|
||||
_FONT_SIZES = {
|
||||
@@ -158,144 +161,116 @@ class WebEngineSettings(websettings.AbstractSettings):
|
||||
# Attributes which don't exist in all Qt versions.
|
||||
new_attributes = {
|
||||
# Qt 5.8
|
||||
'content.print_element_backgrounds': 'PrintElementBackgrounds',
|
||||
'content.print_element_backgrounds':
|
||||
('PrintElementBackgrounds', None),
|
||||
# Qt 5.11
|
||||
'content.autoplay':
|
||||
('PlaybackRequiresUserGesture', lambda val: not val),
|
||||
}
|
||||
for name, attribute in new_attributes.items():
|
||||
for name, (attribute, converter) in new_attributes.items():
|
||||
try:
|
||||
value = getattr(QWebEngineSettings, attribute)
|
||||
except AttributeError:
|
||||
continue
|
||||
|
||||
self._ATTRIBUTES[name] = [value]
|
||||
self._ATTRIBUTES[name] = Attr(value, converter=converter)
|
||||
|
||||
|
||||
def _init_stylesheet(profile):
|
||||
"""Initialize custom stylesheets.
|
||||
class ProfileSetter:
|
||||
|
||||
Partially inspired by QupZilla:
|
||||
https://github.com/QupZilla/qupzilla/blob/v2.0/src/lib/app/mainapplication.cpp#L1063-L1101
|
||||
"""
|
||||
old_script = profile.scripts().findScript('_qute_stylesheet')
|
||||
if not old_script.isNull():
|
||||
profile.scripts().remove(old_script)
|
||||
"""Helper to set various settings on a profile."""
|
||||
|
||||
css = shared.get_user_stylesheet()
|
||||
source = '\n'.join([
|
||||
'"use strict";',
|
||||
'window._qutebrowser = window._qutebrowser || {};',
|
||||
utils.read_file('javascript/stylesheet.js'),
|
||||
javascript.assemble('stylesheet', 'set_css', css),
|
||||
])
|
||||
def __init__(self, profile):
|
||||
self._profile = profile
|
||||
|
||||
script = QWebEngineScript()
|
||||
script.setName('_qute_stylesheet')
|
||||
script.setInjectionPoint(QWebEngineScript.DocumentCreation)
|
||||
script.setWorldId(QWebEngineScript.ApplicationWorld)
|
||||
script.setRunsOnSubFrames(True)
|
||||
script.setSourceCode(source)
|
||||
profile.scripts().insert(script)
|
||||
def init_profile(self):
|
||||
"""Initialize settings on the given profile."""
|
||||
self.set_http_headers()
|
||||
self.set_http_cache_size()
|
||||
|
||||
settings = self._profile.settings()
|
||||
settings.setAttribute(
|
||||
QWebEngineSettings.FullScreenSupportEnabled, True)
|
||||
try:
|
||||
settings.setAttribute(
|
||||
QWebEngineSettings.FocusOnNavigationEnabled, False)
|
||||
except AttributeError:
|
||||
# Added in Qt 5.8
|
||||
pass
|
||||
|
||||
def _update_stylesheet():
|
||||
"""Update the custom stylesheet in existing tabs."""
|
||||
css = shared.get_user_stylesheet()
|
||||
code = javascript.assemble('stylesheet', 'set_css', css)
|
||||
for win_id, window in objreg.window_registry.items():
|
||||
# We could be in the middle of destroying a window here
|
||||
if sip.isdeleted(window):
|
||||
continue
|
||||
tab_registry = objreg.get('tab-registry', scope='window',
|
||||
window=win_id)
|
||||
for tab in tab_registry.values():
|
||||
tab.run_js_async(code)
|
||||
if qtutils.version_check('5.8'):
|
||||
self.set_dictionary_language()
|
||||
|
||||
def set_http_headers(self):
|
||||
"""Set the user agent and accept-language for the given profile.
|
||||
|
||||
def _set_http_headers(profile):
|
||||
"""Set the user agent and accept-language for the given profile.
|
||||
We override those per request in the URL interceptor (to allow for
|
||||
per-domain values), but this one still gets used for things like
|
||||
window.navigator.userAgent/.languages in JS.
|
||||
"""
|
||||
self._profile.setHttpUserAgent(config.val.content.headers.user_agent)
|
||||
accept_language = config.val.content.headers.accept_language
|
||||
if accept_language is not None:
|
||||
self._profile.setHttpAcceptLanguage(accept_language)
|
||||
|
||||
We override those per request in the URL interceptor (to allow for
|
||||
per-domain values), but this one still gets used for things like
|
||||
window.navigator.userAgent/.languages in JS.
|
||||
"""
|
||||
profile.setHttpUserAgent(config.val.content.headers.user_agent)
|
||||
accept_language = config.val.content.headers.accept_language
|
||||
if accept_language is not None:
|
||||
profile.setHttpAcceptLanguage(accept_language)
|
||||
def set_http_cache_size(self):
|
||||
"""Initialize the HTTP cache size for the given profile."""
|
||||
size = config.val.content.cache.size
|
||||
if size is None:
|
||||
size = 0
|
||||
else:
|
||||
size = qtutils.check_overflow(size, 'int', fatal=False)
|
||||
|
||||
# 0: automatically managed by QtWebEngine
|
||||
self._profile.setHttpCacheMaximumSize(size)
|
||||
|
||||
def _set_http_cache_size(profile):
|
||||
"""Initialize the HTTP cache size for the given profile."""
|
||||
size = config.val.content.cache.size
|
||||
if size is None:
|
||||
size = 0
|
||||
else:
|
||||
size = qtutils.check_overflow(size, 'int', fatal=False)
|
||||
def set_persistent_cookie_policy(self):
|
||||
"""Set the HTTP Cookie size for the given profile."""
|
||||
assert not self._profile.isOffTheRecord()
|
||||
if config.val.content.cookies.store:
|
||||
value = QWebEngineProfile.AllowPersistentCookies
|
||||
else:
|
||||
value = QWebEngineProfile.NoPersistentCookies
|
||||
self._profile.setPersistentCookiesPolicy(value)
|
||||
|
||||
# 0: automatically managed by QtWebEngine
|
||||
profile.setHttpCacheMaximumSize(size)
|
||||
def set_dictionary_language(self, warn=True):
|
||||
"""Load the given dictionaries."""
|
||||
filenames = []
|
||||
for code in config.val.spellcheck.languages or []:
|
||||
local_filename = spell.local_filename(code)
|
||||
if not local_filename:
|
||||
if warn:
|
||||
message.warning("Language {} is not installed - see "
|
||||
"scripts/dictcli.py in qutebrowser's "
|
||||
"sources".format(code))
|
||||
continue
|
||||
|
||||
filenames.append(local_filename)
|
||||
|
||||
def _set_persistent_cookie_policy(profile):
|
||||
"""Set the HTTP Cookie size for the given profile."""
|
||||
if config.val.content.cookies.store:
|
||||
value = QWebEngineProfile.AllowPersistentCookies
|
||||
else:
|
||||
value = QWebEngineProfile.NoPersistentCookies
|
||||
profile.setPersistentCookiesPolicy(value)
|
||||
|
||||
|
||||
def _set_dictionary_language(profile, warn=True):
|
||||
filenames = []
|
||||
for code in config.val.spellcheck.languages or []:
|
||||
local_filename = spell.local_filename(code)
|
||||
if not local_filename:
|
||||
if warn:
|
||||
message.warning(
|
||||
"Language {} is not installed - see scripts/dictcli.py "
|
||||
"in qutebrowser's sources".format(code))
|
||||
continue
|
||||
|
||||
filenames.append(local_filename)
|
||||
|
||||
log.config.debug("Found dicts: {}".format(filenames))
|
||||
profile.setSpellCheckLanguages(filenames)
|
||||
log.config.debug("Found dicts: {}".format(filenames))
|
||||
self._profile.setSpellCheckLanguages(filenames)
|
||||
self._profile.setSpellCheckEnabled(bool(filenames))
|
||||
|
||||
|
||||
def _update_settings(option):
|
||||
"""Update global settings when qwebsettings changed."""
|
||||
global_settings.update_setting(option)
|
||||
|
||||
if option in ['scrolling.bar', 'content.user_stylesheets']:
|
||||
_init_stylesheet(default_profile)
|
||||
_init_stylesheet(private_profile)
|
||||
_update_stylesheet()
|
||||
elif option in ['content.headers.user_agent',
|
||||
'content.headers.accept_language']:
|
||||
_set_http_headers(default_profile)
|
||||
_set_http_headers(private_profile)
|
||||
if option in ['content.headers.user_agent',
|
||||
'content.headers.accept_language']:
|
||||
default_profile.setter.set_http_headers()
|
||||
private_profile.setter.set_http_headers()
|
||||
elif option == 'content.cache.size':
|
||||
_set_http_cache_size(default_profile)
|
||||
_set_http_cache_size(private_profile)
|
||||
default_profile.setter.set_http_cache_size()
|
||||
private_profile.setter.set_http_cache_size()
|
||||
elif (option == 'content.cookies.store' and
|
||||
# https://bugreports.qt.io/browse/QTBUG-58650
|
||||
qtutils.version_check('5.9', compiled=False)):
|
||||
_set_persistent_cookie_policy(default_profile)
|
||||
default_profile.setter.set_persistent_cookie_policy()
|
||||
# We're not touching the private profile's cookie policy.
|
||||
elif option == 'spellcheck.languages':
|
||||
_set_dictionary_language(default_profile)
|
||||
_set_dictionary_language(private_profile, warn=False)
|
||||
|
||||
|
||||
def _init_profile(profile):
|
||||
"""Init the given profile."""
|
||||
_init_stylesheet(profile)
|
||||
_set_http_headers(profile)
|
||||
_set_http_cache_size(profile)
|
||||
profile.settings().setAttribute(
|
||||
QWebEngineSettings.FullScreenSupportEnabled, True)
|
||||
if qtutils.version_check('5.8'):
|
||||
profile.setSpellCheckEnabled(True)
|
||||
_set_dictionary_language(profile)
|
||||
default_profile.setter.set_dictionary_language()
|
||||
private_profile.setter.set_dictionary_language(warn=False)
|
||||
|
||||
|
||||
def _init_profiles():
|
||||
@@ -303,60 +278,28 @@ def _init_profiles():
|
||||
global default_profile, private_profile
|
||||
|
||||
default_profile = QWebEngineProfile.defaultProfile()
|
||||
default_profile.setter = ProfileSetter(default_profile)
|
||||
default_profile.setCachePath(
|
||||
os.path.join(standarddir.cache(), 'webengine'))
|
||||
default_profile.setPersistentStoragePath(
|
||||
os.path.join(standarddir.data(), 'webengine'))
|
||||
_init_profile(default_profile)
|
||||
_set_persistent_cookie_policy(default_profile)
|
||||
default_profile.setter.init_profile()
|
||||
default_profile.setter.set_persistent_cookie_policy()
|
||||
|
||||
private_profile = QWebEngineProfile()
|
||||
private_profile.setter = ProfileSetter(private_profile)
|
||||
assert private_profile.isOffTheRecord()
|
||||
_init_profile(private_profile)
|
||||
|
||||
|
||||
def inject_userscripts():
|
||||
"""Register user JavaScript files with the global profiles."""
|
||||
# The Greasemonkey metadata block support in QtWebEngine only starts at
|
||||
# Qt 5.8. With 5.7.1, we need to inject the scripts ourselves in response
|
||||
# to urlChanged.
|
||||
if not qtutils.version_check('5.8'):
|
||||
return
|
||||
|
||||
# Since we are inserting scripts into profile.scripts they won't
|
||||
# just get replaced by new gm scripts like if we were injecting them
|
||||
# ourselves so we need to remove all gm scripts, while not removing
|
||||
# any other stuff that might have been added. Like the one for
|
||||
# stylesheets.
|
||||
greasemonkey = objreg.get('greasemonkey')
|
||||
for profile in [default_profile, private_profile]:
|
||||
scripts = profile.scripts()
|
||||
for script in scripts.toList():
|
||||
if script.name().startswith("GM-"):
|
||||
log.greasemonkey.debug('Removing script: {}'
|
||||
.format(script.name()))
|
||||
removed = scripts.remove(script)
|
||||
assert removed, script.name()
|
||||
|
||||
# Then add the new scripts.
|
||||
for script in greasemonkey.all_scripts():
|
||||
# @run-at (and @include/@exclude/@match) is parsed by
|
||||
# QWebEngineScript.
|
||||
new_script = QWebEngineScript()
|
||||
new_script.setWorldId(QWebEngineScript.MainWorld)
|
||||
new_script.setSourceCode(script.code())
|
||||
new_script.setName("GM-{}".format(script.name))
|
||||
new_script.setRunsOnSubFrames(script.runs_on_sub_frames)
|
||||
log.greasemonkey.debug('adding script: {}'
|
||||
.format(new_script.name()))
|
||||
scripts.insert(new_script)
|
||||
private_profile.setter.init_profile()
|
||||
|
||||
|
||||
def init(args):
|
||||
"""Initialize the global QWebSettings."""
|
||||
if args.enable_webengine_inspector:
|
||||
if (args.enable_webengine_inspector and
|
||||
not hasattr(QWebEnginePage, 'setInspectedPage')): # only Qt < 5.11
|
||||
os.environ['QTWEBENGINE_REMOTE_DEBUGGING'] = str(utils.random_port())
|
||||
|
||||
spell.init()
|
||||
|
||||
_init_profiles()
|
||||
config.instance.changed.connect(_update_settings)
|
||||
|
||||
|
||||
@@ -25,23 +25,23 @@ import sys
|
||||
import re
|
||||
import html as html_utils
|
||||
|
||||
import sip
|
||||
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, Qt, QEvent, QPoint, QPointF,
|
||||
QUrl, QTimer)
|
||||
QUrl, QTimer, QObject, qVersion)
|
||||
from PyQt5.QtGui import QKeyEvent, QIcon
|
||||
from PyQt5.QtNetwork import QAuthenticator
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PyQt5.QtWebEngineWidgets import QWebEnginePage, QWebEngineScript
|
||||
|
||||
from qutebrowser.config import configdata
|
||||
from qutebrowser.config import configdata, config
|
||||
from qutebrowser.browser import browsertab, mouse, shared
|
||||
from qutebrowser.browser.webengine import (webview, webengineelem, tabhistory,
|
||||
interceptor, webenginequtescheme,
|
||||
webenginedownloads,
|
||||
webenginesettings)
|
||||
cookies, webenginedownloads,
|
||||
webenginesettings, certificateerror)
|
||||
from qutebrowser.misc import miscwidgets
|
||||
from qutebrowser.utils import (usertypes, qtutils, log, javascript, utils,
|
||||
message, objreg, jinja, debug)
|
||||
from qutebrowser.qt import sip
|
||||
|
||||
|
||||
_qute_scheme_handler = None
|
||||
@@ -62,8 +62,9 @@ def init():
|
||||
|
||||
log.init.debug("Initializing request interceptor...")
|
||||
host_blocker = objreg.get('host-blocker')
|
||||
args = objreg.get('args')
|
||||
req_interceptor = interceptor.RequestInterceptor(
|
||||
host_blocker, parent=app)
|
||||
host_blocker, args=args, parent=app)
|
||||
req_interceptor.install(webenginesettings.default_profile)
|
||||
req_interceptor.install(webenginesettings.private_profile)
|
||||
|
||||
@@ -73,9 +74,17 @@ def init():
|
||||
download_manager.install(webenginesettings.private_profile)
|
||||
objreg.register('webengine-download-manager', download_manager)
|
||||
|
||||
greasemonkey = objreg.get('greasemonkey')
|
||||
greasemonkey.scripts_reloaded.connect(webenginesettings.inject_userscripts)
|
||||
webenginesettings.inject_userscripts()
|
||||
log.init.debug("Initializing cookie filter...")
|
||||
cookies.install_filter(webenginesettings.default_profile)
|
||||
cookies.install_filter(webenginesettings.private_profile)
|
||||
|
||||
# Clear visited links on web history clear
|
||||
hist = objreg.get('web-history')
|
||||
for p in [webenginesettings.default_profile,
|
||||
webenginesettings.private_profile]:
|
||||
hist.history_cleared.connect(p.clearAllVisitedLinks)
|
||||
hist.url_cleared.connect(lambda url, profile=p:
|
||||
profile.clearVisitedLinks([url]))
|
||||
|
||||
|
||||
# Mapping worlds from usertypes.JsWorld to QWebEngineScript world IDs.
|
||||
@@ -101,7 +110,11 @@ class WebEngineAction(browsertab.AbstractAction):
|
||||
"""Save the current page."""
|
||||
self._widget.triggerPageAction(QWebEnginePage.SavePage)
|
||||
|
||||
def show_source(self):
|
||||
def show_source(self, pygments=False):
|
||||
if pygments:
|
||||
self._show_source_pygments()
|
||||
return
|
||||
|
||||
try:
|
||||
self._widget.triggerPageAction(QWebEnginePage.ViewSource)
|
||||
except AttributeError:
|
||||
@@ -233,8 +246,16 @@ class WebEngineCaret(browsertab.AbstractCaret):
|
||||
self._tab.search.clear()
|
||||
|
||||
self._tab.run_js_async(
|
||||
javascript.assemble('caret', 'setPlatform', sys.platform))
|
||||
self._js_call('setInitialCursor')
|
||||
javascript.assemble('caret',
|
||||
'setPlatform', sys.platform, qVersion()))
|
||||
self._js_call('setInitialCursor', self._selection_cb)
|
||||
|
||||
def _selection_cb(self, enabled):
|
||||
"""Emit selection_toggled based on setInitialCursor."""
|
||||
if enabled is None:
|
||||
log.webview.debug("Ignoring selection status None")
|
||||
return
|
||||
self.selection_toggled.emit(enabled)
|
||||
|
||||
@pyqtSlot(usertypes.KeyMode)
|
||||
def _on_mode_left(self, mode):
|
||||
@@ -301,7 +322,7 @@ class WebEngineCaret(browsertab.AbstractCaret):
|
||||
self._js_call('moveToEndOfDocument')
|
||||
|
||||
def toggle_selection(self):
|
||||
self._js_call('toggleSelection')
|
||||
self._js_call('toggleSelection', self.selection_toggled.emit)
|
||||
|
||||
def drop_selection(self):
|
||||
self._js_call('dropSelection')
|
||||
@@ -323,6 +344,11 @@ class WebEngineCaret(browsertab.AbstractCaret):
|
||||
"""
|
||||
if js_elem is None:
|
||||
return
|
||||
if js_elem == "focused":
|
||||
# we had a focused element, not a selected one. Just send <enter>
|
||||
self._follow_enter(tab)
|
||||
return
|
||||
|
||||
assert isinstance(js_elem, dict), js_elem
|
||||
elem = webengineelem.WebEngineElement(js_elem, tab=self._tab)
|
||||
if tab:
|
||||
@@ -345,20 +371,16 @@ class WebEngineCaret(browsertab.AbstractCaret):
|
||||
|
||||
log.webview.debug("Clicking a searched link via fake key press.")
|
||||
# send a fake enter, clicking the orange selection box
|
||||
if tab:
|
||||
self._tab.key_press(Qt.Key_Enter, modifier=Qt.ControlModifier)
|
||||
else:
|
||||
self._tab.key_press(Qt.Key_Enter)
|
||||
|
||||
self._follow_enter(tab)
|
||||
else:
|
||||
# click an existing blue selection
|
||||
js_code = javascript.assemble('webelem', 'find_selected_link')
|
||||
js_code = javascript.assemble('webelem',
|
||||
'find_selected_focused_link')
|
||||
self._tab.run_js_async(js_code, lambda jsret:
|
||||
self._follow_selected_cb(jsret, tab))
|
||||
|
||||
def _js_call(self, command):
|
||||
self._tab.run_js_async(
|
||||
javascript.assemble('caret', command))
|
||||
def _js_call(self, command, callback=None):
|
||||
self._tab.run_js_async(javascript.assemble('caret', command), callback)
|
||||
|
||||
|
||||
class WebEngineScroller(browsertab.AbstractScroller):
|
||||
@@ -379,7 +401,7 @@ class WebEngineScroller(browsertab.AbstractScroller):
|
||||
|
||||
def _repeated_key_press(self, key, count=1, modifier=Qt.NoModifier):
|
||||
"""Send count fake key presses to this scroller's WebEngineTab."""
|
||||
for _ in range(min(count, 5000)):
|
||||
for _ in range(min(count, 1000)):
|
||||
self._tab.key_press(key, modifier)
|
||||
|
||||
@pyqtSlot(QPointF)
|
||||
@@ -432,6 +454,11 @@ class WebEngineScroller(browsertab.AbstractScroller):
|
||||
js_code = javascript.assemble('window', 'scroll', point.x(), point.y())
|
||||
self._tab.run_js_async(js_code)
|
||||
|
||||
def to_anchor(self, name):
|
||||
url = self._tab.url()
|
||||
url.setFragment(name)
|
||||
self._tab.openurl(url)
|
||||
|
||||
def delta(self, x=0, y=0):
|
||||
self._tab.run_js_async(javascript.assemble('window', 'scrollBy', x, y))
|
||||
|
||||
@@ -506,6 +533,9 @@ class WebEngineHistory(browsertab.AbstractHistory):
|
||||
return qtutils.deserialize(data, self._history)
|
||||
|
||||
def load_items(self, items):
|
||||
if items:
|
||||
self._tab.predicted_navigation.emit(items[-1].url)
|
||||
|
||||
stream, _data, cur_data = tabhistory.serialize(items)
|
||||
qtutils.deserialize_stream(stream, self._history)
|
||||
|
||||
@@ -596,6 +626,333 @@ class WebEngineElements(browsertab.AbstractElements):
|
||||
self._tab.run_js_async(js_code, js_cb)
|
||||
|
||||
|
||||
class WebEngineAudio(browsertab.AbstractAudio):
|
||||
|
||||
"""QtWebEngine implemementations related to audio/muting."""
|
||||
|
||||
def _connect_signals(self):
|
||||
page = self._widget.page()
|
||||
page.audioMutedChanged.connect(self.muted_changed)
|
||||
page.recentlyAudibleChanged.connect(self.recently_audible_changed)
|
||||
|
||||
def set_muted(self, muted: bool):
|
||||
page = self._widget.page()
|
||||
page.setAudioMuted(muted)
|
||||
|
||||
def is_muted(self):
|
||||
page = self._widget.page()
|
||||
return page.isAudioMuted()
|
||||
|
||||
def is_recently_audible(self):
|
||||
page = self._widget.page()
|
||||
return page.recentlyAudible()
|
||||
|
||||
|
||||
class _WebEnginePermissions(QObject):
|
||||
|
||||
"""Handling of various permission-related signals."""
|
||||
|
||||
_abort_questions = pyqtSignal()
|
||||
|
||||
def __init__(self, tab, parent=None):
|
||||
super().__init__(parent)
|
||||
self._tab = tab
|
||||
self._widget = None
|
||||
|
||||
def connect_signals(self):
|
||||
"""Connect related signals from the QWebEnginePage."""
|
||||
page = self._widget.page()
|
||||
page.fullScreenRequested.connect(
|
||||
self._on_fullscreen_requested)
|
||||
page.featurePermissionRequested.connect(
|
||||
self._on_feature_permission_requested)
|
||||
|
||||
if qtutils.version_check('5.11'):
|
||||
page.quotaRequested.connect(
|
||||
self._on_quota_requested)
|
||||
page.registerProtocolHandlerRequested.connect(
|
||||
self._on_register_protocol_handler_requested)
|
||||
|
||||
self._tab.shutting_down.connect(self._abort_questions)
|
||||
self._tab.load_started.connect(self._abort_questions)
|
||||
|
||||
@pyqtSlot('QWebEngineFullScreenRequest')
|
||||
def _on_fullscreen_requested(self, request):
|
||||
request.accept()
|
||||
on = request.toggleOn()
|
||||
|
||||
self._tab.data.fullscreen = on
|
||||
self._tab.fullscreen_requested.emit(on)
|
||||
if on:
|
||||
notification = miscwidgets.FullscreenNotification(self._widget)
|
||||
notification.show()
|
||||
notification.set_timeout(3000)
|
||||
|
||||
@pyqtSlot(QUrl, 'QWebEnginePage::Feature')
|
||||
def _on_feature_permission_requested(self, url, feature):
|
||||
"""Ask the user for approval for geolocation/media/etc.."""
|
||||
options = {
|
||||
QWebEnginePage.Geolocation: 'content.geolocation',
|
||||
QWebEnginePage.MediaAudioCapture: 'content.media_capture',
|
||||
QWebEnginePage.MediaVideoCapture: 'content.media_capture',
|
||||
QWebEnginePage.MediaAudioVideoCapture: 'content.media_capture',
|
||||
}
|
||||
messages = {
|
||||
QWebEnginePage.Geolocation: 'access your location',
|
||||
QWebEnginePage.MediaAudioCapture: 'record audio',
|
||||
QWebEnginePage.MediaVideoCapture: 'record video',
|
||||
QWebEnginePage.MediaAudioVideoCapture: 'record audio/video',
|
||||
}
|
||||
try:
|
||||
options.update({
|
||||
QWebEnginePage.DesktopVideoCapture:
|
||||
'content.desktop_capture',
|
||||
QWebEnginePage.DesktopAudioVideoCapture:
|
||||
'content.desktop_capture',
|
||||
})
|
||||
messages.update({
|
||||
QWebEnginePage.DesktopVideoCapture:
|
||||
'capture your desktop',
|
||||
QWebEnginePage.DesktopAudioVideoCapture:
|
||||
'capture your desktop and audio',
|
||||
})
|
||||
except AttributeError:
|
||||
# Added in Qt 5.10
|
||||
pass
|
||||
|
||||
assert options.keys() == messages.keys()
|
||||
|
||||
page = self._widget.page()
|
||||
|
||||
if feature not in options:
|
||||
log.webview.error("Unhandled feature permission {}".format(
|
||||
debug.qenum_key(QWebEnginePage, feature)))
|
||||
page.setFeaturePermission(url, feature,
|
||||
QWebEnginePage.PermissionDeniedByUser)
|
||||
return
|
||||
|
||||
yes_action = functools.partial(
|
||||
page.setFeaturePermission, url, feature,
|
||||
QWebEnginePage.PermissionGrantedByUser)
|
||||
no_action = functools.partial(
|
||||
page.setFeaturePermission, url, feature,
|
||||
QWebEnginePage.PermissionDeniedByUser)
|
||||
|
||||
question = shared.feature_permission(
|
||||
url=url, option=options[feature], msg=messages[feature],
|
||||
yes_action=yes_action, no_action=no_action,
|
||||
abort_on=[self._abort_questions])
|
||||
|
||||
if question is not None:
|
||||
page.featurePermissionRequestCanceled.connect(
|
||||
functools.partial(self._on_feature_permission_cancelled,
|
||||
question, url, feature))
|
||||
|
||||
def _on_feature_permission_cancelled(self, question, url, feature,
|
||||
cancelled_url, cancelled_feature):
|
||||
"""Slot invoked when a feature permission request was cancelled.
|
||||
|
||||
To be used with functools.partial.
|
||||
"""
|
||||
if url == cancelled_url and feature == cancelled_feature:
|
||||
try:
|
||||
question.abort()
|
||||
except RuntimeError:
|
||||
# The question could already be deleted, e.g. because it was
|
||||
# aborted after a loadStarted signal.
|
||||
pass
|
||||
|
||||
def _on_quota_requested(self, request):
|
||||
size = utils.format_size(request.requestedSize())
|
||||
shared.feature_permission(
|
||||
url=request.origin(),
|
||||
option='content.persistent_storage',
|
||||
msg='use {} of persistent storage'.format(size),
|
||||
yes_action=request.accept, no_action=request.reject,
|
||||
abort_on=[self._abort_questions],
|
||||
blocking=True)
|
||||
|
||||
def _on_register_protocol_handler_requested(self, request):
|
||||
shared.feature_permission(
|
||||
url=request.origin(),
|
||||
option='content.register_protocol_handler',
|
||||
msg='open all {} links'.format(request.scheme()),
|
||||
yes_action=request.accept, no_action=request.reject,
|
||||
abort_on=[self._abort_questions],
|
||||
blocking=True)
|
||||
|
||||
|
||||
class _WebEngineScripts(QObject):
|
||||
|
||||
def __init__(self, tab, parent=None):
|
||||
super().__init__(parent)
|
||||
self._tab = tab
|
||||
self._widget = None
|
||||
self._greasemonkey = objreg.get('greasemonkey')
|
||||
|
||||
def connect_signals(self):
|
||||
config.instance.changed.connect(self._on_config_changed)
|
||||
|
||||
@pyqtSlot(str)
|
||||
def _on_config_changed(self, option):
|
||||
if option in ['scrolling.bar', 'content.user_stylesheets']:
|
||||
self._init_stylesheet()
|
||||
self._update_stylesheet()
|
||||
|
||||
def _update_stylesheet(self):
|
||||
"""Update the custom stylesheet in existing tabs."""
|
||||
css = shared.get_user_stylesheet()
|
||||
code = javascript.assemble('stylesheet', 'set_css', css)
|
||||
self._tab.run_js_async(code)
|
||||
|
||||
def _inject_early_js(self, name, js_code, *,
|
||||
world=QWebEngineScript.ApplicationWorld,
|
||||
subframes=False):
|
||||
"""Inject the given script to run early on a page load.
|
||||
|
||||
This runs the script both on DocumentCreation and DocumentReady as on
|
||||
some internal pages, DocumentCreation will not work.
|
||||
|
||||
That is a WORKAROUND for https://bugreports.qt.io/browse/QTBUG-66011
|
||||
"""
|
||||
scripts = self._widget.page().scripts()
|
||||
for injection in ['creation', 'ready']:
|
||||
injection_points = {
|
||||
'creation': QWebEngineScript.DocumentCreation,
|
||||
'ready': QWebEngineScript.DocumentReady,
|
||||
}
|
||||
script = QWebEngineScript()
|
||||
script.setInjectionPoint(injection_points[injection])
|
||||
script.setSourceCode(js_code)
|
||||
script.setWorldId(world)
|
||||
script.setRunsOnSubFrames(subframes)
|
||||
script.setName('_qute_{}_{}'.format(name, injection))
|
||||
scripts.insert(script)
|
||||
|
||||
def _remove_early_js(self, name):
|
||||
"""Remove an early QWebEngineScript."""
|
||||
scripts = self._widget.page().scripts()
|
||||
for injection in ['creation', 'ready']:
|
||||
full_name = '_qute_{}_{}'.format(name, injection)
|
||||
script = scripts.findScript(full_name)
|
||||
if not script.isNull():
|
||||
scripts.remove(script)
|
||||
|
||||
def init(self):
|
||||
"""Initialize global qutebrowser JavaScript."""
|
||||
js_code = javascript.wrap_global(
|
||||
'scripts',
|
||||
utils.read_file('javascript/scroll.js'),
|
||||
utils.read_file('javascript/webelem.js'),
|
||||
utils.read_file('javascript/caret.js'),
|
||||
)
|
||||
self._inject_early_js('js',
|
||||
utils.read_file('javascript/print.js'),
|
||||
subframes=True,
|
||||
world=QWebEngineScript.MainWorld)
|
||||
# FIXME:qtwebengine what about subframes=True?
|
||||
self._inject_early_js('js', js_code, subframes=True)
|
||||
self._init_stylesheet()
|
||||
|
||||
# The Greasemonkey metadata block support in QtWebEngine only starts at
|
||||
# Qt 5.8. With 5.7.1, we need to inject the scripts ourselves in
|
||||
# response to urlChanged.
|
||||
if not qtutils.version_check('5.8'):
|
||||
self._tab.url_changed.connect(
|
||||
self._inject_greasemonkey_scripts_for_url)
|
||||
else:
|
||||
self._greasemonkey.scripts_reloaded.connect(
|
||||
self._inject_all_greasemonkey_scripts)
|
||||
self._inject_all_greasemonkey_scripts()
|
||||
|
||||
def _init_stylesheet(self):
|
||||
"""Initialize custom stylesheets.
|
||||
|
||||
Partially inspired by QupZilla:
|
||||
https://github.com/QupZilla/qupzilla/blob/v2.0/src/lib/app/mainapplication.cpp#L1063-L1101
|
||||
"""
|
||||
self._remove_early_js('stylesheet')
|
||||
css = shared.get_user_stylesheet()
|
||||
js_code = javascript.wrap_global(
|
||||
'stylesheet',
|
||||
utils.read_file('javascript/stylesheet.js'),
|
||||
javascript.assemble('stylesheet', 'set_css', css),
|
||||
)
|
||||
self._inject_early_js('stylesheet', js_code, subframes=True)
|
||||
|
||||
@pyqtSlot(QUrl)
|
||||
def _inject_greasemonkey_scripts_for_url(self, url):
|
||||
matching_scripts = self._greasemonkey.scripts_for(url)
|
||||
self._inject_greasemonkey_scripts(
|
||||
matching_scripts.start, QWebEngineScript.DocumentCreation, True)
|
||||
self._inject_greasemonkey_scripts(
|
||||
matching_scripts.end, QWebEngineScript.DocumentReady, False)
|
||||
self._inject_greasemonkey_scripts(
|
||||
matching_scripts.idle, QWebEngineScript.Deferred, False)
|
||||
|
||||
@pyqtSlot()
|
||||
def _inject_all_greasemonkey_scripts(self):
|
||||
scripts = self._greasemonkey.all_scripts()
|
||||
self._inject_greasemonkey_scripts(scripts)
|
||||
|
||||
def _inject_greasemonkey_scripts(self, scripts=None, injection_point=None,
|
||||
remove_first=True):
|
||||
"""Register user JavaScript files with the current tab.
|
||||
|
||||
Args:
|
||||
scripts: A list of GreasemonkeyScripts, or None to add all
|
||||
known by the Greasemonkey subsystem.
|
||||
injection_point: The QWebEngineScript::InjectionPoint stage
|
||||
to inject the script into, None to use
|
||||
auto-detection.
|
||||
remove_first: Whether to remove all previously injected
|
||||
scripts before adding these ones.
|
||||
"""
|
||||
if sip.isdeleted(self._widget):
|
||||
return
|
||||
|
||||
# Since we are inserting scripts into a per-tab collection,
|
||||
# rather than just injecting scripts on page load, we need to
|
||||
# make sure we replace existing scripts, not just add new ones.
|
||||
# While, taking care not to remove any other scripts that might
|
||||
# have been added elsewhere, like the one for stylesheets.
|
||||
page_scripts = self._widget.page().scripts()
|
||||
if remove_first:
|
||||
for script in page_scripts.toList():
|
||||
if script.name().startswith("GM-"):
|
||||
log.greasemonkey.debug('Removing script: {}'
|
||||
.format(script.name()))
|
||||
removed = page_scripts.remove(script)
|
||||
assert removed, script.name()
|
||||
|
||||
if not scripts:
|
||||
return
|
||||
|
||||
for script in scripts:
|
||||
new_script = QWebEngineScript()
|
||||
try:
|
||||
world = int(script.jsworld)
|
||||
except ValueError:
|
||||
try:
|
||||
world = _JS_WORLD_MAP[usertypes.JsWorld[
|
||||
script.jsworld.lower()]]
|
||||
except KeyError:
|
||||
log.greasemonkey.error(
|
||||
"script {} has invalid value for '@qute-js-world'"
|
||||
": {}".format(script.name, script.jsworld))
|
||||
continue
|
||||
new_script.setWorldId(world)
|
||||
new_script.setSourceCode(script.code())
|
||||
new_script.setName("GM-{}".format(script.name))
|
||||
new_script.setRunsOnSubFrames(script.runs_on_sub_frames)
|
||||
# Override the @run-at value parsed by QWebEngineScript if desired.
|
||||
if injection_point:
|
||||
new_script.setInjectionPoint(injection_point)
|
||||
log.greasemonkey.debug('adding script: {}'
|
||||
.format(new_script.name()))
|
||||
page_scripts.insert(new_script)
|
||||
|
||||
|
||||
class WebEngineTab(browsertab.AbstractTab):
|
||||
|
||||
"""A QtWebEngine tab in the browser.
|
||||
@@ -622,41 +979,32 @@ class WebEngineTab(browsertab.AbstractTab):
|
||||
self.printing = WebEnginePrinting()
|
||||
self.elements = WebEngineElements(tab=self)
|
||||
self.action = WebEngineAction(tab=self)
|
||||
self.audio = WebEngineAudio(parent=self)
|
||||
self._permissions = _WebEnginePermissions(tab=self, parent=self)
|
||||
self._scripts = _WebEngineScripts(tab=self, parent=self)
|
||||
# We're assigning settings in _set_widget
|
||||
self.settings = webenginesettings.WebEngineSettings(settings=None)
|
||||
self._set_widget(widget)
|
||||
self._connect_signals()
|
||||
self.backend = usertypes.Backend.QtWebEngine
|
||||
self._init_js()
|
||||
self._child_event_filter = None
|
||||
self._saved_zoom = None
|
||||
self._reload_url = None
|
||||
self._scripts.init()
|
||||
|
||||
def _init_js(self):
|
||||
js_code = '\n'.join([
|
||||
'"use strict";',
|
||||
'window._qutebrowser = window._qutebrowser || {};',
|
||||
utils.read_file('javascript/scroll.js'),
|
||||
utils.read_file('javascript/webelem.js'),
|
||||
utils.read_file('javascript/caret.js'),
|
||||
])
|
||||
script = QWebEngineScript()
|
||||
# We can't use DocumentCreation here as WORKAROUND for
|
||||
# https://bugreports.qt.io/browse/QTBUG-66011
|
||||
script.setInjectionPoint(QWebEngineScript.DocumentReady)
|
||||
script.setSourceCode(js_code)
|
||||
|
||||
page = self._widget.page()
|
||||
script.setWorldId(QWebEngineScript.ApplicationWorld)
|
||||
|
||||
# FIXME:qtwebengine what about runsOnSubFrames?
|
||||
page.scripts().insert(script)
|
||||
def _set_widget(self, widget):
|
||||
# pylint: disable=protected-access
|
||||
super()._set_widget(widget)
|
||||
self._permissions._widget = widget
|
||||
self._scripts._widget = widget
|
||||
|
||||
def _install_event_filter(self):
|
||||
self._widget.focusProxy().installEventFilter(self._mouse_event_filter)
|
||||
fp = self._widget.focusProxy()
|
||||
if fp is not None:
|
||||
fp.installEventFilter(self._mouse_event_filter)
|
||||
self._child_event_filter = mouse.ChildEventFilter(
|
||||
eventfilter=self._mouse_event_filter, widget=self._widget,
|
||||
parent=self)
|
||||
win_id=self.win_id, parent=self)
|
||||
self._widget.installEventFilter(self._child_event_filter)
|
||||
|
||||
@pyqtSlot()
|
||||
@@ -669,9 +1017,18 @@ class WebEngineTab(browsertab.AbstractTab):
|
||||
self.zoom.set_factor(self._saved_zoom)
|
||||
self._saved_zoom = None
|
||||
|
||||
def openurl(self, url):
|
||||
def openurl(self, url, *, predict=True):
|
||||
"""Open the given URL in this tab.
|
||||
|
||||
Arguments:
|
||||
url: The QUrl to open.
|
||||
predict: If set to False, predicted_navigation is not emitted.
|
||||
"""
|
||||
if sip.isdeleted(self._widget):
|
||||
# https://github.com/qutebrowser/qutebrowser/issues/3896
|
||||
return
|
||||
self._saved_zoom = self.zoom.factor()
|
||||
self._openurl_prepare(url)
|
||||
self._openurl_prepare(url, predict=predict)
|
||||
self._widget.load(url)
|
||||
|
||||
def url(self, requested=False):
|
||||
@@ -706,7 +1063,6 @@ class WebEngineTab(browsertab.AbstractTab):
|
||||
self._widget.shutdown()
|
||||
|
||||
def reload(self, *, force=False):
|
||||
self.predicted_navigation.emit(self.url())
|
||||
if force:
|
||||
action = QWebEnginePage.ReloadAndBypassCache
|
||||
else:
|
||||
@@ -826,26 +1182,13 @@ class WebEngineTab(browsertab.AbstractTab):
|
||||
# https://www.riverbankcomputing.com/pipermail/pyqt/2016-December/038400.html
|
||||
self._show_error_page(url, "Authentication required")
|
||||
|
||||
@pyqtSlot('QWebEngineFullScreenRequest')
|
||||
def _on_fullscreen_requested(self, request):
|
||||
request.accept()
|
||||
on = request.toggleOn()
|
||||
|
||||
self.data.fullscreen = on
|
||||
self.fullscreen_requested.emit(on)
|
||||
if on:
|
||||
notification = miscwidgets.FullscreenNotification(self)
|
||||
notification.show()
|
||||
notification.set_timeout(3000)
|
||||
|
||||
@pyqtSlot()
|
||||
def _on_load_started(self):
|
||||
"""Clear search when a new load is started if needed."""
|
||||
if (qtutils.version_check('5.9', compiled=False) and
|
||||
not qtutils.version_check('5.9.2', compiled=False)):
|
||||
# WORKAROUND for
|
||||
# https://bugreports.qt.io/browse/QTBUG-61506
|
||||
self.search.clear()
|
||||
# WORKAROUND for
|
||||
# https://bugreports.qt.io/browse/QTBUG-61506
|
||||
# (seems to be back in later Qt versions as well)
|
||||
self.search.clear()
|
||||
super()._on_load_started()
|
||||
self.data.netrc_used = False
|
||||
|
||||
@@ -915,10 +1258,11 @@ class WebEngineTab(browsertab.AbstractTab):
|
||||
if ok and self._reload_url is not None:
|
||||
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-66656
|
||||
log.config.debug(
|
||||
"Reloading {} because of config change".format(
|
||||
"Loading {} again because of config change".format(
|
||||
self._reload_url.toDisplayString()))
|
||||
QTimer.singleShot(100, lambda url=self._reload_url:
|
||||
self.openurl(url))
|
||||
QTimer.singleShot(100, functools.partial(self.openurl,
|
||||
self._reload_url,
|
||||
predict=False))
|
||||
self._reload_url = None
|
||||
|
||||
if not qtutils.version_check('5.10', compiled=False):
|
||||
@@ -928,32 +1272,82 @@ class WebEngineTab(browsertab.AbstractTab):
|
||||
# the old icon is still displayed.
|
||||
self.icon_changed.emit(QIcon())
|
||||
|
||||
@pyqtSlot(certificateerror.CertificateErrorWrapper)
|
||||
def _on_ssl_errors(self, error):
|
||||
self._has_ssl_errors = True
|
||||
|
||||
url = error.url()
|
||||
log.webview.debug("Certificate error: {}".format(error))
|
||||
|
||||
if error.is_overridable():
|
||||
error.ignore = shared.ignore_certificate_errors(
|
||||
url, [error], abort_on=[self.shutting_down, self.load_started])
|
||||
else:
|
||||
log.webview.error("Non-overridable certificate error: "
|
||||
"{}".format(error))
|
||||
|
||||
log.webview.debug("ignore {}, URL {}, requested {}".format(
|
||||
error.ignore, url, self.url(requested=True)))
|
||||
|
||||
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-56207
|
||||
# We can't really know when to show an error page, as the error might
|
||||
# have happened when loading some resource.
|
||||
# However, self.url() is not available yet and the requested URL
|
||||
# might not match the URL we get from the error - so we just apply a
|
||||
# heuristic here.
|
||||
if (not qtutils.version_check('5.9') and
|
||||
not error.ignore and
|
||||
url.matches(self.url(requested=True), QUrl.RemoveScheme)):
|
||||
self._show_error_page(url, str(error))
|
||||
|
||||
@pyqtSlot(QUrl)
|
||||
def _on_predicted_navigation(self, url):
|
||||
"""If we know we're going to visit an URL soon, change the settings."""
|
||||
self.settings.update_for_url(url)
|
||||
"""If we know we're going to visit an URL soon, change the settings.
|
||||
|
||||
This is a WORKAROUND for https://bugreports.qt.io/browse/QTBUG-66656
|
||||
"""
|
||||
super()._on_predicted_navigation(url)
|
||||
if not qtutils.version_check('5.11.1', compiled=False):
|
||||
self.settings.update_for_url(url)
|
||||
|
||||
@pyqtSlot(usertypes.NavigationRequest)
|
||||
def _on_navigation_request(self, navigation):
|
||||
super()._on_navigation_request(navigation)
|
||||
|
||||
if navigation.url == QUrl('qute://print'):
|
||||
command_dispatcher = objreg.get('command-dispatcher',
|
||||
scope='window',
|
||||
window=self.win_id)
|
||||
command_dispatcher.printpage()
|
||||
navigation.accepted = False
|
||||
|
||||
if not navigation.accepted or not navigation.is_main_frame:
|
||||
return
|
||||
|
||||
needs_reload = {
|
||||
settings_needing_reload = {
|
||||
'content.plugins',
|
||||
'content.javascript.enabled',
|
||||
'content.javascript.can_access_clipboard',
|
||||
'content.javascript.can_access_clipboard',
|
||||
'content.print_element_backgrounds',
|
||||
'input.spatial_navigation',
|
||||
'input.spatial_navigation',
|
||||
}
|
||||
assert needs_reload.issubset(configdata.DATA)
|
||||
assert settings_needing_reload.issubset(configdata.DATA)
|
||||
|
||||
changed = self.settings.update_for_url(navigation.url)
|
||||
if (changed & needs_reload and navigation.navigation_type !=
|
||||
navigation.Type.link_clicked):
|
||||
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-66656
|
||||
reload_needed = changed & settings_needing_reload
|
||||
|
||||
# On Qt < 5.11, we don't don't need a reload when type == link_clicked.
|
||||
# On Qt 5.11.0, we always need a reload.
|
||||
# On Qt > 5.11.0, we never need a reload:
|
||||
# https://codereview.qt-project.org/#/c/229525/1
|
||||
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-66656
|
||||
if qtutils.version_check('5.11.1', compiled=False):
|
||||
reload_needed = False
|
||||
elif not qtutils.version_check('5.11.0', exact=True, compiled=False):
|
||||
if navigation.navigation_type == navigation.Type.link_clicked:
|
||||
reload_needed = False
|
||||
|
||||
if reload_needed:
|
||||
self._reload_url = navigation.url
|
||||
|
||||
def _connect_signals(self):
|
||||
@@ -968,7 +1362,6 @@ class WebEngineTab(browsertab.AbstractTab):
|
||||
page.authenticationRequired.connect(self._on_authentication_required)
|
||||
page.proxyAuthenticationRequired.connect(
|
||||
self._on_proxy_authentication_required)
|
||||
page.fullScreenRequested.connect(self._on_fullscreen_requested)
|
||||
page.contentsSizeChanged.connect(self.contents_size_changed)
|
||||
page.navigation_request.connect(self._on_navigation_request)
|
||||
|
||||
@@ -993,5 +1386,10 @@ class WebEngineTab(browsertab.AbstractTab):
|
||||
|
||||
self.predicted_navigation.connect(self._on_predicted_navigation)
|
||||
|
||||
# pylint: disable=protected-access
|
||||
self.audio._connect_signals()
|
||||
self._permissions.connect_signals()
|
||||
self._scripts.connect_signals()
|
||||
|
||||
def event_target(self):
|
||||
return self._widget.focusProxy()
|
||||
return self._widget.render_widget()
|
||||
|
||||
@@ -19,17 +19,17 @@
|
||||
|
||||
"""The main browser widget for QtWebEngine."""
|
||||
|
||||
import functools
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QUrl, PYQT_VERSION
|
||||
from PyQt5.QtCore import pyqtSignal, QUrl, PYQT_VERSION
|
||||
from PyQt5.QtGui import QPalette
|
||||
from PyQt5.QtWebEngineWidgets import (QWebEngineView, QWebEnginePage,
|
||||
QWebEngineScript)
|
||||
from PyQt5.QtWidgets import QWidget
|
||||
from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage
|
||||
|
||||
from qutebrowser.browser import shared
|
||||
from qutebrowser.browser.webengine import certificateerror, webenginesettings
|
||||
from qutebrowser.browser.webengine import webenginesettings, certificateerror
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import log, debug, usertypes, jinja, objreg, qtutils
|
||||
from qutebrowser.utils import log, debug, usertypes, objreg, qtutils
|
||||
from qutebrowser.misc import miscwidgets
|
||||
from qutebrowser.qt import sip
|
||||
|
||||
|
||||
class WebEngineView(QWebEngineView):
|
||||
@@ -51,6 +51,35 @@ class WebEngineView(QWebEngineView):
|
||||
parent=self)
|
||||
self.setPage(page)
|
||||
|
||||
if qtutils.version_check('5.11', compiled=False):
|
||||
# Set a PseudoLayout as a WORKAROUND for
|
||||
# https://bugreports.qt.io/browse/QTBUG-68224
|
||||
# and other related issues.
|
||||
sip.delete(self.layout())
|
||||
self._layout = miscwidgets.PseudoLayout(self)
|
||||
|
||||
def render_widget(self):
|
||||
"""Get the RenderWidgetHostViewQt for this view.
|
||||
|
||||
Normally, this would always be the focusProxy().
|
||||
However, it sometimes isn't, so we use this as a WORKAROUND for
|
||||
https://bugreports.qt.io/browse/QTBUG-68727
|
||||
"""
|
||||
if 'lost-focusproxy' not in objreg.get('args').debug_flags:
|
||||
proxy = self.focusProxy()
|
||||
if proxy is not None:
|
||||
return proxy
|
||||
|
||||
# We don't want e.g. a QMenu.
|
||||
rwhv_class = 'QtWebEngineCore::RenderWidgetHostViewQtDelegateWidget'
|
||||
children = [c for c in self.findChildren(QWidget)
|
||||
if c.isVisible() and c.inherits(rwhv_class)]
|
||||
|
||||
log.webview.debug("Found possibly lost focusProxy: {}"
|
||||
.format(children))
|
||||
|
||||
return children[-1] if children else None
|
||||
|
||||
def shutdown(self):
|
||||
self.page().shutdown()
|
||||
|
||||
@@ -122,23 +151,22 @@ class WebEnginePage(QWebEnginePage):
|
||||
|
||||
Signals:
|
||||
certificate_error: Emitted on certificate errors.
|
||||
Needs to be directly connected to a slot setting the
|
||||
'ignore' attribute.
|
||||
shutting_down: Emitted when the page is shutting down.
|
||||
navigation_request: Emitted on acceptNavigationRequest.
|
||||
"""
|
||||
|
||||
certificate_error = pyqtSignal()
|
||||
certificate_error = pyqtSignal(certificateerror.CertificateErrorWrapper)
|
||||
shutting_down = pyqtSignal()
|
||||
navigation_request = pyqtSignal(usertypes.NavigationRequest)
|
||||
|
||||
def __init__(self, *, theme_color, profile, parent=None):
|
||||
super().__init__(profile, parent)
|
||||
self._is_shutting_down = False
|
||||
self.featurePermissionRequested.connect(
|
||||
self._on_feature_permission_requested)
|
||||
self._theme_color = theme_color
|
||||
self._set_bg_color()
|
||||
config.instance.changed.connect(self._set_bg_color)
|
||||
self.urlChanged.connect(self._inject_userjs)
|
||||
|
||||
@config.change_filter('colors.webpage.bg')
|
||||
def _set_bg_color(self):
|
||||
@@ -147,97 +175,15 @@ class WebEnginePage(QWebEnginePage):
|
||||
col = self._theme_color
|
||||
self.setBackgroundColor(col)
|
||||
|
||||
@pyqtSlot(QUrl, 'QWebEnginePage::Feature')
|
||||
def _on_feature_permission_requested(self, url, feature):
|
||||
"""Ask the user for approval for geolocation/media/etc.."""
|
||||
options = {
|
||||
QWebEnginePage.Geolocation: 'content.geolocation',
|
||||
QWebEnginePage.MediaAudioCapture: 'content.media_capture',
|
||||
QWebEnginePage.MediaVideoCapture: 'content.media_capture',
|
||||
QWebEnginePage.MediaAudioVideoCapture: 'content.media_capture',
|
||||
}
|
||||
messages = {
|
||||
QWebEnginePage.Geolocation: 'access your location',
|
||||
QWebEnginePage.MediaAudioCapture: 'record audio',
|
||||
QWebEnginePage.MediaVideoCapture: 'record video',
|
||||
QWebEnginePage.MediaAudioVideoCapture: 'record audio/video',
|
||||
}
|
||||
assert options.keys() == messages.keys()
|
||||
|
||||
if feature not in options:
|
||||
log.webview.error("Unhandled feature permission {}".format(
|
||||
debug.qenum_key(QWebEnginePage, feature)))
|
||||
self.setFeaturePermission(url, feature,
|
||||
QWebEnginePage.PermissionDeniedByUser)
|
||||
return
|
||||
|
||||
yes_action = functools.partial(
|
||||
self.setFeaturePermission, url, feature,
|
||||
QWebEnginePage.PermissionGrantedByUser)
|
||||
no_action = functools.partial(
|
||||
self.setFeaturePermission, url, feature,
|
||||
QWebEnginePage.PermissionDeniedByUser)
|
||||
|
||||
question = shared.feature_permission(
|
||||
url=url, option=options[feature], msg=messages[feature],
|
||||
yes_action=yes_action, no_action=no_action,
|
||||
abort_on=[self.shutting_down, self.loadStarted])
|
||||
|
||||
if question is not None:
|
||||
self.featurePermissionRequestCanceled.connect(
|
||||
functools.partial(self._on_feature_permission_cancelled,
|
||||
question, url, feature))
|
||||
|
||||
def _on_feature_permission_cancelled(self, question, url, feature,
|
||||
cancelled_url, cancelled_feature):
|
||||
"""Slot invoked when a feature permission request was cancelled.
|
||||
|
||||
To be used with functools.partial.
|
||||
"""
|
||||
if url == cancelled_url and feature == cancelled_feature:
|
||||
try:
|
||||
question.abort()
|
||||
except RuntimeError:
|
||||
# The question could already be deleted, e.g. because it was
|
||||
# aborted after a loadStarted signal.
|
||||
pass
|
||||
|
||||
def shutdown(self):
|
||||
self._is_shutting_down = True
|
||||
self.shutting_down.emit()
|
||||
|
||||
def certificateError(self, error):
|
||||
"""Handle certificate errors coming from Qt."""
|
||||
self.certificate_error.emit()
|
||||
url = error.url()
|
||||
error = certificateerror.CertificateErrorWrapper(error)
|
||||
log.webview.debug("Certificate error: {}".format(error))
|
||||
|
||||
url_string = url.toDisplayString()
|
||||
error_page = jinja.render(
|
||||
'error.html', title="Error loading page: {}".format(url_string),
|
||||
url=url_string, error=str(error))
|
||||
|
||||
if error.is_overridable():
|
||||
ignore = shared.ignore_certificate_errors(
|
||||
url, [error], abort_on=[self.loadStarted, self.shutting_down])
|
||||
else:
|
||||
log.webview.error("Non-overridable certificate error: "
|
||||
"{}".format(error))
|
||||
ignore = False
|
||||
|
||||
# We can't really know when to show an error page, as the error might
|
||||
# have happened when loading some resource.
|
||||
# However, self.url() is not available yet and self.requestedUrl()
|
||||
# might not match the URL we get from the error - so we just apply a
|
||||
# heuristic here.
|
||||
# See https://bugreports.qt.io/browse/QTBUG-56207
|
||||
log.webview.debug("ignore {}, URL {}, requested {}".format(
|
||||
ignore, url, self.requestedUrl()))
|
||||
if not ignore and url.matches(self.requestedUrl(), QUrl.RemoveScheme):
|
||||
self.setHtml(error_page)
|
||||
|
||||
return ignore
|
||||
self.certificate_error.emit(error)
|
||||
return error.ignore
|
||||
|
||||
def javaScriptConfirm(self, url, js_msg):
|
||||
"""Override javaScriptConfirm to use qutebrowser prompts."""
|
||||
@@ -315,43 +261,3 @@ class WebEnginePage(QWebEnginePage):
|
||||
is_main_frame=is_main_frame)
|
||||
self.navigation_request.emit(navigation)
|
||||
return navigation.accepted
|
||||
|
||||
@pyqtSlot('QUrl')
|
||||
def _inject_userjs(self, url):
|
||||
"""Inject userscripts registered for `url` into the current page."""
|
||||
if qtutils.version_check('5.8'):
|
||||
# Handled in webenginetab with the builtin Greasemonkey
|
||||
# support.
|
||||
return
|
||||
|
||||
# Using QWebEnginePage.scripts() to hold the user scripts means
|
||||
# we don't have to worry ourselves about where to inject the
|
||||
# page but also means scripts hang around for the tab lifecycle.
|
||||
# So clear them here.
|
||||
scripts = self.scripts()
|
||||
for script in scripts.toList():
|
||||
if script.name().startswith("GM-"):
|
||||
log.greasemonkey.debug("Removing script: {}"
|
||||
.format(script.name()))
|
||||
removed = scripts.remove(script)
|
||||
assert removed, script.name()
|
||||
|
||||
def _add_script(script, injection_point):
|
||||
new_script = QWebEngineScript()
|
||||
new_script.setInjectionPoint(injection_point)
|
||||
new_script.setWorldId(QWebEngineScript.MainWorld)
|
||||
new_script.setSourceCode(script.code())
|
||||
new_script.setName("GM-{}".format(script.name))
|
||||
new_script.setRunsOnSubFrames(script.runs_on_sub_frames)
|
||||
log.greasemonkey.debug("Adding script: {}"
|
||||
.format(new_script.name()))
|
||||
scripts.insert(new_script)
|
||||
|
||||
greasemonkey = objreg.get('greasemonkey')
|
||||
matching_scripts = greasemonkey.scripts_for(url)
|
||||
for script in matching_scripts.start:
|
||||
_add_script(script, QWebEngineScript.DocumentCreation)
|
||||
for script in matching_scripts.end:
|
||||
_add_script(script, QWebEngineScript.DocumentReady)
|
||||
for script in matching_scripts.idle:
|
||||
_add_script(script, QWebEngineScript.Deferred)
|
||||
|
||||
@@ -28,7 +28,8 @@ from PyQt5.QtCore import (pyqtSlot, pyqtSignal, QCoreApplication, QUrl,
|
||||
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkReply, QSslSocket
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import message, log, usertypes, utils, objreg, urlutils
|
||||
from qutebrowser.utils import (message, log, usertypes, utils, objreg,
|
||||
urlutils, debug)
|
||||
from qutebrowser.browser import shared
|
||||
from qutebrowser.browser.webkit import certificateerror
|
||||
from qutebrowser.browser.webkit.network import (webkitqutescheme, networkreply,
|
||||
@@ -147,6 +148,7 @@ class NetworkManager(QNetworkAccessManager):
|
||||
super().__init__(parent)
|
||||
log.init.debug("NetworkManager init done")
|
||||
self.adopted_downloads = 0
|
||||
self._args = objreg.get('args')
|
||||
self._win_id = win_id
|
||||
self._tab_id = tab_id
|
||||
self._private = private
|
||||
@@ -378,7 +380,7 @@ class NetworkManager(QNetworkAccessManager):
|
||||
result.setParent(self)
|
||||
return result
|
||||
|
||||
for header, value in shared.custom_headers():
|
||||
for header, value in shared.custom_headers(url=req.url()):
|
||||
req.setRawHeader(header, value)
|
||||
|
||||
host_blocker = objreg.get('host-blocker')
|
||||
@@ -406,5 +408,13 @@ class NetworkManager(QNetworkAccessManager):
|
||||
# the webpage shutdown here.
|
||||
current_url = QUrl()
|
||||
|
||||
if 'log-requests' in self._args.debug_flags:
|
||||
operation = debug.qenum_key(QNetworkAccessManager, op)
|
||||
operation = operation.replace('Operation', '').upper()
|
||||
log.webview.debug("{} {}, first-party {}".format(
|
||||
operation,
|
||||
req.url().toDisplayString(),
|
||||
current_url.toDisplayString()))
|
||||
|
||||
self.set_referer(req, current_url)
|
||||
return super().createRequest(op, req, outgoing_data)
|
||||
|
||||
@@ -305,6 +305,9 @@ class WebKitElement(webelem.AbstractWebElement):
|
||||
if self.is_text_input() and self.is_editable():
|
||||
self._tab.caret.move_to_end_of_document()
|
||||
|
||||
def _requires_user_interaction(self):
|
||||
return False
|
||||
|
||||
def _click_editable(self, click_target):
|
||||
ok = self._elem.evaluateJavaScript('this.focus(); true;')
|
||||
if ok:
|
||||
|
||||
@@ -19,11 +19,10 @@
|
||||
|
||||
"""Customized QWebInspector for QtWebKit."""
|
||||
|
||||
|
||||
from PyQt5.QtWebKit import QWebSettings
|
||||
from PyQt5.QtWebKitWidgets import QWebInspector
|
||||
|
||||
from qutebrowser.browser import inspector
|
||||
from qutebrowser.config import config
|
||||
|
||||
|
||||
class WebKitInspector(inspector.AbstractWebInspector):
|
||||
@@ -36,9 +35,6 @@ class WebKitInspector(inspector.AbstractWebInspector):
|
||||
self._set_widget(qwebinspector)
|
||||
|
||||
def inspect(self, page):
|
||||
if not config.val.content.developer_extras:
|
||||
raise inspector.WebInspectorError(
|
||||
"Please enable content.developer_extras before using the "
|
||||
"webinspector!")
|
||||
settings = QWebSettings.globalSettings()
|
||||
settings.setAttribute(QWebSettings.DeveloperExtrasEnabled, True)
|
||||
self._widget.setPage(page)
|
||||
self.show()
|
||||
|
||||
@@ -30,6 +30,7 @@ from PyQt5.QtGui import QFont
|
||||
from PyQt5.QtWebKit import QWebSettings
|
||||
|
||||
from qutebrowser.config import config, websettings
|
||||
from qutebrowser.config.websettings import AttributeInfo as Attr
|
||||
from qutebrowser.utils import standarddir, urlutils
|
||||
from qutebrowser.browser import shared
|
||||
|
||||
@@ -44,50 +45,48 @@ class WebKitSettings(websettings.AbstractSettings):
|
||||
|
||||
_ATTRIBUTES = {
|
||||
'content.images':
|
||||
[QWebSettings.AutoLoadImages],
|
||||
Attr(QWebSettings.AutoLoadImages),
|
||||
'content.javascript.enabled':
|
||||
[QWebSettings.JavascriptEnabled],
|
||||
Attr(QWebSettings.JavascriptEnabled),
|
||||
'content.javascript.can_open_tabs_automatically':
|
||||
[QWebSettings.JavascriptCanOpenWindows],
|
||||
Attr(QWebSettings.JavascriptCanOpenWindows),
|
||||
'content.javascript.can_close_tabs':
|
||||
[QWebSettings.JavascriptCanCloseWindows],
|
||||
Attr(QWebSettings.JavascriptCanCloseWindows),
|
||||
'content.javascript.can_access_clipboard':
|
||||
[QWebSettings.JavascriptCanAccessClipboard],
|
||||
Attr(QWebSettings.JavascriptCanAccessClipboard),
|
||||
'content.plugins':
|
||||
[QWebSettings.PluginsEnabled],
|
||||
Attr(QWebSettings.PluginsEnabled),
|
||||
'content.webgl':
|
||||
[QWebSettings.WebGLEnabled],
|
||||
Attr(QWebSettings.WebGLEnabled),
|
||||
'content.hyperlink_auditing':
|
||||
[QWebSettings.HyperlinkAuditingEnabled],
|
||||
Attr(QWebSettings.HyperlinkAuditingEnabled),
|
||||
'content.local_content_can_access_remote_urls':
|
||||
[QWebSettings.LocalContentCanAccessRemoteUrls],
|
||||
Attr(QWebSettings.LocalContentCanAccessRemoteUrls),
|
||||
'content.local_content_can_access_file_urls':
|
||||
[QWebSettings.LocalContentCanAccessFileUrls],
|
||||
Attr(QWebSettings.LocalContentCanAccessFileUrls),
|
||||
'content.dns_prefetch':
|
||||
[QWebSettings.DnsPrefetchEnabled],
|
||||
Attr(QWebSettings.DnsPrefetchEnabled),
|
||||
'content.frame_flattening':
|
||||
[QWebSettings.FrameFlatteningEnabled],
|
||||
Attr(QWebSettings.FrameFlatteningEnabled),
|
||||
'content.cache.appcache':
|
||||
[QWebSettings.OfflineWebApplicationCacheEnabled],
|
||||
Attr(QWebSettings.OfflineWebApplicationCacheEnabled),
|
||||
'content.local_storage':
|
||||
[QWebSettings.LocalStorageEnabled,
|
||||
QWebSettings.OfflineStorageDatabaseEnabled],
|
||||
'content.developer_extras':
|
||||
[QWebSettings.DeveloperExtrasEnabled],
|
||||
Attr(QWebSettings.LocalStorageEnabled,
|
||||
QWebSettings.OfflineStorageDatabaseEnabled),
|
||||
'content.print_element_backgrounds':
|
||||
[QWebSettings.PrintElementBackgrounds],
|
||||
Attr(QWebSettings.PrintElementBackgrounds),
|
||||
'content.xss_auditing':
|
||||
[QWebSettings.XSSAuditingEnabled],
|
||||
Attr(QWebSettings.XSSAuditingEnabled),
|
||||
|
||||
'input.spatial_navigation':
|
||||
[QWebSettings.SpatialNavigationEnabled],
|
||||
Attr(QWebSettings.SpatialNavigationEnabled),
|
||||
'input.links_included_in_focus_chain':
|
||||
[QWebSettings.LinksIncludedInFocusChain],
|
||||
Attr(QWebSettings.LinksIncludedInFocusChain),
|
||||
|
||||
'zoom.text_only':
|
||||
[QWebSettings.ZoomTextOnly],
|
||||
Attr(QWebSettings.ZoomTextOnly),
|
||||
'scrolling.smooth':
|
||||
[QWebSettings.ScrollAnimatorEnabled],
|
||||
Attr(QWebSettings.ScrollAnimatorEnabled),
|
||||
}
|
||||
|
||||
_FONT_SIZES = {
|
||||
|
||||
@@ -23,11 +23,6 @@ import re
|
||||
import functools
|
||||
import xml.etree.ElementTree
|
||||
|
||||
import pygments
|
||||
import pygments.lexers
|
||||
import pygments.formatters
|
||||
|
||||
import sip
|
||||
from PyQt5.QtCore import (pyqtSlot, Qt, QEvent, QUrl, QPoint, QTimer, QSizeF,
|
||||
QSize)
|
||||
from PyQt5.QtGui import QKeyEvent, QIcon
|
||||
@@ -38,7 +33,8 @@ from PyQt5.QtPrintSupport import QPrinter
|
||||
from qutebrowser.browser import browsertab, shared
|
||||
from qutebrowser.browser.webkit import (webview, tabhistory, webkitelem,
|
||||
webkitsettings)
|
||||
from qutebrowser.utils import qtutils, objreg, usertypes, utils, log, debug
|
||||
from qutebrowser.utils import qtutils, usertypes, utils, log, debug
|
||||
from qutebrowser.qt import sip
|
||||
|
||||
|
||||
class WebKitAction(browsertab.AbstractAction):
|
||||
@@ -55,28 +51,8 @@ class WebKitAction(browsertab.AbstractAction):
|
||||
"""Save the current page."""
|
||||
raise browsertab.UnsupportedOperationError
|
||||
|
||||
def show_source(self):
|
||||
|
||||
def show_source_cb(source):
|
||||
"""Show source as soon as it's ready."""
|
||||
# WORKAROUND for https://github.com/PyCQA/pylint/issues/491
|
||||
# pylint: disable=no-member
|
||||
lexer = pygments.lexers.HtmlLexer()
|
||||
formatter = pygments.formatters.HtmlFormatter(
|
||||
full=True, linenos='table')
|
||||
# pylint: enable=no-member
|
||||
highlighted = pygments.highlight(source, lexer, formatter)
|
||||
|
||||
tb = objreg.get('tabbed-browser', scope='window',
|
||||
window=self._tab.win_id)
|
||||
new_tab = tb.tabopen(background=False, related=True)
|
||||
# The original URL becomes the path of a view-source: URL
|
||||
# (without a host), but query/fragment should stay.
|
||||
url = QUrl('view-source:' + urlstr)
|
||||
new_tab.set_html(highlighted, url)
|
||||
|
||||
urlstr = self._tab.url().toString(QUrl.RemoveUserInfo)
|
||||
self._tab.dump_async(show_source_cb)
|
||||
def show_source(self, pygments=False):
|
||||
self._show_source_pygments()
|
||||
|
||||
|
||||
class WebKitPrinting(browsertab.AbstractPrinting):
|
||||
@@ -196,9 +172,10 @@ class WebKitCaret(browsertab.AbstractCaret):
|
||||
if mode != usertypes.KeyMode.caret:
|
||||
return
|
||||
|
||||
self.selection_enabled = self._widget.hasSelection()
|
||||
self.selection_toggled.emit(self.selection_enabled)
|
||||
settings = self._widget.settings()
|
||||
settings.setAttribute(QWebSettings.CaretBrowsingEnabled, True)
|
||||
self.selection_enabled = self._widget.hasSelection()
|
||||
|
||||
if self._widget.isVisible():
|
||||
# Sometimes the caret isn't immediately visible, but unfocusing
|
||||
@@ -363,9 +340,7 @@ class WebKitCaret(browsertab.AbstractCaret):
|
||||
|
||||
def toggle_selection(self):
|
||||
self.selection_enabled = not self.selection_enabled
|
||||
mainwindow = objreg.get('main-window', scope='window',
|
||||
window=self._tab.win_id)
|
||||
mainwindow.status.set_mode_active(usertypes.KeyMode.caret, True)
|
||||
self.selection_toggled.emit(self.selection_enabled)
|
||||
|
||||
def drop_selection(self):
|
||||
self._widget.triggerPageAction(QWebPage.MoveToNextChar)
|
||||
@@ -378,11 +353,22 @@ class WebKitCaret(browsertab.AbstractCaret):
|
||||
QWebSettings.JavascriptEnabled):
|
||||
if tab:
|
||||
self._tab.data.override_target = usertypes.ClickTarget.tab
|
||||
self._tab.run_js_async(
|
||||
'window.getSelection().anchorNode.parentNode.click()')
|
||||
self._tab.run_js_async("""
|
||||
const aElm = document.activeElement;
|
||||
if (window.getSelection().anchorNode) {
|
||||
window.getSelection().anchorNode.parentNode.click();
|
||||
} else if (aElm && aElm !== document.body) {
|
||||
aElm.click();
|
||||
}
|
||||
""")
|
||||
else:
|
||||
selection = self._widget.selectedHtml()
|
||||
if not selection:
|
||||
# Getting here may mean we crashed, but we can't do anything
|
||||
# about that until this commit is released:
|
||||
# https://github.com/annulen/webkit/commit/0e75f3272d149bc64899c161f150eb341a2417af
|
||||
# TODO find a way to check if something is focused
|
||||
self._follow_enter(tab)
|
||||
return
|
||||
try:
|
||||
selected_element = xml.etree.ElementTree.fromstring(
|
||||
@@ -427,6 +413,9 @@ class WebKitScroller(browsertab.AbstractScroller):
|
||||
def to_point(self, point):
|
||||
self._widget.page().mainFrame().setScrollPosition(point)
|
||||
|
||||
def to_anchor(self, name):
|
||||
self._widget.page().mainFrame().scrollToAnchor(name)
|
||||
|
||||
def delta(self, x=0, y=0):
|
||||
qtutils.check_overflow(x, 'int')
|
||||
qtutils.check_overflow(y, 'int')
|
||||
@@ -537,6 +526,9 @@ class WebKitHistory(browsertab.AbstractHistory):
|
||||
return qtutils.deserialize(data, self._history)
|
||||
|
||||
def load_items(self, items):
|
||||
if items:
|
||||
self._tab.predicted_navigation.emit(items[-1].url)
|
||||
|
||||
stream, _data, user_data = tabhistory.serialize(items)
|
||||
qtutils.deserialize_stream(stream, self._history)
|
||||
for i, data in enumerate(user_data):
|
||||
@@ -635,6 +627,20 @@ class WebKitElements(browsertab.AbstractElements):
|
||||
callback(elem)
|
||||
|
||||
|
||||
class WebKitAudio(browsertab.AbstractAudio):
|
||||
|
||||
"""Dummy handling of audio status for QtWebKit."""
|
||||
|
||||
def set_muted(self, muted: bool):
|
||||
raise browsertab.WebTabError('Muting is not supported on QtWebKit!')
|
||||
|
||||
def is_muted(self):
|
||||
return False
|
||||
|
||||
def is_recently_audible(self):
|
||||
return False
|
||||
|
||||
|
||||
class WebKitTab(browsertab.AbstractTab):
|
||||
|
||||
"""A QtWebKit tab in the browser."""
|
||||
@@ -655,6 +661,7 @@ class WebKitTab(browsertab.AbstractTab):
|
||||
self.printing = WebKitPrinting()
|
||||
self.elements = WebKitElements(tab=self)
|
||||
self.action = WebKitAction(tab=self)
|
||||
self.audio = WebKitAudio(parent=self)
|
||||
# We're assigning settings in _set_widget
|
||||
self.settings = webkitsettings.WebKitSettings(settings=None)
|
||||
self._set_widget(widget)
|
||||
@@ -668,8 +675,8 @@ class WebKitTab(browsertab.AbstractTab):
|
||||
settings = widget.settings()
|
||||
settings.setAttribute(QWebSettings.PrivateBrowsingEnabled, True)
|
||||
|
||||
def openurl(self, url):
|
||||
self._openurl_prepare(url)
|
||||
def openurl(self, url, *, predict=True):
|
||||
self._openurl_prepare(url, predict=predict)
|
||||
self._widget.openurl(url)
|
||||
|
||||
def url(self, requested=False):
|
||||
@@ -701,7 +708,6 @@ class WebKitTab(browsertab.AbstractTab):
|
||||
self._widget.shutdown()
|
||||
|
||||
def reload(self, *, force=False):
|
||||
self.predicted_navigation.emit(self.url())
|
||||
if force:
|
||||
action = QWebPage.ReloadAndBypassCache
|
||||
else:
|
||||
@@ -802,6 +808,10 @@ class WebKitTab(browsertab.AbstractTab):
|
||||
if navigation.is_main_frame:
|
||||
self.settings.update_for_url(navigation.url)
|
||||
|
||||
@pyqtSlot()
|
||||
def _on_ssl_errors(self):
|
||||
self._has_ssl_errors = True
|
||||
|
||||
def _connect_signals(self):
|
||||
view = self._widget
|
||||
page = view.page()
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
import html
|
||||
import functools
|
||||
|
||||
import sip
|
||||
from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QUrl, QPoint
|
||||
from PyQt5.QtGui import QDesktopServices
|
||||
from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest
|
||||
@@ -35,6 +34,7 @@ from qutebrowser.browser import pdfjs, shared
|
||||
from qutebrowser.browser.webkit import http
|
||||
from qutebrowser.browser.webkit.network import networkmanager
|
||||
from qutebrowser.utils import message, usertypes, log, jinja, objreg
|
||||
from qutebrowser.qt import sip
|
||||
|
||||
|
||||
class BrowserPage(QWebPage):
|
||||
@@ -212,7 +212,8 @@ class BrowserPage(QWebPage):
|
||||
page = pdfjs.generate_pdfjs_page(reply.url())
|
||||
except pdfjs.PDFJSNotFound:
|
||||
page = jinja.render('no_pdfjs.html',
|
||||
url=reply.url().toDisplayString())
|
||||
url=reply.url().toDisplayString(),
|
||||
title="PDF.js not found")
|
||||
self.mainFrame().setContent(page.encode('utf-8'), 'text/html',
|
||||
reply.url())
|
||||
reply.deleteLater()
|
||||
@@ -239,7 +240,6 @@ class BrowserPage(QWebPage):
|
||||
printdiag.setAttribute(Qt.WA_DeleteOnClose)
|
||||
printdiag.open(lambda: frame.print(printdiag.printer()))
|
||||
|
||||
@pyqtSlot('QNetworkRequest')
|
||||
def on_download_requested(self, request):
|
||||
"""Called when the user wants to download a link.
|
||||
|
||||
@@ -416,7 +416,7 @@ class BrowserPage(QWebPage):
|
||||
|
||||
def userAgentForUrl(self, url):
|
||||
"""Override QWebPage::userAgentForUrl to customize the user agent."""
|
||||
ua = config.val.content.headers.user_agent
|
||||
ua = config.instance.get('content.headers.user_agent', url=url)
|
||||
if ua is None:
|
||||
return super().userAgentForUrl(url)
|
||||
else:
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
"""The main browser widgets."""
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QUrl
|
||||
from PyQt5.QtCore import pyqtSignal, Qt, QUrl
|
||||
from PyQt5.QtGui import QPalette
|
||||
from PyQt5.QtWidgets import QStyleFactory
|
||||
from PyQt5.QtWebKit import QWebSettings
|
||||
@@ -78,10 +78,6 @@ class WebView(QWebView):
|
||||
|
||||
self.setPage(page)
|
||||
|
||||
mode_manager = objreg.get('mode-manager', scope='window',
|
||||
window=win_id)
|
||||
mode_manager.entered.connect(self.on_mode_entered)
|
||||
mode_manager.left.connect(self.on_mode_left)
|
||||
config.instance.changed.connect(self._set_bg_color)
|
||||
|
||||
def __repr__(self):
|
||||
@@ -130,32 +126,6 @@ class WebView(QWebView):
|
||||
"""
|
||||
self.load(url)
|
||||
|
||||
@pyqtSlot(usertypes.KeyMode)
|
||||
def on_mode_entered(self, mode):
|
||||
"""Ignore attempts to focus the widget if in any status-input mode.
|
||||
|
||||
FIXME:qtwebengine
|
||||
For QtWebEngine, doing the same has no effect, so we do it in here.
|
||||
"""
|
||||
if mode in [usertypes.KeyMode.command, usertypes.KeyMode.prompt,
|
||||
usertypes.KeyMode.yesno]:
|
||||
log.webview.debug("Ignoring focus because mode {} was "
|
||||
"entered.".format(mode))
|
||||
self.setFocusPolicy(Qt.NoFocus)
|
||||
|
||||
@pyqtSlot(usertypes.KeyMode)
|
||||
def on_mode_left(self, mode):
|
||||
"""Restore focus policy if status-input modes were left.
|
||||
|
||||
FIXME:qtwebengine
|
||||
For QtWebEngine, doing the same has no effect, so we do it in here.
|
||||
"""
|
||||
if mode in [usertypes.KeyMode.command, usertypes.KeyMode.prompt,
|
||||
usertypes.KeyMode.yesno]:
|
||||
log.webview.debug("Restoring focus policy because mode {} was "
|
||||
"left.".format(mode))
|
||||
self.setFocusPolicy(Qt.WheelFocus)
|
||||
|
||||
def createWindow(self, wintype):
|
||||
"""Called by Qt when a page wants to create a new window.
|
||||
|
||||
|
||||
@@ -123,6 +123,7 @@ class Command:
|
||||
self.pos_args = []
|
||||
self.desc = None
|
||||
self.flags_with_args = []
|
||||
self._has_vararg = False
|
||||
|
||||
# This is checked by future @cmdutils.argument calls so they fail
|
||||
# (as they'd be silently ignored otherwise)
|
||||
@@ -170,6 +171,8 @@ class Command:
|
||||
|
||||
def get_pos_arg_info(self, pos):
|
||||
"""Get an ArgInfo tuple for the given positional parameter."""
|
||||
if pos >= len(self.pos_args) and self._has_vararg:
|
||||
pos = len(self.pos_args) - 1
|
||||
name = self.pos_args[pos][0]
|
||||
return self._qute_args.get(name, ArgInfo())
|
||||
|
||||
@@ -233,6 +236,8 @@ class Command:
|
||||
log.commands.vdebug('Adding arg {} of type {} -> {}'.format(
|
||||
param.name, typ, callsig))
|
||||
self.parser.add_argument(*args, **kwargs)
|
||||
if param.kind == inspect.Parameter.VAR_POSITIONAL:
|
||||
self._has_vararg = True
|
||||
return signature.parameters.values()
|
||||
|
||||
def _param_to_argparse_kwargs(self, param, is_bool):
|
||||
|
||||
@@ -49,7 +49,7 @@ 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.
|
||||
_last_completion_func: The completion function used for the last text.
|
||||
_last_before_cursor: The prior value of before_cursor.
|
||||
"""
|
||||
|
||||
def __init__(self, *, cmd, win_id, parent=None):
|
||||
@@ -62,7 +62,7 @@ class Completer(QObject):
|
||||
self._timer.timeout.connect(self._update_completion)
|
||||
self._last_cursor_pos = -1
|
||||
self._last_text = None
|
||||
self._last_completion_func = None
|
||||
self._last_before_cursor = None
|
||||
self._cmd.update_completion.connect(self.schedule_completion_update)
|
||||
|
||||
def __repr__(self):
|
||||
@@ -228,7 +228,7 @@ class Completer(QObject):
|
||||
# FIXME complete searches
|
||||
# https://github.com/qutebrowser/qutebrowser/issues/32
|
||||
completion.set_model(None)
|
||||
self._last_completion_func = None
|
||||
self._last_before_cursor = None
|
||||
return
|
||||
|
||||
before_cursor, pattern, after_cursor = self._partition()
|
||||
@@ -242,11 +242,11 @@ class Completer(QObject):
|
||||
if func is None:
|
||||
log.completion.debug('Clearing completion')
|
||||
completion.set_model(None)
|
||||
self._last_completion_func = None
|
||||
self._last_before_cursor = None
|
||||
return
|
||||
|
||||
if func != self._last_completion_func:
|
||||
self._last_completion_func = func
|
||||
if before_cursor != self._last_before_cursor:
|
||||
self._last_before_cursor = before_cursor
|
||||
args = (x for x in before_cursor[1:] if not x.startswith('-'))
|
||||
with debug.log_time(log.completion, 'Starting {} completion'
|
||||
.format(func.__name__)):
|
||||
|
||||
@@ -47,12 +47,12 @@ def customized_option(*, info):
|
||||
return model
|
||||
|
||||
|
||||
def value(optname, *_values, info):
|
||||
def value(optname, *values, info):
|
||||
"""A CompletionModel filled with setting values.
|
||||
|
||||
Args:
|
||||
optname: The name of the config option this model shows.
|
||||
_values: The values already provided on the command line.
|
||||
values: The values already provided on the command line.
|
||||
info: A CompletionInfo instance.
|
||||
"""
|
||||
model = completionmodel.CompletionModel(column_widths=(30, 70, 0))
|
||||
@@ -64,13 +64,18 @@ def value(optname, *_values, info):
|
||||
|
||||
opt = info.config.get_opt(optname)
|
||||
default = opt.typ.to_str(opt.default)
|
||||
cur_cat = listcategory.ListCategory(
|
||||
"Current/Default",
|
||||
[(current, "Current value"), (default, "Default value")])
|
||||
model.add_category(cur_cat)
|
||||
cur_def = []
|
||||
if current not in values:
|
||||
cur_def.append((current, "Current value"))
|
||||
if default not in values:
|
||||
cur_def.append((default, "Default value"))
|
||||
if cur_def:
|
||||
cur_cat = listcategory.ListCategory("Current/Default", cur_def)
|
||||
model.add_category(cur_cat)
|
||||
|
||||
vals = opt.typ.complete()
|
||||
if vals is not None:
|
||||
vals = opt.typ.complete() or []
|
||||
vals = [x for x in vals if x[0] not in values]
|
||||
if vals:
|
||||
model.add_category(listcategory.ListCategory("Completions", vals))
|
||||
return model
|
||||
|
||||
|
||||
@@ -110,18 +110,18 @@ def _buffer(skip_win_id=None):
|
||||
model = completionmodel.CompletionModel(column_widths=(6, 40, 54))
|
||||
|
||||
for win_id in objreg.window_registry:
|
||||
if skip_win_id and win_id == skip_win_id:
|
||||
if skip_win_id is not None and win_id == skip_win_id:
|
||||
continue
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=win_id)
|
||||
if tabbed_browser.shutting_down:
|
||||
continue
|
||||
tabs = []
|
||||
for idx in range(tabbed_browser.count()):
|
||||
tab = tabbed_browser.widget(idx)
|
||||
for idx in range(tabbed_browser.widget.count()):
|
||||
tab = tabbed_browser.widget.widget(idx)
|
||||
tabs.append(("{}/{}".format(win_id, idx + 1),
|
||||
tab.url().toDisplayString(),
|
||||
tabbed_browser.page_title(idx)))
|
||||
tabbed_browser.widget.page_title(idx)))
|
||||
cat = listcategory.ListCategory("{}".format(win_id), tabs,
|
||||
delete_func=delete_buffer)
|
||||
model.add_category(cat)
|
||||
|
||||
@@ -276,7 +276,8 @@ class Config(QObject):
|
||||
"""Set the given option to the given value."""
|
||||
if not isinstance(objects.backend, objects.NoBackend):
|
||||
if objects.backend not in opt.backends:
|
||||
raise configexc.BackendError(opt.name, objects.backend)
|
||||
raise configexc.BackendError(opt.name, objects.backend,
|
||||
opt.raw_backends)
|
||||
|
||||
opt.typ.to_py(value) # for validation
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ def _parse_yaml_type(name, node):
|
||||
|
||||
try:
|
||||
typ = getattr(configtypes, type_name)
|
||||
except AttributeError as e:
|
||||
except AttributeError:
|
||||
raise AttributeError("Did not find type {} for {}".format(
|
||||
type_name, name))
|
||||
|
||||
@@ -149,6 +149,9 @@ def _parse_yaml_backends_dict(name, node):
|
||||
False: False,
|
||||
'Qt 5.8': qtutils.version_check('5.8'),
|
||||
'Qt 5.9': qtutils.version_check('5.9'),
|
||||
'Qt 5.9.2': qtutils.version_check('5.9.2'),
|
||||
'Qt 5.10': qtutils.version_check('5.10'),
|
||||
'Qt 5.11': qtutils.version_check('5.11'),
|
||||
}
|
||||
for key in sorted(node.keys()):
|
||||
if conditionals[node[key]]:
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
aliases:
|
||||
default:
|
||||
w: session-save
|
||||
q: quit
|
||||
wq: quit --save
|
||||
q: close
|
||||
qa: quit
|
||||
wqa: quit --save
|
||||
type:
|
||||
name: Dict
|
||||
keytype:
|
||||
@@ -150,14 +151,24 @@ force_software_rendering:
|
||||
renamed: qt.force_software_rendering
|
||||
|
||||
qt.force_software_rendering:
|
||||
type: Bool
|
||||
default: false
|
||||
type:
|
||||
name: String
|
||||
valid_values:
|
||||
- software-opengl: Tell LibGL to use a software implementation of GL
|
||||
(`LIBGL_ALWAYS_SOFTWARE` / `QT_XCB_FORCE_SOFTWARE_OPENGL`)
|
||||
- qt-quick: Tell Qt Quick to use a software renderer instead of OpenGL.
|
||||
(`QT_QUICK_BACKEND=software`)
|
||||
- chromium: Tell Chromium to disable GPU support and use Skia software
|
||||
rendering instead. (`--disable-gpu`)
|
||||
- none: Don't force software rendering.
|
||||
default: none
|
||||
backend: QtWebEngine
|
||||
restart: true
|
||||
desc: >-
|
||||
Force software rendering for QtWebEngine.
|
||||
|
||||
This is needed for QtWebEngine to work with Nouveau drivers.
|
||||
This is needed for QtWebEngine to work with Nouveau drivers and can be
|
||||
useful in other scenarios related to graphic issues.
|
||||
|
||||
qt.force_platform:
|
||||
type:
|
||||
@@ -204,6 +215,17 @@ auto_save.session:
|
||||
|
||||
## content
|
||||
|
||||
content.autoplay:
|
||||
default: true
|
||||
type: Bool
|
||||
backend:
|
||||
QtWebEngine: Qt 5.10
|
||||
QtWebKit: false
|
||||
desc: >-
|
||||
Automatically start playing `<video>` elements.
|
||||
|
||||
Note this option needs a restart with QtWebEngine on Qt < 5.11.
|
||||
|
||||
content.cache.size:
|
||||
default: null
|
||||
type:
|
||||
@@ -216,6 +238,16 @@ content.cache.size:
|
||||
|
||||
With QtWebEngine, the maximum supported value is 2147483647 (~2 GB).
|
||||
|
||||
content.canvas_reading:
|
||||
default: true
|
||||
type: Bool
|
||||
backend: QtWebEngine
|
||||
restart: true
|
||||
desc: >-
|
||||
Allow websites to read canvas elements.
|
||||
|
||||
Note this is needed for some websites to work properly.
|
||||
|
||||
# Defaults from QWebSettings::QWebSettings() in
|
||||
# qtwebkit/Source/WebKit/qt/Api/qwebsettings.cpp
|
||||
|
||||
@@ -250,14 +282,17 @@ content.cache.appcache:
|
||||
|
||||
content.cookies.accept:
|
||||
default: no-3rdparty
|
||||
backend: QtWebKit
|
||||
backend:
|
||||
QtWebKit: true
|
||||
QtWebEngine: Qt 5.11
|
||||
type:
|
||||
name: String
|
||||
valid_values:
|
||||
- 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."
|
||||
a cookie is already set for the domain. On QtWebEngine, this is the
|
||||
same as no-3rdparty."
|
||||
- never: "Don't accept cookies at all."
|
||||
desc: Which cookies to accept.
|
||||
|
||||
@@ -284,16 +319,18 @@ content.windowed_fullscreen:
|
||||
desc: >-
|
||||
Limit fullscreen to the browser window (does not expand to fill the screen).
|
||||
|
||||
content.developer_extras:
|
||||
type: Bool
|
||||
default: false
|
||||
backend: QtWebKit
|
||||
content.desktop_capture:
|
||||
type: BoolAsk
|
||||
default: ask
|
||||
supports_pattern: true
|
||||
desc: >-
|
||||
Enable extra tools for Web developers.
|
||||
Allow websites to share screen content.
|
||||
|
||||
This needs to be enabled for `:inspector` to work and also adds an
|
||||
_Inspect_ entry to the context menu. For QtWebEngine, see
|
||||
`--enable-webengine-inspector` in `qutebrowser --help` instead.
|
||||
On Qt < 5.10, a dialog box is always displayed, even if this is set to
|
||||
"true".
|
||||
|
||||
content.developer_extras:
|
||||
deleted: true
|
||||
|
||||
content.dns_prefetch:
|
||||
default: true
|
||||
@@ -315,14 +352,19 @@ content.frame_flattening:
|
||||
content.geolocation:
|
||||
default: ask
|
||||
type: BoolAsk
|
||||
supports_pattern: true
|
||||
desc: Allow websites to request geolocations.
|
||||
|
||||
content.headers.accept_language:
|
||||
type:
|
||||
name: String
|
||||
none_ok: true
|
||||
supports_pattern: true
|
||||
default: en-US,en
|
||||
desc: Value to send in the `Accept-Language` header.
|
||||
desc: >-
|
||||
Value to send in the `Accept-Language` header.
|
||||
|
||||
Note that the value read from JavaScript is always the global value.
|
||||
|
||||
content.headers.custom:
|
||||
default: {}
|
||||
@@ -335,6 +377,7 @@ content.headers.custom:
|
||||
name: String
|
||||
encoding: ascii
|
||||
none_ok: true
|
||||
supports_pattern: true
|
||||
desc: Custom headers for qutebrowser HTTP requests.
|
||||
|
||||
content.headers.do_not_track:
|
||||
@@ -342,6 +385,7 @@ content.headers.do_not_track:
|
||||
name: Bool
|
||||
none_ok: true
|
||||
default: true
|
||||
supports_pattern: true
|
||||
desc: >-
|
||||
Value to send in the `DNT` header.
|
||||
|
||||
@@ -416,7 +460,11 @@ content.headers.user_agent:
|
||||
Gecko"
|
||||
- IE 11.0 for Desktop Win7 64-bit
|
||||
|
||||
desc: User agent to send. Unset to send the default.
|
||||
supports_pattern: true
|
||||
desc: >-
|
||||
User agent to send. Unset to send the default.
|
||||
|
||||
Note that the value read from JavaScript is always the global value.
|
||||
|
||||
content.host_blocking.enabled:
|
||||
default: true
|
||||
@@ -425,11 +473,7 @@ content.host_blocking.enabled:
|
||||
|
||||
content.host_blocking.lists:
|
||||
default:
|
||||
- "https://www.malwaredomainlist.com/hostslist/hosts.txt"
|
||||
- "http://someonewhocares.org/hosts/hosts"
|
||||
- "http://winhelp2002.mvps.org/hosts.zip"
|
||||
- "http://malwaredomains.lehigh.edu/files/justdomains.zip"
|
||||
- "https://pgl.yoyo.org/adservers/serverlist.php?hostformat=hosts&mimetype=plaintext"
|
||||
- "https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts"
|
||||
type:
|
||||
name: List
|
||||
valtype: Url
|
||||
@@ -563,6 +607,7 @@ content.local_storage:
|
||||
content.media_capture:
|
||||
default: ask
|
||||
type: BoolAsk
|
||||
supports_pattern: true
|
||||
backend: QtWebEngine
|
||||
desc: Allow websites to record audio/video.
|
||||
|
||||
@@ -579,6 +624,7 @@ content.netrc_file:
|
||||
content.notifications:
|
||||
default: ask
|
||||
type: BoolAsk
|
||||
supports_pattern: true
|
||||
backend: QtWebKit
|
||||
desc: Allow websites to show notifications.
|
||||
|
||||
@@ -592,6 +638,16 @@ content.pdfjs:
|
||||
Note that the files can still be downloaded by clicking the download button
|
||||
in the pdf.js viewer.
|
||||
|
||||
content.persistent_storage:
|
||||
default: ask
|
||||
type: BoolAsk
|
||||
supports_pattern: true
|
||||
backend:
|
||||
QtWebKit: false
|
||||
QtWebEngine: Qt 5.11
|
||||
desc: Allow websites to request persistent storage quota via
|
||||
`navigator.webkitPersistentStorage.requestQuota`.
|
||||
|
||||
content.plugins:
|
||||
default: false
|
||||
type: Bool
|
||||
@@ -629,9 +685,20 @@ content.proxy_dns_requests:
|
||||
backend: QtWebKit
|
||||
desc: Send DNS requests over the configured proxy.
|
||||
|
||||
content.register_protocol_handler:
|
||||
default: ask
|
||||
type: BoolAsk
|
||||
supports_pattern: true
|
||||
backend:
|
||||
QtWebKit: false
|
||||
QtWebEngine: Qt 5.11
|
||||
desc: Allow websites to register protocol handlers via
|
||||
`navigator.registerProtocolHandler`.
|
||||
|
||||
content.ssl_strict:
|
||||
default: ask
|
||||
type: BoolAsk
|
||||
supports_pattern: true
|
||||
desc: Validate SSL handshakes.
|
||||
|
||||
content.user_stylesheets:
|
||||
@@ -648,6 +715,19 @@ content.webgl:
|
||||
supports_pattern: true
|
||||
desc: Enable WebGL.
|
||||
|
||||
content.webrtc_public_interfaces_only:
|
||||
default: false
|
||||
type: Bool
|
||||
backend:
|
||||
QtWebKit: false
|
||||
QtWebEngine: Qt 5.9.2
|
||||
desc: >-
|
||||
Only expose public interfaces via WebRTC.
|
||||
|
||||
On Qt 5.9, this option requires a restart.
|
||||
On Qt 5.10, this option doesn't work at all because of a Qt bug.
|
||||
On Qt >= 5.11, no restart is required.
|
||||
|
||||
content.xss_auditing:
|
||||
type: Bool
|
||||
default: false
|
||||
@@ -965,6 +1045,11 @@ hints.uppercase:
|
||||
|
||||
## input
|
||||
|
||||
input.escape_quits_reporter:
|
||||
type: Bool
|
||||
default: True
|
||||
desc: Allow Escape to quit the crash reporter.
|
||||
|
||||
input.forward_unbound_keys:
|
||||
default: auto
|
||||
type:
|
||||
@@ -1252,9 +1337,14 @@ tabs.favicons.scale:
|
||||
`tabs.padding`.
|
||||
|
||||
tabs.favicons.show:
|
||||
default: true
|
||||
type: Bool
|
||||
desc: Show favicons in the tab bar.
|
||||
default: always
|
||||
type:
|
||||
name: String
|
||||
valid_values:
|
||||
- always: Always show favicons.
|
||||
- never: Always hide favicons.
|
||||
- pinned: Show favicons only on pinned tabs.
|
||||
desc: When to show favicons in the tab bar.
|
||||
|
||||
tabs.last_close:
|
||||
default: ignore
|
||||
@@ -1325,7 +1415,10 @@ tabs.show:
|
||||
|
||||
tabs.show_switching_delay:
|
||||
default: 800
|
||||
type: Int
|
||||
type:
|
||||
name: Int
|
||||
minval: 0
|
||||
maxval: maxint
|
||||
desc: "Duration (in milliseconds) to show the tab bar before hiding it when
|
||||
tabs.show is set to 'switching'."
|
||||
|
||||
@@ -1340,7 +1433,7 @@ tabs.title.alignment:
|
||||
desc: Alignment of the text inside of tabs.
|
||||
|
||||
tabs.title.format:
|
||||
default: '{index}: {title}'
|
||||
default: '{audio}{index}: {title}'
|
||||
type:
|
||||
name: FormatString
|
||||
fields:
|
||||
@@ -1355,6 +1448,7 @@ tabs.title.format:
|
||||
- private
|
||||
- current_url
|
||||
- protocol
|
||||
- audio
|
||||
none_ok: true
|
||||
desc: |
|
||||
Format to use for the tab title.
|
||||
@@ -1372,6 +1466,7 @@ tabs.title.format:
|
||||
* `{private}`: Indicates when private mode is enabled.
|
||||
* `{current_url}`: URL of the current web page.
|
||||
* `{protocol}`: Protocol (http/https/...) of the current web page.
|
||||
* `{audio}`: Indicator for audio/mute status.
|
||||
|
||||
tabs.title.format_pinned:
|
||||
default: '{index}'
|
||||
@@ -1389,6 +1484,7 @@ tabs.title.format_pinned:
|
||||
- private
|
||||
- current_url
|
||||
- protocol
|
||||
- audio
|
||||
none_ok: true
|
||||
desc: Format to use for the tab title for pinned tabs. The same placeholders
|
||||
like for `tabs.title.format` are defined.
|
||||
@@ -1406,6 +1502,19 @@ tabs.width:
|
||||
desc: "Width (in pixels or as percentage of the window) of the tab bar if
|
||||
it's vertical."
|
||||
|
||||
tabs.min_width:
|
||||
default: -1
|
||||
type:
|
||||
name: Int
|
||||
minval: -1
|
||||
maxval: maxint
|
||||
desc: >-
|
||||
Minimum width (in pixels) of tabs (-1 for the default minimum size behavior).
|
||||
|
||||
This setting only applies when tabs are horizontal.
|
||||
|
||||
This setting does not apply to pinned tabs, unless `tabs.pinned.shrink` is False.
|
||||
|
||||
tabs.width.indicator:
|
||||
renamed: tabs.indicator.width
|
||||
|
||||
@@ -1469,6 +1578,11 @@ url.incdec_segments:
|
||||
desc: URL segments where `:navigate increment/decrement` will search for
|
||||
a number.
|
||||
|
||||
url.open_base_url:
|
||||
type: Bool
|
||||
default: false
|
||||
desc: Open base URL of the searchengine if a searchengine shortcut is invoked without parameters.
|
||||
|
||||
url.searchengines:
|
||||
default:
|
||||
DEFAULT: https://duckduckgo.com/?q={}
|
||||
@@ -1513,10 +1627,15 @@ url.yank_ignored_parameters:
|
||||
## window
|
||||
|
||||
window.hide_wayland_decoration:
|
||||
renamed: window.hide_decoration
|
||||
|
||||
window.hide_decoration:
|
||||
type: Bool
|
||||
default: false
|
||||
restart: true
|
||||
desc: Hide the window decoration when using wayland.
|
||||
desc: |
|
||||
Hide the window decoration.
|
||||
|
||||
This setting requires a restart on Wayland.
|
||||
|
||||
window.title_format:
|
||||
type:
|
||||
@@ -1533,6 +1652,7 @@ window.title_format:
|
||||
- private
|
||||
- current_url
|
||||
- protocol
|
||||
- audio
|
||||
default: '{perc}{title}{title_sep}qutebrowser'
|
||||
desc: |
|
||||
Format to use for the window title. The same placeholders like for
|
||||
@@ -2244,6 +2364,7 @@ bindings.default:
|
||||
;R: hint --rapid links window
|
||||
;d: hint links download
|
||||
;t: hint inputs
|
||||
gi: hint inputs --first
|
||||
h: scroll left
|
||||
j: scroll down
|
||||
k: scroll up
|
||||
@@ -2302,6 +2423,7 @@ bindings.default:
|
||||
gf: view-source
|
||||
gt: set-cmd-text -s :buffer
|
||||
<Ctrl-Tab>: tab-focus last
|
||||
<Ctrl-Shift-Tab>: nop
|
||||
<Ctrl-^>: tab-focus last
|
||||
<Ctrl-V>: enter-mode passthrough
|
||||
<Ctrl-Q>: quit
|
||||
@@ -2334,6 +2456,7 @@ bindings.default:
|
||||
<Ctrl-Return>: follow-selected -t
|
||||
.: repeat-command
|
||||
<Ctrl-p>: tab-pin
|
||||
<Alt-m>: tab-mute
|
||||
q: record-macro
|
||||
"@": run-macro
|
||||
tsh: config-cycle -p -t -u *://{url:host}/* content.javascript.enabled ;; reload
|
||||
|
||||
@@ -44,9 +44,15 @@ class BackendError(Error):
|
||||
|
||||
"""Raised when this setting is unavailable with the current backend."""
|
||||
|
||||
def __init__(self, name, backend):
|
||||
super().__init__("The {} setting is not available with the {} "
|
||||
"backend!".format(name, backend.name))
|
||||
def __init__(self, name, backend, raw_backends):
|
||||
if raw_backends is None or not raw_backends[backend.name]:
|
||||
msg = ("The {} setting is not available with the {} backend!"
|
||||
.format(name, backend.name))
|
||||
else:
|
||||
msg = ("The {} setting needs {} with the {} backend!"
|
||||
.format(name, raw_backends[backend.name], backend.name))
|
||||
|
||||
super().__init__(msg)
|
||||
|
||||
|
||||
class NoPatternError(Error):
|
||||
|
||||
@@ -233,6 +233,14 @@ class YamlConfig(QObject):
|
||||
if errors:
|
||||
raise configexc.ConfigFileErrors('autoconfig.yml', errors)
|
||||
|
||||
def _migrate_bool(self, settings, name, true_value, false_value):
|
||||
"""Migrate a boolean in the settings."""
|
||||
if name in settings:
|
||||
for scope, val in settings[name].items():
|
||||
if isinstance(val, bool):
|
||||
settings[name][scope] = true_value if val else false_value
|
||||
self._mark_changed()
|
||||
|
||||
def _handle_migrations(self, settings):
|
||||
"""Migrate older configs to the newest format."""
|
||||
# Simple renamed/deleted options
|
||||
@@ -268,6 +276,10 @@ class YamlConfig(QObject):
|
||||
del settings['bindings.default']
|
||||
self._mark_changed()
|
||||
|
||||
self._migrate_bool(settings, 'tabs.favicons.show', 'always', 'never')
|
||||
self._migrate_bool(settings, 'qt.force_software_rendering',
|
||||
'software-opengl', 'none')
|
||||
|
||||
return settings
|
||||
|
||||
def _validate(self, settings):
|
||||
@@ -564,7 +576,7 @@ def read_autoconfig():
|
||||
"""Read the autoconfig.yml file."""
|
||||
try:
|
||||
config.instance.read_yaml()
|
||||
except configexc.ConfigFileErrors as e:
|
||||
except configexc.ConfigFileErrors:
|
||||
raise # caught in outer block
|
||||
except configexc.Error as e:
|
||||
desc = configexc.ConfigErrorDesc("Error", e)
|
||||
|
||||
@@ -26,7 +26,8 @@ from PyQt5.QtWidgets import QMessageBox
|
||||
|
||||
from qutebrowser.config import (config, configdata, configfiles, configtypes,
|
||||
configexc, configcommands)
|
||||
from qutebrowser.utils import objreg, usertypes, log, standarddir, message
|
||||
from qutebrowser.utils import (objreg, usertypes, log, standarddir, message,
|
||||
qtutils)
|
||||
from qutebrowser.misc import msgbox, objects
|
||||
|
||||
|
||||
@@ -82,14 +83,17 @@ def early_init(args):
|
||||
|
||||
def _init_envvars():
|
||||
"""Initialize environment variables which need to be set early."""
|
||||
if (objects.backend == usertypes.Backend.QtWebEngine and
|
||||
config.val.qt.force_software_rendering):
|
||||
os.environ['QT_XCB_FORCE_SOFTWARE_OPENGL'] = '1'
|
||||
if objects.backend == usertypes.Backend.QtWebEngine:
|
||||
software_rendering = config.val.qt.force_software_rendering
|
||||
if software_rendering == 'software-opengl':
|
||||
os.environ['QT_XCB_FORCE_SOFTWARE_OPENGL'] = '1'
|
||||
elif software_rendering == 'qt-quick':
|
||||
os.environ['QT_QUICK_BACKEND'] = 'software'
|
||||
|
||||
if config.val.qt.force_platform is not None:
|
||||
os.environ['QT_QPA_PLATFORM'] = config.val.qt.force_platform
|
||||
|
||||
if config.val.window.hide_wayland_decoration:
|
||||
if config.val.window.hide_decoration:
|
||||
os.environ['QT_WAYLAND_DISABLE_WINDOWDECORATION'] = '1'
|
||||
|
||||
if config.val.qt.highdpi:
|
||||
@@ -161,4 +165,26 @@ def qt_args(namespace):
|
||||
argv += ['--' + name, value]
|
||||
|
||||
argv += ['--' + arg for arg in config.val.qt.args]
|
||||
|
||||
if objects.backend == usertypes.Backend.QtWebEngine:
|
||||
if not qtutils.version_check('5.11', compiled=False):
|
||||
# WORKAROUND equivalent to
|
||||
# https://codereview.qt-project.org/#/c/217932/
|
||||
# Needed for Qt < 5.9.5 and < 5.10.1
|
||||
argv.append('--disable-shared-workers')
|
||||
|
||||
if config.val.qt.force_software_rendering == 'chromium':
|
||||
argv.append('--disable-gpu')
|
||||
|
||||
if not config.val.content.canvas_reading:
|
||||
argv.append('--disable-reading-from-canvas')
|
||||
|
||||
if not qtutils.version_check('5.11'):
|
||||
# On Qt 5.11, we can control this via QWebEngineSettings
|
||||
if not config.val.content.autoplay:
|
||||
argv.append('--autoplay-policy=user-gesture-required')
|
||||
if config.val.content.webrtc_public_interfaces_only:
|
||||
argv.append('--force-webrtc-ip-handling-policy='
|
||||
'default_public_interface_only')
|
||||
|
||||
return argv
|
||||
|
||||
@@ -451,7 +451,7 @@ class List(BaseType):
|
||||
def from_obj(self, value):
|
||||
if value is None:
|
||||
return []
|
||||
return value
|
||||
return [self.valtype.from_obj(v) for v in value]
|
||||
|
||||
def to_py(self, value):
|
||||
self._basic_py_validation(value, list)
|
||||
@@ -506,6 +506,16 @@ class ListOrValue(BaseType):
|
||||
self.listtype = List(valtype, none_ok=none_ok, *args, **kwargs)
|
||||
self.valtype = valtype
|
||||
|
||||
def _val_and_type(self, value):
|
||||
"""Get the value and type to use for to_str/to_doc/from_str."""
|
||||
if isinstance(value, list):
|
||||
if len(value) == 1:
|
||||
return value[0], self.valtype
|
||||
else:
|
||||
return value, self.listtype
|
||||
else:
|
||||
return value, self.valtype
|
||||
|
||||
def get_name(self):
|
||||
return self.listtype.get_name() + ', or ' + self.valtype.get_name()
|
||||
|
||||
@@ -533,25 +543,15 @@ class ListOrValue(BaseType):
|
||||
if value is None:
|
||||
return ''
|
||||
|
||||
if isinstance(value, list):
|
||||
if len(value) == 1:
|
||||
return self.valtype.to_str(value[0])
|
||||
else:
|
||||
return self.listtype.to_str(value)
|
||||
else:
|
||||
return self.valtype.to_str(value)
|
||||
val, typ = self._val_and_type(value)
|
||||
return typ.to_str(val)
|
||||
|
||||
def to_doc(self, value, indent=0):
|
||||
if value is None:
|
||||
return 'empty'
|
||||
|
||||
if isinstance(value, list):
|
||||
if len(value) == 1:
|
||||
return self.valtype.to_doc(value[0], indent)
|
||||
else:
|
||||
return self.listtype.to_doc(value, indent)
|
||||
else:
|
||||
return self.valtype.to_doc(value, indent)
|
||||
val, typ = self._val_and_type(value)
|
||||
return typ.to_doc(val)
|
||||
|
||||
|
||||
class FlagList(List):
|
||||
@@ -1199,7 +1199,9 @@ class Dict(BaseType):
|
||||
def from_obj(self, value):
|
||||
if value is None:
|
||||
return {}
|
||||
return value
|
||||
|
||||
return {self.keytype.from_obj(key): self.valtype.from_obj(val)
|
||||
for key, val in value.items()}
|
||||
|
||||
def _fill_fixed_keys(self, value):
|
||||
"""Fill missing fixed keys with a None-value."""
|
||||
@@ -1407,7 +1409,7 @@ class SearchEngineUrl(BaseType):
|
||||
|
||||
try:
|
||||
value.format("")
|
||||
except (KeyError, IndexError) as e:
|
||||
except (KeyError, IndexError):
|
||||
raise configexc.ValidationError(
|
||||
value, "may not contain {...} (use {{ and }} for literal {/})")
|
||||
except ValueError as e:
|
||||
@@ -1623,9 +1625,7 @@ class TimestampTemplate(BaseType):
|
||||
|
||||
"""An strftime-like template for timestamps.
|
||||
|
||||
See
|
||||
https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior
|
||||
for reference.
|
||||
See https://sqlite.org/lang_datefunc.html for reference.
|
||||
"""
|
||||
|
||||
def to_py(self, value):
|
||||
@@ -1648,6 +1648,10 @@ class Key(BaseType):
|
||||
|
||||
"""A name of a key."""
|
||||
|
||||
def from_obj(self, value):
|
||||
"""Make sure key sequences are always normalized."""
|
||||
return str(keyutils.KeySequence.parse(value))
|
||||
|
||||
def to_py(self, value):
|
||||
self._basic_py_validation(value, str)
|
||||
if not value:
|
||||
|
||||
@@ -22,12 +22,24 @@
|
||||
from PyQt5.QtGui import QFont
|
||||
|
||||
from qutebrowser.config import config, configutils
|
||||
from qutebrowser.utils import log, usertypes, urlmatch
|
||||
from qutebrowser.utils import log, usertypes, urlmatch, qtutils
|
||||
from qutebrowser.misc import objects
|
||||
|
||||
UNSET = object()
|
||||
|
||||
|
||||
class AttributeInfo:
|
||||
|
||||
"""Info about a settings attribute."""
|
||||
|
||||
def __init__(self, *attributes, converter=None):
|
||||
self.attributes = attributes
|
||||
if converter is None:
|
||||
self.converter = lambda val: val
|
||||
else:
|
||||
self.converter = converter
|
||||
|
||||
|
||||
class AbstractSettings:
|
||||
|
||||
"""Abstract base class for settings set via QWeb(Engine)Settings."""
|
||||
@@ -50,12 +62,13 @@ class AbstractSettings:
|
||||
"""
|
||||
old_value = self.test_attribute(name)
|
||||
|
||||
for attribute in self._ATTRIBUTES[name]:
|
||||
info = self._ATTRIBUTES[name]
|
||||
for attribute in info.attributes:
|
||||
if value is configutils.UNSET:
|
||||
self._settings.resetAttribute(attribute)
|
||||
new_value = self.test_attribute(name)
|
||||
else:
|
||||
self._settings.setAttribute(attribute, value)
|
||||
self._settings.setAttribute(attribute, info.converter(value))
|
||||
new_value = value
|
||||
|
||||
return old_value != new_value
|
||||
@@ -66,7 +79,8 @@ class AbstractSettings:
|
||||
If the setting resolves to a list of attributes, only the first
|
||||
attribute is tested.
|
||||
"""
|
||||
return self._settings.testAttribute(self._ATTRIBUTES[name][0])
|
||||
info = self._ATTRIBUTES[name]
|
||||
return self._settings.testAttribute(info.attributes[0])
|
||||
|
||||
def set_font_size(self, name, value):
|
||||
"""Set the given QWebSettings/QWebEngineSettings font size.
|
||||
@@ -141,6 +155,7 @@ class AbstractSettings:
|
||||
Return:
|
||||
A set of settings which actually changed.
|
||||
"""
|
||||
qtutils.ensure_valid(url)
|
||||
changed_settings = set()
|
||||
for values in config.instance:
|
||||
if not values.opt.supports_pattern:
|
||||
|
||||
@@ -74,7 +74,7 @@ function tryagain()
|
||||
</td>
|
||||
<td style="padding-left: 40px;">
|
||||
<h1>Unable to load page</h1>
|
||||
Error while opening {{ url }}<br>
|
||||
Error while opening {{ url | default('page', true) }}<br>
|
||||
<p id="error-message-text" style="color: #a31a1a;">{{ error }}</p><br><br>
|
||||
|
||||
<form name="bl">
|
||||
|
||||
@@ -2,3 +2,4 @@
|
||||
pac_utils.js
|
||||
# Actually a jinja template so eslint chokes on the {{}} syntax.
|
||||
greasemonkey_wrapper.js
|
||||
global_wrapper.js
|
||||
|
||||
@@ -56,3 +56,5 @@ rules:
|
||||
no-bitwise: "off"
|
||||
no-ternary: "off"
|
||||
max-lines: "off"
|
||||
multiline-ternary: ["error", "always-multiline"]
|
||||
max-lines-per-function: "off"
|
||||
|
||||
@@ -324,9 +324,8 @@ window._qutebrowser.caret = (function() {
|
||||
const color = axs.color.parseColor(style.backgroundColor);
|
||||
if (color &&
|
||||
(style.opacity < 1 &&
|
||||
(color.alpha *= style.opacity),
|
||||
color.alpha !== 0 &&
|
||||
(el.push(color), color.alpha === 1))) {
|
||||
(color.alpha *= style.opacity), color.alpha !== 0 &&
|
||||
(el.push(color), color.alpha === 1))) {
|
||||
iter = !0;
|
||||
break;
|
||||
}
|
||||
@@ -781,6 +780,18 @@ window._qutebrowser.caret = (function() {
|
||||
*/
|
||||
CaretBrowsing.blinkFlag = true;
|
||||
|
||||
/**
|
||||
* Whether we're running on Windows.
|
||||
* @type {boolean}
|
||||
*/
|
||||
CaretBrowsing.isWindows = null;
|
||||
|
||||
/**
|
||||
* Whether we're running on on old Qt 5.7.1.
|
||||
* @type {boolean}
|
||||
*/
|
||||
CaretBrowsing.isOldQt = null;
|
||||
|
||||
/**
|
||||
* Check if a node is a control that normally allows the user to interact
|
||||
* with it using arrow keys. We won't override the arrow keys when such a
|
||||
@@ -857,12 +868,24 @@ window._qutebrowser.caret = (function() {
|
||||
};
|
||||
|
||||
CaretBrowsing.injectCaretStyles = function() {
|
||||
const style = ".CaretBrowsing_Caret {" +
|
||||
" position: absolute;" +
|
||||
" z-index: 2147483647;" +
|
||||
" min-height: 10px;" +
|
||||
" background-color: #000;" +
|
||||
"}";
|
||||
const prefix = CaretBrowsing.isOldQt ? "-webkit-" : "";
|
||||
const style = `
|
||||
.CaretBrowsing_Caret {
|
||||
position: absolute;
|
||||
z-index: 2147483647;
|
||||
min-height: 1em;
|
||||
min-width: 0.2em;
|
||||
animation: blink 1s step-end infinite;
|
||||
--inherited-color: inherit;
|
||||
background-color: var(--inherited-color, #000);
|
||||
color: var(--inherited-color, #000);
|
||||
mix-blend-mode: difference;
|
||||
${prefix}filter: invert(85%);
|
||||
}
|
||||
@keyframes blink {
|
||||
50% { visibility: hidden; }
|
||||
}
|
||||
`;
|
||||
const node = document.createElement("style");
|
||||
node.innerHTML = style;
|
||||
document.body.appendChild(node);
|
||||
@@ -1159,6 +1182,8 @@ window._qutebrowser.caret = (function() {
|
||||
CaretBrowsing.updateCaretOrSelection(true);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
CaretBrowsing.stopAnimation();
|
||||
};
|
||||
|
||||
CaretBrowsing.moveToBlock = function(paragraph, boundary) {
|
||||
@@ -1177,6 +1202,8 @@ window._qutebrowser.caret = (function() {
|
||||
window.setTimeout(() => {
|
||||
CaretBrowsing.updateCaretOrSelection(true);
|
||||
}, 0);
|
||||
|
||||
CaretBrowsing.stopAnimation();
|
||||
};
|
||||
|
||||
CaretBrowsing.toggle = function(value) {
|
||||
@@ -1243,6 +1270,17 @@ window._qutebrowser.caret = (function() {
|
||||
CaretBrowsing.updateIsCaretVisible();
|
||||
};
|
||||
|
||||
CaretBrowsing.startAnimation = function() {
|
||||
CaretBrowsing.caretElement.style.animationIterationCount = "infinite";
|
||||
};
|
||||
|
||||
CaretBrowsing.stopAnimation = function() {
|
||||
CaretBrowsing.caretElement.style.animationIterationCount = 0;
|
||||
window.setTimeout(() => {
|
||||
CaretBrowsing.startAnimation();
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
CaretBrowsing.init = function() {
|
||||
CaretBrowsing.isWindowFocused = document.hasFocus();
|
||||
|
||||
@@ -1270,17 +1308,19 @@ window._qutebrowser.caret = (function() {
|
||||
funcs.setInitialCursor = () => {
|
||||
if (!CaretBrowsing.initiated) {
|
||||
CaretBrowsing.setInitialCursor();
|
||||
return;
|
||||
return CaretBrowsing.selectionEnabled;
|
||||
}
|
||||
|
||||
if (window.getSelection().toString().length === 0) {
|
||||
positionCaret();
|
||||
}
|
||||
CaretBrowsing.toggle();
|
||||
return CaretBrowsing.selectionEnabled;
|
||||
};
|
||||
|
||||
funcs.setPlatform = (platform) => {
|
||||
funcs.setPlatform = (platform, qtVersion) => {
|
||||
CaretBrowsing.isWindows = platform.startsWith("win");
|
||||
CaretBrowsing.isOldQt = qtVersion === "5.7.1";
|
||||
};
|
||||
|
||||
funcs.disableCaret = () => {
|
||||
@@ -1362,6 +1402,7 @@ window._qutebrowser.caret = (function() {
|
||||
|
||||
funcs.toggleSelection = () => {
|
||||
CaretBrowsing.selectionEnabled = !CaretBrowsing.selectionEnabled;
|
||||
return CaretBrowsing.selectionEnabled;
|
||||
};
|
||||
|
||||
return funcs;
|
||||
|
||||
12
qutebrowser/javascript/global_wrapper.js
Normal file
@@ -0,0 +1,12 @@
|
||||
(function() {
|
||||
"use strict";
|
||||
if (!("_qutebrowser" in window)) {
|
||||
window._qutebrowser = {"initialized": {}};
|
||||
}
|
||||
|
||||
if (window._qutebrowser.initialized["{{name}}"]) {
|
||||
return;
|
||||
}
|
||||
{{code}}
|
||||
window._qutebrowser.initialized["{{name}}"] = true;
|
||||
})();
|
||||
@@ -1,5 +1,5 @@
|
||||
(function() {
|
||||
const _qute_script_id = "__gm_" + {{ scriptName | tojson }};
|
||||
const _qute_script_id = "__gm_{{ scriptName }}";
|
||||
|
||||
function GM_log(text) {
|
||||
console.log(text);
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
const GM_info = {
|
||||
'script': {{ scriptInfo }},
|
||||
'scriptMetaStr': {{ scriptMeta | tojson }},
|
||||
'scriptMetaStr': "{{ scriptMeta }}",
|
||||
'scriptWillUpdate': false,
|
||||
'version': "0.0.1",
|
||||
// so scripts don't expect exportFunction
|
||||
@@ -100,11 +100,8 @@
|
||||
|
||||
const head = document.getElementsByTagName("head")[0];
|
||||
if (head === undefined) {
|
||||
document.onreadystatechange = function() {
|
||||
if (document.readyState === "interactive") {
|
||||
document.getElementsByTagName("head")[0].appendChild(oStyle);
|
||||
}
|
||||
};
|
||||
// no head yet, stick it whereever
|
||||
document.documentElement.appendChild(oStyle);
|
||||
} else {
|
||||
head.appendChild(oStyle);
|
||||
}
|
||||
@@ -148,9 +145,59 @@
|
||||
}
|
||||
};
|
||||
|
||||
const unsafeWindow = window;
|
||||
{% if use_proxy %}
|
||||
/*
|
||||
* Try to give userscripts an environment that they expect. Which
|
||||
* seems to be that the global window object should look the same as
|
||||
* the page's one and that if a script writes to an attribute of
|
||||
* window it should be able to access that variable in the global
|
||||
* scope.
|
||||
* Use a Proxy to stop scripts from actually changing the global
|
||||
* window (that's what unsafeWindow is for).
|
||||
* Use the "with" statement to make the proxy provide what looks
|
||||
* like global scope.
|
||||
*
|
||||
* There are other Proxy functions that we may need to override.
|
||||
* set, get and has are definitely required.
|
||||
*/
|
||||
const unsafeWindow = window;
|
||||
const qute_gm_window_shadow = {}; // stores local changes to window
|
||||
const qute_gm_windowProxyHandler = {
|
||||
get: function(target, prop) {
|
||||
if (prop in qute_gm_window_shadow)
|
||||
return qute_gm_window_shadow[prop];
|
||||
if (prop in target) {
|
||||
if (typeof target[prop] === 'function' && typeof target[prop].prototype == 'undefined')
|
||||
// Getting TypeError: Illegal Execution when callers try to execute
|
||||
// eg addEventListener from here because they were returned
|
||||
// unbound
|
||||
return target[prop].bind(target);
|
||||
return target[prop];
|
||||
}
|
||||
},
|
||||
set: function(target, prop, val) {
|
||||
return qute_gm_window_shadow[prop] = val;
|
||||
},
|
||||
has: function(target, key) {
|
||||
return key in qute_gm_window_shadow || key in target;
|
||||
}
|
||||
};
|
||||
const qute_gm_window_proxy = new Proxy(
|
||||
unsafeWindow, qute_gm_windowProxyHandler);
|
||||
|
||||
// ====== The actual user script source ====== //
|
||||
with (qute_gm_window_proxy) {
|
||||
// We can't return `this` or `qute_gm_window_proxy` from
|
||||
// `qute_gm_window_proxy.get('window')` because the Proxy implementation
|
||||
// does typechecking on read-only things. So we have to shadow `window`
|
||||
// more conventionally here.
|
||||
const window = qute_gm_window_proxy;
|
||||
// ====== The actual user script source ====== //
|
||||
{{ scriptSource }}
|
||||
// ====== End User Script ====== //
|
||||
// ====== End User Script ====== //
|
||||
};
|
||||
{% else %}
|
||||
// ====== The actual user script source ====== //
|
||||
{{ scriptSource }}
|
||||
// ====== End User Script ====== //
|
||||
{% endif %}
|
||||
})();
|
||||
|
||||