Compare commits
587 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b3cd31a808 | ||
|
|
f31f254d9b | ||
|
|
63dee327c9 | ||
|
|
8d3a8f9eda | ||
|
|
bf4e968c67 | ||
|
|
b3869fe42b | ||
|
|
58b738ca5b | ||
|
|
9eaa0d0968 | ||
|
|
93f5e30a00 | ||
|
|
ddf7f202d8 | ||
|
|
086010d81e | ||
|
|
6dbac1c047 | ||
|
|
e9b5c355d2 | ||
|
|
956baed76b | ||
|
|
f10b9f1172 | ||
|
|
97cc90b49f | ||
|
|
345d048f43 | ||
|
|
f61aaa9053 | ||
|
|
4652843b38 | ||
|
|
5d490a4e22 | ||
|
|
534a85cf8f | ||
|
|
75b894a186 | ||
|
|
220ac021f0 | ||
|
|
24424a0486 | ||
|
|
db267ae195 | ||
|
|
d5d85bd9c7 | ||
|
|
f8d66f3fe1 | ||
|
|
0f1ba4739c | ||
|
|
4c7c38efcb | ||
|
|
b7c3e7b959 | ||
|
|
b21b4377a8 | ||
|
|
10b00da1ae | ||
|
|
b337cfe4c6 | ||
|
|
3dbf3f9e0a | ||
|
|
d02b63a847 | ||
|
|
f2d7391974 | ||
|
|
4dd23c530a | ||
|
|
e459ac52cc | ||
|
|
5cf1dce89e | ||
|
|
94d394001e | ||
|
|
e2c375b874 | ||
|
|
fd82587213 | ||
|
|
a5610fd6da | ||
|
|
85f6b3c6df | ||
|
|
08c8a5f7dd | ||
|
|
b0012fd410 | ||
|
|
894ec7d66a | ||
|
|
42b5ee831e | ||
|
|
3ba63128da | ||
|
|
425a6d33cf | ||
|
|
2f59abaf13 | ||
|
|
3de1299650 | ||
|
|
0d59a1cba8 | ||
|
|
9ca06ecfa2 | ||
|
|
ef78f69822 | ||
|
|
c72da37916 | ||
|
|
1cc6a6669b | ||
|
|
14237c2c62 | ||
|
|
a5c078516b | ||
|
|
c3c52220f6 | ||
|
|
0350d19bd3 | ||
|
|
c64d9520ff | ||
|
|
59cdbd780c | ||
|
|
703b0043db | ||
|
|
6dbdea0ee3 | ||
|
|
b1334bcc22 | ||
|
|
dfe98d1053 | ||
|
|
a024c14dd6 | ||
|
|
e7b84d4089 | ||
|
|
0119cf510f | ||
|
|
a545b919f7 | ||
|
|
b43d8b13d8 | ||
|
|
70699988ed | ||
|
|
8dc9f0562a | ||
|
|
f1ba14b496 | ||
|
|
9bf749643a | ||
|
|
219c2f8ae8 | ||
|
|
f17131f6c2 | ||
|
|
84a269f36a | ||
|
|
8033931bae | ||
|
|
9b066ec50a | ||
|
|
b1e9ff059a | ||
|
|
e8830a631e | ||
|
|
425fcdf8e4 | ||
|
|
d2e103ecc1 | ||
|
|
167faafff2 | ||
|
|
8369c74f74 | ||
|
|
6f690c442e | ||
|
|
efcea65596 | ||
|
|
8ecc3a3bb0 | ||
|
|
ea1921defd | ||
|
|
36a2f4a15a | ||
|
|
fc32858e5c | ||
|
|
1956158096 | ||
|
|
33ad0ab1fc | ||
|
|
fc5349e1dc | ||
|
|
d3b727d0c7 | ||
|
|
6736f6a3f2 | ||
|
|
5828bbafe9 | ||
|
|
84dacc9bc8 | ||
|
|
8a87b5d357 | ||
|
|
ad401e035f | ||
|
|
1f67353a40 | ||
|
|
62d2018695 | ||
|
|
163bc2e12e | ||
|
|
1a9bc64776 | ||
|
|
231f1d90ce | ||
|
|
17bb9fc21c | ||
|
|
90bbe4d1ef | ||
|
|
364e13f4c2 | ||
|
|
a79b07bd94 | ||
|
|
c4eabcd663 | ||
|
|
599f582c20 | ||
|
|
3e8a394217 | ||
|
|
480c4e878e | ||
|
|
fdd302e4f7 | ||
|
|
e16d89a548 | ||
|
|
9b7b97d626 | ||
|
|
ab27612139 | ||
|
|
863e194073 | ||
|
|
5a8b7910e0 | ||
|
|
67473c6db1 | ||
|
|
68d8900c6c | ||
|
|
ddd343c89c | ||
|
|
67e895b6c7 | ||
|
|
b57027f800 | ||
|
|
fc15e85811 | ||
|
|
3be9a9b051 | ||
|
|
645a1512dd | ||
|
|
4532176e7b | ||
|
|
80a59720de | ||
|
|
839d2b1cbe | ||
|
|
0120061456 | ||
|
|
108e722c85 | ||
|
|
3b4fe97dbc | ||
|
|
b6349437f7 | ||
|
|
b5cd082e43 | ||
|
|
154af84714 | ||
|
|
10e8b78695 | ||
|
|
150ca90517 | ||
|
|
5a2d909607 | ||
|
|
171a0f201b | ||
|
|
7f27c183be | ||
|
|
0e50760b70 | ||
|
|
c08078841f | ||
|
|
1fcce12870 | ||
|
|
00747be9d3 | ||
|
|
261c44bea9 | ||
|
|
34d4c08374 | ||
|
|
ebc013ac2a | ||
|
|
1e982a9a84 | ||
|
|
e60f698615 | ||
|
|
df53ccf426 | ||
|
|
4204579c06 | ||
|
|
4a4856c176 | ||
|
|
90b3927906 | ||
|
|
2ff6dbd482 | ||
|
|
f85ca19cef | ||
|
|
da2ff6f3cb | ||
|
|
525d3ee4c9 | ||
|
|
6b94dc5279 | ||
|
|
2fa6c952c2 | ||
|
|
83f7cf84a9 | ||
|
|
37750b9e30 | ||
|
|
a82b0d007d | ||
|
|
e98a05e53d | ||
|
|
d887623377 | ||
|
|
e86a79740a | ||
|
|
aa4cb2927d | ||
|
|
2117b2afc6 | ||
|
|
5310c60d58 | ||
|
|
a0e5a3e8ee | ||
|
|
5a73ad0c19 | ||
|
|
def41e70bf | ||
|
|
fd75f77108 | ||
|
|
5bacbc9d38 | ||
|
|
de0686c50a | ||
|
|
b0880df695 | ||
|
|
94178c558a | ||
|
|
2459f14f6f | ||
|
|
015de0e6db | ||
|
|
ace7877010 | ||
|
|
d3e85ad982 | ||
|
|
5fb23f1373 | ||
|
|
8001099661 | ||
|
|
708d0d9c27 | ||
|
|
d3f7d9319a | ||
|
|
6ec8bbaca5 | ||
|
|
e38169433e | ||
|
|
dfada850e0 | ||
|
|
a7b10a090f | ||
|
|
fc4c7bd2e4 | ||
|
|
402aa66756 | ||
|
|
b55e22b5c3 | ||
|
|
fa65f345ac | ||
|
|
57ddd8e95e | ||
|
|
728f06e797 | ||
|
|
7102459c81 | ||
|
|
622938e3d3 | ||
|
|
c776958388 | ||
|
|
05fe68ccab | ||
|
|
c907572557 | ||
|
|
4a909aa028 | ||
|
|
f41acc8fb5 | ||
|
|
9ec6e6da80 | ||
|
|
d60d4d756c | ||
|
|
0132bea42b | ||
|
|
472071c047 | ||
|
|
85eea17b18 | ||
|
|
e780efb3d9 | ||
|
|
4d141f489f | ||
|
|
f0c58b58dd | ||
|
|
36803cba06 | ||
|
|
d8e58b5886 | ||
|
|
592ace18d4 | ||
|
|
e767f7c0b8 | ||
|
|
1bf036d1ba | ||
|
|
131f345007 | ||
|
|
dc59ed4d73 | ||
|
|
e22ef776f9 | ||
|
|
b5a70dbdec | ||
|
|
6c2fe3417e | ||
|
|
f1c0781a4c | ||
|
|
7daf1cb239 | ||
|
|
0ddf1316f7 | ||
|
|
a14685be3d | ||
|
|
f52f3db1f2 | ||
|
|
e7619477cd | ||
|
|
018d7a87be | ||
|
|
4204a8de9a | ||
|
|
122f0a7edc | ||
|
|
9cece08b2b | ||
|
|
172d0c3ca2 | ||
|
|
4c8b1be19c | ||
|
|
3d0721afea | ||
|
|
27cbe618f0 | ||
|
|
c0b6aef774 | ||
|
|
d0eda3336c | ||
|
|
1cd64481de | ||
|
|
87e9888167 | ||
|
|
c5c145320c | ||
|
|
4ff9d585ea | ||
|
|
1e5c67f152 | ||
|
|
54c1cd7c05 | ||
|
|
1814b672d7 | ||
|
|
6b550defae | ||
|
|
cdde1d7dfc | ||
|
|
11b258568d | ||
|
|
5b3ffa2419 | ||
|
|
b0bd8170e0 | ||
|
|
81345eb17e | ||
|
|
b1f8a70c02 | ||
|
|
0be0884a5b | ||
|
|
3879b8301f | ||
|
|
3c8e616eb9 | ||
|
|
b501677c0e | ||
|
|
5b891ecaca | ||
|
|
4dc54f881c | ||
|
|
5c599879f8 | ||
|
|
b59dc8e89b | ||
|
|
fed2cdad4e | ||
|
|
7b5d2ace24 | ||
|
|
989e3b7291 | ||
|
|
b1dd649278 | ||
|
|
e48e063c0f | ||
|
|
a56a14fb70 | ||
|
|
6ca541d359 | ||
|
|
70956aaeca | ||
|
|
9c99c22f1b | ||
|
|
6d592c7c75 | ||
|
|
0d19d1bcf7 | ||
|
|
1b89d880f5 | ||
|
|
8c80f99a32 | ||
|
|
c1dadeff6f | ||
|
|
27fdf4903a | ||
|
|
c7dcaff025 | ||
|
|
f7b517f3aa | ||
|
|
48735315f8 | ||
|
|
d20872d576 | ||
|
|
c76221c14e | ||
|
|
63c9e6a444 | ||
|
|
f5d299d8c7 | ||
|
|
b5eea81e2e | ||
|
|
4851a3d442 | ||
|
|
e12dce9d55 | ||
|
|
cfae36c5c8 | ||
|
|
4e61a6123e | ||
|
|
f326fa28a6 | ||
|
|
534dbfc4c2 | ||
|
|
091353a773 | ||
|
|
2a269e9cd9 | ||
|
|
1b48dc8749 | ||
|
|
ddf86600d1 | ||
|
|
6f3fa9dca6 | ||
|
|
a969fe021d | ||
|
|
6452c8f883 | ||
|
|
b8dd71a343 | ||
|
|
460308f388 | ||
|
|
6a26bc23ab | ||
|
|
48de8b145b | ||
|
|
0788054dd3 | ||
|
|
b2d763f993 | ||
|
|
35f0b26f4a | ||
|
|
ba9c782824 | ||
|
|
fa69786b0f | ||
|
|
e10da78a1a | ||
|
|
92abf4bdf8 | ||
|
|
27e82ce6c8 | ||
|
|
f1129460d8 | ||
|
|
c54c637ccc | ||
|
|
e300b2e30d | ||
|
|
11c03d79cd | ||
|
|
6b98c48985 | ||
|
|
b858b6ac75 | ||
|
|
a8d2dbfdfb | ||
|
|
0553094494 | ||
|
|
61519e6383 | ||
|
|
45dea54e3c | ||
|
|
a345b02729 | ||
|
|
6d879bbca3 | ||
|
|
0f13d9325b | ||
|
|
120d2e12b0 | ||
|
|
8d15bbdded | ||
|
|
ad7920dda1 | ||
|
|
93b92f4aab | ||
|
|
61f32b3e9b | ||
|
|
14ba20670b | ||
|
|
29b25206f6 | ||
|
|
58f031630c | ||
|
|
ee0eabc202 | ||
|
|
43898ebb71 | ||
|
|
0252f5fdbf | ||
|
|
aaab05793e | ||
|
|
ddb6743b26 | ||
|
|
269676318b | ||
|
|
6f904759b5 | ||
|
|
f8db4b8147 | ||
|
|
14df72a7a1 | ||
|
|
e590bf26ad | ||
|
|
40cc354030 | ||
|
|
c0b41d8c62 | ||
|
|
1f048a38f8 | ||
|
|
e187cda292 | ||
|
|
daaf7a62c8 | ||
|
|
341708f543 | ||
|
|
ad181ec7eb | ||
|
|
069d7b26a2 | ||
|
|
cc88451003 | ||
|
|
7ca9a007f8 | ||
|
|
b78d5f57aa | ||
|
|
98d1fca220 | ||
|
|
beb970d7d5 | ||
|
|
37b431f72f | ||
|
|
9a1cf2b03a | ||
|
|
e0dee14df4 | ||
|
|
f2e2748c59 | ||
|
|
03e59051dc | ||
|
|
91ad91cc7b | ||
|
|
b650ec75f3 | ||
|
|
c00dccfbb2 | ||
|
|
8941b5dc96 | ||
|
|
8e417970c3 | ||
|
|
1a957b6c10 | ||
|
|
8eb483d66b | ||
|
|
dd292b0781 | ||
|
|
54eae77328 | ||
|
|
81ba49e79b | ||
|
|
a9f5d45c34 | ||
|
|
e24d2e1b8c | ||
|
|
10985c3505 | ||
|
|
5ef40829aa | ||
|
|
b60f673468 | ||
|
|
8ab2772dd9 | ||
|
|
f17238d3d4 | ||
|
|
b5dc4ea040 | ||
|
|
7fc99f3d80 | ||
|
|
f54c416ddd | ||
|
|
f6ad556f34 | ||
|
|
b94fcf2c3c | ||
|
|
315725a3ac | ||
|
|
002346a125 | ||
|
|
b619d835e6 | ||
|
|
3f98bf372e | ||
|
|
9be5992a9a | ||
|
|
62426380e5 | ||
|
|
a1f7eed5a7 | ||
|
|
d7999577dd | ||
|
|
54131e9d3e | ||
|
|
aab5411317 | ||
|
|
183049ef2e | ||
|
|
42c27ddbc0 | ||
|
|
c762340a0c | ||
|
|
9b372de4a9 | ||
|
|
4dbc4ba93f | ||
|
|
dd83a40df4 | ||
|
|
677cfc9410 | ||
|
|
c91344cdf5 | ||
|
|
137badc77f | ||
|
|
ad338e7a17 | ||
|
|
0cabedfeef | ||
|
|
cd53318c7f | ||
|
|
f855d5f349 | ||
|
|
e3bfe73442 | ||
|
|
7e2c67a7e4 | ||
|
|
12940eb542 | ||
|
|
1a1a8ba26f | ||
|
|
1a67794293 | ||
|
|
aaf09dc573 | ||
|
|
f49dba6e38 | ||
|
|
c236046a73 | ||
|
|
17fc6622bb | ||
|
|
d992caf8fc | ||
|
|
947dcd556b | ||
|
|
bc54eb8671 | ||
|
|
222627b08d | ||
|
|
a728704cce | ||
|
|
f8f8699ab8 | ||
|
|
5d13d0073c | ||
|
|
f6ef657952 | ||
|
|
25005ded8a | ||
|
|
a93bf184aa | ||
|
|
f59a147589 | ||
|
|
866b299fef | ||
|
|
a74a9c8a21 | ||
|
|
88fc186402 | ||
|
|
ce1b82616d | ||
|
|
dd0e230a32 | ||
|
|
e35d284282 | ||
|
|
9fde38d96a | ||
|
|
e62ba57291 | ||
|
|
2775f2b2ee | ||
|
|
7edfdaa271 | ||
|
|
54ae6a63ee | ||
|
|
2b440bc8db | ||
|
|
27a34d5499 | ||
|
|
aa2e5a35d6 | ||
|
|
ae512f451e | ||
|
|
c88393ccfd | ||
|
|
d9655f5eb9 | ||
|
|
3cb756699f | ||
|
|
785f948bc7 | ||
|
|
38ac2c6598 | ||
|
|
a960658617 | ||
|
|
28ec7b4698 | ||
|
|
d1e88c5e8d | ||
|
|
3f21ac6b6a | ||
|
|
7a67af24f0 | ||
|
|
f36a7444d7 | ||
|
|
229733f1b0 | ||
|
|
0d66647918 | ||
|
|
14c1332017 | ||
|
|
1a2a57d59e | ||
|
|
418934644b | ||
|
|
8b435ec88f | ||
|
|
756aa3e16f | ||
|
|
1f94e0fee6 | ||
|
|
37050c49fc | ||
|
|
a36c0fcd4c | ||
|
|
d3c6ebcf15 | ||
|
|
012e124eaf | ||
|
|
9fadc78e4d | ||
|
|
6f620a6a9e | ||
|
|
21dcf73e38 | ||
|
|
18eace37f8 | ||
|
|
244d2753df | ||
|
|
452e03f9af | ||
|
|
db0a54b03f | ||
|
|
392fb3e1d7 | ||
|
|
021c94eece | ||
|
|
8398fe3bdd | ||
|
|
99a4765e75 | ||
|
|
69f729dbe5 | ||
|
|
41ecc0ad3d | ||
|
|
f9876823b8 | ||
|
|
7975bd8796 | ||
|
|
8837abc208 | ||
|
|
ad822b72c7 | ||
|
|
ec43aab999 | ||
|
|
3b5b49daac | ||
|
|
57cad14714 | ||
|
|
778ad5df3a | ||
|
|
d936be450b | ||
|
|
178d0dfa58 | ||
|
|
564a589bc6 | ||
|
|
9ceb43ec44 | ||
|
|
98596d439f | ||
|
|
f99a070735 | ||
|
|
21dfcf1e1b | ||
|
|
2f0b976bca | ||
|
|
9a5839650c | ||
|
|
deb3c31f2f | ||
|
|
2d91ff3f5d | ||
|
|
4fb026708b | ||
|
|
5b2013c037 | ||
|
|
b98bafaefe | ||
|
|
8806aac362 | ||
|
|
903d437943 | ||
|
|
024549e3b0 | ||
|
|
842c69dfdd | ||
|
|
2777e4113e | ||
|
|
8aec5244de | ||
|
|
15c8a937f4 | ||
|
|
489c913e58 | ||
|
|
d594798db8 | ||
|
|
aeaa20c3b7 | ||
|
|
46dbfa2fce | ||
|
|
530fe5e933 | ||
|
|
f499fd85d0 | ||
|
|
d3a7b2e4ca | ||
|
|
d496ea2d59 | ||
|
|
32562c6878 | ||
|
|
9e8c781871 | ||
|
|
640f758605 | ||
|
|
1903792239 | ||
|
|
329030e913 | ||
|
|
205f37fe09 | ||
|
|
8edfa4281e | ||
|
|
f5227ef982 | ||
|
|
844473e47a | ||
|
|
71608af486 | ||
|
|
8e0ef128c9 | ||
|
|
07552dddfe | ||
|
|
e1f2259e98 | ||
|
|
4925091ede | ||
|
|
09c77cfa83 | ||
|
|
c21ae0b651 | ||
|
|
049955dfd5 | ||
|
|
5359463d79 | ||
|
|
6ca39dd851 | ||
|
|
6c8e073dc8 | ||
|
|
3164ee06eb | ||
|
|
9f443d026a | ||
|
|
e783310eb4 | ||
|
|
a7dfdd48e0 | ||
|
|
9ee74253e4 | ||
|
|
b805f903c9 | ||
|
|
f7cf33b596 | ||
|
|
2a0a2d926e | ||
|
|
7439586334 | ||
|
|
0195cb31bb | ||
|
|
8f1b074595 | ||
|
|
94d49b4801 | ||
|
|
1b13b0c385 | ||
|
|
c098d0de37 | ||
|
|
69061c5629 | ||
|
|
f93eef848c | ||
|
|
f55242ad93 | ||
|
|
2d19708a41 | ||
|
|
6c97a4a6e0 | ||
|
|
9442fd4b75 | ||
|
|
78bbbb968f | ||
|
|
66640df541 | ||
|
|
f5e6091ff6 | ||
|
|
987bab9960 | ||
|
|
ba678e29fb | ||
|
|
10214a8b83 | ||
|
|
0233c96d48 | ||
|
|
6ae94d6f49 | ||
|
|
e8ddd9397d | ||
|
|
e603d9a2d0 | ||
|
|
a6443231e5 | ||
|
|
941eac848e | ||
|
|
3433a1ec7a | ||
|
|
fa2340b61e | ||
|
|
f4c46ec1c5 | ||
|
|
3bc55e0405 | ||
|
|
0b2e39e4a4 | ||
|
|
29c51c288b | ||
|
|
6f1e830aba | ||
|
|
253f3b2cd7 | ||
|
|
55e3645131 | ||
|
|
91b72ef292 | ||
|
|
695712e50c | ||
|
|
96ddfd5b65 | ||
|
|
74f4642a2c | ||
|
|
a2772db9da | ||
|
|
44a6617184 | ||
|
|
1770570921 | ||
|
|
343a091aee | ||
|
|
853280feeb | ||
|
|
6037fd74cd | ||
|
|
b18c1254a4 | ||
|
|
c3e615dfa3 | ||
|
|
d91400c3be | ||
|
|
d375ddebea | ||
|
|
894a2a4e7b | ||
|
|
63ce7d6e02 |
18
.appveyor.yml
Normal file
18
.appveyor.yml
Normal file
@@ -0,0 +1,18 @@
|
||||
shallow_clone: true
|
||||
version: '{branch}-{build}'
|
||||
cache: C:\Users\appveyor\pip\wheels
|
||||
build: off
|
||||
environment:
|
||||
PYTHON: 'C:\Python34'
|
||||
PYTHONUNBUFFERED: 1
|
||||
|
||||
install:
|
||||
- C:\Python27\python -u scripts\ci_install.py
|
||||
|
||||
test_script:
|
||||
- C:\Python34\Scripts\tox -e smoke
|
||||
- C:\Python34\Scripts\tox -e smoke-frozen
|
||||
- C:\Python34\Scripts\tox -e unittests
|
||||
- C:\Python34\Scripts\tox -e unittests-frozen
|
||||
- C:\Python34\Scripts\tox -e pyflakes
|
||||
- C:\Python34\Scripts\tox -e pylint
|
||||
@@ -3,6 +3,7 @@ branch = true
|
||||
omit =
|
||||
qutebrowser/__main__.py
|
||||
*/__init__.py
|
||||
qutebrowser/resources.py
|
||||
|
||||
[report]
|
||||
exclude_lines =
|
||||
|
||||
47
.eslintrc
Normal file
47
.eslintrc
Normal file
@@ -0,0 +1,47 @@
|
||||
# vim: ft=yaml
|
||||
|
||||
env:
|
||||
browser: true
|
||||
|
||||
rules:
|
||||
block-scoped-var: 2
|
||||
dot-location: 2
|
||||
default-case: 2
|
||||
guard-for-in: 2
|
||||
no-div-regex: 2
|
||||
no-param-reassign: 2
|
||||
no-eq-null: 2
|
||||
no-floating-decimal: 2
|
||||
no-self-compare: 2
|
||||
no-throw-literal: 2
|
||||
no-void: 2
|
||||
radix: 2
|
||||
wrap-iife: [2, "inside"]
|
||||
brace-style: [2, "1tbs", {"allowSingleLine": true}]
|
||||
comma-style: [2, "last"]
|
||||
consistent-this: [2, "self"]
|
||||
func-style: [2, "declaration"]
|
||||
indent: [2, 4, {"indentSwitchCase": true}]
|
||||
linebreak-style: [2, "unix"]
|
||||
max-nested-callbacks: [2, 3]
|
||||
no-lonely-if: 2
|
||||
no-multiple-empty-lines: [2, {"max": 2}]
|
||||
no-nested-ternary: 2
|
||||
no-unneeded-ternary: 2
|
||||
operator-assignment: [2, "always"]
|
||||
operator-linebreak: [2, "after"]
|
||||
space-after-keywords: [2, "always"]
|
||||
space-before-blocks: [2, "always"]
|
||||
space-before-function-paren: [2, {"anonymous": "never", "named": "never"}]
|
||||
space-in-brackets: [2, "never"]
|
||||
space-in-parens: [2, "never"]
|
||||
space-unary-ops: [2, {"words": true, "nonwords": false}]
|
||||
spaced-line-comment: [2, "always"]
|
||||
max-depth: [2, 5]
|
||||
max-len: [2, 79, 4]
|
||||
max-params: [2, 5]
|
||||
max-statements: [2, 30]
|
||||
no-bitwise: 2
|
||||
no-reserved-keys: 2
|
||||
global-strict: 0
|
||||
quotes: 0
|
||||
13
.flake8
13
.flake8
@@ -1,13 +0,0 @@
|
||||
# vim: ft=dosini fileencoding=utf-8:
|
||||
|
||||
[flake8]
|
||||
# E265: Block comment should start with '#'
|
||||
# E501: Line too long
|
||||
# F841: unused variable
|
||||
# F401: Unused import
|
||||
# E402: module level import not at top of file
|
||||
# E266: too many leading '#' for block comment
|
||||
# W503: line break before binary operator
|
||||
ignore=E265,E501,F841,F401,E402,E266,W503
|
||||
max_complexity = 12
|
||||
exclude=resources.py
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -21,3 +21,5 @@ __pycache__
|
||||
/.coverage
|
||||
/htmlcov
|
||||
/.tox
|
||||
/testresults.html
|
||||
/.cache
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
ignore=resources.py
|
||||
extension-pkg-whitelist=PyQt5,sip
|
||||
load-plugins=pylint_checkers.config,
|
||||
pylint_checkers.crlf,
|
||||
pylint_checkers.modeline,
|
||||
pylint_checkers.openencoding,
|
||||
pylint_checkers.settrace
|
||||
@@ -28,7 +27,8 @@ disable=no-self-use,
|
||||
broad-except,
|
||||
bare-except,
|
||||
eval-used,
|
||||
exec-used
|
||||
exec-used,
|
||||
file-ignored
|
||||
|
||||
[BASIC]
|
||||
module-rgx=(__)?[a-z][a-z0-9_]*(__)?$
|
||||
|
||||
28
.travis.yml
Normal file
28
.travis.yml
Normal file
@@ -0,0 +1,28 @@
|
||||
dist: trusty
|
||||
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
|
||||
# Not really, but this is here so we can do stuff by hand.
|
||||
language: c
|
||||
|
||||
install:
|
||||
- python scripts/ci_install.py
|
||||
|
||||
script:
|
||||
- xvfb-run -s "-screen 0 640x480x16" tox -e unittests,smoke
|
||||
- tox -e misc
|
||||
- tox -e pep257
|
||||
- tox -e pyflakes
|
||||
- tox -e pep8
|
||||
- tox -e mccabe
|
||||
- tox -e pylint
|
||||
- tox -e pyroma
|
||||
- tox -e check-manifest
|
||||
|
||||
# Travis bug - OS X builds get routed to Ubuntu Trusty if "dist: trusty" is
|
||||
# given.
|
||||
matrix:
|
||||
allow_failures:
|
||||
- os: osx
|
||||
@@ -14,6 +14,92 @@ This project adheres to http://semver.org/[Semantic Versioning].
|
||||
// `Fixed` for any bug fixes.
|
||||
// `Security` to invite users to upgrade in case of vulnerabilities.
|
||||
|
||||
v0.3.0 (unreleased)
|
||||
-------------------
|
||||
|
||||
Added
|
||||
~~~~~
|
||||
|
||||
- New commands `:message-info`, `:message-error` and `:message-warning` to show messages in the statusbar, e.g. from an userscript.
|
||||
- New command `:scroll-px` which replaces `:scroll` for pixel-exact scrolling.
|
||||
- New command `:jseval` to run a javascript snippet on the current page.
|
||||
- New (hidden) command `:follow-selected` (bound to `Enter`/`Ctrl-Enter` by default) to follow the link which is currently selected (e.g. after searching via `/`).
|
||||
- New (hidden) command `:clear-keychain` to clear a partially entered keychain (bound to `<Escape>` by default, in addition to clearing search).
|
||||
- New setting `ui -> smooth-scrolling`.
|
||||
- New setting `content -> webgl` to enable/disable https://www.khronos.org/webgl/[WebGL].
|
||||
- New setting `content -> css-regions` to enable/disable support for http://dev.w3.org/csswg/css-regions/[CSS Regions].
|
||||
- New setting `content -> hyperlink-auditing` to enable/disable support for https://html.spec.whatwg.org/multipage/semantics.html#hyperlink-auditing[hyperlink auditing].
|
||||
- New setting `tabs -> mousewheel-tab-switching` to control mousewheel behavior on the tab bar.
|
||||
- New arguments `--datadir` and `--cachedir` to set the data/cache location.
|
||||
- New arguments `--basedir` and `--temp-basedir` (intended for debugging) to set a different base directory for all data, which allows multiple invocations.
|
||||
- New argument `--no-err-windows` to suppress all error windows.
|
||||
- New arguments `--top-navigate` and `--bottom-navigate` (`-t`/`-b`) for `:scroll-page` to specify a navigation action (e.g. automatically go to the next page when arriving at the bottom).
|
||||
- New flag `-d`/`--detach` for `:spawn` to detach the spawned process so it's not closed when qutebrowser is.
|
||||
- New flag `-v`/`--verbose` for `:spawn` to print informations when the process started/exited successfully.
|
||||
- Many new color settings (foreground setting for every background setting).
|
||||
- New setting `ui -> modal-js-dialog` to use the standard modal dialogs for javascript questions instead of using the statusbar.
|
||||
- New setting `colors -> webpage.bg` to set the background color to use for websites which don't set one.
|
||||
- New setting `completion -> auto-open` to only open the completion when tab is pressed (if set to false).
|
||||
- New visual/caret mode (bound to `v`) to select text by keyboard.
|
||||
- There are now some example userscripts in `misc/userscripts`.
|
||||
- Support for Qt 5.5 and tox 2.0
|
||||
|
||||
Changed
|
||||
~~~~~~~
|
||||
|
||||
- *Breaking change for userscripts:* `QUTE_HTML` and `QUTE_TEXT` for userscripts now don't store the contents directly, and instead contain a filename.
|
||||
- The `content -> geolocation` and `notifications` settings now support a `true` value to always allow those. However, this is *not recommended*.
|
||||
- New bindings `<Ctrl-R>` (rapid), `<Ctrl-F>` (foreground) and `<Ctrl-B>` (background) to switch hint modes while hinting.
|
||||
- `<Ctrl-M>` and numpad-enter are now bound by default for bindings where `<Return>` was bound.
|
||||
- `:hint tab` and `F` now respect the `background-tabs` setting. To enforce a foreground tab (what `F` did before), use `:hint tab-fg` or `;f`.
|
||||
- `:scroll` now takes a direction argument (`up`/`down`/`left`/`right`/`top`/`bottom`/`page-up`/`page-down`) instead of two pixel arguments (`dx`/`dy`). The old form still works but is deprecated.
|
||||
- The `ui -> user-stylesheet` setting now also takes file paths relative to the config directory.
|
||||
- The `content -> cookies-accept` setting now has new `no-3rdparty` (default) and `no-unknown-3rdparty` values to block third-party cookies. The `default` value got renamed to `all`.
|
||||
- Improved startup time by reading the webpage history while qutebrowser is open.
|
||||
- The way `:spawn` splits its commandline has been changed slightly to allow commands with flags.
|
||||
- The default for the `new-instance-open-target` setting has been changed to `tab`.
|
||||
- Sessions now store zoom/scroll-position separately for each entry.
|
||||
|
||||
Deprecated
|
||||
~~~~~~~~~~
|
||||
|
||||
- `:scroll` with two pixel-arguments is now deprecated - `:scroll-px` should be used instead.
|
||||
|
||||
Removed
|
||||
~~~~~~~
|
||||
|
||||
- The `--no-crash-dialog` argument which was intended for debugging only was removed as it's replaced by `--no-err-windows` which suppresses all error windows.
|
||||
- Support for Qt installations without SSL support was dropped.
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
|
||||
- Scrolling should now work more reliably on some pages where arrow keys worked but `hjkl` didn't.
|
||||
- Small improvements when checking if an input is an URL or not.
|
||||
- Fixed wrong cursor position when completing the first item in the completion.
|
||||
- Fixed exception when using search engines with {foo} in their name.
|
||||
- Fixed a bug where the same title was shown for all tabs on some systems.
|
||||
- Don't install the scripts package when installing qutebrowser.
|
||||
- Fixed searching for terms starting with a hyphen (e.g. `/-foo`)
|
||||
- Proxy authentication credentials are now remembered between different tabs.
|
||||
- Fixed updating of the tab title on pages without title.
|
||||
- Fixed AssertionError when closing many windows quickly.
|
||||
- Various fixes for deprecated key bindings and auto-migrations.
|
||||
- Workaround for qutebrowser not starting when there are NUL-bytes in the history (because of a currently unknown bug).
|
||||
- Fixed handling of keybindings containing Ctrl/Meta on OS X.
|
||||
- Fixed crash when downloading an URL without filename (e.g. magnet links) via "Save as...".
|
||||
- Fixed exception when starting qutebrowser with `:set` as argument.
|
||||
- Fixed horrible completion performance when the `shrink` option was set.
|
||||
- Sessions now store zoom/scroll-position correctly.
|
||||
|
||||
https://github.com/The-Compiler/qutebrowser/releases/tag/v0.2.1[v0.2.1]
|
||||
-----------------------------------------------------------------------
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
|
||||
- Added missing manpage (doc/qutebrowser.1.asciidoc) to archive.
|
||||
|
||||
https://github.com/The-Compiler/qutebrowser/releases/tag/v0.2.0[v0.2.0]
|
||||
-----------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -86,14 +86,15 @@ Useful utilities
|
||||
Checkers
|
||||
~~~~~~~~
|
||||
|
||||
qutbebrowser uses http://tox.readthedocs.org/en/latest/[tox] to run its
|
||||
qutebrowser uses http://tox.readthedocs.org/en/latest/[tox] to run its
|
||||
unittests and several linters/checkers.
|
||||
|
||||
Currently, the following tools will be invoked when you run `tox`:
|
||||
|
||||
* Unit tests using the Python
|
||||
https://docs.python.org/3.4/library/unittest.html[unittest] framework
|
||||
* https://pypi.python.org/pypi/flake8/[flake8]
|
||||
* Unit tests using https://www.pytest.org[pytest].
|
||||
* https://pypi.python.org/pypi/pyflakes[pyflakes] via https://pypi.python.org/pypi/pytest-flakes[pytest-flakes]
|
||||
* https://pypi.python.org/pypi/pep8[pep8] via https://pypi.python.org/pypi/pytest-pep8[pytest-pep8]
|
||||
* https://pypi.python.org/pypi/mccabe[mccabe] via https://pypi.python.org/pypi/pytest-mccabe[pytest-mccabe]
|
||||
* https://github.com/GreenSteam/pep257/[pep257]
|
||||
* http://pylint.org/[pylint]
|
||||
* https://pypi.python.org/pypi/pyroma/[pyroma]
|
||||
@@ -152,7 +153,7 @@ Useful websites
|
||||
|
||||
Some resources which might be handy:
|
||||
|
||||
* http://qt-project.org/doc/qt-5/classes.html[The Qt5 reference]
|
||||
* http://doc.qt.io/qt-5/classes.html[The Qt5 reference]
|
||||
* https://docs.python.org/3/library/index.html[The Python reference]
|
||||
* http://httpbin.org/[httpbin, a test service for HTTP requests/responses]
|
||||
* http://requestb.in/[RequestBin, a service to inspect HTTP requests]
|
||||
@@ -210,8 +211,7 @@ Other
|
||||
Languages] (http://www.rfc-editor.org/errata_search.php?rfc=5646[Errata])
|
||||
* http://www.w3.org/TR/CSS2/[Cascading Style Sheets Level 2 Revision 1 (CSS
|
||||
2.1) Specification]
|
||||
* http://qt-project.org/doc/qt-4.8/stylesheet-reference.html[Qt Style Sheets
|
||||
Reference]
|
||||
* http://doc.qt.io/qt-5/stylesheet-reference.html[Qt Style Sheets Reference]
|
||||
* http://mimesniff.spec.whatwg.org/[MIME Sniffing Standard]
|
||||
* http://spec.whatwg.org/[WHATWG specifications]
|
||||
* http://www.w3.org/html/wg/drafts/html/master/Overview.html[HTML 5.1 Nightly]
|
||||
@@ -237,9 +237,7 @@ There are some exceptions to that:
|
||||
|
||||
* `QThread` is used instead of Python threads because it provides signals and
|
||||
slots.
|
||||
* `QProcess` is used instead of Python's `subprocess` if certain actions (e.g.
|
||||
cleanup) when the process finished are desired, as it provides signals for
|
||||
that.
|
||||
* `QProcess` is used instead of Python's `subprocess`
|
||||
* `QUrl` is used instead of storing URLs as string, see the
|
||||
<<handling-urls,handling URLs>> section for details.
|
||||
|
||||
@@ -294,8 +292,8 @@ All objects can be printed by starting with the `--debug` flag and using the
|
||||
|
||||
The registry is mainly used for <<commands,command handlers>> but also can be
|
||||
useful in places where using Qt's
|
||||
http://qt-project.org/doc/qt-5/signalsandslots.html[signals and slots]
|
||||
mechanism would be difficult.
|
||||
http://doc.qt.io/qt-5/signalsandslots.html[signals and slots] mechanism would
|
||||
be difficult.
|
||||
|
||||
Logging
|
||||
~~~~~~~
|
||||
@@ -397,13 +395,12 @@ then automatically checked. Possible values:
|
||||
e.g. `('foo', 'bar')` or `(int, 'foo')`.
|
||||
* `flag`: The flag to be used, as 1-char string (default: First char of the
|
||||
long name).
|
||||
* `name`: The long name to be used, as string (default: Name of the parameter).
|
||||
* `special`: The string `count` or `win_id` if the parameter should be
|
||||
auto-filled (with the count given by the user and the window ID the command was
|
||||
executed in, respectively).
|
||||
* `nargs`: Gets passed to argparse, see
|
||||
https://docs.python.org/dev/library/argparse.html#nargs[its documentation].
|
||||
|
||||
The name of an argument will always be the parameter name, with any trailing
|
||||
underscores stripped.
|
||||
|
||||
[[handling-urls]]
|
||||
Handling URLs
|
||||
~~~~~~~~~~~~~
|
||||
@@ -541,7 +538,7 @@ New Qt release
|
||||
|
||||
* Run all tests and check nothing is broken.
|
||||
* Check the
|
||||
https://bugreports.qt-project.org/issues/?jql=reporter%20%3D%20%22The%20Compiler%22%20ORDER%20BY%20fixVersion%20ASC[Qt bugtracker]
|
||||
https://bugreports.qt.io/issues/?jql=reporter%20%3D%20%22The%20Compiler%22%20ORDER%20BY%20fixVersion%20ASC[Qt bugtracker]
|
||||
and make sure all bugs marked as resolved are actually fixed.
|
||||
* Update own PKGBUILDs based on upstream Archlinux updates and rebuild.
|
||||
* Update recommended Qt version in `README`
|
||||
|
||||
31
FAQ.asciidoc
31
FAQ.asciidoc
@@ -4,8 +4,8 @@ The Compiler <mail@qutebrowser.org>
|
||||
|
||||
[qanda]
|
||||
What is qutebrowser based on?::
|
||||
qutebrowser uses http://www.python.org/[Python], http://qt-project.org/[Qt]
|
||||
and http://www.riverbankcomputing.com/software/pyqt/intro[PyQt].
|
||||
qutebrowser uses http://www.python.org/[Python], http://qt.io/[Qt] and
|
||||
http://www.riverbankcomputing.com/software/pyqt/intro[PyQt].
|
||||
+
|
||||
The concept of it is largely inspired by http://portix.bitbucket.org/dwb/[dwb]
|
||||
and http://www.vimperator.org/vimperator[Vimperator]. Many actions and
|
||||
@@ -15,7 +15,7 @@ Why another browser?::
|
||||
It might be hard to believe, but I didn't find any browser which I was
|
||||
happy with, so I started to write my own. Also, I needed a project to get
|
||||
into writing GUI applications with Python and
|
||||
link:http://qt-project.org/[Qt]/link:http://www.riverbankcomputing.com/software/pyqt/intro[PyQt].
|
||||
link:http://qt.io/[Qt]/link:http://www.riverbankcomputing.com/software/pyqt/intro[PyQt].
|
||||
+
|
||||
Read the next few questions to find out why I was unhappy with existing
|
||||
software.
|
||||
@@ -32,12 +32,11 @@ API] seems to lack basic features like proxy support, and almost no projects
|
||||
seem to have started porting to WebKit2 (I only know of
|
||||
http://www.uzbl.org/[uzbl]).
|
||||
+
|
||||
qutebrowser uses http://qt-project.org/[Qt] and
|
||||
http://qt-project.org/wiki/QtWebKit[QtWebKit] instead, which suffers from far
|
||||
less such crashes. It might switch to
|
||||
http://qt-project.org/wiki/QtWebEngine[QtWebEngine] in the future, which is
|
||||
based on Google's https://en.wikipedia.org/wiki/Blink_(layout_engine)[Blink]
|
||||
rendering engine.
|
||||
qutebrowser uses http://qt.io/[Qt] and http://wiki.qt.io/QtWebKit[QtWebKit]
|
||||
instead, which suffers from far less such crashes. It might switch to
|
||||
http://wiki.qt.io/QtWebEngine[QtWebEngine] in the future, which is based on
|
||||
Google's https://en.wikipedia.org/wiki/Blink_(layout_engine)[Blink] rendering
|
||||
engine.
|
||||
|
||||
What's wrong with https://www.mozilla.org/en-US/firefox/new/[Firefox] and link:http://5digits.org/pentadactyl/[Pentadactyl]/link:http://www.vimperator.org/vimperator[Vimperator]?::
|
||||
Firefox likes to break compatibility with addons on each upgrade, gets
|
||||
@@ -54,10 +53,10 @@ What's wrong with http://www.chromium.org/Home[Chromium] and https://vimium.gith
|
||||
|
||||
Why Python?::
|
||||
I enjoy writing Python since 2011, which made it one of the possible
|
||||
choices. I wanted to use http://qt-project.org/[Qt] because of
|
||||
http://qt-project.org/wiki/QtWebKit[QtWebKit] so I didn't have
|
||||
http://qt-project.org/wiki/Category:LanguageBindings[many other choices]. I
|
||||
don't like C++ and can't write it very well, so that wasn't an alternative.
|
||||
choices. I wanted to use http://qt.io/[Qt] because of
|
||||
http://wiki.qt.io/QtWebKit[QtWebKit] so I didn't have
|
||||
http://wiki.qt.io/Category:LanguageBindings[many other choices]. I don't
|
||||
like C++ and can't write it very well, so that wasn't an alternative.
|
||||
|
||||
But isn't Python too slow for a browser?::
|
||||
http://www.infoworld.com/d/application-development/van-rossum-python-not-too-slow-188715[No.]
|
||||
@@ -112,10 +111,10 @@ Experiencing segfaults (crashes) on Debian systems.::
|
||||
|
||||
Segfaults on Facebook, Medium, Amazon, ...::
|
||||
If you are on a Debian or Ubuntu based system, you might experience some crashes
|
||||
visting these sites. This is caused by a known bug in Qt which has been
|
||||
visting these sites. This is caused by various bugs in Qt which have been
|
||||
fixed in Qt 5.4. However Debian and Ubuntu are slow to adopt or upgrade
|
||||
some packages. There is currently no easy way to manually upgrade to Qt
|
||||
5.4 on those systems.
|
||||
some packages. On Debian Jessie, it's recommended to use the experimental
|
||||
repos as described in https://github.com/The-Compiler/qutebrowser/blob/master/INSTALL.asciidoc#on-debian--ubuntu[INSTALL].
|
||||
+
|
||||
Since Ubuntu Trusty (using Qt 5.2.1),
|
||||
https://bugreports.qt.io/browse/QTBUG-42417?jql=component%20%3D%20WebKit%20and%20resolution%20%3D%20Done%20and%20fixVersion%20in%20(5.3.0%2C%20%225.3.0%20Alpha%22%2C%20%225.3.0%20Beta1%22%2C%20%225.3.0%20RC1%22%2C%205.3.1%2C%205.3.2%2C%205.4.0%2C%20%225.4.0%20Alpha%22%2C%20%225.4.0%20Beta%22%2C%20%225.4.0%20RC%22)%20and%20priority%20in%20(%22P2%3A%20Important%22%2C%20%22P1%3A%20Critical%22%2C%20%22P0%3A%20Blocker%22)[over
|
||||
|
||||
@@ -12,6 +12,39 @@ qutebrowser should run on these systems:
|
||||
|
||||
Install the dependencies via apt-get:
|
||||
|
||||
[NOTE]
|
||||
==========================
|
||||
On Debian, it's recommended to install the Qt packages from the
|
||||
https://wiki.debian.org/DebianExperimental[experimental] repository as those
|
||||
are a much newer version of Qt which is more stable.
|
||||
|
||||
Add the following line to your `/etc/apt/sources.list`:
|
||||
|
||||
----
|
||||
deb http://ftp.debian.org/debian experimental main
|
||||
----
|
||||
|
||||
Then install the packages like this:
|
||||
|
||||
----
|
||||
# apt-get update
|
||||
# apt-get install -t experimental python3-pyqt5 python3-pyqt5.qtwebkit
|
||||
# apt-get install python-tox
|
||||
----
|
||||
|
||||
It's also recommended to pin those packages to receive updates by creating a
|
||||
file `/etc/apt/preferences.d/qutebrowser` with the following contents:
|
||||
|
||||
----
|
||||
Package: python3-pyqt5* libqt5*
|
||||
Pin: release a=experimental
|
||||
Pin-Priority: 800
|
||||
----
|
||||
==========================
|
||||
|
||||
For distributions other than Debian or if you prefer to not use the
|
||||
experimental repo:
|
||||
|
||||
----
|
||||
# apt-get install python3-pyqt5 python3-pyqt5.qtwebkit python-tox
|
||||
----
|
||||
@@ -51,13 +84,22 @@ There are two Archlinux packages available in the AUR:
|
||||
https://aur.archlinux.org/packages/qutebrowser/[qutebrowser] and
|
||||
https://aur.archlinux.org/packages/qutebrowser-git/[qutebrowser-git].
|
||||
|
||||
You can install them like this:
|
||||
You can install them (and the needed pypeg2 dependency) like this:
|
||||
|
||||
----
|
||||
$ mkdir qutebrowser
|
||||
$ cd qutebrowser
|
||||
$ wget https://aur.archlinux.org/packages/qu/qutebrowser-git/PKGBUILD
|
||||
$ wget https://aur.archlinux.org/packages/py/python-pypeg2/python-pypeg2.tar.gz
|
||||
$ tar xzf python-pypeg2.tar.gz
|
||||
$ cd python-pypeg2
|
||||
$ makepkg -si
|
||||
$ cd ..
|
||||
$ rm -r python-pypeg2 python-pypeg2.tar.gz
|
||||
|
||||
$ wget https://aur.archlinux.org/packages/qu/qutebrowser/qutebrowser.tar.gz
|
||||
$ tar xzf qutebrowser.tar.gz
|
||||
$ cd qutebrowser
|
||||
$ makepkg -si
|
||||
$ cd ..
|
||||
$ rm -r qutebrowser qutebrowser.tar.gz
|
||||
----
|
||||
|
||||
or you could use an AUR helper, e.g. `yaourt -S qutebrowser-git`.
|
||||
|
||||
25
MANIFEST.in
25
MANIFEST.in
@@ -1,11 +1,13 @@
|
||||
global-exclude __pycache__ *.pyc *.pyo
|
||||
|
||||
recursive-include qutebrowser *.py
|
||||
recursive-include qutebrowser/html *.html
|
||||
recursive-include qutebrowser/test *.py
|
||||
recursive-include qutebrowser/javascript *.js
|
||||
graft icons
|
||||
graft scripts/pylint_checkers
|
||||
graft doc/img
|
||||
graft misc
|
||||
graft scripts
|
||||
include qutebrowser/utils/testfile
|
||||
include qutebrowser/git-commit-id
|
||||
include COPYING doc/* README.asciidoc CONTRIBUTING.asciidoc FAQ.asciidoc INSTALL.asciidoc CHANGELOG.asciidoc
|
||||
@@ -14,19 +16,24 @@ include requirements.txt
|
||||
include tox.ini
|
||||
include qutebrowser.py
|
||||
|
||||
exclude scripts/cleanup.py
|
||||
exclude scripts/minimal_webkit_testbrowser.py
|
||||
exclude scripts/run_profile.py
|
||||
exclude scripts/src2asciidoc.sh
|
||||
exclude scripts/gen_resources.sh
|
||||
exclude scripts/quit_segfault_test.sh
|
||||
exclude scripts/segfault_test.sh
|
||||
include scripts/__init__.py
|
||||
include scripts/hostblock_blame.py
|
||||
include scripts/importer.py
|
||||
include scripts/keytester.py
|
||||
include scripts/link_pyqt.py
|
||||
include scripts/minimal_webkit_testbrowser.py
|
||||
include scripts/setupcommon.py
|
||||
include scripts/utils.py
|
||||
|
||||
exclude doc/notes
|
||||
recursive-exclude doc *.asciidoc
|
||||
include doc/qutebrowser.1.asciidoc
|
||||
prune tests
|
||||
exclude qutebrowser.rcc
|
||||
exclude .coveragerc
|
||||
exclude .flake8
|
||||
exclude .pylintrc
|
||||
exclude .eslintrc
|
||||
exclude doc/help
|
||||
exclude .appveyor.yml
|
||||
exclude .travis.yml
|
||||
exclude misc/appveyor_install.py
|
||||
|
||||
@@ -68,7 +68,7 @@ Contributions / Bugs
|
||||
--------------------
|
||||
|
||||
You want to contribute to qutebrowser? Awesome! Please read
|
||||
link:doc/CONTRIBUTING.asciidoc[the contribution guidelines] for details and
|
||||
link:CONTRIBUTING.asciidoc[the contribution guidelines] for details and
|
||||
useful hints.
|
||||
|
||||
If you found a bug or have a feature request, you can report it in several
|
||||
@@ -89,10 +89,10 @@ Requirements
|
||||
The following software and libraries are required to run qutebrowser:
|
||||
|
||||
* http://www.python.org/[Python] 3.4
|
||||
* http://qt-project.org/[Qt] 5.2.0 or newer (5.4.1 recommended)
|
||||
* http://qt.io/[Qt] 5.2.0 or newer (5.4.2 recommended)
|
||||
* QtWebKit
|
||||
* http://www.riverbankcomputing.com/software/pyqt/intro[PyQt] 5.2.0 or newer
|
||||
(5.4.1 recommended) for Python 3
|
||||
(5.4.2 recommended) for Python 3
|
||||
* https://pypi.python.org/pypi/setuptools/[pkg_resources/setuptools]
|
||||
* http://fdik.org/pyPEG/[pyPEG2]
|
||||
* http://jinja.pocoo.org/[jinja2]
|
||||
@@ -135,24 +135,33 @@ Contributors, sorted by the number of commits in descending order:
|
||||
// QUTE_AUTHORS_START
|
||||
* Florian Bruhin
|
||||
* Bruno Oliveira
|
||||
* Joel Torstensson
|
||||
* Raphael Pierzina
|
||||
* Joel Torstensson
|
||||
* Martin Tournoij
|
||||
* Claude
|
||||
* Lamar Pavel
|
||||
* Austin Anderson
|
||||
* Artur Shaik
|
||||
* Antoni Boucher
|
||||
* ZDarian
|
||||
* Peter Vilim
|
||||
* John ShaggyTwoDope Jenkins
|
||||
* Jimmy
|
||||
* Zach-Button
|
||||
* rikn00
|
||||
* Patric Schmitz
|
||||
* Martin Zimmermann
|
||||
* Error 800
|
||||
* Brian Jackson
|
||||
* sbinix
|
||||
* Tobias Patzl
|
||||
* Johannes Altmanninger
|
||||
* Samir Benmendil
|
||||
* Regina Hug
|
||||
* Mathias Fussenegger
|
||||
* Larry Hynes
|
||||
* Fritz V155 Reichwald
|
||||
* Franz Fellner
|
||||
* error800
|
||||
* Thorsten Wißmann
|
||||
* Thiago Barroso Perrotta
|
||||
@@ -160,7 +169,6 @@ Contributors, sorted by the number of commits in descending order:
|
||||
* Helen Sherwood-Taylor
|
||||
* HalosGhost
|
||||
* Gregor Pohl
|
||||
* Franz Fellner
|
||||
* Eivind Uggedal
|
||||
* Andreas Fischer
|
||||
// QUTE_AUTHORS_END
|
||||
@@ -214,7 +222,7 @@ Also, thanks to:
|
||||
|
||||
* Everyone who had the patience to test qutebrowser before v0.1.
|
||||
* Everyone triaging/fixing my bugs in the
|
||||
https://bugreports.qt-project.org/secure/Dashboard.jspa[Qt bugtracker]
|
||||
https://bugreports.qt.io/secure/Dashboard.jspa[Qt bugtracker]
|
||||
* Everyone answering my questions on http://stackoverflow.com/[Stack Overflow]
|
||||
and in IRC.
|
||||
* All the projects which were a great help while developing qutebrowser.
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
|<<hint,hint>>|Start hinting.
|
||||
|<<home,home>>|Open main startpage in current tab.
|
||||
|<<inspector,inspector>>|Toggle the web inspector.
|
||||
|<<jseval,jseval>>|Evaluate a JavaScript string.
|
||||
|<<later,later>>|Execute a command after some time.
|
||||
|<<navigate,navigate>>|Open typical prev/next links or navigate using the URL path.
|
||||
|<<open,open>>|Open a URL in the current/[count]th tab.
|
||||
@@ -198,7 +199,9 @@ Start hinting.
|
||||
* +'target'+: What to do with the selected element.
|
||||
|
||||
- `normal`: Open the link in the current tab.
|
||||
- `tab`: Open the link in a new tab.
|
||||
- `tab`: Open the link in a new tab (honoring the
|
||||
background-tabs setting).
|
||||
- `tab-fg`: Open the link in a new foreground tab.
|
||||
- `tab-bg`: Open the link in a new background tab.
|
||||
- `window`: Open the link in a new window.
|
||||
- `hover` : Hover over the link.
|
||||
@@ -227,8 +230,8 @@ Start hinting.
|
||||
|
||||
|
||||
==== optional arguments
|
||||
* +*-r*+, +*--rapid*+: Whether to do rapid hinting. This is only possible with targets `tab-bg`, `window`, `run`, `hover`, `userscript` and
|
||||
`spawn`.
|
||||
* +*-r*+, +*--rapid*+: Whether to do rapid hinting. This is only possible with targets `tab` (with background-tabs=true), `tab-bg`,
|
||||
`window`, `run`, `hover`, `userscript` and `spawn`.
|
||||
|
||||
|
||||
[[home]]
|
||||
@@ -239,6 +242,22 @@ Open main startpage in current tab.
|
||||
=== inspector
|
||||
Toggle the web inspector.
|
||||
|
||||
[[jseval]]
|
||||
=== jseval
|
||||
Syntax: +:jseval [*--quiet*] 'js-code'+
|
||||
|
||||
Evaluate a JavaScript string.
|
||||
|
||||
==== positional arguments
|
||||
* +'js-code'+: The string to evaluate.
|
||||
|
||||
==== optional arguments
|
||||
* +*-q*+, +*--quiet*+: Don't show resulting JS object.
|
||||
|
||||
==== note
|
||||
* This command does not split arguments after the last argument and handles quotes literally.
|
||||
* With this command, +;;+ is interpreted literally instead of splitting off a second command.
|
||||
|
||||
[[later]]
|
||||
=== later
|
||||
Syntax: +:later 'ms' 'command'+
|
||||
@@ -510,17 +529,23 @@ Preset the statusbar to some text.
|
||||
|
||||
[[spawn]]
|
||||
=== spawn
|
||||
Syntax: +:spawn [*--userscript*] 'args' ['args' ...]+
|
||||
Syntax: +:spawn [*--userscript*] [*--verbose*] [*--detach*] 'cmdline'+
|
||||
|
||||
Spawn a command in a shell.
|
||||
|
||||
Note the {url} variable which gets replaced by the current URL might be useful here.
|
||||
|
||||
==== positional arguments
|
||||
* +'args'+: The commandline to execute.
|
||||
* +'cmdline'+: The commandline to execute.
|
||||
|
||||
==== optional arguments
|
||||
* +*-u*+, +*--userscript*+: Run the command as an userscript.
|
||||
* +*-v*+, +*--verbose*+: Show notifications when the command started/exited.
|
||||
* +*-d*+, +*--detach*+: Whether the command should be detached from qutebrowser.
|
||||
|
||||
==== note
|
||||
* This command does not split arguments after the last argument and handles quotes literally.
|
||||
* With this command, +;;+ is interpreted literally instead of splitting off a second command.
|
||||
|
||||
[[stop]]
|
||||
=== stop
|
||||
@@ -639,13 +664,14 @@ Save open pages and quit.
|
||||
|
||||
[[yank]]
|
||||
=== yank
|
||||
Syntax: +:yank [*--title*] [*--sel*]+
|
||||
Syntax: +:yank [*--title*] [*--sel*] [*--domain*]+
|
||||
|
||||
Yank the current URL/title to the clipboard or primary selection.
|
||||
|
||||
==== optional arguments
|
||||
* +*-t*+, +*--title*+: Yank the title instead of the URL.
|
||||
* +*-s*+, +*--sel*+: Use the primary selection instead of the clipboard.
|
||||
* +*-d*+, +*--domain*+: Yank only the scheme, domain, and port number.
|
||||
|
||||
[[zoom]]
|
||||
=== zoom
|
||||
@@ -681,14 +707,35 @@ How many steps to zoom out.
|
||||
[options="header",width="75%",cols="25%,75%"]
|
||||
|==============
|
||||
|Command|Description
|
||||
|<<clear-keychain,clear-keychain>>|Clear the currently entered key chain.
|
||||
|<<command-accept,command-accept>>|Execute the command currently in the commandline.
|
||||
|<<command-history-next,command-history-next>>|Go forward in the commandline history.
|
||||
|<<command-history-prev,command-history-prev>>|Go back in the commandline history.
|
||||
|<<completion-item-next,completion-item-next>>|Select the next completion item.
|
||||
|<<completion-item-prev,completion-item-prev>>|Select the previous completion item.
|
||||
|<<drop-selection,drop-selection>>|Drop selection and keep selection mode enabled.
|
||||
|<<enter-mode,enter-mode>>|Enter a key mode.
|
||||
|<<follow-hint,follow-hint>>|Follow the currently selected hint.
|
||||
|<<follow-selected,follow-selected>>|Follow the selected text.
|
||||
|<<leave-mode,leave-mode>>|Leave the mode we're currently in.
|
||||
|<<message-error,message-error>>|Show an error message in the statusbar.
|
||||
|<<message-info,message-info>>|Show an info message in the statusbar.
|
||||
|<<message-warning,message-warning>>|Show a warning message in the statusbar.
|
||||
|<<move-to-end-of-document,move-to-end-of-document>>|Move the cursor or selection to the end of the document.
|
||||
|<<move-to-end-of-line,move-to-end-of-line>>|Move the cursor or selection to the end of line.
|
||||
|<<move-to-end-of-next-block,move-to-end-of-next-block>>|Move the cursor or selection to the end of next block.
|
||||
|<<move-to-end-of-prev-block,move-to-end-of-prev-block>>|Move the cursor or selection to the end of previous block.
|
||||
|<<move-to-end-of-word,move-to-end-of-word>>|Move the cursor or selection to the end of the word.
|
||||
|<<move-to-next-char,move-to-next-char>>|Move the cursor or selection to the next char.
|
||||
|<<move-to-next-line,move-to-next-line>>|Move the cursor or selection to the next line.
|
||||
|<<move-to-next-word,move-to-next-word>>|Move the cursor or selection to the next word.
|
||||
|<<move-to-prev-char,move-to-prev-char>>|Move the cursor or selection to the previous char.
|
||||
|<<move-to-prev-line,move-to-prev-line>>|Move the cursor or selection to the prev line.
|
||||
|<<move-to-prev-word,move-to-prev-word>>|Move the cursor or selection to the previous word.
|
||||
|<<move-to-start-of-document,move-to-start-of-document>>|Move the cursor or selection to the start of the document.
|
||||
|<<move-to-start-of-line,move-to-start-of-line>>|Move the cursor or selection to the start of the line.
|
||||
|<<move-to-start-of-next-block,move-to-start-of-next-block>>|Move the cursor or selection to the start of next block.
|
||||
|<<move-to-start-of-prev-block,move-to-start-of-prev-block>>|Move the cursor or selection to the start of previous block.
|
||||
|<<open-editor,open-editor>>|Open an external editor with the currently selected form field.
|
||||
|<<prompt-accept,prompt-accept>>|Accept the current prompt.
|
||||
|<<prompt-no,prompt-no>>|Answer no to a yes/no prompt.
|
||||
@@ -706,12 +753,19 @@ How many steps to zoom out.
|
||||
|<<rl-unix-line-discard,rl-unix-line-discard>>|Remove chars backward from the cursor to the beginning of the line.
|
||||
|<<rl-unix-word-rubout,rl-unix-word-rubout>>|Remove chars from the cursor to the beginning of the word.
|
||||
|<<rl-yank,rl-yank>>|Paste the most recently deleted text.
|
||||
|<<scroll,scroll>>|Scroll the current tab by 'count * dx/dy'.
|
||||
|<<scroll,scroll>>|Scroll the current tab in the given direction.
|
||||
|<<scroll-page,scroll-page>>|Scroll the frame page-wise.
|
||||
|<<scroll-perc,scroll-perc>>|Scroll to a specific percentage of the page.
|
||||
|<<scroll-px,scroll-px>>|Scroll the current tab by 'count * dx/dy' pixels.
|
||||
|<<search-next,search-next>>|Continue the search to the ([count]th) next term.
|
||||
|<<search-prev,search-prev>>|Continue the search to the ([count]th) previous term.
|
||||
|<<toggle-selection,toggle-selection>>|Toggle caret selection mode.
|
||||
|<<yank-selected,yank-selected>>|Yank the selected text to the clipboard or primary selection.
|
||||
|==============
|
||||
[[clear-keychain]]
|
||||
=== clear-keychain
|
||||
Clear the currently entered key chain.
|
||||
|
||||
[[command-accept]]
|
||||
=== command-accept
|
||||
Execute the command currently in the commandline.
|
||||
@@ -732,6 +786,10 @@ Select the next completion item.
|
||||
=== completion-item-prev
|
||||
Select the previous completion item.
|
||||
|
||||
[[drop-selection]]
|
||||
=== drop-selection
|
||||
Drop selection and keep selection mode enabled.
|
||||
|
||||
[[enter-mode]]
|
||||
=== enter-mode
|
||||
Syntax: +:enter-mode 'mode'+
|
||||
@@ -745,10 +803,139 @@ Enter a key mode.
|
||||
=== follow-hint
|
||||
Follow the currently selected hint.
|
||||
|
||||
[[follow-selected]]
|
||||
=== follow-selected
|
||||
Syntax: +:follow-selected [*--tab*]+
|
||||
|
||||
Follow the selected text.
|
||||
|
||||
==== optional arguments
|
||||
* +*-t*+, +*--tab*+: Load the selected link in a new tab.
|
||||
|
||||
[[leave-mode]]
|
||||
=== leave-mode
|
||||
Leave the mode we're currently in.
|
||||
|
||||
[[message-error]]
|
||||
=== message-error
|
||||
Syntax: +:message-error 'text'+
|
||||
|
||||
Show an error message in the statusbar.
|
||||
|
||||
==== positional arguments
|
||||
* +'text'+: The text to show.
|
||||
|
||||
[[message-info]]
|
||||
=== message-info
|
||||
Syntax: +:message-info 'text'+
|
||||
|
||||
Show an info message in the statusbar.
|
||||
|
||||
==== positional arguments
|
||||
* +'text'+: The text to show.
|
||||
|
||||
[[message-warning]]
|
||||
=== message-warning
|
||||
Syntax: +:message-warning 'text'+
|
||||
|
||||
Show a warning message in the statusbar.
|
||||
|
||||
==== positional arguments
|
||||
* +'text'+: The text to show.
|
||||
|
||||
[[move-to-end-of-document]]
|
||||
=== move-to-end-of-document
|
||||
Move the cursor or selection to the end of the document.
|
||||
|
||||
[[move-to-end-of-line]]
|
||||
=== move-to-end-of-line
|
||||
Move the cursor or selection to the end of line.
|
||||
|
||||
[[move-to-end-of-next-block]]
|
||||
=== move-to-end-of-next-block
|
||||
Move the cursor or selection to the end of next block.
|
||||
|
||||
==== count
|
||||
How many blocks to move.
|
||||
|
||||
[[move-to-end-of-prev-block]]
|
||||
=== move-to-end-of-prev-block
|
||||
Move the cursor or selection to the end of previous block.
|
||||
|
||||
==== count
|
||||
How many blocks to move.
|
||||
|
||||
[[move-to-end-of-word]]
|
||||
=== move-to-end-of-word
|
||||
Move the cursor or selection to the end of the word.
|
||||
|
||||
==== count
|
||||
How many words to move.
|
||||
|
||||
[[move-to-next-char]]
|
||||
=== move-to-next-char
|
||||
Move the cursor or selection to the next char.
|
||||
|
||||
==== count
|
||||
How many lines to move.
|
||||
|
||||
[[move-to-next-line]]
|
||||
=== move-to-next-line
|
||||
Move the cursor or selection to the next line.
|
||||
|
||||
==== count
|
||||
How many lines to move.
|
||||
|
||||
[[move-to-next-word]]
|
||||
=== move-to-next-word
|
||||
Move the cursor or selection to the next word.
|
||||
|
||||
==== count
|
||||
How many words to move.
|
||||
|
||||
[[move-to-prev-char]]
|
||||
=== move-to-prev-char
|
||||
Move the cursor or selection to the previous char.
|
||||
|
||||
==== count
|
||||
How many chars to move.
|
||||
|
||||
[[move-to-prev-line]]
|
||||
=== move-to-prev-line
|
||||
Move the cursor or selection to the prev line.
|
||||
|
||||
==== count
|
||||
How many lines to move.
|
||||
|
||||
[[move-to-prev-word]]
|
||||
=== move-to-prev-word
|
||||
Move the cursor or selection to the previous word.
|
||||
|
||||
==== count
|
||||
How many words to move.
|
||||
|
||||
[[move-to-start-of-document]]
|
||||
=== move-to-start-of-document
|
||||
Move the cursor or selection to the start of the document.
|
||||
|
||||
[[move-to-start-of-line]]
|
||||
=== move-to-start-of-line
|
||||
Move the cursor or selection to the start of the line.
|
||||
|
||||
[[move-to-start-of-next-block]]
|
||||
=== move-to-start-of-next-block
|
||||
Move the cursor or selection to the start of next block.
|
||||
|
||||
==== count
|
||||
How many blocks to move.
|
||||
|
||||
[[move-to-start-of-prev-block]]
|
||||
=== move-to-start-of-prev-block
|
||||
Move the cursor or selection to the start of previous block.
|
||||
|
||||
==== count
|
||||
How many blocks to move.
|
||||
|
||||
[[open-editor]]
|
||||
=== open-editor
|
||||
Open an external editor with the currently selected form field.
|
||||
@@ -847,20 +1034,20 @@ This acts like readline's yank.
|
||||
|
||||
[[scroll]]
|
||||
=== scroll
|
||||
Syntax: +:scroll 'dx' 'dy'+
|
||||
Syntax: +:scroll 'direction' ['dy']+
|
||||
|
||||
Scroll the current tab by 'count * dx/dy'.
|
||||
Scroll the current tab in the given direction.
|
||||
|
||||
==== positional arguments
|
||||
* +'dx'+: How much to scroll in x-direction.
|
||||
* +'dy'+: How much to scroll in x-direction.
|
||||
* +'direction'+: In which direction to scroll (up/down/left/right/top/bottom).
|
||||
|
||||
|
||||
==== count
|
||||
multiplier
|
||||
|
||||
[[scroll-page]]
|
||||
=== scroll-page
|
||||
Syntax: +:scroll-page 'x' 'y'+
|
||||
Syntax: +:scroll-page [*--top-navigate* 'ACTION'] [*--bottom-navigate* 'ACTION'] 'x' 'y'+
|
||||
|
||||
Scroll the frame page-wise.
|
||||
|
||||
@@ -868,6 +1055,12 @@ Scroll the frame page-wise.
|
||||
* +'x'+: How many pages to scroll to the right.
|
||||
* +'y'+: How many pages to scroll down.
|
||||
|
||||
==== optional arguments
|
||||
* +*-t*+, +*--top-navigate*+: :navigate action (prev, decrement) to run when scrolling up at the top of the page.
|
||||
|
||||
* +*-b*+, +*--bottom-navigate*+: :navigate action (next, increment) to run when scrolling down at the bottom of the page.
|
||||
|
||||
|
||||
==== count
|
||||
multiplier
|
||||
|
||||
@@ -888,6 +1081,19 @@ The percentage can be given either as argument or as count. If no percentage is
|
||||
==== count
|
||||
Percentage to scroll.
|
||||
|
||||
[[scroll-px]]
|
||||
=== scroll-px
|
||||
Syntax: +:scroll-px 'dx' 'dy'+
|
||||
|
||||
Scroll the current tab by 'count * dx/dy' pixels.
|
||||
|
||||
==== positional arguments
|
||||
* +'dx'+: How much to scroll in x-direction.
|
||||
* +'dy'+: How much to scroll in x-direction.
|
||||
|
||||
==== count
|
||||
multiplier
|
||||
|
||||
[[search-next]]
|
||||
=== search-next
|
||||
Continue the search to the ([count]th) next term.
|
||||
@@ -902,6 +1108,20 @@ Continue the search to the ([count]th) previous term.
|
||||
==== count
|
||||
How many elements to ignore.
|
||||
|
||||
[[toggle-selection]]
|
||||
=== toggle-selection
|
||||
Toggle caret selection mode.
|
||||
|
||||
[[yank-selected]]
|
||||
=== yank-selected
|
||||
Syntax: +:yank-selected [*--sel*] [*--keep*]+
|
||||
|
||||
Yank the selected text to the clipboard or primary selection.
|
||||
|
||||
==== optional arguments
|
||||
* +*-s*+, +*--sel*+: Use the primary selection instead of the clipboard.
|
||||
* +*-k*+, +*--keep*+: If given, stay in visual mode after yanking.
|
||||
|
||||
|
||||
== Debugging commands
|
||||
These commands are mainly intended for debugging. They are hidden if qutebrowser was started without the `--debug`-flag.
|
||||
@@ -916,6 +1136,7 @@ These commands are mainly intended for debugging. They are hidden if qutebrowser
|
||||
|<<debug-crash,debug-crash>>|Crash for debugging purposes.
|
||||
|<<debug-pyeval,debug-pyeval>>|Evaluate a python string and display the results as a web page.
|
||||
|<<debug-trace,debug-trace>>|Trace executed code via hunter.
|
||||
|<<debug-webaction,debug-webaction>>|Execute a webaction.
|
||||
|==============
|
||||
[[debug-all-objects]]
|
||||
=== debug-all-objects
|
||||
@@ -964,3 +1185,17 @@ Trace executed code via hunter.
|
||||
* This command does not split arguments after the last argument and handles quotes literally.
|
||||
* With this command, +;;+ is interpreted literally instead of splitting off a second command.
|
||||
|
||||
[[debug-webaction]]
|
||||
=== debug-webaction
|
||||
Syntax: +:debug-webaction 'action'+
|
||||
|
||||
Execute a webaction.
|
||||
|
||||
See http://doc.qt.io/qt-5/qwebpage.html#WebAction-enum for the available actions.
|
||||
|
||||
==== positional arguments
|
||||
* +'action'+: The action to execute, e.g. MoveToNextChar.
|
||||
|
||||
==== count
|
||||
How many times to repeat the action.
|
||||
|
||||
|
||||
@@ -38,12 +38,14 @@
|
||||
|<<ui-display-statusbar-messages,display-statusbar-messages>>|Whether to display javascript statusbar messages.
|
||||
|<<ui-zoom-text-only,zoom-text-only>>|Whether the zoom factor on a frame applies only to the text or to all content.
|
||||
|<<ui-frame-flattening,frame-flattening>>|Whether to expand each subframe to its contents.
|
||||
|<<ui-user-stylesheet,user-stylesheet>>|User stylesheet to use (absolute filename or CSS string). Will expand environment variables.
|
||||
|<<ui-user-stylesheet,user-stylesheet>>|User stylesheet to use (absolute filename, filename relative to the config directory or CSS string). Will expand environment variables.
|
||||
|<<ui-css-media-type,css-media-type>>|Set the CSS media type.
|
||||
|<<ui-smooth-scrolling,smooth-scrolling>>|Whether to enable smooth scrolling for webpages.
|
||||
|<<ui-remove-finished-downloads,remove-finished-downloads>>|Whether to remove finished downloads automatically.
|
||||
|<<ui-hide-statusbar,hide-statusbar>>|Whether to hide the statusbar unless a message is shown.
|
||||
|<<ui-window-title-format,window-title-format>>|The format to use for the window title. The following placeholders are defined:
|
||||
|<<ui-hide-mouse-cursor,hide-mouse-cursor>>|Whether to hide the mouse cursor.
|
||||
|<<ui-modal-js-dialog,modal-js-dialog>>|Use standard JavaScript modal dialog for alert() and confirm()
|
||||
|==============
|
||||
|
||||
.Quick reference for section ``network''
|
||||
@@ -63,6 +65,7 @@
|
||||
[options="header",width="75%",cols="25%,75%"]
|
||||
|==============
|
||||
|Setting|Description
|
||||
|<<completion-auto-open,auto-open>>|Automatically open completion when typing.
|
||||
|<<completion-download-path-suggestion,download-path-suggestion>>|What to display in the download filename input.
|
||||
|<<completion-timestamp-format,timestamp-format>>|How to format timestamps (e.g. for history)
|
||||
|<<completion-show,show>>|Whether to show the autocompletion window.
|
||||
@@ -97,7 +100,7 @@
|
||||
|<<tabs-select-on-remove,select-on-remove>>|Which tab to select when the focused tab is removed.
|
||||
|<<tabs-new-tab-position,new-tab-position>>|How new tabs are positioned.
|
||||
|<<tabs-new-tab-position-explicit,new-tab-position-explicit>>|How new tabs opened explicitly are positioned.
|
||||
|<<tabs-last-close,last-close>>|Behaviour when the last tab is closed.
|
||||
|<<tabs-last-close,last-close>>|Behavior when the last tab is closed.
|
||||
|<<tabs-hide-auto,hide-auto>>|Hide the tab bar if only one tab is open.
|
||||
|<<tabs-hide-always,hide-always>>|Always hide the tab bar.
|
||||
|<<tabs-wrap,wrap>>|Whether to wrap when changing tabs.
|
||||
@@ -110,6 +113,7 @@
|
||||
|<<tabs-indicator-space,indicator-space>>|Spacing between tab edge and indicator.
|
||||
|<<tabs-tabs-are-windows,tabs-are-windows>>|Whether to open windows instead of tabs.
|
||||
|<<tabs-title-format,title-format>>|The format to use for the tab title. The following placeholders are defined:
|
||||
|<<tabs-mousewheel-tab-switching,mousewheel-tab-switching>>|Switch between tabs using the mouse wheel.
|
||||
|==============
|
||||
|
||||
.Quick reference for section ``storage''
|
||||
@@ -134,6 +138,9 @@
|
||||
|<<content-allow-images,allow-images>>|Whether images are automatically loaded in web pages.
|
||||
|<<content-allow-javascript,allow-javascript>>|Enables or disables the running of JavaScript programs.
|
||||
|<<content-allow-plugins,allow-plugins>>|Enables or disables plugins in Web pages.
|
||||
|<<content-webgl,webgl>>|Enables or disables WebGL.
|
||||
|<<content-css-regions,css-regions>>|Enable or disable support for CSS regions.
|
||||
|<<content-hyperlink-auditing,hyperlink-auditing>>|Enable or disable hyperlink auditing (<a ping>).
|
||||
|<<content-geolocation,geolocation>>|Allow websites to request geolocations.
|
||||
|<<content-notifications,notifications>>|Allow websites to show notifications.
|
||||
|<<content-javascript-can-open-windows,javascript-can-open-windows>>|Whether JavaScript programs can open new windows.
|
||||
@@ -143,7 +150,7 @@
|
||||
|<<content-ignore-javascript-alert,ignore-javascript-alert>>|Whether all javascript alerts should be ignored.
|
||||
|<<content-local-content-can-access-remote-urls,local-content-can-access-remote-urls>>|Whether locally loaded documents are allowed to access remote urls.
|
||||
|<<content-local-content-can-access-file-urls,local-content-can-access-file-urls>>|Whether locally loaded documents are allowed to access other local urls.
|
||||
|<<content-cookies-accept,cookies-accept>>|Whether to accept cookies.
|
||||
|<<content-cookies-accept,cookies-accept>>|Control which cookies to accept.
|
||||
|<<content-cookies-store,cookies-store>>|Whether to store cookies.
|
||||
|<<content-host-block-lists,host-block-lists>>|List of URLs of lists which contain hosts to block.
|
||||
|<<content-host-blocking-enabled,host-blocking-enabled>>|Whether host blocking is enabled.
|
||||
@@ -181,12 +188,22 @@
|
||||
|<<colors-completion.item.selected.border.top,completion.item.selected.border.top>>|Top border color of the completion widget category headers.
|
||||
|<<colors-completion.item.selected.border.bottom,completion.item.selected.border.bottom>>|Bottom border color of the selected completion item.
|
||||
|<<colors-completion.match.fg,completion.match.fg>>|Foreground color of the matched text in the completion.
|
||||
|<<colors-statusbar.bg,statusbar.bg>>|Foreground color of the statusbar.
|
||||
|<<colors-statusbar.fg,statusbar.fg>>|Foreground color of the statusbar.
|
||||
|<<colors-statusbar.bg,statusbar.bg>>|Foreground color of the statusbar.
|
||||
|<<colors-statusbar.fg.error,statusbar.fg.error>>|Foreground color of the statusbar if there was an error.
|
||||
|<<colors-statusbar.bg.error,statusbar.bg.error>>|Background color of the statusbar if there was an error.
|
||||
|<<colors-statusbar.fg.warning,statusbar.fg.warning>>|Foreground color of the statusbar if there is a warning.
|
||||
|<<colors-statusbar.bg.warning,statusbar.bg.warning>>|Background color of the statusbar if there is a warning.
|
||||
|<<colors-statusbar.fg.prompt,statusbar.fg.prompt>>|Foreground color of the statusbar if there is a prompt.
|
||||
|<<colors-statusbar.bg.prompt,statusbar.bg.prompt>>|Background color of the statusbar if there is a prompt.
|
||||
|<<colors-statusbar.fg.insert,statusbar.fg.insert>>|Foreground color of the statusbar in insert mode.
|
||||
|<<colors-statusbar.bg.insert,statusbar.bg.insert>>|Background color of the statusbar in insert mode.
|
||||
|<<colors-statusbar.fg.command,statusbar.fg.command>>|Foreground color of the statusbar in command mode.
|
||||
|<<colors-statusbar.bg.command,statusbar.bg.command>>|Background color of the statusbar in command mode.
|
||||
|<<colors-statusbar.fg.caret,statusbar.fg.caret>>|Foreground color of the statusbar in caret mode.
|
||||
|<<colors-statusbar.bg.caret,statusbar.bg.caret>>|Background color of the statusbar in caret mode.
|
||||
|<<colors-statusbar.fg.caret-selection,statusbar.fg.caret-selection>>|Foreground color of the statusbar in caret mode with a selection
|
||||
|<<colors-statusbar.bg.caret-selection,statusbar.bg.caret-selection>>|Background color of the statusbar in caret mode with a selection
|
||||
|<<colors-statusbar.progress.bg,statusbar.progress.bg>>|Background color of the progress bar.
|
||||
|<<colors-statusbar.url.fg,statusbar.url.fg>>|Default foreground color of the URL in the statusbar.
|
||||
|<<colors-statusbar.url.fg.success,statusbar.url.fg.success>>|Foreground color of the URL in the statusbar on successful load.
|
||||
@@ -194,10 +211,10 @@
|
||||
|<<colors-statusbar.url.fg.warn,statusbar.url.fg.warn>>|Foreground color of the URL in the statusbar when there's a warning.
|
||||
|<<colors-statusbar.url.fg.hover,statusbar.url.fg.hover>>|Foreground color of the URL in the statusbar for hovered links.
|
||||
|<<colors-tabs.fg.odd,tabs.fg.odd>>|Foreground color of unselected odd tabs.
|
||||
|<<colors-tabs.fg.even,tabs.fg.even>>|Foreground color of unselected even tabs.
|
||||
|<<colors-tabs.fg.selected,tabs.fg.selected>>|Foreground color of selected tabs.
|
||||
|<<colors-tabs.bg.odd,tabs.bg.odd>>|Background color of unselected odd tabs.
|
||||
|<<colors-tabs.fg.even,tabs.fg.even>>|Foreground color of unselected even tabs.
|
||||
|<<colors-tabs.bg.even,tabs.bg.even>>|Background color of unselected even tabs.
|
||||
|<<colors-tabs.fg.selected,tabs.fg.selected>>|Foreground color of selected tabs.
|
||||
|<<colors-tabs.bg.selected,tabs.bg.selected>>|Background color of selected tabs.
|
||||
|<<colors-tabs.bg.bar,tabs.bg.bar>>|Background color of the tab bar.
|
||||
|<<colors-tabs.indicator.start,tabs.indicator.start>>|Color gradient start for the tab indicator.
|
||||
@@ -205,14 +222,18 @@
|
||||
|<<colors-tabs.indicator.error,tabs.indicator.error>>|Color for the tab indicator on errors..
|
||||
|<<colors-tabs.indicator.system,tabs.indicator.system>>|Color gradient interpolation system for the tab indicator.
|
||||
|<<colors-hints.fg,hints.fg>>|Font color for hints.
|
||||
|<<colors-hints.fg.match,hints.fg.match>>|Font color for the matched part of hints.
|
||||
|<<colors-hints.bg,hints.bg>>|Background color for hints.
|
||||
|<<colors-downloads.fg,downloads.fg>>|Foreground color for downloads.
|
||||
|<<colors-hints.fg.match,hints.fg.match>>|Font color for the matched part of hints.
|
||||
|<<colors-downloads.bg.bar,downloads.bg.bar>>|Background color for the download bar.
|
||||
|<<colors-downloads.bg.start,downloads.bg.start>>|Color gradient start for downloads.
|
||||
|<<colors-downloads.bg.stop,downloads.bg.stop>>|Color gradient end for downloads.
|
||||
|<<colors-downloads.bg.system,downloads.bg.system>>|Color gradient interpolation system for downloads.
|
||||
|<<colors-downloads.fg.start,downloads.fg.start>>|Color gradient start for download text.
|
||||
|<<colors-downloads.bg.start,downloads.bg.start>>|Color gradient start for download backgrounds.
|
||||
|<<colors-downloads.fg.stop,downloads.fg.stop>>|Color gradient end for download text.
|
||||
|<<colors-downloads.bg.stop,downloads.bg.stop>>|Color gradient stop for download backgrounds.
|
||||
|<<colors-downloads.fg.system,downloads.fg.system>>|Color gradient interpolation system for download text.
|
||||
|<<colors-downloads.bg.system,downloads.bg.system>>|Color gradient interpolation system for download backgrounds.
|
||||
|<<colors-downloads.fg.error,downloads.fg.error>>|Foreground color for downloads with errors.
|
||||
|<<colors-downloads.bg.error,downloads.bg.error>>|Background color for downloads with errors.
|
||||
|<<colors-webpage.bg,webpage.bg>>|Background color for webpages if unset (or empty to use the theme's color)
|
||||
|==============
|
||||
|
||||
.Quick reference for section ``fonts''
|
||||
@@ -392,13 +413,13 @@ How to open links in an existing instance if a new one is launched.
|
||||
|
||||
Valid values:
|
||||
|
||||
* +tab+: Open a new tab in the existing window and activate it.
|
||||
* +tab-bg+: Open a new background tab in the existing window and activate it.
|
||||
* +tab-silent+: Open a new tab in the existing window without activating it.
|
||||
* +tab-bg-silent+: Open a new background tab in the existing window without activating it.
|
||||
* +tab+: Open a new tab in the existing window and activate the window.
|
||||
* +tab-bg+: Open a new background tab in the existing window and activate the window.
|
||||
* +tab-silent+: Open a new tab in the existing window without activating the window.
|
||||
* +tab-bg-silent+: Open a new background tab in the existing window without activating the window.
|
||||
* +window+: Open in a new window.
|
||||
|
||||
Default: +pass:[window]+
|
||||
Default: +pass:[tab]+
|
||||
|
||||
[[general-log-javascript-console]]
|
||||
=== log-javascript-console
|
||||
@@ -521,7 +542,7 @@ Default: +pass:[false]+
|
||||
|
||||
[[ui-user-stylesheet]]
|
||||
=== user-stylesheet
|
||||
User stylesheet to use (absolute filename or CSS string). Will expand environment variables.
|
||||
User stylesheet to use (absolute filename, filename relative to the config directory or CSS string). Will expand environment variables.
|
||||
|
||||
Default: +pass:[::-webkit-scrollbar { width: 0px; height: 0px; }]+
|
||||
|
||||
@@ -531,6 +552,17 @@ Set the CSS media type.
|
||||
|
||||
Default: empty
|
||||
|
||||
[[ui-smooth-scrolling]]
|
||||
=== smooth-scrolling
|
||||
Whether to enable smooth scrolling for webpages.
|
||||
|
||||
Valid values:
|
||||
|
||||
* +true+
|
||||
* +false+
|
||||
|
||||
Default: +pass:[false]+
|
||||
|
||||
[[ui-remove-finished-downloads]]
|
||||
=== remove-finished-downloads
|
||||
Whether to remove finished downloads automatically.
|
||||
@@ -576,6 +608,17 @@ Valid values:
|
||||
|
||||
Default: +pass:[false]+
|
||||
|
||||
[[ui-modal-js-dialog]]
|
||||
=== modal-js-dialog
|
||||
Use standard JavaScript modal dialog for alert() and confirm()
|
||||
|
||||
Valid values:
|
||||
|
||||
* +true+
|
||||
* +false+
|
||||
|
||||
Default: +pass:[false]+
|
||||
|
||||
== network
|
||||
Settings related to the network.
|
||||
|
||||
@@ -652,6 +695,17 @@ Default: +pass:[true]+
|
||||
== completion
|
||||
Options related to completion and command history.
|
||||
|
||||
[[completion-auto-open]]
|
||||
=== auto-open
|
||||
Automatically open completion when typing.
|
||||
|
||||
Valid values:
|
||||
|
||||
* +true+
|
||||
* +false+
|
||||
|
||||
Default: +pass:[true]+
|
||||
|
||||
[[completion-download-path-suggestion]]
|
||||
=== download-path-suggestion
|
||||
What to display in the download filename input.
|
||||
@@ -880,7 +934,7 @@ Default: +pass:[last]+
|
||||
|
||||
[[tabs-last-close]]
|
||||
=== last-close
|
||||
Behaviour when the last tab is closed.
|
||||
Behavior when the last tab is closed.
|
||||
|
||||
Valid values:
|
||||
|
||||
@@ -1014,6 +1068,17 @@ The format to use for the tab title. The following placeholders are defined:
|
||||
|
||||
Default: +pass:[{index}: {title}]+
|
||||
|
||||
[[tabs-mousewheel-tab-switching]]
|
||||
=== mousewheel-tab-switching
|
||||
Switch between tabs using the mouse wheel.
|
||||
|
||||
Valid values:
|
||||
|
||||
* +true+
|
||||
* +false+
|
||||
|
||||
Default: +pass:[true]+
|
||||
|
||||
== storage
|
||||
Settings related to cache and storage.
|
||||
|
||||
@@ -1138,12 +1203,46 @@ Valid values:
|
||||
|
||||
Default: +pass:[false]+
|
||||
|
||||
[[content-webgl]]
|
||||
=== webgl
|
||||
Enables or disables WebGL.
|
||||
|
||||
Valid values:
|
||||
|
||||
* +true+
|
||||
* +false+
|
||||
|
||||
Default: +pass:[true]+
|
||||
|
||||
[[content-css-regions]]
|
||||
=== css-regions
|
||||
Enable or disable support for CSS regions.
|
||||
|
||||
Valid values:
|
||||
|
||||
* +true+
|
||||
* +false+
|
||||
|
||||
Default: +pass:[true]+
|
||||
|
||||
[[content-hyperlink-auditing]]
|
||||
=== hyperlink-auditing
|
||||
Enable or disable hyperlink auditing (<a ping>).
|
||||
|
||||
Valid values:
|
||||
|
||||
* +true+
|
||||
* +false+
|
||||
|
||||
Default: +pass:[false]+
|
||||
|
||||
[[content-geolocation]]
|
||||
=== geolocation
|
||||
Allow websites to request geolocations.
|
||||
|
||||
Valid values:
|
||||
|
||||
* +true+
|
||||
* +false+
|
||||
* +ask+
|
||||
|
||||
@@ -1155,6 +1254,7 @@ Allow websites to show notifications.
|
||||
|
||||
Valid values:
|
||||
|
||||
* +true+
|
||||
* +false+
|
||||
* +ask+
|
||||
|
||||
@@ -1239,14 +1339,16 @@ Default: +pass:[true]+
|
||||
|
||||
[[content-cookies-accept]]
|
||||
=== cookies-accept
|
||||
Whether to accept cookies.
|
||||
Control which cookies to accept.
|
||||
|
||||
Valid values:
|
||||
|
||||
* +default+: Default QtWebKit behavior.
|
||||
* +all+: Accept all cookies.
|
||||
* +no-3rdparty+: Accept cookies from the same origin only.
|
||||
* +no-unknown-3rdparty+: Accept cookies from the same origin only, unless a cookie is already set for the domain.
|
||||
* +never+: Don't accept cookies at all.
|
||||
|
||||
Default: +pass:[default]+
|
||||
Default: +pass:[no-3rdparty]+
|
||||
|
||||
[[content-cookies-store]]
|
||||
=== cookies-store
|
||||
@@ -1357,7 +1459,7 @@ Default: +pass:[true]+
|
||||
=== next-regexes
|
||||
A comma-separated list of regexes to use for 'next' links.
|
||||
|
||||
Default: +pass:[\bnext\b,\bmore\b,\bnewer\b,\b[>→≫]\b,\b(>>|»)\b]+
|
||||
Default: +pass:[\bnext\b,\bmore\b,\bnewer\b,\b[>→≫]\b,\b(>>|»)\b,\bcontinue\b]+
|
||||
|
||||
[[hints-prev-regexes]]
|
||||
=== prev-regexes
|
||||
@@ -1384,7 +1486,9 @@ A value can be in one of the following format:
|
||||
* transparent (no color)
|
||||
* `rgb(r, g, b)` / `rgba(r, g, b, a)` (values 0-255 or percentages)
|
||||
* `hsv(h, s, v)` / `hsva(h, s, v, a)` (values 0-255, hue 0-359)
|
||||
* A gradient as explained in http://qt-project.org/doc/qt-4.8/stylesheet-reference.html#list-of-property-types[the Qt documentation] under ``Gradient''.
|
||||
* A gradient as explained in http://doc.qt.io/qt-5/stylesheet-reference.html#list-of-property-types[the Qt documentation] under ``Gradient''.
|
||||
|
||||
A *.system value determines the color system to use for color interpolation between similarly-named *.start and *.stop entries, regardless of how they are defined in the options. Valid values are 'rgb', 'hsv', and 'hsl'.
|
||||
|
||||
The `hints.*` values are a special case as they're real CSS colors, not Qt-CSS colors. There, for a gradient, you need to use `-webkit-gradient`, see https://www.webkit.org/blog/175/introducing-css-gradients/[the WebKit documentation].
|
||||
|
||||
@@ -1460,17 +1564,23 @@ Foreground color of the matched text in the completion.
|
||||
|
||||
Default: +pass:[#ff4444]+
|
||||
|
||||
[[colors-statusbar.fg]]
|
||||
=== statusbar.fg
|
||||
Foreground color of the statusbar.
|
||||
|
||||
Default: +pass:[white]+
|
||||
|
||||
[[colors-statusbar.bg]]
|
||||
=== statusbar.bg
|
||||
Foreground color of the statusbar.
|
||||
|
||||
Default: +pass:[black]+
|
||||
|
||||
[[colors-statusbar.fg]]
|
||||
=== statusbar.fg
|
||||
Foreground color of the statusbar.
|
||||
[[colors-statusbar.fg.error]]
|
||||
=== statusbar.fg.error
|
||||
Foreground color of the statusbar if there was an error.
|
||||
|
||||
Default: +pass:[white]+
|
||||
Default: +pass:[${statusbar.fg}]+
|
||||
|
||||
[[colors-statusbar.bg.error]]
|
||||
=== statusbar.bg.error
|
||||
@@ -1478,24 +1588,78 @@ Background color of the statusbar if there was an error.
|
||||
|
||||
Default: +pass:[red]+
|
||||
|
||||
[[colors-statusbar.fg.warning]]
|
||||
=== statusbar.fg.warning
|
||||
Foreground color of the statusbar if there is a warning.
|
||||
|
||||
Default: +pass:[${statusbar.fg}]+
|
||||
|
||||
[[colors-statusbar.bg.warning]]
|
||||
=== statusbar.bg.warning
|
||||
Background color of the statusbar if there is a warning.
|
||||
|
||||
Default: +pass:[darkorange]+
|
||||
|
||||
[[colors-statusbar.fg.prompt]]
|
||||
=== statusbar.fg.prompt
|
||||
Foreground color of the statusbar if there is a prompt.
|
||||
|
||||
Default: +pass:[${statusbar.fg}]+
|
||||
|
||||
[[colors-statusbar.bg.prompt]]
|
||||
=== statusbar.bg.prompt
|
||||
Background color of the statusbar if there is a prompt.
|
||||
|
||||
Default: +pass:[darkblue]+
|
||||
|
||||
[[colors-statusbar.fg.insert]]
|
||||
=== statusbar.fg.insert
|
||||
Foreground color of the statusbar in insert mode.
|
||||
|
||||
Default: +pass:[${statusbar.fg}]+
|
||||
|
||||
[[colors-statusbar.bg.insert]]
|
||||
=== statusbar.bg.insert
|
||||
Background color of the statusbar in insert mode.
|
||||
|
||||
Default: +pass:[darkgreen]+
|
||||
|
||||
[[colors-statusbar.fg.command]]
|
||||
=== statusbar.fg.command
|
||||
Foreground color of the statusbar in command mode.
|
||||
|
||||
Default: +pass:[${statusbar.fg}]+
|
||||
|
||||
[[colors-statusbar.bg.command]]
|
||||
=== statusbar.bg.command
|
||||
Background color of the statusbar in command mode.
|
||||
|
||||
Default: +pass:[${statusbar.bg}]+
|
||||
|
||||
[[colors-statusbar.fg.caret]]
|
||||
=== statusbar.fg.caret
|
||||
Foreground color of the statusbar in caret mode.
|
||||
|
||||
Default: +pass:[${statusbar.fg}]+
|
||||
|
||||
[[colors-statusbar.bg.caret]]
|
||||
=== statusbar.bg.caret
|
||||
Background color of the statusbar in caret mode.
|
||||
|
||||
Default: +pass:[purple]+
|
||||
|
||||
[[colors-statusbar.fg.caret-selection]]
|
||||
=== statusbar.fg.caret-selection
|
||||
Foreground color of the statusbar in caret mode with a selection
|
||||
|
||||
Default: +pass:[${statusbar.fg}]+
|
||||
|
||||
[[colors-statusbar.bg.caret-selection]]
|
||||
=== statusbar.bg.caret-selection
|
||||
Background color of the statusbar in caret mode with a selection
|
||||
|
||||
Default: +pass:[#a12dff]+
|
||||
|
||||
[[colors-statusbar.progress.bg]]
|
||||
=== statusbar.progress.bg
|
||||
Background color of the progress bar.
|
||||
@@ -1538,30 +1702,30 @@ Foreground color of unselected odd tabs.
|
||||
|
||||
Default: +pass:[white]+
|
||||
|
||||
[[colors-tabs.fg.even]]
|
||||
=== tabs.fg.even
|
||||
Foreground color of unselected even tabs.
|
||||
|
||||
Default: +pass:[white]+
|
||||
|
||||
[[colors-tabs.fg.selected]]
|
||||
=== tabs.fg.selected
|
||||
Foreground color of selected tabs.
|
||||
|
||||
Default: +pass:[white]+
|
||||
|
||||
[[colors-tabs.bg.odd]]
|
||||
=== tabs.bg.odd
|
||||
Background color of unselected odd tabs.
|
||||
|
||||
Default: +pass:[grey]+
|
||||
|
||||
[[colors-tabs.fg.even]]
|
||||
=== tabs.fg.even
|
||||
Foreground color of unselected even tabs.
|
||||
|
||||
Default: +pass:[white]+
|
||||
|
||||
[[colors-tabs.bg.even]]
|
||||
=== tabs.bg.even
|
||||
Background color of unselected even tabs.
|
||||
|
||||
Default: +pass:[darkgrey]+
|
||||
|
||||
[[colors-tabs.fg.selected]]
|
||||
=== tabs.fg.selected
|
||||
Foreground color of selected tabs.
|
||||
|
||||
Default: +pass:[white]+
|
||||
|
||||
[[colors-tabs.bg.selected]]
|
||||
=== tabs.bg.selected
|
||||
Background color of selected tabs.
|
||||
@@ -1610,23 +1774,17 @@ Font color for hints.
|
||||
|
||||
Default: +pass:[black]+
|
||||
|
||||
[[colors-hints.fg.match]]
|
||||
=== hints.fg.match
|
||||
Font color for the matched part of hints.
|
||||
|
||||
Default: +pass:[green]+
|
||||
|
||||
[[colors-hints.bg]]
|
||||
=== hints.bg
|
||||
Background color for hints.
|
||||
|
||||
Default: +pass:[-webkit-gradient(linear, left top, left bottom, color-stop(0%,#FFF785), color-stop(100%,#FFC542))]+
|
||||
|
||||
[[colors-downloads.fg]]
|
||||
=== downloads.fg
|
||||
Foreground color for downloads.
|
||||
[[colors-hints.fg.match]]
|
||||
=== hints.fg.match
|
||||
Font color for the matched part of hints.
|
||||
|
||||
Default: +pass:[#ffffff]+
|
||||
Default: +pass:[green]+
|
||||
|
||||
[[colors-downloads.bg.bar]]
|
||||
=== downloads.bg.bar
|
||||
@@ -1634,21 +1792,33 @@ Background color for the download bar.
|
||||
|
||||
Default: +pass:[black]+
|
||||
|
||||
[[colors-downloads.fg.start]]
|
||||
=== downloads.fg.start
|
||||
Color gradient start for download text.
|
||||
|
||||
Default: +pass:[white]+
|
||||
|
||||
[[colors-downloads.bg.start]]
|
||||
=== downloads.bg.start
|
||||
Color gradient start for downloads.
|
||||
Color gradient start for download backgrounds.
|
||||
|
||||
Default: +pass:[#0000aa]+
|
||||
|
||||
[[colors-downloads.fg.stop]]
|
||||
=== downloads.fg.stop
|
||||
Color gradient end for download text.
|
||||
|
||||
Default: +pass:[${downloads.fg.start}]+
|
||||
|
||||
[[colors-downloads.bg.stop]]
|
||||
=== downloads.bg.stop
|
||||
Color gradient end for downloads.
|
||||
Color gradient stop for download backgrounds.
|
||||
|
||||
Default: +pass:[#00aa00]+
|
||||
|
||||
[[colors-downloads.bg.system]]
|
||||
=== downloads.bg.system
|
||||
Color gradient interpolation system for downloads.
|
||||
[[colors-downloads.fg.system]]
|
||||
=== downloads.fg.system
|
||||
Color gradient interpolation system for download text.
|
||||
|
||||
Valid values:
|
||||
|
||||
@@ -1658,12 +1828,36 @@ Valid values:
|
||||
|
||||
Default: +pass:[rgb]+
|
||||
|
||||
[[colors-downloads.bg.system]]
|
||||
=== downloads.bg.system
|
||||
Color gradient interpolation system for download backgrounds.
|
||||
|
||||
Valid values:
|
||||
|
||||
* +rgb+: Interpolate in the RGB color system.
|
||||
* +hsv+: Interpolate in the HSV color system.
|
||||
* +hsl+: Interpolate in the HSL color system.
|
||||
|
||||
Default: +pass:[rgb]+
|
||||
|
||||
[[colors-downloads.fg.error]]
|
||||
=== downloads.fg.error
|
||||
Foreground color for downloads with errors.
|
||||
|
||||
Default: +pass:[white]+
|
||||
|
||||
[[colors-downloads.bg.error]]
|
||||
=== downloads.bg.error
|
||||
Background color for downloads with errors.
|
||||
|
||||
Default: +pass:[red]+
|
||||
|
||||
[[colors-webpage.bg]]
|
||||
=== webpage.bg
|
||||
Background color for webpages if unset (or empty to use the theme's color)
|
||||
|
||||
Default: +pass:[white]+
|
||||
|
||||
== fonts
|
||||
Fonts used for the UI, with optional style/weight/size.
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ What to do now
|
||||
* View the http://qutebrowser.org/img/cheatsheet-big.png[key binding cheatsheet]
|
||||
to make yourself familiar with the key bindings: +
|
||||
image:http://qutebrowser.org/img/cheatsheet-small.png["qutebrowser key binding cheatsheet",link="http://qutebrowser.org/img/cheatsheet-big.png"]
|
||||
* Run `:adblock-update` to download adblock lists and activate adblocking.
|
||||
* If you just cloned the repository, you'll need to run
|
||||
`scripts/asciidoc2html.py` to generate the documentation.
|
||||
* Go to the link:qute://settings[settings page] to set up qutebrowser the way you want it.
|
||||
|
||||
@@ -41,6 +41,15 @@ show it.
|
||||
*-c* 'CONFDIR', *--confdir* 'CONFDIR'::
|
||||
Set config directory (empty for no config storage).
|
||||
|
||||
*--datadir* 'DATADIR'::
|
||||
Set data directory (empty for no data storage).
|
||||
|
||||
*--cachedir* 'CACHEDIR'::
|
||||
Set cache directory (empty for no cache storage).
|
||||
|
||||
*--basedir* 'BASEDIR'::
|
||||
Base directory for all storage. Other --*dir arguments are ignored if this is given.
|
||||
|
||||
*-V*, *--version*::
|
||||
Show version and quit.
|
||||
|
||||
@@ -81,12 +90,15 @@ show it.
|
||||
*--debug-exit*::
|
||||
Turn on debugging of late exit.
|
||||
|
||||
*--no-crash-dialog*::
|
||||
Don't show a crash dialog.
|
||||
|
||||
*--pdb-postmortem*::
|
||||
Drop into pdb on exceptions.
|
||||
|
||||
*--temp-basedir*::
|
||||
Use a temporary basedir.
|
||||
|
||||
*--no-err-windows*::
|
||||
Don't show any error windows (used for tests/smoke.py).
|
||||
|
||||
*--qt-name* 'NAME'::
|
||||
Set the window name.
|
||||
|
||||
|
||||
@@ -24,8 +24,8 @@ The following environment variables will be set when an userscript is launched:
|
||||
command or key binding).
|
||||
- `QUTE_USER_AGENT`: The currently set user agent.
|
||||
- `QUTE_FIFO`: The FIFO or file to write commands to.
|
||||
- `QUTE_HTML`: The HTML source of the current page.
|
||||
- `QUTE_TEXT`: The plaintext of the current page.
|
||||
- `QUTE_HTML`: Path of a file containing the HTML source of the current page.
|
||||
- `QUTE_TEXT`: Path of a file containing the plaintext of the current page.
|
||||
|
||||
In `command` mode:
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
height="640"
|
||||
id="svg2"
|
||||
sodipodi:version="0.32"
|
||||
inkscape:version="0.91 r13725"
|
||||
inkscape:version="0.48.5 r10040"
|
||||
version="1.0"
|
||||
sodipodi:docname="cheatsheet.svg"
|
||||
inkscape:output_extension="org.inkscape.output.svg.inkscape"
|
||||
@@ -33,16 +33,16 @@
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.8791156"
|
||||
inkscape:cx="327.65084"
|
||||
inkscape:cy="233.0095"
|
||||
inkscape:cx="768.67127"
|
||||
inkscape:cy="133.80749"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
width="1024px"
|
||||
height="640px"
|
||||
showgrid="false"
|
||||
inkscape:window-width="1366"
|
||||
inkscape:window-height="768"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-width="636"
|
||||
inkscape:window-height="536"
|
||||
inkscape:window-x="2560"
|
||||
inkscape:window-y="0"
|
||||
showguides="true"
|
||||
inkscape:guide-bbox="true"
|
||||
@@ -1939,7 +1939,7 @@
|
||||
x="542.06946"
|
||||
sodipodi:role="line"
|
||||
id="tspan4938"
|
||||
style="font-size:8px">scoll</tspan><tspan
|
||||
style="font-size:8px">scroll</tspan><tspan
|
||||
y="276.1955"
|
||||
x="542.06946"
|
||||
sodipodi:role="line"
|
||||
@@ -2999,6 +2999,8 @@
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara3626-73">;b - open hint in background tab</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara4051">;f - open hint in foreground tab</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara3788">;h - hover over hint (mouse-over)</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara3790">;i - hint images</flowPara><flowPara
|
||||
@@ -3324,27 +3326,15 @@
|
||||
style="font-size:8px">tab</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:8px;line-height:89.99999762%;font-family:TlwgTypewriter;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none"
|
||||
x="267.67316"
|
||||
y="326.20523"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:8px;line-height:89.99999762%;font-family:TlwgTypewriter;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#ff0000;fill-opacity:1;stroke:none"
|
||||
x="274.21381"
|
||||
y="343.17578"
|
||||
id="text10547-23-6-7"
|
||||
sodipodi:linespacing="89.999998%"><tspan
|
||||
sodipodi:role="line"
|
||||
x="267.67316"
|
||||
y="326.20523"
|
||||
id="tspan10560-1-3-1" /><tspan
|
||||
sodipodi:role="line"
|
||||
x="267.67316"
|
||||
y="333.40524"
|
||||
id="tspan5325">co: close</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="267.67316"
|
||||
y="340.60522"
|
||||
id="tspan10562-12-5-98">other tabs</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="267.67316"
|
||||
y="347.80524"
|
||||
id="tspan4045">cd: clea</tspan></text>
|
||||
x="274.21381"
|
||||
y="343.17578"
|
||||
id="tspan4052">(10)</tspan></text>
|
||||
<text
|
||||
sodipodi:linespacing="89.999998%"
|
||||
id="text10564-6-7-8-0"
|
||||
@@ -3469,5 +3459,20 @@
|
||||
y="177.63554"
|
||||
style="font-size:8px"
|
||||
id="tspan3719">cache)</tspan></text>
|
||||
<text
|
||||
sodipodi:linespacing="89.999998%"
|
||||
id="text9514-60-7-7-0-8"
|
||||
y="338.04874"
|
||||
x="342.42523"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:8px;line-height:89.99999762%;font-family:TlwgTypewriter;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none"
|
||||
xml:space="preserve"><tspan
|
||||
y="338.04874"
|
||||
x="342.42523"
|
||||
sodipodi:role="line"
|
||||
id="tspan5689-6">visual</tspan><tspan
|
||||
y="345.24875"
|
||||
x="342.42523"
|
||||
sodipodi:role="line"
|
||||
id="tspan4112">mode</tspan></text>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 137 KiB |
48
misc/userscripts/dmenu_qutebrowser
Executable file
48
misc/userscripts/dmenu_qutebrowser
Executable file
@@ -0,0 +1,48 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Copyright 2015 Zach-Button <zachrey.button@gmail.com>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
# qutebrowser is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# qutebrowser is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# Pipes history, quickmarks, and URL into dmenu.
|
||||
#
|
||||
# If run from qutebrowser as a userscript, it runs :open on the URL
|
||||
# If not, it opens a new qutebrowser window at the URL
|
||||
#
|
||||
# Ideal for use with tabs-are-windows. Set a hotkey to launch this script, then:
|
||||
# :bind o spawn --userscript dmenu_qutebrowser
|
||||
#
|
||||
# Use the hotkey to open in new tab/window, press 'o' to open URL in current tab/window
|
||||
# You can simulate "go" by pressing "o<tab>", as the current URL is always first in the list
|
||||
#
|
||||
# I personally use "<Mod4>o" to launch this script. For me, my workflow is:
|
||||
# Default keys Keys with this script
|
||||
# O <Mod4>o
|
||||
# o o
|
||||
# go o<Tab>
|
||||
# gO gC, then o<Tab>
|
||||
# (This is unnecessarily long. I use this rarely, feel free to make this script accept parameters.)
|
||||
#
|
||||
|
||||
[ -z "$QUTE_URL" ] && QUTE_URL='http://google.com'
|
||||
|
||||
url=$(echo "$QUTE_URL" | cat - ~/.config/qutebrowser/quickmarks ~/.local/share/qutebrowser/history | dmenu -l 15 -p qutebrowser)
|
||||
url=$(echo $url | sed -E 's/[^ ]+ +//g' | egrep "https?:" || echo $url)
|
||||
|
||||
[ -z "${url// }" ] && exit
|
||||
|
||||
echo "open $url" >> "$QUTE_FIFO" || qutebrowser "$url"
|
||||
|
||||
32
misc/userscripts/qutebrowser_viewsource
Executable file
32
misc/userscripts/qutebrowser_viewsource
Executable file
@@ -0,0 +1,32 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Copyright 2015 Zach-Button <zachrey.button@gmail.com>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
# qutebrowser is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# qutebrowser is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
#
|
||||
# This script fetches the unprocessed HTML source for a page and opens it in vim.
|
||||
# :bind gf spawn --userscript qutebrowser_viewsource
|
||||
#
|
||||
# Caveat: Does not use authentication of any kind. Add it in if you want it to.
|
||||
#
|
||||
|
||||
path=/tmp/qutebrowser_$(mktemp XXXXXXXX).html
|
||||
|
||||
curl "$QUTE_URL" > $path
|
||||
urxvt -e vim "$path"
|
||||
|
||||
rm "$path"
|
||||
@@ -28,7 +28,7 @@ __copyright__ = "Copyright 2014-2015 Florian Bruhin (The Compiler)"
|
||||
__license__ = "GPL"
|
||||
__maintainer__ = __author__
|
||||
__email__ = "mail@qutebrowser.org"
|
||||
__version_info__ = (0, 2, 1)
|
||||
__version_info__ = (0, 3, 0)
|
||||
__version__ = '.'.join(map(str, __version_info__))
|
||||
__description__ = "A keyboard-driven, vim-like browser based on PyQt5 and QtWebKit."
|
||||
|
||||
|
||||
1207
qutebrowser/app.py
1207
qutebrowser/app.py
File diff suppressed because it is too large
Load Diff
@@ -27,7 +27,7 @@ import zipfile
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import objreg, standarddir, log, message
|
||||
from qutebrowser.commands import cmdutils
|
||||
from qutebrowser.commands import cmdutils, cmdexc
|
||||
|
||||
|
||||
def guess_zip_filename(zf):
|
||||
@@ -90,12 +90,18 @@ class HostBlocker:
|
||||
self.blocked_hosts = set()
|
||||
self._in_progress = []
|
||||
self._done_count = 0
|
||||
self._hosts_file = os.path.join(standarddir.data(), 'blocked-hosts')
|
||||
data_dir = standarddir.data()
|
||||
if data_dir is None:
|
||||
self._hosts_file = None
|
||||
else:
|
||||
self._hosts_file = os.path.join(data_dir, 'blocked-hosts')
|
||||
objreg.get('config').changed.connect(self.on_config_changed)
|
||||
|
||||
def read_hosts(self):
|
||||
"""Read hosts from the existing blocked-hosts file."""
|
||||
self.blocked_hosts = set()
|
||||
if self._hosts_file is None:
|
||||
return
|
||||
if os.path.exists(self._hosts_file):
|
||||
try:
|
||||
with open(self._hosts_file, 'r', encoding='utf-8') as f:
|
||||
@@ -104,13 +110,17 @@ class HostBlocker:
|
||||
except OSError:
|
||||
log.misc.exception("Failed to read host blocklist!")
|
||||
else:
|
||||
if config.get('content', 'host-block-lists') is not None:
|
||||
args = objreg.get('args')
|
||||
if (config.get('content', 'host-block-lists') is not None and
|
||||
args.basedir is None):
|
||||
message.info('current',
|
||||
"Run :adblock-update to get adblock lists.")
|
||||
|
||||
@cmdutils.register(instance='host-blocker')
|
||||
def adblock_update(self, win_id: {'special': 'win_id'}):
|
||||
@cmdutils.register(instance='host-blocker', win_id='win_id')
|
||||
def adblock_update(self, win_id):
|
||||
"""Update the adblock block lists."""
|
||||
if self._hosts_file is None:
|
||||
raise cmdexc.CommandError("No data storage is configured!")
|
||||
self.blocked_hosts = set()
|
||||
self._done_count = 0
|
||||
urls = config.get('content', 'host-block-lists')
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
import os.path
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot
|
||||
from PyQt5.QtNetwork import QNetworkDiskCache, QNetworkCacheMetaData
|
||||
|
||||
from qutebrowser.config import config
|
||||
@@ -29,23 +30,41 @@ from qutebrowser.utils import utils, standarddir, objreg
|
||||
|
||||
class DiskCache(QNetworkDiskCache):
|
||||
|
||||
"""Disk cache which sets correct cache dir and size."""
|
||||
"""Disk cache which sets correct cache dir and size.
|
||||
|
||||
Attributes:
|
||||
_activated: Whether the cache should be used.
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setCacheDirectory(os.path.join(standarddir.cache(), 'http'))
|
||||
cache_dir = standarddir.cache()
|
||||
if config.get('general', 'private-browsing') or cache_dir is None:
|
||||
self._activated = False
|
||||
else:
|
||||
self._activated = True
|
||||
self.setCacheDirectory(os.path.join(standarddir.cache(), 'http'))
|
||||
self.setMaximumCacheSize(config.get('storage', 'cache-size'))
|
||||
objreg.get('config').changed.connect(self.cache_size_changed)
|
||||
objreg.get('config').changed.connect(self.on_config_changed)
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self, size=self.cacheSize(),
|
||||
maxsize=self.maximumCacheSize(),
|
||||
path=self.cacheDirectory())
|
||||
|
||||
@config.change_filter('storage', 'cache-size')
|
||||
def cache_size_changed(self):
|
||||
"""Update cache size if the config was changed."""
|
||||
self.setMaximumCacheSize(config.get('storage', 'cache-size'))
|
||||
@pyqtSlot(str, str)
|
||||
def on_config_changed(self, section, option):
|
||||
"""Update cache size/activated if the config was changed."""
|
||||
if (section, option) == ('storage', 'cache-size'):
|
||||
self.setMaximumCacheSize(config.get('storage', 'cache-size'))
|
||||
elif (section, option) == ('general', 'private-browsing'):
|
||||
if (config.get('general', 'private-browsing') or
|
||||
standarddir.cache() is None):
|
||||
self._activated = False
|
||||
else:
|
||||
self._activated = True
|
||||
self.setCacheDirectory(
|
||||
os.path.join(standarddir.cache(), 'http'))
|
||||
|
||||
def cacheSize(self):
|
||||
"""Return the current size taken up by the cache.
|
||||
@@ -53,10 +72,10 @@ class DiskCache(QNetworkDiskCache):
|
||||
Return:
|
||||
An int.
|
||||
"""
|
||||
if config.get('general', 'private-browsing'):
|
||||
return 0
|
||||
else:
|
||||
if self._activated:
|
||||
return super().cacheSize()
|
||||
else:
|
||||
return 0
|
||||
|
||||
def fileMetaData(self, filename):
|
||||
"""Return the QNetworkCacheMetaData for the cache file filename.
|
||||
@@ -67,10 +86,10 @@ class DiskCache(QNetworkDiskCache):
|
||||
Return:
|
||||
A QNetworkCacheMetaData object.
|
||||
"""
|
||||
if config.get('general', 'private-browsing'):
|
||||
return QNetworkCacheMetaData()
|
||||
else:
|
||||
if self._activated:
|
||||
return super().fileMetaData(filename)
|
||||
else:
|
||||
return QNetworkCacheMetaData()
|
||||
|
||||
def data(self, url):
|
||||
"""Return the data associated with url.
|
||||
@@ -81,10 +100,10 @@ class DiskCache(QNetworkDiskCache):
|
||||
return:
|
||||
A QIODevice or None.
|
||||
"""
|
||||
if config.get('general', 'private-browsing'):
|
||||
return None
|
||||
else:
|
||||
if self._activated:
|
||||
return super().data(url)
|
||||
else:
|
||||
return None
|
||||
|
||||
def insert(self, device):
|
||||
"""Insert the data in device and the prepared meta data into the cache.
|
||||
@@ -92,10 +111,10 @@ class DiskCache(QNetworkDiskCache):
|
||||
Args:
|
||||
device: A QIODevice.
|
||||
"""
|
||||
if config.get('general', 'private-browsing'):
|
||||
return
|
||||
else:
|
||||
if self._activated:
|
||||
super().insert(device)
|
||||
else:
|
||||
return None
|
||||
|
||||
def metaData(self, url):
|
||||
"""Return the meta data for the url url.
|
||||
@@ -106,10 +125,10 @@ class DiskCache(QNetworkDiskCache):
|
||||
Return:
|
||||
A QNetworkCacheMetaData object.
|
||||
"""
|
||||
if config.get('general', 'private-browsing'):
|
||||
return QNetworkCacheMetaData()
|
||||
else:
|
||||
if self._activated:
|
||||
return super().metaData(url)
|
||||
else:
|
||||
return QNetworkCacheMetaData()
|
||||
|
||||
def prepare(self, meta_data):
|
||||
"""Return the device that should be populated with the data.
|
||||
@@ -120,10 +139,10 @@ class DiskCache(QNetworkDiskCache):
|
||||
Return:
|
||||
A QIODevice or None.
|
||||
"""
|
||||
if config.get('general', 'private-browsing'):
|
||||
return None
|
||||
else:
|
||||
if self._activated:
|
||||
return super().prepare(meta_data)
|
||||
else:
|
||||
return None
|
||||
|
||||
def remove(self, url):
|
||||
"""Remove the cache entry for url.
|
||||
@@ -131,10 +150,10 @@ class DiskCache(QNetworkDiskCache):
|
||||
Return:
|
||||
True on success, False otherwise.
|
||||
"""
|
||||
if config.get('general', 'private-browsing'):
|
||||
return False
|
||||
else:
|
||||
if self._activated:
|
||||
return super().remove(url)
|
||||
else:
|
||||
return False
|
||||
|
||||
def updateMetaData(self, meta_data):
|
||||
"""Update the cache meta date for the meta_data's url to meta_data.
|
||||
@@ -142,14 +161,14 @@ class DiskCache(QNetworkDiskCache):
|
||||
Args:
|
||||
meta_data: A QNetworkCacheMetaData object.
|
||||
"""
|
||||
if config.get('general', 'private-browsing'):
|
||||
return
|
||||
else:
|
||||
if self._activated:
|
||||
super().updateMetaData(meta_data)
|
||||
else:
|
||||
return
|
||||
|
||||
def clear(self):
|
||||
"""Remove all items from the cache."""
|
||||
if config.get('general', 'private-browsing'):
|
||||
return
|
||||
else:
|
||||
if self._activated:
|
||||
super().clear()
|
||||
else:
|
||||
return
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -84,7 +84,7 @@ class CookieJar(RAMCookieJar):
|
||||
def purge_old_cookies(self):
|
||||
"""Purge expired cookies from the cookie jar."""
|
||||
# Based on:
|
||||
# http://qt-project.org/doc/qt-5/qtwebkitexamples-webkitwidgets-browser-cookiejar-cpp.html
|
||||
# http://doc.qt.io/qt-5/qtwebkitexamples-webkitwidgets-browser-cookiejar-cpp.html
|
||||
now = QDateTime.currentDateTime()
|
||||
cookies = [c for c in self.allCookies()
|
||||
if c.isSessionCookie() or c.expirationDate() >= now]
|
||||
|
||||
@@ -148,7 +148,7 @@ class DownloadItemStats(QObject):
|
||||
|
||||
@pyqtSlot(int, int)
|
||||
def on_download_progress(self, bytes_done, bytes_total):
|
||||
"""Upload local variables when the download progress changed.
|
||||
"""Update local variables when the download progress changed.
|
||||
|
||||
Args:
|
||||
bytes_done: How many bytes are downloaded.
|
||||
@@ -158,7 +158,6 @@ class DownloadItemStats(QObject):
|
||||
bytes_total = None
|
||||
self.done = bytes_done
|
||||
self.total = bytes_total
|
||||
self.updated.emit()
|
||||
|
||||
|
||||
class DownloadItem(QObject):
|
||||
@@ -356,12 +355,19 @@ class DownloadItem(QObject):
|
||||
if reply.error() != QNetworkReply.NoError:
|
||||
QTimer.singleShot(0, lambda: self.error.emit(reply.errorString()))
|
||||
|
||||
def bg_color(self):
|
||||
"""Background color to be shown."""
|
||||
start = config.get('colors', 'downloads.bg.start')
|
||||
stop = config.get('colors', 'downloads.bg.stop')
|
||||
system = config.get('colors', 'downloads.bg.system')
|
||||
error = config.get('colors', 'downloads.bg.error')
|
||||
def get_status_color(self, position):
|
||||
"""Choose an appropriate color for presenting the download's status.
|
||||
|
||||
Args:
|
||||
position: The color type requested, can be 'fg' or 'bg'.
|
||||
"""
|
||||
# pylint: disable=bad-config-call
|
||||
# WORKAROUND for https://bitbucket.org/logilab/astroid/issue/104/
|
||||
assert position in ("fg", "bg")
|
||||
start = config.get('colors', 'downloads.{}.start'.format(position))
|
||||
stop = config.get('colors', 'downloads.{}.stop'.format(position))
|
||||
system = config.get('colors', 'downloads.{}.system'.format(position))
|
||||
error = config.get('colors', 'downloads.{}.error'.format(position))
|
||||
if self.error_msg is not None:
|
||||
assert not self.successful
|
||||
return error
|
||||
@@ -679,15 +685,18 @@ class DownloadManager(QAbstractListModel):
|
||||
if fileobj is not None and filename is not None:
|
||||
raise TypeError("Only one of fileobj/filename may be given!")
|
||||
# WORKAROUND for Qt corrupting data loaded from cache:
|
||||
# https://bugreports.qt-project.org/browse/QTBUG-42757
|
||||
# https://bugreports.qt.io/browse/QTBUG-42757
|
||||
request.setAttribute(QNetworkRequest.CacheLoadControlAttribute,
|
||||
QNetworkRequest.AlwaysNetwork)
|
||||
suggested_fn = urlutils.filename_from_url(request.url())
|
||||
if fileobj is not None or filename is not None:
|
||||
return self.fetch_request(request, page, fileobj, filename,
|
||||
auto_remove, suggested_fn)
|
||||
encoding = sys.getfilesystemencoding()
|
||||
suggested_fn = utils.force_encoding(suggested_fn, encoding)
|
||||
if suggested_fn is None:
|
||||
suggested_fn = 'qutebrowser-download'
|
||||
else:
|
||||
encoding = sys.getfilesystemencoding()
|
||||
suggested_fn = utils.force_encoding(suggested_fn, encoding)
|
||||
q = self._prepare_question()
|
||||
q.default = _path_suggestion(suggested_fn)
|
||||
message_bridge = objreg.get('message-bridge', scope='window',
|
||||
@@ -794,8 +803,9 @@ class DownloadManager(QAbstractListModel):
|
||||
raise cmdexc.CommandError("There's no download!")
|
||||
raise cmdexc.CommandError("There's no download {}!".format(count))
|
||||
|
||||
@cmdutils.register(instance='download-manager', scope='window')
|
||||
def download_cancel(self, count: {'special': 'count'}=0):
|
||||
@cmdutils.register(instance='download-manager', scope='window',
|
||||
count='count')
|
||||
def download_cancel(self, count=0):
|
||||
"""Cancel the last/[count]th download.
|
||||
|
||||
Args:
|
||||
@@ -812,8 +822,9 @@ class DownloadManager(QAbstractListModel):
|
||||
.format(count))
|
||||
download.cancel()
|
||||
|
||||
@cmdutils.register(instance='download-manager', scope='window')
|
||||
def download_delete(self, count: {'special': 'count'}=0):
|
||||
@cmdutils.register(instance='download-manager', scope='window',
|
||||
count='count')
|
||||
def download_delete(self, count=0):
|
||||
"""Delete the last/[count]th download from disk.
|
||||
|
||||
Args:
|
||||
@@ -831,8 +842,9 @@ class DownloadManager(QAbstractListModel):
|
||||
self.remove_item(download)
|
||||
|
||||
@cmdutils.register(instance='download-manager', scope='window',
|
||||
deprecated="Use :download-cancel instead.")
|
||||
def cancel_download(self, count: {'special': 'count'}=1):
|
||||
deprecated="Use :download-cancel instead.",
|
||||
count='count')
|
||||
def cancel_download(self, count=1):
|
||||
"""Cancel the first/[count]th download.
|
||||
|
||||
Args:
|
||||
@@ -840,8 +852,9 @@ class DownloadManager(QAbstractListModel):
|
||||
"""
|
||||
self.download_cancel(count)
|
||||
|
||||
@cmdutils.register(instance='download-manager', scope='window')
|
||||
def download_open(self, count: {'special': 'count'}=0):
|
||||
@cmdutils.register(instance='download-manager', scope='window',
|
||||
count='count')
|
||||
def download_open(self, count=0):
|
||||
"""Open the last/[count]th download.
|
||||
|
||||
Args:
|
||||
@@ -912,9 +925,9 @@ class DownloadManager(QAbstractListModel):
|
||||
"""Check if there are finished downloads to clear."""
|
||||
return any(download.done for download in self.downloads)
|
||||
|
||||
@cmdutils.register(instance='download-manager', scope='window')
|
||||
def download_remove(self, all_: {'name': 'all'}=False,
|
||||
count: {'special': 'count'}=0):
|
||||
@cmdutils.register(instance='download-manager', scope='window',
|
||||
count='count')
|
||||
def download_remove(self, all_=False, count=0):
|
||||
"""Remove the last/[count]th download from the list.
|
||||
|
||||
Args:
|
||||
@@ -1016,9 +1029,9 @@ class DownloadManager(QAbstractListModel):
|
||||
if role == Qt.DisplayRole:
|
||||
data = str(item)
|
||||
elif role == Qt.ForegroundRole:
|
||||
data = config.get('colors', 'downloads.fg')
|
||||
data = item.get_status_color('fg')
|
||||
elif role == Qt.BackgroundRole:
|
||||
data = item.bg_color()
|
||||
data = item.get_status_color('bg')
|
||||
elif role == ModelRole.item:
|
||||
data = item
|
||||
elif role == Qt.ToolTipRole:
|
||||
@@ -1034,7 +1047,7 @@ class DownloadManager(QAbstractListModel):
|
||||
"""Override flags so items aren't selectable.
|
||||
|
||||
The default would be Qt.ItemIsEnabled | Qt.ItemIsSelectable."""
|
||||
return Qt.ItemIsEnabled
|
||||
return Qt.ItemIsEnabled | Qt.ItemNeverHasChildren
|
||||
|
||||
def rowCount(self, parent=QModelIndex()):
|
||||
"""Get count of active downloads."""
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
|
||||
import math
|
||||
import functools
|
||||
import subprocess
|
||||
import collections
|
||||
|
||||
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, QObject, QEvent, Qt, QUrl,
|
||||
@@ -36,15 +35,16 @@ from qutebrowser.keyinput import modeman, modeparsers
|
||||
from qutebrowser.browser import webelem
|
||||
from qutebrowser.commands import userscripts, cmdexc, cmdutils, runners
|
||||
from qutebrowser.utils import usertypes, log, qtutils, message, objreg
|
||||
from qutebrowser.misc import guiprocess
|
||||
|
||||
|
||||
ElemTuple = collections.namedtuple('ElemTuple', ['elem', 'label'])
|
||||
|
||||
|
||||
Target = usertypes.enum('Target', ['normal', 'tab', 'tab_bg', 'window', 'yank',
|
||||
'yank_primary', 'run', 'fill', 'hover',
|
||||
'rapid', 'rapid_win', 'download',
|
||||
'userscript', 'spawn'])
|
||||
Target = usertypes.enum('Target', ['normal', 'tab', 'tab_fg', 'tab_bg',
|
||||
'window', 'yank', 'yank_primary', 'run',
|
||||
'fill', 'hover', 'rapid', 'rapid_win',
|
||||
'download', 'userscript', 'spawn'])
|
||||
|
||||
|
||||
@pyqtSlot(usertypes.KeyMode)
|
||||
@@ -65,7 +65,7 @@ class HintContext:
|
||||
elems: A mapping from key strings to (elem, label) namedtuples.
|
||||
baseurl: The URL of the current page.
|
||||
target: What to do with the opened links.
|
||||
normal/tab/tab_bg/window: Get passed to BrowserTab.
|
||||
normal/tab/tab_fg/tab_bg/window: Get passed to BrowserTab.
|
||||
yank/yank_primary: Yank to clipboard/primary selection.
|
||||
run: Run a command.
|
||||
fill: Fill commandline with link.
|
||||
@@ -117,13 +117,14 @@ class HintManager(QObject):
|
||||
mouse_event: Mouse event to be posted in the web view.
|
||||
arg: A QMouseEvent
|
||||
start_hinting: Emitted when hinting starts, before a link is clicked.
|
||||
arg: The hinting target name.
|
||||
arg: The ClickTarget to use.
|
||||
stop_hinting: Emitted after a link was clicked.
|
||||
"""
|
||||
|
||||
HINT_TEXTS = {
|
||||
Target.normal: "Follow hint",
|
||||
Target.tab: "Follow hint in new tab",
|
||||
Target.tab_fg: "Follow hint in foreground tab",
|
||||
Target.tab_bg: "Follow hint in background tab",
|
||||
Target.window: "Follow hint in new window",
|
||||
Target.yank: "Yank hint to clipboard",
|
||||
@@ -137,7 +138,7 @@ class HintManager(QObject):
|
||||
}
|
||||
|
||||
mouse_event = pyqtSignal('QMouseEvent')
|
||||
start_hinting = pyqtSignal(str)
|
||||
start_hinting = pyqtSignal(usertypes.ClickTarget)
|
||||
stop_hinting = pyqtSignal()
|
||||
|
||||
def __init__(self, win_id, tab_id, parent=None):
|
||||
@@ -413,22 +414,30 @@ class HintManager(QObject):
|
||||
elem: The QWebElement to click.
|
||||
context: The HintContext to use.
|
||||
"""
|
||||
if context.target == Target.rapid:
|
||||
target = Target.tab_bg
|
||||
elif context.target == Target.rapid_win:
|
||||
target = Target.window
|
||||
target_mapping = {
|
||||
Target.rapid: usertypes.ClickTarget.tab_bg,
|
||||
Target.rapid_win: usertypes.ClickTarget.window,
|
||||
Target.normal: usertypes.ClickTarget.normal,
|
||||
Target.tab_fg: usertypes.ClickTarget.tab,
|
||||
Target.tab_bg: usertypes.ClickTarget.tab_bg,
|
||||
Target.window: usertypes.ClickTarget.window,
|
||||
Target.hover: usertypes.ClickTarget.normal,
|
||||
}
|
||||
if config.get('tabs', 'background-tabs'):
|
||||
target_mapping[Target.tab] = usertypes.ClickTarget.tab_bg
|
||||
else:
|
||||
target = context.target
|
||||
target_mapping[Target.tab] = usertypes.ClickTarget.tab
|
||||
# FIXME Instead of clicking the center, we could have nicer heuristics.
|
||||
# e.g. parse (-webkit-)border-radius correctly and click text fields at
|
||||
# the bottom right, and everything else on the top left or so.
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/70
|
||||
pos = elem.rect_on_view().center()
|
||||
action = "Hovering" if target == Target.hover else "Clicking"
|
||||
action = "Hovering" if context.target == Target.hover else "Clicking"
|
||||
log.hints.debug("{} on '{}' at {}/{}".format(
|
||||
action, elem, pos.x(), pos.y()))
|
||||
self.start_hinting.emit(target.name)
|
||||
if target in (Target.tab, Target.tab_bg, Target.window):
|
||||
self.start_hinting.emit(target_mapping[context.target])
|
||||
if context.target in [Target.tab, Target.tab_fg, Target.tab_bg,
|
||||
Target.window, Target.rapid, Target.rapid_win]:
|
||||
modifiers = Qt.ControlModifier
|
||||
else:
|
||||
modifiers = Qt.NoModifier
|
||||
@@ -436,7 +445,7 @@ class HintManager(QObject):
|
||||
QMouseEvent(QEvent.MouseMove, pos, Qt.NoButton, Qt.NoButton,
|
||||
Qt.NoModifier),
|
||||
]
|
||||
if target != Target.hover:
|
||||
if context.target != Target.hover:
|
||||
events += [
|
||||
QMouseEvent(QEvent.MouseButtonPress, pos, Qt.LeftButton,
|
||||
Qt.LeftButton, modifiers),
|
||||
@@ -523,12 +532,11 @@ class HintManager(QObject):
|
||||
'QUTE_MODE': 'hints',
|
||||
'QUTE_SELECTED_TEXT': str(elem),
|
||||
'QUTE_SELECTED_HTML': elem.toOuterXml(),
|
||||
'QUTE_HTML': frame.toHtml(),
|
||||
'QUTE_TEXT': frame.toPlainText(),
|
||||
}
|
||||
url = self._resolve_url(elem, context.baseurl)
|
||||
if url is not None:
|
||||
env['QUTE_URL'] = url.toString(QUrl.FullyEncoded)
|
||||
env.update(userscripts.store_source(frame))
|
||||
userscripts.run(cmd, *args, win_id=self._win_id, env=env)
|
||||
|
||||
def _spawn(self, url, context):
|
||||
@@ -540,11 +548,9 @@ class HintManager(QObject):
|
||||
"""
|
||||
urlstr = url.toString(QUrl.FullyEncoded | QUrl.RemovePassword)
|
||||
args = context.get_args(urlstr)
|
||||
try:
|
||||
subprocess.Popen(args)
|
||||
except OSError as e:
|
||||
msg = "Error while spawning command: {}".format(e)
|
||||
message.error(self._win_id, msg, immediately=True)
|
||||
cmd, *args = args
|
||||
proc = guiprocess.GUIProcess(self._win_id, what='command', parent=self)
|
||||
proc.start(cmd, args)
|
||||
|
||||
def _resolve_url(self, elem, baseurl):
|
||||
"""Resolve a URL and check if we want to keep it.
|
||||
@@ -694,15 +700,16 @@ class HintManager(QObject):
|
||||
tab=self._tab_id)
|
||||
webview.openurl(url)
|
||||
|
||||
@cmdutils.register(instance='hintmanager', scope='tab', name='hint')
|
||||
@cmdutils.register(instance='hintmanager', scope='tab', name='hint',
|
||||
win_id='win_id')
|
||||
def start(self, rapid=False, group=webelem.Group.all, target=Target.normal,
|
||||
*args: {'nargs': '*'}, win_id: {'special': 'win_id'}):
|
||||
*args: {'nargs': '*'}, win_id):
|
||||
"""Start hinting.
|
||||
|
||||
Args:
|
||||
rapid: Whether to do rapid hinting. This is only possible with
|
||||
targets `tab-bg`, `window`, `run`, `hover`, `userscript` and
|
||||
`spawn`.
|
||||
targets `tab` (with background-tabs=true), `tab-bg`,
|
||||
`window`, `run`, `hover`, `userscript` and `spawn`.
|
||||
group: The hinting mode to use.
|
||||
|
||||
- `all`: All clickable elements.
|
||||
@@ -712,7 +719,9 @@ class HintManager(QObject):
|
||||
target: What to do with the selected element.
|
||||
|
||||
- `normal`: Open the link in the current tab.
|
||||
- `tab`: Open the link in a new tab.
|
||||
- `tab`: Open the link in a new tab (honoring the
|
||||
background-tabs setting).
|
||||
- `tab-fg`: Open the link in a new foreground tab.
|
||||
- `tab-bg`: Open the link in a new background tab.
|
||||
- `window`: Open the link in a new window.
|
||||
- `hover` : Hover over the link.
|
||||
@@ -748,14 +757,19 @@ class HintManager(QObject):
|
||||
mode_manager = objreg.get('mode-manager', scope='window',
|
||||
window=self._win_id)
|
||||
if mode_manager.mode == usertypes.KeyMode.hint:
|
||||
raise cmdexc.CommandError("Already hinting!")
|
||||
modeman.leave(win_id, usertypes.KeyMode.hint, 're-hinting')
|
||||
|
||||
if rapid and target not in (Target.tab_bg, Target.window, Target.run,
|
||||
Target.hover, Target.userscript,
|
||||
Target.spawn):
|
||||
name = target.name.replace('_', '-')
|
||||
raise cmdexc.CommandError("Rapid hinting makes no sense with "
|
||||
"target {}!".format(name))
|
||||
if rapid:
|
||||
if target in [Target.tab_bg, Target.window, Target.run,
|
||||
Target.hover, Target.userscript, Target.spawn]:
|
||||
pass
|
||||
elif (target == Target.tab and
|
||||
config.get('tabs', 'background-tabs')):
|
||||
pass
|
||||
else:
|
||||
name = target.name.replace('_', '-')
|
||||
raise cmdexc.CommandError("Rapid hinting makes no sense with "
|
||||
"target {}!".format(name))
|
||||
|
||||
self._check_args(target, *args)
|
||||
self._context = HintContext()
|
||||
@@ -874,6 +888,7 @@ class HintManager(QObject):
|
||||
elem_handlers = {
|
||||
Target.normal: self._click,
|
||||
Target.tab: self._click,
|
||||
Target.tab_fg: self._click,
|
||||
Target.tab_bg: self._click,
|
||||
Target.window: self._click,
|
||||
Target.hover: self._click,
|
||||
|
||||
@@ -67,41 +67,30 @@ class WebHistory(QWebHistoryInterface):
|
||||
_history_dict: An OrderedDict of URLs read from the on-disk history.
|
||||
_new_history: A list of HistoryEntry items of the current session.
|
||||
_saved_count: How many HistoryEntries have been written to disk.
|
||||
_initial_read_started: Whether async_read was called.
|
||||
_initial_read_done: Whether async_read has completed.
|
||||
_temp_history: OrderedDict of temporary history entries before
|
||||
async_read was called.
|
||||
|
||||
Signals:
|
||||
item_about_to_be_added: Emitted before a new HistoryEntry is added.
|
||||
arg: The new HistoryEntry.
|
||||
add_completion_item: Emitted before a new HistoryEntry is added.
|
||||
arg: The new HistoryEntry.
|
||||
item_added: Emitted after a new HistoryEntry is added.
|
||||
arg: The new HistoryEntry.
|
||||
"""
|
||||
|
||||
item_about_to_be_added = pyqtSignal(HistoryEntry)
|
||||
add_completion_item = pyqtSignal(HistoryEntry)
|
||||
item_added = pyqtSignal(HistoryEntry)
|
||||
async_read_done = pyqtSignal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._initial_read_started = False
|
||||
self._initial_read_done = False
|
||||
self._lineparser = lineparser.AppendLineParser(
|
||||
standarddir.data(), 'history', parent=self)
|
||||
self._history_dict = collections.OrderedDict()
|
||||
with self._lineparser.open():
|
||||
for line in self._lineparser:
|
||||
data = line.rstrip().split(maxsplit=1)
|
||||
if not data:
|
||||
# empty line
|
||||
continue
|
||||
elif len(data) != 2:
|
||||
# other malformed line
|
||||
log.init.warning("Invalid history entry {!r}!".format(
|
||||
line))
|
||||
continue
|
||||
atime, url = data
|
||||
# This de-duplicates history entries; only the latest
|
||||
# entry for each URL is kept. If you want to keep
|
||||
# information about previous hits change the items in
|
||||
# old_urls to be lists or change HistoryEntry to have a
|
||||
# list of atimes.
|
||||
self._history_dict[url] = HistoryEntry(atime, url)
|
||||
self._history_dict.move_to_end(url)
|
||||
self._temp_history = collections.OrderedDict()
|
||||
self._new_history = []
|
||||
self._saved_count = 0
|
||||
objreg.get('save-manager').add_saveable(
|
||||
@@ -119,6 +108,60 @@ class WebHistory(QWebHistoryInterface):
|
||||
def __len__(self):
|
||||
return len(self._history_dict)
|
||||
|
||||
def async_read(self):
|
||||
"""Read the initial history."""
|
||||
if self._initial_read_started:
|
||||
log.init.debug("Ignoring async_read() because reading is started.")
|
||||
return
|
||||
self._initial_read_started = True
|
||||
|
||||
if standarddir.data() is None:
|
||||
self._initial_read_done = True
|
||||
self.async_read_done.emit()
|
||||
return
|
||||
|
||||
with self._lineparser.open():
|
||||
for line in self._lineparser:
|
||||
yield
|
||||
data = line.rstrip().split(maxsplit=1)
|
||||
if not data:
|
||||
# empty line
|
||||
continue
|
||||
elif len(data) != 2:
|
||||
# other malformed line
|
||||
log.init.warning("Invalid history entry {!r}!".format(
|
||||
line))
|
||||
continue
|
||||
atime, url = data
|
||||
if atime.startswith('\0'):
|
||||
log.init.warning(
|
||||
"Removing NUL bytes from entry {!r} - see "
|
||||
"https://github.com/The-Compiler/qutebrowser/issues/"
|
||||
"670".format(data))
|
||||
atime = atime.lstrip('\0')
|
||||
# This de-duplicates history entries; only the latest
|
||||
# entry for each URL is kept. If you want to keep
|
||||
# information about previous hits change the items in
|
||||
# old_urls to be lists or change HistoryEntry to have a
|
||||
# list of atimes.
|
||||
entry = HistoryEntry(atime, url)
|
||||
self._add_entry(entry)
|
||||
|
||||
self._initial_read_done = True
|
||||
self.async_read_done.emit()
|
||||
|
||||
for url, entry in self._temp_history.items():
|
||||
self._new_history.append(entry)
|
||||
self._add_entry(entry)
|
||||
self.add_completion_item.emit(entry)
|
||||
|
||||
def _add_entry(self, entry, target=None):
|
||||
"""Add an entry to self._history_dict or another given OrderedDict."""
|
||||
if target is None:
|
||||
target = self._history_dict
|
||||
target[entry.url_string] = entry
|
||||
target.move_to_end(entry.url_string)
|
||||
|
||||
def get_recent(self):
|
||||
"""Get the most recent history entries."""
|
||||
old = self._lineparser.get_recent()
|
||||
@@ -139,13 +182,16 @@ class WebHistory(QWebHistoryInterface):
|
||||
"""
|
||||
if not url_string:
|
||||
return
|
||||
if not config.get('general', 'private-browsing'):
|
||||
entry = HistoryEntry(time.time(), url_string)
|
||||
self.item_about_to_be_added.emit(entry)
|
||||
if config.get('general', 'private-browsing'):
|
||||
return
|
||||
entry = HistoryEntry(time.time(), url_string)
|
||||
if self._initial_read_done:
|
||||
self.add_completion_item.emit(entry)
|
||||
self._new_history.append(entry)
|
||||
self._history_dict[url_string] = entry
|
||||
self._history_dict.move_to_end(url_string)
|
||||
self._add_entry(entry)
|
||||
self.item_added.emit(entry)
|
||||
else:
|
||||
self._add_entry(entry, target=self._temp_history)
|
||||
|
||||
def historyContains(self, url_string):
|
||||
"""Called by WebKit to determine if an URL is contained in the history.
|
||||
|
||||
61
qutebrowser/browser/inspector.py
Normal file
61
qutebrowser/browser/inspector.py
Normal file
@@ -0,0 +1,61 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
# qutebrowser is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# qutebrowser is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Customized QWebInspector."""
|
||||
|
||||
import base64
|
||||
import binascii
|
||||
|
||||
from PyQt5.QtWebKitWidgets import QWebInspector
|
||||
|
||||
from qutebrowser.utils import log, objreg
|
||||
|
||||
|
||||
class WebInspector(QWebInspector):
|
||||
|
||||
"""A customized WebInspector which stores its geometry."""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._load_state_geometry()
|
||||
|
||||
def closeEvent(self, e):
|
||||
"""Save the geometry when closed."""
|
||||
state_config = objreg.get('state-config')
|
||||
data = bytes(self.saveGeometry())
|
||||
geom = base64.b64encode(data).decode('ASCII')
|
||||
state_config['geometry']['inspector'] = geom
|
||||
super().closeEvent(e)
|
||||
|
||||
def _load_state_geometry(self):
|
||||
"""Load the geometry from the state file."""
|
||||
state_config = objreg.get('state-config')
|
||||
try:
|
||||
data = state_config['geometry']['inspector']
|
||||
geom = base64.b64decode(data, validate=True)
|
||||
except KeyError:
|
||||
# First start
|
||||
pass
|
||||
except binascii.Error:
|
||||
log.misc.exception("Error while reading geometry")
|
||||
else:
|
||||
log.init.debug("Loading geometry from {}".format(geom))
|
||||
ok = self.restoreGeometry(geom)
|
||||
if not ok:
|
||||
log.init.warning("Error while loading geometry.")
|
||||
@@ -23,14 +23,8 @@ import collections
|
||||
|
||||
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, PYQT_VERSION, QCoreApplication,
|
||||
QUrl)
|
||||
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkReply, QSslError
|
||||
|
||||
try:
|
||||
from PyQt5.QtNetwork import QSslSocket
|
||||
except ImportError:
|
||||
SSL_AVAILABLE = False
|
||||
else:
|
||||
SSL_AVAILABLE = QSslSocket.supportsSsl()
|
||||
from PyQt5.QtNetwork import (QNetworkAccessManager, QNetworkReply, QSslError,
|
||||
QSslSocket)
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import (message, log, usertypes, utils, objreg, qtutils,
|
||||
@@ -40,17 +34,18 @@ from qutebrowser.browser.network import qutescheme, networkreply
|
||||
|
||||
|
||||
HOSTBLOCK_ERROR_STRING = '%HOSTBLOCK%'
|
||||
ProxyId = collections.namedtuple('ProxyId', 'type, hostname, port')
|
||||
_proxy_auth_cache = {}
|
||||
|
||||
|
||||
def init():
|
||||
"""Disable insecure SSL ciphers on old Qt versions."""
|
||||
if SSL_AVAILABLE:
|
||||
if not qtutils.version_check('5.3.0'):
|
||||
# Disable weak SSL ciphers.
|
||||
# See https://codereview.qt-project.org/#/c/75943/
|
||||
good_ciphers = [c for c in QSslSocket.supportedCiphers()
|
||||
if c.usedBits() >= 128]
|
||||
QSslSocket.setDefaultCiphers(good_ciphers)
|
||||
if not qtutils.version_check('5.3.0'):
|
||||
# Disable weak SSL ciphers.
|
||||
# See https://codereview.qt-project.org/#/c/75943/
|
||||
good_ciphers = [c for c in QSslSocket.supportedCiphers()
|
||||
if c.usedBits() >= 128]
|
||||
QSslSocket.setDefaultCiphers(good_ciphers)
|
||||
|
||||
|
||||
class SslError(QSslError):
|
||||
@@ -105,10 +100,9 @@ class NetworkManager(QNetworkAccessManager):
|
||||
}
|
||||
self._set_cookiejar()
|
||||
self._set_cache()
|
||||
if SSL_AVAILABLE:
|
||||
self.sslErrors.connect(self.on_ssl_errors)
|
||||
self._rejected_ssl_errors = collections.defaultdict(list)
|
||||
self._accepted_ssl_errors = collections.defaultdict(list)
|
||||
self.sslErrors.connect(self.on_ssl_errors)
|
||||
self._rejected_ssl_errors = collections.defaultdict(list)
|
||||
self._accepted_ssl_errors = collections.defaultdict(list)
|
||||
self.authenticationRequired.connect(self.on_authentication_required)
|
||||
self.proxyAuthenticationRequired.connect(
|
||||
self.on_proxy_authentication_required)
|
||||
@@ -171,16 +165,6 @@ class NetworkManager(QNetworkAccessManager):
|
||||
q.deleteLater()
|
||||
return q.answer
|
||||
|
||||
def _fill_authenticator(self, authenticator, answer):
|
||||
"""Fill a given QAuthenticator object with an answer."""
|
||||
if answer is not None:
|
||||
# Since the answer could be something else than (user, password)
|
||||
# pylint seems to think we're unpacking a non-sequence. However we
|
||||
# *did* explicitly ask for a tuple, so it *will* always be one.
|
||||
user, password = answer
|
||||
authenticator.setUser(user)
|
||||
authenticator.setPassword(password)
|
||||
|
||||
def shutdown(self):
|
||||
"""Abort all running requests."""
|
||||
self.setNetworkAccessible(QNetworkAccessManager.NotAccessible)
|
||||
@@ -189,64 +173,67 @@ class NetworkManager(QNetworkAccessManager):
|
||||
request.deleteLater()
|
||||
self.shutting_down.emit()
|
||||
|
||||
if SSL_AVAILABLE: # noqa
|
||||
@pyqtSlot('QNetworkReply*', 'QList<QSslError>')
|
||||
def on_ssl_errors(self, reply, errors):
|
||||
"""Decide if SSL errors should be ignored or not.
|
||||
@pyqtSlot('QNetworkReply*', 'QList<QSslError>')
|
||||
def on_ssl_errors(self, reply, errors): # pragma: no mccabe
|
||||
"""Decide if SSL errors should be ignored or not.
|
||||
|
||||
This slot is called on SSL/TLS errors by the self.sslErrors signal.
|
||||
This slot is called on SSL/TLS errors by the self.sslErrors signal.
|
||||
|
||||
Args:
|
||||
reply: The QNetworkReply that is encountering the errors.
|
||||
errors: A list of errors.
|
||||
"""
|
||||
errors = [SslError(e) for e in errors]
|
||||
ssl_strict = config.get('network', 'ssl-strict')
|
||||
if ssl_strict == 'ask':
|
||||
Args:
|
||||
reply: The QNetworkReply that is encountering the errors.
|
||||
errors: A list of errors.
|
||||
"""
|
||||
errors = [SslError(e) for e in errors]
|
||||
ssl_strict = config.get('network', 'ssl-strict')
|
||||
if ssl_strict == 'ask':
|
||||
try:
|
||||
host_tpl = urlutils.host_tuple(reply.url())
|
||||
if set(errors).issubset(self._accepted_ssl_errors[host_tpl]):
|
||||
reply.ignoreSslErrors()
|
||||
elif set(errors).issubset(self._rejected_ssl_errors[host_tpl]):
|
||||
pass
|
||||
else:
|
||||
err_string = '\n'.join('- ' + err.errorString() for err in
|
||||
errors)
|
||||
answer = self._ask('SSL errors - continue?\n{}'.format(
|
||||
err_string), mode=usertypes.PromptMode.yesno,
|
||||
owner=reply)
|
||||
if answer:
|
||||
reply.ignoreSslErrors()
|
||||
self._accepted_ssl_errors[host_tpl] += errors
|
||||
else:
|
||||
self._rejected_ssl_errors[host_tpl] += errors
|
||||
elif ssl_strict:
|
||||
except ValueError:
|
||||
host_tpl = None
|
||||
is_accepted = False
|
||||
is_rejected = False
|
||||
else:
|
||||
is_accepted = set(errors).issubset(
|
||||
self._accepted_ssl_errors[host_tpl])
|
||||
is_rejected = set(errors).issubset(
|
||||
self._rejected_ssl_errors[host_tpl])
|
||||
if is_accepted:
|
||||
reply.ignoreSslErrors()
|
||||
elif is_rejected:
|
||||
pass
|
||||
else:
|
||||
for err in errors:
|
||||
# FIXME we might want to use warn here (non-fatal error)
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/114
|
||||
message.error(self._win_id,
|
||||
'SSL error: {}'.format(err.errorString()))
|
||||
reply.ignoreSslErrors()
|
||||
err_string = '\n'.join('- ' + err.errorString() for err in
|
||||
errors)
|
||||
answer = self._ask('SSL errors - continue?\n{}'.format(
|
||||
err_string), mode=usertypes.PromptMode.yesno,
|
||||
owner=reply)
|
||||
if answer:
|
||||
reply.ignoreSslErrors()
|
||||
d = self._accepted_ssl_errors
|
||||
else:
|
||||
d = self._rejected_ssl_errors
|
||||
if host_tpl is not None:
|
||||
d[host_tpl] += errors
|
||||
elif ssl_strict:
|
||||
pass
|
||||
else:
|
||||
for err in errors:
|
||||
# FIXME we might want to use warn here (non-fatal error)
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/114
|
||||
message.error(self._win_id,
|
||||
'SSL error: {}'.format(err.errorString()))
|
||||
reply.ignoreSslErrors()
|
||||
|
||||
@pyqtSlot(QUrl)
|
||||
def clear_rejected_ssl_errors(self, url):
|
||||
"""Clear the rejected SSL errors on a reload.
|
||||
@pyqtSlot(QUrl)
|
||||
def clear_rejected_ssl_errors(self, url):
|
||||
"""Clear the rejected SSL errors on a reload.
|
||||
|
||||
Args:
|
||||
url: The URL to remove.
|
||||
"""
|
||||
try:
|
||||
del self._rejected_ssl_errors[url]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
@pyqtSlot(QUrl)
|
||||
def clear_rejected_ssl_errors(self, _url):
|
||||
"""Clear the rejected SSL errors on a reload.
|
||||
|
||||
Does nothing because SSL is unavailable.
|
||||
"""
|
||||
Args:
|
||||
url: The URL to remove.
|
||||
"""
|
||||
try:
|
||||
del self._rejected_ssl_errors[url]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
@pyqtSlot('QNetworkReply', 'QAuthenticator')
|
||||
@@ -255,14 +242,25 @@ class NetworkManager(QNetworkAccessManager):
|
||||
answer = self._ask("Username ({}):".format(authenticator.realm()),
|
||||
mode=usertypes.PromptMode.user_pwd,
|
||||
owner=reply)
|
||||
self._fill_authenticator(authenticator, answer)
|
||||
if answer is not None:
|
||||
authenticator.setUser(answer.user)
|
||||
authenticator.setPassword(answer.password)
|
||||
|
||||
@pyqtSlot('QNetworkProxy', 'QAuthenticator')
|
||||
def on_proxy_authentication_required(self, _proxy, authenticator):
|
||||
def on_proxy_authentication_required(self, proxy, authenticator):
|
||||
"""Called when a proxy needs authentication."""
|
||||
answer = self._ask("Proxy username ({}):".format(
|
||||
authenticator.realm()), mode=usertypes.PromptMode.user_pwd)
|
||||
self._fill_authenticator(authenticator, answer)
|
||||
proxy_id = ProxyId(proxy.type(), proxy.hostName(), proxy.port())
|
||||
if proxy_id in _proxy_auth_cache:
|
||||
user, password = _proxy_auth_cache[proxy_id]
|
||||
authenticator.setUser(user)
|
||||
authenticator.setPassword(password)
|
||||
else:
|
||||
answer = self._ask("Proxy username ({}):".format(
|
||||
authenticator.realm()), mode=usertypes.PromptMode.user_pwd)
|
||||
if answer is not None:
|
||||
authenticator.setUser(answer.user)
|
||||
authenticator.setPassword(answer.password)
|
||||
_proxy_auth_cache[proxy_id] = answer
|
||||
|
||||
@config.change_filter('general', 'private-browsing')
|
||||
def on_config_changed(self):
|
||||
@@ -319,11 +317,7 @@ class NetworkManager(QNetworkAccessManager):
|
||||
A QNetworkReply.
|
||||
"""
|
||||
scheme = req.url().scheme()
|
||||
if scheme == 'https' and not SSL_AVAILABLE:
|
||||
return networkreply.ErrorNetworkReply(
|
||||
req, "SSL is not supported by the installed Qt library!",
|
||||
QNetworkReply.ProtocolUnknownError, self)
|
||||
elif scheme in self._scheme_handlers:
|
||||
if scheme in self._scheme_handlers:
|
||||
return self._scheme_handlers[scheme].createRequest(
|
||||
op, req, outgoing_data)
|
||||
|
||||
|
||||
@@ -96,6 +96,12 @@ class JSBridge(QObject):
|
||||
@pyqtSlot(int, str, str, str)
|
||||
def set(self, win_id, sectname, optname, value):
|
||||
"""Slot to set a setting from qute:settings."""
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/727
|
||||
if ((sectname, optname) == ('content', 'allow-javascript') and
|
||||
value == 'false'):
|
||||
message.error(win_id, "Refusing to disable javascript via "
|
||||
"qute:settings as it needs javascript support.")
|
||||
return
|
||||
try:
|
||||
objreg.get('config').set('conf', sectname, optname, value)
|
||||
except (configexc.Error, configparser.Error) as e:
|
||||
@@ -153,7 +159,7 @@ def qute_help(win_id, request):
|
||||
url=request.url().toDisplayString(),
|
||||
error="This most likely means the documentation was not generated "
|
||||
"properly. If you are running qutebrowser from the git "
|
||||
"repository, please run scripts/asciidoc2html.py."
|
||||
"repository, please run scripts/asciidoc2html.py. "
|
||||
"If you're running a released version this is a bug, please "
|
||||
"use :report to report it.",
|
||||
icon='')
|
||||
|
||||
@@ -105,8 +105,8 @@ class QuickmarkManager(QObject):
|
||||
win_id, "Add quickmark:", usertypes.PromptMode.text,
|
||||
functools.partial(self.quickmark_add, win_id, urlstr))
|
||||
|
||||
@cmdutils.register(instance='quickmark-manager')
|
||||
def quickmark_add(self, win_id: {'special': 'win_id'}, url, name):
|
||||
@cmdutils.register(instance='quickmark-manager', win_id='win_id')
|
||||
def quickmark_add(self, win_id, url, name):
|
||||
"""Add a new quickmark.
|
||||
|
||||
Args:
|
||||
|
||||
@@ -312,7 +312,7 @@ def javascript_escape(text):
|
||||
def get_child_frames(startframe):
|
||||
"""Get all children recursively of a given QWebFrame.
|
||||
|
||||
Loosly based on http://blog.nextgenetics.net/?e=64
|
||||
Loosely based on http://blog.nextgenetics.net/?e=64
|
||||
|
||||
Args:
|
||||
startframe: The QWebFrame to start with.
|
||||
|
||||
@@ -109,7 +109,7 @@ class BrowserPage(QWebPage):
|
||||
def _handle_errorpage(self, info, errpage):
|
||||
"""Display an error page if needed.
|
||||
|
||||
Loosly based on Helpviewer/HelpBrowserWV.py from eric5
|
||||
Loosely based on Helpviewer/HelpBrowserWV.py from eric5
|
||||
(line 260 @ 5d937eb378dd)
|
||||
|
||||
Args:
|
||||
@@ -178,7 +178,7 @@ class BrowserPage(QWebPage):
|
||||
def _handle_multiple_files(self, info, files):
|
||||
"""Handle uploading of multiple files.
|
||||
|
||||
Loosly based on Helpviewer/HelpBrowserWV.py from eric5.
|
||||
Loosely based on Helpviewer/HelpBrowserWV.py from eric5.
|
||||
|
||||
Args:
|
||||
info: The ChooseMultipleFilesExtensionOption instance.
|
||||
@@ -241,7 +241,7 @@ class BrowserPage(QWebPage):
|
||||
if cur_data is not None:
|
||||
frame = self.mainFrame()
|
||||
if 'zoom' in cur_data:
|
||||
frame.setZoomFactor(cur_data['zoom'])
|
||||
frame.page().view().zoom_perc(cur_data['zoom'] * 100)
|
||||
if ('scroll-pos' in cur_data and
|
||||
frame.scrollPosition() == QPoint(0, 0)):
|
||||
QTimer.singleShot(0, functools.partial(
|
||||
@@ -325,7 +325,8 @@ class BrowserPage(QWebPage):
|
||||
QWebPage.Notifications: ('content', 'notifications'),
|
||||
QWebPage.Geolocation: ('content', 'geolocation'),
|
||||
}
|
||||
if config.get(*options[feature]) == 'ask':
|
||||
config_val = config.get(*options[feature])
|
||||
if config_val == 'ask':
|
||||
bridge = objreg.get('message-bridge', scope='window',
|
||||
window=self._win_id)
|
||||
q = usertypes.Question(bridge)
|
||||
@@ -361,6 +362,9 @@ class BrowserPage(QWebPage):
|
||||
self.loadStarted.connect(q.abort)
|
||||
|
||||
bridge.ask(q, blocking=False)
|
||||
elif config_val:
|
||||
self.setFeaturePermission(frame, feature,
|
||||
QWebPage.PermissionGrantedByUser)
|
||||
else:
|
||||
self.setFeaturePermission(frame, feature,
|
||||
QWebPage.PermissionDeniedByUser)
|
||||
@@ -414,7 +418,7 @@ class BrowserPage(QWebPage):
|
||||
if data is None:
|
||||
return
|
||||
if 'zoom' in data:
|
||||
frame.setZoomFactor(data['zoom'])
|
||||
frame.page().view().zoom_perc(data['zoom'] * 100)
|
||||
if 'scroll-pos' in data and frame.scrollPosition() == QPoint(0, 0):
|
||||
frame.setScrollPosition(data['scroll-pos'])
|
||||
|
||||
@@ -423,14 +427,10 @@ class BrowserPage(QWebPage):
|
||||
"""Emitted before a hinting-click takes place.
|
||||
|
||||
Args:
|
||||
hint_target: A string to set self._hint_target to.
|
||||
hint_target: A ClickTarget member to set self._hint_target to.
|
||||
"""
|
||||
t = getattr(usertypes.ClickTarget, hint_target, None)
|
||||
if t is None:
|
||||
return
|
||||
log.webview.debug("Setting force target to {}/{}".format(
|
||||
hint_target, t))
|
||||
self._hint_target = t
|
||||
log.webview.debug("Setting force target to {}".format(hint_target))
|
||||
self._hint_target = hint_target
|
||||
|
||||
@pyqtSlot()
|
||||
def on_stop_hinting(self):
|
||||
@@ -478,17 +478,23 @@ class BrowserPage(QWebPage):
|
||||
return super().extension(ext, opt, out)
|
||||
return handler(opt, out)
|
||||
|
||||
def javaScriptAlert(self, _frame, msg):
|
||||
def javaScriptAlert(self, frame, msg):
|
||||
"""Override javaScriptAlert to use the statusbar."""
|
||||
log.js.debug("alert: {}".format(msg))
|
||||
if config.get('ui', 'modal-js-dialog'):
|
||||
return super().javaScriptAlert(frame, msg)
|
||||
|
||||
if (self._is_shutting_down or
|
||||
config.get('content', 'ignore-javascript-alert')):
|
||||
return
|
||||
self._ask("[js alert] {}".format(msg), usertypes.PromptMode.alert)
|
||||
|
||||
def javaScriptConfirm(self, _frame, msg):
|
||||
def javaScriptConfirm(self, frame, msg):
|
||||
"""Override javaScriptConfirm to use the statusbar."""
|
||||
log.js.debug("confirm: {}".format(msg))
|
||||
if config.get('ui', 'modal-js-dialog'):
|
||||
return super().javaScriptConfirm(frame, msg)
|
||||
|
||||
if self._is_shutting_down:
|
||||
return False
|
||||
ans = self._ask("[js confirm] {}".format(msg),
|
||||
|
||||
@@ -24,6 +24,7 @@ import itertools
|
||||
import functools
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QTimer, QUrl
|
||||
from PyQt5.QtGui import QPalette
|
||||
from PyQt5.QtWidgets import QApplication, QStyleFactory
|
||||
from PyQt5.QtWebKit import QWebSettings
|
||||
from PyQt5.QtWebKitWidgets import QWebView, QWebPage
|
||||
@@ -32,7 +33,6 @@ from qutebrowser.config import config
|
||||
from qutebrowser.keyinput import modeman
|
||||
from qutebrowser.utils import message, log, usertypes, utils, qtutils, objreg
|
||||
from qutebrowser.browser import webpage, hints, webelem
|
||||
from qutebrowser.commands import cmdexc
|
||||
|
||||
|
||||
LoadStatus = usertypes.enum('LoadStatus', ['none', 'success', 'error', 'warn',
|
||||
@@ -106,7 +106,9 @@ class WebView(QWebView):
|
||||
self.keep_icon = False
|
||||
self.search_text = None
|
||||
self.search_flags = 0
|
||||
self.selection_enabled = False
|
||||
self.init_neighborlist()
|
||||
self._set_bg_color()
|
||||
cfg = objreg.get('config')
|
||||
cfg.changed.connect(self.init_neighborlist)
|
||||
# For some reason, this signal doesn't get disconnected automatically
|
||||
@@ -160,7 +162,7 @@ class WebView(QWebView):
|
||||
return utils.get_repr(self, tab_id=self.tab_id, url=url)
|
||||
|
||||
def __del__(self):
|
||||
# Explicitely releasing the page here seems to prevent some segfaults
|
||||
# Explicitly releasing the page here seems to prevent some segfaults
|
||||
# when quitting.
|
||||
# Copied from:
|
||||
# https://code.google.com/p/webscraping/source/browse/webkit.py#325
|
||||
@@ -180,6 +182,15 @@ class WebView(QWebView):
|
||||
self.load_status = val
|
||||
self.load_status_changed.emit(val.name)
|
||||
|
||||
def _set_bg_color(self):
|
||||
"""Set the webpage background color as configured."""
|
||||
col = config.get('colors', 'webpage.bg')
|
||||
palette = self.palette()
|
||||
if col is None:
|
||||
col = self.style().standardPalette().color(QPalette.Base)
|
||||
palette.setColor(QPalette.Base, col)
|
||||
self.setPalette(palette)
|
||||
|
||||
@pyqtSlot(str, str)
|
||||
def on_config_changed(self, section, option):
|
||||
"""Reinitialize the zoom neighborlist if related config changed."""
|
||||
@@ -194,6 +205,8 @@ class WebView(QWebView):
|
||||
self.setContextMenuPolicy(Qt.PreventContextMenu)
|
||||
else:
|
||||
self.setContextMenuPolicy(Qt.DefaultContextMenu)
|
||||
elif section == 'colors' and option == 'webpage.bg':
|
||||
self._set_bg_color()
|
||||
|
||||
def init_neighborlist(self):
|
||||
"""Initialize the _zoom neighborlist."""
|
||||
@@ -355,9 +368,8 @@ class WebView(QWebView):
|
||||
if fuzzyval:
|
||||
self._zoom.fuzzyval = int(perc)
|
||||
if perc < 0:
|
||||
raise cmdexc.CommandError("Can't zoom {}%!".format(perc))
|
||||
raise ValueError("Can't zoom {}%!".format(perc))
|
||||
self.setZoomFactor(float(perc) / 100)
|
||||
message.info(self.win_id, "Zoom level: {}%".format(perc))
|
||||
self._default_zoom_changed = True
|
||||
|
||||
def zoom(self, offset):
|
||||
@@ -365,9 +377,13 @@ class WebView(QWebView):
|
||||
|
||||
Args:
|
||||
offset: The offset in the zoom level list.
|
||||
|
||||
Return:
|
||||
The new zoom percentage.
|
||||
"""
|
||||
level = self._zoom.getitem(offset)
|
||||
self.zoom_perc(level, fuzzyval=False)
|
||||
return level
|
||||
|
||||
@pyqtSlot('QUrl')
|
||||
def on_url_changed(self, url):
|
||||
@@ -378,6 +394,8 @@ class WebView(QWebView):
|
||||
if url.isValid():
|
||||
self.cur_url = url
|
||||
self.url_text_changed.emit(url.toDisplayString())
|
||||
if not self.title():
|
||||
self.titleChanged.emit(self.url().toDisplayString())
|
||||
|
||||
@pyqtSlot('QMouseEvent')
|
||||
def on_mouse_event(self, evt):
|
||||
@@ -396,7 +414,7 @@ class WebView(QWebView):
|
||||
|
||||
@pyqtSlot()
|
||||
def on_load_finished(self):
|
||||
"""Handle auto-insert-mode after loading finished.
|
||||
"""Handle a finished page load.
|
||||
|
||||
We don't take loadFinished's ok argument here as it always seems to be
|
||||
true when the QWebPage has an ErrorPageExtension implemented.
|
||||
@@ -409,6 +427,12 @@ class WebView(QWebView):
|
||||
self._set_load_status(LoadStatus.warn)
|
||||
else:
|
||||
self._set_load_status(LoadStatus.error)
|
||||
if not self.title():
|
||||
self.titleChanged.emit(self.url().toDisplayString())
|
||||
self._handle_auto_insert_mode(ok)
|
||||
|
||||
def _handle_auto_insert_mode(self, ok):
|
||||
"""Handle auto-insert-mode after loading finished."""
|
||||
if not config.get('input', 'auto-insert-mode'):
|
||||
return
|
||||
mode_manager = objreg.get('mode-manager', scope='window',
|
||||
@@ -435,6 +459,25 @@ class WebView(QWebView):
|
||||
log.webview.debug("Ignoring focus because mode {} was "
|
||||
"entered.".format(mode))
|
||||
self.setFocusPolicy(Qt.NoFocus)
|
||||
elif mode == usertypes.KeyMode.caret:
|
||||
settings = self.settings()
|
||||
settings.setAttribute(QWebSettings.CaretBrowsingEnabled, True)
|
||||
self.selection_enabled = bool(self.page().selectedText())
|
||||
|
||||
if self.isVisible():
|
||||
# Sometimes the caret isn't immediately visible, but unfocusing
|
||||
# and refocusing it fixes that.
|
||||
self.clearFocus()
|
||||
self.setFocus(Qt.OtherFocusReason)
|
||||
|
||||
# Move the caret to the first element in the viewport if there
|
||||
# isn't any text which is already selected.
|
||||
#
|
||||
# Note: We can't use hasSelection() here, as that's always
|
||||
# true in caret mode.
|
||||
if not self.page().selectedText():
|
||||
self.page().currentFrame().evaluateJavaScript(
|
||||
utils.read_file('javascript/position_caret.js'))
|
||||
|
||||
@pyqtSlot(usertypes.KeyMode)
|
||||
def on_mode_left(self, mode):
|
||||
@@ -443,6 +486,15 @@ class WebView(QWebView):
|
||||
usertypes.KeyMode.yesno):
|
||||
log.webview.debug("Restoring focus policy because mode {} was "
|
||||
"left.".format(mode))
|
||||
elif mode == usertypes.KeyMode.caret:
|
||||
settings = self.settings()
|
||||
if settings.testAttribute(QWebSettings.CaretBrowsingEnabled):
|
||||
if self.selection_enabled and self.hasSelection():
|
||||
# Remove selection if it exists
|
||||
self.triggerPageAction(QWebPage.MoveToNextChar)
|
||||
settings.setAttribute(QWebSettings.CaretBrowsingEnabled, False)
|
||||
self.selection_enabled = False
|
||||
|
||||
self.setFocusPolicy(Qt.WheelFocus)
|
||||
|
||||
def search(self, text, flags):
|
||||
@@ -457,12 +509,25 @@ class WebView(QWebView):
|
||||
old_scroll_pos = self.scroll_pos
|
||||
flags = QWebPage.FindFlags(flags)
|
||||
found = self.findText(text, flags)
|
||||
if not found and not flags & QWebPage.HighlightAllOccurrences and text:
|
||||
message.error(self.win_id, "Text '{}' not found on "
|
||||
"page!".format(text), immediately=True)
|
||||
else:
|
||||
backward = int(flags) & QWebPage.FindBackward
|
||||
backward = flags & QWebPage.FindBackward
|
||||
|
||||
if not found and not flags & QWebPage.HighlightAllOccurrences and text:
|
||||
# User disabled wrapping; but findText() just returns False. If we
|
||||
# have a selection, we know there's a match *somewhere* on the page
|
||||
if (not flags & QWebPage.FindWrapsAroundDocument and
|
||||
self.hasSelection()):
|
||||
if not backward:
|
||||
message.warning(self.win_id, "Search hit BOTTOM without "
|
||||
"match for: {}".format(text),
|
||||
immediately=True)
|
||||
else:
|
||||
message.warning(self.win_id, "Search hit TOP without "
|
||||
"match for: {}".format(text),
|
||||
immediately=True)
|
||||
else:
|
||||
message.error(self.win_id, "Text '{}' not found on "
|
||||
"page!".format(text), immediately=True)
|
||||
else:
|
||||
def check_scroll_pos():
|
||||
"""Check if the scroll position got smaller and show info."""
|
||||
if not backward and self.scroll_pos < old_scroll_pos:
|
||||
@@ -564,6 +629,7 @@ class WebView(QWebView):
|
||||
"""Save a reference to the context menu so we can close it."""
|
||||
menu = self.page().createStandardContextMenu()
|
||||
self.shutting_down.connect(menu.close)
|
||||
modeman.instance(self.win_id).entered.connect(menu.close)
|
||||
menu.exec_(e.globalPos())
|
||||
|
||||
def wheelEvent(self, e):
|
||||
|
||||
@@ -29,6 +29,11 @@ from qutebrowser.utils import log, utils, message, docutils, objreg, usertypes
|
||||
from qutebrowser.utils import debug as debug_utils
|
||||
|
||||
|
||||
def arg_name(name):
|
||||
"""Get the name an argument should have based on its Python name."""
|
||||
return name.rstrip('_').replace('_', '-')
|
||||
|
||||
|
||||
class Command:
|
||||
|
||||
"""Base skeleton for a command.
|
||||
@@ -44,12 +49,11 @@ class Command:
|
||||
completion: Completions to use for arguments, as a list of strings.
|
||||
debug: Whether this is a debugging command (only shown with --debug).
|
||||
parser: The ArgumentParser to use to parse this command.
|
||||
special_params: A dict with the names of the special parameters as
|
||||
values.
|
||||
count_arg: The name of the count parameter, or None.
|
||||
win_id_arg: The name of the win_id parameter, or None.
|
||||
flags_with_args: A list of flags which take an argument.
|
||||
no_cmd_split: If true, ';;' to split sub-commands is ignored.
|
||||
_type_conv: A mapping of conversion functions for arguments.
|
||||
_name_conv: A mapping of argument names to parameter names.
|
||||
_needs_js: Whether the command needs javascript enabled
|
||||
_modes: The modes the command can be executed in.
|
||||
_not_modes: The modes the command can not be executed in.
|
||||
@@ -62,13 +66,14 @@ class Command:
|
||||
"""
|
||||
|
||||
AnnotationInfo = collections.namedtuple('AnnotationInfo',
|
||||
['kwargs', 'type', 'name', 'flag',
|
||||
'special'])
|
||||
['kwargs', 'type', 'flag', 'hide',
|
||||
'metavar'])
|
||||
|
||||
def __init__(self, *, handler, name, instance=None, maxsplit=None,
|
||||
hide=False, completion=None, modes=None, not_modes=None,
|
||||
needs_js=False, debug=False, ignore_args=False,
|
||||
deprecated=False, no_cmd_split=False, scope='global'):
|
||||
deprecated=False, no_cmd_split=False, scope='global',
|
||||
count=None, win_id=None):
|
||||
# I really don't know how to solve this in a better way, I tried.
|
||||
# pylint: disable=too-many-arguments,too-many-locals
|
||||
if modes is not None and not_modes is not None:
|
||||
@@ -81,6 +86,9 @@ class Command:
|
||||
for m in not_modes:
|
||||
if not isinstance(m, usertypes.KeyMode):
|
||||
raise TypeError("Mode {} is no KeyMode member!".format(m))
|
||||
if scope != 'global' and instance is None:
|
||||
raise ValueError("Setting scope without setting instance makes "
|
||||
"no sense!")
|
||||
self.name = name
|
||||
self.maxsplit = maxsplit
|
||||
self.hide = hide
|
||||
@@ -95,6 +103,8 @@ class Command:
|
||||
self.ignore_args = ignore_args
|
||||
self.handler = handler
|
||||
self.no_cmd_split = no_cmd_split
|
||||
self.count_arg = count
|
||||
self.win_id_arg = win_id
|
||||
self.docparser = docutils.DocstringParser(handler)
|
||||
self.parser = argparser.ArgumentParser(
|
||||
name, description=self.docparser.short_desc,
|
||||
@@ -107,11 +117,9 @@ class Command:
|
||||
self.namespace = None
|
||||
self._count = None
|
||||
self.pos_args = []
|
||||
self.special_params = {'count': None, 'win_id': None}
|
||||
self.desc = None
|
||||
self.flags_with_args = []
|
||||
self._type_conv = {}
|
||||
self._name_conv = {}
|
||||
count = self._inspect_func()
|
||||
if self.completion is not None and len(self.completion) > count:
|
||||
raise ValueError("Got {} completions, but only {} "
|
||||
@@ -173,52 +181,22 @@ class Command:
|
||||
type_conv[param.name] = argparser.multitype_conv(typ)
|
||||
return type_conv
|
||||
|
||||
def _get_nameconv(self, param, annotation_info):
|
||||
"""Get a dict with a name conversion for the parameter.
|
||||
|
||||
Args:
|
||||
param: The inspect.Parameter to handle.
|
||||
annotation_info: The AnnotationInfo tuple for the parameter.
|
||||
"""
|
||||
d = {}
|
||||
if annotation_info.name is not None:
|
||||
d[param.name] = annotation_info.name
|
||||
return d
|
||||
|
||||
def _inspect_special_param(self, param, annotation_info):
|
||||
def _inspect_special_param(self, param):
|
||||
"""Check if the given parameter is a special one.
|
||||
|
||||
Args:
|
||||
param: The inspect.Parameter to handle.
|
||||
annotation_info: The AnnotationInfo tuple for the parameter.
|
||||
|
||||
Return:
|
||||
True if the parameter is special, False otherwise.
|
||||
"""
|
||||
special = annotation_info.special
|
||||
if special == 'count':
|
||||
if self.special_params['count'] is not None:
|
||||
raise ValueError("Registered multiple parameters ({}/{}) as "
|
||||
"count!".format(self.special_params['count'],
|
||||
param.name))
|
||||
if param.name == self.count_arg:
|
||||
if param.default is inspect.Parameter.empty:
|
||||
raise TypeError("{}: handler has count parameter "
|
||||
"without default!".format(self.name))
|
||||
self.special_params['count'] = param.name
|
||||
return True
|
||||
elif special == 'win_id':
|
||||
if self.special_params['win_id'] is not None:
|
||||
raise ValueError("Registered multiple parameters ({}/{}) as "
|
||||
"win_id!".format(
|
||||
self.special_params['win_id'],
|
||||
param.name))
|
||||
self.special_params['win_id'] = param.name
|
||||
elif param.name == self.win_id_arg:
|
||||
return True
|
||||
elif special is None:
|
||||
return False
|
||||
else:
|
||||
raise ValueError("{}: Invalid value '{}' for 'special' "
|
||||
"annotation!".format(self.name, special))
|
||||
|
||||
def _inspect_func(self):
|
||||
"""Inspect the function to get useful informations from it.
|
||||
@@ -236,20 +214,28 @@ class Command:
|
||||
self.desc = doc.splitlines()[0].strip()
|
||||
else:
|
||||
self.desc = ""
|
||||
|
||||
if (self.count_arg is not None and
|
||||
self.count_arg not in signature.parameters):
|
||||
raise ValueError("count parameter {} does not exist!".format(
|
||||
self.count_arg))
|
||||
if (self.win_id_arg is not None and
|
||||
self.win_id_arg not in signature.parameters):
|
||||
raise ValueError("win_id parameter {} does not exist!".format(
|
||||
self.win_id_arg))
|
||||
|
||||
if not self.ignore_args:
|
||||
for param in signature.parameters.values():
|
||||
annotation_info = self._parse_annotation(param)
|
||||
if param.name == 'self':
|
||||
continue
|
||||
if self._inspect_special_param(param, annotation_info):
|
||||
if self._inspect_special_param(param):
|
||||
continue
|
||||
arg_count += 1
|
||||
typ = self._get_type(param, annotation_info)
|
||||
kwargs = self._param_to_argparse_kwargs(param, annotation_info)
|
||||
args = self._param_to_argparse_args(param, annotation_info)
|
||||
self._type_conv.update(self._get_typeconv(param, typ))
|
||||
self._name_conv.update(
|
||||
self._get_nameconv(param, annotation_info))
|
||||
callsig = debug_utils.format_call(
|
||||
self.parser.add_argument, args, kwargs,
|
||||
full=False)
|
||||
@@ -276,11 +262,13 @@ class Command:
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
kwargs['dest'] = param.name
|
||||
|
||||
if isinstance(typ, tuple):
|
||||
pass
|
||||
kwargs['metavar'] = annotation_info.metavar or param.name
|
||||
elif utils.is_enum(typ):
|
||||
kwargs['choices'] = [e.name.replace('_', '-') for e in typ]
|
||||
kwargs['metavar'] = param.name
|
||||
kwargs['choices'] = [arg_name(e.name) for e in typ]
|
||||
kwargs['metavar'] = annotation_info.metavar or param.name
|
||||
elif typ is bool:
|
||||
kwargs['action'] = 'store_true'
|
||||
elif typ is not None:
|
||||
@@ -307,8 +295,8 @@ class Command:
|
||||
A list of args.
|
||||
"""
|
||||
args = []
|
||||
name = annotation_info.name or param.name
|
||||
shortname = annotation_info.flag or param.name[0]
|
||||
name = arg_name(param.name)
|
||||
shortname = annotation_info.flag or name[0]
|
||||
if len(shortname) != 1:
|
||||
raise ValueError("Flag '{}' of parameter {} (command {}) must be "
|
||||
"exactly 1 char!".format(shortname, name,
|
||||
@@ -323,8 +311,8 @@ class Command:
|
||||
if typ is not bool:
|
||||
self.flags_with_args += [short_flag, long_flag]
|
||||
else:
|
||||
args.append(name)
|
||||
self.pos_args.append((param.name, name))
|
||||
if not annotation_info.hide:
|
||||
self.pos_args.append((param.name, name))
|
||||
return args
|
||||
|
||||
def _parse_annotation(self, param):
|
||||
@@ -341,12 +329,12 @@ class Command:
|
||||
flag: The short name/flag if overridden.
|
||||
name: The long name if overridden.
|
||||
"""
|
||||
info = {'kwargs': {}, 'type': None, 'flag': None, 'name': None,
|
||||
'special': None}
|
||||
info = {'kwargs': {}, 'type': None, 'flag': None, 'hide': False,
|
||||
'metavar': None}
|
||||
if param.annotation is not inspect.Parameter.empty:
|
||||
log.commands.vdebug("Parsing annotation {}".format(
|
||||
param.annotation))
|
||||
for field in ('type', 'flag', 'name', 'special'):
|
||||
for field in ('type', 'flag', 'name', 'hide', 'metavar'):
|
||||
if field in param.annotation:
|
||||
info[field] = param.annotation[field]
|
||||
if 'nargs' in param.annotation:
|
||||
@@ -426,19 +414,18 @@ class Command:
|
||||
raise TypeError("{}: invalid parameter type {} for argument "
|
||||
"{!r}!".format(self.name, param.kind, param.name))
|
||||
|
||||
def _get_param_name_and_value(self, param):
|
||||
"""Get the converted name and value for an inspect.Parameter."""
|
||||
name = self._name_conv.get(param.name, param.name)
|
||||
value = getattr(self.namespace, name)
|
||||
def _get_param_value(self, param):
|
||||
"""Get the converted value for an inspect.Parameter."""
|
||||
value = getattr(self.namespace, param.name)
|
||||
if param.name in self._type_conv:
|
||||
# We convert enum types after getting the values from
|
||||
# argparse, because argparse's choices argument is
|
||||
# processed after type conversation, which is not what we
|
||||
# want.
|
||||
value = self._type_conv[param.name](value)
|
||||
return name, value
|
||||
return value
|
||||
|
||||
def _get_call_args(self, win_id): # noqa
|
||||
def _get_call_args(self, win_id):
|
||||
"""Get arguments for a function call.
|
||||
|
||||
Args:
|
||||
@@ -462,22 +449,22 @@ class Command:
|
||||
# Special case for 'self'.
|
||||
self._get_self_arg(win_id, param, args)
|
||||
continue
|
||||
elif param.name == self.special_params['count']:
|
||||
elif param.name == self.count_arg:
|
||||
# Special case for count parameter.
|
||||
self._get_count_arg(param, args, kwargs)
|
||||
continue
|
||||
elif param.name == self.special_params['win_id']:
|
||||
elif param.name == self.win_id_arg:
|
||||
# Special case for win_id parameter.
|
||||
self._get_win_id_arg(win_id, param, args, kwargs)
|
||||
continue
|
||||
name, value = self._get_param_name_and_value(param)
|
||||
value = self._get_param_value(param)
|
||||
if param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD:
|
||||
args.append(value)
|
||||
elif param.kind == inspect.Parameter.VAR_POSITIONAL:
|
||||
if value is not None:
|
||||
args += value
|
||||
elif param.kind == inspect.Parameter.KEYWORD_ONLY:
|
||||
kwargs[name] = value
|
||||
kwargs[param.name] = value
|
||||
else:
|
||||
raise TypeError("{}: Invalid parameter type {} for argument "
|
||||
"'{}'!".format(
|
||||
|
||||
@@ -23,12 +23,12 @@ import os
|
||||
import os.path
|
||||
import tempfile
|
||||
|
||||
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, QObject, QSocketNotifier,
|
||||
QProcessEnvironment, QProcess)
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QSocketNotifier
|
||||
|
||||
from qutebrowser.utils import message, log, objreg, standarddir
|
||||
from qutebrowser.commands import runners, cmdexc
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.misc import guiprocess
|
||||
|
||||
|
||||
class _QtFIFOReader(QObject):
|
||||
@@ -70,13 +70,9 @@ class _BaseUserscriptRunner(QObject):
|
||||
|
||||
Attributes:
|
||||
_filepath: The path of the file/FIFO which is being read.
|
||||
_proc: The QProcess which is being executed.
|
||||
_proc: The GUIProcess which is being executed.
|
||||
_win_id: The window ID this runner is associated with.
|
||||
|
||||
Class attributes:
|
||||
PROCESS_MESSAGES: A mapping of QProcess::ProcessError members to
|
||||
human-readable error strings.
|
||||
|
||||
Signals:
|
||||
got_cmd: Emitted when a new command arrived and should be executed.
|
||||
finished: Emitted when the userscript finished running.
|
||||
@@ -85,82 +81,75 @@ class _BaseUserscriptRunner(QObject):
|
||||
got_cmd = pyqtSignal(str)
|
||||
finished = pyqtSignal()
|
||||
|
||||
PROCESS_MESSAGES = {
|
||||
QProcess.FailedToStart: "The process failed to start.",
|
||||
QProcess.Crashed: "The process crashed.",
|
||||
QProcess.Timedout: "The last waitFor...() function timed out.",
|
||||
QProcess.WriteError: ("An error occurred when attempting to write to "
|
||||
"the process."),
|
||||
QProcess.ReadError: ("An error occurred when attempting to read from "
|
||||
"the process."),
|
||||
QProcess.UnknownError: "An unknown error occurred.",
|
||||
}
|
||||
|
||||
def __init__(self, win_id, parent=None):
|
||||
super().__init__(parent)
|
||||
self._win_id = win_id
|
||||
self._filepath = None
|
||||
self._proc = None
|
||||
self._env = None
|
||||
|
||||
def _run_process(self, cmd, *args, env):
|
||||
"""Start the given command via QProcess.
|
||||
def _run_process(self, cmd, *args, env, verbose):
|
||||
"""Start the given command.
|
||||
|
||||
Args:
|
||||
cmd: The command to be started.
|
||||
*args: The arguments to hand to the command
|
||||
env: A dictionary of environment variables to add.
|
||||
verbose: Show notifications when the command started/exited.
|
||||
"""
|
||||
self._proc = QProcess(self)
|
||||
procenv = QProcessEnvironment.systemEnvironment()
|
||||
procenv.insert('QUTE_FIFO', self._filepath)
|
||||
if env is not None:
|
||||
for k, v in env.items():
|
||||
procenv.insert(k, v)
|
||||
self._proc.setProcessEnvironment(procenv)
|
||||
self._env = {'QUTE_FIFO': self._filepath}
|
||||
self._env.update(env)
|
||||
self._proc = guiprocess.GUIProcess(self._win_id, 'userscript',
|
||||
additional_env=self._env,
|
||||
verbose=verbose, parent=self)
|
||||
self._proc.error.connect(self.on_proc_error)
|
||||
self._proc.finished.connect(self.on_proc_finished)
|
||||
self._proc.start(cmd, args)
|
||||
|
||||
def _cleanup(self):
|
||||
"""Clean up the temporary file."""
|
||||
log.procs.debug("Deleting temporary file {}.".format(self._filepath))
|
||||
try:
|
||||
os.remove(self._filepath)
|
||||
except OSError as e:
|
||||
# NOTE: Do not replace this with "raise CommandError" as it's
|
||||
# executed async.
|
||||
message.error(self._win_id,
|
||||
"Failed to delete tempfile... ({})".format(e))
|
||||
"""Clean up temporary files."""
|
||||
tempfiles = [self._filepath]
|
||||
if 'QUTE_HTML' in self._env:
|
||||
tempfiles.append(self._env['QUTE_HTML'])
|
||||
if 'QUTE_TEXT' in self._env:
|
||||
tempfiles.append(self._env['QUTE_TEXT'])
|
||||
for fn in tempfiles:
|
||||
log.procs.debug("Deleting temporary file {}.".format(fn))
|
||||
try:
|
||||
os.remove(fn)
|
||||
except OSError as e:
|
||||
# NOTE: Do not replace this with "raise CommandError" as it's
|
||||
# executed async.
|
||||
message.error(
|
||||
self._win_id, "Failed to delete tempfile {} ({})!".format(
|
||||
fn, e))
|
||||
self._filepath = None
|
||||
self._proc = None
|
||||
self._env = None
|
||||
|
||||
def run(self, cmd, *args, env=None):
|
||||
def run(self, cmd, *args, env=None, verbose=False):
|
||||
"""Run the userscript given.
|
||||
|
||||
Needs to be overridden by superclasses.
|
||||
Needs to be overridden by subclasses.
|
||||
|
||||
Args:
|
||||
cmd: The command to be started.
|
||||
*args: The arguments to hand to the command
|
||||
env: A dictionary of environment variables to add.
|
||||
verbose: Show notifications when the command started/exited.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def on_proc_finished(self):
|
||||
"""Called when the process has finished.
|
||||
|
||||
Needs to be overridden by superclasses.
|
||||
Needs to be overridden by subclasses.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def on_proc_error(self, error):
|
||||
"""Called when the process encountered an error."""
|
||||
msg = self.PROCESS_MESSAGES[error]
|
||||
# NOTE: Do not replace this with "raise CommandError" as it's
|
||||
# executed async.
|
||||
message.error(self._win_id,
|
||||
"Error while calling userscript: {}".format(msg))
|
||||
log.procs.debug("Userscript process error: {} - {}".format(error, msg))
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class _POSIXUserscriptRunner(_BaseUserscriptRunner):
|
||||
@@ -177,7 +166,7 @@ class _POSIXUserscriptRunner(_BaseUserscriptRunner):
|
||||
super().__init__(win_id, parent)
|
||||
self._reader = None
|
||||
|
||||
def run(self, cmd, *args, env=None):
|
||||
def run(self, cmd, *args, env=None, verbose=False):
|
||||
try:
|
||||
# tempfile.mktemp is deprecated and discouraged, but we use it here
|
||||
# to create a FIFO since the only other alternative would be to
|
||||
@@ -195,16 +184,14 @@ class _POSIXUserscriptRunner(_BaseUserscriptRunner):
|
||||
self._reader = _QtFIFOReader(self._filepath)
|
||||
self._reader.got_line.connect(self.got_cmd)
|
||||
|
||||
self._run_process(cmd, *args, env=env)
|
||||
self._run_process(cmd, *args, env=env, verbose=verbose)
|
||||
|
||||
def on_proc_finished(self):
|
||||
"""Interrupt the reader when the process finished."""
|
||||
log.procs.debug("Userscript process finished.")
|
||||
self.finish()
|
||||
|
||||
def on_proc_error(self, error):
|
||||
"""Interrupt the reader when the process had an error."""
|
||||
super().on_proc_error(error)
|
||||
self.finish()
|
||||
|
||||
def finish(self):
|
||||
@@ -249,7 +236,6 @@ class _WindowsUserscriptRunner(_BaseUserscriptRunner):
|
||||
|
||||
def on_proc_finished(self):
|
||||
"""Read back the commands when the process finished."""
|
||||
log.procs.debug("Userscript process finished.")
|
||||
try:
|
||||
with open(self._filepath, 'r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
@@ -261,18 +247,17 @@ class _WindowsUserscriptRunner(_BaseUserscriptRunner):
|
||||
|
||||
def on_proc_error(self, error):
|
||||
"""Clean up when the process had an error."""
|
||||
super().on_proc_error(error)
|
||||
self._cleanup()
|
||||
self.finished.emit()
|
||||
|
||||
def run(self, cmd, *args, env=None):
|
||||
def run(self, cmd, *args, env=None, verbose=False):
|
||||
try:
|
||||
self._oshandle, self._filepath = tempfile.mkstemp(text=True)
|
||||
except OSError as e:
|
||||
message.error(self._win_id, "Error while creating tempfile: "
|
||||
"{}".format(e))
|
||||
return
|
||||
self._run_process(cmd, *args, env=env)
|
||||
self._run_process(cmd, *args, env=env, verbose=verbose)
|
||||
|
||||
|
||||
class _DummyUserscriptRunner:
|
||||
@@ -288,8 +273,9 @@ class _DummyUserscriptRunner:
|
||||
|
||||
finished = pyqtSignal()
|
||||
|
||||
def run(self, _cmd, *_args, _env=None):
|
||||
def run(self, cmd, *args, env=None, verbose=False):
|
||||
"""Print an error as userscripts are not supported."""
|
||||
# pylint: disable=unused-argument,unused-variable
|
||||
self.finished.emit()
|
||||
raise cmdexc.CommandError(
|
||||
"Userscripts are not supported on this platform!")
|
||||
@@ -305,7 +291,38 @@ else:
|
||||
UserscriptRunner = _DummyUserscriptRunner
|
||||
|
||||
|
||||
def run(cmd, *args, win_id, env):
|
||||
def store_source(frame):
|
||||
"""Store HTML/plaintext in files.
|
||||
|
||||
This writes files containing the HTML/plaintext source of the page, and
|
||||
returns a dict with the paths as QUTE_HTML/QUTE_TEXT.
|
||||
|
||||
Args:
|
||||
frame: The QWebFrame to get the info from, or None to do nothing.
|
||||
|
||||
Return:
|
||||
A dictionary with the needed environment variables.
|
||||
|
||||
Warning:
|
||||
The caller is responsible to delete the files after using them!
|
||||
"""
|
||||
if frame is None:
|
||||
return {}
|
||||
env = {}
|
||||
with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8',
|
||||
suffix='.html',
|
||||
delete=False) as html_file:
|
||||
html_file.write(frame.toHtml())
|
||||
env['QUTE_HTML'] = html_file.name
|
||||
with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8',
|
||||
suffix='.txt',
|
||||
delete=False) as txt_file:
|
||||
txt_file.write(frame.toPlainText())
|
||||
env['QUTE_TEXT'] = txt_file.name
|
||||
return env
|
||||
|
||||
|
||||
def run(cmd, *args, win_id, env, verbose=False):
|
||||
"""Convenience method to run an userscript.
|
||||
|
||||
Args:
|
||||
@@ -313,6 +330,7 @@ def run(cmd, *args, win_id, env):
|
||||
*args: The arguments to pass to the userscript.
|
||||
win_id: The window id the userscript is executed in.
|
||||
env: A dictionary of variables to add to the process environment.
|
||||
verbose: Show notifications when the command started/exited.
|
||||
"""
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=win_id)
|
||||
@@ -325,6 +343,6 @@ def run(cmd, *args, win_id, env):
|
||||
user_agent = config.get('network', 'user-agent')
|
||||
if user_agent is not None:
|
||||
env['QUTE_USER_AGENT'] = user_agent
|
||||
runner.run(cmd, *args, env=env)
|
||||
runner.run(cmd, *args, env=env, verbose=verbose)
|
||||
runner.finished.connect(commandrunner.deleteLater)
|
||||
runner.finished.connect(runner.deleteLater)
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
"""Completer attached to a CompletionView."""
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot, QObject, QTimer
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QTimer
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.commands import cmdutils, runners
|
||||
@@ -40,14 +40,22 @@ class Completer(QObject):
|
||||
_last_cursor_pos: The old cursor position so we avoid double completion
|
||||
updates.
|
||||
_last_text: The old command text so we avoid double completion updates.
|
||||
_signals_connected: Whether the signals are connected to update the
|
||||
completion when the command widget requests that.
|
||||
|
||||
Signals:
|
||||
next_prev_item: Emitted to select the next/previous item in the
|
||||
completion.
|
||||
arg0: True for the previous item, False for the next.
|
||||
"""
|
||||
|
||||
next_prev_item = pyqtSignal(bool)
|
||||
|
||||
def __init__(self, cmd, win_id, parent=None):
|
||||
super().__init__(parent)
|
||||
self._win_id = win_id
|
||||
self._cmd = cmd
|
||||
self._cmd.update_completion.connect(self.schedule_completion_update)
|
||||
self._cmd.textEdited.connect(self.on_text_edited)
|
||||
self._signals_connected = False
|
||||
self._ignore_change = False
|
||||
self._empty_item_idx = None
|
||||
self._timer = QTimer()
|
||||
@@ -58,9 +66,63 @@ class Completer(QObject):
|
||||
self._last_cursor_pos = None
|
||||
self._last_text = None
|
||||
|
||||
objreg.get('config').changed.connect(self.on_auto_open_changed)
|
||||
self.handle_signal_connections()
|
||||
self._cmd.clear_completion_selection.connect(
|
||||
self.handle_signal_connections)
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self)
|
||||
|
||||
@config.change_filter('completion', 'auto-open')
|
||||
def on_auto_open_changed(self):
|
||||
self.handle_signal_connections()
|
||||
|
||||
@pyqtSlot()
|
||||
def handle_signal_connections(self):
|
||||
self._connect_signals(config.get('completion', 'auto-open'))
|
||||
|
||||
def _connect_signals(self, connect=True):
|
||||
"""Connect or disconnect the completion signals.
|
||||
|
||||
Args:
|
||||
connect: Whether to connect (True) or disconnect (False) the
|
||||
signals.
|
||||
|
||||
Return:
|
||||
True if the signals were connected (connect=True and aren't
|
||||
connected yet) - otherwise False.
|
||||
"""
|
||||
connections = [
|
||||
(self._cmd.update_completion, self.schedule_completion_update),
|
||||
(self._cmd.textChanged, self.on_text_edited),
|
||||
]
|
||||
|
||||
if connect and not self._signals_connected:
|
||||
for sender, receiver in connections:
|
||||
sender.connect(receiver)
|
||||
self._signals_connected = True
|
||||
return True
|
||||
elif not connect:
|
||||
for sender, receiver in connections:
|
||||
try:
|
||||
sender.disconnect(receiver)
|
||||
except TypeError:
|
||||
# Don't fail if not connected
|
||||
pass
|
||||
self._signals_connected = False
|
||||
return False
|
||||
|
||||
def _open_completion_if_needed(self):
|
||||
"""If auto-open is false, temporarily connect signals.
|
||||
|
||||
Also opens the completion.
|
||||
"""
|
||||
if not config.get('completion', 'auto-open'):
|
||||
connected = self._connect_signals(True)
|
||||
if connected:
|
||||
self.update_completion()
|
||||
|
||||
def _model(self):
|
||||
"""Convienience method to get the current completion model."""
|
||||
completion = objreg.get('completion', scope='window',
|
||||
@@ -272,7 +334,7 @@ class Completer(QObject):
|
||||
pattern = parts[self._cursor_part].strip()
|
||||
except IndexError:
|
||||
pattern = ''
|
||||
self._model().set_pattern(pattern)
|
||||
completion.set_pattern(pattern)
|
||||
|
||||
log.completion.debug(
|
||||
"New completion for {}: {}, with pattern '{}'".format(
|
||||
@@ -328,7 +390,7 @@ class Completer(QObject):
|
||||
cursor_pos))
|
||||
skip = 0
|
||||
for i, part in enumerate(parts):
|
||||
log.completion.vdebug("Checking part {}: {}".format(i, parts[i]))
|
||||
log.completion.vdebug("Checking part {}: {!r}".format(i, parts[i]))
|
||||
if not part:
|
||||
skip += 1
|
||||
continue
|
||||
@@ -350,7 +412,11 @@ class Completer(QObject):
|
||||
"Removing len({!r}) -> {} from cursor_pos -> {}".format(
|
||||
part, len(part), cursor_pos))
|
||||
else:
|
||||
self._cursor_part = i - skip
|
||||
if i == 0:
|
||||
# Initial `:` press without any text.
|
||||
self._cursor_part = 0
|
||||
else:
|
||||
self._cursor_part = i - skip
|
||||
if spaces:
|
||||
self._empty_item_idx = i - skip
|
||||
else:
|
||||
@@ -401,3 +467,17 @@ class Completer(QObject):
|
||||
# We also want to update the cursor part and emit update_completion
|
||||
# here, but that's already done for us by cursorPositionChanged
|
||||
# anyways, so we don't need to do it twice.
|
||||
|
||||
@cmdutils.register(instance='completer', hide=True,
|
||||
modes=[usertypes.KeyMode.command], scope='window')
|
||||
def completion_item_prev(self):
|
||||
"""Select the previous completion item."""
|
||||
self._open_completion_if_needed()
|
||||
self.next_prev_item.emit(True)
|
||||
|
||||
@cmdutils.register(instance='completer', hide=True,
|
||||
modes=[usertypes.KeyMode.command], scope='window')
|
||||
def completion_item_next(self):
|
||||
"""Select the next completion item."""
|
||||
self._open_completion_if_needed()
|
||||
self.next_prev_item.emit(False)
|
||||
|
||||
@@ -145,7 +145,6 @@ class CompletionItemDelegate(QStyledItemDelegate):
|
||||
rect: The QRect to clip the drawing to.
|
||||
"""
|
||||
# We can't use drawContents because then the color would be ignored.
|
||||
# See: https://qt-project.org/forums/viewthread/21492
|
||||
clip = QRectF(0, 0, rect.width(), rect.height())
|
||||
self._painter.save()
|
||||
if self._opt.state & QStyle.State_Selected:
|
||||
|
||||
@@ -26,10 +26,9 @@ subclasses to provide completions.
|
||||
from PyQt5.QtWidgets import QStyle, QTreeView, QSizePolicy
|
||||
from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QItemSelectionModel
|
||||
|
||||
from qutebrowser.commands import cmdutils
|
||||
from qutebrowser.config import config, style
|
||||
from qutebrowser.completion import completiondelegate, completer
|
||||
from qutebrowser.utils import usertypes, qtutils, objreg, utils
|
||||
from qutebrowser.utils import qtutils, objreg, utils
|
||||
|
||||
|
||||
class CompletionView(QTreeView):
|
||||
@@ -96,12 +95,13 @@ class CompletionView(QTreeView):
|
||||
objreg.register('completion', self, scope='window', window=win_id)
|
||||
cmd = objreg.get('status-command', scope='window', window=win_id)
|
||||
completer_obj = completer.Completer(cmd, win_id, self)
|
||||
completer_obj.next_prev_item.connect(self.on_next_prev_item)
|
||||
objreg.register('completer', completer_obj, scope='window',
|
||||
window=win_id)
|
||||
self.enabled = config.get('completion', 'show')
|
||||
objreg.get('config').changed.connect(self.set_enabled)
|
||||
# FIXME handle new aliases.
|
||||
#objreg.get('config').changed.connect(self.init_command_completion)
|
||||
# objreg.get('config').changed.connect(self.init_command_completion)
|
||||
|
||||
self._delegate = completiondelegate.CompletionItemDelegate(self)
|
||||
self.setItemDelegate(self._delegate)
|
||||
@@ -168,12 +168,15 @@ class CompletionView(QTreeView):
|
||||
# Item is a real item, not a category header -> success
|
||||
return idx
|
||||
|
||||
def _next_prev_item(self, prev):
|
||||
@pyqtSlot(bool)
|
||||
def on_next_prev_item(self, prev):
|
||||
"""Handle a tab press for the CompletionView.
|
||||
|
||||
Select the previous/next item and write the new text to the
|
||||
statusbar.
|
||||
|
||||
Called from the Completer's next_prev_item signal.
|
||||
|
||||
Args:
|
||||
prev: True for prev item, False for next one.
|
||||
"""
|
||||
@@ -201,8 +204,17 @@ class CompletionView(QTreeView):
|
||||
for i in range(model.rowCount()):
|
||||
self.expand(model.index(i, 0))
|
||||
self._resize_columns()
|
||||
model.rowsRemoved.connect(self.maybe_resize_completion)
|
||||
model.rowsInserted.connect(self.maybe_resize_completion)
|
||||
self.maybe_resize_completion()
|
||||
|
||||
def set_pattern(self, pattern):
|
||||
"""Set the completion pattern for the current model.
|
||||
|
||||
Called from on_update_completion().
|
||||
|
||||
Args:
|
||||
pattern: The filter pattern to set (what the user entered).
|
||||
"""
|
||||
self.model().set_pattern(pattern)
|
||||
self.maybe_resize_completion()
|
||||
|
||||
@pyqtSlot()
|
||||
@@ -224,18 +236,6 @@ class CompletionView(QTreeView):
|
||||
selmod.clearSelection()
|
||||
selmod.clearCurrentIndex()
|
||||
|
||||
@cmdutils.register(instance='completion', hide=True,
|
||||
modes=[usertypes.KeyMode.command], scope='window')
|
||||
def completion_item_prev(self):
|
||||
"""Select the previous completion item."""
|
||||
self._next_prev_item(prev=True)
|
||||
|
||||
@cmdutils.register(instance='completion', hide=True,
|
||||
modes=[usertypes.KeyMode.command], scope='window')
|
||||
def completion_item_next(self):
|
||||
"""Select the next completion item."""
|
||||
self._next_prev_item(prev=False)
|
||||
|
||||
def selectionChanged(self, selected, deselected):
|
||||
"""Extend selectionChanged to call completers selection_changed."""
|
||||
super().selectionChanged(selected, deselected)
|
||||
|
||||
@@ -109,7 +109,8 @@ class BaseCompletionModel(QStandardItemModel):
|
||||
qtutils.ensure_valid(index)
|
||||
if index.parent().isValid():
|
||||
# item
|
||||
return Qt.ItemIsEnabled | Qt.ItemIsSelectable
|
||||
return (Qt.ItemIsEnabled | Qt.ItemIsSelectable |
|
||||
Qt.ItemNeverHasChildren)
|
||||
else:
|
||||
# category
|
||||
return Qt.NoItemFlags
|
||||
|
||||
@@ -165,6 +165,11 @@ def init():
|
||||
quickmark_manager.changed.connect(
|
||||
functools.partial(update, [usertypes.Completion.quickmark_by_url,
|
||||
usertypes.Completion.quickmark_by_name]))
|
||||
|
||||
session_manager = objreg.get('session-manager')
|
||||
session_manager.update_completion.connect(
|
||||
functools.partial(update, [usertypes.Completion.sessions]))
|
||||
|
||||
history = objreg.get('web-history')
|
||||
history.async_read_done.connect(
|
||||
functools.partial(update, [usertypes.Completion.url]))
|
||||
|
||||
@@ -54,7 +54,7 @@ class UrlCompletionModel(base.BaseCompletionModel):
|
||||
history = utils.newest_slice(self._history, max_history)
|
||||
for entry in history:
|
||||
self._add_history_entry(entry)
|
||||
self._history.item_about_to_be_added.connect(
|
||||
self._history.add_completion_item.connect(
|
||||
self.on_history_item_added)
|
||||
|
||||
objreg.get('config').changed.connect(self.reformat_timestamps)
|
||||
|
||||
@@ -33,12 +33,12 @@ import collections
|
||||
import collections.abc
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QUrl, QSettings
|
||||
from PyQt5.QtWidgets import QMessageBox
|
||||
|
||||
from qutebrowser.config import configdata, configexc, textwrapper
|
||||
from qutebrowser.config.parsers import ini, keyconf
|
||||
from qutebrowser.commands import cmdexc, cmdutils
|
||||
from qutebrowser.utils import message, objreg, utils, standarddir, log, qtutils
|
||||
from qutebrowser.utils import (message, objreg, utils, standarddir, log,
|
||||
qtutils, error, usertypes)
|
||||
from qutebrowser.utils.usertypes import Completion
|
||||
|
||||
|
||||
@@ -52,9 +52,10 @@ class change_filter: # pylint: disable=invalid-name
|
||||
Attributes:
|
||||
_sectname: The section to be filtered.
|
||||
_optname: The option to be filtered.
|
||||
_function: Whether a function rather than a method is decorated.
|
||||
"""
|
||||
|
||||
def __init__(self, sectname, optname=None):
|
||||
def __init__(self, sectname, optname=None, function=False):
|
||||
"""Save decorator arguments.
|
||||
|
||||
Gets called on parse-time with the decorator arguments.
|
||||
@@ -62,6 +63,7 @@ class change_filter: # pylint: disable=invalid-name
|
||||
Args:
|
||||
sectname: The section to be filtered.
|
||||
optname: The option to be filtered.
|
||||
function: Whether a function rather than a method is decorated.
|
||||
"""
|
||||
if sectname not in configdata.DATA:
|
||||
raise configexc.NoSectionError(sectname)
|
||||
@@ -69,6 +71,7 @@ class change_filter: # pylint: disable=invalid-name
|
||||
raise configexc.NoOptionError(optname, sectname)
|
||||
self._sectname = sectname
|
||||
self._optname = optname
|
||||
self._function = function
|
||||
|
||||
def __call__(self, func):
|
||||
"""Filter calls to the decorated function.
|
||||
@@ -86,19 +89,34 @@ class change_filter: # pylint: disable=invalid-name
|
||||
Return:
|
||||
The decorated function.
|
||||
"""
|
||||
@pyqtSlot(str, str)
|
||||
@functools.wraps(func)
|
||||
def wrapper(wrapper_self, sectname=None, optname=None):
|
||||
# pylint: disable=missing-docstring
|
||||
if sectname is None and optname is None:
|
||||
# Called directly, not from a config change event.
|
||||
return func(wrapper_self)
|
||||
elif sectname != self._sectname:
|
||||
return
|
||||
elif self._optname is not None and optname != self._optname:
|
||||
return
|
||||
else:
|
||||
return func(wrapper_self)
|
||||
if self._function:
|
||||
@pyqtSlot(str, str)
|
||||
@functools.wraps(func)
|
||||
def wrapper(sectname=None, optname=None):
|
||||
# pylint: disable=missing-docstring
|
||||
if sectname is None and optname is None:
|
||||
# Called directly, not from a config change event.
|
||||
return func()
|
||||
elif sectname != self._sectname:
|
||||
return
|
||||
elif self._optname is not None and optname != self._optname:
|
||||
return
|
||||
else:
|
||||
return func()
|
||||
else:
|
||||
@pyqtSlot(str, str)
|
||||
@functools.wraps(func)
|
||||
def wrapper(wrapper_self, sectname=None, optname=None):
|
||||
# pylint: disable=missing-docstring
|
||||
if sectname is None and optname is None:
|
||||
# Called directly, not from a config change event.
|
||||
return func(wrapper_self)
|
||||
elif sectname != self._sectname:
|
||||
return
|
||||
elif self._optname is not None and optname != self._optname:
|
||||
return
|
||||
else:
|
||||
return func(wrapper_self)
|
||||
|
||||
return wrapper
|
||||
|
||||
@@ -119,8 +137,8 @@ def _init_main_config(parent=None):
|
||||
Args:
|
||||
parent: The parent to pass to ConfigManager.
|
||||
"""
|
||||
args = objreg.get('args')
|
||||
try:
|
||||
args = objreg.get('args')
|
||||
config_obj = ConfigManager(standarddir.config(), 'qutebrowser.conf',
|
||||
args.relaxed_config, parent=parent)
|
||||
except (configexc.Error, configparser.Error, UnicodeDecodeError) as e:
|
||||
@@ -131,12 +149,11 @@ def _init_main_config(parent=None):
|
||||
e.section, e.option) # pylint: disable=no-member
|
||||
except AttributeError:
|
||||
pass
|
||||
errstr += "\n{}".format(e)
|
||||
msgbox = QMessageBox(QMessageBox.Critical,
|
||||
"Error while reading config!", errstr)
|
||||
msgbox.exec_()
|
||||
errstr += "\n"
|
||||
error.handle_fatal_exc(e, args, "Error while reading config!",
|
||||
pre_text=errstr)
|
||||
# We didn't really initialize much so far, so we just quit hard.
|
||||
sys.exit(1)
|
||||
sys.exit(usertypes.Exit.err_config)
|
||||
else:
|
||||
objreg.register('config', config_obj)
|
||||
if standarddir.config() is not None:
|
||||
@@ -160,20 +177,20 @@ def _init_key_config(parent):
|
||||
Args:
|
||||
parent: The parent to use for the KeyConfigParser.
|
||||
"""
|
||||
args = objreg.get('args')
|
||||
try:
|
||||
key_config = keyconf.KeyConfigParser(standarddir.config(), 'keys.conf',
|
||||
args.relaxed_config,
|
||||
parent=parent)
|
||||
except (keyconf.KeyConfigError, UnicodeDecodeError) as e:
|
||||
log.init.exception(e)
|
||||
errstr = "Error while reading key config:\n"
|
||||
if e.lineno is not None:
|
||||
errstr += "In line {}: ".format(e.lineno)
|
||||
errstr += str(e)
|
||||
msgbox = QMessageBox(QMessageBox.Critical,
|
||||
"Error while reading key config!", errstr)
|
||||
msgbox.exec_()
|
||||
error.handle_fatal_exc(e, args, "Error while reading key config!",
|
||||
pre_text=errstr)
|
||||
# We didn't really initialize much so far, so we just quit hard.
|
||||
sys.exit(1)
|
||||
sys.exit(usertypes.Exit.err_key_config)
|
||||
else:
|
||||
objreg.register('key-config', key_config)
|
||||
if standarddir.config() is not None:
|
||||
@@ -235,6 +252,25 @@ def init(parent=None):
|
||||
_init_misc()
|
||||
|
||||
|
||||
def _get_value_transformer(old, new):
|
||||
"""Get a function which transforms a value for CHANGED_OPTIONS.
|
||||
|
||||
Args:
|
||||
old: The old value - if the supplied value doesn't match this, it's
|
||||
returned untransformed.
|
||||
new: The new value.
|
||||
|
||||
Return:
|
||||
A function which takes a value and transforms it.
|
||||
"""
|
||||
def transformer(val):
|
||||
if val == old:
|
||||
return new
|
||||
else:
|
||||
return val
|
||||
return transformer
|
||||
|
||||
|
||||
class ConfigManager(QObject):
|
||||
|
||||
"""Configuration manager for qutebrowser.
|
||||
@@ -246,6 +282,10 @@ class ConfigManager(QObject):
|
||||
RENAMED_SECTIONS: A mapping of renamed sections, {'oldname': 'newname'}
|
||||
RENAMED_OPTIONS: A mapping of renamed options,
|
||||
{('section', 'oldname'): 'newname'}
|
||||
CHANGED_OPTIONS: A mapping of arbitrarily changed options,
|
||||
{('section', 'option'): callable}.
|
||||
The callable takes the old value and returns the new
|
||||
one.
|
||||
DELETED_OPTIONS: A (section, option) list of deleted options.
|
||||
|
||||
Attributes:
|
||||
@@ -281,12 +321,17 @@ class ConfigManager(QObject):
|
||||
('colors', 'tab.indicator.system'): 'tabs.indicator.system',
|
||||
('tabs', 'auto-hide'): 'hide-auto',
|
||||
('completion', 'history-length'): 'cmd-history-max-items',
|
||||
('colors', 'downloads.fg'): 'downloads.fg.start',
|
||||
}
|
||||
DELETED_OPTIONS = [
|
||||
('colors', 'tab.separator'),
|
||||
('colors', 'tabs.separator'),
|
||||
('colors', 'completion.item.bg'),
|
||||
]
|
||||
CHANGED_OPTIONS = {
|
||||
('content', 'cookies-accept'):
|
||||
_get_value_transformer('default', 'no-3rdparty'),
|
||||
}
|
||||
|
||||
changed = pyqtSignal(str, str)
|
||||
style_changed = pyqtSignal(str, str)
|
||||
@@ -445,10 +490,15 @@ class ConfigManager(QObject):
|
||||
for k, v in cp[real_sectname].items():
|
||||
if k.startswith(self.ESCAPE_CHAR):
|
||||
k = k[1:]
|
||||
|
||||
if (sectname, k) in self.DELETED_OPTIONS:
|
||||
return
|
||||
elif (sectname, k) in self.RENAMED_OPTIONS:
|
||||
if (sectname, k) in self.RENAMED_OPTIONS:
|
||||
k = self.RENAMED_OPTIONS[sectname, k]
|
||||
if (sectname, k) in self.CHANGED_OPTIONS:
|
||||
func = self.CHANGED_OPTIONS[(sectname, k)]
|
||||
v = func(v)
|
||||
|
||||
try:
|
||||
self.set('conf', sectname, k, v, validate=False)
|
||||
except configexc.NoOptionError:
|
||||
@@ -579,13 +629,11 @@ class ConfigManager(QObject):
|
||||
newval = val.typ.transform(newval)
|
||||
return newval
|
||||
|
||||
@cmdutils.register(name='set', instance='config',
|
||||
@cmdutils.register(name='set', instance='config', win_id='win_id',
|
||||
completion=[Completion.section, Completion.option,
|
||||
Completion.value])
|
||||
def set_command(self, win_id: {'special': 'win_id'},
|
||||
sectname: {'name': 'section'}=None,
|
||||
optname: {'name': 'option'}=None, value=None, temp=False,
|
||||
print_val: {'name': 'print'}=False):
|
||||
def set_command(self, win_id, section_=None, option=None, value=None,
|
||||
temp=False, print_=False):
|
||||
"""Set an option.
|
||||
|
||||
If the option name ends with '?', the value of the option is shown
|
||||
@@ -598,38 +646,38 @@ class ConfigManager(QObject):
|
||||
Wrapper for self.set() to output exceptions in the status bar.
|
||||
|
||||
Args:
|
||||
sectname: The section where the option is in.
|
||||
optname: The name of the option.
|
||||
section_: The section where the option is in.
|
||||
option: The name of the option.
|
||||
value: The value to set.
|
||||
temp: Set value temporarily.
|
||||
print_val: Print the value after setting.
|
||||
print_: Print the value after setting.
|
||||
"""
|
||||
if sectname is not None and optname is None:
|
||||
if section_ is not None and option is None:
|
||||
raise cmdexc.CommandError(
|
||||
"set: Either both section and option have to be given, or "
|
||||
"neither!")
|
||||
if sectname is None and optname is None:
|
||||
if section_ is None and option is None:
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=win_id)
|
||||
tabbed_browser.openurl(QUrl('qute:settings'), newtab=False)
|
||||
return
|
||||
|
||||
if optname.endswith('?'):
|
||||
optname = optname[:-1]
|
||||
print_val = True
|
||||
if option.endswith('?'):
|
||||
option = option[:-1]
|
||||
print_ = True
|
||||
else:
|
||||
try:
|
||||
if optname.endswith('!') and value is None:
|
||||
val = self.get(sectname, optname[:-1])
|
||||
if option.endswith('!') and value is None:
|
||||
val = self.get(section_, option[:-1])
|
||||
layer = 'temp' if temp else 'conf'
|
||||
if isinstance(val, bool):
|
||||
self.set(layer, sectname, optname[:-1], str(not val))
|
||||
self.set(layer, section_, option[:-1], str(not val))
|
||||
else:
|
||||
raise cmdexc.CommandError(
|
||||
"set: Attempted inversion of non-boolean value.")
|
||||
elif value is not None:
|
||||
layer = 'temp' if temp else 'conf'
|
||||
self.set(layer, sectname, optname, value)
|
||||
self.set(layer, section_, option, value)
|
||||
else:
|
||||
raise cmdexc.CommandError("set: The following arguments "
|
||||
"are required: value")
|
||||
@@ -637,10 +685,10 @@ class ConfigManager(QObject):
|
||||
raise cmdexc.CommandError("set: {} - {}".format(
|
||||
e.__class__.__name__, e))
|
||||
|
||||
if print_val:
|
||||
val = self.get(sectname, optname, transformed=False)
|
||||
if print_:
|
||||
val = self.get(section_, option, transformed=False)
|
||||
message.info(win_id, "{} {} = {}".format(
|
||||
sectname, optname, val), immediately=True)
|
||||
section_, option, val), immediately=True)
|
||||
|
||||
def set(self, layer, sectname, optname, value, validate=True):
|
||||
"""Set an option.
|
||||
|
||||
@@ -100,9 +100,13 @@ SECTION_DESC = {
|
||||
" * `rgb(r, g, b)` / `rgba(r, g, b, a)` (values 0-255 or "
|
||||
"percentages)\n"
|
||||
" * `hsv(h, s, v)` / `hsva(h, s, v, a)` (values 0-255, hue 0-359)\n"
|
||||
" * A gradient as explained in http://qt-project.org/doc/qt-4.8/"
|
||||
" * A gradient as explained in http://doc.qt.io/qt-5/"
|
||||
"stylesheet-reference.html#list-of-property-types[the Qt "
|
||||
"documentation] under ``Gradient''.\n\n"
|
||||
"A *.system value determines the color system to use for color "
|
||||
"interpolation between similarly-named *.start and *.stop entries, "
|
||||
"regardless of how they are defined in the options. "
|
||||
"Valid values are 'rgb', 'hsv', and 'hsl'.\n\n"
|
||||
"The `hints.*` values are a special case as they're real CSS "
|
||||
"colors, not Qt-CSS colors. There, for a gradient, you need to use "
|
||||
"`-webkit-gradient`, see https://www.webkit.org/blog/175/introducing-"
|
||||
@@ -204,7 +208,7 @@ def data(readonly=False):
|
||||
"be used."),
|
||||
|
||||
('new-instance-open-target',
|
||||
SettingValue(typ.NewInstanceOpenTarget(), 'window'),
|
||||
SettingValue(typ.NewInstanceOpenTarget(), 'tab'),
|
||||
"How to open links in an existing instance if a new one is "
|
||||
"launched."),
|
||||
|
||||
@@ -269,13 +273,18 @@ def data(readonly=False):
|
||||
('user-stylesheet',
|
||||
SettingValue(typ.UserStyleSheet(),
|
||||
'::-webkit-scrollbar { width: 0px; height: 0px; }'),
|
||||
"User stylesheet to use (absolute filename or CSS string). Will "
|
||||
"expand environment variables."),
|
||||
"User stylesheet to use (absolute filename, filename relative to "
|
||||
"the config directory or CSS string). Will expand environment "
|
||||
"variables."),
|
||||
|
||||
('css-media-type',
|
||||
SettingValue(typ.String(none_ok=True), ''),
|
||||
"Set the CSS media type."),
|
||||
|
||||
('smooth-scrolling',
|
||||
SettingValue(typ.Bool(), 'false'),
|
||||
"Whether to enable smooth scrolling for webpages."),
|
||||
|
||||
('remove-finished-downloads',
|
||||
SettingValue(typ.Bool(), 'false'),
|
||||
"Whether to remove finished downloads automatically."),
|
||||
@@ -301,6 +310,10 @@ def data(readonly=False):
|
||||
SettingValue(typ.Bool(), 'false'),
|
||||
"Whether to hide the mouse cursor."),
|
||||
|
||||
('modal-js-dialog',
|
||||
SettingValue(typ.Bool(), 'false'),
|
||||
"Use standard JavaScript modal dialog for alert() and confirm()"),
|
||||
|
||||
readonly=readonly
|
||||
)),
|
||||
|
||||
@@ -339,6 +352,10 @@ def data(readonly=False):
|
||||
)),
|
||||
|
||||
('completion', sect.KeyValue(
|
||||
('auto-open',
|
||||
SettingValue(typ.Bool(), 'true'),
|
||||
"Automatically open completion when typing."),
|
||||
|
||||
('download-path-suggestion',
|
||||
SettingValue(typ.DownloadPathSuggestion(), 'path'),
|
||||
"What to display in the download filename input."),
|
||||
@@ -456,7 +473,7 @@ def data(readonly=False):
|
||||
|
||||
('last-close',
|
||||
SettingValue(typ.LastClose(), 'ignore'),
|
||||
"Behaviour when the last tab is closed."),
|
||||
"Behavior when the last tab is closed."),
|
||||
|
||||
('hide-auto',
|
||||
SettingValue(typ.Bool(), 'false'),
|
||||
@@ -518,6 +535,10 @@ def data(readonly=False):
|
||||
"* `{index}`: The index of this tab.\n"
|
||||
"* `{id}`: The internal tab ID of this tab."),
|
||||
|
||||
('mousewheel-tab-switching',
|
||||
SettingValue(typ.Bool(), 'true'),
|
||||
"Switch between tabs using the mouse wheel."),
|
||||
|
||||
readonly=readonly
|
||||
)),
|
||||
|
||||
@@ -605,12 +626,24 @@ def data(readonly=False):
|
||||
'Qt plugins with a mimetype such as "application/x-qt-plugin" '
|
||||
"are not affected by this setting."),
|
||||
|
||||
('webgl',
|
||||
SettingValue(typ.Bool(), 'true'),
|
||||
"Enables or disables WebGL."),
|
||||
|
||||
('css-regions',
|
||||
SettingValue(typ.Bool(), 'true'),
|
||||
"Enable or disable support for CSS regions."),
|
||||
|
||||
('hyperlink-auditing',
|
||||
SettingValue(typ.Bool(), 'false'),
|
||||
"Enable or disable hyperlink auditing (<a ping>)."),
|
||||
|
||||
('geolocation',
|
||||
SettingValue(typ.NoAsk(), 'ask'),
|
||||
SettingValue(typ.BoolAsk(), 'ask'),
|
||||
"Allow websites to request geolocations."),
|
||||
|
||||
('notifications',
|
||||
SettingValue(typ.NoAsk(), 'ask'),
|
||||
SettingValue(typ.BoolAsk(), 'ask'),
|
||||
"Allow websites to show notifications."),
|
||||
|
||||
#('allow-java',
|
||||
@@ -650,8 +683,8 @@ def data(readonly=False):
|
||||
"local urls."),
|
||||
|
||||
('cookies-accept',
|
||||
SettingValue(typ.AcceptCookies(), 'default'),
|
||||
"Whether to accept cookies."),
|
||||
SettingValue(typ.AcceptCookies(), 'no-3rdparty'),
|
||||
"Control which cookies to accept."),
|
||||
|
||||
('cookies-store',
|
||||
SettingValue(typ.Bool(), 'true'),
|
||||
@@ -716,7 +749,8 @@ def data(readonly=False):
|
||||
|
||||
('next-regexes',
|
||||
SettingValue(typ.RegexList(flags=re.IGNORECASE),
|
||||
r'\bnext\b,\bmore\b,\bnewer\b,\b[>→≫]\b,\b(>>|»)\b'),
|
||||
r'\bnext\b,\bmore\b,\bnewer\b,\b[>→≫]\b,\b(>>|»)\b,'
|
||||
r'\bcontinue\b'),
|
||||
"A comma-separated list of regexes to use for 'next' links."),
|
||||
|
||||
('prev-regexes',
|
||||
@@ -792,30 +826,72 @@ def data(readonly=False):
|
||||
SettingValue(typ.QssColor(), '#ff4444'),
|
||||
"Foreground color of the matched text in the completion."),
|
||||
|
||||
('statusbar.fg',
|
||||
SettingValue(typ.QssColor(), 'white'),
|
||||
"Foreground color of the statusbar."),
|
||||
|
||||
('statusbar.bg',
|
||||
SettingValue(typ.QssColor(), 'black'),
|
||||
"Foreground color of the statusbar."),
|
||||
|
||||
('statusbar.fg',
|
||||
SettingValue(typ.QssColor(), 'white'),
|
||||
"Foreground color of the statusbar."),
|
||||
('statusbar.fg.error',
|
||||
SettingValue(typ.QssColor(), '${statusbar.fg}'),
|
||||
"Foreground color of the statusbar if there was an error."),
|
||||
|
||||
('statusbar.bg.error',
|
||||
SettingValue(typ.QssColor(), 'red'),
|
||||
"Background color of the statusbar if there was an error."),
|
||||
|
||||
('statusbar.fg.warning',
|
||||
SettingValue(typ.QssColor(), '${statusbar.fg}'),
|
||||
"Foreground color of the statusbar if there is a warning."),
|
||||
|
||||
('statusbar.bg.warning',
|
||||
SettingValue(typ.QssColor(), 'darkorange'),
|
||||
"Background color of the statusbar if there is a warning."),
|
||||
|
||||
('statusbar.fg.prompt',
|
||||
SettingValue(typ.QssColor(), '${statusbar.fg}'),
|
||||
"Foreground color of the statusbar if there is a prompt."),
|
||||
|
||||
('statusbar.bg.prompt',
|
||||
SettingValue(typ.QssColor(), 'darkblue'),
|
||||
"Background color of the statusbar if there is a prompt."),
|
||||
|
||||
('statusbar.fg.insert',
|
||||
SettingValue(typ.QssColor(), '${statusbar.fg}'),
|
||||
"Foreground color of the statusbar in insert mode."),
|
||||
|
||||
('statusbar.bg.insert',
|
||||
SettingValue(typ.QssColor(), 'darkgreen'),
|
||||
"Background color of the statusbar in insert mode."),
|
||||
|
||||
('statusbar.fg.command',
|
||||
SettingValue(typ.QssColor(), '${statusbar.fg}'),
|
||||
"Foreground color of the statusbar in command mode."),
|
||||
|
||||
('statusbar.bg.command',
|
||||
SettingValue(typ.QssColor(), '${statusbar.bg}'),
|
||||
"Background color of the statusbar in command mode."),
|
||||
|
||||
('statusbar.fg.caret',
|
||||
SettingValue(typ.QssColor(), '${statusbar.fg}'),
|
||||
"Foreground color of the statusbar in caret mode."),
|
||||
|
||||
('statusbar.bg.caret',
|
||||
SettingValue(typ.QssColor(), 'purple'),
|
||||
"Background color of the statusbar in caret mode."),
|
||||
|
||||
('statusbar.fg.caret-selection',
|
||||
SettingValue(typ.QssColor(), '${statusbar.fg}'),
|
||||
"Foreground color of the statusbar in caret mode with a "
|
||||
"selection"),
|
||||
|
||||
('statusbar.bg.caret-selection',
|
||||
SettingValue(typ.QssColor(), '#a12dff'),
|
||||
"Background color of the statusbar in caret mode with a "
|
||||
"selection"),
|
||||
|
||||
('statusbar.progress.bg',
|
||||
SettingValue(typ.QssColor(), 'white'),
|
||||
"Background color of the progress bar."),
|
||||
@@ -847,22 +923,22 @@ def data(readonly=False):
|
||||
SettingValue(typ.QtColor(), 'white'),
|
||||
"Foreground color of unselected odd tabs."),
|
||||
|
||||
('tabs.fg.even',
|
||||
SettingValue(typ.QtColor(), 'white'),
|
||||
"Foreground color of unselected even tabs."),
|
||||
|
||||
('tabs.fg.selected',
|
||||
SettingValue(typ.QtColor(), 'white'),
|
||||
"Foreground color of selected tabs."),
|
||||
|
||||
('tabs.bg.odd',
|
||||
SettingValue(typ.QtColor(), 'grey'),
|
||||
"Background color of unselected odd tabs."),
|
||||
|
||||
('tabs.fg.even',
|
||||
SettingValue(typ.QtColor(), 'white'),
|
||||
"Foreground color of unselected even tabs."),
|
||||
|
||||
('tabs.bg.even',
|
||||
SettingValue(typ.QtColor(), 'darkgrey'),
|
||||
"Background color of unselected even tabs."),
|
||||
|
||||
('tabs.fg.selected',
|
||||
SettingValue(typ.QtColor(), 'white'),
|
||||
"Foreground color of selected tabs."),
|
||||
|
||||
('tabs.bg.selected',
|
||||
SettingValue(typ.QtColor(), 'black'),
|
||||
"Background color of selected tabs."),
|
||||
@@ -891,10 +967,6 @@ def data(readonly=False):
|
||||
SettingValue(typ.CssColor(), 'black'),
|
||||
"Font color for hints."),
|
||||
|
||||
('hints.fg.match',
|
||||
SettingValue(typ.CssColor(), 'green'),
|
||||
"Font color for the matched part of hints."),
|
||||
|
||||
('hints.bg',
|
||||
SettingValue(
|
||||
typ.CssColor(), '-webkit-gradient(linear, left top, '
|
||||
@@ -902,30 +974,51 @@ def data(readonly=False):
|
||||
'color-stop(100%,#FFC542))'),
|
||||
"Background color for hints."),
|
||||
|
||||
('downloads.fg',
|
||||
SettingValue(typ.QtColor(), '#ffffff'),
|
||||
"Foreground color for downloads."),
|
||||
('hints.fg.match',
|
||||
SettingValue(typ.CssColor(), 'green'),
|
||||
"Font color for the matched part of hints."),
|
||||
|
||||
('downloads.bg.bar',
|
||||
SettingValue(typ.QssColor(), 'black'),
|
||||
"Background color for the download bar."),
|
||||
|
||||
('downloads.fg.start',
|
||||
SettingValue(typ.QtColor(), 'white'),
|
||||
"Color gradient start for download text."),
|
||||
|
||||
('downloads.bg.start',
|
||||
SettingValue(typ.QtColor(), '#0000aa'),
|
||||
"Color gradient start for downloads."),
|
||||
"Color gradient start for download backgrounds."),
|
||||
|
||||
('downloads.fg.stop',
|
||||
SettingValue(typ.QtColor(), '${downloads.fg.start}'),
|
||||
"Color gradient end for download text."),
|
||||
|
||||
('downloads.bg.stop',
|
||||
SettingValue(typ.QtColor(), '#00aa00'),
|
||||
"Color gradient end for downloads."),
|
||||
"Color gradient stop for download backgrounds."),
|
||||
|
||||
('downloads.fg.system',
|
||||
SettingValue(typ.ColorSystem(), 'rgb'),
|
||||
"Color gradient interpolation system for download text."),
|
||||
|
||||
('downloads.bg.system',
|
||||
SettingValue(typ.ColorSystem(), 'rgb'),
|
||||
"Color gradient interpolation system for downloads."),
|
||||
"Color gradient interpolation system for download backgrounds."),
|
||||
|
||||
('downloads.fg.error',
|
||||
SettingValue(typ.QtColor(), 'white'),
|
||||
"Foreground color for downloads with errors."),
|
||||
|
||||
('downloads.bg.error',
|
||||
SettingValue(typ.QtColor(), 'red'),
|
||||
"Background color for downloads with errors."),
|
||||
|
||||
('webpage.bg',
|
||||
SettingValue(typ.QtColor(none_ok=True), 'white'),
|
||||
"Background color for webpages if unset (or empty to use the "
|
||||
"theme's color)"),
|
||||
|
||||
readonly=readonly
|
||||
)),
|
||||
|
||||
@@ -1088,8 +1181,16 @@ KEY_SECTION_DESC = {
|
||||
" * `prompt-accept`: Confirm the entered value.\n"
|
||||
" * `prompt-yes`: Answer yes to a yes/no question.\n"
|
||||
" * `prompt-no`: Answer no to a yes/no question."),
|
||||
'caret': (
|
||||
""),
|
||||
}
|
||||
|
||||
# Keys which are similar to Return and should be bound by default where Return
|
||||
# is bound.
|
||||
|
||||
RETURN_KEYS = ['<Return>', '<Ctrl-M>', '<Ctrl-J>', '<Shift-Return>', '<Enter>',
|
||||
'<Shift-Enter>']
|
||||
|
||||
|
||||
KEY_DATA = collections.OrderedDict([
|
||||
('!normal', collections.OrderedDict([
|
||||
@@ -1097,7 +1198,7 @@ KEY_DATA = collections.OrderedDict([
|
||||
])),
|
||||
|
||||
('normal', collections.OrderedDict([
|
||||
('search ""', ['<Escape>']),
|
||||
('search ;; clear-keychain', ['<Escape>']),
|
||||
('set-cmd-text -s :open', ['o']),
|
||||
('set-cmd-text :open {url}', ['go']),
|
||||
('set-cmd-text -s :open -t', ['O']),
|
||||
@@ -1130,6 +1231,7 @@ KEY_DATA = collections.OrderedDict([
|
||||
('hint all tab', ['F']),
|
||||
('hint all window', ['wf']),
|
||||
('hint all tab-bg', [';b']),
|
||||
('hint all tab-fg', [';f']),
|
||||
('hint all hover', [';h']),
|
||||
('hint images', [';i']),
|
||||
('hint images tab', [';I']),
|
||||
@@ -1139,23 +1241,26 @@ KEY_DATA = collections.OrderedDict([
|
||||
('hint links fill ":open -b {hint-url}"', ['.o']),
|
||||
('hint links yank', [';y']),
|
||||
('hint links yank-primary', [';Y']),
|
||||
('hint links rapid', [';r']),
|
||||
('hint links rapid-win', [';R']),
|
||||
('hint --rapid links tab-bg', [';r']),
|
||||
('hint --rapid links window', [';R']),
|
||||
('hint links download', [';d']),
|
||||
('scroll -50 0', ['h']),
|
||||
('scroll 0 50', ['j']),
|
||||
('scroll 0 -50', ['k']),
|
||||
('scroll 50 0', ['l']),
|
||||
('scroll left', ['h']),
|
||||
('scroll down', ['j']),
|
||||
('scroll up', ['k']),
|
||||
('scroll right', ['l']),
|
||||
('undo', ['u', '<Ctrl-Shift-T>']),
|
||||
('scroll-perc 0', ['gg']),
|
||||
('scroll-perc', ['G']),
|
||||
('search-next', ['n']),
|
||||
('search-prev', ['N']),
|
||||
('enter-mode insert', ['i']),
|
||||
('enter-mode caret', ['v']),
|
||||
('yank', ['yy']),
|
||||
('yank -s', ['yY']),
|
||||
('yank -t', ['yt']),
|
||||
('yank -ts', ['yT']),
|
||||
('yank -d', ['yd']),
|
||||
('yank -ds', ['yD']),
|
||||
('paste', ['pp']),
|
||||
('paste -s', ['pP']),
|
||||
('paste -t', ['Pp']),
|
||||
@@ -1206,6 +1311,8 @@ KEY_DATA = collections.OrderedDict([
|
||||
('stop', ['<Ctrl-s>']),
|
||||
('print', ['<Ctrl-Alt-p>']),
|
||||
('open qute:settings', ['Ss']),
|
||||
('follow-selected', RETURN_KEYS),
|
||||
('follow-selected -t', ['<Ctrl-Return>', '<Ctrl-Enter>']),
|
||||
])),
|
||||
|
||||
('insert', collections.OrderedDict([
|
||||
@@ -1213,7 +1320,10 @@ KEY_DATA = collections.OrderedDict([
|
||||
])),
|
||||
|
||||
('hint', collections.OrderedDict([
|
||||
('follow-hint', ['<Return>']),
|
||||
('follow-hint', RETURN_KEYS),
|
||||
('hint --rapid links tab-bg', ['<Ctrl-R>']),
|
||||
('hint links', ['<Ctrl-F>']),
|
||||
('hint all tab-bg', ['<Ctrl-B>']),
|
||||
])),
|
||||
|
||||
('passthrough', {}),
|
||||
@@ -1223,11 +1333,11 @@ KEY_DATA = collections.OrderedDict([
|
||||
('command-history-next', ['<Ctrl-N>']),
|
||||
('completion-item-prev', ['<Shift-Tab>', '<Up>']),
|
||||
('completion-item-next', ['<Tab>', '<Down>']),
|
||||
('command-accept', ['<Return>', '<Ctrl-J>', '<Shift-Return>']),
|
||||
('command-accept', RETURN_KEYS),
|
||||
])),
|
||||
|
||||
('prompt', collections.OrderedDict([
|
||||
('prompt-accept', ['<Return>', '<Ctrl-J>', '<Shift-Return>']),
|
||||
('prompt-accept', RETURN_KEYS),
|
||||
('prompt-yes', ['y']),
|
||||
('prompt-no', ['n']),
|
||||
])),
|
||||
@@ -1242,11 +1352,38 @@ KEY_DATA = collections.OrderedDict([
|
||||
('rl-unix-line-discard', ['<Ctrl-U>']),
|
||||
('rl-kill-line', ['<Ctrl-K>']),
|
||||
('rl-kill-word', ['<Alt-D>']),
|
||||
('rl-unix-word-rubout', ['<Ctrl-W>']),
|
||||
('rl-unix-word-rubout', ['<Ctrl-W>', '<Alt-Backspace>']),
|
||||
('rl-yank', ['<Ctrl-Y>']),
|
||||
('rl-delete-char', ['<Ctrl-?>']),
|
||||
('rl-backward-delete-char', ['<Ctrl-H>']),
|
||||
])),
|
||||
|
||||
('caret', collections.OrderedDict([
|
||||
('toggle-selection', ['v', '<Space>']),
|
||||
('drop-selection', ['<Ctrl-Space>']),
|
||||
('enter-mode normal', ['c']),
|
||||
('move-to-next-line', ['j']),
|
||||
('move-to-prev-line', ['k']),
|
||||
('move-to-next-char', ['l']),
|
||||
('move-to-prev-char', ['h']),
|
||||
('move-to-end-of-word', ['e']),
|
||||
('move-to-next-word', ['w']),
|
||||
('move-to-prev-word', ['b']),
|
||||
('move-to-start-of-next-block', [']']),
|
||||
('move-to-start-of-prev-block', ['[']),
|
||||
('move-to-end-of-next-block', ['}']),
|
||||
('move-to-end-of-prev-block', ['{']),
|
||||
('move-to-start-of-line', ['0']),
|
||||
('move-to-end-of-line', ['$']),
|
||||
('move-to-start-of-document', ['gg']),
|
||||
('move-to-end-of-document', ['G']),
|
||||
('yank-selected -p', ['Y']),
|
||||
('yank-selected', ['y'] + RETURN_KEYS),
|
||||
('scroll left', ['H']),
|
||||
('scroll down', ['J']),
|
||||
('scroll up', ['K']),
|
||||
('scroll right', ['L']),
|
||||
])),
|
||||
])
|
||||
|
||||
|
||||
@@ -1254,10 +1391,22 @@ KEY_DATA = collections.OrderedDict([
|
||||
|
||||
CHANGED_KEY_COMMANDS = [
|
||||
(re.compile(r'^open -([twb]) about:blank$'), r'open -\1'),
|
||||
|
||||
(re.compile(r'^download-page$'), r'download'),
|
||||
(re.compile(r'^cancel-download$'), r'download-cancel'),
|
||||
(re.compile(r'^search ""$'), r'search'),
|
||||
(re.compile(r"^search ''$"), r'search'),
|
||||
|
||||
(re.compile(r"""^search (''|"")$"""), r'search ;; clear-keychain'),
|
||||
(re.compile(r'^search$'), r'search ;; clear-keychain'),
|
||||
|
||||
(re.compile(r"""^set-cmd-text ['"](.*) ['"]$"""), r'set-cmd-text -s \1'),
|
||||
(re.compile(r"""^set-cmd-text ['"](.*)['"]$"""), r'set-cmd-text \1'),
|
||||
|
||||
(re.compile(r"^hint links rapid$"), r'hint --rapid links tab-bg'),
|
||||
(re.compile(r"^hint links rapid-win$"), r'hint --rapid links window'),
|
||||
|
||||
(re.compile(r'^scroll -50 0$'), r'scroll left'),
|
||||
(re.compile(r'^scroll 0 50$'), r'scroll down'),
|
||||
(re.compile(r'^scroll 0 -50$'), r'scroll up'),
|
||||
(re.compile(r'^scroll 50 0$'), r'scroll right'),
|
||||
(re.compile(r'^scroll ([-\d]+ [-\d]+)$'), r'scroll-px \1'),
|
||||
]
|
||||
|
||||
@@ -34,6 +34,7 @@ from PyQt5.QtWidgets import QTabWidget, QTabBar
|
||||
|
||||
from qutebrowser.commands import cmdutils
|
||||
from qutebrowser.config import configexc
|
||||
from qutebrowser.utils import standarddir
|
||||
|
||||
|
||||
SYSTEM_PROXY = object() # Return value for Proxy type
|
||||
@@ -266,34 +267,6 @@ class BoolAsk(Bool):
|
||||
super().validate(value)
|
||||
|
||||
|
||||
class NoAsk(BaseType):
|
||||
|
||||
"""A no/ask question."""
|
||||
|
||||
valid_values = ValidValues('false', 'ask')
|
||||
|
||||
def transform(self, value):
|
||||
if value.lower() == 'ask':
|
||||
return 'ask'
|
||||
else:
|
||||
return BOOLEAN_STATES[value.lower()]
|
||||
|
||||
def validate(self, value):
|
||||
if not value:
|
||||
if self._none_ok:
|
||||
return
|
||||
else:
|
||||
raise configexc.ValidationError(value, "may not be empty!")
|
||||
if value.lower() == 'ask':
|
||||
return
|
||||
try:
|
||||
v = BOOLEAN_STATES[value.lower()]
|
||||
if v:
|
||||
raise configexc.ValidationError(value, "must be ask/false!")
|
||||
except KeyError:
|
||||
raise configexc.ValidationError(value, "must be ask/false!")
|
||||
|
||||
|
||||
class Int(BaseType):
|
||||
|
||||
"""Base class for an integer setting.
|
||||
@@ -721,7 +694,7 @@ class FontFamily(Font):
|
||||
|
||||
class QtFont(Font):
|
||||
|
||||
"""A Font which gets converted to q QFont."""
|
||||
"""A Font which gets converted to a QFont."""
|
||||
|
||||
def transform(self, value):
|
||||
if not value:
|
||||
@@ -826,6 +799,17 @@ class File(BaseType):
|
||||
|
||||
typestr = 'file'
|
||||
|
||||
def transform(self, value):
|
||||
if not value:
|
||||
return None
|
||||
value = os.path.expanduser(value)
|
||||
value = os.path.expandvars(value)
|
||||
if not os.path.isabs(value):
|
||||
cfgdir = standarddir.config()
|
||||
if cfgdir is not None:
|
||||
return os.path.join(cfgdir, value)
|
||||
return value
|
||||
|
||||
def validate(self, value):
|
||||
if not value:
|
||||
if self._none_ok:
|
||||
@@ -833,20 +817,26 @@ class File(BaseType):
|
||||
else:
|
||||
raise configexc.ValidationError(value, "may not be empty!")
|
||||
value = os.path.expanduser(value)
|
||||
value = os.path.expandvars(value)
|
||||
try:
|
||||
if not os.path.isfile(value):
|
||||
raise configexc.ValidationError(value, "must be a valid file!")
|
||||
if not os.path.isabs(value):
|
||||
cfgdir = standarddir.config()
|
||||
if cfgdir is None:
|
||||
raise configexc.ValidationError(
|
||||
value, "must be an absolute path when not using a "
|
||||
"config directory!")
|
||||
elif not os.path.isfile(os.path.join(cfgdir, value)):
|
||||
raise configexc.ValidationError(
|
||||
value, "must be a valid path relative to the config "
|
||||
"directory!")
|
||||
else:
|
||||
return
|
||||
elif not os.path.isfile(value):
|
||||
raise configexc.ValidationError(
|
||||
value, "must be an absolute path!")
|
||||
value, "must be a valid file!")
|
||||
except UnicodeEncodeError as e:
|
||||
raise configexc.ValidationError(value, e)
|
||||
|
||||
def transform(self, value):
|
||||
if not value:
|
||||
return None
|
||||
return os.path.expanduser(value)
|
||||
|
||||
|
||||
class Directory(BaseType):
|
||||
|
||||
@@ -1120,8 +1110,15 @@ class SearchEngineUrl(BaseType):
|
||||
return
|
||||
else:
|
||||
raise configexc.ValidationError(value, "may not be empty!")
|
||||
|
||||
if '{}' not in value:
|
||||
raise configexc.ValidationError(value, "must contain \"{}\"")
|
||||
try:
|
||||
value.format("")
|
||||
except KeyError:
|
||||
raise configexc.ValidationError(
|
||||
value, "may not contain {...} (use {{ and }} for literal {/})")
|
||||
|
||||
url = QUrl(value.replace('{}', 'foobar'))
|
||||
if not url.isValid():
|
||||
raise configexc.ValidationError(value, "invalid url, {}".format(
|
||||
@@ -1179,6 +1176,16 @@ class UserStyleSheet(File):
|
||||
def __init__(self):
|
||||
super().__init__(none_ok=True)
|
||||
|
||||
def transform(self, value):
|
||||
if not value:
|
||||
return None
|
||||
path = super().transform(value)
|
||||
if os.path.exists(path):
|
||||
return QUrl.fromLocalFile(path)
|
||||
else:
|
||||
data = base64.b64encode(value.encode('utf-8')).decode('ascii')
|
||||
return QUrl("data:text/css;charset=utf-8;base64,{}".format(data))
|
||||
|
||||
def validate(self, value):
|
||||
if not value:
|
||||
if self._none_ok:
|
||||
@@ -1188,31 +1195,17 @@ class UserStyleSheet(File):
|
||||
value = os.path.expandvars(value)
|
||||
value = os.path.expanduser(value)
|
||||
try:
|
||||
if not os.path.isabs(value):
|
||||
# probably a CSS, so we don't handle it as filename.
|
||||
# FIXME We just try if it is encodable, maybe we should
|
||||
# validate CSS?
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/115
|
||||
try:
|
||||
super().validate(value)
|
||||
except configexc.ValidationError:
|
||||
try:
|
||||
if not os.path.isabs(value):
|
||||
# probably a CSS, so we don't handle it as filename.
|
||||
# FIXME We just try if it is encodable, maybe we should
|
||||
# validate CSS?
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/115
|
||||
value.encode('utf-8')
|
||||
except UnicodeEncodeError as e:
|
||||
raise configexc.ValidationError(value, str(e))
|
||||
return
|
||||
elif not os.path.isfile(value):
|
||||
raise configexc.ValidationError(value, "must be a valid file!")
|
||||
except UnicodeEncodeError as e:
|
||||
raise configexc.ValidationError(value, e)
|
||||
|
||||
def transform(self, value):
|
||||
path = os.path.expandvars(value)
|
||||
path = os.path.expanduser(path)
|
||||
if not value:
|
||||
return None
|
||||
elif os.path.isabs(path):
|
||||
return QUrl.fromLocalFile(path)
|
||||
else:
|
||||
data = base64.b64encode(value.encode('utf-8')).decode('ascii')
|
||||
return QUrl("data:text/css;charset=utf-8;base64,{}".format(data))
|
||||
except UnicodeEncodeError as e:
|
||||
raise configexc.ValidationError(value, str(e))
|
||||
|
||||
|
||||
class AutoSearch(BaseType):
|
||||
@@ -1340,7 +1333,7 @@ class SelectOnRemove(BaseType):
|
||||
|
||||
class LastClose(BaseType):
|
||||
|
||||
"""Behaviour when the last tab is closed."""
|
||||
"""Behavior when the last tab is closed."""
|
||||
|
||||
valid_values = ValidValues(('ignore', "Don't do anything."),
|
||||
('blank', "Load a blank page."),
|
||||
@@ -1351,9 +1344,14 @@ class LastClose(BaseType):
|
||||
|
||||
class AcceptCookies(BaseType):
|
||||
|
||||
"""Whether to accept a cookie."""
|
||||
"""Control which cookies to accept."""
|
||||
|
||||
valid_values = ValidValues(('default', "Default QtWebKit behavior."),
|
||||
valid_values = ValidValues(('all', "Accept all cookies."),
|
||||
('no-3rdparty', "Accept cookies from the same"
|
||||
" origin only."),
|
||||
('no-unknown-3rdparty', "Accept cookies from "
|
||||
"the same origin only, unless a cookie is "
|
||||
"already set for the domain."),
|
||||
('never', "Don't accept cookies at all."))
|
||||
|
||||
|
||||
@@ -1464,15 +1462,17 @@ class NewInstanceOpenTarget(BaseType):
|
||||
"""How to open links in an existing instance if a new one is launched."""
|
||||
|
||||
valid_values = ValidValues(('tab', "Open a new tab in the existing "
|
||||
"window and activate it."),
|
||||
"window and activate the window."),
|
||||
('tab-bg', "Open a new background tab in the "
|
||||
"existing window and activate it."),
|
||||
"existing window and activate the "
|
||||
"window."),
|
||||
('tab-silent', "Open a new tab in the existing "
|
||||
"window without activating "
|
||||
"it."),
|
||||
"the window."),
|
||||
('tab-bg-silent', "Open a new background tab "
|
||||
"in the existing window "
|
||||
"without activating it."),
|
||||
"without activating the "
|
||||
"window."),
|
||||
('window', "Open in a new window."))
|
||||
|
||||
|
||||
|
||||
@@ -47,11 +47,15 @@ class ReadConfigParser(configparser.ConfigParser):
|
||||
self.optionxform = lambda opt: opt # be case-insensitive
|
||||
self._configdir = configdir
|
||||
self._fname = fname
|
||||
if self._configdir is None:
|
||||
self._configfile = None
|
||||
return
|
||||
self._configfile = os.path.join(self._configdir, fname)
|
||||
if not os.path.isfile(self._configfile):
|
||||
return
|
||||
log.init.debug("Reading config from {}".format(self._configfile))
|
||||
self.read(self._configfile, encoding='utf-8')
|
||||
if self._configfile is not None:
|
||||
self.read(self._configfile, encoding='utf-8')
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self, constructor=True,
|
||||
@@ -64,6 +68,8 @@ class ReadWriteConfigParser(ReadConfigParser):
|
||||
|
||||
def save(self):
|
||||
"""Save the config file."""
|
||||
if self._configdir is None:
|
||||
return
|
||||
if not os.path.exists(self._configdir):
|
||||
os.makedirs(self._configdir, 0o755)
|
||||
log.destroy.debug("Saving config to {}".format(self._configfile))
|
||||
|
||||
@@ -75,12 +75,13 @@ class KeyConfigParser(QObject):
|
||||
config_dirty = pyqtSignal()
|
||||
UNBOUND_COMMAND = '<unbound>'
|
||||
|
||||
def __init__(self, configdir, fname, parent=None):
|
||||
def __init__(self, configdir, fname, relaxed=False, parent=None):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
configdir: The directory to save the configs in.
|
||||
fname: The filename of the config.
|
||||
relaxed: If given, unknwon commands are ignored.
|
||||
"""
|
||||
super().__init__(parent)
|
||||
self.is_dirty = False
|
||||
@@ -95,7 +96,7 @@ class KeyConfigParser(QObject):
|
||||
if self._configfile is None or not os.path.exists(self._configfile):
|
||||
self._load_default()
|
||||
else:
|
||||
self._read()
|
||||
self._read(relaxed)
|
||||
self._load_default(only_new=True)
|
||||
log.init.debug("Loaded bindings: {}".format(self.keybindings))
|
||||
|
||||
@@ -236,27 +237,40 @@ class KeyConfigParser(QObject):
|
||||
only_new: If set, only keybindings which are completely unused
|
||||
(same command/key not bound) are added.
|
||||
"""
|
||||
# {'sectname': {'keychain1': 'command', 'keychain2': 'command'}, ...}
|
||||
bindings_to_add = collections.OrderedDict()
|
||||
|
||||
for sectname, sect in configdata.KEY_DATA.items():
|
||||
sectname = self._normalize_sectname(sectname)
|
||||
bindings_to_add[sectname] = collections.OrderedDict()
|
||||
for command, keychains in sect.items():
|
||||
for e in keychains:
|
||||
if not only_new or self._is_new(sectname, command, e):
|
||||
assert e not in bindings_to_add[sectname]
|
||||
bindings_to_add[sectname][e] = command
|
||||
|
||||
for sectname, sect in bindings_to_add.items():
|
||||
if not sect:
|
||||
if not only_new:
|
||||
self.keybindings[sectname] = collections.OrderedDict()
|
||||
self._mark_config_dirty()
|
||||
else:
|
||||
for command, keychains in sect.items():
|
||||
for e in keychains:
|
||||
if not only_new or self._is_new(sectname, command, e):
|
||||
self._add_binding(sectname, e, command)
|
||||
self._mark_config_dirty()
|
||||
for keychain, command in sect.items():
|
||||
self._add_binding(sectname, keychain, command)
|
||||
self.changed.emit(sectname)
|
||||
|
||||
if bindings_to_add:
|
||||
self._mark_config_dirty()
|
||||
|
||||
def _is_new(self, sectname, command, keychain):
|
||||
"""Check if a given binding is new.
|
||||
|
||||
A binding is considered new if both the command is not bound to any key
|
||||
yet, and the key isn't used anywhere else in the same section.
|
||||
"""
|
||||
bindings = self.keybindings[sectname]
|
||||
try:
|
||||
bindings = self.keybindings[sectname]
|
||||
except KeyError:
|
||||
return True
|
||||
if keychain in bindings:
|
||||
return False
|
||||
elif command in bindings.values():
|
||||
@@ -264,8 +278,12 @@ class KeyConfigParser(QObject):
|
||||
else:
|
||||
return True
|
||||
|
||||
def _read(self):
|
||||
"""Read the config file from disk and parse it."""
|
||||
def _read(self, relaxed=False):
|
||||
"""Read the config file from disk and parse it.
|
||||
|
||||
Args:
|
||||
relaxed: Ignore unknown commands.
|
||||
"""
|
||||
try:
|
||||
with open(self._configfile, 'r', encoding='utf-8') as f:
|
||||
for i, line in enumerate(f):
|
||||
@@ -284,8 +302,11 @@ class KeyConfigParser(QObject):
|
||||
line = line.strip()
|
||||
self._read_command(line)
|
||||
except KeyConfigError as e:
|
||||
e.lineno = i
|
||||
raise
|
||||
if relaxed:
|
||||
continue
|
||||
else:
|
||||
e.lineno = i
|
||||
raise
|
||||
except OSError:
|
||||
log.keyboard.exception("Failed to read key bindings!")
|
||||
for sectname in self.keybindings:
|
||||
|
||||
@@ -84,8 +84,8 @@ class Base:
|
||||
qws: The QWebSettings instance to use, or None to use the global
|
||||
instance.
|
||||
"""
|
||||
log.config.vdebug("Restoring default {!r}.".format(self._default))
|
||||
if self._default is not UNSET:
|
||||
log.config.vdebug("Restoring default {!r}.".format(self._default))
|
||||
self._set(self._default, qws=qws)
|
||||
|
||||
def get(self, qws=None):
|
||||
@@ -238,6 +238,25 @@ class GlobalSetter(Setter):
|
||||
self._setter(*args)
|
||||
|
||||
|
||||
class CookiePolicy(Base):
|
||||
|
||||
"""The ThirdPartyCookiePolicy setting is different from other settings."""
|
||||
|
||||
MAPPING = {
|
||||
'all': QWebSettings.AlwaysAllowThirdPartyCookies,
|
||||
'no-3rdparty': QWebSettings.AlwaysBlockThirdPartyCookies,
|
||||
'never': QWebSettings.AlwaysBlockThirdPartyCookies,
|
||||
'no-unknown-3rdparty': QWebSettings.AllowThirdPartyWithExistingCookies,
|
||||
}
|
||||
|
||||
def get(self, qws=None):
|
||||
return config.get('content', 'cookies-accept')
|
||||
|
||||
def _set(self, value, qws=None):
|
||||
QWebSettings.globalSettings().setThirdPartyCookiePolicy(
|
||||
self.MAPPING[value])
|
||||
|
||||
|
||||
MAPPINGS = {
|
||||
'content': {
|
||||
'allow-images':
|
||||
@@ -254,10 +273,18 @@ MAPPINGS = {
|
||||
# Attribute(QWebSettings.JavaEnabled),
|
||||
'allow-plugins':
|
||||
Attribute(QWebSettings.PluginsEnabled),
|
||||
'webgl':
|
||||
Attribute(QWebSettings.WebGLEnabled),
|
||||
'css-regions':
|
||||
Attribute(QWebSettings.CSSRegionsEnabled),
|
||||
'hyperlink-auditing':
|
||||
Attribute(QWebSettings.HyperlinkAuditingEnabled),
|
||||
'local-content-can-access-remote-urls':
|
||||
Attribute(QWebSettings.LocalContentCanAccessRemoteUrls),
|
||||
'local-content-can-access-file-urls':
|
||||
Attribute(QWebSettings.LocalContentCanAccessFileUrls),
|
||||
'cookies-accept':
|
||||
CookiePolicy(),
|
||||
},
|
||||
'network': {
|
||||
'dns-prefetch':
|
||||
@@ -322,6 +349,8 @@ MAPPINGS = {
|
||||
'css-media-type':
|
||||
NullStringSetter(getter=QWebSettings.cssMediaType,
|
||||
setter=QWebSettings.setCSSMediaType),
|
||||
'smooth-scrolling':
|
||||
Attribute(QWebSettings.ScrollAnimatorEnabled),
|
||||
#'accelerated-compositing':
|
||||
# Attribute(QWebSettings.AcceleratedCompositingEnabled),
|
||||
#'tiled-backing-store':
|
||||
@@ -369,16 +398,20 @@ MAPPINGS = {
|
||||
|
||||
def init():
|
||||
"""Initialize the global QWebSettings."""
|
||||
if config.get('general', 'private-browsing'):
|
||||
cache_path = standarddir.cache()
|
||||
data_path = standarddir.data()
|
||||
if config.get('general', 'private-browsing') or cache_path is None:
|
||||
QWebSettings.setIconDatabasePath('')
|
||||
else:
|
||||
QWebSettings.setIconDatabasePath(standarddir.cache())
|
||||
QWebSettings.setOfflineWebApplicationCachePath(
|
||||
os.path.join(standarddir.cache(), 'application-cache'))
|
||||
QWebSettings.globalSettings().setLocalStoragePath(
|
||||
os.path.join(standarddir.data(), 'local-storage'))
|
||||
QWebSettings.setOfflineStoragePath(
|
||||
os.path.join(standarddir.data(), 'offline-storage'))
|
||||
QWebSettings.setIconDatabasePath(cache_path)
|
||||
if cache_path is not None:
|
||||
QWebSettings.setOfflineWebApplicationCachePath(
|
||||
os.path.join(cache_path, 'application-cache'))
|
||||
if data_path is not None:
|
||||
QWebSettings.globalSettings().setLocalStoragePath(
|
||||
os.path.join(data_path, 'local-storage'))
|
||||
QWebSettings.setOfflineStoragePath(
|
||||
os.path.join(data_path, 'offline-storage'))
|
||||
|
||||
for sectname, section in MAPPINGS.items():
|
||||
for optname, mapping in section.items():
|
||||
@@ -394,11 +427,12 @@ def init():
|
||||
|
||||
def update_settings(section, option):
|
||||
"""Update global settings when qwebsettings changed."""
|
||||
cache_path = standarddir.cache()
|
||||
if (section, option) == ('general', 'private-browsing'):
|
||||
if config.get('general', 'private-browsing'):
|
||||
if config.get('general', 'private-browsing') or cache_path is None:
|
||||
QWebSettings.setIconDatabasePath('')
|
||||
else:
|
||||
QWebSettings.setIconDatabasePath(standarddir.cache())
|
||||
QWebSettings.setIconDatabasePath(cache_path)
|
||||
else:
|
||||
try:
|
||||
mapping = MAPPINGS[section][option]
|
||||
|
||||
@@ -14,10 +14,12 @@ pre { margin: 2px; }
|
||||
th, td { border: 1px solid grey; padding: 0px 5px; }
|
||||
th { background: lightgrey; }
|
||||
th pre { color: grey; text-align: left; }
|
||||
.noscript, .noscript-text { color:red; }
|
||||
.noscript-text { margin-bottom: 5cm; }
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<noscript><h1>View Only</h1><p>Changing settings requires javascript to be enabled</p></noscript>
|
||||
<noscript><h1 class="noscript">View Only</h1><p class="noscript-text">Changing settings requires javascript to be enabled!</p></noscript>
|
||||
<header><h1>{{ title }}</h1></header>
|
||||
<table>
|
||||
{% for section in config.DATA %}
|
||||
|
||||
110
qutebrowser/javascript/position_caret.js
Normal file
110
qutebrowser/javascript/position_caret.js
Normal file
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* Copyright 2015 Artur Shaik <ashaihullin@gmail.com>
|
||||
* Copyright 2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
*
|
||||
* This file is part of qutebrowser.
|
||||
*
|
||||
* qutebrowser is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* qutebrowser is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* eslint-disable max-len */
|
||||
|
||||
/**
|
||||
* Snippet to position caret at top of the page when caret mode is enabled.
|
||||
* Some code was borrowed from:
|
||||
*
|
||||
* https://github.com/1995eaton/chromium-vim/blob/master/content_scripts/dom.js
|
||||
* https://github.com/1995eaton/chromium-vim/blob/master/content_scripts/visual.js
|
||||
*/
|
||||
|
||||
/* eslint-enable max-len */
|
||||
|
||||
"use strict";
|
||||
|
||||
function isElementInViewport(node) {
|
||||
var i;
|
||||
var boundingRect = (node.getClientRects()[0] ||
|
||||
node.getBoundingClientRect());
|
||||
if (boundingRect.width <= 1 && boundingRect.height <= 1) {
|
||||
var rects = node.getClientRects();
|
||||
for (i = 0; i < rects.length; i++) {
|
||||
if (rects[i].width > rects[0].height &&
|
||||
rects[i].height > rects[0].height) {
|
||||
boundingRect = rects[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (boundingRect === undefined) {
|
||||
return null;
|
||||
}
|
||||
if (boundingRect.top > innerHeight || boundingRect.left > innerWidth) {
|
||||
return null;
|
||||
}
|
||||
if (boundingRect.width <= 1 || boundingRect.height <= 1) {
|
||||
var children = node.children;
|
||||
var visibleChildNode = false;
|
||||
var l = children.length;
|
||||
for (i = 0; i < l; ++i) {
|
||||
boundingRect = (children[i].getClientRects()[0] ||
|
||||
children[i].getBoundingClientRect());
|
||||
if (boundingRect.width > 1 && boundingRect.height > 1) {
|
||||
visibleChildNode = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (visibleChildNode === false) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (boundingRect.top + boundingRect.height < 10 ||
|
||||
boundingRect.left + boundingRect.width < -10) {
|
||||
return null;
|
||||
}
|
||||
var computedStyle = window.getComputedStyle(node, null);
|
||||
if (computedStyle.visibility !== 'visible' ||
|
||||
computedStyle.display === 'none' ||
|
||||
node.hasAttribute('disabled') ||
|
||||
parseInt(computedStyle.width, 10) === 0 ||
|
||||
parseInt(computedStyle.height, 10) === 0) {
|
||||
return null;
|
||||
}
|
||||
return boundingRect.top >= -20;
|
||||
}
|
||||
|
||||
(function() {
|
||||
var walker = document.createTreeWalker(document.body, 4, null);
|
||||
var node;
|
||||
var textNodes = [];
|
||||
var el;
|
||||
while ((node = walker.nextNode())) {
|
||||
if (node.nodeType === 3 && node.data.trim() !== '') {
|
||||
textNodes.push(node);
|
||||
}
|
||||
}
|
||||
for (var i = 0; i < textNodes.length; i++) {
|
||||
var element = textNodes[i].parentElement;
|
||||
if (isElementInViewport(element.parentElement)) {
|
||||
el = element;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (el !== undefined) {
|
||||
var range = document.createRange();
|
||||
range.setStart(el, 0);
|
||||
range.setEnd(el, 0);
|
||||
var sel = window.getSelection();
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(range);
|
||||
}
|
||||
})();
|
||||
@@ -23,7 +23,7 @@ import re
|
||||
import functools
|
||||
import unicodedata
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QObject
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import usertypes, log, utils, objreg
|
||||
@@ -49,6 +49,8 @@ class BaseKeyParser(QObject):
|
||||
special: execute() was called via a special key binding
|
||||
|
||||
do_log: Whether to log keypresses or not.
|
||||
passthrough: Whether unbound keys should be passed through with this
|
||||
handler.
|
||||
|
||||
Attributes:
|
||||
bindings: Bound key bindings
|
||||
@@ -69,6 +71,7 @@ class BaseKeyParser(QObject):
|
||||
|
||||
keystring_updated = pyqtSignal(str)
|
||||
do_log = True
|
||||
passthrough = False
|
||||
|
||||
Match = usertypes.enum('Match', ['partial', 'definitive', 'ambiguous',
|
||||
'other', 'none'])
|
||||
@@ -137,6 +140,9 @@ class BaseKeyParser(QObject):
|
||||
(countstr, cmd_input) = re.match(r'^(\d*)(.*)',
|
||||
self._keystring).groups()
|
||||
count = int(countstr) if countstr else None
|
||||
if count == 0 and not cmd_input:
|
||||
cmd_input = self._keystring
|
||||
count = None
|
||||
else:
|
||||
cmd_input = self._keystring
|
||||
count = None
|
||||
@@ -159,12 +165,6 @@ class BaseKeyParser(QObject):
|
||||
key = e.key()
|
||||
self._debug_log("Got key: 0x{:x} / text: '{}'".format(key, txt))
|
||||
|
||||
if key == Qt.Key_Escape:
|
||||
self._debug_log("Escape pressed, discarding '{}'.".format(
|
||||
self._keystring))
|
||||
self._keystring = ''
|
||||
return self.Match.none
|
||||
|
||||
if len(txt) == 1:
|
||||
category = unicodedata.category(txt)
|
||||
is_control_char = (category == 'Cc')
|
||||
@@ -195,7 +195,7 @@ class BaseKeyParser(QObject):
|
||||
self._keystring = ''
|
||||
self.execute(binding, self.Type.chain, count)
|
||||
elif match == self.Match.ambiguous:
|
||||
self._debug_log("Ambigious match for '{}'.".format(
|
||||
self._debug_log("Ambiguous match for '{}'.".format(
|
||||
self._keystring))
|
||||
self._handle_ambiguous_match(binding, count)
|
||||
elif match == self.Match.partial:
|
||||
@@ -300,6 +300,7 @@ class BaseKeyParser(QObject):
|
||||
True if the event was handled, False otherwise.
|
||||
"""
|
||||
handled = self._handle_special_key(e)
|
||||
|
||||
if handled or not self._supports_chains:
|
||||
return handled
|
||||
match = self._handle_single_key(e)
|
||||
@@ -356,3 +357,9 @@ class BaseKeyParser(QObject):
|
||||
"defined!")
|
||||
if mode == self._modename:
|
||||
self.read_config()
|
||||
|
||||
def clear_keystring(self):
|
||||
"""Clear the currently entered key sequence."""
|
||||
self._debug_log("discarding keystring '{}'.".format(self._keystring))
|
||||
self._keystring = ''
|
||||
self.keystring_updated.emit(self._keystring)
|
||||
|
||||
@@ -55,6 +55,7 @@ class PassthroughKeyParser(CommandKeyParser):
|
||||
"""
|
||||
|
||||
do_log = False
|
||||
passthrough = True
|
||||
|
||||
def __init__(self, win_id, mode, parent=None, warn=True):
|
||||
"""Constructor.
|
||||
|
||||
@@ -78,42 +78,36 @@ def init(win_id, parent):
|
||||
KM.prompt: keyparser.PassthroughKeyParser(win_id, 'prompt', modeman,
|
||||
warn=False),
|
||||
KM.yesno: modeparsers.PromptKeyParser(win_id, modeman),
|
||||
KM.caret: modeparsers.CaretKeyParser(win_id, modeman),
|
||||
}
|
||||
objreg.register('keyparsers', keyparsers, scope='window', window=win_id)
|
||||
modeman.destroyed.connect(
|
||||
functools.partial(objreg.delete, 'keyparsers', scope='window',
|
||||
window=win_id))
|
||||
modeman.register(KM.normal, keyparsers[KM.normal].handle)
|
||||
modeman.register(KM.hint, keyparsers[KM.hint].handle)
|
||||
modeman.register(KM.insert, keyparsers[KM.insert].handle, passthrough=True)
|
||||
modeman.register(KM.passthrough, keyparsers[KM.passthrough].handle,
|
||||
passthrough=True)
|
||||
modeman.register(KM.command, keyparsers[KM.command].handle,
|
||||
passthrough=True)
|
||||
modeman.register(KM.prompt, keyparsers[KM.prompt].handle, passthrough=True)
|
||||
modeman.register(KM.yesno, keyparsers[KM.yesno].handle)
|
||||
for mode, parser in keyparsers.items():
|
||||
modeman.register(mode, parser)
|
||||
return modeman
|
||||
|
||||
|
||||
def _get_modeman(win_id):
|
||||
def instance(win_id):
|
||||
"""Get a modemanager object."""
|
||||
return objreg.get('mode-manager', scope='window', window=win_id)
|
||||
|
||||
|
||||
def enter(win_id, mode, reason=None, only_if_normal=False):
|
||||
"""Enter the mode 'mode'."""
|
||||
_get_modeman(win_id).enter(mode, reason, only_if_normal)
|
||||
instance(win_id).enter(mode, reason, only_if_normal)
|
||||
|
||||
|
||||
def leave(win_id, mode, reason=None):
|
||||
"""Leave the mode 'mode'."""
|
||||
_get_modeman(win_id).leave(mode, reason)
|
||||
instance(win_id).leave(mode, reason)
|
||||
|
||||
|
||||
def maybe_leave(win_id, mode, reason=None):
|
||||
"""Convenience method to leave 'mode' without exceptions."""
|
||||
try:
|
||||
_get_modeman(win_id).leave(mode, reason)
|
||||
instance(win_id).leave(mode, reason)
|
||||
except NotInModeError as e:
|
||||
# This is rather likely to happen, so we only log to debug log.
|
||||
log.modes.debug("{} (leave reason: {})".format(e, reason))
|
||||
@@ -124,10 +118,9 @@ class ModeManager(QObject):
|
||||
"""Manager for keyboard modes.
|
||||
|
||||
Attributes:
|
||||
passthrough: A list of modes in which to pass through events.
|
||||
mode: The mode we're currently in.
|
||||
_win_id: The window ID of this ModeManager
|
||||
_handlers: A dictionary of modes and their handlers.
|
||||
_parsers: A dictionary of modes and their keyparsers.
|
||||
_forward_unbound_keys: If we should forward unbound keys.
|
||||
_releaseevents_to_pass: A set of KeyEvents where the keyPressEvent was
|
||||
passed through, so the release event should as
|
||||
@@ -149,8 +142,7 @@ class ModeManager(QObject):
|
||||
def __init__(self, win_id, parent=None):
|
||||
super().__init__(parent)
|
||||
self._win_id = win_id
|
||||
self._handlers = {}
|
||||
self.passthrough = []
|
||||
self._parsers = {}
|
||||
self.mode = usertypes.KeyMode.normal
|
||||
self._releaseevents_to_pass = set()
|
||||
self._forward_unbound_keys = config.get(
|
||||
@@ -158,8 +150,7 @@ class ModeManager(QObject):
|
||||
objreg.get('config').changed.connect(self.set_forward_unbound_keys)
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self, mode=self.mode,
|
||||
passthrough=self.passthrough)
|
||||
return utils.get_repr(self, mode=self.mode)
|
||||
|
||||
def _eventFilter_keypress(self, event):
|
||||
"""Handle filtering of KeyPress events.
|
||||
@@ -171,11 +162,11 @@ class ModeManager(QObject):
|
||||
True if event should be filtered, False otherwise.
|
||||
"""
|
||||
curmode = self.mode
|
||||
handler = self._handlers[curmode]
|
||||
parser = self._parsers[curmode]
|
||||
if curmode != usertypes.KeyMode.insert:
|
||||
log.modes.debug("got keypress in mode {} - calling handler "
|
||||
"{}".format(curmode, utils.qualname(handler)))
|
||||
handled = handler(event) if handler is not None else False
|
||||
log.modes.debug("got keypress in mode {} - delegating to "
|
||||
"{}".format(curmode, utils.qualname(parser)))
|
||||
handled = parser.handle(event)
|
||||
|
||||
is_non_alnum = bool(event.modifiers()) or not event.text().strip()
|
||||
focus_widget = QApplication.instance().focusWidget()
|
||||
@@ -185,7 +176,7 @@ class ModeManager(QObject):
|
||||
filter_this = True
|
||||
elif is_tab and not isinstance(focus_widget, QWebView):
|
||||
filter_this = True
|
||||
elif (curmode in self.passthrough or
|
||||
elif (parser.passthrough or
|
||||
self._forward_unbound_keys == 'all' or
|
||||
(self._forward_unbound_keys == 'auto' and is_non_alnum)):
|
||||
filter_this = False
|
||||
@@ -200,8 +191,8 @@ class ModeManager(QObject):
|
||||
"passthrough: {}, is_non_alnum: {}, is_tab {} --> "
|
||||
"filter: {} (focused: {!r})".format(
|
||||
handled, self._forward_unbound_keys,
|
||||
curmode in self.passthrough, is_non_alnum,
|
||||
is_tab, filter_this, focus_widget))
|
||||
parser.passthrough, is_non_alnum, is_tab,
|
||||
filter_this, focus_widget))
|
||||
return filter_this
|
||||
|
||||
def _eventFilter_keyrelease(self, event):
|
||||
@@ -224,20 +215,16 @@ class ModeManager(QObject):
|
||||
log.modes.debug("filter: {}".format(filter_this))
|
||||
return filter_this
|
||||
|
||||
def register(self, mode, handler, passthrough=False):
|
||||
def register(self, mode, parser):
|
||||
"""Register a new mode.
|
||||
|
||||
Args:
|
||||
mode: The name of the mode.
|
||||
handler: Handler for keyPressEvents.
|
||||
passthrough: Whether to pass key bindings in this mode through to
|
||||
the widgets.
|
||||
parser: The KeyParser which should be used.
|
||||
"""
|
||||
if not isinstance(mode, usertypes.KeyMode):
|
||||
raise TypeError("Mode {} is no KeyMode member!".format(mode))
|
||||
self._handlers[mode] = handler
|
||||
if passthrough:
|
||||
self.passthrough.append(mode)
|
||||
assert isinstance(mode, usertypes.KeyMode)
|
||||
assert parser is not None
|
||||
self._parsers[mode] = parser
|
||||
|
||||
def enter(self, mode, reason=None, only_if_normal=False):
|
||||
"""Enter a new mode.
|
||||
@@ -251,8 +238,8 @@ class ModeManager(QObject):
|
||||
raise TypeError("Mode {} is no KeyMode member!".format(mode))
|
||||
log.modes.debug("Entering mode {}{}".format(
|
||||
mode, '' if reason is None else ' (reason: {})'.format(reason)))
|
||||
if mode not in self._handlers:
|
||||
raise ValueError("No handler for mode {}".format(mode))
|
||||
if mode not in self._parsers:
|
||||
raise ValueError("No keyparser for mode {}".format(mode))
|
||||
prompt_modes = (usertypes.KeyMode.prompt, usertypes.KeyMode.yesno)
|
||||
if self.mode == mode or (self.mode in prompt_modes and
|
||||
mode in prompt_modes):
|
||||
@@ -330,3 +317,8 @@ class ModeManager(QObject):
|
||||
return self._eventFilter_keypress(event)
|
||||
else:
|
||||
return self._eventFilter_keyrelease(event)
|
||||
|
||||
@cmdutils.register(instance='mode-manager', scope='window', hide=True)
|
||||
def clear_keychain(self):
|
||||
"""Clear the currently entered key chain."""
|
||||
self._parsers[self.mode].clear_keystring()
|
||||
|
||||
@@ -218,3 +218,15 @@ class HintKeyParser(keyparser.CommandKeyParser):
|
||||
hintmanager = objreg.get('hintmanager', scope='tab',
|
||||
window=self._win_id, tab='current')
|
||||
hintmanager.handle_partial_key(keystr)
|
||||
|
||||
|
||||
class CaretKeyParser(keyparser.CommandKeyParser):
|
||||
|
||||
"""KeyParser for caret mode."""
|
||||
|
||||
passthrough = True
|
||||
|
||||
def __init__(self, win_id, parent=None):
|
||||
super().__init__(win_id, parent, supports_count=True,
|
||||
supports_chains=True)
|
||||
self.read_config('caret')
|
||||
|
||||
@@ -22,9 +22,10 @@
|
||||
import binascii
|
||||
import base64
|
||||
import itertools
|
||||
import functools
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot, QRect, QPoint, QTimer, Qt
|
||||
from PyQt5.QtWidgets import QWidget, QVBoxLayout
|
||||
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication
|
||||
|
||||
from qutebrowser.commands import runners, cmdutils
|
||||
from qutebrowser.config import config
|
||||
@@ -33,12 +34,53 @@ from qutebrowser.mainwindow import tabbedbrowser
|
||||
from qutebrowser.mainwindow.statusbar import bar
|
||||
from qutebrowser.completion import completionwidget
|
||||
from qutebrowser.keyinput import modeman
|
||||
from qutebrowser.browser import hints, downloads, downloadview
|
||||
from qutebrowser.browser import hints, downloads, downloadview, commands
|
||||
|
||||
|
||||
win_id_gen = itertools.count(0)
|
||||
|
||||
|
||||
def get_window(via_ipc, force_window=False, force_tab=False):
|
||||
"""Helper function for app.py to get a window id.
|
||||
|
||||
Args:
|
||||
via_ipc: Whether the request was made via IPC.
|
||||
force_window: Whether to force opening in a window.
|
||||
force_tab: Whether to force opening in a tab.
|
||||
"""
|
||||
if force_window and force_tab:
|
||||
raise ValueError("force_window and force_tab are mutually exclusive!")
|
||||
if not via_ipc:
|
||||
# Initial main window
|
||||
return 0
|
||||
window_to_raise = None
|
||||
open_target = config.get('general', 'new-instance-open-target')
|
||||
if (open_target == 'window' or force_window) and not force_tab:
|
||||
window = MainWindow()
|
||||
window.show()
|
||||
win_id = window.win_id
|
||||
window_to_raise = window
|
||||
else:
|
||||
try:
|
||||
window = objreg.last_window()
|
||||
except objreg.NoWindow:
|
||||
# There is no window left, so we open a new one
|
||||
window = MainWindow()
|
||||
window.show()
|
||||
win_id = window.win_id
|
||||
window_to_raise = window
|
||||
win_id = window.win_id
|
||||
if open_target not in ('tab-silent', 'tab-bg-silent'):
|
||||
window_to_raise = window
|
||||
if window_to_raise is not None:
|
||||
window_to_raise.setWindowState(window.windowState() &
|
||||
~Qt.WindowMinimized | Qt.WindowActive)
|
||||
window_to_raise.raise_()
|
||||
window_to_raise.activateWindow()
|
||||
QApplication.instance().alert(window_to_raise)
|
||||
return win_id
|
||||
|
||||
|
||||
class MainWindow(QWidget):
|
||||
|
||||
"""The main window of qutebrowser.
|
||||
@@ -48,8 +90,8 @@ class MainWindow(QWidget):
|
||||
|
||||
Attributes:
|
||||
status: The StatusBar widget.
|
||||
tabbed_browser: The TabbedBrowser widget.
|
||||
_downloadview: The DownloadView widget.
|
||||
_tabbed_browser: The TabbedBrowser widget.
|
||||
_vbox: The main QVBoxLayout.
|
||||
_commandrunner: The main CommandRunner instance.
|
||||
"""
|
||||
@@ -78,14 +120,6 @@ class MainWindow(QWidget):
|
||||
window=self.win_id)
|
||||
|
||||
self.setWindowTitle('qutebrowser')
|
||||
if geometry is not None:
|
||||
self._load_geometry(geometry)
|
||||
elif self.win_id == 0:
|
||||
self._load_state_geometry()
|
||||
else:
|
||||
self._set_default_geometry()
|
||||
log.init.debug("Initial main window geometry: {}".format(
|
||||
self.geometry()))
|
||||
self._vbox = QVBoxLayout(self)
|
||||
self._vbox.setContentsMargins(0, 0, 0, 0)
|
||||
self._vbox.setSpacing(0)
|
||||
@@ -97,9 +131,16 @@ class MainWindow(QWidget):
|
||||
|
||||
self._downloadview = downloadview.DownloadView(self.win_id)
|
||||
|
||||
self._tabbed_browser = tabbedbrowser.TabbedBrowser(self.win_id)
|
||||
objreg.register('tabbed-browser', self._tabbed_browser, scope='window',
|
||||
self.tabbed_browser = tabbedbrowser.TabbedBrowser(self.win_id)
|
||||
objreg.register('tabbed-browser', self.tabbed_browser, scope='window',
|
||||
window=self.win_id)
|
||||
dispatcher = commands.CommandDispatcher(self.win_id,
|
||||
self.tabbed_browser)
|
||||
objreg.register('command-dispatcher', dispatcher, scope='window',
|
||||
window=self.win_id)
|
||||
self.tabbed_browser.destroyed.connect(
|
||||
functools.partial(objreg.delete, 'command-dispatcher',
|
||||
scope='window', window=self.win_id))
|
||||
|
||||
# We need to set an explicit parent for StatusBar because it does some
|
||||
# show/hide magic immediately which would mean it'd show up as a
|
||||
@@ -116,6 +157,15 @@ class MainWindow(QWidget):
|
||||
log.init.debug("Initializing modes...")
|
||||
modeman.init(self.win_id, self)
|
||||
|
||||
if geometry is not None:
|
||||
self._load_geometry(geometry)
|
||||
elif self.win_id == 0:
|
||||
self._load_state_geometry()
|
||||
else:
|
||||
self._set_default_geometry()
|
||||
log.init.debug("Initial main window geometry: {}".format(
|
||||
self.geometry()))
|
||||
|
||||
self._connect_signals()
|
||||
|
||||
# When we're here the statusbar might not even really exist yet, so
|
||||
@@ -144,15 +194,15 @@ class MainWindow(QWidget):
|
||||
|
||||
def _add_widgets(self):
|
||||
"""Add or readd all widgets to the VBox."""
|
||||
self._vbox.removeWidget(self._tabbed_browser)
|
||||
self._vbox.removeWidget(self.tabbed_browser)
|
||||
self._vbox.removeWidget(self._downloadview)
|
||||
self._vbox.removeWidget(self.status)
|
||||
position = config.get('ui', 'downloads-position')
|
||||
if position == 'north':
|
||||
self._vbox.addWidget(self._downloadview)
|
||||
self._vbox.addWidget(self._tabbed_browser)
|
||||
self._vbox.addWidget(self.tabbed_browser)
|
||||
elif position == 'south':
|
||||
self._vbox.addWidget(self._tabbed_browser)
|
||||
self._vbox.addWidget(self.tabbed_browser)
|
||||
self._vbox.addWidget(self._downloadview)
|
||||
else:
|
||||
raise ValueError("Invalid position {}!".format(position))
|
||||
@@ -173,6 +223,13 @@ class MainWindow(QWidget):
|
||||
else:
|
||||
self._load_geometry(geom)
|
||||
|
||||
def _save_geometry(self):
|
||||
"""Save the window geometry to the state config."""
|
||||
state_config = objreg.get('state-config')
|
||||
data = bytes(self.saveGeometry())
|
||||
geom = base64.b64encode(data).decode('ASCII')
|
||||
state_config['geometry']['mainwindow'] = geom
|
||||
|
||||
def _load_geometry(self, geom):
|
||||
"""Load geometry from a bytes object.
|
||||
|
||||
@@ -212,7 +269,7 @@ class MainWindow(QWidget):
|
||||
prompter = self._get_object('prompter')
|
||||
|
||||
# misc
|
||||
self._tabbed_browser.close_window.connect(self.close)
|
||||
self.tabbed_browser.close_window.connect(self.close)
|
||||
mode_manager.entered.connect(hints.on_mode_entered)
|
||||
|
||||
# status bar
|
||||
@@ -333,12 +390,12 @@ class MainWindow(QWidget):
|
||||
super().resizeEvent(e)
|
||||
self.resize_completion()
|
||||
self._downloadview.updateGeometry()
|
||||
self._tabbed_browser.tabBar().refresh()
|
||||
self.tabbed_browser.tabBar().refresh()
|
||||
|
||||
def closeEvent(self, e):
|
||||
"""Override closeEvent to display a confirmation if needed."""
|
||||
confirm_quit = config.get('ui', 'confirm-quit')
|
||||
tab_count = self._tabbed_browser.count()
|
||||
tab_count = self.tabbed_browser.count()
|
||||
download_manager = objreg.get('download-manager', scope='window',
|
||||
window=self.win_id)
|
||||
download_count = download_manager.rowCount()
|
||||
@@ -368,8 +425,7 @@ class MainWindow(QWidget):
|
||||
e.ignore()
|
||||
return
|
||||
e.accept()
|
||||
if len(objreg.window_registry) == 1:
|
||||
objreg.get('session-manager').save_last_window_session()
|
||||
objreg.get('app').geometry = bytes(self.saveGeometry())
|
||||
objreg.get('session-manager').save_last_window_session()
|
||||
self._save_geometry()
|
||||
log.destroy.debug("Closing window {}".format(self.win_id))
|
||||
self._tabbed_browser.shutdown()
|
||||
self.tabbed_browser.shutdown()
|
||||
|
||||
@@ -36,6 +36,7 @@ from qutebrowser.mainwindow.statusbar import text as textwidget
|
||||
PreviousWidget = usertypes.enum('PreviousWidget', ['none', 'prompt',
|
||||
'command'])
|
||||
Severity = usertypes.enum('Severity', ['normal', 'warning', 'error'])
|
||||
CaretMode = usertypes.enum('CaretMode', ['off', 'on', 'selection'])
|
||||
|
||||
|
||||
class StatusBar(QWidget):
|
||||
@@ -77,6 +78,16 @@ class StatusBar(QWidget):
|
||||
For some reason we need to have this as class attribute
|
||||
so pyqtProperty works correctly.
|
||||
|
||||
_command_active: If we're currently in command mode.
|
||||
|
||||
For some reason we need to have this as class
|
||||
attribute so pyqtProperty works correctly.
|
||||
|
||||
_caret_mode: The current caret mode (off/on/selection).
|
||||
|
||||
For some reason we need to have this as class attribute
|
||||
so pyqtProperty works correctly.
|
||||
|
||||
Signals:
|
||||
resized: Emitted when the statusbar has resized, so the completion
|
||||
widget can adjust its size to it.
|
||||
@@ -91,32 +102,68 @@ class StatusBar(QWidget):
|
||||
_severity = None
|
||||
_prompt_active = False
|
||||
_insert_active = False
|
||||
_command_active = False
|
||||
_caret_mode = CaretMode.off
|
||||
|
||||
STYLESHEET = """
|
||||
QWidget#StatusBar {
|
||||
|
||||
QWidget#StatusBar,
|
||||
QWidget#StatusBar QLabel,
|
||||
QWidget#StatusBar QLineEdit {
|
||||
{{ font['statusbar'] }}
|
||||
{{ color['statusbar.bg'] }}
|
||||
{{ color['statusbar.fg'] }}
|
||||
}
|
||||
|
||||
QWidget#StatusBar[insert_active="true"] {
|
||||
{{ color['statusbar.bg.insert'] }}
|
||||
QWidget#StatusBar[caret_mode="on"],
|
||||
QWidget#StatusBar[caret_mode="on"] QLabel,
|
||||
QWidget#StatusBar[caret_mode="on"] QLineEdit {
|
||||
{{ color['statusbar.fg.caret'] }}
|
||||
{{ color['statusbar.bg.caret'] }}
|
||||
}
|
||||
|
||||
QWidget#StatusBar[prompt_active="true"] {
|
||||
{{ color['statusbar.bg.prompt'] }}
|
||||
QWidget#StatusBar[caret_mode="selection"],
|
||||
QWidget#StatusBar[caret_mode="selection"] QLabel,
|
||||
QWidget#StatusBar[caret_mode="selection"] QLineEdit {
|
||||
{{ color['statusbar.fg.caret-selection'] }}
|
||||
{{ color['statusbar.bg.caret-selection'] }}
|
||||
}
|
||||
|
||||
QWidget#StatusBar[severity="error"] {
|
||||
QWidget#StatusBar[severity="error"],
|
||||
QWidget#StatusBar[severity="error"] QLabel,
|
||||
QWidget#StatusBar[severity="error"] QLineEdit {
|
||||
{{ color['statusbar.fg.error'] }}
|
||||
{{ color['statusbar.bg.error'] }}
|
||||
}
|
||||
|
||||
QWidget#StatusBar[severity="warning"] {
|
||||
QWidget#StatusBar[severity="warning"],
|
||||
QWidget#StatusBar[severity="warning"] QLabel,
|
||||
QWidget#StatusBar[severity="warning"] QLineEdit {
|
||||
{{ color['statusbar.fg.warning'] }}
|
||||
{{ color['statusbar.bg.warning'] }}
|
||||
}
|
||||
|
||||
QLabel, QLineEdit {
|
||||
{{ color['statusbar.fg'] }}
|
||||
{{ font['statusbar'] }}
|
||||
QWidget#StatusBar[prompt_active="true"],
|
||||
QWidget#StatusBar[prompt_active="true"] QLabel,
|
||||
QWidget#StatusBar[prompt_active="true"] QLineEdit {
|
||||
{{ color['statusbar.fg.prompt'] }}
|
||||
{{ color['statusbar.bg.prompt'] }}
|
||||
}
|
||||
|
||||
QWidget#StatusBar[insert_active="true"],
|
||||
QWidget#StatusBar[insert_active="true"] QLabel,
|
||||
QWidget#StatusBar[insert_active="true"] QLineEdit {
|
||||
{{ color['statusbar.fg.insert'] }}
|
||||
{{ color['statusbar.bg.insert'] }}
|
||||
}
|
||||
|
||||
QWidget#StatusBar[command_active="true"],
|
||||
QWidget#StatusBar[command_active="true"] QLabel,
|
||||
QWidget#StatusBar[command_active="true"] QLineEdit {
|
||||
{{ color['statusbar.fg.command'] }}
|
||||
{{ color['statusbar.bg.command'] }}
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, win_id, parent=None):
|
||||
@@ -248,19 +295,47 @@ class StatusBar(QWidget):
|
||||
self._prompt_active = val
|
||||
self.setStyleSheet(style.get_stylesheet(self.STYLESHEET))
|
||||
|
||||
@pyqtProperty(bool)
|
||||
def command_active(self):
|
||||
"""Getter for self.command_active, so it can be used as Qt property."""
|
||||
return self._command_active
|
||||
|
||||
@pyqtProperty(bool)
|
||||
def insert_active(self):
|
||||
"""Getter for self.insert_active, so it can be used as Qt property."""
|
||||
return self._insert_active
|
||||
|
||||
def _set_insert_active(self, val):
|
||||
"""Setter for self.insert_active.
|
||||
@pyqtProperty(str)
|
||||
def caret_mode(self):
|
||||
"""Getter for self._caret_mode, so it can be used as Qt property."""
|
||||
return self._caret_mode.name
|
||||
|
||||
def set_mode_active(self, mode, val):
|
||||
"""Setter for self.{insert,command,caret}_active.
|
||||
|
||||
Re-set the stylesheet after setting the value, so everything gets
|
||||
updated by Qt properly.
|
||||
"""
|
||||
log.statusbar.debug("Setting insert_active to {}".format(val))
|
||||
self._insert_active = val
|
||||
if mode == usertypes.KeyMode.insert:
|
||||
log.statusbar.debug("Setting insert_active to {}".format(val))
|
||||
self._insert_active = val
|
||||
if mode == usertypes.KeyMode.command:
|
||||
log.statusbar.debug("Setting command_active to {}".format(val))
|
||||
self._command_active = val
|
||||
elif mode == usertypes.KeyMode.caret:
|
||||
webview = objreg.get('tabbed-browser', scope='window',
|
||||
window=self._win_id).currentWidget()
|
||||
log.statusbar.debug("Setting caret_mode - val {}, selection "
|
||||
"{}".format(val, webview.selection_enabled))
|
||||
if val:
|
||||
if webview.selection_enabled:
|
||||
self._set_mode_text("{} selection".format(mode.name))
|
||||
self._caret_mode = CaretMode.selection
|
||||
else:
|
||||
self._set_mode_text(mode.name)
|
||||
self._caret_mode = CaretMode.on
|
||||
else:
|
||||
self._caret_mode = CaretMode.off
|
||||
self.setStyleSheet(style.get_stylesheet(self.STYLESHEET))
|
||||
|
||||
def _set_mode_text(self, mode):
|
||||
@@ -434,25 +509,29 @@ class StatusBar(QWidget):
|
||||
@pyqtSlot(usertypes.KeyMode)
|
||||
def on_mode_entered(self, mode):
|
||||
"""Mark certain modes in the commandline."""
|
||||
mode_manager = objreg.get('mode-manager', scope='window',
|
||||
window=self._win_id)
|
||||
if mode in mode_manager.passthrough:
|
||||
keyparsers = objreg.get('keyparsers', scope='window',
|
||||
window=self._win_id)
|
||||
if keyparsers[mode].passthrough:
|
||||
self._set_mode_text(mode.name)
|
||||
if mode == usertypes.KeyMode.insert:
|
||||
self._set_insert_active(True)
|
||||
if mode in (usertypes.KeyMode.insert,
|
||||
usertypes.KeyMode.command,
|
||||
usertypes.KeyMode.caret):
|
||||
self.set_mode_active(mode, True)
|
||||
|
||||
@pyqtSlot(usertypes.KeyMode, usertypes.KeyMode)
|
||||
def on_mode_left(self, old_mode, new_mode):
|
||||
"""Clear marked mode."""
|
||||
mode_manager = objreg.get('mode-manager', scope='window',
|
||||
window=self._win_id)
|
||||
if old_mode in mode_manager.passthrough:
|
||||
if new_mode in mode_manager.passthrough:
|
||||
keyparsers = objreg.get('keyparsers', scope='window',
|
||||
window=self._win_id)
|
||||
if keyparsers[old_mode].passthrough:
|
||||
if keyparsers[new_mode].passthrough:
|
||||
self._set_mode_text(new_mode.name)
|
||||
else:
|
||||
self.txt.set_text(self.txt.Text.normal, '')
|
||||
if old_mode == usertypes.KeyMode.insert:
|
||||
self._set_insert_active(False)
|
||||
if old_mode in (usertypes.KeyMode.insert,
|
||||
usertypes.KeyMode.command,
|
||||
usertypes.KeyMode.caret):
|
||||
self.set_mode_active(old_mode, False)
|
||||
|
||||
@config.change_filter('ui', 'message-timeout')
|
||||
def set_pop_timer_interval(self):
|
||||
|
||||
@@ -163,8 +163,8 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
|
||||
"""Execute the command currently in the commandline."""
|
||||
prefixes = {
|
||||
':': '',
|
||||
'/': 'search ',
|
||||
'?': 'search -r ',
|
||||
'/': 'search -- ',
|
||||
'?': 'search -r -- ',
|
||||
}
|
||||
text = self.text()
|
||||
self.history.append(text)
|
||||
|
||||
@@ -33,6 +33,7 @@ from qutebrowser.utils import usertypes, log, qtutils, objreg, utils
|
||||
PromptContext = collections.namedtuple('PromptContext',
|
||||
['question', 'text', 'input_text',
|
||||
'echo_mode', 'input_visible'])
|
||||
AuthTuple = collections.namedtuple('AuthTuple', ['user', 'password'])
|
||||
|
||||
|
||||
class Prompter(QObject):
|
||||
@@ -237,7 +238,7 @@ class Prompter(QObject):
|
||||
elif self._question.mode == usertypes.PromptMode.user_pwd:
|
||||
# User just entered a password
|
||||
password = prompt.lineedit.text()
|
||||
self._question.answer = (self._question.user, password)
|
||||
self._question.answer = AuthTuple(self._question.user, password)
|
||||
modeman.maybe_leave(self._win_id, usertypes.KeyMode.prompt,
|
||||
'prompt accept')
|
||||
self._question.done()
|
||||
|
||||
@@ -70,7 +70,7 @@ class TextBase(QLabel):
|
||||
More info:
|
||||
|
||||
http://stackoverflow.com/q/21890462/2085149
|
||||
https://bugreports.qt-project.org/browse/QTBUG-36945
|
||||
https://bugreports.qt.io/browse/QTBUG-36945
|
||||
https://codereview.qt-project.org/#/c/79181/
|
||||
|
||||
Args:
|
||||
|
||||
@@ -29,7 +29,7 @@ from PyQt5.QtGui import QIcon
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.keyinput import modeman
|
||||
from qutebrowser.mainwindow import tabwidget
|
||||
from qutebrowser.browser import signalfilter, commands, webview
|
||||
from qutebrowser.browser import signalfilter, webview
|
||||
from qutebrowser.utils import log, usertypes, utils, qtutils, objreg, urlutils
|
||||
|
||||
|
||||
@@ -107,12 +107,6 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
self._undo_stack = []
|
||||
self._filter = signalfilter.SignalFilter(win_id, self)
|
||||
dispatcher = commands.CommandDispatcher(win_id)
|
||||
objreg.register('command-dispatcher', dispatcher, scope='window',
|
||||
window=win_id)
|
||||
self.destroyed.connect(
|
||||
functools.partial(objreg.delete, 'command-dispatcher',
|
||||
scope='window', window=win_id))
|
||||
self._now_focused = None
|
||||
# FIXME adjust this to font size
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/119
|
||||
@@ -302,7 +296,7 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
newtab: True to open URL in a new tab, False otherwise.
|
||||
"""
|
||||
qtutils.ensure_valid(url)
|
||||
if newtab:
|
||||
if newtab or self.currentWidget() is None:
|
||||
self.tabopen(url, background=False)
|
||||
else:
|
||||
self.currentWidget().openurl(url)
|
||||
@@ -338,7 +332,7 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
the default settings we handle it like Chromium does:
|
||||
- Tabs from clicked links etc. are to the right of
|
||||
the current.
|
||||
- Explicitely opened tabs are at the very right.
|
||||
- Explicitly opened tabs are at the very right.
|
||||
|
||||
Return:
|
||||
The opened WebView instance.
|
||||
@@ -518,7 +512,8 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
tab = self.widget(idx)
|
||||
log.modes.debug("Current tab changed, focusing {!r}".format(tab))
|
||||
tab.setFocus()
|
||||
for mode in (usertypes.KeyMode.hint, usertypes.KeyMode.insert):
|
||||
for mode in (usertypes.KeyMode.hint, usertypes.KeyMode.insert,
|
||||
usertypes.KeyMode.caret):
|
||||
modeman.maybe_leave(self._win_id, mode, 'tab changed')
|
||||
if self._now_focused is not None:
|
||||
objreg.register('last-focused-tab', self._now_focused, update=True,
|
||||
@@ -582,3 +577,14 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
"""
|
||||
super().resizeEvent(e)
|
||||
self.resized.emit(self.geometry())
|
||||
|
||||
def wheelEvent(self, e):
|
||||
"""Override wheelEvent of QWidget to forward it to the focused tab.
|
||||
|
||||
Args:
|
||||
e: The QWheelEvent
|
||||
"""
|
||||
if self._now_focused is not None:
|
||||
self._now_focused.wheelEvent(e)
|
||||
else:
|
||||
e.ignore()
|
||||
|
||||
@@ -391,9 +391,9 @@ class TabBar(QTabBar):
|
||||
def paintEvent(self, _e):
|
||||
"""Override paintEvent to draw the tabs like we want to."""
|
||||
p = QStylePainter(self)
|
||||
tab = QStyleOptionTab()
|
||||
selected = self.currentIndex()
|
||||
for idx in range(self.count()):
|
||||
tab = QStyleOptionTab()
|
||||
self.initStyleOption(tab, idx)
|
||||
if idx == selected:
|
||||
bg_color = config.get('colors', 'tabs.bg.selected')
|
||||
@@ -480,6 +480,19 @@ class TabBar(QTabBar):
|
||||
new_idx = super().insertTab(idx, icon, '')
|
||||
self.set_page_title(new_idx, text)
|
||||
|
||||
def wheelEvent(self, e):
|
||||
"""Override wheelEvent to make the action configurable.
|
||||
|
||||
Args:
|
||||
e: The QWheelEvent
|
||||
"""
|
||||
if config.get('tabs', 'mousewheel-tab-switching'):
|
||||
super().wheelEvent(e)
|
||||
else:
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=self._win_id)
|
||||
tabbed_browser.wheelEvent(e)
|
||||
|
||||
|
||||
class TabBarStyle(QCommonStyle):
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ def check_python_version():
|
||||
version_str = '.'.join(map(str, sys.version_info[:3]))
|
||||
text = ("At least Python 3.4 is required to run qutebrowser, but " +
|
||||
version_str + " is installed!\n")
|
||||
if Tk:
|
||||
if Tk and '--no-err-windows' not in sys.argv:
|
||||
root = Tk()
|
||||
root.withdraw()
|
||||
messagebox.showerror("qutebrowser: Fatal error!", text)
|
||||
|
||||
@@ -24,9 +24,8 @@ import sys
|
||||
import html
|
||||
import getpass
|
||||
import traceback
|
||||
import distutils.version # pylint: disable=no-name-in-module,import-error
|
||||
# https://bitbucket.org/logilab/pylint/issue/73/
|
||||
|
||||
import pkg_resources
|
||||
from PyQt5.QtCore import pyqtSlot, Qt, QSize, qVersion
|
||||
from PyQt5.QtWidgets import (QDialog, QLabel, QTextEdit, QPushButton,
|
||||
QVBoxLayout, QHBoxLayout, QCheckBox,
|
||||
@@ -183,7 +182,7 @@ class _CrashDialog(QDialog):
|
||||
def _init_text(self):
|
||||
"""Initialize the main text to be displayed on an exception.
|
||||
|
||||
Should be extended by superclass to set the actual text."""
|
||||
Should be extended by subclasses to set the actual text."""
|
||||
self._lbl = QLabel(wordWrap=True, openExternalLinks=True,
|
||||
textInteractionFlags=Qt.LinksAccessibleByMouse)
|
||||
self._vbox.addWidget(self._lbl)
|
||||
@@ -328,8 +327,8 @@ class _CrashDialog(QDialog):
|
||||
"""
|
||||
# pylint: disable=no-member
|
||||
# https://bitbucket.org/logilab/pylint/issue/73/
|
||||
new_version = distutils.version.StrictVersion(newest)
|
||||
cur_version = distutils.version.StrictVersion(qutebrowser.__version__)
|
||||
new_version = pkg_resources.parse_version(newest)
|
||||
cur_version = pkg_resources.parse_version(qutebrowser.__version__)
|
||||
lines = ['The report has been sent successfully. Thanks!']
|
||||
if new_version > cur_version:
|
||||
lines.append("<b>Note:</b> The newest available version is v{}, "
|
||||
@@ -584,3 +583,38 @@ class ReportErrorDialog(QDialog):
|
||||
btn.clicked.connect(self.close)
|
||||
hbox.addWidget(btn)
|
||||
vbox.addLayout(hbox)
|
||||
|
||||
|
||||
def dump_exception_info(exc, pages, cmdhist, objects):
|
||||
"""Dump exception info to stderr.
|
||||
|
||||
Args:
|
||||
exc: An exception tuple (type, value, traceback)
|
||||
pages: A list of lists of the open pages (URLs as strings)
|
||||
cmdhist: A list with the command history (as strings)
|
||||
objects: A list of all QObjects as string.
|
||||
"""
|
||||
print(file=sys.stderr)
|
||||
print("\n\n===== Handling exception with --no-err-windows... =====\n\n",
|
||||
file=sys.stderr)
|
||||
print("\n---- Exceptions ----", file=sys.stderr)
|
||||
print(''.join(traceback.format_exception(*exc)), file=sys.stderr)
|
||||
print("\n---- Version info ----", file=sys.stderr)
|
||||
try:
|
||||
print(version.version(), file=sys.stderr)
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
print("\n---- Config ----", file=sys.stderr)
|
||||
try:
|
||||
conf = objreg.get('config')
|
||||
print(conf.dump_userconfig(), file=sys.stderr)
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
print("\n---- Commandline args ----", file=sys.stderr)
|
||||
print(' '.join(sys.argv[1:]), file=sys.stderr)
|
||||
print("\n---- Open pages ----", file=sys.stderr)
|
||||
print('\n\n'.join('\n'.join(e) for e in pages), file=sys.stderr)
|
||||
print("\n---- Command history ----", file=sys.stderr)
|
||||
print('\n'.join(cmdhist), file=sys.stderr)
|
||||
print("\n---- Objects ----", file=sys.stderr)
|
||||
print(objects, file=sys.stderr)
|
||||
|
||||
386
qutebrowser/misc/crashsignal.py
Normal file
386
qutebrowser/misc/crashsignal.py
Normal file
@@ -0,0 +1,386 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
# qutebrowser is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# qutebrowser is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Handlers for crashes and OS signals."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import bdb
|
||||
import pdb
|
||||
import signal
|
||||
import functools
|
||||
import faulthandler
|
||||
import os.path
|
||||
import collections
|
||||
|
||||
from PyQt5.QtCore import (pyqtSlot, qInstallMessageHandler, QObject,
|
||||
QSocketNotifier, QTimer, QUrl)
|
||||
from PyQt5.QtWidgets import QApplication, QDialog
|
||||
|
||||
from qutebrowser.commands import cmdutils
|
||||
from qutebrowser.misc import earlyinit, crashdialog
|
||||
from qutebrowser.utils import usertypes, standarddir, log, objreg, debug
|
||||
|
||||
|
||||
ExceptionInfo = collections.namedtuple('ExceptionInfo',
|
||||
'pages, cmd_history, objects')
|
||||
|
||||
|
||||
class CrashHandler(QObject):
|
||||
|
||||
"""Handler for crashes, reports and exceptions.
|
||||
|
||||
Attributes:
|
||||
_app: The QApplication instance.
|
||||
_quitter: The Quitter instance.
|
||||
_args: The argparse namespace.
|
||||
_crash_dialog: The CrashDialog currently being shown.
|
||||
_crash_log_file: The file handle for the faulthandler crash log.
|
||||
"""
|
||||
|
||||
def __init__(self, *, app, quitter, args, parent=None):
|
||||
super().__init__(parent)
|
||||
self._app = app
|
||||
self._quitter = quitter
|
||||
self._args = args
|
||||
self._crash_log_file = None
|
||||
self._crash_dialog = None
|
||||
|
||||
def activate(self):
|
||||
"""Activate the exception hook."""
|
||||
sys.excepthook = self.exception_hook
|
||||
|
||||
def handle_segfault(self):
|
||||
"""Handle a segfault from a previous run."""
|
||||
data_dir = None
|
||||
if data_dir is None:
|
||||
return
|
||||
logname = os.path.join(data_dir, 'crash.log')
|
||||
try:
|
||||
# First check if an old logfile exists.
|
||||
if os.path.exists(logname):
|
||||
with open(logname, 'r', encoding='ascii') as f:
|
||||
data = f.read()
|
||||
os.remove(logname)
|
||||
self._init_crashlogfile()
|
||||
if data:
|
||||
# Crashlog exists and has data in it, so something crashed
|
||||
# previously.
|
||||
self._crash_dialog = crashdialog.get_fatal_crash_dialog(
|
||||
self._args.debug, data)
|
||||
self._crash_dialog.show()
|
||||
else:
|
||||
# There's no log file, so we can use this to display crashes to
|
||||
# the user on the next start.
|
||||
self._init_crashlogfile()
|
||||
except OSError:
|
||||
log.init.exception("Error while handling crash log file!")
|
||||
self._init_crashlogfile()
|
||||
|
||||
def _recover_pages(self, forgiving=False):
|
||||
"""Try to recover all open pages.
|
||||
|
||||
Called from exception_hook, so as forgiving as possible.
|
||||
|
||||
Args:
|
||||
forgiving: Whether to ignore exceptions.
|
||||
|
||||
Return:
|
||||
A list containing a list for each window, which in turn contain the
|
||||
opened URLs.
|
||||
"""
|
||||
pages = []
|
||||
for win_id in objreg.window_registry:
|
||||
win_pages = []
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=win_id)
|
||||
for tab in tabbed_browser.widgets():
|
||||
try:
|
||||
urlstr = tab.cur_url.toString(
|
||||
QUrl.RemovePassword | QUrl.FullyEncoded)
|
||||
if urlstr:
|
||||
win_pages.append(urlstr)
|
||||
except Exception:
|
||||
if forgiving:
|
||||
log.destroy.exception("Error while recovering tab")
|
||||
else:
|
||||
raise
|
||||
pages.append(win_pages)
|
||||
return pages
|
||||
|
||||
def _init_crashlogfile(self):
|
||||
"""Start a new logfile and redirect faulthandler to it."""
|
||||
assert not self._args.no_err_windows
|
||||
data_dir = standarddir.data()
|
||||
if data_dir is None:
|
||||
return
|
||||
logname = os.path.join(data_dir, 'crash.log')
|
||||
try:
|
||||
self._crash_log_file = open(logname, 'w', encoding='ascii')
|
||||
except OSError:
|
||||
log.init.exception("Error while opening crash log file!")
|
||||
else:
|
||||
earlyinit.init_faulthandler(self._crash_log_file)
|
||||
|
||||
@cmdutils.register(instance='crash-handler')
|
||||
def report(self):
|
||||
"""Report a bug in qutebrowser."""
|
||||
pages = self._recover_pages()
|
||||
cmd_history = objreg.get('command-history')[-5:]
|
||||
objects = debug.get_all_objects()
|
||||
self._crash_dialog = crashdialog.ReportDialog(pages, cmd_history,
|
||||
objects)
|
||||
self._crash_dialog.show()
|
||||
|
||||
def destroy_crashlogfile(self):
|
||||
"""Clean up the crash log file and delete it."""
|
||||
if self._crash_log_file is None:
|
||||
return
|
||||
# We use sys.__stderr__ instead of sys.stderr here so this will still
|
||||
# work when sys.stderr got replaced, e.g. by "Python Tools for Visual
|
||||
# Studio".
|
||||
if sys.__stderr__ is not None:
|
||||
faulthandler.enable(sys.__stderr__)
|
||||
else:
|
||||
faulthandler.disable()
|
||||
try:
|
||||
self._crash_log_file.close()
|
||||
os.remove(self._crash_log_file.name)
|
||||
except OSError:
|
||||
log.destroy.exception("Could not remove crash log!")
|
||||
|
||||
def _get_exception_info(self):
|
||||
"""Get info needed for the exception hook/dialog.
|
||||
|
||||
Return:
|
||||
An ExceptionInfo namedtuple.
|
||||
"""
|
||||
try:
|
||||
pages = self._recover_pages(forgiving=True)
|
||||
except Exception:
|
||||
log.destroy.exception("Error while recovering pages")
|
||||
pages = []
|
||||
|
||||
try:
|
||||
cmd_history = objreg.get('command-history')[-5:]
|
||||
except Exception:
|
||||
log.destroy.exception("Error while getting history: {}")
|
||||
cmd_history = []
|
||||
|
||||
try:
|
||||
objects = debug.get_all_objects()
|
||||
except Exception:
|
||||
log.destroy.exception("Error while getting objects")
|
||||
objects = ""
|
||||
return ExceptionInfo(pages, cmd_history, objects)
|
||||
|
||||
def exception_hook(self, exctype, excvalue, tb):
|
||||
"""Handle uncaught python exceptions.
|
||||
|
||||
It'll try very hard to write all open tabs to a file, and then exit
|
||||
gracefully.
|
||||
"""
|
||||
exc = (exctype, excvalue, tb)
|
||||
qapp = QApplication.instance()
|
||||
|
||||
if not self._quitter.quit_status['crash']:
|
||||
log.misc.error("ARGH, there was an exception while the crash "
|
||||
"dialog is already shown:", exc_info=exc)
|
||||
return
|
||||
|
||||
log.misc.error("Uncaught exception", exc_info=exc)
|
||||
|
||||
is_ignored_exception = (exctype is bdb.BdbQuit or
|
||||
not issubclass(exctype, Exception))
|
||||
|
||||
if self._args.pdb_postmortem:
|
||||
pdb.post_mortem(tb)
|
||||
|
||||
if is_ignored_exception or self._args.pdb_postmortem:
|
||||
# pdb exit, KeyboardInterrupt, ...
|
||||
status = 0 if is_ignored_exception else 2
|
||||
try:
|
||||
self._quitter.shutdown(status)
|
||||
return
|
||||
except Exception:
|
||||
log.init.exception("Error while shutting down")
|
||||
qapp.quit()
|
||||
return
|
||||
|
||||
self._quitter.quit_status['crash'] = False
|
||||
info = self._get_exception_info()
|
||||
|
||||
try:
|
||||
objreg.get('ipc-server').ignored = True
|
||||
except Exception:
|
||||
log.destroy.exception("Error while ignoring ipc")
|
||||
|
||||
try:
|
||||
self._app.lastWindowClosed.disconnect(
|
||||
self._quitter.on_last_window_closed)
|
||||
except TypeError:
|
||||
log.destroy.exception("Error while preventing shutdown")
|
||||
self._app.closeAllWindows()
|
||||
if self._args.no_err_windows:
|
||||
crashdialog.dump_exception_info(exc, info.pages, info.cmd_history,
|
||||
info.objects)
|
||||
else:
|
||||
self._crash_dialog = crashdialog.ExceptionCrashDialog(
|
||||
self._args.debug, info.pages, info.cmd_history, exc,
|
||||
info.objects)
|
||||
ret = self._crash_dialog.exec_()
|
||||
if ret == QDialog.Accepted: # restore
|
||||
self._quitter.restart(info.pages)
|
||||
|
||||
# We might risk a segfault here, but that's better than continuing to
|
||||
# run in some undefined state, so we only do the most needed shutdown
|
||||
# here.
|
||||
qInstallMessageHandler(None)
|
||||
self.destroy_crashlogfile()
|
||||
sys.exit(usertypes.Exit.exception)
|
||||
|
||||
def raise_crashdlg(self):
|
||||
"""Raise the crash dialog if one exists."""
|
||||
if self._crash_dialog is not None:
|
||||
self._crash_dialog.raise_()
|
||||
|
||||
|
||||
class SignalHandler(QObject):
|
||||
|
||||
"""Handler responsible for handling OS signals (SIGINT, SIGTERM, etc.).
|
||||
|
||||
Attributes:
|
||||
_app: The QApplication instance.
|
||||
_quitter: The Quitter instance.
|
||||
_activated: Whether activate() was called.
|
||||
_notifier: A QSocketNotifier used for signals on Unix.
|
||||
_timer: A QTimer used to poll for signals on Windows.
|
||||
_orig_handlers: A {signal: handler} dict of original signal handlers.
|
||||
_orig_wakeup_fd: The original wakeup filedescriptor.
|
||||
"""
|
||||
|
||||
def __init__(self, *, app, quitter, parent=None):
|
||||
super().__init__(parent)
|
||||
self._app = app
|
||||
self._quitter = quitter
|
||||
self._notifier = None
|
||||
self._timer = usertypes.Timer(self, 'python_hacks')
|
||||
self._orig_handlers = {}
|
||||
self._activated = False
|
||||
self._orig_wakeup_fd = None
|
||||
|
||||
def activate(self):
|
||||
"""Set up signal handlers.
|
||||
|
||||
On Windows this uses a QTimer to periodically hand control over to
|
||||
Python so it can handle signals.
|
||||
|
||||
On Unix, it uses a QSocketNotifier with os.set_wakeup_fd to get
|
||||
notified.
|
||||
"""
|
||||
self._orig_handlers[signal.SIGINT] = signal.signal(
|
||||
signal.SIGINT, self.interrupt)
|
||||
self._orig_handlers[signal.SIGTERM] = signal.signal(
|
||||
signal.SIGTERM, self.interrupt)
|
||||
|
||||
if os.name == 'posix' and hasattr(signal, 'set_wakeup_fd'):
|
||||
# pylint: disable=import-error,no-member
|
||||
import fcntl
|
||||
read_fd, write_fd = os.pipe()
|
||||
for fd in (read_fd, write_fd):
|
||||
flags = fcntl.fcntl(fd, fcntl.F_GETFL)
|
||||
fcntl.fcntl(fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
|
||||
self._notifier = QSocketNotifier(
|
||||
read_fd, QSocketNotifier.Read, self)
|
||||
self._notifier.activated.connect(self.handle_signal_wakeup)
|
||||
self._orig_wakeup_fd = signal.set_wakeup_fd(write_fd)
|
||||
else:
|
||||
self._timer.start(1000)
|
||||
self._timer.timeout.connect(lambda: None)
|
||||
self._activated = True
|
||||
|
||||
def deactivate(self):
|
||||
"""Deactivate all signal handlers."""
|
||||
if not self._activated:
|
||||
return
|
||||
if self._notifier is not None:
|
||||
self._notifier.setEnabled(False)
|
||||
rfd = self._notifier.socket()
|
||||
wfd = signal.set_wakeup_fd(self._orig_wakeup_fd)
|
||||
os.close(rfd)
|
||||
os.close(wfd)
|
||||
for sig, handler in self._orig_handlers.items():
|
||||
signal.signal(sig, handler)
|
||||
self._timer.stop()
|
||||
self._activated = False
|
||||
|
||||
@pyqtSlot()
|
||||
def handle_signal_wakeup(self):
|
||||
"""Handle a newly arrived signal.
|
||||
|
||||
This gets called via self._notifier when there's a signal.
|
||||
|
||||
Python will get control here, so the signal will get handled.
|
||||
"""
|
||||
log.destroy.debug("Handling signal wakeup!")
|
||||
self._notifier.setEnabled(False)
|
||||
read_fd = self._notifier.socket()
|
||||
try:
|
||||
os.read(read_fd, 1)
|
||||
except OSError:
|
||||
log.destroy.exception("Failed to read wakeup fd.")
|
||||
self._notifier.setEnabled(True)
|
||||
|
||||
def interrupt(self, signum, _frame):
|
||||
"""Handler for signals to gracefully shutdown (SIGINT/SIGTERM).
|
||||
|
||||
This calls shutdown and remaps the signal to call
|
||||
interrupt_forcefully the next time.
|
||||
"""
|
||||
log.destroy.info("SIGINT/SIGTERM received, shutting down!")
|
||||
log.destroy.info("Do the same again to forcefully quit.")
|
||||
signal.signal(signal.SIGINT, self.interrupt_forcefully)
|
||||
signal.signal(signal.SIGTERM, self.interrupt_forcefully)
|
||||
# If we call shutdown directly here, we get a segfault.
|
||||
QTimer.singleShot(0, functools.partial(
|
||||
self._quitter.shutdown, 128 + signum))
|
||||
|
||||
def interrupt_forcefully(self, signum, _frame):
|
||||
"""Interrupt forcefully on the second SIGINT/SIGTERM request.
|
||||
|
||||
This skips our shutdown routine and calls QApplication:exit instead.
|
||||
It then remaps the signals to call self.interrupt_really_forcefully the
|
||||
next time.
|
||||
"""
|
||||
log.destroy.info("Forceful quit requested, goodbye cruel world!")
|
||||
log.destroy.info("Do the same again to quit with even more force.")
|
||||
signal.signal(signal.SIGINT, self.interrupt_really_forcefully)
|
||||
signal.signal(signal.SIGTERM, self.interrupt_really_forcefully)
|
||||
# This *should* work without a QTimer, but because of the trouble in
|
||||
# self.interrupt we're better safe than sorry.
|
||||
QTimer.singleShot(0, functools.partial(self._app.exit, 128 + signum))
|
||||
|
||||
def interrupt_really_forcefully(self, signum, _frame):
|
||||
"""Interrupt with even more force on the third SIGINT/SIGTERM request.
|
||||
|
||||
This doesn't run *any* Qt cleanup and simply exits via Python.
|
||||
It will most likely lead to a segfault.
|
||||
"""
|
||||
log.destroy.info("WHY ARE YOU DOING THIS TO ME? :(")
|
||||
sys.exit(128 + signum)
|
||||
@@ -80,16 +80,21 @@ def _die(message, exception=None):
|
||||
"""
|
||||
from PyQt5.QtWidgets import QApplication, QMessageBox
|
||||
from PyQt5.QtCore import Qt
|
||||
if '--debug' in sys.argv and exception is not None:
|
||||
if (('--debug' in sys.argv or '--no-err-windows' in sys.argv) and
|
||||
exception is not None):
|
||||
print(file=sys.stderr)
|
||||
traceback.print_exc()
|
||||
app = QApplication(sys.argv)
|
||||
message += '<br/><br/><br/><b>Error:</b><br/>{}'.format(exception)
|
||||
msgbox = QMessageBox(QMessageBox.Critical, "qutebrowser: Fatal error!",
|
||||
message)
|
||||
msgbox.setTextFormat(Qt.RichText)
|
||||
msgbox.resize(msgbox.sizeHint())
|
||||
msgbox.exec_()
|
||||
if '--no-err-windows' in sys.argv:
|
||||
print(message, file=sys.stderr)
|
||||
print("Exiting because of --no-err-windows.", file=sys.stderr)
|
||||
else:
|
||||
message += '<br/><br/><br/><b>Error:</b><br/>{}'.format(exception)
|
||||
msgbox = QMessageBox(QMessageBox.Critical, "qutebrowser: Fatal error!",
|
||||
message)
|
||||
msgbox.setTextFormat(Qt.RichText)
|
||||
msgbox.resize(msgbox.sizeHint())
|
||||
msgbox.exec_()
|
||||
app.quit()
|
||||
sys.exit(1)
|
||||
|
||||
@@ -132,10 +137,10 @@ def fix_harfbuzz(args):
|
||||
- On Qt 5.2 (and probably earlier) the new engine probably has more
|
||||
crashes and is also experimental.
|
||||
|
||||
e.g. https://bugreports.qt-project.org/browse/QTBUG-36099
|
||||
e.g. https://bugreports.qt.io/browse/QTBUG-36099
|
||||
|
||||
- On Qt 5.3.0 there's a bug that affects a lot of websites:
|
||||
https://bugreports.qt-project.org/browse/QTBUG-39278
|
||||
https://bugreports.qt.io/browse/QTBUG-39278
|
||||
So the new engine will be more stable.
|
||||
|
||||
- On Qt 5.3.1 this bug is fixed and the old engine will be the more stable
|
||||
@@ -186,13 +191,13 @@ def check_pyqt_core():
|
||||
text = text.replace('</b>', '')
|
||||
text = text.replace('<br />', '\n')
|
||||
text += '\n\nError: {}'.format(e)
|
||||
if tkinter:
|
||||
if tkinter and '--no-err-windows' not in sys.argv:
|
||||
root = tkinter.Tk()
|
||||
root.withdraw()
|
||||
tkinter.messagebox.showerror("qutebrowser: Fatal error!", text)
|
||||
else:
|
||||
print(text, file=sys.stderr)
|
||||
if '--debug' in sys.argv:
|
||||
if '--debug' in sys.argv or '--no-err-windows' in sys.argv:
|
||||
print(file=sys.stderr)
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
@@ -208,6 +213,19 @@ def check_qt_version():
|
||||
_die(text)
|
||||
|
||||
|
||||
def check_ssl_support():
|
||||
"""Check if SSL support is available."""
|
||||
try:
|
||||
from PyQt5.QtNetwork import QSslSocket
|
||||
except ImportError:
|
||||
ok = False
|
||||
else:
|
||||
ok = QSslSocket.supportsSsl()
|
||||
if not ok:
|
||||
text = "Fatal error: Your Qt is built without SSL support."
|
||||
_die(text)
|
||||
|
||||
|
||||
def check_libraries():
|
||||
"""Check if all needed Python libraries are installed."""
|
||||
modules = {
|
||||
@@ -283,6 +301,7 @@ def earlyinit(args):
|
||||
# Now we can be sure QtCore is available, so we can print dialogs on
|
||||
# errors, so people only using the GUI notice them as well.
|
||||
check_qt_version()
|
||||
check_ssl_support()
|
||||
remove_inputhook()
|
||||
check_libraries()
|
||||
init_log(args)
|
||||
|
||||
@@ -22,10 +22,11 @@
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, QProcess, QObject
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QProcess
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import message, log
|
||||
from qutebrowser.misc import guiprocess
|
||||
|
||||
|
||||
class ExternalEditor(QObject):
|
||||
@@ -36,7 +37,7 @@ class ExternalEditor(QObject):
|
||||
_text: The current text before the editor is opened.
|
||||
_oshandle: The OS level handle to the tmpfile.
|
||||
_filehandle: The file handle to the tmpfile.
|
||||
_proc: The QProcess of the editor.
|
||||
_proc: The GUIProcess of the editor.
|
||||
_win_id: The window ID the ExternalEditor is associated with.
|
||||
"""
|
||||
|
||||
@@ -69,15 +70,10 @@ class ExternalEditor(QObject):
|
||||
log.procs.debug("Editor closed")
|
||||
if exitstatus != QProcess.NormalExit:
|
||||
# No error/cleanup here, since we already handle this in
|
||||
# on_proc_error
|
||||
# on_proc_error.
|
||||
return
|
||||
try:
|
||||
if exitcode != 0:
|
||||
# NOTE: Do not replace this with "raise CommandError" as it's
|
||||
# executed async.
|
||||
message.error(
|
||||
self._win_id, "Editor did quit abnormally (status "
|
||||
"{})!".format(exitcode))
|
||||
return
|
||||
encoding = config.get('general', 'editor-encoding')
|
||||
try:
|
||||
@@ -94,22 +90,8 @@ class ExternalEditor(QObject):
|
||||
finally:
|
||||
self._cleanup()
|
||||
|
||||
def on_proc_error(self, error):
|
||||
"""Display an error message and clean up when editor crashed."""
|
||||
messages = {
|
||||
QProcess.FailedToStart: "The process failed to start.",
|
||||
QProcess.Crashed: "The process crashed.",
|
||||
QProcess.Timedout: "The last waitFor...() function timed out.",
|
||||
QProcess.WriteError: ("An error occurred when attempting to write "
|
||||
"to the process."),
|
||||
QProcess.ReadError: ("An error occurred when attempting to read "
|
||||
"from the process."),
|
||||
QProcess.UnknownError: "An unknown error occurred.",
|
||||
}
|
||||
# NOTE: Do not replace this with "raise CommandError" as it's
|
||||
# executed async.
|
||||
message.error(self._win_id,
|
||||
"Error while calling editor: {}".format(messages[error]))
|
||||
@pyqtSlot(QProcess.ProcessError)
|
||||
def on_proc_error(self, _err):
|
||||
self._cleanup()
|
||||
|
||||
def edit(self, text):
|
||||
@@ -122,7 +104,8 @@ class ExternalEditor(QObject):
|
||||
raise ValueError("Already editing a file!")
|
||||
self._text = text
|
||||
try:
|
||||
self._oshandle, self._filename = tempfile.mkstemp(text=True)
|
||||
self._oshandle, self._filename = tempfile.mkstemp(
|
||||
text=True, prefix='qutebrowser-editor-')
|
||||
if text:
|
||||
encoding = config.get('general', 'editor-encoding')
|
||||
with open(self._filename, 'w', encoding=encoding) as f:
|
||||
@@ -131,7 +114,8 @@ class ExternalEditor(QObject):
|
||||
message.error(self._win_id, "Failed to create initial file: "
|
||||
"{}".format(e))
|
||||
return
|
||||
self._proc = QProcess(self)
|
||||
self._proc = guiprocess.GUIProcess(self._win_id, what='editor',
|
||||
parent=self)
|
||||
self._proc.finished.connect(self.on_proc_closed)
|
||||
self._proc.error.connect(self.on_proc_error)
|
||||
editor = config.get('general', 'editor')
|
||||
|
||||
152
qutebrowser/misc/guiprocess.py
Normal file
152
qutebrowser/misc/guiprocess.py
Normal file
@@ -0,0 +1,152 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
# qutebrowser is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# qutebrowser is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""A QProcess which shows notifications in the GUI."""
|
||||
|
||||
import shlex
|
||||
|
||||
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, QObject, QProcess,
|
||||
QProcessEnvironment)
|
||||
|
||||
from qutebrowser.utils import message, log
|
||||
|
||||
# A mapping of QProcess::ErrorCode's to human-readable strings.
|
||||
|
||||
ERROR_STRINGS = {
|
||||
QProcess.FailedToStart: "The process failed to start.",
|
||||
QProcess.Crashed: "The process crashed.",
|
||||
QProcess.Timedout: "The last waitFor...() function timed out.",
|
||||
QProcess.WriteError: ("An error occurred when attempting to write to the "
|
||||
"process."),
|
||||
QProcess.ReadError: ("An error occurred when attempting to read from the "
|
||||
"process."),
|
||||
QProcess.UnknownError: "An unknown error occurred.",
|
||||
}
|
||||
|
||||
|
||||
class GUIProcess(QObject):
|
||||
|
||||
"""An external process which shows notifications in the GUI.
|
||||
|
||||
Args:
|
||||
cmd: The command which was started.
|
||||
args: A list of arguments which gets passed.
|
||||
_started: Whether the underlying process is started.
|
||||
_proc: The underlying QProcess.
|
||||
_win_id: The window ID this process is used in.
|
||||
_what: What kind of thing is spawned (process/editor/userscript/...).
|
||||
Used in messages.
|
||||
_verbose: Whether to show more messages.
|
||||
|
||||
Signals:
|
||||
error/finished/started signals proxied from QProcess.
|
||||
"""
|
||||
|
||||
error = pyqtSignal(QProcess.ProcessError)
|
||||
finished = pyqtSignal(int, QProcess.ExitStatus)
|
||||
started = pyqtSignal()
|
||||
|
||||
def __init__(self, win_id, what, *, verbose=False, additional_env=None,
|
||||
parent=None):
|
||||
super().__init__(parent)
|
||||
self._win_id = win_id
|
||||
self._what = what
|
||||
self._verbose = verbose
|
||||
self._started = False
|
||||
self.cmd = None
|
||||
self.args = None
|
||||
|
||||
self._proc = QProcess(self)
|
||||
self._proc.error.connect(self.on_error)
|
||||
self._proc.error.connect(self.error)
|
||||
self._proc.finished.connect(self.on_finished)
|
||||
self._proc.finished.connect(self.finished)
|
||||
self._proc.started.connect(self.on_started)
|
||||
self._proc.started.connect(self.started)
|
||||
|
||||
if additional_env is not None:
|
||||
procenv = QProcessEnvironment.systemEnvironment()
|
||||
for k, v in additional_env.items():
|
||||
procenv.insert(k, v)
|
||||
self._proc.setProcessEnvironment(procenv)
|
||||
|
||||
@pyqtSlot(QProcess.ProcessError)
|
||||
def on_error(self, error):
|
||||
"""Show a message if there was an error while spawning."""
|
||||
msg = ERROR_STRINGS[error]
|
||||
message.error(self._win_id, "Error while spawning {}: {}".format(
|
||||
self._what, msg), immediately=True)
|
||||
|
||||
@pyqtSlot(int, QProcess.ExitStatus)
|
||||
def on_finished(self, code, status):
|
||||
"""Show a message when the process finished."""
|
||||
self._started = False
|
||||
log.procs.debug("Process finished with code {}, status {}.".format(
|
||||
code, status))
|
||||
if status == QProcess.CrashExit:
|
||||
message.error(self._win_id,
|
||||
"{} crashed!".format(self._what.capitalize()),
|
||||
immediately=True)
|
||||
elif status == QProcess.NormalExit and code == 0:
|
||||
if self._verbose:
|
||||
message.info(self._win_id, "{} exited successfully.".format(
|
||||
self._what.capitalize()))
|
||||
else:
|
||||
assert status == QProcess.NormalExit
|
||||
message.error(self._win_id, "{} exited with status {}.".format(
|
||||
self._what.capitalize(), code))
|
||||
|
||||
@pyqtSlot()
|
||||
def on_started(self):
|
||||
"""Called when the process started successfully."""
|
||||
log.procs.debug("Process started.")
|
||||
assert not self._started
|
||||
self._started = True
|
||||
|
||||
def _pre_start(self, cmd, args):
|
||||
"""Prepare starting of a QProcess."""
|
||||
if self._started:
|
||||
raise ValueError("Trying to start a running QProcess!")
|
||||
self.cmd = cmd
|
||||
self.args = args
|
||||
if self._verbose:
|
||||
fake_cmdline = ' '.join(shlex.quote(e) for e in [cmd] + list(args))
|
||||
message.info(self._win_id, 'Executing: ' + fake_cmdline)
|
||||
|
||||
def start(self, cmd, args, mode=None):
|
||||
"""Convenience wrapper around QProcess::start."""
|
||||
log.procs.debug("Starting process.")
|
||||
self._pre_start(cmd, args)
|
||||
if mode is None:
|
||||
self._proc.start(cmd, args)
|
||||
else:
|
||||
self._proc.start(cmd, args, mode)
|
||||
|
||||
def start_detached(self, cmd, args, cwd=None):
|
||||
"""Convenience wrapper around QProcess::startDetached."""
|
||||
log.procs.debug("Starting detached.")
|
||||
self._pre_start(cmd, args)
|
||||
ok, _pid = self._proc.startDetached(cmd, args, cwd)
|
||||
|
||||
if ok:
|
||||
log.procs.debug("Process started.")
|
||||
self._started = True
|
||||
else:
|
||||
message.error(self._win_id, "Error while spawning {}: {}.".format(
|
||||
self._what, self._proc.error()), immediately=True)
|
||||
@@ -23,20 +23,28 @@ import os
|
||||
import json
|
||||
import getpass
|
||||
import binascii
|
||||
import hashlib
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot, QObject
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject
|
||||
from PyQt5.QtNetwork import QLocalSocket, QLocalServer, QAbstractSocket
|
||||
from PyQt5.QtWidgets import QMessageBox
|
||||
|
||||
from qutebrowser.utils import log, objreg, usertypes
|
||||
from qutebrowser.utils import log, usertypes, error
|
||||
|
||||
|
||||
SOCKETNAME = 'qutebrowser-{}'.format(getpass.getuser())
|
||||
CONNECT_TIMEOUT = 100
|
||||
WRITE_TIMEOUT = 1000
|
||||
READ_TIMEOUT = 5000
|
||||
|
||||
|
||||
def _get_socketname(args):
|
||||
"""Get a socketname to use."""
|
||||
parts = ['qutebrowser', getpass.getuser()]
|
||||
if args.basedir is not None:
|
||||
md5 = hashlib.md5(args.basedir.encode('utf-8'))
|
||||
parts.append(md5.hexdigest())
|
||||
return '-'.join(parts)
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
|
||||
"""Exception raised when there was a problem with IPC."""
|
||||
@@ -80,18 +88,31 @@ class IPCServer(QObject):
|
||||
_timer: A timer to handle timeouts.
|
||||
_server: A QLocalServer to accept new connections.
|
||||
_socket: The QLocalSocket we're currently connected to.
|
||||
_socketname: The socketname to use.
|
||||
|
||||
Signals:
|
||||
got_args: Emitted when there was an IPC connection and arguments were
|
||||
passed.
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
"""Start the IPC server and listen to commands."""
|
||||
got_args = pyqtSignal(list, str)
|
||||
|
||||
def __init__(self, args, parent=None):
|
||||
"""Start the IPC server and listen to commands.
|
||||
|
||||
Args:
|
||||
args: The argparse namespace.
|
||||
parent: The parent to be used.
|
||||
"""
|
||||
super().__init__(parent)
|
||||
self.ignored = False
|
||||
self._socketname = _get_socketname(args)
|
||||
self._remove_server()
|
||||
self._timer = usertypes.Timer(self, 'ipc-timeout')
|
||||
self._timer.setInterval(READ_TIMEOUT)
|
||||
self._timer.timeout.connect(self.on_timeout)
|
||||
self._server = QLocalServer(self)
|
||||
ok = self._server.listen(SOCKETNAME)
|
||||
ok = self._server.listen(self._socketname)
|
||||
if not ok:
|
||||
if self._server.serverError() == QAbstractSocket.AddressInUseError:
|
||||
raise AddressInUseError(self._server)
|
||||
@@ -102,17 +123,18 @@ class IPCServer(QObject):
|
||||
|
||||
def _remove_server(self):
|
||||
"""Remove an existing server."""
|
||||
ok = QLocalServer.removeServer(SOCKETNAME)
|
||||
ok = QLocalServer.removeServer(self._socketname)
|
||||
if not ok:
|
||||
raise Error("Error while removing server {}!".format(SOCKETNAME))
|
||||
raise Error("Error while removing server {}!".format(
|
||||
self._socketname))
|
||||
|
||||
@pyqtSlot(int)
|
||||
def on_error(self, error):
|
||||
def on_error(self, err):
|
||||
"""Convenience method which calls _socket_error on an error."""
|
||||
self._timer.stop()
|
||||
log.ipc.debug("Socket error {}: {}".format(
|
||||
self._socket.error(), self._socket.errorString()))
|
||||
if error != QLocalSocket.PeerClosedError:
|
||||
if err != QLocalSocket.PeerClosedError:
|
||||
_socket_error("handling IPC connection", self._socket)
|
||||
|
||||
@pyqtSlot()
|
||||
@@ -187,8 +209,7 @@ class IPCServer(QObject):
|
||||
log.ipc.debug("no args: {}".format(decoded.strip()))
|
||||
return
|
||||
cwd = json_data.get('cwd', None)
|
||||
app = objreg.get('app')
|
||||
app.process_pos_args(args, via_ipc=True, cwd=cwd)
|
||||
self.got_args.emit(args, cwd)
|
||||
|
||||
@pyqtSlot()
|
||||
def on_timeout(self):
|
||||
@@ -207,13 +228,6 @@ class IPCServer(QObject):
|
||||
self._remove_server()
|
||||
|
||||
|
||||
def init():
|
||||
"""Initialize the global IPC server."""
|
||||
app = objreg.get('app')
|
||||
server = IPCServer(app)
|
||||
objreg.register('ipc-server', server)
|
||||
|
||||
|
||||
def _socket_error(action, socket):
|
||||
"""Raise an Error based on an action and a QLocalSocket.
|
||||
|
||||
@@ -225,23 +239,23 @@ def _socket_error(action, socket):
|
||||
action, socket.errorString(), socket.error()))
|
||||
|
||||
|
||||
def send_to_running_instance(cmdlist):
|
||||
def send_to_running_instance(args):
|
||||
"""Try to send a commandline to a running instance.
|
||||
|
||||
Blocks for CONNECT_TIMEOUT ms.
|
||||
|
||||
Args:
|
||||
cmdlist: A list to send (URLs/commands)
|
||||
args: The argparse namespace.
|
||||
|
||||
Return:
|
||||
True if connecting was successful, False if no connection was made.
|
||||
"""
|
||||
socket = QLocalSocket()
|
||||
socket.connectToServer(SOCKETNAME)
|
||||
socket.connectToServer(_get_socketname(args))
|
||||
connected = socket.waitForConnected(100)
|
||||
if connected:
|
||||
log.ipc.info("Opening in existing instance")
|
||||
json_data = {'args': cmdlist}
|
||||
json_data = {'args': args.command}
|
||||
try:
|
||||
cwd = os.getcwd()
|
||||
except OSError:
|
||||
@@ -267,9 +281,8 @@ def send_to_running_instance(cmdlist):
|
||||
return False
|
||||
|
||||
|
||||
def display_error(exc):
|
||||
def display_error(exc, args):
|
||||
"""Display a message box with an IPC error."""
|
||||
text = '{}\n\nMaybe another instance is running but frozen?'.format(exc)
|
||||
msgbox = QMessageBox(QMessageBox.Critical, "Error while connecting to "
|
||||
"running instance!", text)
|
||||
msgbox.exec_()
|
||||
error.handle_fatal_exc(
|
||||
exc, args, "Error while connecting to running instance!",
|
||||
post_text="Maybe another instance is running but frozen?")
|
||||
|
||||
@@ -35,7 +35,7 @@ class BaseLineParser(QObject):
|
||||
"""A LineParser without any real data.
|
||||
|
||||
Attributes:
|
||||
_configdir: The directory to read the config from.
|
||||
_configdir: Directory to read the config from, or None.
|
||||
_configfile: The config file path.
|
||||
_fname: Filename of the config.
|
||||
_binary: Whether to open the file in binary mode.
|
||||
@@ -53,12 +53,17 @@ class BaseLineParser(QObject):
|
||||
configdir: Directory to read the config from.
|
||||
fname: Filename of the config file.
|
||||
binary: Whether to open the file in binary mode.
|
||||
_opened: Whether the underlying file is open
|
||||
"""
|
||||
super().__init__(parent)
|
||||
self._configdir = configdir
|
||||
self._configfile = os.path.join(self._configdir, fname)
|
||||
if self._configdir is None:
|
||||
self._configfile = None
|
||||
else:
|
||||
self._configfile = os.path.join(self._configdir, fname)
|
||||
self._fname = fname
|
||||
self._binary = binary
|
||||
self._opened = False
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self, constructor=True,
|
||||
@@ -66,21 +71,38 @@ class BaseLineParser(QObject):
|
||||
binary=self._binary)
|
||||
|
||||
def _prepare_save(self):
|
||||
"""Prepare saving of the file."""
|
||||
"""Prepare saving of the file.
|
||||
|
||||
Return:
|
||||
True if the file should be saved, False otherwise.
|
||||
"""
|
||||
if self._configdir is None:
|
||||
return False
|
||||
log.destroy.debug("Saving to {}".format(self._configfile))
|
||||
if not os.path.exists(self._configdir):
|
||||
os.makedirs(self._configdir, 0o755)
|
||||
return True
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _open(self, mode):
|
||||
"""Open self._configfile for reading.
|
||||
|
||||
Args:
|
||||
mode: The mode to use ('a'/'r'/'w')
|
||||
"""
|
||||
if self._binary:
|
||||
return open(self._configfile, mode + 'b')
|
||||
else:
|
||||
return open(self._configfile, mode, encoding='utf-8')
|
||||
assert self._configfile is not None
|
||||
if self._opened:
|
||||
raise IOError("Refusing to double-open AppendLineParser.")
|
||||
self._opened = True
|
||||
try:
|
||||
if self._binary:
|
||||
with open(self._configfile, mode + 'b') as f:
|
||||
yield f
|
||||
else:
|
||||
with open(self._configfile, mode, encoding='utf-8') as f:
|
||||
yield f
|
||||
finally:
|
||||
self._opened = False
|
||||
|
||||
def _write(self, fp, data):
|
||||
"""Write the data to a file.
|
||||
@@ -150,7 +172,9 @@ class AppendLineParser(BaseLineParser):
|
||||
return data
|
||||
|
||||
def save(self):
|
||||
self._prepare_save()
|
||||
do_save = self._prepare_save()
|
||||
if not do_save:
|
||||
return
|
||||
with self._open('a') as f:
|
||||
self._write(f, self.new_data)
|
||||
self.new_data = []
|
||||
@@ -173,7 +197,7 @@ class LineParser(BaseLineParser):
|
||||
binary: Whether to open the file in binary mode.
|
||||
"""
|
||||
super().__init__(configdir, fname, binary=binary, parent=parent)
|
||||
if not os.path.isfile(self._configfile):
|
||||
if configdir is None or not os.path.isfile(self._configfile):
|
||||
self.data = []
|
||||
else:
|
||||
log.init.debug("Reading {}".format(self._configfile))
|
||||
@@ -195,9 +219,18 @@ class LineParser(BaseLineParser):
|
||||
|
||||
def save(self):
|
||||
"""Save the config file."""
|
||||
self._prepare_save()
|
||||
with qtutils.savefile_open(self._configfile, self._binary) as f:
|
||||
self._write(f, self.data)
|
||||
if self._opened:
|
||||
raise IOError("Refusing to double-open AppendLineParser.")
|
||||
do_save = self._prepare_save()
|
||||
if not do_save:
|
||||
return
|
||||
self._opened = True
|
||||
try:
|
||||
assert self._configfile is not None
|
||||
with qtutils.savefile_open(self._configfile, self._binary) as f:
|
||||
self._write(f, self.data)
|
||||
finally:
|
||||
self._opened = False
|
||||
|
||||
|
||||
class LimitLineParser(LineParser):
|
||||
@@ -213,14 +246,14 @@ class LimitLineParser(LineParser):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
configdir: Directory to read the config from.
|
||||
configdir: Directory to read the config from, or None.
|
||||
fname: Filename of the config file.
|
||||
limit: Config tuple (section, option) which contains a limit.
|
||||
binary: Whether to open the file in binary mode.
|
||||
"""
|
||||
super().__init__(configdir, fname, binary=binary, parent=parent)
|
||||
self._limit = limit
|
||||
if limit is not None:
|
||||
if limit is not None and configdir is not None:
|
||||
objreg.get('config').changed.connect(self.cleanup_file)
|
||||
|
||||
def __repr__(self):
|
||||
@@ -231,6 +264,7 @@ class LimitLineParser(LineParser):
|
||||
@pyqtSlot(str, str)
|
||||
def cleanup_file(self, section, option):
|
||||
"""Delete the file if the limit was changed to 0."""
|
||||
assert self._configfile is not None
|
||||
if (section, option) != self._limit:
|
||||
return
|
||||
value = config.get(section, option)
|
||||
@@ -243,6 +277,9 @@ class LimitLineParser(LineParser):
|
||||
limit = config.get(*self._limit)
|
||||
if limit == 0:
|
||||
return
|
||||
self._prepare_save()
|
||||
do_save = self._prepare_save()
|
||||
if not do_save:
|
||||
return
|
||||
assert self._configfile is not None
|
||||
with qtutils.savefile_open(self._configfile, self._binary) as f:
|
||||
self._write(f, self.data[-limit:])
|
||||
|
||||
@@ -77,7 +77,7 @@ class CommandLineEdit(QLineEdit):
|
||||
def __on_cursor_position_changed(self, _old, new):
|
||||
"""Prevent the cursor moving to the prompt.
|
||||
|
||||
We use __ here to avoid accidentally overriding it in superclasses.
|
||||
We use __ here to avoid accidentally overriding it in subclasses.
|
||||
"""
|
||||
if new < self._promptlen:
|
||||
self.setCursorPosition(self._promptlen)
|
||||
|
||||
@@ -184,9 +184,8 @@ class SaveManager(QObject):
|
||||
message.error('current', "Failed to auto-save {}: "
|
||||
"{}".format(key, e))
|
||||
|
||||
@cmdutils.register(instance='save-manager', name='save')
|
||||
def save_command(self, win_id: {'special': 'win_id'},
|
||||
*what: {'nargs': '*'}):
|
||||
@cmdutils.register(instance='save-manager', name='save', win_id='win_id')
|
||||
def save_command(self, win_id, *what: {'nargs': '*'}):
|
||||
"""Save configs and state.
|
||||
|
||||
Args:
|
||||
|
||||
@@ -82,10 +82,14 @@ class SessionManager(QObject):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._current = None
|
||||
self._base_path = os.path.join(standarddir.data(), 'sessions')
|
||||
data_dir = standarddir.data()
|
||||
if data_dir is None:
|
||||
self._base_path = None
|
||||
else:
|
||||
self._base_path = os.path.join(standarddir.data(), 'sessions')
|
||||
self._last_window_session = None
|
||||
self.did_load = False
|
||||
if not os.path.exists(self._base_path):
|
||||
if self._base_path is not None and not os.path.exists(self._base_path):
|
||||
os.mkdir(self._base_path)
|
||||
|
||||
def _get_session_path(self, name, check_exists=False):
|
||||
@@ -100,6 +104,11 @@ class SessionManager(QObject):
|
||||
if os.path.isabs(path) and ((not check_exists) or
|
||||
os.path.exists(path)):
|
||||
return path
|
||||
elif self._base_path is None:
|
||||
if check_exists:
|
||||
raise SessionNotFoundError(name)
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
path = os.path.join(self._base_path, name + '.yml')
|
||||
if check_exists and not os.path.exists(path):
|
||||
@@ -136,21 +145,23 @@ class SessionManager(QObject):
|
||||
if item.originalUrl() != item.url():
|
||||
encoded = item.originalUrl().toEncoded()
|
||||
item_data['original-url'] = bytes(encoded).decode('ascii')
|
||||
user_data = item.userData()
|
||||
|
||||
if history.currentItemIndex() == idx:
|
||||
item_data['active'] = True
|
||||
if user_data is None:
|
||||
pos = tab.page().mainFrame().scrollPosition()
|
||||
data['zoom'] = tab.zoomFactor()
|
||||
data['scroll-pos'] = {'x': pos.x(), 'y': pos.y()}
|
||||
data['history'].append(item_data)
|
||||
|
||||
if user_data is not None:
|
||||
user_data = item.userData()
|
||||
if history.currentItemIndex() == idx:
|
||||
pos = tab.page().mainFrame().scrollPosition()
|
||||
item_data['zoom'] = tab.zoomFactor()
|
||||
item_data['scroll-pos'] = {'x': pos.x(), 'y': pos.y()}
|
||||
elif user_data is not None:
|
||||
if 'zoom' in user_data:
|
||||
data['zoom'] = user_data['zoom']
|
||||
item_data['zoom'] = user_data['zoom']
|
||||
if 'scroll-pos' in user_data:
|
||||
pos = user_data['scroll-pos']
|
||||
data['scroll-pos'] = {'x': pos.x(), 'y': pos.y()}
|
||||
item_data['scroll-pos'] = {'x': pos.x(), 'y': pos.y()}
|
||||
|
||||
data['history'].append(item_data)
|
||||
return data
|
||||
|
||||
def _save_all(self):
|
||||
@@ -194,6 +205,8 @@ class SessionManager(QObject):
|
||||
else:
|
||||
name = 'default'
|
||||
path = self._get_session_path(name)
|
||||
if path is None:
|
||||
raise SessionError("No data storage configured.")
|
||||
|
||||
log.sessions.debug("Saving session {} to {}...".format(name, path))
|
||||
if last_window:
|
||||
@@ -224,11 +237,25 @@ class SessionManager(QObject):
|
||||
entries = []
|
||||
for histentry in data['history']:
|
||||
user_data = {}
|
||||
|
||||
if 'zoom' in data:
|
||||
# The zoom was accidentally stored in 'data' instead of per-tab
|
||||
# earlier.
|
||||
# See https://github.com/The-Compiler/qutebrowser/issues/728
|
||||
user_data['zoom'] = data['zoom']
|
||||
elif 'zoom' in histentry:
|
||||
user_data['zoom'] = histentry['zoom']
|
||||
|
||||
if 'scroll-pos' in data:
|
||||
# The scroll position was accidentally stored in 'data' instead
|
||||
# of per-tab earlier.
|
||||
# See https://github.com/The-Compiler/qutebrowser/issues/728
|
||||
pos = data['scroll-pos']
|
||||
user_data['scroll-pos'] = QPoint(pos['x'], pos['y'])
|
||||
elif 'scroll-pos' in histentry:
|
||||
pos = histentry['scroll-pos']
|
||||
user_data['scroll-pos'] = QPoint(pos['x'], pos['y'])
|
||||
|
||||
active = histentry.get('active', False)
|
||||
url = QUrl.fromEncoded(histentry['url'].encode('ascii'))
|
||||
if 'original-url' in histentry:
|
||||
@@ -289,6 +316,8 @@ class SessionManager(QObject):
|
||||
def list_sessions(self):
|
||||
"""Get a list of all session names."""
|
||||
sessions = []
|
||||
if self._base_path is None:
|
||||
return sessions
|
||||
for filename in os.listdir(self._base_path):
|
||||
base, ext = os.path.splitext(filename)
|
||||
if ext == '.yml':
|
||||
@@ -323,12 +352,11 @@ class SessionManager(QObject):
|
||||
for win in old_windows:
|
||||
win.close()
|
||||
|
||||
@cmdutils.register(name=['session-save', 'w'],
|
||||
@cmdutils.register(name=['session-save', 'w'], win_id='win_id',
|
||||
completion=[usertypes.Completion.sessions],
|
||||
instance='session-manager')
|
||||
def session_save(self, win_id: {'special': 'win_id'},
|
||||
name: {'type': str}=default, current=False, quiet=False,
|
||||
force=False):
|
||||
def session_save(self, win_id, name: {'type': str}=default, current=False,
|
||||
quiet=False, force=False):
|
||||
"""Save a session.
|
||||
|
||||
Args:
|
||||
|
||||
@@ -55,7 +55,7 @@ class ShellLexer:
|
||||
self.token = ''
|
||||
self.state = ' '
|
||||
|
||||
def __iter__(self): # noqa
|
||||
def __iter__(self): # pragma: no mccabe
|
||||
"""Read a raw token from the input stream."""
|
||||
# pylint: disable=too-many-branches,too-many-statements
|
||||
self.reset()
|
||||
@@ -127,7 +127,7 @@ def split(s, keep=False):
|
||||
"""Split a string via ShellLexer.
|
||||
|
||||
Args:
|
||||
keep: Whether to keep are special chars in the split output.
|
||||
keep: Whether to keep special chars in the split output.
|
||||
"""
|
||||
lexer = ShellLexer(s)
|
||||
lexer.keep = keep
|
||||
|
||||
@@ -21,21 +21,24 @@
|
||||
|
||||
import functools
|
||||
import types
|
||||
import traceback
|
||||
|
||||
from PyQt5.QtCore import QCoreApplication
|
||||
try:
|
||||
import hunter
|
||||
except ImportError:
|
||||
hunter = None
|
||||
|
||||
from qutebrowser.utils import log, objreg, usertypes
|
||||
from qutebrowser.browser.network import qutescheme
|
||||
from qutebrowser.utils import log, objreg, usertypes, message, debug
|
||||
from qutebrowser.commands import cmdutils, runners, cmdexc
|
||||
from qutebrowser.config import style
|
||||
from qutebrowser.misc import consolewidget
|
||||
|
||||
from PyQt5.QtCore import QUrl
|
||||
|
||||
@cmdutils.register(scope='window', maxsplit=1, no_cmd_split=True)
|
||||
def later(ms: {'type': int}, command, win_id: {'special': 'win_id'}):
|
||||
|
||||
@cmdutils.register(maxsplit=1, no_cmd_split=True, win_id='win_id')
|
||||
def later(ms: {'type': int}, command, win_id):
|
||||
"""Execute a command after some time.
|
||||
|
||||
Args:
|
||||
@@ -63,8 +66,8 @@ def later(ms: {'type': int}, command, win_id: {'special': 'win_id'}):
|
||||
raise
|
||||
|
||||
|
||||
@cmdutils.register(scope='window', maxsplit=1, no_cmd_split=True)
|
||||
def repeat(times: {'type': int}, command, win_id: {'special': 'win_id'}):
|
||||
@cmdutils.register(maxsplit=1, no_cmd_split=True, win_id='win_id')
|
||||
def repeat(times: {'type': int}, command, win_id):
|
||||
"""Repeat a given command.
|
||||
|
||||
Args:
|
||||
@@ -78,6 +81,36 @@ def repeat(times: {'type': int}, command, win_id: {'special': 'win_id'}):
|
||||
commandrunner.run_safely(command)
|
||||
|
||||
|
||||
@cmdutils.register(hide=True, win_id='win_id')
|
||||
def message_error(win_id, text):
|
||||
"""Show an error message in the statusbar.
|
||||
|
||||
Args:
|
||||
text: The text to show.
|
||||
"""
|
||||
message.error(win_id, text)
|
||||
|
||||
|
||||
@cmdutils.register(hide=True, win_id='win_id')
|
||||
def message_info(win_id, text):
|
||||
"""Show an info message in the statusbar.
|
||||
|
||||
Args:
|
||||
text: The text to show.
|
||||
"""
|
||||
message.info(win_id, text)
|
||||
|
||||
|
||||
@cmdutils.register(hide=True, win_id='win_id')
|
||||
def message_warning(win_id, text):
|
||||
"""Show a warning message in the statusbar.
|
||||
|
||||
Args:
|
||||
text: The text to show.
|
||||
"""
|
||||
message.warning(win_id, text)
|
||||
|
||||
|
||||
@cmdutils.register(debug=True)
|
||||
def debug_crash(typ: {'type': ('exception', 'segfault')}='exception'):
|
||||
"""Crash for debugging purposes.
|
||||
@@ -98,7 +131,7 @@ def debug_crash(typ: {'type': ('exception', 'segfault')}='exception'):
|
||||
@cmdutils.register(debug=True)
|
||||
def debug_all_objects():
|
||||
"""Print a list of all objects to the debug log."""
|
||||
s = QCoreApplication.instance().get_all_objects()
|
||||
s = debug.get_all_objects()
|
||||
log.misc.debug(s)
|
||||
|
||||
|
||||
@@ -136,3 +169,21 @@ def debug_trace(expr=""):
|
||||
eval('hunter.trace({})'.format(expr))
|
||||
except Exception as e:
|
||||
raise cmdexc.CommandError("{}: {}".format(e.__class__.__name__, e))
|
||||
|
||||
|
||||
@cmdutils.register(maxsplit=0, debug=True, no_cmd_split=True)
|
||||
def debug_pyeval(s):
|
||||
"""Evaluate a python string and display the results as a web page.
|
||||
|
||||
Args:
|
||||
s: The string to evaluate.
|
||||
"""
|
||||
try:
|
||||
r = eval(s)
|
||||
out = repr(r)
|
||||
except Exception:
|
||||
out = traceback.format_exc()
|
||||
qutescheme.pyeval_output = out
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window='last-focused')
|
||||
tabbed_browser.openurl(QUrl('qute:pyeval'), newtab=True)
|
||||
|
||||
@@ -48,6 +48,12 @@ def get_argparser():
|
||||
description=qutebrowser.__description__)
|
||||
parser.add_argument('-c', '--confdir', help="Set config directory (empty "
|
||||
"for no config storage).")
|
||||
parser.add_argument('--datadir', help="Set data directory (empty for "
|
||||
"no data storage).")
|
||||
parser.add_argument('--cachedir', help="Set cache directory (empty for "
|
||||
"no cache storage).")
|
||||
parser.add_argument('--basedir', help="Base directory for all storage. "
|
||||
"Other --*dir arguments are ignored if this is given.")
|
||||
parser.add_argument('-V', '--version', help="Show version and quit.",
|
||||
action='store_true')
|
||||
parser.add_argument('-s', '--set', help="Set a temporary setting for "
|
||||
@@ -84,10 +90,12 @@ def get_argparser():
|
||||
"the main window.")
|
||||
debug.add_argument('--debug-exit', help="Turn on debugging of late exit.",
|
||||
action='store_true')
|
||||
debug.add_argument('--no-crash-dialog', action='store_true', help="Don't "
|
||||
"show a crash dialog.")
|
||||
debug.add_argument('--pdb-postmortem', action='store_true',
|
||||
help="Drop into pdb on exceptions.")
|
||||
debug.add_argument('--temp-basedir', action='store_true', help="Use a "
|
||||
"temporary basedir.")
|
||||
debug.add_argument('--no-err-windows', action='store_true', help="Don't "
|
||||
"show any error windows (used for tests/smoke.py).")
|
||||
# For the Qt args, we use store_const with const=True rather than
|
||||
# store_true because we want the default to be None, to make
|
||||
# utils.qt:get_args easier.
|
||||
@@ -138,24 +146,4 @@ def main():
|
||||
# We do this imports late as earlyinit needs to be run first (because of
|
||||
# the harfbuzz fix and version checking).
|
||||
from qutebrowser import app
|
||||
import PyQt5.QtWidgets as QtWidgets
|
||||
app = app.Application(args)
|
||||
|
||||
def qt_mainloop():
|
||||
"""Simple wrapper to get a nicer stack trace for segfaults.
|
||||
|
||||
WARNING: misc/crashdialog.py checks the stacktrace for this function
|
||||
name, so if this is changed, it should be changed there as well!
|
||||
"""
|
||||
return app.exec_()
|
||||
|
||||
# We set qApp explicitly here to reduce the risk of segfaults while
|
||||
# quitting.
|
||||
# See https://bugs.launchpad.net/ubuntu/+source/python-qt4/+bug/561303/comments/7
|
||||
# While this is a workaround for PyQt4 which should be fixed in PyQt, it
|
||||
# seems this still reduces segfaults.
|
||||
# FIXME: We should do another attempt at contacting upstream about this.
|
||||
QtWidgets.qApp = app
|
||||
ret = qt_mainloop()
|
||||
QtWidgets.qApp = None
|
||||
return ret
|
||||
return app.run(args)
|
||||
|
||||
@@ -25,9 +25,10 @@ import functools
|
||||
import datetime
|
||||
import contextlib
|
||||
|
||||
from PyQt5.QtCore import QEvent, QMetaMethod
|
||||
from PyQt5.QtCore import QEvent, QMetaMethod, QObject
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
|
||||
from qutebrowser.utils import log, utils, qtutils
|
||||
from qutebrowser.utils import log, utils, qtutils, objreg
|
||||
|
||||
|
||||
def log_events(klass):
|
||||
@@ -229,7 +230,42 @@ def log_time(logger, action='operation'):
|
||||
action: A description of what's being done.
|
||||
"""
|
||||
started = datetime.datetime.now()
|
||||
yield
|
||||
finished = datetime.datetime.now()
|
||||
delta = (finished - started).total_seconds()
|
||||
logger.debug("{} took {} seconds.".format(action.capitalize(), delta))
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
finished = datetime.datetime.now()
|
||||
delta = (finished - started).total_seconds()
|
||||
logger.debug("{} took {} seconds.".format(action.capitalize(), delta))
|
||||
|
||||
|
||||
def _get_widgets():
|
||||
"""Get a string list of all widgets."""
|
||||
widgets = QApplication.instance().allWidgets()
|
||||
widgets.sort(key=repr)
|
||||
return [repr(w) for w in widgets]
|
||||
|
||||
|
||||
def _get_pyqt_objects(lines, obj, depth=0):
|
||||
"""Recursive method for get_all_objects to get Qt objects."""
|
||||
for kid in obj.findChildren(QObject):
|
||||
lines.append(' ' * depth + repr(kid))
|
||||
_get_pyqt_objects(lines, kid, depth + 1)
|
||||
|
||||
|
||||
def get_all_objects():
|
||||
"""Get all children of an object recursively as a string."""
|
||||
output = ['']
|
||||
widget_lines = _get_widgets()
|
||||
widget_lines = [' ' + e for e in widget_lines]
|
||||
widget_lines.insert(0, "Qt widgets - {} objects".format(
|
||||
len(widget_lines)))
|
||||
output += widget_lines
|
||||
pyqt_lines = []
|
||||
_get_pyqt_objects(pyqt_lines, QApplication.instance())
|
||||
pyqt_lines = [' ' + e for e in pyqt_lines]
|
||||
pyqt_lines.insert(0, 'Qt objects - {} objects:'.format(
|
||||
len(pyqt_lines)))
|
||||
output += pyqt_lines
|
||||
output += ['']
|
||||
output += objreg.dump_objects()
|
||||
return '\n'.join(output)
|
||||
|
||||
54
qutebrowser/utils/error.py
Normal file
54
qutebrowser/utils/error.py
Normal file
@@ -0,0 +1,54 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
# qutebrowser is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# qutebrowser is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Tools related to error printing/displaying."""
|
||||
|
||||
from PyQt5.QtWidgets import QMessageBox
|
||||
|
||||
from qutebrowser.utils import log
|
||||
|
||||
|
||||
def handle_fatal_exc(exc, args, title, *, pre_text='', post_text=''):
|
||||
"""Handle a fatal "expected" exception by displaying an error box.
|
||||
|
||||
If --no-err-windows is given as argument, the text is logged to the error
|
||||
logger instead.
|
||||
|
||||
Args:
|
||||
exc: The Exception object being handled.
|
||||
args: The argparser namespace.
|
||||
title: The title to be used for the error message.
|
||||
pre_text: The text to be displayed before the exception text.
|
||||
post_text: The text to be displayed after the exception text.
|
||||
"""
|
||||
if args.no_err_windows:
|
||||
log.misc.exception("Handling fatal {} with --no-err-windows!".format(
|
||||
exc.__class__.__name__))
|
||||
log.misc.error("title: {}".format(title))
|
||||
log.misc.error("pre_text: {}".format(pre_text))
|
||||
log.misc.error("post_text: {}".format(post_text))
|
||||
else:
|
||||
if pre_text:
|
||||
msg_text = '{}: {}'.format(pre_text, exc)
|
||||
else:
|
||||
msg_text = str(exc)
|
||||
if post_text:
|
||||
msg_text += '\n\n{}'.format(post_text)
|
||||
msgbox = QMessageBox(QMessageBox.Critical, title, msg_text)
|
||||
msgbox.exec_()
|
||||
@@ -29,8 +29,7 @@ import faulthandler
|
||||
import traceback
|
||||
import warnings
|
||||
|
||||
from PyQt5.QtCore import (QtDebugMsg, QtWarningMsg, QtCriticalMsg, QtFatalMsg,
|
||||
qInstallMessageHandler)
|
||||
from PyQt5 import QtCore
|
||||
# Optional imports
|
||||
try:
|
||||
import colorama
|
||||
@@ -153,15 +152,17 @@ def init_log(args):
|
||||
root.setLevel(logging.NOTSET)
|
||||
logging.captureWarnings(True)
|
||||
warnings.simplefilter('default')
|
||||
qInstallMessageHandler(qt_message_handler)
|
||||
QtCore.qInstallMessageHandler(qt_message_handler)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def disable_qt_msghandler():
|
||||
"""Contextmanager which temporarily disables the Qt message handler."""
|
||||
old_handler = qInstallMessageHandler(None)
|
||||
yield
|
||||
qInstallMessageHandler(old_handler)
|
||||
old_handler = QtCore.qInstallMessageHandler(None)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
QtCore.qInstallMessageHandler(old_handler)
|
||||
|
||||
|
||||
def _init_handlers(level, color, ram_capacity):
|
||||
@@ -244,40 +245,50 @@ def qt_message_handler(msg_type, context, msg):
|
||||
# Note we map critical to ERROR as it's actually "just" an error, and fatal
|
||||
# to critical.
|
||||
qt_to_logging = {
|
||||
QtDebugMsg: logging.DEBUG,
|
||||
QtWarningMsg: logging.WARNING,
|
||||
QtCriticalMsg: logging.ERROR,
|
||||
QtFatalMsg: logging.CRITICAL,
|
||||
QtCore.QtDebugMsg: logging.DEBUG,
|
||||
QtCore.QtWarningMsg: logging.WARNING,
|
||||
QtCore.QtCriticalMsg: logging.ERROR,
|
||||
QtCore.QtFatalMsg: logging.CRITICAL,
|
||||
}
|
||||
try:
|
||||
# pylint: disable=no-member
|
||||
qt_to_logging[QtCore.QtInfoMsg] = logging.INFO
|
||||
except AttributeError:
|
||||
# Qt < 5.5
|
||||
pass
|
||||
# Change levels of some well-known messages to debug so they don't get
|
||||
# shown to the user.
|
||||
# suppressed_msgs is a list of regexes matching the message texts to hide.
|
||||
suppressed_msgs = (
|
||||
# PNGs in Qt with broken color profile
|
||||
# https://bugreports.qt-project.org/browse/QTBUG-39788
|
||||
# https://bugreports.qt.io/browse/QTBUG-39788
|
||||
"libpng warning: iCCP: Not recognizing known sRGB profile that has "
|
||||
"been edited",
|
||||
# Hopefully harmless warning
|
||||
"OpenType support missing for script ",
|
||||
# Error if a QNetworkReply gets two different errors set. Harmless Qt
|
||||
# bug on some pages.
|
||||
# https://bugreports.qt-project.org/browse/QTBUG-30298
|
||||
# https://bugreports.qt.io/browse/QTBUG-30298
|
||||
"QNetworkReplyImplPrivate::error: Internal problem, this method must "
|
||||
"only be called once.",
|
||||
# Not much information about this, but it seems harmless
|
||||
'QXcbWindow: Unhandled client message: "_GTK_LOAD_ICONTHEMES"',
|
||||
# Sometimes indicates missing text, but most of the time harmless
|
||||
"load glyph failed ",
|
||||
# Harmless, see https://bugreports.qt-project.org/browse/QTBUG-42479
|
||||
# Harmless, see https://bugreports.qt.io/browse/QTBUG-42479
|
||||
"content-type missing in HTTP POST, defaulting to "
|
||||
"application/x-www-form-urlencoded. Use QNetworkRequest::setHeader() "
|
||||
"to fix this problem.",
|
||||
# https://bugreports.qt-project.org/browse/QTBUG-43118
|
||||
# https://bugreports.qt.io/browse/QTBUG-43118
|
||||
"Using blocking call!",
|
||||
# Hopefully harmless
|
||||
'"Method "GetAll" with signature "s" on interface '
|
||||
'"org.freedesktop.DBus.Properties" doesn\'t exist',
|
||||
'WOFF support requires QtWebKit to be built with zlib support.'
|
||||
'WOFF support requires QtWebKit to be built with zlib support.',
|
||||
# Weird Enlightment/GTK X extensions
|
||||
'QXcbWindow: Unhandled client message: "_E_',
|
||||
'QXcbWindow: Unhandled client message: "_ECORE_',
|
||||
'QXcbWindow: Unhandled client message: "_GTK_',
|
||||
# Happens on AppVeyor CI
|
||||
'SetProcessDpiAwareness failed:',
|
||||
)
|
||||
if any(msg.strip().startswith(pattern) for pattern in suppressed_msgs):
|
||||
level = logging.DEBUG
|
||||
@@ -310,8 +321,10 @@ def hide_qt_warning(pattern, logger='qt'):
|
||||
log_filter = QtWarningFilter(pattern)
|
||||
logger_obj = logging.getLogger(logger)
|
||||
logger_obj.addFilter(log_filter)
|
||||
yield
|
||||
logger_obj.removeFilter(log_filter)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
logger_obj.removeFilter(log_filter)
|
||||
|
||||
|
||||
class QtWarningFilter(logging.Filter):
|
||||
@@ -370,7 +383,7 @@ class RAMHandler(logging.Handler):
|
||||
|
||||
"""Logging handler which keeps the messages in a deque in RAM.
|
||||
|
||||
Loosly based on logging.BufferingHandler which is unsuitable because it
|
||||
Loosely based on logging.BufferingHandler which is unsuitable because it
|
||||
uses a simple list rather than a deque.
|
||||
|
||||
Attributes:
|
||||
|
||||
@@ -31,10 +31,9 @@ import io
|
||||
import os
|
||||
import sys
|
||||
import operator
|
||||
import distutils.version # pylint: disable=no-name-in-module,import-error
|
||||
# https://bitbucket.org/logilab/pylint/issue/73/
|
||||
import contextlib
|
||||
|
||||
import pkg_resources
|
||||
from PyQt5.QtCore import (qVersion, QEventLoop, QDataStream, QByteArray,
|
||||
QIODevice, QSaveFile)
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
@@ -60,8 +59,8 @@ def version_check(version, op=operator.ge):
|
||||
"""
|
||||
# pylint: disable=no-member
|
||||
# https://bitbucket.org/logilab/pylint/issue/73/
|
||||
return op(distutils.version.StrictVersion(qVersion()),
|
||||
distutils.version.StrictVersion(version))
|
||||
return op(pkg_resources.parse_version(qVersion()),
|
||||
pkg_resources.parse_version(version))
|
||||
|
||||
|
||||
def check_overflow(arg, ctype, fatal=True):
|
||||
@@ -131,7 +130,7 @@ def ensure_valid(obj):
|
||||
def ensure_not_null(obj):
|
||||
"""Ensure a Qt object with an .isNull() method is not null."""
|
||||
if obj.isNull():
|
||||
raise QtValueError(obj)
|
||||
raise QtValueError(obj, null=True)
|
||||
|
||||
|
||||
def check_qdatastream(stream):
|
||||
@@ -180,7 +179,7 @@ def deserialize_stream(stream, obj):
|
||||
def savefile_open(filename, binary=False, encoding='utf-8'):
|
||||
"""Context manager to easily use a QSaveFile."""
|
||||
f = QSaveFile(filename)
|
||||
new_f = None
|
||||
cancelled = False
|
||||
try:
|
||||
ok = f.open(QIODevice.WriteOnly)
|
||||
if not ok:
|
||||
@@ -192,13 +191,14 @@ def savefile_open(filename, binary=False, encoding='utf-8'):
|
||||
yield new_f
|
||||
except:
|
||||
f.cancelWriting()
|
||||
cancelled = True
|
||||
raise
|
||||
else:
|
||||
new_f.flush()
|
||||
finally:
|
||||
if new_f is not None:
|
||||
new_f.flush()
|
||||
commit_ok = f.commit()
|
||||
if not commit_ok:
|
||||
raise OSError(f.errorString())
|
||||
if not commit_ok and not cancelled:
|
||||
raise OSError("Commit failed!")
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
@@ -221,27 +221,58 @@ class PyQIODevice(io.BufferedIOBase):
|
||||
"""Wrapper for a QIODevice which provides a python interface.
|
||||
|
||||
Attributes:
|
||||
_dev: The underlying QIODevice.
|
||||
dev: The underlying QIODevice.
|
||||
"""
|
||||
|
||||
# pylint: disable=missing-docstring
|
||||
|
||||
def __init__(self, dev):
|
||||
self._dev = dev
|
||||
self.dev = dev
|
||||
|
||||
def __len__(self):
|
||||
return self._dev.size()
|
||||
return self.dev.size()
|
||||
|
||||
def _check_open(self):
|
||||
"""Check if the device is open, raise OSError if not."""
|
||||
if not self._dev.isOpen():
|
||||
raise OSError("IO operation on closed device!")
|
||||
"""Check if the device is open, raise ValueError if not."""
|
||||
if not self.dev.isOpen():
|
||||
raise ValueError("IO operation on closed device!")
|
||||
|
||||
def _check_random(self):
|
||||
"""Check if the device supports random access, raise OSError if not."""
|
||||
if not self.seekable():
|
||||
raise OSError("Random access not allowed!")
|
||||
|
||||
def _check_readable(self):
|
||||
"""Check if the device is readable, raise OSError if not."""
|
||||
if not self.dev.isReadable():
|
||||
raise OSError("Trying to read unreadable file!")
|
||||
|
||||
def _check_writable(self):
|
||||
"""Check if the device is writable, raise OSError if not."""
|
||||
if not self.writable():
|
||||
raise OSError("Trying to write to unwritable file!")
|
||||
|
||||
def open(self, mode):
|
||||
"""Open the underlying device and ensure opening succeeded.
|
||||
|
||||
Raises OSError if opening failed.
|
||||
|
||||
Args:
|
||||
mode: QIODevice::OpenMode flags.
|
||||
|
||||
Return:
|
||||
A contextlib.closing() object so this can be used as
|
||||
contextmanager.
|
||||
"""
|
||||
ok = self.dev.open(mode)
|
||||
if not ok:
|
||||
raise OSError(self.dev.errorString())
|
||||
return contextlib.closing(self)
|
||||
|
||||
def close(self):
|
||||
"""Close the underlying device."""
|
||||
self.dev.close()
|
||||
|
||||
def fileno(self):
|
||||
raise io.UnsupportedOperation
|
||||
|
||||
@@ -249,85 +280,102 @@ class PyQIODevice(io.BufferedIOBase):
|
||||
self._check_open()
|
||||
self._check_random()
|
||||
if whence == io.SEEK_SET:
|
||||
ok = self._dev.seek(offset)
|
||||
ok = self.dev.seek(offset)
|
||||
elif whence == io.SEEK_CUR:
|
||||
ok = self._dev.seek(self.tell() + offset)
|
||||
ok = self.dev.seek(self.tell() + offset)
|
||||
elif whence == io.SEEK_END:
|
||||
ok = self._dev.seek(len(self) + offset)
|
||||
ok = self.dev.seek(len(self) + offset)
|
||||
else:
|
||||
raise io.UnsupportedOperation("whence = {} is not "
|
||||
"supported!".format(whence))
|
||||
if not ok:
|
||||
raise OSError(self._dev.errorString())
|
||||
raise OSError("seek failed!")
|
||||
|
||||
def truncate(self, size=None): # pylint: disable=unused-argument
|
||||
raise io.UnsupportedOperation
|
||||
|
||||
def close(self):
|
||||
self._dev.close()
|
||||
|
||||
@property
|
||||
def closed(self):
|
||||
return not self._dev.isOpen()
|
||||
return not self.dev.isOpen()
|
||||
|
||||
def flush(self):
|
||||
self._check_open()
|
||||
self._dev.waitForBytesWritten(-1)
|
||||
self.dev.waitForBytesWritten(-1)
|
||||
|
||||
def isatty(self):
|
||||
self._check_open()
|
||||
return False
|
||||
|
||||
def readable(self):
|
||||
return self._dev.isReadable()
|
||||
return self.dev.isReadable()
|
||||
|
||||
def readline(self, size=-1):
|
||||
self._check_open()
|
||||
if size == -1:
|
||||
size = 0
|
||||
return self._dev.readLine(size)
|
||||
self._check_readable()
|
||||
|
||||
if size < 0:
|
||||
qt_size = 0 # no maximum size
|
||||
elif size == 0:
|
||||
return QByteArray()
|
||||
else:
|
||||
qt_size = size + 1 # Qt also counts the NUL byte
|
||||
|
||||
if self.dev.canReadLine():
|
||||
buf = self.dev.readLine(qt_size)
|
||||
else:
|
||||
if size < 0:
|
||||
buf = self.dev.readAll()
|
||||
else:
|
||||
buf = self.dev.read(size)
|
||||
|
||||
if buf is None:
|
||||
raise OSError(self.dev.errorString())
|
||||
return buf
|
||||
|
||||
def seekable(self):
|
||||
return not self._dev.isSequential()
|
||||
return not self.dev.isSequential()
|
||||
|
||||
def tell(self):
|
||||
self._check_open()
|
||||
self._check_random()
|
||||
return self._dev.pos()
|
||||
return self.dev.pos()
|
||||
|
||||
def writable(self):
|
||||
return self._dev.isWritable()
|
||||
|
||||
def readinto(self, b):
|
||||
self._check_open()
|
||||
return self._dev.read(b, len(b))
|
||||
return self.dev.isWritable()
|
||||
|
||||
def write(self, b):
|
||||
self._check_open()
|
||||
num = self._dev.write(b)
|
||||
self._check_writable()
|
||||
num = self.dev.write(b)
|
||||
if num == -1 or num < len(b):
|
||||
raise OSError(self._dev.errorString())
|
||||
raise OSError(self.dev.errorString())
|
||||
return num
|
||||
|
||||
def read(self, size):
|
||||
def read(self, size=-1):
|
||||
self._check_open()
|
||||
buf = bytes()
|
||||
num = self._dev.read(buf, size)
|
||||
if num == -1:
|
||||
raise OSError(self._dev.errorString())
|
||||
return num
|
||||
self._check_readable()
|
||||
if size < 0:
|
||||
buf = self.dev.readAll()
|
||||
else:
|
||||
buf = self.dev.read(size)
|
||||
if buf is None:
|
||||
raise OSError(self.dev.errorString())
|
||||
return buf
|
||||
|
||||
|
||||
class QtValueError(ValueError):
|
||||
|
||||
"""Exception which gets raised by ensure_valid."""
|
||||
|
||||
def __init__(self, obj):
|
||||
def __init__(self, obj, null=False):
|
||||
try:
|
||||
self.reason = obj.errorString()
|
||||
except AttributeError:
|
||||
self.reason = None
|
||||
err = "{} is not valid".format(obj)
|
||||
if null:
|
||||
err = "{} is null".format(obj)
|
||||
else:
|
||||
err = "{} is not valid".format(obj)
|
||||
if self.reason:
|
||||
err += ": {}".format(self.reason)
|
||||
super().__init__(err)
|
||||
|
||||
@@ -80,10 +80,26 @@ def _from_args(typ, args):
|
||||
path: The overridden path, or None to turn off storage.
|
||||
"""
|
||||
typ_to_argparse_arg = {
|
||||
QStandardPaths.ConfigLocation: 'confdir'
|
||||
QStandardPaths.ConfigLocation: 'confdir',
|
||||
QStandardPaths.DataLocation: 'datadir',
|
||||
QStandardPaths.CacheLocation: 'cachedir',
|
||||
}
|
||||
basedir_suffix = {
|
||||
QStandardPaths.ConfigLocation: 'config',
|
||||
QStandardPaths.DataLocation: 'data',
|
||||
QStandardPaths.CacheLocation: 'cache',
|
||||
QStandardPaths.DownloadLocation: 'download',
|
||||
QStandardPaths.RuntimeLocation: 'runtime',
|
||||
}
|
||||
|
||||
if args is None:
|
||||
return (False, None)
|
||||
|
||||
if getattr(args, 'basedir', None) is not None:
|
||||
basedir = args.basedir
|
||||
suffix = basedir_suffix[typ]
|
||||
return (True, os.path.join(basedir, suffix))
|
||||
|
||||
try:
|
||||
argname = typ_to_argparse_arg[typ]
|
||||
except KeyError:
|
||||
@@ -102,7 +118,7 @@ def _get(typ):
|
||||
|
||||
Args:
|
||||
typ: A member of the QStandardPaths::StandardLocation enum,
|
||||
see http://qt-project.org/doc/qt-5/qstandardpaths.html#StandardLocation-enum
|
||||
see http://doc.qt.io/qt-5/qstandardpaths.html#StandardLocation-enum
|
||||
"""
|
||||
overridden, path = _from_args(typ, _args)
|
||||
if not overridden:
|
||||
@@ -111,7 +127,7 @@ def _get(typ):
|
||||
if (typ == QStandardPaths.ConfigLocation and
|
||||
path.split(os.sep)[-1] != appname):
|
||||
# WORKAROUND - see
|
||||
# https://bugreports.qt-project.org/browse/QTBUG-38872
|
||||
# https://bugreports.qt.io/browse/QTBUG-38872
|
||||
path = os.path.join(path, appname)
|
||||
if typ == QStandardPaths.DataLocation and os.name == 'nt':
|
||||
# Under windows, config/data might end up in the same directory.
|
||||
@@ -135,8 +151,18 @@ def init(args):
|
||||
"""Initialize all standard dirs."""
|
||||
global _args
|
||||
_args = args
|
||||
# http://www.brynosaurus.com/cachedir/spec.html
|
||||
cachedir_tag = os.path.join(cache(), 'CACHEDIR.TAG')
|
||||
_init_cachedir_tag()
|
||||
|
||||
|
||||
def _init_cachedir_tag():
|
||||
"""Create CACHEDIR.TAG if it doesn't exist.
|
||||
|
||||
See http://www.brynosaurus.com/cachedir/spec.html
|
||||
"""
|
||||
cache_dir = cache()
|
||||
if cache_dir is None:
|
||||
return
|
||||
cachedir_tag = os.path.join(cache_dir, 'CACHEDIR.TAG')
|
||||
if not os.path.exists(cachedir_tag):
|
||||
try:
|
||||
with open(cachedir_tag, 'w', encoding='utf-8') as f:
|
||||
@@ -144,6 +170,7 @@ def init(args):
|
||||
f.write("# This file is a cache directory tag created by "
|
||||
"qutebrowser.\n")
|
||||
f.write("# For information about cache directory tags, see:\n")
|
||||
f.write("# http://www.brynosaurus.com/cachedir/\n")
|
||||
f.write("# http://www.brynosaurus.com/" # pragma: no branch
|
||||
"cachedir/\n")
|
||||
except OSError:
|
||||
log.init.exception("Failed to create CACHEDIR.TAG")
|
||||
|
||||
@@ -29,7 +29,7 @@ from PyQt5.QtCore import QUrl
|
||||
from PyQt5.QtNetwork import QHostInfo, QHostAddress
|
||||
|
||||
from qutebrowser.config import config, configexc
|
||||
from qutebrowser.utils import log, qtutils, message
|
||||
from qutebrowser.utils import log, qtutils, message, utils
|
||||
from qutebrowser.commands import cmdexc
|
||||
|
||||
|
||||
@@ -74,8 +74,7 @@ def _get_search_url(txt):
|
||||
"""
|
||||
log.url.debug("Finding search engine for '{}'".format(txt))
|
||||
engine, term = _parse_search_term(txt)
|
||||
if not term:
|
||||
raise FuzzyUrlError("No search term given")
|
||||
assert term
|
||||
if engine is None:
|
||||
template = config.get('searchengines', 'DEFAULT')
|
||||
else:
|
||||
@@ -95,11 +94,9 @@ def _is_url_naive(urlstr):
|
||||
True if the URL really is a URL, False otherwise.
|
||||
"""
|
||||
url = qurl_from_user_input(urlstr)
|
||||
try:
|
||||
ipaddress.ip_address(urlstr)
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
assert url.isValid()
|
||||
|
||||
if not utils.raises(ValueError, ipaddress.ip_address, urlstr):
|
||||
# Valid IPv4/IPv6 address
|
||||
return True
|
||||
|
||||
@@ -109,31 +106,36 @@ def _is_url_naive(urlstr):
|
||||
if not QHostAddress(urlstr).isNull():
|
||||
return False
|
||||
|
||||
if not url.isValid():
|
||||
return False
|
||||
elif '.' in url.host():
|
||||
return True
|
||||
elif url.host() == 'localhost':
|
||||
if '.' in url.host():
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def _is_url_dns(url):
|
||||
def _is_url_dns(urlstr):
|
||||
"""Check if a URL is really a URL via DNS.
|
||||
|
||||
Args:
|
||||
url: The URL to check for as QUrl, ideally via qurl_from_user_input.
|
||||
url: The URL to check for as a string.
|
||||
|
||||
Return:
|
||||
True if the URL really is a URL, False otherwise.
|
||||
"""
|
||||
if not url.isValid():
|
||||
url = qurl_from_user_input(urlstr)
|
||||
assert url.isValid()
|
||||
|
||||
if (utils.raises(ValueError, ipaddress.ip_address, urlstr) and
|
||||
not QHostAddress(urlstr).isNull()):
|
||||
log.url.debug("Bogus IP URL -> False")
|
||||
# Qt treats things like "23.42" or "1337" or "0xDEAD" as valid URLs
|
||||
# which we don't want to.
|
||||
return False
|
||||
|
||||
host = url.host()
|
||||
log.url.debug("DNS request for {}".format(host))
|
||||
if not host:
|
||||
log.url.debug("URL has no host -> False")
|
||||
return False
|
||||
log.url.debug("Doing DNS request for {}".format(host))
|
||||
info = QHostInfo.fromName(host)
|
||||
return not info.error()
|
||||
|
||||
@@ -230,6 +232,7 @@ def is_url(urlstr):
|
||||
|
||||
urlstr = urlstr.strip()
|
||||
qurl = QUrl(urlstr)
|
||||
qurl_userinput = qurl_from_user_input(urlstr)
|
||||
|
||||
if not autosearch:
|
||||
# no autosearch, so everything is a URL unless it has an explicit
|
||||
@@ -240,29 +243,33 @@ def is_url(urlstr):
|
||||
else:
|
||||
return False
|
||||
|
||||
if not qurl_userinput.isValid():
|
||||
# This will also catch URLs containing spaces.
|
||||
return False
|
||||
|
||||
if _has_explicit_scheme(qurl):
|
||||
# URLs with explicit schemes are always URLs
|
||||
log.url.debug("Contains explicit scheme")
|
||||
url = True
|
||||
elif ' ' in urlstr:
|
||||
# A URL will never contain a space
|
||||
log.url.debug("Contains space -> no URL")
|
||||
url = False
|
||||
elif qurl_userinput.host() in ('localhost', '127.0.0.1', '::1'):
|
||||
log.url.debug("Is localhost.")
|
||||
url = True
|
||||
elif is_special_url(qurl):
|
||||
# Special URLs are always URLs, even with autosearch=False
|
||||
log.url.debug("Is an special URL.")
|
||||
url = True
|
||||
elif autosearch == 'dns':
|
||||
log.url.debug("Checking via DNS")
|
||||
log.url.debug("Checking via DNS check")
|
||||
# We want to use qurl_from_user_input here, as the user might enter
|
||||
# "foo.de" and that should be treated as URL here.
|
||||
url = _is_url_dns(qurl_from_user_input(urlstr))
|
||||
url = _is_url_dns(urlstr)
|
||||
elif autosearch == 'naive':
|
||||
log.url.debug("Checking via naive check")
|
||||
url = _is_url_naive(urlstr)
|
||||
else:
|
||||
raise ValueError("Invalid autosearch value")
|
||||
return url and qurl_from_user_input(urlstr).isValid()
|
||||
log.url.debug("url = {}".format(url))
|
||||
return url
|
||||
|
||||
|
||||
def qurl_from_user_input(urlstr):
|
||||
@@ -272,7 +279,7 @@ def qurl_from_user_input(urlstr):
|
||||
IPv6, so we first try to handle it as a valid IPv6, and if that fails we
|
||||
use QUrl.fromUserInput.
|
||||
|
||||
WORKAROUND - https://bugreports.qt-project.org/browse/QTBUG-41089
|
||||
WORKAROUND - https://bugreports.qt.io/browse/QTBUG-41089
|
||||
FIXME - Maybe https://codereview.qt-project.org/#/c/93851/ has a better way
|
||||
to solve this?
|
||||
https://github.com/The-Compiler/qutebrowser/issues/109
|
||||
@@ -311,20 +318,15 @@ def invalid_url_error(win_id, url, action):
|
||||
if url.isValid():
|
||||
raise ValueError("Calling invalid_url_error with valid URL {}".format(
|
||||
url.toDisplayString()))
|
||||
errstring = "Trying to {} with invalid URL".format(action)
|
||||
if url.errorString():
|
||||
errstring += " - {}".format(url.errorString())
|
||||
errstring = get_errstring(
|
||||
url, "Trying to {} with invalid URL".format(action))
|
||||
message.error(win_id, errstring)
|
||||
|
||||
|
||||
def raise_cmdexc_if_invalid(url):
|
||||
"""Check if the given QUrl is invalid, and if so, raise a CommandError."""
|
||||
if not url.isValid():
|
||||
errstr = "Invalid URL {}".format(url.toDisplayString())
|
||||
url_error = url.errorString()
|
||||
if url_error:
|
||||
errstr += " - {}".format(url_error)
|
||||
raise cmdexc.CommandError(errstr)
|
||||
raise cmdexc.CommandError(get_errstring(url))
|
||||
|
||||
|
||||
def filename_from_url(url):
|
||||
@@ -348,11 +350,46 @@ def filename_from_url(url):
|
||||
|
||||
|
||||
def host_tuple(url):
|
||||
"""Get a (scheme, host, port) tuple.
|
||||
"""Get a (scheme, host, port) tuple from a QUrl.
|
||||
|
||||
This is suitable to identify a connection, e.g. for SSL errors.
|
||||
"""
|
||||
return (url.scheme(), url.host(), url.port())
|
||||
if not url.isValid():
|
||||
raise ValueError(get_errstring(url))
|
||||
scheme, host, port = url.scheme(), url.host(), url.port()
|
||||
assert scheme
|
||||
if not host:
|
||||
raise ValueError("Got URL {} without host.".format(
|
||||
url.toDisplayString()))
|
||||
if port == -1:
|
||||
port_mapping = {
|
||||
'http': 80,
|
||||
'https': 443,
|
||||
'ftp': 21,
|
||||
}
|
||||
try:
|
||||
port = port_mapping[scheme]
|
||||
except KeyError:
|
||||
raise ValueError("Got URL {} with unknown port.".format(
|
||||
url.toDisplayString()))
|
||||
return scheme, host, port
|
||||
|
||||
|
||||
def get_errstring(url, base="Invalid URL"):
|
||||
"""Get an error string for an URL.
|
||||
|
||||
Args:
|
||||
url: The URL as a QUrl.
|
||||
base: The base error string.
|
||||
|
||||
Return:
|
||||
A new string with url.errorString() is appended if available.
|
||||
"""
|
||||
url_error = url.errorString()
|
||||
if url_error:
|
||||
return base + " - {}".format(url_error)
|
||||
else:
|
||||
return base
|
||||
|
||||
|
||||
class FuzzyUrlError(Exception):
|
||||
@@ -360,17 +397,19 @@ class FuzzyUrlError(Exception):
|
||||
"""Exception raised by fuzzy_url on problems.
|
||||
|
||||
Attributes:
|
||||
msg: The error message to use.
|
||||
url: The QUrl which caused the error.
|
||||
"""
|
||||
|
||||
def __init__(self, msg, url=None):
|
||||
super().__init__(msg)
|
||||
if url is not None:
|
||||
assert not url.isValid()
|
||||
if url is not None and url.isValid():
|
||||
raise ValueError("Got valid URL {}!".format(url.toDisplayString()))
|
||||
self.url = url
|
||||
self.msg = msg
|
||||
|
||||
def __str__(self):
|
||||
if self.url is None or not self.url.errorString():
|
||||
return str(super())
|
||||
return self.msg
|
||||
else:
|
||||
return '{}: {}'.format(str(super()), self.url.errorString())
|
||||
return '{}: {}'.format(self.msg, self.url.errorString())
|
||||
|
||||
@@ -73,7 +73,7 @@ class NeighborList(collections.abc.Sequence):
|
||||
Args:
|
||||
items: The list of items to iterate in.
|
||||
_default: The initially selected value.
|
||||
_mode: Behaviour when the first/last item is reached.
|
||||
_mode: Behavior when the first/last item is reached.
|
||||
Modes.block: Stay on the selected item
|
||||
Modes.wrap: Wrap around to the other end
|
||||
Modes.exception: Raise an IndexError.
|
||||
@@ -231,7 +231,7 @@ ClickTarget = enum('ClickTarget', ['normal', 'tab', 'tab_bg', 'window'])
|
||||
|
||||
# Key input modes
|
||||
KeyMode = enum('KeyMode', ['normal', 'hint', 'command', 'yesno', 'prompt',
|
||||
'insert', 'passthrough'])
|
||||
'insert', 'passthrough', 'caret'])
|
||||
|
||||
|
||||
# Available command completions
|
||||
@@ -240,6 +240,11 @@ Completion = enum('Completion', ['command', 'section', 'option', 'value',
|
||||
'quickmark_by_name', 'url', 'sessions'])
|
||||
|
||||
|
||||
# Exit statuses for errors. Needs to be an int for sys.exit.
|
||||
Exit = enum('Exit', ['ok', 'reserved', 'exception', 'err_ipc', 'err_init',
|
||||
'err_config', 'err_key_config'], is_int=True, start=0)
|
||||
|
||||
|
||||
class Question(QObject):
|
||||
|
||||
"""A question asked to the user, e.g. via the status bar.
|
||||
|
||||
@@ -50,8 +50,6 @@ def elide(text, length):
|
||||
def compact_text(text, elidelength=None):
|
||||
"""Remove leading whitespace and newlines from a text and maybe elide it.
|
||||
|
||||
FIXME: Add tests.
|
||||
|
||||
Args:
|
||||
text: The text to compact.
|
||||
elidelength: To how many chars to elide.
|
||||
@@ -105,12 +103,12 @@ def actute_warning():
|
||||
try:
|
||||
if qtutils.version_check('5.3.0'):
|
||||
return
|
||||
except ValueError:
|
||||
except ValueError: # pragma: no cover
|
||||
pass
|
||||
try:
|
||||
with open('/usr/share/X11/locale/en_US.UTF-8/Compose', 'r',
|
||||
encoding='utf-8') as f:
|
||||
for line in f:
|
||||
for line in f: # pragma: no branch
|
||||
if '<dead_actute>' in line:
|
||||
if sys.stdout is not None:
|
||||
sys.stdout.flush()
|
||||
@@ -118,7 +116,7 @@ def actute_warning():
|
||||
"that is not a bug in qutebrowser! See "
|
||||
"https://bugs.freedesktop.org/show_bug.cgi?id=69476 "
|
||||
"for details.")
|
||||
break
|
||||
break # pragma: no branch
|
||||
except OSError:
|
||||
log.init.exception("Failed to read Compose file")
|
||||
|
||||
@@ -242,7 +240,7 @@ def key_to_string(key):
|
||||
"""
|
||||
special_names_str = {
|
||||
# Some keys handled in a weird way by QKeySequence::toString.
|
||||
# See https://bugreports.qt-project.org/browse/QTBUG-40030
|
||||
# See https://bugreports.qt.io/browse/QTBUG-40030
|
||||
# Most are unlikely to be ever needed, but you never know ;)
|
||||
# For dead/combining keys, we return the corresponding non-combining
|
||||
# key, as that's easier to add to the config.
|
||||
@@ -290,6 +288,18 @@ def key_to_string(key):
|
||||
'Key_TouchpadOn': 'Touchpad On',
|
||||
'Key_TouchpadToggle': 'Touchpad toggle',
|
||||
'Key_Yellow': 'Yellow',
|
||||
'Key_Alt': 'Alt',
|
||||
'Key_AltGr': 'AltGr',
|
||||
'Key_Control': 'Control',
|
||||
'Key_Direction_L': 'Direction L',
|
||||
'Key_Direction_R': 'Direction R',
|
||||
'Key_Hyper_L': 'Hyper L',
|
||||
'Key_Hyper_R': 'Hyper R',
|
||||
'Key_Meta': 'Meta',
|
||||
'Key_Shift': 'Shift',
|
||||
'Key_Super_L': 'Super L',
|
||||
'Key_Super_R': 'Super R',
|
||||
'Key_unknown': 'Unknown',
|
||||
}
|
||||
# We now build our real special_names dict from the string mapping above.
|
||||
# The reason we don't do this directly is that certain Qt versions don't
|
||||
@@ -326,17 +336,24 @@ def keyevent_to_string(e):
|
||||
A name of the key (combination) as a string or
|
||||
None if only modifiers are pressed..
|
||||
"""
|
||||
modmask2str = collections.OrderedDict([
|
||||
(Qt.ControlModifier, 'Ctrl'),
|
||||
(Qt.AltModifier, 'Alt'),
|
||||
(Qt.MetaModifier, 'Meta'),
|
||||
(Qt.ShiftModifier, 'Shift'),
|
||||
])
|
||||
if sys.platform == 'darwin':
|
||||
# FIXME verify this feels right on a real Mac as well.
|
||||
# In my Virtualbox VM, the Ctrl key shows up as meta.
|
||||
# Qt swaps Ctrl/Meta on OS X, so we switch it back here so the user can
|
||||
# use it in the config as expected. See:
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/110
|
||||
modmask2str[Qt.MetaModifier] = 'Ctrl'
|
||||
# http://doc.qt.io/qt-5.4/osx-issues.html#special-keys
|
||||
modmask2str = collections.OrderedDict([
|
||||
(Qt.MetaModifier, 'Ctrl'),
|
||||
(Qt.AltModifier, 'Alt'),
|
||||
(Qt.ControlModifier, 'Meta'),
|
||||
(Qt.ShiftModifier, 'Shift'),
|
||||
])
|
||||
else:
|
||||
modmask2str = collections.OrderedDict([
|
||||
(Qt.ControlModifier, 'Ctrl'),
|
||||
(Qt.AltModifier, 'Alt'),
|
||||
(Qt.MetaModifier, 'Meta'),
|
||||
(Qt.ShiftModifier, 'Shift'),
|
||||
])
|
||||
modifiers = (Qt.Key_Control, Qt.Key_Alt, Qt.Key_Shift, Qt.Key_Meta,
|
||||
Qt.Key_AltGr, Qt.Key_Super_L, Qt.Key_Super_R,
|
||||
Qt.Key_Hyper_L, Qt.Key_Hyper_R, Qt.Key_Direction_L,
|
||||
@@ -421,11 +438,13 @@ def disabled_excepthook():
|
||||
"""Run code with the exception hook temporarily disabled."""
|
||||
old_excepthook = sys.excepthook
|
||||
sys.excepthook = sys.__excepthook__
|
||||
yield
|
||||
# If the code we did run did change sys.excepthook, we leave it
|
||||
# unchanged. Otherwise, we reset it.
|
||||
if sys.excepthook is sys.__excepthook__:
|
||||
sys.excepthook = old_excepthook
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
# If the code we did run did change sys.excepthook, we leave it
|
||||
# unchanged. Otherwise, we reset it.
|
||||
if sys.excepthook is sys.__excepthook__:
|
||||
sys.excepthook = old_excepthook
|
||||
|
||||
|
||||
class prevent_exceptions: # pylint: disable=invalid-name
|
||||
@@ -503,7 +522,8 @@ def get_repr(obj, constructor=False, **attrs):
|
||||
"""
|
||||
cls = qualname(obj.__class__)
|
||||
parts = []
|
||||
for name, val in attrs.items():
|
||||
items = sorted(attrs.items())
|
||||
for name, val in items:
|
||||
parts.append('{}={!r}'.format(name, val))
|
||||
if constructor:
|
||||
return '{}({})'.format(cls, ', '.join(parts))
|
||||
@@ -533,7 +553,7 @@ def qualname(obj):
|
||||
elif hasattr(obj, '__name__'):
|
||||
name = obj.__name__
|
||||
else:
|
||||
name = '<unknown>'
|
||||
name = repr(obj)
|
||||
|
||||
if inspect.isclass(obj) or inspect.isfunction(obj):
|
||||
module = obj.__module__
|
||||
|
||||
@@ -29,10 +29,8 @@ import collections
|
||||
|
||||
from PyQt5.QtCore import QT_VERSION_STR, PYQT_VERSION_STR, qVersion
|
||||
from PyQt5.QtWebKit import qWebKitVersion
|
||||
try:
|
||||
from PyQt5.QtNetwork import QSslSocket
|
||||
except ImportError:
|
||||
QSslSocket = None
|
||||
from PyQt5.QtNetwork import QSslSocket
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
|
||||
import qutebrowser
|
||||
from qutebrowser.utils import log, utils
|
||||
@@ -114,7 +112,7 @@ def _release_info():
|
||||
for fn in glob.glob("/etc/*-release"):
|
||||
try:
|
||||
with open(fn, 'r', encoding='utf-8') as f:
|
||||
data.append((fn, ''.join(f.readlines())))
|
||||
data.append((fn, ''.join(f.readlines()))) # pragma: no branch
|
||||
except OSError:
|
||||
log.misc.exception("Error while reading {}.".format(fn))
|
||||
return data
|
||||
@@ -127,18 +125,8 @@ def _module_versions():
|
||||
A list of lines with version info.
|
||||
"""
|
||||
lines = []
|
||||
try:
|
||||
import sipconfig # pylint: disable=import-error,unused-variable
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
lines.append('SIP: {}'.format(
|
||||
sipconfig.Configuration().sip_version_str))
|
||||
except (AttributeError, TypeError):
|
||||
log.misc.exception("Error while getting SIP version")
|
||||
lines.append('SIP: ?')
|
||||
modules = collections.OrderedDict([
|
||||
('sip', ['SIP_VERSION_STR']),
|
||||
('colorlog', []),
|
||||
('colorama', ['VERSION', '__version__']),
|
||||
('pypeg2', ['__version__']),
|
||||
@@ -196,8 +184,12 @@ def _os_info():
|
||||
return lines
|
||||
|
||||
|
||||
def version():
|
||||
"""Return a string with various version informations."""
|
||||
def version(short=False):
|
||||
"""Return a string with various version informations.
|
||||
|
||||
Args:
|
||||
short: Return a shortened output.
|
||||
"""
|
||||
lines = ["qutebrowser v{}".format(qutebrowser.__version__)]
|
||||
gitver = _git_str()
|
||||
if gitver is not None:
|
||||
@@ -209,20 +201,24 @@ def version():
|
||||
'Qt: {}, runtime: {}'.format(QT_VERSION_STR, qVersion()),
|
||||
'PyQt: {}'.format(PYQT_VERSION_STR),
|
||||
]
|
||||
lines += _module_versions()
|
||||
|
||||
if QSslSocket is not None and QSslSocket.supportsSsl():
|
||||
ssl_version = QSslSocket.sslLibraryVersionString()
|
||||
else:
|
||||
ssl_version = 'unavailable'
|
||||
lines += [
|
||||
'Webkit: {}'.format(qWebKitVersion()),
|
||||
'Harfbuzz: {}'.format(os.environ.get('QT_HARFBUZZ', 'system')),
|
||||
'SSL: {}'.format(ssl_version),
|
||||
'',
|
||||
'Frozen: {}'.format(hasattr(sys, 'frozen')),
|
||||
'Platform: {}, {}'.format(platform.platform(),
|
||||
platform.architecture()[0]),
|
||||
]
|
||||
lines += _os_info()
|
||||
if not short:
|
||||
style = QApplication.instance().style()
|
||||
lines += [
|
||||
'Style: {}'.format(style.metaObject().className()),
|
||||
'Desktop: {}'.format(os.environ.get('DESKTOP_SESSION')),
|
||||
]
|
||||
|
||||
lines += _module_versions()
|
||||
|
||||
lines += [
|
||||
'Webkit: {}'.format(qWebKitVersion()),
|
||||
'Harfbuzz: {}'.format(os.environ.get('QT_HARFBUZZ', 'system')),
|
||||
'SSL: {}'.format(QSslSocket.sslLibraryVersionString()),
|
||||
'',
|
||||
'Frozen: {}'.format(hasattr(sys, 'frozen')),
|
||||
'Platform: {}, {}'.format(platform.platform(),
|
||||
platform.architecture()[0]),
|
||||
]
|
||||
lines += _os_info()
|
||||
return '\n'.join(lines)
|
||||
|
||||
@@ -46,6 +46,20 @@ def call_script(name, *args, python=sys.executable):
|
||||
subprocess.check_call([python, path] + list(args))
|
||||
|
||||
|
||||
def call_freeze(*args, python=sys.executable):
|
||||
"""Call freeze.py via tox.
|
||||
|
||||
Args:
|
||||
*args: The arguments to pass.
|
||||
python: The python interpreter to use.
|
||||
"""
|
||||
env = os.environ.copy()
|
||||
env['PYTHON'] = python
|
||||
subprocess.check_call(
|
||||
[sys.executable, '-m', 'tox', '-e', 'cxfreeze-windows'] + list(args),
|
||||
env=env)
|
||||
|
||||
|
||||
def build_common(args):
|
||||
"""Common buildsteps used for all OS'."""
|
||||
utils.print_title("Running asciidoc2html.py")
|
||||
@@ -64,22 +78,33 @@ def _maybe_remove(path):
|
||||
pass
|
||||
|
||||
|
||||
def smoke_test(executable):
|
||||
"""Try starting the given qutebrowser executable."""
|
||||
subprocess.check_call([executable, '--no-err-windows', '--nowindow',
|
||||
'--temp-basedir', 'about:blank', ':later 500 quit'])
|
||||
|
||||
|
||||
def build_windows():
|
||||
"""Build windows executables/setups."""
|
||||
parts = str(sys.version_info.major), str(sys.version_info.minor)
|
||||
ver = ''.join(parts)
|
||||
dotver = '.'.join(parts)
|
||||
python_x86 = r'C:\Python{}_x32\python.exe'.format(ver)
|
||||
python_x64 = r'C:\Python{}\python.exe'.format(ver)
|
||||
python_x86 = r'C:\Python{}_x32'.format(ver)
|
||||
python_x64 = r'C:\Python{}'.format(ver)
|
||||
|
||||
utils.print_title("Running 32bit freeze.py build_exe")
|
||||
call_script('freeze.py', 'build_exe', python=python_x86)
|
||||
utils.print_title("Running 64bit freeze.py build_exe")
|
||||
call_script('freeze.py', 'build_exe', python=python_x64)
|
||||
call_freeze('build_exe', python=python_x86)
|
||||
utils.print_title("Running 32bit freeze.py bdist_msi")
|
||||
call_script('freeze.py', 'bdist_msi', python=python_x86)
|
||||
call_freeze('bdist_msi', python=python_x86)
|
||||
utils.print_title("Running 64bit freeze.py build_exe")
|
||||
call_freeze('build_exe', python=python_x64)
|
||||
utils.print_title("Running 64bit freeze.py bdist_msi")
|
||||
call_script('freeze.py', 'bdist_msi', python=python_x64)
|
||||
call_freeze('bdist_msi', python=python_x64)
|
||||
|
||||
utils.print_title("Running 32bit smoke test")
|
||||
smoke_test('build/exe.win32-{}/qutebrowser.exe'.format(dotver))
|
||||
utils.print_title("Running 64bit smoke test")
|
||||
smoke_test('build/exe.win-amd64-{}/qutebrowser.exe'.format(dotver))
|
||||
|
||||
destdir = os.path.join('dist', 'zip')
|
||||
_maybe_remove(destdir)
|
||||
@@ -126,6 +151,14 @@ def main():
|
||||
args = parser.parse_args()
|
||||
utils.change_cwd()
|
||||
if os.name == 'nt':
|
||||
if sys.maxsize > 2**32:
|
||||
# WORKAROUND
|
||||
print("Due to a python/Windows bug, this script needs to be run ")
|
||||
print("with a 32bit Python.")
|
||||
print()
|
||||
print("See http://bugs.python.org/issue24493 and ")
|
||||
print("https://github.com/pypa/virtualenv/issues/774")
|
||||
sys.exit(1)
|
||||
build_common(args)
|
||||
build_windows()
|
||||
else:
|
||||
|
||||
101
scripts/ci_install.py
Normal file
101
scripts/ci_install.py
Normal file
@@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env python2
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
# qutebrowser is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# qutebrowser is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# pylint: skip-file
|
||||
|
||||
"""Install needed prerequisites on the AppVeyor/Travis CI.
|
||||
|
||||
Note this file is written in python2 as this is more readily available on the
|
||||
CI machines.
|
||||
"""
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import urllib
|
||||
|
||||
PYQT_VERSION = '5.4.2'
|
||||
|
||||
|
||||
def apt_get(args):
|
||||
subprocess.check_call(['sudo', 'apt-get', '-y', '-q'] + args)
|
||||
|
||||
|
||||
def brew(args, silent=False):
|
||||
if silent:
|
||||
with open(os.devnull, 'w') as f:
|
||||
subprocess.check_call(['brew'] + args, stdout=f)
|
||||
else:
|
||||
subprocess.check_call(['brew'] + args)
|
||||
|
||||
|
||||
if 'APPVEYOR' in os.environ:
|
||||
print("Getting PyQt5...")
|
||||
urllib.urlretrieve(
|
||||
('http://sourceforge.net/projects/pyqt/files/PyQt5/PyQt-{v}/'
|
||||
'PyQt5-{v}-gpl-Py3.4-Qt{v}-x32.exe'.format(v=PYQT_VERSION)),
|
||||
r'C:\install-PyQt5.exe')
|
||||
|
||||
print("Installing PyQt5...")
|
||||
subprocess.check_call([r'C:\install-PyQt5.exe', '/S'])
|
||||
|
||||
print("Installing tox...")
|
||||
subprocess.check_call([r'C:\Python34\Scripts\pip', 'install', 'tox'])
|
||||
|
||||
print("Linking Python...")
|
||||
with open(r'C:\Windows\system32\python3.bat', 'w') as f:
|
||||
f.write(r'@C:\Python34\python %*')
|
||||
elif os.environ.get('TRAVIS_OS_NAME', None) == 'linux':
|
||||
print("apt-get update...")
|
||||
apt_get(['update'])
|
||||
|
||||
print("Installing packages...")
|
||||
pkgs = 'python3-pyqt5 python3-pyqt5.qtwebkit python-tox python3-dev xvfb'
|
||||
apt_get(['install'] + pkgs.split())
|
||||
elif os.environ.get('TRAVIS_OS_NAME', None) == 'osx':
|
||||
print("brew update...")
|
||||
brew(['update'], silent=True)
|
||||
|
||||
print("Installing packages...")
|
||||
brew(['install', 'python3', 'pyqt5'])
|
||||
|
||||
print("Installing tox...")
|
||||
subprocess.check_call(['sudo', 'pip3.4', 'install', 'tox'])
|
||||
|
||||
os.system('ls -l /usr/local/bin/xvfb-run')
|
||||
print("Creating xvfb-run stub...")
|
||||
with open('/usr/local/bin/xvfb-run', 'w') as f:
|
||||
# This will break when xvfb-run is called differently in .travis.yml,
|
||||
# but I can't be bothered to do it in a nicer way.
|
||||
f.write('#!/bin/bash\n')
|
||||
f.write('shift 2\n')
|
||||
f.write('exec "$@"\n')
|
||||
os.system('sudo chmod 755 /usr/local/bin/xvfb-run')
|
||||
os.system('ls -l /usr/local/bin/xvfb-run')
|
||||
else:
|
||||
def env(key):
|
||||
return os.environ.get(key, None)
|
||||
print("Unknown environment! (CI {}, APPVEYOR {}, TRAVIS {}, "
|
||||
"TRAVIS_OS_NAME {})".format(env('CI'), env('APPVEYOR'),
|
||||
env('TRAVIS'), env('TRAVIS_OS_NAME')),
|
||||
file=sys.stderr)
|
||||
sys.exit(1)
|
||||
@@ -47,20 +47,41 @@ def get_egl_path():
|
||||
return os.path.join(distutils.sysconfig.get_python_lib(),
|
||||
r'PyQt5\libEGL.dll')
|
||||
|
||||
build_exe_options = {
|
||||
'include_files': [
|
||||
('qutebrowser/html', 'html'),
|
||||
('qutebrowser/html/doc', 'html/doc'),
|
||||
('qutebrowser/git-commit-id', 'git-commit-id'),
|
||||
],
|
||||
'include_msvcr': True,
|
||||
'excludes': ['tkinter'],
|
||||
'packages': ['pygments'],
|
||||
}
|
||||
|
||||
egl_path = get_egl_path()
|
||||
if egl_path is not None:
|
||||
build_exe_options['include_files'].append((egl_path, 'libEGL.dll'))
|
||||
def get_build_exe_options(skip_html=False):
|
||||
"""Get the options passed as build_exe_options to cx_Freeze.
|
||||
|
||||
If either skip_html or --qute-skip-html as argument is given, doesn't
|
||||
freeze the documentation.
|
||||
"""
|
||||
if '--qute-skip-html' in sys.argv:
|
||||
skip_html = True
|
||||
sys.argv.remove('--qute-skip-html')
|
||||
|
||||
include_files = [
|
||||
('qutebrowser/javascript', 'javascript'),
|
||||
('qutebrowser/git-commit-id', 'git-commit-id'),
|
||||
('qutebrowser/utils/testfile', 'utils/testfile'),
|
||||
]
|
||||
|
||||
if not skip_html:
|
||||
include_files += [
|
||||
('qutebrowser/html', 'html'),
|
||||
('qutebrowser/html/doc', 'html/doc'),
|
||||
]
|
||||
|
||||
egl_path = get_egl_path()
|
||||
if egl_path is not None:
|
||||
include_files.append((egl_path, 'libEGL.dll'))
|
||||
|
||||
return {
|
||||
'include_files': include_files,
|
||||
'include_msvcr': True,
|
||||
'includes': [],
|
||||
'excludes': ['tkinter'],
|
||||
'packages': ['pygments'],
|
||||
}
|
||||
|
||||
|
||||
bdist_msi_options = {
|
||||
# random GUID generated by uuid.uuid4()
|
||||
@@ -92,19 +113,21 @@ executable = cx.Executable('qutebrowser/__main__.py', base=base,
|
||||
icon=os.path.join(BASEDIR, 'icons',
|
||||
'qutebrowser.ico'))
|
||||
|
||||
try:
|
||||
setupcommon.write_git_file()
|
||||
cx.setup(
|
||||
executables=[executable],
|
||||
options={
|
||||
'build_exe': build_exe_options,
|
||||
'bdist_msi': bdist_msi_options,
|
||||
'bdist_mac': bdist_mac_options,
|
||||
'bdist_dmg': bdist_dmg_options,
|
||||
},
|
||||
**setupcommon.setupdata
|
||||
)
|
||||
finally:
|
||||
path = os.path.join(BASEDIR, 'qutebrowser', 'git-commit-id')
|
||||
if os.path.exists(path):
|
||||
os.remove(path)
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
setupcommon.write_git_file()
|
||||
cx.setup(
|
||||
executables=[executable],
|
||||
options={
|
||||
'build_exe': get_build_exe_options(),
|
||||
'bdist_msi': bdist_msi_options,
|
||||
'bdist_mac': bdist_mac_options,
|
||||
'bdist_dmg': bdist_dmg_options,
|
||||
},
|
||||
**setupcommon.setupdata
|
||||
)
|
||||
finally:
|
||||
path = os.path.join(BASEDIR, 'qutebrowser', 'git-commit-id')
|
||||
if os.path.exists(path):
|
||||
os.remove(path)
|
||||
|
||||
70
scripts/freeze_tests.py
Executable file
70
scripts/freeze_tests.py
Executable file
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env python3
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
# qutebrowser is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# qutebrowser is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""cx_Freeze script to freeze qutebrowser and its tests."""
|
||||
|
||||
|
||||
import os
|
||||
import os.path
|
||||
import sys
|
||||
import contextlib
|
||||
|
||||
import cx_Freeze as cx # pylint: disable=import-error
|
||||
# cx_Freeze is hard to install (needs C extensions) so we don't check for it.
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
|
||||
from scripts import setupcommon, freeze
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def temp_git_commit_file():
|
||||
"""Context manager to temporarily create a fake git-commit-id file."""
|
||||
basedir = os.path.join(os.path.dirname(os.path.realpath(__file__)),
|
||||
os.path.pardir)
|
||||
path = os.path.join(basedir, 'qutebrowser', 'git-commit-id')
|
||||
with open(path, 'wb') as f:
|
||||
f.write(b'fake-frozen-git-commit')
|
||||
yield
|
||||
os.remove(path)
|
||||
|
||||
|
||||
def get_build_exe_options():
|
||||
"""Get build_exe options with additional includes."""
|
||||
opts = freeze.get_build_exe_options(skip_html=True)
|
||||
opts['includes'] += pytest.freeze_includes() # pylint: disable=no-member
|
||||
opts['includes'] += ['unittest.mock', 'PyQt5.QtTest']
|
||||
opts['packages'].append('qutebrowser')
|
||||
return opts
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
with temp_git_commit_file():
|
||||
cx.setup(
|
||||
executables=[cx.Executable('scripts/run_frozen_tests.py',
|
||||
targetName='run-frozen-tests')],
|
||||
options={'build_exe': get_build_exe_options()},
|
||||
**setupcommon.setupdata
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
56
scripts/keytester.py
Normal file
56
scripts/keytester.py
Normal file
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env python3
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
# qutebrowser is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# qutebrowser is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Small test script to show key presses.
|
||||
|
||||
Use python3 -m scripts.keytester to launch it.
|
||||
"""
|
||||
|
||||
from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QHBoxLayout
|
||||
|
||||
from qutebrowser.utils import utils
|
||||
|
||||
|
||||
class KeyWidget(QWidget):
|
||||
|
||||
"""Widget displaying key presses."""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._layout = QHBoxLayout(self)
|
||||
self._label = QLabel(text="Waiting for keypress...")
|
||||
self._layout.addWidget(self._label)
|
||||
|
||||
def keyPressEvent(self, e):
|
||||
"""Show pressed keys."""
|
||||
lines = [
|
||||
str(utils.keyevent_to_string(e)),
|
||||
'',
|
||||
'key: 0x{:x}'.format(int(e.key())),
|
||||
'modifiers: 0x{:x}'.format(int(e.modifiers())),
|
||||
'text: {!r}'.format(e.text()),
|
||||
]
|
||||
self._label.setText('\n'.join(lines))
|
||||
|
||||
|
||||
app = QApplication([])
|
||||
w = KeyWidget()
|
||||
w.show()
|
||||
app.exec_()
|
||||
@@ -28,6 +28,7 @@ import sys
|
||||
import glob
|
||||
import subprocess
|
||||
import platform
|
||||
import filecmp
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
@@ -39,7 +40,8 @@ class Error(Exception):
|
||||
|
||||
def verbose_copy(src, dst, *, follow_symlinks=True):
|
||||
"""Copy function for shutil.copytree which prints copied files."""
|
||||
print('{} -> {}'.format(src, dst))
|
||||
if '-v' in sys.argv:
|
||||
print('{} -> {}'.format(src, dst))
|
||||
shutil.copy(src, dst, follow_symlinks=follow_symlinks)
|
||||
|
||||
|
||||
@@ -58,6 +60,22 @@ def get_ignored_files(directory, files):
|
||||
return filtered
|
||||
|
||||
|
||||
def needs_update(source, dest):
|
||||
"""Check if a file to be linked/copied needs to be updated."""
|
||||
if os.path.islink(dest):
|
||||
# No need to delete a link and relink -> skip this
|
||||
return False
|
||||
elif os.path.isdir(dest):
|
||||
diffs = filecmp.dircmp(source, dest)
|
||||
ignored = get_ignored_files(source, diffs.left_only)
|
||||
has_new_files = set(ignored) != set(diffs.left_only)
|
||||
return (has_new_files or diffs.right_only or
|
||||
diffs.common_funny or diffs.diff_files or
|
||||
diffs.funny_files)
|
||||
else:
|
||||
return not filecmp.cmp(source, dest)
|
||||
|
||||
|
||||
def link_pyqt(sys_path, venv_path):
|
||||
"""Symlink the systemwide PyQt/sip into the venv.
|
||||
|
||||
@@ -70,28 +88,48 @@ def link_pyqt(sys_path, venv_path):
|
||||
if not globbed_sip:
|
||||
raise Error("Did not find sip in {}!".format(sys_path))
|
||||
|
||||
files = ['PyQt5']
|
||||
files += [os.path.basename(e) for e in globbed_sip]
|
||||
for fn in files:
|
||||
files = [('PyQt5', True), ('sipconfig.py', False)]
|
||||
files += [(os.path.basename(e), True) for e in globbed_sip]
|
||||
for fn, required in files:
|
||||
source = os.path.join(sys_path, fn)
|
||||
dest = os.path.join(venv_path, fn)
|
||||
|
||||
if not os.path.exists(source):
|
||||
raise FileNotFoundError(source)
|
||||
if required:
|
||||
raise FileNotFoundError(source)
|
||||
else:
|
||||
continue
|
||||
|
||||
if os.path.exists(dest):
|
||||
if os.path.isdir(dest) and not os.path.islink(dest):
|
||||
shutil.rmtree(dest)
|
||||
if needs_update(source, dest):
|
||||
remove(dest)
|
||||
else:
|
||||
os.unlink(dest)
|
||||
if os.name == 'nt':
|
||||
if os.path.isdir(source):
|
||||
shutil.copytree(source, dest, ignore=get_ignored_files,
|
||||
copy_function=verbose_copy)
|
||||
else:
|
||||
print('{} -> {}'.format(source, dest))
|
||||
shutil.copy(source, dest)
|
||||
continue
|
||||
|
||||
copy_or_link(source, dest)
|
||||
|
||||
|
||||
def copy_or_link(source, dest):
|
||||
"""Copy or symlink source to dest."""
|
||||
if os.name == 'nt':
|
||||
if os.path.isdir(source):
|
||||
print('{} -> {}'.format(source, dest))
|
||||
shutil.copytree(source, dest, ignore=get_ignored_files,
|
||||
copy_function=verbose_copy)
|
||||
else:
|
||||
print('{} -> {}'.format(source, dest))
|
||||
os.symlink(source, dest)
|
||||
shutil.copy(source, dest)
|
||||
else:
|
||||
print('{} -> {}'.format(source, dest))
|
||||
os.symlink(source, dest)
|
||||
|
||||
|
||||
def remove(filename):
|
||||
"""Remove a given filename, regardless of whether it's a file or dir."""
|
||||
if os.path.isdir(filename):
|
||||
shutil.rmtree(filename)
|
||||
else:
|
||||
os.unlink(filename)
|
||||
|
||||
|
||||
def get_python_lib(executable, venv=False):
|
||||
@@ -102,7 +140,10 @@ def get_python_lib(executable, venv=False):
|
||||
treatments for Windows/Ubuntu shouldn't take place.
|
||||
"""
|
||||
distribution = platform.linux_distribution(full_distribution_name=False)
|
||||
if os.name == 'nt' and not venv:
|
||||
if 'PYTHON' in os.environ and not venv:
|
||||
# e.g. on AppVeyor
|
||||
return os.path.join(os.environ['PYTHON'], 'Lib', 'site-packages')
|
||||
elif os.name == 'nt' and not venv:
|
||||
# For some reason, we get an empty string from get_python_lib() on
|
||||
# Windows when running via tox, and sys.prefix is empty too...
|
||||
return os.path.join(os.path.dirname(executable), '..', 'Lib',
|
||||
|
||||
@@ -35,9 +35,13 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
|
||||
from scripts import utils
|
||||
|
||||
|
||||
def _py_files(target):
|
||||
def _py_files():
|
||||
"""Iterate over all python files and yield filenames."""
|
||||
for (dirpath, _dirnames, filenames) in os.walk(target):
|
||||
for (dirpath, _dirnames, filenames) in os.walk('.'):
|
||||
parts = dirpath.split(os.sep)
|
||||
if len(parts) >= 2 and parts[1].startswith('.'):
|
||||
# ignore hidden dirs
|
||||
continue
|
||||
for name in (e for e in filenames if e.endswith('.py')):
|
||||
yield os.path.join(dirpath, name)
|
||||
|
||||
@@ -64,31 +68,32 @@ def check_git():
|
||||
return status
|
||||
|
||||
|
||||
def check_spelling(target):
|
||||
def check_spelling():
|
||||
"""Check commonly misspelled words."""
|
||||
# Words which I often misspell
|
||||
words = {'behaviour', 'quitted', 'likelyhood', 'sucessfully',
|
||||
'occur[^r .]', 'seperator', 'explicitely', 'resetted',
|
||||
'auxillary', 'accidentaly', 'ambigious', 'loosly',
|
||||
'initialis', 'convienence', 'similiar', 'uncommited',
|
||||
'reproducable'}
|
||||
words = {'[Bb]ehaviour', '[Qq]uitted', 'Ll]ikelyhood', '[Ss]ucessfully',
|
||||
'[Oo]ccur[^r .]', '[Ss]eperator', '[Ee]xplicitely', '[Rr]esetted',
|
||||
'[Aa]uxillary', '[Aa]ccidentaly', '[Aa]mbigious', '[Ll]oosly',
|
||||
'[Ii]nitialis', '[Cc]onvienence', '[Ss]imiliar', '[Uu]ncommited',
|
||||
'[Rr]eproducable'}
|
||||
|
||||
# Words which look better when splitted, but might need some fine tuning.
|
||||
words |= {'keystrings', 'webelements', 'mouseevent', 'keysequence',
|
||||
'normalmode', 'eventloops', 'sizehint', 'statemachine',
|
||||
'metaobject', 'logrecord', 'filetype'}
|
||||
words |= {'[Kk]eystrings', '[Ww]ebelements', '[Mm]ouseevent',
|
||||
'[Kk]eysequence', '[Nn]ormalmode', '[Ee]ventloops',
|
||||
'[Ss]izehint', '[Ss]tatemachine', '[Mm]etaobject',
|
||||
'[Ll]ogrecord', '[Ff]iletype'}
|
||||
|
||||
seen = collections.defaultdict(list)
|
||||
try:
|
||||
ok = True
|
||||
for fn in _py_files(target):
|
||||
for fn in _py_files():
|
||||
with tokenize.open(fn) as f:
|
||||
if fn == os.path.join('scripts', 'misc_checks.py'):
|
||||
if fn == os.path.join('.', 'scripts', 'misc_checks.py'):
|
||||
continue
|
||||
for line in f:
|
||||
for w in words:
|
||||
if re.search(w, line) and fn not in seen[w]:
|
||||
print("Found '{}' in {}!".format(w, fn))
|
||||
print('Found "{}" in {}!'.format(w, fn))
|
||||
seen[w].append(fn)
|
||||
ok = False
|
||||
print()
|
||||
@@ -98,11 +103,11 @@ def check_spelling(target):
|
||||
return None
|
||||
|
||||
|
||||
def check_vcs_conflict(target):
|
||||
def check_vcs_conflict():
|
||||
"""Check VCS conflict markers."""
|
||||
try:
|
||||
ok = True
|
||||
for fn in _py_files(target):
|
||||
for fn in _py_files():
|
||||
with tokenize.open(fn) as f:
|
||||
for line in f:
|
||||
if any(line.startswith(c * 7) for c in '<>=|'):
|
||||
@@ -120,25 +125,14 @@ def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('checker', choices=('git', 'vcs', 'spelling'),
|
||||
help="Which checker to run.")
|
||||
parser.add_argument('target', help="What to check", nargs='*')
|
||||
args = parser.parse_args()
|
||||
if args.checker == 'git':
|
||||
ok = check_git()
|
||||
return 0 if ok else 1
|
||||
elif args.checker == 'vcs':
|
||||
is_ok = True
|
||||
for target in args.target:
|
||||
ok = check_vcs_conflict(target)
|
||||
if not ok:
|
||||
is_ok = False
|
||||
return 0 if is_ok else 1
|
||||
ok = check_vcs_conflict()
|
||||
elif args.checker == 'spelling':
|
||||
is_ok = True
|
||||
for target in args.target:
|
||||
ok = check_spelling(target)
|
||||
if not ok:
|
||||
is_ok = False
|
||||
return 0 if is_ok else 1
|
||||
ok = check_spelling()
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#!/usr/bin/env python3
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
# qutebrowser is free software: you can redistribute it and/or modify
|
||||
@@ -16,30 +18,16 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Checker for CRLF in files."""
|
||||
# pylint: disable=import-error,no-member
|
||||
|
||||
from pylint import interfaces, checkers
|
||||
"""cx_Freeze script to run qutebrowser tests on the frozen executable."""
|
||||
|
||||
import sys
|
||||
|
||||
class CrlfChecker(checkers.BaseChecker):
|
||||
import pytest
|
||||
import pytestqt.plugin
|
||||
import pytest_mock
|
||||
import pytest_capturelog
|
||||
|
||||
"""Check for CRLF in files."""
|
||||
|
||||
__implements__ = interfaces.IRawChecker
|
||||
|
||||
name = 'crlf'
|
||||
msgs = {'W9001': ('Uses CRLFs', 'crlf', None)}
|
||||
options = ()
|
||||
priority = -1
|
||||
|
||||
def process_module(self, node):
|
||||
"""Process the module."""
|
||||
for (lineno, line) in enumerate(node.file_stream):
|
||||
if b'\r\n' in line:
|
||||
self.add_message('crlf', line=lineno)
|
||||
return
|
||||
|
||||
|
||||
def register(linter):
|
||||
"""Register the checker."""
|
||||
linter.register_checker(CrlfChecker(linter))
|
||||
sys.exit(pytest.main(plugins=[pytestqt.plugin, pytest_mock,
|
||||
pytest_capturelog]))
|
||||
@@ -39,18 +39,29 @@ if '--profile-keep' in sys.argv:
|
||||
profilefile = os.path.join(os.getcwd(), 'profile')
|
||||
else:
|
||||
profilefile = os.path.join(tempdir, 'profile')
|
||||
|
||||
if '--profile-noconv' in sys.argv:
|
||||
sys.argv.remove('--profile-noconv')
|
||||
noconv = True
|
||||
else:
|
||||
noconv = False
|
||||
|
||||
if '--profile-dot' in sys.argv:
|
||||
sys.argv.remove('--profile-dot')
|
||||
dot = True
|
||||
else:
|
||||
dot = False
|
||||
|
||||
callgraphfile = os.path.join(tempdir, 'callgraph')
|
||||
profiler = cProfile.Profile()
|
||||
profiler.run('qutebrowser.qutebrowser.main()')
|
||||
profiler.dump_stats(profilefile)
|
||||
|
||||
if not noconv:
|
||||
subprocess.call(['pyprof2calltree', '-k', '-i', profilefile,
|
||||
'-o', callgraphfile])
|
||||
if dot:
|
||||
subprocess.call('gprof2dot -f pstats profile | dot -Tpng | feh -F -',
|
||||
shell=True) # yep, shell=True. I know what I'm doing.
|
||||
else:
|
||||
subprocess.call(['pyprof2calltree', '-k', '-i', profilefile,
|
||||
'-o', callgraphfile])
|
||||
shutil.rmtree(tempdir)
|
||||
|
||||
@@ -70,20 +70,20 @@ def main():
|
||||
if len(sys.argv) < 2:
|
||||
# pages which previously caused problems
|
||||
pages = [
|
||||
# ANGLE, https://bugreports.qt-project.org/browse/QTBUG-39723
|
||||
# ANGLE, https://bugreports.qt.io/browse/QTBUG-39723
|
||||
('http://www.binpress.com/', False),
|
||||
('http://david.li/flow/', False),
|
||||
('https://imzdl.com/', False),
|
||||
# not reproducible
|
||||
# https://bugreports.qt-project.org/browse/QTBUG-39847
|
||||
# https://bugreports.qt.io/browse/QTBUG-39847
|
||||
('http://www.20min.ch/', True),
|
||||
# HarfBuzz, https://bugreports.qt-project.org/browse/QTBUG-39278
|
||||
# HarfBuzz, https://bugreports.qt.io/browse/QTBUG-39278
|
||||
('http://www.the-compiler.org/', True),
|
||||
('http://phoronix.com', True),
|
||||
('http://twitter.com', True),
|
||||
# HarfBuzz #2, https://bugreports.qt-project.org/browse/QTBUG-36099
|
||||
# HarfBuzz #2, https://bugreports.qt.io/browse/QTBUG-36099
|
||||
('http://lenta.ru/', True),
|
||||
# Unknown, https://bugreports.qt-project.org/browse/QTBUG-41360
|
||||
# Unknown, https://bugreports.qt.io/browse/QTBUG-41360
|
||||
('http://salt.readthedocs.org/en/latest/topics/pillar/', True),
|
||||
]
|
||||
else:
|
||||
|
||||
@@ -37,7 +37,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
|
||||
import qutebrowser.app
|
||||
from scripts import asciidoc2html, utils
|
||||
from qutebrowser import qutebrowser
|
||||
from qutebrowser.commands import cmdutils
|
||||
from qutebrowser.commands import cmdutils, command
|
||||
from qutebrowser.config import configdata
|
||||
from qutebrowser.utils import docutils
|
||||
|
||||
@@ -54,6 +54,14 @@ class UsageFormatter(argparse.HelpFormatter):
|
||||
"""Override _format_usage to not add the 'usage:' prefix."""
|
||||
return super()._format_usage(usage, actions, groups, '')
|
||||
|
||||
def _get_default_metavar_for_optional(self, action):
|
||||
"""Do name transforming when getting metavar."""
|
||||
return command.arg_name(action.dest.upper())
|
||||
|
||||
def _get_default_metavar_for_positional(self, action):
|
||||
"""Do name transforming when getting metavar."""
|
||||
return command.arg_name(action.dest)
|
||||
|
||||
def _metavar_formatter(self, action, default_metavar):
|
||||
"""Override _metavar_formatter to add asciidoc markup to metavars.
|
||||
|
||||
@@ -184,7 +192,7 @@ def _get_command_doc_args(cmd, parser):
|
||||
yield "* +'{}'+: {}".format(name, parser.arg_descs[arg])
|
||||
except KeyError as e:
|
||||
raise KeyError("No description for arg {} of command "
|
||||
"'{}'!".format(e, cmd.name))
|
||||
"'{}'!".format(e, cmd.name)) from e
|
||||
|
||||
if cmd.opt_args:
|
||||
yield ""
|
||||
@@ -193,9 +201,9 @@ def _get_command_doc_args(cmd, parser):
|
||||
try:
|
||||
yield '* +*{}*+, +*{}*+: {}'.format(short_flag, long_flag,
|
||||
parser.arg_descs[arg])
|
||||
except KeyError:
|
||||
except KeyError as e:
|
||||
raise KeyError("No description for arg {} of command "
|
||||
"'{}'!".format(e, cmd.name))
|
||||
"'{}'!".format(e, cmd.name)) from e
|
||||
|
||||
|
||||
def _get_command_doc_count(cmd, parser):
|
||||
@@ -208,10 +216,14 @@ def _get_command_doc_count(cmd, parser):
|
||||
Yield:
|
||||
Strings which should be added to the docs.
|
||||
"""
|
||||
if cmd.special_params['count'] is not None:
|
||||
if cmd.count_arg is not None:
|
||||
yield ""
|
||||
yield "==== count"
|
||||
yield parser.arg_descs[cmd.special_params['count']]
|
||||
try:
|
||||
yield parser.arg_descs[cmd.count_arg]
|
||||
except KeyError as e:
|
||||
raise KeyError("No description for count arg {!r} of command "
|
||||
"{!r}!".format(cmd.count_arg, cmd.name)) from e
|
||||
|
||||
|
||||
def _get_command_doc_notes(cmd):
|
||||
|
||||
2
setup.py
2
setup.py
@@ -38,7 +38,7 @@ except NameError:
|
||||
try:
|
||||
common.write_git_file()
|
||||
setuptools.setup(
|
||||
packages=setuptools.find_packages(exclude=['qutebrowser.test']),
|
||||
packages=setuptools.find_packages(exclude=['scripts', 'scripts.*']),
|
||||
include_package_data=True,
|
||||
entry_points={'gui_scripts':
|
||||
['qutebrowser = qutebrowser.qutebrowser:main']},
|
||||
|
||||
@@ -378,11 +378,11 @@ class TestIsEditable:
|
||||
webelem.config = old_config
|
||||
|
||||
@pytest.fixture
|
||||
def stub_config(self, stubs, mocker):
|
||||
def stubbed_config(self, config_stub, monkeypatch):
|
||||
"""Fixture to create a config stub with an input section."""
|
||||
config = stubs.ConfigStub({'input': {}})
|
||||
mocker.patch('qutebrowser.browser.webelem.config', new=config)
|
||||
return config
|
||||
config_stub.data = {'input': {}}
|
||||
monkeypatch.setattr('qutebrowser.browser.webelem.config', config_stub)
|
||||
return config_stub
|
||||
|
||||
def test_input_plain(self):
|
||||
"""Test with plain input element."""
|
||||
@@ -469,27 +469,27 @@ class TestIsEditable:
|
||||
elem = get_webelem(tagname='textarea', attributes={'readonly': None})
|
||||
assert not elem.is_editable()
|
||||
|
||||
def test_embed_true(self, stub_config):
|
||||
def test_embed_true(self, stubbed_config):
|
||||
"""Test embed-element with insert-mode-on-plugins true."""
|
||||
stub_config.data['input']['insert-mode-on-plugins'] = True
|
||||
stubbed_config.data['input']['insert-mode-on-plugins'] = True
|
||||
elem = get_webelem(tagname='embed')
|
||||
assert elem.is_editable()
|
||||
|
||||
def test_applet_true(self, stub_config):
|
||||
def test_applet_true(self, stubbed_config):
|
||||
"""Test applet-element with insert-mode-on-plugins true."""
|
||||
stub_config.data['input']['insert-mode-on-plugins'] = True
|
||||
stubbed_config.data['input']['insert-mode-on-plugins'] = True
|
||||
elem = get_webelem(tagname='applet')
|
||||
assert elem.is_editable()
|
||||
|
||||
def test_embed_false(self, stub_config):
|
||||
def test_embed_false(self, stubbed_config):
|
||||
"""Test embed-element with insert-mode-on-plugins false."""
|
||||
stub_config.data['input']['insert-mode-on-plugins'] = False
|
||||
stubbed_config.data['input']['insert-mode-on-plugins'] = False
|
||||
elem = get_webelem(tagname='embed')
|
||||
assert not elem.is_editable()
|
||||
|
||||
def test_applet_false(self, stub_config):
|
||||
def test_applet_false(self, stubbed_config):
|
||||
"""Test applet-element with insert-mode-on-plugins false."""
|
||||
stub_config.data['input']['insert-mode-on-plugins'] = False
|
||||
stubbed_config.data['input']['insert-mode-on-plugins'] = False
|
||||
elem = get_webelem(tagname='applet')
|
||||
assert not elem.is_editable()
|
||||
|
||||
@@ -503,30 +503,30 @@ class TestIsEditable:
|
||||
elem = get_webelem(tagname='object', attributes={'type': 'image/gif'})
|
||||
assert not elem.is_editable()
|
||||
|
||||
def test_object_application(self, stub_config):
|
||||
def test_object_application(self, stubbed_config):
|
||||
"""Test object-element with application type."""
|
||||
stub_config.data['input']['insert-mode-on-plugins'] = True
|
||||
stubbed_config.data['input']['insert-mode-on-plugins'] = True
|
||||
elem = get_webelem(tagname='object',
|
||||
attributes={'type': 'application/foo'})
|
||||
assert elem.is_editable()
|
||||
|
||||
def test_object_application_false(self, stub_config):
|
||||
def test_object_application_false(self, stubbed_config):
|
||||
"""Test object-element with application type but not ...-on-plugins."""
|
||||
stub_config.data['input']['insert-mode-on-plugins'] = False
|
||||
stubbed_config.data['input']['insert-mode-on-plugins'] = False
|
||||
elem = get_webelem(tagname='object',
|
||||
attributes={'type': 'application/foo'})
|
||||
assert not elem.is_editable()
|
||||
|
||||
def test_object_classid(self, stub_config):
|
||||
def test_object_classid(self, stubbed_config):
|
||||
"""Test object-element with classid."""
|
||||
stub_config.data['input']['insert-mode-on-plugins'] = True
|
||||
stubbed_config.data['input']['insert-mode-on-plugins'] = True
|
||||
elem = get_webelem(tagname='object',
|
||||
attributes={'type': 'foo', 'classid': 'foo'})
|
||||
assert elem.is_editable()
|
||||
|
||||
def test_object_classid_false(self, stub_config):
|
||||
def test_object_classid_false(self, stubbed_config):
|
||||
"""Test object-element with classid but not insert-mode-on-plugins."""
|
||||
stub_config.data['input']['insert-mode-on-plugins'] = False
|
||||
stubbed_config.data['input']['insert-mode-on-plugins'] = False
|
||||
elem = get_webelem(tagname='object',
|
||||
attributes={'type': 'foo', 'classid': 'foo'})
|
||||
assert not elem.is_editable()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user