Compare commits
1365 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 | ||
|
|
5b48b0a7fe | ||
|
|
42577b454b | ||
|
|
900fe3aa08 | ||
|
|
43df32949d | ||
|
|
38d34e1dea | ||
|
|
4436941d97 | ||
|
|
006b7716c8 | ||
|
|
ffd1e673b3 | ||
|
|
71ffe8f656 | ||
|
|
cc738fa846 | ||
|
|
508993ac68 | ||
|
|
980b3506a3 | ||
|
|
3cf6d1c185 | ||
|
|
f5e6091ff6 | ||
|
|
f313bcaf13 | ||
|
|
d8d29449ca | ||
|
|
987bab9960 | ||
|
|
ba678e29fb | ||
|
|
10214a8b83 | ||
|
|
f6b88770d1 | ||
|
|
0233c96d48 | ||
|
|
6ae94d6f49 | ||
|
|
e8ddd9397d | ||
|
|
a6e3199616 | ||
|
|
982733e1f4 | ||
|
|
4e18e54803 | ||
|
|
501138d5a0 | ||
|
|
b609f993c3 | ||
|
|
9381aac501 | ||
|
|
e603d9a2d0 | ||
|
|
a6443231e5 | ||
|
|
e5d33a6706 | ||
|
|
d413aacc19 | ||
|
|
941eac848e | ||
|
|
3e1d62171f | ||
|
|
bd9168fdfe | ||
|
|
4547fd2c5d | ||
|
|
566ffdbe23 | ||
|
|
39f7850942 | ||
|
|
c071bcbec8 | ||
|
|
f85ba8645f | ||
|
|
3433a1ec7a | ||
|
|
fa2340b61e | ||
|
|
d700d18780 | ||
|
|
e24b06cdf9 | ||
|
|
6b0c16f109 | ||
|
|
f4c46ec1c5 | ||
|
|
31bcc70efb | ||
|
|
3bc55e0405 | ||
|
|
0b2e39e4a4 | ||
|
|
f865b87a74 | ||
|
|
e294e325f0 | ||
|
|
29c51c288b | ||
|
|
6f1e830aba | ||
|
|
253f3b2cd7 | ||
|
|
55e3645131 | ||
|
|
91b72ef292 | ||
|
|
8d98868ccd | ||
|
|
83dbe48469 | ||
|
|
f77ba5744b | ||
|
|
695712e50c | ||
|
|
7160a89cb9 | ||
|
|
2d8df76609 | ||
|
|
ecb0a4e2f8 | ||
|
|
9e0d65c219 | ||
|
|
9111ae7b3c | ||
|
|
96ddfd5b65 | ||
|
|
e33517f592 | ||
|
|
2a796d9aa4 | ||
|
|
12c83b721f | ||
|
|
cc2c7c09ea | ||
|
|
2fa66ba250 | ||
|
|
425cffc2f7 | ||
|
|
74f4642a2c | ||
|
|
a2772db9da | ||
|
|
44a6617184 | ||
|
|
1770570921 | ||
|
|
343a091aee | ||
|
|
6c566198f1 | ||
|
|
33dbed5624 | ||
|
|
853280feeb | ||
|
|
6037fd74cd | ||
|
|
b18c1254a4 | ||
|
|
c3e615dfa3 | ||
|
|
d91400c3be | ||
|
|
d375ddebea | ||
|
|
894a2a4e7b | ||
|
|
63ce7d6e02 | ||
|
|
ebfcc0a83c | ||
|
|
e584aa319f | ||
|
|
76651822bd | ||
|
|
7d4e6dfd67 | ||
|
|
679ffa452a | ||
|
|
fe696aeba5 | ||
|
|
fd88311d9b | ||
|
|
6e3c3d7a70 | ||
|
|
a29b78e8ca | ||
|
|
abc2c2b087 | ||
|
|
26dc275db3 | ||
|
|
8702ac8a98 | ||
|
|
75386e4051 | ||
|
|
10619325f6 | ||
|
|
b591dedf7f | ||
|
|
1f39c7782a | ||
|
|
1345a13a71 | ||
|
|
4e2ef45cd8 | ||
|
|
a480b297ca | ||
|
|
2d258ec53f | ||
|
|
b1c475c61d | ||
|
|
20f0ef7ccc | ||
|
|
46d1760798 | ||
|
|
2876ba5cfa | ||
|
|
d83da987ae | ||
|
|
067ac13018 | ||
|
|
9b8f5e3ff0 | ||
|
|
2c99520f79 | ||
|
|
1eb2a9a725 | ||
|
|
476ccd8fe1 | ||
|
|
9d44f777c0 | ||
|
|
143228d593 | ||
|
|
d3a92d505c | ||
|
|
eb76cd71de | ||
|
|
967c706bf0 | ||
|
|
3864eff0be | ||
|
|
bfc99f09f9 | ||
|
|
5fb7ad383d | ||
|
|
8bbff689b4 | ||
|
|
17ebbc37c5 | ||
|
|
ce0b9eab58 | ||
|
|
3de584f02c | ||
|
|
6a03089639 | ||
|
|
1e2a902a9f | ||
|
|
87d5f57c08 | ||
|
|
1e1a433dff | ||
|
|
84d2556863 | ||
|
|
1fdd7051c3 | ||
|
|
57158e7191 | ||
|
|
4fa2294805 | ||
|
|
8222097942 | ||
|
|
2666388a6d | ||
|
|
4ab03b1536 | ||
|
|
5cf8ff1f84 | ||
|
|
bd9c807fb1 | ||
|
|
12903a34f4 | ||
|
|
065c3fcd9d | ||
|
|
6388ec4794 | ||
|
|
7e7a1b7b28 | ||
|
|
9128ac41cb | ||
|
|
8000ea33d2 | ||
|
|
ea16fb3684 | ||
|
|
cef88d6e19 | ||
|
|
6429d29a23 | ||
|
|
b2df5a5b47 | ||
|
|
18dea8c7cb | ||
|
|
217e788f4b | ||
|
|
f1ebbda7a0 | ||
|
|
8e93747040 | ||
|
|
dd4096b5a4 | ||
|
|
e23c9401f2 | ||
|
|
99abd1edeb | ||
|
|
cd7319de1e | ||
|
|
80b0692971 | ||
|
|
22df30cdcc | ||
|
|
3129def33e | ||
|
|
1c9f116370 | ||
|
|
2ac0c7b8f0 | ||
|
|
05087b976a | ||
|
|
7442e30f29 | ||
|
|
f57223f7eb | ||
|
|
70f52fa9cc | ||
|
|
544dc650e7 | ||
|
|
0e76f9b1f1 | ||
|
|
e94a8a80f1 | ||
|
|
3421e5e34f | ||
|
|
fba0ae69ce | ||
|
|
231feda2c8 | ||
|
|
efbc8e0cbf | ||
|
|
7540a5bbf4 | ||
|
|
22522406e1 | ||
|
|
9c533e1941 | ||
|
|
9d39fbd4e5 | ||
|
|
45e95d497d | ||
|
|
068947ba7e | ||
|
|
91a8b23aeb | ||
|
|
6fb83aacae | ||
|
|
58a8a7e992 | ||
|
|
25fca03dca | ||
|
|
6917c3b32d | ||
|
|
3b3b55234b | ||
|
|
ac63fc073f | ||
|
|
630a827afc | ||
|
|
a504bd1436 | ||
|
|
0b26e295bc | ||
|
|
5b372aeee0 | ||
|
|
0b063ab4b4 | ||
|
|
ff75d18e62 | ||
|
|
086f12600c | ||
|
|
75e927f79e | ||
|
|
6482025399 | ||
|
|
f68cfc13e0 | ||
|
|
9a47848794 | ||
|
|
96a600e9dc | ||
|
|
b938318d5f | ||
|
|
6b7ae70e6d | ||
|
|
1b476d9af7 | ||
|
|
9e59108788 | ||
|
|
df3096fbb5 | ||
|
|
11ded52f06 | ||
|
|
d4d14598dd | ||
|
|
64b1b48be6 | ||
|
|
7e51addeb0 | ||
|
|
4e0712622b | ||
|
|
1dcc5a32d6 | ||
|
|
298892a4a8 | ||
|
|
751b62e344 | ||
|
|
18b5512fe9 | ||
|
|
953119ef75 | ||
|
|
47b9ea1f88 | ||
|
|
e1cdbd5f16 | ||
|
|
009e595780 | ||
|
|
84b9d34a7f | ||
|
|
3d3324ccfa | ||
|
|
9f9996bc66 | ||
|
|
214347497a | ||
|
|
37ab5296a7 | ||
|
|
79be5b0f4a | ||
|
|
1f08d8e319 | ||
|
|
3096f3856a | ||
|
|
068e1c14b6 | ||
|
|
1fb848249e | ||
|
|
840652f396 | ||
|
|
2ba28a59fe | ||
|
|
371ec564e1 | ||
|
|
11bd4a13f6 | ||
|
|
8748420b1b | ||
|
|
6e435ad215 | ||
|
|
a98060e020 | ||
|
|
eeb875d098 | ||
|
|
431257d380 | ||
|
|
16ffafb769 | ||
|
|
38c63ca2ea | ||
|
|
8ebac8d38c | ||
|
|
eb3b0b960f | ||
|
|
96090b86fd | ||
|
|
36421934f9 | ||
|
|
2f629befc3 | ||
|
|
70ccdd86b2 | ||
|
|
fab6bc285c | ||
|
|
a38c3ae1e1 | ||
|
|
33dff70357 | ||
|
|
dff8f73a11 | ||
|
|
5233e7fac8 | ||
|
|
b2427701fa | ||
|
|
8af2e712ae | ||
|
|
34a0976a6f | ||
|
|
d062ff5138 | ||
|
|
1e18ce94cf | ||
|
|
51141adb24 | ||
|
|
c562fac9cb | ||
|
|
16ab2ad167 | ||
|
|
acb13bb61e | ||
|
|
b6dc43396b | ||
|
|
6a02ee1cbb | ||
|
|
67b9036574 | ||
|
|
cb3fcd3d8a | ||
|
|
738f6a4510 | ||
|
|
b409517777 | ||
|
|
a1df3194ff | ||
|
|
fc14b5b6b2 | ||
|
|
8285245641 | ||
|
|
320fd87cbc | ||
|
|
9099d8c466 | ||
|
|
1d29e3462f | ||
|
|
07da31e2a0 | ||
|
|
513fbb1539 | ||
|
|
f518b5b7f2 | ||
|
|
82322beb03 | ||
|
|
5f454f3440 | ||
|
|
09526ad715 | ||
|
|
ec487dd6b1 | ||
|
|
5043f58f3c | ||
|
|
db98b03f34 | ||
|
|
1d1ac1ef6f | ||
|
|
1425d306bc | ||
|
|
0e8b42a9d8 | ||
|
|
e7f5433da3 | ||
|
|
21d2bb2291 | ||
|
|
8811947f50 | ||
|
|
d2f829ebd3 | ||
|
|
97b678d8c7 | ||
|
|
c13e09b706 | ||
|
|
157c25bb13 | ||
|
|
19d369377e | ||
|
|
8c6ad697ce | ||
|
|
c67fcc4fd6 | ||
|
|
565303ebcd | ||
|
|
858c38964b | ||
|
|
f77c0f9afa | ||
|
|
adb11360db | ||
|
|
7a4a4a4a4e | ||
|
|
218822d6e8 | ||
|
|
d6732c64a3 | ||
|
|
fb5fbd09da | ||
|
|
330e03d382 | ||
|
|
e3f9a08611 | ||
|
|
1a534454e2 | ||
|
|
c83775cf29 | ||
|
|
706cc1a87f | ||
|
|
ca22ed02e6 | ||
|
|
2b10adfad7 | ||
|
|
2dcf323077 | ||
|
|
94bc10405a | ||
|
|
9a405df560 | ||
|
|
210ce8ca7c | ||
|
|
6dc65287a9 | ||
|
|
f1b9a3c8b5 | ||
|
|
4157cfe86f | ||
|
|
b1f99392e8 | ||
|
|
b226426f15 | ||
|
|
1aaa538b45 | ||
|
|
59bbca9b40 | ||
|
|
777e3f58e1 | ||
|
|
806742abd3 | ||
|
|
3df5e13c65 | ||
|
|
503060881a | ||
|
|
693ea0c312 | ||
|
|
553d8cf986 | ||
|
|
46c31911a6 | ||
|
|
57b7b43802 | ||
|
|
001bf982e5 | ||
|
|
d266665955 | ||
|
|
9512a52d21 | ||
|
|
cdbb118238 | ||
|
|
70cd8e74eb | ||
|
|
a857b9a638 | ||
|
|
833830d5e9 | ||
|
|
55eabafc0d | ||
|
|
901db0911e | ||
|
|
994546f04d | ||
|
|
94f694bd77 | ||
|
|
96da7d9fe6 | ||
|
|
74892ac8e4 | ||
|
|
cef49864d9 | ||
|
|
ccce2eddad | ||
|
|
389feab1df | ||
|
|
dbd121a079 | ||
|
|
97dd86735a | ||
|
|
8023b1456d | ||
|
|
7a28b6c821 | ||
|
|
299dbfa56a | ||
|
|
61e732f217 | ||
|
|
1efe18ecc6 | ||
|
|
734268187c | ||
|
|
9ee19be70d | ||
|
|
0b975db4dd | ||
|
|
34b24aafa8 | ||
|
|
fe4f32606d | ||
|
|
834832e3ba | ||
|
|
59948a038c | ||
|
|
f6a7ef3985 | ||
|
|
0778e33142 | ||
|
|
4439ef8ddd | ||
|
|
fb85a279f4 | ||
|
|
e8e6d8409b | ||
|
|
cd14ae2f1f | ||
|
|
1a4f7170a5 | ||
|
|
12a82eb371 | ||
|
|
4fa64350ca | ||
|
|
94666fe979 | ||
|
|
0f5391c4fa | ||
|
|
1b879faf84 | ||
|
|
5b4f6d39c2 | ||
|
|
5ab052c40f | ||
|
|
1ead66a4d5 | ||
|
|
2b06d4e684 | ||
|
|
acc33b4f91 | ||
|
|
aca44da26e | ||
|
|
596bff0772 | ||
|
|
d87a1bb2b4 | ||
|
|
2f0522ebb0 | ||
|
|
4b6d49e926 | ||
|
|
2da45e98ca | ||
|
|
8307b546b7 | ||
|
|
9ffb30a16f | ||
|
|
e78fa431c5 | ||
|
|
6a16875f50 | ||
|
|
bfc114ae35 | ||
|
|
bd3d091318 | ||
|
|
181bcc4f8d | ||
|
|
60b6519b04 | ||
|
|
181426b50a | ||
|
|
2010e8115b | ||
|
|
27f4ada799 | ||
|
|
5b4b793538 | ||
|
|
99de995813 | ||
|
|
a3a2c15114 | ||
|
|
86e77e19b6 | ||
|
|
ee8beb174d | ||
|
|
cd34562d34 | ||
|
|
1d9738c1ab | ||
|
|
e48f419f78 | ||
|
|
a919ce2ffe | ||
|
|
f7b036cf15 | ||
|
|
edf762e210 | ||
|
|
8ee8d28f03 | ||
|
|
858131c9bc | ||
|
|
0827ddec86 | ||
|
|
3e5b9a4a4a | ||
|
|
2c9b5f24fc | ||
|
|
034f1136d3 | ||
|
|
0fb74da4ff | ||
|
|
7ed8f3d4ac | ||
|
|
1c48440797 | ||
|
|
7c125642b9 | ||
|
|
d449a60078 | ||
|
|
9cfa34c009 | ||
|
|
73de32d62d | ||
|
|
a46d36b3b0 | ||
|
|
61a52f3b91 | ||
|
|
048823650c | ||
|
|
b61f8941de | ||
|
|
7d48845afa | ||
|
|
4fa5872733 | ||
|
|
ebae77e8c5 | ||
|
|
e5ebea80b3 | ||
|
|
be6ea2f0e8 | ||
|
|
e431f09fab | ||
|
|
4e7e97232e | ||
|
|
49c666a4a8 | ||
|
|
68774a2c75 | ||
|
|
8e0c1cff7b | ||
|
|
82deaeed2e | ||
|
|
40af99bacc | ||
|
|
801f6b2667 | ||
|
|
c8c095d499 | ||
|
|
f19eba3b40 | ||
|
|
21ab5f8685 | ||
|
|
caf0c76a4e | ||
|
|
65f21fc8ee | ||
|
|
c0eb8daff7 | ||
|
|
003f7fd957 | ||
|
|
bb2caaa11d | ||
|
|
cdc298fbc5 | ||
|
|
84643b4a39 | ||
|
|
072210c47b | ||
|
|
e696898c4a | ||
|
|
42e2438efb | ||
|
|
d66997610b | ||
|
|
bfd0a3fbc2 | ||
|
|
561ebd07f9 | ||
|
|
5f46870594 | ||
|
|
6d51fcfb2e | ||
|
|
a76868c0f4 | ||
|
|
fa0bfaa49e | ||
|
|
eb8bad3d18 | ||
|
|
a12dee8898 | ||
|
|
ddb39275eb | ||
|
|
4b4bb3af88 | ||
|
|
0ebef4069e | ||
|
|
813ce9a513 | ||
|
|
8e0dddf86a | ||
|
|
fcbd69e209 | ||
|
|
8078068552 | ||
|
|
f91aaf778a | ||
|
|
0d9bf5e2c9 | ||
|
|
81af41d77f | ||
|
|
25b09b60d9 | ||
|
|
1dc9862c0b | ||
|
|
1d27dcca81 | ||
|
|
31d9018fc4 | ||
|
|
0fcd016427 | ||
|
|
9d716d74b4 | ||
|
|
891c07f7e3 | ||
|
|
caad56c978 | ||
|
|
0f9a1fe178 | ||
|
|
94434ea739 | ||
|
|
c5a2039da4 | ||
|
|
cabe5bf2a3 | ||
|
|
617cd8977b | ||
|
|
359482b511 | ||
|
|
0ccb104f48 | ||
|
|
05d8a2429b | ||
|
|
42c8acc7aa | ||
|
|
f33bc7bf31 | ||
|
|
684f0d3df5 | ||
|
|
5fe85d0dde | ||
|
|
60d4305cc4 | ||
|
|
634028e277 | ||
|
|
14f2420500 | ||
|
|
a41331a402 | ||
|
|
87951ee3a8 | ||
|
|
b5d3b264e8 | ||
|
|
2d4b03fbc9 | ||
|
|
81fb57bbf0 | ||
|
|
852fe2f84c | ||
|
|
7dd908bd51 | ||
|
|
543c6cb90b | ||
|
|
3d5012ccca | ||
|
|
dc9e2a9772 | ||
|
|
8c32fb86e2 | ||
|
|
ea2dba6b38 | ||
|
|
3d72235023 | ||
|
|
9534deb2e7 | ||
|
|
1268cbecf3 | ||
|
|
78e2d03f04 | ||
|
|
0ec05e071f | ||
|
|
e04af40140 | ||
|
|
7a90d7fca8 | ||
|
|
83b636a0a7 | ||
|
|
62fb4b0d0b | ||
|
|
b31a432a1a | ||
|
|
238761bd5b | ||
|
|
5b33f6c5fe | ||
|
|
56b0ae2b6e | ||
|
|
d8fe62bc61 | ||
|
|
46ca0e447e | ||
|
|
8f1d81a644 | ||
|
|
53b024f246 | ||
|
|
b78d3934d7 | ||
|
|
8e2e996369 | ||
|
|
4d2aa6a4d4 | ||
|
|
da9a8d368f | ||
|
|
f27d1364df | ||
|
|
5817f3c18d | ||
|
|
66d3ec1c08 | ||
|
|
1cf34e7984 | ||
|
|
a38a77b16b | ||
|
|
94b51128d1 | ||
|
|
44b21374cb | ||
|
|
3cf9768f21 | ||
|
|
e459e1a472 | ||
|
|
1c5f036d4e | ||
|
|
3eb4aec0ca | ||
|
|
049a360abc | ||
|
|
cd5d4f4fee | ||
|
|
dfb801a0b7 | ||
|
|
9f0658f191 | ||
|
|
8cd5f9e6d1 | ||
|
|
dd995c434c | ||
|
|
1f39200b28 | ||
|
|
91f7056649 | ||
|
|
22fab87311 | ||
|
|
0b55f4df77 | ||
|
|
5a73f5d2c1 | ||
|
|
c2f9cae770 | ||
|
|
658ab70e98 | ||
|
|
4e5bac709b | ||
|
|
d4ef66714f | ||
|
|
e44c5aee5b | ||
|
|
6e3d5867f9 | ||
|
|
55193803a1 | ||
|
|
e9da7b5391 | ||
|
|
43c9d69295 | ||
|
|
bc43fb5e4c | ||
|
|
2ad1c4737c | ||
|
|
767ca42e46 | ||
|
|
494825fed0 | ||
|
|
4704e81b41 | ||
|
|
0f48ea62c1 | ||
|
|
3729ccb8cf | ||
|
|
6bbb655a54 | ||
|
|
9428338389 | ||
|
|
6f89ab628b | ||
|
|
be48f3c875 | ||
|
|
1c055a25b6 | ||
|
|
31e71ed6d9 | ||
|
|
97d7e727b7 | ||
|
|
514ae1e798 | ||
|
|
a55076dfdf | ||
|
|
f78b21874f | ||
|
|
7615e20091 | ||
|
|
5ed592a447 | ||
|
|
370c182f48 | ||
|
|
0957d5df8e | ||
|
|
7d01abacaa | ||
|
|
af53a670ee | ||
|
|
a18b3fe2a8 | ||
|
|
fba2b2b5ae | ||
|
|
a95dda8e92 | ||
|
|
cbde36948a | ||
|
|
18b58b2001 | ||
|
|
59a11c178f | ||
|
|
9b1729c77e | ||
|
|
1c919967bb | ||
|
|
2f01c7c3ae | ||
|
|
dc6aaecc78 | ||
|
|
d9ae2183e8 | ||
|
|
503fc9f56b | ||
|
|
33a2181e31 | ||
|
|
933151abd7 | ||
|
|
1266f147c8 | ||
|
|
822bf90b26 | ||
|
|
3b667325ca | ||
|
|
43c5dc3bf6 | ||
|
|
d6e87a2672 | ||
|
|
9736224fa6 | ||
|
|
55649882a0 | ||
|
|
c40e70ed11 | ||
|
|
6bf87dd1d7 | ||
|
|
4138debd1e | ||
|
|
b721a0e992 | ||
|
|
66ec4f0599 | ||
|
|
81b91888f4 | ||
|
|
052d4f513c | ||
|
|
4486573b2a | ||
|
|
cf5fd9456b | ||
|
|
1526cf1532 | ||
|
|
b9f16804f7 | ||
|
|
68a0428a09 | ||
|
|
d90814aabe | ||
|
|
4c87287f4e | ||
|
|
7169d02609 | ||
|
|
bc380fca61 | ||
|
|
ab2d2d79ca | ||
|
|
6576796718 | ||
|
|
cd39be62ee | ||
|
|
594438e4d8 | ||
|
|
fe90b153ed | ||
|
|
8ffc1a3966 | ||
|
|
41fd89a206 | ||
|
|
323db55a9c | ||
|
|
bd0a3a86d9 | ||
|
|
09ea733231 | ||
|
|
9702433d4e | ||
|
|
7a11be1fb1 | ||
|
|
ad53950e28 | ||
|
|
0d93d1eaff | ||
|
|
6ab65eb9d3 | ||
|
|
1e52f3856c | ||
|
|
29b9526a8e | ||
|
|
1d167fa428 | ||
|
|
b808aa07ba | ||
|
|
3edffefff4 | ||
|
|
5b3b324331 | ||
|
|
62adc5ffe3 | ||
|
|
b86aa9061a | ||
|
|
d16ac8f3ce | ||
|
|
100e21d50c | ||
|
|
bd633609ff | ||
|
|
4dbbde9eaa | ||
|
|
768e6ac5bf | ||
|
|
feb964cff9 | ||
|
|
be568e1681 | ||
|
|
2b7a843136 | ||
|
|
f3d570dd5b | ||
|
|
6ceb0a41ff | ||
|
|
409c04b6d4 | ||
|
|
ca590c5df7 | ||
|
|
e3ca06bc53 | ||
|
|
0587cc8b1d | ||
|
|
e38f9747e7 | ||
|
|
d7c7e91f2b | ||
|
|
981a3ef96b | ||
|
|
a08b814e5f | ||
|
|
00f67135ae | ||
|
|
eb428f2aeb | ||
|
|
7580473a43 | ||
|
|
2201ca600b | ||
|
|
d1df0b843e | ||
|
|
d1d43b29dc | ||
|
|
6f1facac60 | ||
|
|
b2646cb5c0 | ||
|
|
b8c54b5f02 | ||
|
|
907440d12c | ||
|
|
2264b55e92 | ||
|
|
816fcf3a6c | ||
|
|
f76ce3c152 | ||
|
|
2404c75012 | ||
|
|
7813d9a93d | ||
|
|
4eefc53ed0 | ||
|
|
f6d0907736 | ||
|
|
2a72d290a7 | ||
|
|
b387b4c7a9 | ||
|
|
d7b5f2bf52 | ||
|
|
5c92144f6b | ||
|
|
011e398f77 | ||
|
|
011cd08fc8 | ||
|
|
5d5e26eb7b | ||
|
|
5a0a0302df | ||
|
|
3b6a504d7b | ||
|
|
aa3017dd58 | ||
|
|
50557a9b3e | ||
|
|
dffa7ccf46 | ||
|
|
ef5412f596 | ||
|
|
335f72a93f | ||
|
|
5100f6fc8f | ||
|
|
fb2e84be2a | ||
|
|
8da4e2b6f4 | ||
|
|
407edef2bc | ||
|
|
6c2471bf9c | ||
|
|
4f20d6123c | ||
|
|
d540a1ee22 | ||
|
|
6d3f871119 | ||
|
|
958e67ab9e | ||
|
|
2e45c2c063 | ||
|
|
532ec30d00 | ||
|
|
f4479a8140 | ||
|
|
6a7e454789 | ||
|
|
c30978be2f | ||
|
|
18443a6880 | ||
|
|
aa6750ac1b | ||
|
|
dc9263a77c | ||
|
|
1e8729eac7 | ||
|
|
380537d49c | ||
|
|
d0f416386a | ||
|
|
52afa1a479 | ||
|
|
3c21d5986e | ||
|
|
ddc4e7b309 | ||
|
|
b3b576f5d2 | ||
|
|
c98bfa9a9d | ||
|
|
aae33a0308 | ||
|
|
dbd0d1fff9 | ||
|
|
4f1e0d32b0 | ||
|
|
e9786458fa | ||
|
|
eadaef3ce9 | ||
|
|
04598b2315 | ||
|
|
4485e4ee1b | ||
|
|
a32f1e6180 | ||
|
|
223f8f243e | ||
|
|
969c3550cd | ||
|
|
48c83505df | ||
|
|
8c227324fe | ||
|
|
a412814dee | ||
|
|
d147ba90d4 | ||
|
|
354018efcd | ||
|
|
3eac528716 | ||
|
|
3ba202d467 | ||
|
|
b9ed0b37f0 | ||
|
|
8fb1a887db | ||
|
|
5947994479 | ||
|
|
4c1113cdf4 | ||
|
|
6d0fff1c24 | ||
|
|
30e93ca4b8 | ||
|
|
e767862bee | ||
|
|
5fc900a1cb | ||
|
|
395047d778 | ||
|
|
c4b4027104 | ||
|
|
14afb3ef14 | ||
|
|
8c7a7aaf20 | ||
|
|
34c9a73e32 | ||
|
|
1086c33ec2 | ||
|
|
eba4b58a7c | ||
|
|
b12b83f98c | ||
|
|
75ac9cce49 | ||
|
|
77df4c7241 | ||
|
|
9ebf36f26b | ||
|
|
55d9f62c9f | ||
|
|
82ee78b3db | ||
|
|
c4619874e6 | ||
|
|
c008ee8dd7 | ||
|
|
492f066bd8 | ||
|
|
c4bb9344a9 | ||
|
|
dda54a2cc9 | ||
|
|
3ff28027de | ||
|
|
5c37d4a19d | ||
|
|
f828e554f7 | ||
|
|
46396cce1e | ||
|
|
e07146be7c | ||
|
|
dfa276a20c | ||
|
|
e339b0cef9 | ||
|
|
9cc5bedb4d | ||
|
|
2d77381660 | ||
|
|
85c89305a7 | ||
|
|
7bf0013e60 | ||
|
|
6722780f86 | ||
|
|
60874aad28 | ||
|
|
ce5629eab3 | ||
|
|
0305dedbfb | ||
|
|
94ea35c9e8 | ||
|
|
ff0c845c50 | ||
|
|
efe96462c9 | ||
|
|
013f906c3b | ||
|
|
b8a04f5309 | ||
|
|
04c8a17b2e | ||
|
|
f64269c57a | ||
|
|
b1b1cecdb7 | ||
|
|
ecc7f09f86 | ||
|
|
37e31d92c7 | ||
|
|
abe2dd7589 | ||
|
|
6d8bffe405 | ||
|
|
e0483363aa | ||
|
|
8a3aca63b0 | ||
|
|
2814456586 | ||
|
|
2203db298d | ||
|
|
f811f511fa | ||
|
|
9521d253a1 | ||
|
|
e54d3c21ee | ||
|
|
7dbbfedd3b | ||
|
|
450d1ab70d | ||
|
|
7e642fb3e4 | ||
|
|
7165ce0b1f | ||
|
|
0712037a52 | ||
|
|
6d1ac5d2a2 | ||
|
|
b0afe72e42 | ||
|
|
e30f79981d | ||
|
|
a5ce9571ff | ||
|
|
eac186bd64 | ||
|
|
df45cdfa86 | ||
|
|
f2649ccc4b | ||
|
|
6ec5b70067 | ||
|
|
56d844aff8 | ||
|
|
8b69f9b62c | ||
|
|
4471f81c11 | ||
|
|
8b9f323f41 | ||
|
|
0cd0f97587 | ||
|
|
8e550ebe88 | ||
|
|
0ea25c6ef0 | ||
|
|
0a1fa87ac9 | ||
|
|
5ca58843fc | ||
|
|
b5848a70ee | ||
|
|
ad6065605a | ||
|
|
8746715bf0 | ||
|
|
19f75984e3 |
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
|
||||
14
.coveragerc
Normal file
14
.coveragerc
Normal file
@@ -0,0 +1,14 @@
|
||||
[run]
|
||||
branch = true
|
||||
omit =
|
||||
qutebrowser/__main__.py
|
||||
*/__init__.py
|
||||
qutebrowser/resources.py
|
||||
|
||||
[report]
|
||||
exclude_lines =
|
||||
pragma: no cover
|
||||
def __repr__
|
||||
raise AssertionError
|
||||
raise NotImplementedError
|
||||
if __name__ == ["']__main__["']:
|
||||
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
|
||||
19
.flake8
19
.flake8
@@ -1,19 +0,0 @@
|
||||
# vim: ft=dosini fileencoding=utf-8:
|
||||
|
||||
[flake8]
|
||||
# E241: Multiple spaces after ,
|
||||
# E265: Block comment should start with '#'
|
||||
# checked by pylint:
|
||||
# F401: Unused import
|
||||
# E501: Line too long
|
||||
# F821: undefined name
|
||||
# F841: unused variable
|
||||
# E222: Multiple spaces after operator
|
||||
# F811: Redifiniton
|
||||
# W292: No newline at end of file
|
||||
# E701: multiple statements on one line
|
||||
# E702: multiple statements on one line
|
||||
# E225: missing whitespace around operator
|
||||
ignore=E241,E265,F401,E501,F821,F841,E222,F811,W292,E701,E702,E225
|
||||
max_complexity = 12
|
||||
exclude = ez_setup.py
|
||||
10
.gitignore
vendored
10
.gitignore
vendored
@@ -12,4 +12,14 @@ __pycache__
|
||||
/qutebrowser/git-commit-id
|
||||
/doc/*.html
|
||||
/README.html
|
||||
/CHANGELOG.html
|
||||
/CONTRIBUTING.html
|
||||
/FAQ.html
|
||||
/INSTALL.html
|
||||
/qutebrowser/html/doc/
|
||||
/.venv
|
||||
/.coverage
|
||||
/htmlcov
|
||||
/.tox
|
||||
/testresults.html
|
||||
/.cache
|
||||
|
||||
24
.pylintrc
24
.pylintrc
@@ -1,29 +1,34 @@
|
||||
# vim: ft=dosini fileencoding=utf-8:
|
||||
|
||||
[MASTER]
|
||||
ignore=ez_setup.py
|
||||
ignore=resources.py
|
||||
extension-pkg-whitelist=PyQt5,sip
|
||||
load-plugins=pylint_checkers.config,
|
||||
pylint_checkers.modeline,
|
||||
pylint_checkers.openencoding,
|
||||
pylint_checkers.settrace
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
disable=no-self-use,
|
||||
super-on-old-class,
|
||||
old-style-class,
|
||||
abstract-class-little-used,
|
||||
bad-builtin,
|
||||
star-args,
|
||||
fixme,
|
||||
global-statement,
|
||||
no-init,
|
||||
locally-disabled,
|
||||
too-many-ancestors,
|
||||
too-few-public-methods,
|
||||
too-many-public-methods,
|
||||
cyclic-import,
|
||||
bad-option-value,
|
||||
bad-continuation,
|
||||
too-many-instance-attributes,
|
||||
unnecessary-lambda,
|
||||
blacklisted-name,
|
||||
too-many-lines
|
||||
too-many-lines,
|
||||
logging-format-interpolation,
|
||||
interface-not-implemented,
|
||||
broad-except,
|
||||
bare-except,
|
||||
eval-used,
|
||||
exec-used,
|
||||
file-ignored
|
||||
|
||||
[BASIC]
|
||||
module-rgx=(__)?[a-z][a-z0-9_]*(__)?$
|
||||
@@ -35,6 +40,7 @@ argument-rgx=[a-z_][a-z0-9_]{0,30}$
|
||||
variable-rgx=[a-z_][a-z0-9_]{0,30}$
|
||||
class-attribute-rgx=[A-Za-z_][A-Za-z0-9_]{1,30}$
|
||||
inlinevar-rgx=[a-z_][a-z0-9_]*$
|
||||
docstring-min-length=2
|
||||
|
||||
[FORMAT]
|
||||
max-line-length=79
|
||||
|
||||
20
.run_checks
20
.run_checks
@@ -1,20 +0,0 @@
|
||||
# vim: ft=dosini
|
||||
|
||||
[DEFAULT]
|
||||
targets=qutebrowser,scripts
|
||||
|
||||
[pep257]
|
||||
# D102: Docstring missing, will be handled by others
|
||||
# D209: Blank line before closing """ (removed from PEP257)
|
||||
# D402: First line should not be function's signature (false-positives)
|
||||
disable=D102,D209,D402
|
||||
exclude=test_.*
|
||||
|
||||
[pylint]
|
||||
args=--output-format=colorized,--reports=no,--rcfile=.pylintrc
|
||||
plugins=config,crlf,modeline,settrace,openencoding
|
||||
exclude=resources.py
|
||||
|
||||
[flake8]
|
||||
args=--config=.flake8
|
||||
exclude=resources.py
|
||||
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
|
||||
417
CHANGELOG.asciidoc
Normal file
417
CHANGELOG.asciidoc
Normal file
@@ -0,0 +1,417 @@
|
||||
Change Log
|
||||
===========
|
||||
|
||||
// http://keepachangelog.com/
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
This project adheres to http://semver.org/[Semantic Versioning].
|
||||
|
||||
// tags:
|
||||
// `Added` for new features.
|
||||
// `Changed` for changes in existing functionality.
|
||||
// `Deprecated` for once-stable features removed in upcoming releases.
|
||||
// `Removed` for deprecated features removed in this release.
|
||||
// `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]
|
||||
-----------------------------------------------------------------------
|
||||
|
||||
Added
|
||||
~~~~~
|
||||
|
||||
- Session support
|
||||
* new command `:session-load` to load a session.
|
||||
* new command `:session-save` to save a session.
|
||||
* new command `:session-delete` to delete a session.
|
||||
* new setting `general -> save-session` to always save the session on quit.
|
||||
* new setting `general -> session-default-name` to configure the session name to use if none is given.
|
||||
* new argument `-r`/`--restore` to specify a session to load.
|
||||
* new argument `-R`/`--override-restore` to not load a session even if one was saved.
|
||||
- New commands to manage downloads:
|
||||
* `:download` to download a URL or the current page.
|
||||
* `:download-cancel` to cancel a download.
|
||||
* `:download-delete` to delete a download from disk.
|
||||
* `:download-open` to open a finished download.
|
||||
* `:download-remove` to remove a download from the list. `:download-remove --all` or the new 'cd' keybinding can be used to clear all finished downloads.
|
||||
- History completion
|
||||
* New option `completion -> timestamp-format` to set the format used to display the history timestamps.
|
||||
* New option `completion -> web-history-max-items` to configure how many history items to show in the completion.
|
||||
* The option `completion -> history-length` for the command history got renamed to `cmd-history-max-items`.
|
||||
- Better save logic for the config/state:
|
||||
* Only save files if modified (e.g. don't overwrite the config if it was edited outside of qutebrowser and nothing was changed in qutebrowser).
|
||||
* Save things (cookies, config, quickmarks, ...) periodically all 15 seconds (time can be changed with the `general -> auto-save-interval` option).
|
||||
- Opera-like mouse rocker gestures
|
||||
* New option `input -> rocker-gestures`. When turned on, the history can be navigated back/forward by holding a mouse button and pressing the other one.
|
||||
- New `-f` option for `:reload` to reload and bypass the cache.
|
||||
- Pass more information (`QUTE_MODE`, `QUTE_SELECTED_TEXT`, `QUTE_SELECTED_HTML`, `QUTE_USER_AGENT`, `QUTE_HTML`, `QUTE_TEXT`) to userscripts.
|
||||
- New `--userscript` option to `:spawn` (which deprecates `:run-userscript`).
|
||||
- Ability to toggle a value to `:set` by appending a `!` to the value.
|
||||
- New options to hide the tab-/statusbar:
|
||||
* `tabs -> hide-always` for the tabbar
|
||||
* `ui -> hide-statusbar` for the statusbar
|
||||
- New options to configure how the tab/window titles should look:
|
||||
* `tabs -> title-format` for the tabbar
|
||||
* `ui -> window-title-format` for the window title
|
||||
- HTML5 Geolocation/Notification support:
|
||||
* New option `content -> geolocation` to permanently turn the geolocation off.
|
||||
* New option `content -> notifications` to permanently turn notifications off.
|
||||
- New options to disable javascript prompts/alerts:
|
||||
* `content -> ignore-javascript-prompt` to turn off prompts.
|
||||
* `content -> ignore-javascript-alerts` to turn off alerts.
|
||||
- Two new options to customize the behavior of hints:
|
||||
* `hints -> min-chars` to set minimum number of chars in hints.
|
||||
* `hints -> scatter` which when turned off distributes the hints sequentially (like dwb) instead of scattering their positions (like Vimium).
|
||||
- Make it possible to use `:open -[twb]` without url.
|
||||
* New option `general -> default-page` to set the page to be opened when doing that.
|
||||
- New `input -> partial-timeout` option to clear partial keystrings.
|
||||
- New option `completion -> download-path-suggestion` to configure what to show in the completion for downloads.
|
||||
- Queue messages shown in unfocused windows and show them when the window is focused.
|
||||
* New option `ui -> message-unfocused` to disable this behavior.
|
||||
- New `--relaxed-config` argument which ignores unknown options.
|
||||
- New `:tab-detach` command to open the current tab in a new window.
|
||||
- Zooming via Ctrl-Mousewheel.
|
||||
* New option `input -> mouse-zoom-divider` to control how much the page is zoomed when rotating the wheel.
|
||||
- New option (`content -> host-blocking-enabled`) to enable/disable host blocking.
|
||||
- New values `tab-bg`/`tab-bg-silent` for `new-instance-open-target` to open a background tab.
|
||||
- New `ui -> downloads-position` setting to move the downloads to the bottom.
|
||||
- New `ui -> hide-mouse-cursor` option to hide the mouse cursor inside qutebrowser.
|
||||
- New argument `-s` for qutebrowser to set a temporary config option.
|
||||
- New argument `-p` for the `:set` command to print the new value.
|
||||
- New `--rapid` option to `:hint`. The `rapid`/`rapid-win` targets are now deprecated, and `--rapid` can be used as well with the targets run/hover/userscript/spawn as well.
|
||||
- New `-f` argument to `:bind` to overwrite the old binding.
|
||||
- New `--qt-name` argument to qutebrowser which is passed to Qt to set `WM_CLASS`.
|
||||
- Alternating row colors in completion. This adds a new `colors -> completion.alternate-bg` option.
|
||||
|
||||
Changed
|
||||
~~~~~~~
|
||||
|
||||
- Ignore quotes with maxsplit-commands (`:open`, `:quickmark-load`, etc.) and don't quote arguments for those commands in the completions. This also means some commands needed adjustments:
|
||||
* Clear search when `:search` without arguments is given. (`:search ""` will now search for the literal text `""`)
|
||||
* Add `-s`/`--space` argument to `:set-cmd-text` (as `:set-cmd-text "foo "` will now set the literal text `"foo "`)
|
||||
- Ignore `;;` for splitting with some commands like `:bind`.
|
||||
- Add unbound (new) default keybindings to config. This also adds a new `<unbound>` special command.
|
||||
* To unbind a command keybinding without binding it to a new key, you now have to bind it to `<unbound>` or it'll be readded automatically.
|
||||
- If an SSL error is raised multiple times with the same error/certificate/host/scheme/port, the user is only asked once.
|
||||
- Jump to last instead of first item when pressing Shift-Tab the first time in the completion.
|
||||
- Add a fullscreen keybinding.
|
||||
- Add a `:search` command in addition to `/foo` so it's more visible and can be used from scripts.
|
||||
- Various improvements to documentation, logging, and the crash reporter.
|
||||
- Expand `~` to the users home directory with `:run-userscript`.
|
||||
- Improve the userscript runner on Linux/OS X by using `QSocketNotifier`.
|
||||
- Add luakit-like `gt`/`gT` keybindings to cycle through tabs.
|
||||
- Show default value for config values in the completion.
|
||||
- Clone tab icon, tab text and zoom level when cloning tabs.
|
||||
- Don't open relative file paths with `:open`, only with commandline arguments.
|
||||
- Expand environment variables in config settings which take a file path.
|
||||
- Add a list of common user agents to the user agent setting completion.
|
||||
- Move cursor to end of textboxes when hinting.
|
||||
- Don't start searches on invalid URLs for quickmarks/startpage.
|
||||
- Various performance improvements for the completion.
|
||||
- Always open URLs given as argument in the foreground.
|
||||
- Improve various error messages.
|
||||
- Add `startpage`/`default-page` values to `tabs -> last-close`.
|
||||
- Various improvements to `:restart` - it should be more robust now and uses sessions so all state (focused tab, scroll position, etc.) gets remembered.
|
||||
- Add tab index display to the statusbar.
|
||||
- Keep progress bar height fixed when the statusbar is multiline.
|
||||
- Many improvements to tests and related infrastructure:
|
||||
* `init_venv.py` and `run_checks.py` have been replaced by http://tox.readthedocs.org/[tox]. Install tox and run `tox -e mkvenv` instead.
|
||||
* The tests now use http://pytest.org/[pytest]
|
||||
* Many new tests added
|
||||
* Mac Mini buildbot to run the tests on OS X.
|
||||
* Coverage recording via http://nedbatchelder.com/code/coverage/[coverage.py].
|
||||
* New `--pdb-postmortem argument` to drop into the pdb debugger on exceptions.
|
||||
* Use https://github.com/ionelmc/python-hunter[hunter] for line tracing instead of a selfmade solution.
|
||||
|
||||
Deprecated
|
||||
~~~~~~~~~~
|
||||
|
||||
- The `:run-userscript` command - use `:spawn --userscript` instead.
|
||||
- The `rapid` and `rapid-win` targets for `:hint` - use the `--rapid` argument to `:hint` instead.
|
||||
- The `:cancel-download` command - use `:download-cancel` instead.
|
||||
- The `:download-page` command - use `:download` instead.
|
||||
|
||||
Removed
|
||||
~~~~~~~
|
||||
|
||||
- `init_venv.py` and `run_checks.py` have been replaced by http://tox.readthedocs.org/[tox]. Install tox and run `tox -e mkvenv` instead..
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
|
||||
- Fix for cache never being used.
|
||||
- Fixed handling of key release events (e.g. for javascript) when holding a key and pressing a second one.
|
||||
- Fix handling of commands using `;;` at various places (key config, command parser, `:bind`)
|
||||
- Fix splitting of flags with arguments (`:bind -m`/`--mode`).
|
||||
- Fix bindings of special keys with lower-case modifiers (e.g. `<ctrl-x>`)
|
||||
- Fix for weird search highlights when changing tabs while search is active.
|
||||
- Fix starting with `-c ""`.
|
||||
- Fix removing of partial downloads when a download is cancelled via context menu.
|
||||
- Fix retrying of downloads which were started in a now closed tab.
|
||||
- Highlight text case-insensitively in completion.
|
||||
- Scroll completion to top when showing it.
|
||||
- Handle unencodable file paths in config types correctly.
|
||||
- Fix for crash when executing a delayed command (because of a shadowed keybinding) and then unfocusing the window.
|
||||
- Fix for crash when hinting on a page which doesn't have an URL yet.
|
||||
- Fix exception when using `:set-cmd-text` with an empty argument.
|
||||
- Add a timeout to pastebin HTTP replies.
|
||||
- Various other fixes for small/rare bugs.
|
||||
|
||||
https://github.com/The-Compiler/qutebrowser/releases/tag/v0.1.4[v0.1.4]
|
||||
-----------------------------------------------------------------------
|
||||
|
||||
Changed
|
||||
~~~~~~~
|
||||
|
||||
* The Windows builds come with Qt 5.4.1 which has some https://lists.schokokeks.org/pipermail/qutebrowser/2015-March/000054.html[related bugfixes].
|
||||
* Improvements to CPU usage when idle.
|
||||
* Ensure there's no size for `font-family` settings.
|
||||
* Handle URLs with double-colon as search strings.
|
||||
* Adjust prompt size hint based on content.
|
||||
* Refactor websettings and save/restore defaults.
|
||||
* Various small improvements to logging.
|
||||
* Various improvements for hinting.
|
||||
* Improve parsing of `faulthandler` logs.
|
||||
|
||||
Removed
|
||||
~~~~~~~
|
||||
|
||||
* Remove default search engines.
|
||||
* Remove debug console completing completely.
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
|
||||
* Ignore RuntimeError in `mouserelease_insertmode`.
|
||||
* Hide Qt warning when aborting download reply.
|
||||
* Hide "Error while shutting down tabs" message.
|
||||
* Clear open target in `acceptNavigationRequest`.
|
||||
* Fix handling of signals with deleted tabs.
|
||||
* Restore `sys.std*` in `utils.fake_io` on exceptions.
|
||||
* Allow font names with integers in them.
|
||||
* Fix `QIODevice` warnings when closing tabs.
|
||||
* Set the `QSettings` path to a config-subdirectory.
|
||||
* Add workaround for adblock-message without window.
|
||||
* Fix searching for terms starting with a slash.
|
||||
* Ignore tab key presses if they'd switch focus.
|
||||
|
||||
Security
|
||||
~~~~~~~~
|
||||
|
||||
* Stop the icon database from being created when private-browsing is set to true.
|
||||
* Disable insecure SSL ciphers.
|
||||
|
||||
https://github.com/The-Compiler/qutebrowser/releases/tag/v0.1.3[v0.1.3]
|
||||
-----------------------------------------------------------------------
|
||||
|
||||
Changed
|
||||
~~~~~~~
|
||||
|
||||
* Various small logging improvements.
|
||||
* Don't open relative files in `fuzzy_url` with `:open`
|
||||
* Various crashdialog improvements.
|
||||
* Hide adblocked iframes.
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
|
||||
* Handle shutdown of page with prompt correctly.
|
||||
* fuzzy_url: handle invalid URLs with autosearch off
|
||||
* Handle explicit searches with `auto-search=false`.
|
||||
* Abort download override question on error/cancel.
|
||||
* Set a higher z-index for hint labels.
|
||||
* Close contextmenu when closing tab to avoid crash.
|
||||
* Fix statusbar quickly popping up as window.
|
||||
* Clean up `NetworkManager` after downloads finished.
|
||||
* Fix restoring of cmd widget after an error.
|
||||
* Fix retrying of downloads after the tab is closed.
|
||||
* Fix `check_libraries()` output for Arch Linux.
|
||||
* Handle all `IPCErrors` properly.
|
||||
* Handle another `webelem.IsNullError` with hints.
|
||||
* Handle `UnicodeDecodeError` when reading configs.
|
||||
|
||||
Security
|
||||
~~~~~~~~
|
||||
|
||||
* Fix for HTTP passwords accidentally being written to debug log.
|
||||
|
||||
https://github.com/The-Compiler/qutebrowser/releases/tag/v0.1.2[v0.1.2]
|
||||
-----------------------------------------------------------------------
|
||||
|
||||
Changed
|
||||
~~~~~~~
|
||||
|
||||
* Uncheck sending of debug log by default when private browsing is on.
|
||||
* Add SSL info to version info.
|
||||
|
||||
Removed
|
||||
~~~~~~~
|
||||
|
||||
* Remove hosts-file.net from blocker default lists.
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
|
||||
* Fix rare exception when a key is pressed shorly after opening a window
|
||||
* Fix exception with certain invalid URLs like `http:foo:0`
|
||||
* Work around Qt bug which renders checkboxes on OS X unusable
|
||||
* Fix exception when a local files can't be read in `:adblock-update`
|
||||
* Hide 2 more Qt warnings.
|
||||
* Add `!important` to hint CSS so websites don't override the hint look
|
||||
* Make `init_venv.py` work with multiple sip `.so` files.
|
||||
* Fix splitting with certain commands with an empty argument
|
||||
* Fix uppercase hints.
|
||||
* Fix segfaults if another page is loaded while a prompt is open
|
||||
* Fix exception with invalid `ShellCommand` config values.
|
||||
* Replace unencodable chars
|
||||
* Fix user-stylesheet setting with an empty value.
|
||||
|
||||
|
||||
https://github.com/The-Compiler/qutebrowser/releases/tag/v0.1.1[v0.1.1]
|
||||
-----------------------------------------------------------------------
|
||||
|
||||
Added
|
||||
~~~~~
|
||||
|
||||
* Set window icon and add a qutebrowser.ico file for Windows.
|
||||
* Ask the user when downloading to an already existing file.
|
||||
* Add a `network -> proxy-dns-requests` option.
|
||||
* Add "Remove finished" to the download context menu
|
||||
* Open and remove clicked downloads.
|
||||
|
||||
Changes
|
||||
~~~~~~~
|
||||
|
||||
* Windows releases are now built with Qt 5.4 which brings many improvements and bugfixes.
|
||||
* Add a troubleshooting section to the FAQ.
|
||||
* Display IPC errors to the user.
|
||||
* Rewrite keymode handling to use only one mode which also fixes various bugs.
|
||||
* Save version to state config.
|
||||
* Set zoom to default instead of 100% with `:zoom`/`=`.
|
||||
* Adjust page zoom if default zoom changed.
|
||||
* Force tabs to be focused on `:undo`.
|
||||
* Replace manual installation instructions on OS X with homebrew/macports.
|
||||
* Allow min-/maximizing of print preview on Windows.
|
||||
* Various documentation improvements.
|
||||
* Various other small improvements and cleanups.
|
||||
|
||||
Removed
|
||||
~~~~~~~
|
||||
|
||||
* Clean up and temporarily disable alias completion.
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
|
||||
* Fix setting of `QWebSettings` (e.g. web fonts) with empty strings.
|
||||
* Re-focus web view when leaving prompt/yesno mode.
|
||||
* Handle `:restart` correctly with Python eggs.
|
||||
* Handle an invalid cwd properly.
|
||||
* Fix popping of a dead question in prompter.
|
||||
* Fix `AttributeError` on config changes on Ubuntu.
|
||||
* Don't treat things like "31c3" as IP address.
|
||||
* Handle category being `None` in Qt message handler.
|
||||
* Force-include pygments in `freeze.py`.
|
||||
* Fix scroll percentage not updating on some pages like twitter.
|
||||
* Encode `Content-Disposition` header name properly.
|
||||
* Fix item sorting in `NeighborList`.
|
||||
* Handle data being `None` in download read timer.
|
||||
* Stop download read timer when reply has finished.
|
||||
* Fix handling of small/big `fuzzyval`'s in `NeighborList`.
|
||||
* Fix crashes when entering invalid values in `qute:settings`.
|
||||
* Abort questions in `NetworkManager` when destroyed.
|
||||
* Fix height calculation of download view.
|
||||
* Always auto-remove adblock downloads when done.
|
||||
* Ensure the docs get included in `freeze.py`.
|
||||
* Fix crash with `:zoom`.
|
||||
|
||||
https://github.com/The-Compiler/qutebrowser/releases/tag/v0.1[v0.1]
|
||||
-------------------------------------------------------------------
|
||||
|
||||
Initial release.
|
||||
@@ -1,5 +1,5 @@
|
||||
qutebrowser HACKING
|
||||
===================
|
||||
Contributing to qutebrowser
|
||||
===========================
|
||||
The Compiler <mail@qutebrowser.org>
|
||||
:icons:
|
||||
:data-uri:
|
||||
@@ -37,8 +37,6 @@ If you want to find something useful to do, check the
|
||||
https://github.com/The-Compiler/qutebrowser/issues[issue tracker]. Some
|
||||
pointers:
|
||||
|
||||
* https://github.com/The-Compiler/qutebrowser/milestones/v0.1[Open issues for
|
||||
the v0.1 release]
|
||||
* https://github.com/The-Compiler/qutebrowser/labels/easy[Issues which should
|
||||
be easy to solve]
|
||||
* https://github.com/The-Compiler/qutebrowser/labels/not%20code[Issues which
|
||||
@@ -57,7 +55,7 @@ qutebrowser uses http://git-scm.com/[git] for its development. You can clone
|
||||
the repo like this:
|
||||
|
||||
----
|
||||
git clone git://the-compiler.org/qutebrowser
|
||||
git clone https://github.com/The-Compiler/qutebrowser.git
|
||||
----
|
||||
|
||||
If you don't know git, a http://git-scm.com/[git cheatsheet] might come in
|
||||
@@ -68,8 +66,13 @@ contributing, feel free to send normal patches instead, e.g. generated via
|
||||
Getting patches
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
After you finished your work and did `git commit`, you can get patches of your
|
||||
changes like this:
|
||||
The preferred way of submitting changes is to
|
||||
https://help.github.com/articles/fork-a-repo/[fork the repository] and to
|
||||
https://help.github.com/articles/creating-a-pull-request/[submit a pull
|
||||
request].
|
||||
|
||||
If you prefer to send a patch to the mailinglist, you can generate a patch
|
||||
based on your changes like this:
|
||||
|
||||
----
|
||||
git format-patch origin/master <1>
|
||||
@@ -83,32 +86,26 @@ Useful utilities
|
||||
Checkers
|
||||
~~~~~~~~
|
||||
|
||||
In the _scripts/_ subfolder, there is a `run_checks.py` script.
|
||||
qutebrowser uses http://tox.readthedocs.org/en/latest/[tox] to run its
|
||||
unittests and several linters/checkers.
|
||||
|
||||
It runs a bunch of static checks on all source files, using the following
|
||||
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/1.3.1[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]
|
||||
* A custom checker for the following things:
|
||||
* https://pypi.python.org/pypi/pyroma/[pyroma]
|
||||
* https://github.com/mgedmin/check-manifest[check-manifest]
|
||||
* `scripts/misc_checks.py` which checks for the following things:
|
||||
- untracked git files
|
||||
- VCS conflict markers
|
||||
|
||||
If you changed `setup.py` or `MANIFEST.in`, add the `--setup` argument to run
|
||||
the following additional checkers:
|
||||
|
||||
* https://pypi.python.org/pypi/pyroma/0.9.3[pyroma]
|
||||
* https://github.com/mgedmin/check-manifest[check-manifest]
|
||||
|
||||
It needs all the checkers to be installed and also needs
|
||||
https://pypi.python.org/pypi/colorama/[colorama].
|
||||
|
||||
Please make sure this script runs without any warnings on your new
|
||||
contributions. There's of course the possibility of false-positives, and the
|
||||
following techniques are useful to handle these:
|
||||
Please make sure the checks run without any warnings on your new contributions.
|
||||
There's of course the possibility of false-positives, and the following
|
||||
techniques are useful to handle these:
|
||||
|
||||
* Use `_foo` for unused parameters, with `foo` being a descriptive name. Using
|
||||
`_` is discouraged.
|
||||
@@ -156,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]
|
||||
@@ -214,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]
|
||||
@@ -241,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.
|
||||
|
||||
@@ -282,7 +276,7 @@ There are currently these object registries, also called 'scopes':
|
||||
`cookie-jar`, etc.)
|
||||
* The `tab` scope with objects which are per-tab (`hintmanager`, `webview`,
|
||||
etc.). Passing this scope to `objreg.get()` selects the object in the currently
|
||||
focused tab by default. A tab can be explicitely selected by passing
|
||||
focused tab by default. A tab can be explicitly selected by passing
|
||||
+tab=_tab-id_, window=_win-id_+ to it.
|
||||
|
||||
A new object can be registered by using
|
||||
@@ -298,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
|
||||
~~~~~~~
|
||||
@@ -377,7 +371,7 @@ The types of the function arguments are inferred based on their default values,
|
||||
e.g. an argument `foo=True` will be converted to a flag `-f`/`--foo` in
|
||||
qutebrowser's commandline.
|
||||
|
||||
This behaviour can be overridden using Python's
|
||||
This behavior can be overridden using Python's
|
||||
http://legacy.python.org/dev/peps/pep-3107/[function annotations]. The
|
||||
annotation should always be a `dict`, like this:
|
||||
|
||||
@@ -401,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
|
||||
~~~~~~~~~~~~~
|
||||
@@ -436,6 +429,30 @@ displaying it to the user.
|
||||
`QUrl` and take appropriate action if not. Note the URL of the current page
|
||||
always could be an invalid QUrl (if nothing is loaded yet).
|
||||
|
||||
Running valgrind on QtWebKit
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
If you want to run qutebrowser (and thus QtWebKit) with
|
||||
http://valgrind.org/[valgrind], you'll need to pass `--smc-check=all` to it or
|
||||
recompile QtWebKit with the Javascript JIT disabled.
|
||||
|
||||
This is needed so valgrind handles self-modifying code correctly:
|
||||
|
||||
[quote]
|
||||
____
|
||||
This option controls Valgrind's detection of self-modifying code. If no
|
||||
checking is done, if a program executes some code, then overwrites it with new
|
||||
code, and executes the new code, Valgrind will continue to execute the
|
||||
translations it made for the old code. This will likely lead to incorrect
|
||||
behavior and/or crashes.
|
||||
|
||||
...
|
||||
|
||||
Note that the default option will catch the vast majority of cases. The main
|
||||
case it will not catch is programs such as JIT compilers that dynamically
|
||||
generate code and subsequently overwrite part or all of it. Running with all
|
||||
will slow Valgrind down noticeably.
|
||||
____
|
||||
|
||||
Style conventions
|
||||
-----------------
|
||||
@@ -521,18 +538,15 @@ 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.
|
||||
* Build developer packages.
|
||||
* Build non-developer symbol packages.
|
||||
* Upload symbols patch to http://www.qutebrowser.org/qt-symbols.patch
|
||||
* Upload symbols packages to http://www.qutebrowser.org/qt-symbols-pkg/
|
||||
* Update own PKGBUILDs based on upstream Archlinux updates and rebuild.
|
||||
* Update recommended Qt version in `README`
|
||||
* Update OS X instructions in `README`
|
||||
* Make sure Gentoo instructions are up to date.
|
||||
* Grep for `WORKAROUND` in the code and test if fixed stuff works without the
|
||||
workaround.
|
||||
* Check relevant
|
||||
https://github.com/The-Compiler/qutebrowser/issues?q=is%3Aopen+is%3Aissue+label%3Aqt[qutebrowser
|
||||
bugs] and check if they're fixed.
|
||||
|
||||
qutebrowser release
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
@@ -546,17 +560,15 @@ qutebrowser release
|
||||
* Test an upgrade from the previous version (no manual intervention).
|
||||
* Test an upgrade from the first version (no manual intervention).
|
||||
|
||||
* Create annotated git tag (`git tag -s "v0.1" -m "Release v0.1"`)
|
||||
* Create git branch `v0.1.x`
|
||||
* Push including `--tags`
|
||||
* Create annotated git tag (`git tag -s "v0.X.Y" -m "Release v0.X.Y"`)
|
||||
* If it's a new minor, create git branch `v0.X.x`
|
||||
* `git push`; `git push "v0.X.Y"`
|
||||
* Create release on github
|
||||
* Mark the milestone at https://github.com/The-Compiler/qutebrowser/milestones
|
||||
as closed.
|
||||
|
||||
* Create standalone Windows package (32/64bit) in Windows VM
|
||||
* Upload to PyPI: `python setup.py register sdist upload --sign`
|
||||
* Maybe upload to http://qt-apps.org/
|
||||
* Upload to webpage with checksum/GPG (when/if it exists)
|
||||
* Upload to qutebrowser.org with checksum/GPG
|
||||
|
||||
* Announce to qutebrowser mailinglist
|
||||
* Maybe annouce at other places?
|
||||
@@ -4,18 +4,18 @@ 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
|
||||
keybindings are similar to dwb.
|
||||
key bindings are similar to dwb.
|
||||
|
||||
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.]
|
||||
@@ -75,6 +74,15 @@ Is there an adblocker?::
|
||||
usage], so implementing it properly might take some time and won't be done
|
||||
for v0.1 if at all.
|
||||
|
||||
How do I play Youtube videos with mpv?::
|
||||
You can easily add a key binding to play youtube videos inside a real video
|
||||
player - optionally even with hinting for links:
|
||||
+
|
||||
----
|
||||
:bind x spawn mpv {url}
|
||||
:bind ;x hint links spawn mpv {hint-url}
|
||||
----
|
||||
|
||||
== Troubleshooting
|
||||
|
||||
Configuration not saved after modifying config.::
|
||||
@@ -103,10 +111,17 @@ 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
|
||||
70 important bugs] have been fixed in QtWebKit. For Debian Jessie (using Qt 5.3.2)
|
||||
it's still
|
||||
https://bugreports.qt.io/browse/QTBUG-42417?jql=component%20%3D%20WebKit%20and%20resolution%20%3D%20Done%20and%20fixVersion%20in%20(5.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)[nearly
|
||||
20 important bugs].
|
||||
|
||||
My issue is not listed.::
|
||||
If you experience any segfaults or crashes, you can report the issue in
|
||||
@@ -12,8 +12,41 @@ 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`:
|
||||
|
||||
----
|
||||
# apt-get install python3-pyqt5 python3-pyqt5.qtwebkit python-virtualenv
|
||||
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
|
||||
----
|
||||
|
||||
To generate the documentation for the `:help` command, when using the git
|
||||
@@ -21,27 +54,29 @@ repository (rather than a release):
|
||||
|
||||
----
|
||||
# apt-get install asciidoc
|
||||
# python3 scripts/asciidoc2html.py
|
||||
$ python3 scripts/asciidoc2html.py
|
||||
----
|
||||
|
||||
Then run the supplied script to run qutebrowser inside a
|
||||
https://virtualenv.pypa.io/en/latest/virtualenv.html[virtualenv]:
|
||||
Then run tox like this to set up a
|
||||
https://docs.python.org/3/library/venv.html[virtual environment]:
|
||||
|
||||
----
|
||||
# python3 scripts/init_venv.py
|
||||
$ tox -e mkvenv
|
||||
----
|
||||
|
||||
This installs all needed Python dependencies in a `.venv` subfolder. The
|
||||
system-wide Qt5/PyQt5 installations are symlinked into the virtualenv.
|
||||
system-wide Qt5/PyQt5 installations are symlinked into the virtual environment.
|
||||
|
||||
You can then create a simple wrapper script to start qutebrowser somewhere in
|
||||
your `$PATH` (e.g. `/usr/local/bin/qutebrowser` or `~/bin/qutebrowser`):
|
||||
|
||||
----
|
||||
#!/bin/bash
|
||||
~/path/to/qutebrowser/.venv/bin/python3 -m qutebrowser
|
||||
~/path/to/qutebrowser/.venv/bin/python3 -m qutebrowser "$@"
|
||||
----
|
||||
|
||||
Please also read about <<updating,updating qutebrowser with tox>>.
|
||||
|
||||
On Archlinux
|
||||
------------
|
||||
|
||||
@@ -49,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`.
|
||||
@@ -68,7 +112,6 @@ https://github.com/posativ/qutebrowser-overlay[GitHub]. To install it, add the
|
||||
overlay with http://wiki.gentoo.org/wiki/Layman[layman]:
|
||||
|
||||
----
|
||||
# wget https://raw.githubusercontent.com/posativ/qutebrowser-overlay/master/overlays.xml -O /etc/layman/overlays/qutebrowser.xml
|
||||
# layman -a qutebrowser
|
||||
----
|
||||
|
||||
@@ -81,6 +124,16 @@ in your `PYTHON_TARGETS` (`/etc/portage/make.conf`) and rebuild your system
|
||||
# emerge -av qutebrowser
|
||||
----
|
||||
|
||||
On Void Linux
|
||||
-------------
|
||||
|
||||
qutebrowser is available in the official repositories and can be installed
|
||||
with:
|
||||
|
||||
----
|
||||
# xbps-install qutebrowser
|
||||
----
|
||||
|
||||
On Windows
|
||||
----------
|
||||
|
||||
@@ -93,19 +146,24 @@ Python 3 (be sure to install pip).
|
||||
* Use the installer from
|
||||
http://www.riverbankcomputing.com/software/pyqt/download5[Riverbank computing]
|
||||
to get Qt and PyQt5.
|
||||
* Run `pip install virtualenv` or
|
||||
http://www.lfd.uci.edu/~gohlke/pythonlibs/#virtualenv[the installer from here]
|
||||
to install virtualenv.
|
||||
|
||||
Then run the supplied script to run qutebrowser inside a
|
||||
https://virtualenv.pypa.io/en/latest/virtualenv.html[virtualenv]:
|
||||
* Install https://testrun.org/tox/latest/index.html[tox] via
|
||||
https://pip.pypa.io/en/latest/[pip]:
|
||||
|
||||
----
|
||||
# python3 scripts/init_venv.py
|
||||
$ pip install tox
|
||||
----
|
||||
|
||||
Then run tox like this to set up a
|
||||
https://docs.python.org/3/library/venv.html[virtual environment]:
|
||||
|
||||
----
|
||||
$ tox -e mkvenv
|
||||
----
|
||||
|
||||
This installs all needed Python dependencies in a `.venv` subfolder. The
|
||||
system-wide Qt5/PyQt5 installations are used in the virtualenv.
|
||||
system-wide Qt5/PyQt5 installations are used in the virtual environment.
|
||||
|
||||
Please also read about <<updating,updating qutebrowser with tox>>.
|
||||
|
||||
On OS X
|
||||
-------
|
||||
@@ -140,3 +198,17 @@ standard location for your distro (`/usr/share/applications` and
|
||||
|
||||
The normal `setup.py install` doesn't install these files, so you'll have to do
|
||||
it as part of the packaging process.
|
||||
|
||||
[[updating]]
|
||||
Updating qutebrowser with tox
|
||||
-----------------------------
|
||||
|
||||
When you updated your local copy of the code (e.g. by pulling the git repo, or
|
||||
extracting a new version), the virtualenv should automatically use the updated
|
||||
code. However, if dependencies got added, this won't be reflected in the
|
||||
virtualenv. Thus it's recommended to run the following command to recreate the
|
||||
virtualenv:
|
||||
|
||||
----
|
||||
$ tox -r -e mkvenv
|
||||
----
|
||||
44
MANIFEST.in
44
MANIFEST.in
@@ -1,17 +1,39 @@
|
||||
global-exclude __pycache__ *.pyc *.pyo
|
||||
|
||||
recursive-include qutebrowser *.py
|
||||
recursive-include qutebrowser/html *.html
|
||||
recursive-include qutebrowser/test *.py
|
||||
recursive-include icons *
|
||||
include qutebrowser/test/testfile
|
||||
recursive-include qutebrowser/javascript *.js
|
||||
graft icons
|
||||
graft doc/img
|
||||
graft misc
|
||||
graft scripts
|
||||
include qutebrowser/utils/testfile
|
||||
include qutebrowser/git-commit-id
|
||||
include COPYING doc/* README.asciidoc
|
||||
include COPYING doc/* README.asciidoc CONTRIBUTING.asciidoc FAQ.asciidoc INSTALL.asciidoc CHANGELOG.asciidoc
|
||||
include qutebrowser.desktop
|
||||
include requirements.txt
|
||||
include tox.ini
|
||||
include qutebrowser.py
|
||||
|
||||
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 scripts/run_checks.py
|
||||
exclude scripts/cleanup.py
|
||||
exclude scripts/minimal_webkit_testbrowser.py
|
||||
exclude scripts/run_profile.py
|
||||
exclude scripts/generate_authors.sh
|
||||
exclude .flake8
|
||||
exclude .pylintrc
|
||||
exclude doc/notes
|
||||
prune pkg
|
||||
recursive-exclude doc *.asciidoc
|
||||
include doc/qutebrowser.1.asciidoc
|
||||
prune tests
|
||||
exclude qutebrowser.rcc
|
||||
exclude .coveragerc
|
||||
exclude .pylintrc
|
||||
exclude .eslintrc
|
||||
exclude doc/help
|
||||
exclude .appveyor.yml
|
||||
exclude .travis.yml
|
||||
exclude misc/appveyor_install.py
|
||||
|
||||
@@ -6,8 +6,13 @@
|
||||
qutebrowser
|
||||
===========
|
||||
|
||||
image:icons/qutebrowser-64x64.png[] _A keyboard-driven, vim-like browser based
|
||||
on PyQt5 and QtWebKit._
|
||||
image:icons/qutebrowser-64x64.png[qutebrowser logo] *A keyboard-driven, vim-like browser based on PyQt5 and QtWebKit.*
|
||||
|
||||
image:https://img.shields.io/pypi/l/qutebrowser.svg?style=flat["license badge",link="https://github.com/The-Compiler/qutebrowser/blob/master/COPYING"]
|
||||
image:https://img.shields.io/pypi/v/qutebrowser.svg?style=flat["version badge",link="https://pypi.python.org/pypi/qutebrowser/"]
|
||||
image:https://img.shields.io/github/issues/The-Compiler/qutebrowser.svg?style=flat["issues badge",link="https://github.com/The-Compiler/qutebrowser/issues"]
|
||||
image:https://requires.io/github/The-Compiler/qutebrowser/requirements.svg?branch=master["requirements badge",link="https://requires.io/github/The-Compiler/qutebrowser/requirements/?branch=master"]
|
||||
image:http://qutebrowser.org:8010/png?builder=archlinux["build badge",link="http://qutebrowser.org:8010/waterfall"]
|
||||
|
||||
qutebrowser is a keyboard-focused browser with with a minimal GUI. It's based
|
||||
on Python, PyQt5 and QtWebKit and free software, licensed under the GPL.
|
||||
@@ -17,10 +22,10 @@ It was inspired by other browsers/addons like dwb and Vimperator/Pentadactyl.
|
||||
Screenshots
|
||||
-----------
|
||||
|
||||
image:doc/img/main.png[width=300,link="doc/img/main.png"]
|
||||
image:doc/img/downloads.png[width=300,link="doc/img/downloads.png"]
|
||||
image:doc/img/completion.png[width=300,link="doc/img/completion.png"]
|
||||
image:doc/img/hints.png[width=300,link="doc/img/hints.png"]
|
||||
image:doc/img/main.png["screenshot 1",width=300,link="doc/img/main.png"]
|
||||
image:doc/img/downloads.png["screenshot 2",width=300,link="doc/img/downloads.png"]
|
||||
image:doc/img/completion.png["screenshot 3",width=300,link="doc/img/completion.png"]
|
||||
image:doc/img/hints.png["screenshot 4",width=300,link="doc/img/hints.png"]
|
||||
|
||||
Downloads
|
||||
---------
|
||||
@@ -29,7 +34,7 @@ See the https://github.com/The-Compiler/qutebrowser/releases[github releases
|
||||
page] for available downloads (currently a source archive, and standalone
|
||||
packages as well as MSI installers for Windows).
|
||||
|
||||
See link:doc/INSTALL.asciidoc[INSTALL] for detailed instructions on how to get
|
||||
See link:INSTALL.asciidoc[INSTALL] for detailed instructions on how to get
|
||||
qutebrowser running for various platforms.
|
||||
|
||||
Documentation
|
||||
@@ -38,13 +43,15 @@ Documentation
|
||||
In addition to the topics mentioned in this README, the following documents are
|
||||
available:
|
||||
|
||||
* A http://qutebrowser.org/img/cheatsheet-big.png[keybinding cheatsheet]: +
|
||||
image:http://qutebrowser.org/img/cheatsheet-small.png["qutebrowser keybinding cheatsheet",link="http://qutebrowser.org/img/cheatsheet-big.png"]
|
||||
* A http://qutebrowser.org/img/cheatsheet-big.png[key binding cheatsheet]: +
|
||||
image:http://qutebrowser.org/img/cheatsheet-small.png["qutebrowser key binding cheatsheet",link="http://qutebrowser.org/img/cheatsheet-big.png"]
|
||||
* link:doc/quickstart.asciidoc[Quick start guide]
|
||||
* link:doc/FAQ.asciidoc[Frequently asked questions]
|
||||
* link:doc/HACKING.asciidoc[HACKING]
|
||||
* link:doc/INSTALL.asciidoc[INSTALL]
|
||||
* link:FAQ.asciidoc[Frequently asked questions]
|
||||
* link:CONTRIBUTING.asciidoc[Contributing to qutebrowser]
|
||||
* link:INSTALL.asciidoc[INSTALL]
|
||||
* link:CHANGELOG.asciidoc[Change Log]
|
||||
* link:doc/stacktrace.asciidoc[Reporting segfaults]
|
||||
* link:doc/userscripts.asciidoc[How to write userscripts]
|
||||
|
||||
Getting help
|
||||
------------
|
||||
@@ -61,7 +68,8 @@ Contributions / Bugs
|
||||
--------------------
|
||||
|
||||
You want to contribute to qutebrowser? Awesome! Please read
|
||||
link:doc/HACKING.asciidoc[HACKING] for details and useful hints.
|
||||
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
|
||||
ways:
|
||||
@@ -81,14 +89,15 @@ 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 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.3.2 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]
|
||||
* http://pygments.org/[pygments]
|
||||
* http://pyyaml.org/wiki/PyYAML[PyYAML]
|
||||
|
||||
To generate the documentation for the `:help` command, when using the git
|
||||
repository (rather than a release), http://asciidoc.org/[asciidoc] is needed.
|
||||
@@ -99,8 +108,8 @@ console:
|
||||
* https://pypi.python.org/pypi/colorlog/[colorlog]
|
||||
* On Windows: https://pypi.python.org/pypi/colorama/[colorama]
|
||||
|
||||
See link:doc/INSTALL.asciidoc[INSTALL] for directions on how to install
|
||||
qutebrowser and its dependencies.
|
||||
See link:INSTALL.asciidoc[INSTALL] for directions on how to install qutebrowser
|
||||
and its dependencies.
|
||||
|
||||
Donating
|
||||
--------
|
||||
@@ -125,24 +134,49 @@ Contributors, sorted by the number of commits in descending order:
|
||||
|
||||
// QUTE_AUTHORS_START
|
||||
* Florian Bruhin
|
||||
* Bruno Oliveira
|
||||
* 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
|
||||
* Brian Jackson
|
||||
* Patric Schmitz
|
||||
* Martin Zimmermann
|
||||
* Error 800
|
||||
* Brian Jackson
|
||||
* sbinix
|
||||
* Tobias Patzl
|
||||
* Johannes Altmanninger
|
||||
* Samir Benmendil
|
||||
* Regina Hug
|
||||
* Mathias Fussenegger
|
||||
* Larry Hynes
|
||||
* Johannes Altmanninger
|
||||
* Joel Torstensson
|
||||
* Regina Hug
|
||||
* Peter Vilim
|
||||
* Fritz V155 Reichwald
|
||||
* Franz Fellner
|
||||
* error800
|
||||
* Thorsten Wißmann
|
||||
* Thiago Barroso Perrotta
|
||||
* Matthias Lisin
|
||||
* Helen Sherwood-Taylor
|
||||
* HalosGhost
|
||||
* Gregor Pohl
|
||||
* Eivind Uggedal
|
||||
* Andreas Fischer
|
||||
// QUTE_AUTHORS_END
|
||||
|
||||
The following people have contributed graphics:
|
||||
|
||||
* WOFall (icon)
|
||||
* regines (keybinding cheatsheet)
|
||||
* regines (key binding cheatsheet)
|
||||
|
||||
Thanks / Similiar projects
|
||||
--------------------------
|
||||
@@ -188,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.
|
||||
|
||||
@@ -8,15 +8,19 @@
|
||||
|<<adblock-update,adblock-update>>|Update the adblock block lists.
|
||||
|<<back,back>>|Go back in the history of the current tab.
|
||||
|<<bind,bind>>|Bind a key to a command.
|
||||
|<<cancel-download,cancel-download>>|Cancel the first/[count]th download.
|
||||
|<<close,close>>|Close the current window.
|
||||
|<<download,download>>|Download a given URL, given as string.
|
||||
|<<download-page,download-page>>|Download the current page.
|
||||
|<<download,download>>|Download a given URL, or current page if no URL given.
|
||||
|<<download-cancel,download-cancel>>|Cancel the last/[count]th download.
|
||||
|<<download-delete,download-delete>>|Delete the last/[count]th download from disk.
|
||||
|<<download-open,download-open>>|Open the last/[count]th download.
|
||||
|<<download-remove,download-remove>>|Remove the last/[count]th download from the list.
|
||||
|<<forward,forward>>|Go forward in the history of the current tab.
|
||||
|<<fullscreen,fullscreen>>|Toggle fullscreen mode.
|
||||
|<<help,help>>|Show help about a command or setting.
|
||||
|<<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.
|
||||
@@ -31,14 +35,18 @@
|
||||
|<<repeat,repeat>>|Repeat a given command.
|
||||
|<<report,report>>|Report a bug in qutebrowser.
|
||||
|<<restart,restart>>|Restart qutebrowser while keeping existing tabs open.
|
||||
|<<run-userscript,run-userscript>>|Run an userscript given as argument.
|
||||
|<<save,save>>|Save the config file.
|
||||
|<<save,save>>|Save configs and state.
|
||||
|<<search,search>>|Search for a text on the current page. With no text, clear results.
|
||||
|<<session-delete,session-delete>>|Delete a session.
|
||||
|<<session-load,session-load>>|Load a session.
|
||||
|<<session-save,session-save>>|Save a session.
|
||||
|<<set,set>>|Set an option.
|
||||
|<<set-cmd-text,set-cmd-text>>|Preset the statusbar to some text.
|
||||
|<<spawn,spawn>>|Spawn a command in a shell.
|
||||
|<<stop,stop>>|Stop loading in the current/[count]th tab.
|
||||
|<<tab-clone,tab-clone>>|Duplicate the current tab.
|
||||
|<<tab-close,tab-close>>|Close the current/[count]th tab.
|
||||
|<<tab-detach,tab-detach>>|Detach the current tab to its own window.
|
||||
|<<tab-focus,tab-focus>>|Select the tab given as argument/[count].
|
||||
|<<tab-move,tab-move>>|Move the current tab.
|
||||
|<<tab-next,tab-next>>|Switch to the next tab, or switch [count] tabs forward.
|
||||
@@ -47,6 +55,7 @@
|
||||
|<<unbind,unbind>>|Unbind a keychain.
|
||||
|<<undo,undo>>|Re-open a closed tab (optionally skipping [count] closed tabs).
|
||||
|<<view-source,view-source>>|Show the source of the current page.
|
||||
|<<wq,wq>>|Save open pages and quit.
|
||||
|<<yank,yank>>|Yank the current URL/title to the clipboard or primary selection.
|
||||
|<<zoom,zoom>>|Set the zoom level for the current tab.
|
||||
|<<zoom-in,zoom-in>>|Increase the zoom level for the current tab.
|
||||
@@ -72,7 +81,7 @@ How many pages to go back.
|
||||
|
||||
[[bind]]
|
||||
=== bind
|
||||
Syntax: +:bind [*--mode* 'MODE'] 'key' 'command'+
|
||||
Syntax: +:bind [*--mode* 'MODE'] [*--force*] 'key' 'command'+
|
||||
|
||||
Bind a key to a command.
|
||||
|
||||
@@ -83,13 +92,11 @@ Bind a key to a command.
|
||||
==== optional arguments
|
||||
* +*-m*+, +*--mode*+: A comma-separated list of modes to bind the key in (default: `normal`).
|
||||
|
||||
* +*-f*+, +*--force*+: Rebind the key if it is already bound.
|
||||
|
||||
[[cancel-download]]
|
||||
=== cancel-download
|
||||
Cancel the first/[count]th download.
|
||||
|
||||
==== count
|
||||
The index of the download to cancel.
|
||||
==== note
|
||||
* This command does not split arguments after the last argument and handles quotes literally.
|
||||
* With this command, +;;+ is interpreted literally instead of splitting off a second command.
|
||||
|
||||
[[close]]
|
||||
=== close
|
||||
@@ -97,17 +104,46 @@ Close the current window.
|
||||
|
||||
[[download]]
|
||||
=== download
|
||||
Syntax: +:download 'url' ['dest']+
|
||||
Syntax: +:download ['url'] ['dest']+
|
||||
|
||||
Download a given URL, given as string.
|
||||
Download a given URL, or current page if no URL given.
|
||||
|
||||
==== positional arguments
|
||||
* +'url'+: The URL to download
|
||||
* +'dest'+: The file path to write the download to to ask.
|
||||
* +'url'+: The URL to download. If not given, download the current page.
|
||||
* +'dest'+: The file path to write the download to, or not given to ask.
|
||||
|
||||
[[download-page]]
|
||||
=== download-page
|
||||
Download the current page.
|
||||
[[download-cancel]]
|
||||
=== download-cancel
|
||||
Cancel the last/[count]th download.
|
||||
|
||||
==== count
|
||||
The index of the download to cancel.
|
||||
|
||||
[[download-delete]]
|
||||
=== download-delete
|
||||
Delete the last/[count]th download from disk.
|
||||
|
||||
==== count
|
||||
The index of the download to cancel.
|
||||
|
||||
[[download-open]]
|
||||
=== download-open
|
||||
Open the last/[count]th download.
|
||||
|
||||
==== count
|
||||
The index of the download to cancel.
|
||||
|
||||
[[download-remove]]
|
||||
=== download-remove
|
||||
Syntax: +:download-remove [*--all*]+
|
||||
|
||||
Remove the last/[count]th download from the list.
|
||||
|
||||
==== optional arguments
|
||||
* +*-a*+, +*--all*+: If given removes all finished downloads.
|
||||
|
||||
==== count
|
||||
The index of the download to cancel.
|
||||
|
||||
[[forward]]
|
||||
=== forward
|
||||
@@ -123,6 +159,10 @@ Go forward in the history of the current tab.
|
||||
==== count
|
||||
How many pages to go forward.
|
||||
|
||||
[[fullscreen]]
|
||||
=== fullscreen
|
||||
Toggle fullscreen mode.
|
||||
|
||||
[[help]]
|
||||
=== help
|
||||
Syntax: +:help [*--tab*] [*--bg*] [*--window*] ['topic']+
|
||||
@@ -143,7 +183,7 @@ Show help about a command or setting.
|
||||
|
||||
[[hint]]
|
||||
=== hint
|
||||
Syntax: +:hint ['group'] ['target'] ['args' ['args' ...]]+
|
||||
Syntax: +:hint [*--rapid*] ['group'] ['target'] ['args' ['args' ...]]+
|
||||
|
||||
Start hinting.
|
||||
|
||||
@@ -159,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.
|
||||
@@ -168,9 +210,6 @@ Start hinting.
|
||||
- `run`: Run the argument as command.
|
||||
- `fill`: Fill the commandline with the command given as
|
||||
argument.
|
||||
- `rapid`: Open the link in a new tab and stay in hinting mode.
|
||||
- `rapid-win`: Open the link in a new window and stay in
|
||||
hinting mode.
|
||||
- `download`: Download the link.
|
||||
- `userscript`: Call an userscript with `$QUTE_URL` set to the
|
||||
link.
|
||||
@@ -190,6 +229,11 @@ Start hinting.
|
||||
- With `run`: Same as `fill`.
|
||||
|
||||
|
||||
==== optional arguments
|
||||
* +*-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]]
|
||||
=== home
|
||||
Open main startpage in current tab.
|
||||
@@ -198,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'+
|
||||
@@ -208,6 +268,10 @@ Execute a command after some time.
|
||||
* +'ms'+: How many milliseconds to wait.
|
||||
* +'command'+: The command to run, with optional args.
|
||||
|
||||
==== 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.
|
||||
|
||||
[[navigate]]
|
||||
=== navigate
|
||||
Syntax: +:navigate [*--tab*] [*--bg*] [*--window*] 'where'+
|
||||
@@ -235,7 +299,7 @@ This tries to automatically click on typical _Previous Page_ or _Next Page_ link
|
||||
|
||||
[[open]]
|
||||
=== open
|
||||
Syntax: +:open [*--bg*] [*--tab*] [*--window*] 'url'+
|
||||
Syntax: +:open [*--bg*] [*--tab*] [*--window*] ['url']+
|
||||
|
||||
Open a URL in the current/[count]th tab.
|
||||
|
||||
@@ -250,6 +314,10 @@ Open a URL in the current/[count]th tab.
|
||||
==== count
|
||||
The tab index to open the URL in.
|
||||
|
||||
==== 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.
|
||||
|
||||
[[paste]]
|
||||
=== paste
|
||||
Syntax: +:paste [*--sel*] [*--tab*] [*--bg*] [*--window*]+
|
||||
@@ -293,6 +361,10 @@ Delete a quickmark.
|
||||
==== positional arguments
|
||||
* +'name'+: The name of the quickmark to delete.
|
||||
|
||||
==== note
|
||||
* This command does not split arguments after the last argument and handles quotes literally.
|
||||
* With this command, +;;+ is interpreted literally instead of splitting off a second command.
|
||||
|
||||
[[quickmark-load]]
|
||||
=== quickmark-load
|
||||
Syntax: +:quickmark-load [*--tab*] [*--bg*] [*--window*] 'name'+
|
||||
@@ -307,6 +379,10 @@ Load a quickmark.
|
||||
* +*-b*+, +*--bg*+: Load the quickmark in a new background tab.
|
||||
* +*-w*+, +*--window*+: Load the quickmark in a new window.
|
||||
|
||||
==== note
|
||||
* This command does not split arguments after the last argument and handles quotes literally.
|
||||
* With this command, +;;+ is interpreted literally instead of splitting off a second command.
|
||||
|
||||
[[quickmark-save]]
|
||||
=== quickmark-save
|
||||
Save the current page as a quickmark.
|
||||
@@ -317,8 +393,13 @@ Quit qutebrowser.
|
||||
|
||||
[[reload]]
|
||||
=== reload
|
||||
Syntax: +:reload [*--force*]+
|
||||
|
||||
Reload the current/[count]th tab.
|
||||
|
||||
==== optional arguments
|
||||
* +*-f*+, +*--force*+: Bypass the page cache.
|
||||
|
||||
==== count
|
||||
The tab index to reload.
|
||||
|
||||
@@ -332,6 +413,10 @@ Repeat a given command.
|
||||
* +'times'+: How many times to repeat.
|
||||
* +'command'+: The command to run, with optional args.
|
||||
|
||||
==== 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.
|
||||
|
||||
[[report]]
|
||||
=== report
|
||||
Report a bug in qutebrowser.
|
||||
@@ -340,27 +425,82 @@ Report a bug in qutebrowser.
|
||||
=== restart
|
||||
Restart qutebrowser while keeping existing tabs open.
|
||||
|
||||
[[run-userscript]]
|
||||
=== run-userscript
|
||||
Syntax: +:run-userscript 'cmd' ['args' ['args' ...]]+
|
||||
|
||||
Run an userscript given as argument.
|
||||
|
||||
==== positional arguments
|
||||
* +'cmd'+: The userscript to run.
|
||||
* +'args'+: Arguments to pass to the userscript.
|
||||
|
||||
[[save]]
|
||||
=== save
|
||||
Save the config file.
|
||||
Syntax: +:save ['what' ['what' ...]]+
|
||||
|
||||
Save configs and state.
|
||||
|
||||
==== positional arguments
|
||||
* +'what'+: What to save (`config`/`key-config`/`cookies`/...). If not given, everything is saved.
|
||||
|
||||
|
||||
[[search]]
|
||||
=== search
|
||||
Syntax: +:search [*--reverse*] ['text']+
|
||||
|
||||
Search for a text on the current page. With no text, clear results.
|
||||
|
||||
==== positional arguments
|
||||
* +'text'+: The text to search for.
|
||||
|
||||
==== optional arguments
|
||||
* +*-r*+, +*--reverse*+: Reverse search direction.
|
||||
|
||||
==== 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.
|
||||
|
||||
[[session-delete]]
|
||||
=== session-delete
|
||||
Syntax: +:session-delete [*--force*] 'name'+
|
||||
|
||||
Delete a session.
|
||||
|
||||
==== positional arguments
|
||||
* +'name'+: The name of the session.
|
||||
|
||||
==== optional arguments
|
||||
* +*-f*+, +*--force*+: Force deleting internal sessions (starting with an underline).
|
||||
|
||||
|
||||
[[session-load]]
|
||||
=== session-load
|
||||
Syntax: +:session-load [*--clear*] [*--temp*] [*--force*] 'name'+
|
||||
|
||||
Load a session.
|
||||
|
||||
==== positional arguments
|
||||
* +'name'+: The name of the session.
|
||||
|
||||
==== optional arguments
|
||||
* +*-c*+, +*--clear*+: Close all existing windows.
|
||||
* +*-t*+, +*--temp*+: Don't set the current session for :session-save.
|
||||
* +*-f*+, +*--force*+: Force loading internal sessions (starting with an underline).
|
||||
|
||||
|
||||
[[session-save]]
|
||||
=== session-save
|
||||
Syntax: +:session-save [*--current*] [*--quiet*] [*--force*] ['name']+
|
||||
|
||||
Save a session.
|
||||
|
||||
==== positional arguments
|
||||
* +'name'+: The name of the session. If not given, the session configured in general -> session-default-name is saved.
|
||||
|
||||
|
||||
==== optional arguments
|
||||
* +*-c*+, +*--current*+: Save the current session instead of the default.
|
||||
* +*-q*+, +*--quiet*+: Don't show confirmation message.
|
||||
* +*-f*+, +*--force*+: Force saving internal sessions (starting with an underline).
|
||||
|
||||
[[set]]
|
||||
=== set
|
||||
Syntax: +:set [*--temp*] ['section'] ['option'] ['value']+
|
||||
Syntax: +:set [*--temp*] [*--print*] ['section'] ['option'] ['value']+
|
||||
|
||||
Set an option.
|
||||
|
||||
If the option name ends with '?', the value of the option is shown instead.
|
||||
If the option name ends with '?', the value of the option is shown instead. If the option name ends with '!' and it is a boolean value, toggle it.
|
||||
|
||||
==== positional arguments
|
||||
* +'section'+: The section where the option is in.
|
||||
@@ -369,26 +509,43 @@ If the option name ends with '?', the value of the option is shown instead.
|
||||
|
||||
==== optional arguments
|
||||
* +*-t*+, +*--temp*+: Set value temporarily.
|
||||
* +*-p*+, +*--print*+: Print the value after setting.
|
||||
|
||||
[[set-cmd-text]]
|
||||
=== set-cmd-text
|
||||
Syntax: +:set-cmd-text 'text'+
|
||||
Syntax: +:set-cmd-text [*--space*] 'text'+
|
||||
|
||||
Preset the statusbar to some text.
|
||||
|
||||
==== positional arguments
|
||||
* +'text'+: The commandline to set.
|
||||
|
||||
==== optional arguments
|
||||
* +*-s*+, +*--space*+: If given, a space is added to the end.
|
||||
|
||||
==== 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.
|
||||
|
||||
[[spawn]]
|
||||
=== spawn
|
||||
Syntax: +:spawn '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
|
||||
@@ -416,12 +573,16 @@ Close the current/[count]th tab.
|
||||
==== optional arguments
|
||||
* +*-l*+, +*--left*+: Force selecting the tab to the left of the current tab.
|
||||
* +*-r*+, +*--right*+: Force selecting the tab to the right of the current tab.
|
||||
* +*-o*+, +*--opposite*+: Force selecting the tab in the oppsite direction of what's configured in 'tabs->select-on-remove'.
|
||||
* +*-o*+, +*--opposite*+: Force selecting the tab in the opposite direction of what's configured in 'tabs->select-on-remove'.
|
||||
|
||||
|
||||
==== count
|
||||
The tab index to close
|
||||
|
||||
[[tab-detach]]
|
||||
=== tab-detach
|
||||
Detach the current tab to its own window.
|
||||
|
||||
[[tab-focus]]
|
||||
=== tab-focus
|
||||
Syntax: +:tab-focus ['index']+
|
||||
@@ -492,15 +653,25 @@ Re-open a closed tab (optionally skipping [count] closed tabs).
|
||||
=== view-source
|
||||
Show the source of the current page.
|
||||
|
||||
[[wq]]
|
||||
=== wq
|
||||
Syntax: +:wq ['name']+
|
||||
|
||||
Save open pages and quit.
|
||||
|
||||
==== positional arguments
|
||||
* +'name'+: The name of the session.
|
||||
|
||||
[[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
|
||||
@@ -536,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.
|
||||
@@ -561,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.
|
||||
@@ -587,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'+
|
||||
@@ -600,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.
|
||||
@@ -702,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.
|
||||
|
||||
@@ -723,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
|
||||
|
||||
@@ -743,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.
|
||||
@@ -757,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.
|
||||
@@ -769,7 +1134,9 @@ These commands are mainly intended for debugging. They are hidden if qutebrowser
|
||||
|<<debug-cache-stats,debug-cache-stats>>|Print LRU cache stats.
|
||||
|<<debug-console,debug-console>>|Show the debugging console.
|
||||
|<<debug-crash,debug-crash>>|Crash for debugging purposes.
|
||||
|<<debug-pyeval,debug-pyeval>>|Evaluate a python string and display the results as a webpage.
|
||||
|<<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
|
||||
@@ -796,8 +1163,39 @@ Crash for debugging purposes.
|
||||
=== debug-pyeval
|
||||
Syntax: +:debug-pyeval 's'+
|
||||
|
||||
Evaluate a python string and display the results as a webpage.
|
||||
Evaluate a python string and display the results as a web page.
|
||||
|
||||
==== positional arguments
|
||||
* +'s'+: The string to evaluate.
|
||||
|
||||
==== note
|
||||
* This command does not split arguments after the last argument and handles quotes literally.
|
||||
* With this command, +;;+ is interpreted literally instead of splitting off a second command.
|
||||
|
||||
[[debug-trace]]
|
||||
=== debug-trace
|
||||
Syntax: +:debug-trace ['expr']+
|
||||
|
||||
Trace executed code via hunter.
|
||||
|
||||
==== positional arguments
|
||||
* +'expr'+: What to trace, passed to hunter.
|
||||
|
||||
==== note
|
||||
* This command does not split arguments after the last argument and handles quotes literally.
|
||||
* With this command, +;;+ is interpreted literally instead of splitting off a second command.
|
||||
|
||||
[[debug-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.
|
||||
|
||||
|
||||
@@ -8,8 +8,10 @@ The following help pages are currently available:
|
||||
|
||||
* link:quickstart.html[Quick start guide]
|
||||
* link:FAQ.html[Frequently asked questions]
|
||||
* link:CHANGELOG.html[Change Log]
|
||||
* link:commands.html[Documentation of commands]
|
||||
* link:settings.html[Documentation of settings]
|
||||
* link:userscripts.html[How to write userscripts]
|
||||
|
||||
Getting help
|
||||
------------
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
114
doc/notes
114
doc/notes
@@ -80,3 +80,117 @@ some cases passing the url to some cli program).
|
||||
I've also noticed the lack of completion. For example, on "o" pentadactyl will
|
||||
show sites (e.g. from history) that can be completed. I think I've been spoiled
|
||||
by pentadactyl having completion for just about everything.
|
||||
|
||||
|
||||
suckless surf ML post
|
||||
=====================
|
||||
|
||||
From: Ben Woolley <tautolog_AT_gmail.com>
|
||||
Date: Wed, 7 Jan 2015 18:29:25 -0800
|
||||
|
||||
Hi all,
|
||||
|
||||
This patch is a bit of a beast for surf. It is intended to be applied after
|
||||
the disk cache patch. It breaks some internal interfaces, so it could
|
||||
conflict with other patches.
|
||||
|
||||
I have been wanting a browser to implement a complete same-origin policy,
|
||||
and have been investigating how to do this in various browsers for many
|
||||
months. When I saw how surf opened new windows in a separate process, and
|
||||
was so simple, I knew I could do it quickly. Over the last two weeks, I
|
||||
have been developing this implementation on surf.
|
||||
|
||||
The basic idea is to prevent browser-based tracking as you browse from site
|
||||
to site, or origin to origin. By "origin" domain, I mean the "first-party"
|
||||
domain, the domain normally in the location bar (of the typical browser
|
||||
interface). Each origin domain effectively gets its own browser profile,
|
||||
and a browser process only ever deals with one origin domain at a time.
|
||||
This isolates origins vertically, preventing cookies, disk cache, memory
|
||||
cache, and window.name vulnerabilities. Basically, all known
|
||||
vulnerabilities that google and Mozilla cite as counter-examples when they
|
||||
explain why they haven't disabled third-party cookies yet.
|
||||
|
||||
When you are on msnbc.com, the tracking pixels will be stored in a cookie
|
||||
file for msnbc.com. When you go to cnn.com, the tracking pixels will be
|
||||
stored in a cookie file for cnn.com. You will not be tracked between them.
|
||||
However, third-party cookies, and the caching of third party resources will
|
||||
still work, but they will be isolated between origin domains. Instead of
|
||||
blocking cookies and cache entries, they are "double-keyed", or *also*
|
||||
keyed by origin.
|
||||
|
||||
There is a unidirectional communication channel, however, from one origin
|
||||
to the next, through navigation from one origin to the next. That is, the
|
||||
query string is passed from one origin to the next, and may embed
|
||||
identifiers. One example is an affiliate link that identifies where the
|
||||
lead came from. I have implemented what I call "horizontal isolation", in
|
||||
the form of an "Origin Crossing Gate".
|
||||
|
||||
Whenever you follow a link to a new domain, or even are just redirected to
|
||||
a new domain, a new window/tab is opened, and passed the referring origin
|
||||
via -R. The page passed to -O, for example -O originprompt.html, is an HTML
|
||||
page that is loaded in the new origin's context. That page tells you the
|
||||
origin you were on, the new origin, and the full link, and you can decide
|
||||
to go just to the new origin, or go to the full URL, after reviewing it for
|
||||
tracking data.
|
||||
|
||||
Also, you may click links that store your trust of that relationship with
|
||||
various expiration times, the same way you would trust geolocation requests
|
||||
for a particular origin for a period of time. The database used is actually
|
||||
the new origin's cookie file. Since the origin prompt is loaded in the new
|
||||
origin's context, I can set a cookie on behalf of the new origin. The
|
||||
expiration time of the trust is the expiration time of the cookie. The
|
||||
cookie implementation in webkit automatically expires the trust as part of
|
||||
how cookies work. Each time you cross an origin, the origin crossing page
|
||||
checks the cookie to see if trust is still established. If so, it will use
|
||||
window.location.replace() to continue on automatically. The initial page
|
||||
renders blank until the trust is invalidated, in which case the content of
|
||||
the gate is made visible.
|
||||
|
||||
However, the new origin is technically able to mess with those cookies, so
|
||||
a website could set trust for an origin crossing. I have addressed that by
|
||||
hashing the key with a salt, and setting the real expiration time as the
|
||||
value, along with an HMAC to verify the contents of the value. If the
|
||||
cookie is messed with in any way, the trust will be disabled, and the
|
||||
prompt will appear again. So it has a fail-safe function.
|
||||
|
||||
I know it seems a bit convoluted, but it just started out as a nice little
|
||||
rabbit hole, and I just wanted to get something workable. At first I
|
||||
thought using the cookie expiration time was convenient, but then when I
|
||||
realized that I needed to protect the cookie, things got a bit hairy. But
|
||||
it works.
|
||||
|
||||
Each profile is, by default, stored in ~/.surf/origins/$origin/
|
||||
The interesting side effect is that if there is a problem where a website
|
||||
relies on the cross-site cookie vulnerability to make a connection, you can
|
||||
simply make a symbolic link from one origin folder to another, and they
|
||||
will share the same profile. And if you want to delete cookies and/or cache
|
||||
for a particular origin, you just rm -rf the origin's profile folder, and
|
||||
don't have to interfere with your other sites that are working just fine.
|
||||
|
||||
One thing I don't handle are cross-origins POSTs. They just end up as GET
|
||||
requests right now. I intend to do something about that, but I haven't
|
||||
figured that out yet.
|
||||
|
||||
I have only been using this functionality for a few days myself, so I have
|
||||
absolutely no feedback yet. I wanted to provide the first implementation of
|
||||
the management of identity as a system resource the same way that things
|
||||
like geolocation, camera, and microphone resources are managed in browsers
|
||||
and mobile apps.
|
||||
|
||||
Currently, Mozilla and Tor have are working on third-party tracking issues
|
||||
in Firefox.
|
||||
https://blog.mozilla.org/privacy/2014/11/10/introducing-polaris-privacy-initiative-to-accelerate-user-focused-privacy-online/
|
||||
|
||||
Up to this point, Tor has provided a patch that double-keys cookies with
|
||||
the origin domain, but no other progress is visible. I have seen no
|
||||
discussion of how horizontal isolation is supposed to happen, and I wanted
|
||||
to show people that it can be done, and this is one way it can be done, and
|
||||
to compel the other browser makers to catch up, and hopefully the community
|
||||
can work toward a standard *without* the tracking loopholes, by showing
|
||||
people what a *complete* solution looks like.
|
||||
|
||||
Thank you,
|
||||
|
||||
Ben Woolley
|
||||
|
||||
Patch: http://lists.suckless.org/dev/att-25070/0005-same-origin-policy.patch
|
||||
|
||||
@@ -8,9 +8,10 @@ time, use the `:help` command.
|
||||
What to do now
|
||||
--------------
|
||||
|
||||
* View the http://qutebrowser.org/img/cheatsheet-big.png[keybinding cheatsheet]
|
||||
to make yourself familiar with the keybindings: +
|
||||
image:http://qutebrowser.org/img/cheatsheet-small.png["qutebrowser keybinding cheatsheet",link="http://qutebrowser.org/img/cheatsheet-big.png"]
|
||||
* 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.
|
||||
|
||||
@@ -21,6 +21,10 @@ on Python, PyQt5 and QtWebKit and free software, licensed under the GPL.
|
||||
|
||||
It was inspired by other browsers/addons like dwb and Vimperator/Pentadactyl.
|
||||
|
||||
Note the commands and settings of qutebrowser are not described in this
|
||||
manpage, but in the help integrated in qutebrowser - use the ":help" command to
|
||||
show it.
|
||||
|
||||
== OPTIONS
|
||||
// QUTE_OPTIONS_START
|
||||
=== positional arguments
|
||||
@@ -35,11 +39,29 @@ It was inspired by other browsers/addons like dwb and Vimperator/Pentadactyl.
|
||||
show this help message and exit
|
||||
|
||||
*-c* 'CONFDIR', *--confdir* 'CONFDIR'::
|
||||
Set config directory (empty for no config storage)
|
||||
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.
|
||||
|
||||
*-s* 'SECTION' 'OPTION' 'VALUE', *--set* 'SECTION' 'OPTION' 'VALUE'::
|
||||
Set a temporary setting for this session.
|
||||
|
||||
*-r* 'SESSION', *--restore* 'SESSION'::
|
||||
Restore a named session.
|
||||
|
||||
*-R*, *--override-restore*::
|
||||
Don't restore a session even if one would be restored.
|
||||
|
||||
=== debug arguments
|
||||
*-l* 'LOGLEVEL', *--loglevel* 'LOGLEVEL'::
|
||||
Set loglevel
|
||||
@@ -59,14 +81,26 @@ It was inspired by other browsers/addons like dwb and Vimperator/Pentadactyl.
|
||||
*--harfbuzz* '{old,new,system,auto}'::
|
||||
HarfBuzz engine version to use. Default: auto.
|
||||
|
||||
*--relaxed-config*::
|
||||
Silently remove unknown config options.
|
||||
|
||||
*--nowindow*::
|
||||
Don't show the main window.
|
||||
|
||||
*--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.
|
||||
|
||||
*--qt-style* 'STYLE'::
|
||||
Set the Qt GUI style to use.
|
||||
@@ -88,8 +122,14 @@ It was inspired by other browsers/addons like dwb and Vimperator/Pentadactyl.
|
||||
|
||||
- '~/.config/qutebrowser/qutebrowser.conf': Main config file.
|
||||
- '~/.config/qutebrowser/quickmarks': Saved quickmarks.
|
||||
- '~/.config/qutebrowser/keys.conf': Defined keybindings.
|
||||
- '~/.local/share/qutebrowser/': Various state information
|
||||
- '~/.config/qutebrowser/keys.conf': Defined key bindings.
|
||||
- '~/.local/share/qutebrowser/': Various state information.
|
||||
- '~/.cache/qutebrowser/': Temporary data.
|
||||
|
||||
Note qutebrowser conforms to the XDG basedir specification - if
|
||||
'XDG_CONFIG_HOME', 'XDG_DATA_HOME' or 'XDG_CACHE_HOME' are set in the
|
||||
environment, the directories configured there are used instead of the above
|
||||
defaults.
|
||||
|
||||
== BUGS
|
||||
Bugs are tracked in the Github issue tracker at
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
Getting stacktraces on crashes
|
||||
==============================
|
||||
:toc:
|
||||
The Compiler <mail@qutebrowser.org>
|
||||
|
||||
When there is a fatal crash in qutebrowser - most of the times a
|
||||
@@ -14,10 +15,17 @@ https://en.wikipedia.org/wiki/Debug_symbol[debugging symbols] is required.
|
||||
The rest of this guide is quite Linux specific, though there is a
|
||||
<<windows,section for Windows>> at the end.
|
||||
|
||||
Getting debugging symbols
|
||||
-------------------------
|
||||
Crashes which can be reproduced
|
||||
-------------------------------
|
||||
|
||||
.Debian/Ubuntu/...
|
||||
If a crash can be reproduced, packages with debugging symbols should be
|
||||
installed, and the crash should be reproduced under gdb.
|
||||
|
||||
Getting debugging symbols
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Debian/Ubuntu/...
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
For Debian based systems (Debian, Ubuntu, Linux Mint, ...), debug information
|
||||
is available in the repositories:
|
||||
@@ -26,76 +34,66 @@ is available in the repositories:
|
||||
# apt-get install python3-pyqt5-dbg python3-pyqt5.qtwebkit-dbg python3-dbg libqt5webkit5-dbg
|
||||
----
|
||||
|
||||
.Archlinux
|
||||
Archlinux
|
||||
^^^^^^^^^
|
||||
|
||||
For Archlinux, no debug informations are provided. You can either compile Qt
|
||||
yourself (which will take a few hours even on a modern machine) or use
|
||||
debugging symbols compiled by me (x86_64 only).
|
||||
debugging symbols compiled/packaged by me (x86_64 only).
|
||||
|
||||
To compile by yourself:
|
||||
.To compile by yourself
|
||||
|
||||
----
|
||||
$ git clone https://github.com/The-Compiler/qt-debug-pkgbuild.git
|
||||
$ cd qt-debug-pkgbuild
|
||||
$ git checkout symbols
|
||||
$ export DEBUG_CFLAGS='-ggdb3 -fvar-tracking-assignments -Og'
|
||||
$ export DEBUG_CXXFLAGS='-ggdb3 -fvar-tracking-assignments -Og'
|
||||
$ cd qt5
|
||||
$ makepkg -si
|
||||
$ makepkg -si --pkg qt5-base-debug,qt5-webkit-debug
|
||||
$ cd ../pyqt5
|
||||
$ makepkg -si
|
||||
$ makepkg -si --pkg pyqt5-common-debug,python-pyqt5-debug
|
||||
----
|
||||
|
||||
To install my pre-built packages:
|
||||
.To install my pre-built packages
|
||||
|
||||
First download and sign the key:
|
||||
|
||||
----
|
||||
$ mkdir qt-debug
|
||||
$ cd qt-debug
|
||||
$ wget -r -l1 -A '*.tar.xz' -L -np -nd http://www.qutebrowser.org/qt-symbols-pkg/
|
||||
# pacman -U *.pkg.tar.xz
|
||||
# pacman-key -r 0xD6A1C70FE80A0C82
|
||||
$ pacman-key -f 0xD6A1C70FE80A0C82
|
||||
Key fingerprint = 14AF EC28 70C6 4863 C5C7 ACCB D6A1 C70F E80A 0C82
|
||||
# pacman-key --lsign-key 0xD6A1C70FE80A0C82
|
||||
----
|
||||
|
||||
After you are done debugging, make sure to install the system packages again so
|
||||
you get updates. This can be done with this command:
|
||||
Then edit your `/etc/pacman.conf` to add the repository to the bottom:
|
||||
|
||||
----
|
||||
# pacman -S qt5
|
||||
[qt-debug]
|
||||
Server = http://qutebrowser.org/qt-debug/$arch
|
||||
----
|
||||
|
||||
Getting a core dump
|
||||
-------------------
|
||||
|
||||
The next step is finding the core dump so we can get a stacktrace from it.
|
||||
|
||||
First of all, try to reproduce your problem. If you can, run qutebrowser
|
||||
directly inside gdb like this:
|
||||
Then install the packages:
|
||||
|
||||
----
|
||||
$ gdb $(which python3) -ex 'run -m qutebrowser --debug'
|
||||
# pacman -Sy pyqt5-common-debug python-pyqt5-debug qt5-base-debug qt5-webkit-debug
|
||||
----
|
||||
|
||||
If you cannot reproduce the problem, you need to check if a coredump got
|
||||
written somewhere.
|
||||
The `-debug` packages conflict with the non-debug variants - it's safe to
|
||||
remove them.
|
||||
|
||||
Check the file `/proc/sys/kernel/core_pattern` on your system. If it does not
|
||||
start with a `|` character (pipe), check if there is a file named `core` or
|
||||
`core.NNNN` in the directory from that file, or in the current directory.
|
||||
Getting the stack trace
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
If so, execute gdb like this:
|
||||
First install `gdb` on your system if it's not installed already.
|
||||
|
||||
Then run qutebrowser directly inside gdb like this:
|
||||
|
||||
----
|
||||
$ gdb $(which python3) /path/to/core
|
||||
$ gdb $(readlink -f $(which python3)) -ex 'run -m qutebrowser --debug'
|
||||
----
|
||||
|
||||
If your `/proc/sys/kernel/core_pattern` contains something like
|
||||
`|/usr/lib/systemd/systemd-coredump`, use `coredumpctl` as root to run gdb:
|
||||
|
||||
----
|
||||
# coredumpctl gdb $(which python3)
|
||||
----
|
||||
|
||||
Getting a stack trace
|
||||
---------------------
|
||||
|
||||
Regardless of the way you used to open gdb, you should now see something like:
|
||||
After you reproduce the crash, you should now see something like:
|
||||
|
||||
----
|
||||
Program received signal SIGSEGV, Segmentation fault.
|
||||
@@ -107,16 +105,58 @@ Now enter these commands at the gdb prompt:
|
||||
|
||||
----
|
||||
(gdb) set logging on
|
||||
(gdb) set logging redirect on
|
||||
(gdb) bt
|
||||
(gdb) bt full
|
||||
# you might have to press enter a few times until you get the prompt back
|
||||
(gdb) set logging redirect off
|
||||
(gdb) quit
|
||||
----
|
||||
|
||||
Now copy the last few lines of the debug log (before you got the gdb prompt)
|
||||
and the full content of `gdb.txt` into the bug report. Please also add some
|
||||
words about what you were doing (or what pages you visited) before the crash
|
||||
This will create a `gdb.txt` in your current directory.
|
||||
|
||||
Copy the last few lines of the debug log (before you got the gdb prompt) and
|
||||
the full content of `gdb.txt` into the bug report. Please also add some words
|
||||
about what you were doing (or what pages you visited) before the crash
|
||||
happened.
|
||||
|
||||
Crashes which can NOT be reproduced
|
||||
-----------------------------------
|
||||
|
||||
If you cannot reproduce the problem, you need to check if a coredump got
|
||||
written somewhere. You should not install debug symbols as they won't match the
|
||||
generated coredump.
|
||||
|
||||
First install `gdb` on your system if it's not installed already.
|
||||
|
||||
Then check the file `/proc/sys/kernel/core_pattern` on your system. If it does
|
||||
not start with a `|` character (pipe), check if there is a file named `core` or
|
||||
`core.NNNN` in the directory from that file, or in the current directory.
|
||||
|
||||
If so, execute gdb like this:
|
||||
|
||||
----
|
||||
$ gdb $(readlink -f $(which python3)) /path/to/core
|
||||
----
|
||||
|
||||
If your `/proc/sys/kernel/core_pattern` contains something like
|
||||
`|/usr/lib/systemd/systemd-coredump`, use `coredumpctl` to run gdb:
|
||||
|
||||
----
|
||||
$ coredumpctl gdb $(readlink -f $(which python3))
|
||||
----
|
||||
|
||||
Getting the stack trace
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Now enter these commands at the gdb prompt:
|
||||
|
||||
----
|
||||
(gdb) set logging on
|
||||
(gdb) bt
|
||||
# you might have to press enter a few times until you get the prompt back
|
||||
(gdb) quit
|
||||
----
|
||||
|
||||
Copy the content of `gdb.txt` into the bug report. Please also add some words
|
||||
about what you were doing (or what pages you visited) before the crash
|
||||
happened.
|
||||
|
||||
[[windows]]
|
||||
@@ -130,9 +170,9 @@ file displayed there.
|
||||
|
||||
Now install
|
||||
http://www.microsoft.com/en-us/download/details.aspx?id=42933[DebugDiag] from
|
||||
Microsoft, then run the "DebugDiag 2 Analysis" tool. There, check
|
||||
"CrashHangAnalysis" and add your crash dump via "Add Data files". Then click
|
||||
"Start analysis".
|
||||
Microsoft, then run the *DebugDiag 2 Analysis* tool. There, check
|
||||
*CrashHangAnalysis* and add your crash dump via *Add Data files*. Then click
|
||||
*Start analysis*.
|
||||
|
||||
Close the Internet Explorer which opens when it's done and use the
|
||||
folder-button at the top left to get to the reports. There find the report file
|
||||
|
||||
66
doc/userscripts.asciidoc
Normal file
66
doc/userscripts.asciidoc
Normal file
@@ -0,0 +1,66 @@
|
||||
Writing qutebrowser userscripts
|
||||
===============================
|
||||
The Compiler <mail@qutebrowser.org>
|
||||
|
||||
qutebrowser is extensible by writing userscripts which can be called via the
|
||||
`:spawn --userscript` command, or via a key binding.
|
||||
|
||||
These userscripts are similiar to the (non-javascript) dwb userscripts. They
|
||||
can be written in any language which can read environment variables and write
|
||||
to a FIFO. Note they are *not* related to Greasemonkey userscripts.
|
||||
|
||||
Note for simple things such as opening the current page with another browser or
|
||||
mpv, a simple key binding to something like `:spawn mpv {url}` should suffice.
|
||||
|
||||
Also note userscripts need to have the executable bit set (`chmod +x`) for
|
||||
qutebrowser to run them.
|
||||
|
||||
Getting information
|
||||
-------------------
|
||||
|
||||
The following environment variables will be set when an userscript is launched:
|
||||
|
||||
- `QUTE_MODE`: Either `hints` (started via hints) or `command` (started via
|
||||
command or key binding).
|
||||
- `QUTE_USER_AGENT`: The currently set user agent.
|
||||
- `QUTE_FIFO`: The FIFO or file to write commands to.
|
||||
- `QUTE_HTML`: 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:
|
||||
|
||||
- `QUTE_URL`: The current URL.
|
||||
- `QUTE_TITLE`: The title of the current page.
|
||||
- `QUTE_SELECTED_TEXT`: The text currently selected on the page.
|
||||
- `QUTE_SELECTED_HTML` The HTML currently selected on the page.
|
||||
|
||||
In `hints` mode:
|
||||
|
||||
- `QUTE_URL`: The URL selected via hints.
|
||||
- `QUTE_SELECTED_TEXT`: The plain text of the element selected via hints.
|
||||
- `QUTE_SELECTED_HTML` The HTML of the element selected via hints.
|
||||
|
||||
Sending commands
|
||||
----------------
|
||||
|
||||
Normal qutebrowser commands can be written to `$QUTE_FIFO` and will be
|
||||
executed.
|
||||
|
||||
On Unix/OS X, this is a named pipe and commands written to it will get executed
|
||||
immediately.
|
||||
|
||||
On Windows, this is a regular file, and the commands in it will be executed as
|
||||
soon as your userscript terminates. This means when writing multiple commands,
|
||||
you should append to the file (`>>` in bash) rather than overwrite it (`>`).
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
Opening the currently selected word on http://www.dict.cc/[dict.cc]:
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
#!/bin/bash
|
||||
|
||||
echo "open -t http://www.dict.cc/?s=$QUTE_SELECTED_TEXT" >> "$QUTE_FIFO"
|
||||
----
|
||||
BIN
icons/qutebrowser.icns
Normal file
BIN
icons/qutebrowser.icns
Normal file
Binary file not shown.
@@ -32,21 +32,22 @@
|
||||
objecttolerance="10"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="1.2432572"
|
||||
inkscape:cx="510.06077"
|
||||
inkscape:cy="315.85317"
|
||||
inkscape:zoom="0.8791156"
|
||||
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="1024"
|
||||
inkscape:window-height="723"
|
||||
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"
|
||||
inkscape:window-maximized="1">
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:snap-text-baseline="true">
|
||||
<inkscape:grid
|
||||
id="GridFromPre046Settings"
|
||||
type="xygrid"
|
||||
@@ -1454,23 +1455,27 @@
|
||||
x="714.29938"
|
||||
y="108.87096">)</tspan></text>
|
||||
<rect
|
||||
ry="4.3646927"
|
||||
y="363.55695"
|
||||
ry="3.3457608"
|
||||
y="363.19348"
|
||||
x="238.30771"
|
||||
height="58.443066"
|
||||
height="44.799603"
|
||||
width="361.69229"
|
||||
id="rect5017"
|
||||
style="font-size:18px;fill:#babdb6;fill-opacity:1;stroke:none" />
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-size:13px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:DejaVu Sans Mono"
|
||||
x="245.32532"
|
||||
y="395.78867"
|
||||
id="text5021"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan5023"
|
||||
<g
|
||||
id="g4061"
|
||||
transform="translate(0,-6.7232151)">
|
||||
<text
|
||||
id="text5021"
|
||||
y="395.78867"
|
||||
x="245.32532"
|
||||
y="395.78867">Space</tspan></text>
|
||||
style="font-style:normal;font-weight:normal;font-size:13px;font-family:'DejaVu Sans Mono';fill:#000000;fill-opacity:1;stroke:none"
|
||||
xml:space="preserve"><tspan
|
||||
y="395.78867"
|
||||
x="245.32532"
|
||||
id="tspan5023"
|
||||
sodipodi:role="line">Space</tspan></text>
|
||||
</g>
|
||||
<text
|
||||
id="text6971"
|
||||
y="317.98907"
|
||||
@@ -1865,16 +1870,16 @@
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-size:9px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:start;line-height:89.99999762%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;font-family:TlwgTypewriter"
|
||||
x="320.22501"
|
||||
x="317.63174"
|
||||
y="195.40761"
|
||||
id="text7245"
|
||||
sodipodi:linespacing="89.999998%"><tspan
|
||||
sodipodi:role="line"
|
||||
x="320.22501"
|
||||
x="317.63174"
|
||||
y="195.40761"
|
||||
id="tspan7366" /><tspan
|
||||
sodipodi:role="line"
|
||||
x="320.22501"
|
||||
x="317.63174"
|
||||
y="202.78995"
|
||||
id="tspan7249"
|
||||
style="font-size:8px">reload</tspan></text>
|
||||
@@ -1934,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"
|
||||
@@ -2089,7 +2094,7 @@
|
||||
id="tspan4998"
|
||||
style="font-size:8px">new tab<tspan
|
||||
style="fill:#ff0000"
|
||||
id="tspan3699"></tspan></tspan><tspan
|
||||
id="tspan3699" /></tspan><tspan
|
||||
y="177.83009"
|
||||
x="670.26074"
|
||||
sodipodi:role="line"
|
||||
@@ -2624,8 +2629,8 @@
|
||||
<flowRoot
|
||||
xml:space="preserve"
|
||||
id="flowRoot5691"
|
||||
style="font-size:40px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
|
||||
transform="translate(0,-14.539167)"><flowRegion
|
||||
style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
|
||||
transform="translate(0,-38.539167)"><flowRegion
|
||||
id="flowRegion5693"><rect
|
||||
id="rect5695"
|
||||
width="322.5"
|
||||
@@ -2634,8 +2639,8 @@
|
||||
y="448.75"
|
||||
style="fill:#000000" /></flowRegion><flowPara
|
||||
id="flowPara5697"
|
||||
style="font-size:10px;font-weight:bold;fill:#000000;-inkscape-font-specification:Sans Bold"><flowSpan
|
||||
style="font-weight:bold;fill:#ff0000;-inkscape-font-specification:Sans Bold"
|
||||
style="font-weight:bold;font-size:10px;-inkscape-font-specification:'Sans Bold';fill:#000000"><flowSpan
|
||||
style="font-weight:bold;-inkscape-font-specification:'Sans Bold';fill:#ff0000"
|
||||
id="flowSpan5705">(1)</flowSpan> copying/yanking:</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara5701">yy - copy/yank URL</flowPara><flowPara
|
||||
@@ -2647,10 +2652,10 @@
|
||||
id="flowPara5709">yT - copy title to selection</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara5711" /></flowRoot> <flowRoot
|
||||
transform="translate(0.713591,62.823906)"
|
||||
transform="translate(0.713591,38.823906)"
|
||||
xml:space="preserve"
|
||||
id="flowRoot5691-0"
|
||||
style="font-size:40px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"><flowRegion
|
||||
style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"><flowRegion
|
||||
id="flowRegion5693-7"><rect
|
||||
id="rect5695-0"
|
||||
width="322.5"
|
||||
@@ -2659,8 +2664,8 @@
|
||||
y="448.75"
|
||||
style="fill:#000000" /></flowRegion><flowPara
|
||||
id="flowPara5697-9"
|
||||
style="font-size:10px;font-weight:bold;fill:#000000;-inkscape-font-specification:Sans Bold"><flowSpan
|
||||
style="font-weight:bold;fill:#ff0000;-inkscape-font-specification:Sans Bold"
|
||||
style="font-weight:bold;font-size:10px;-inkscape-font-specification:'Sans Bold';fill:#000000"><flowSpan
|
||||
style="font-weight:bold;-inkscape-font-specification:'Sans Bold';fill:#ff0000"
|
||||
id="flowSpan5705-5">(2)</flowSpan> pasting:</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara5701-9">pp - open URL from clipboard</flowPara><flowPara
|
||||
@@ -2668,26 +2673,26 @@
|
||||
id="flowPara5703-8">pP - open URL from selection</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara5707-0">Pp - like <flowSpan
|
||||
style="font-style:italic;-inkscape-font-specification:Sans Italic"
|
||||
style="font-style:italic;-inkscape-font-specification:'Sans Italic'"
|
||||
id="flowSpan6101">pp</flowSpan>, in new tab</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara5709-3">PP - like <flowSpan
|
||||
style="font-style:italic;-inkscape-font-specification:Sans Italic"
|
||||
style="font-style:italic;-inkscape-font-specification:'Sans Italic'"
|
||||
id="flowSpan6103">pP</flowSpan>, in new tab</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara5763">wp - like <flowSpan
|
||||
style="font-style:italic;-inkscape-font-specification:Sans Italic"
|
||||
style="font-style:italic;-inkscape-font-specification:'Sans Italic'"
|
||||
id="flowSpan6105">pp</flowSpan>, in new window</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara5765">wP - like <flowSpan
|
||||
style="font-style:italic;-inkscape-font-specification:Sans Italic"
|
||||
style="font-style:italic;-inkscape-font-specification:'Sans Italic'"
|
||||
id="flowSpan6107">pP</flowSpan>, in new window</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara5711-1" /></flowRoot> <flowRoot
|
||||
transform="translate(171.2479,-14.539167)"
|
||||
transform="translate(171.2479,-38.539167)"
|
||||
xml:space="preserve"
|
||||
id="flowRoot5691-0-9"
|
||||
style="font-size:40px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"><flowRegion
|
||||
style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"><flowRegion
|
||||
id="flowRegion5693-7-0"><rect
|
||||
id="rect5695-0-5"
|
||||
width="322.5"
|
||||
@@ -2695,9 +2700,9 @@
|
||||
x="17.5"
|
||||
y="448.75"
|
||||
style="fill:#000000" /></flowRegion><flowPara
|
||||
style="font-size:10px;font-weight:bold;fill:#000000;-inkscape-font-specification:Sans Bold"
|
||||
style="font-weight:bold;font-size:10px;-inkscape-font-specification:'Sans Bold';fill:#000000"
|
||||
id="flowPara5701-9-6"><flowSpan
|
||||
style="font-weight:bold;fill:#ff0000;-inkscape-font-specification:Sans Bold"
|
||||
style="font-weight:bold;-inkscape-font-specification:'Sans Bold';fill:#ff0000"
|
||||
id="flowSpan5705-5-8">(3)</flowSpan> navigation:</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara5829">[[ - click "previous"-link on page</flowPara><flowPara
|
||||
@@ -2705,11 +2710,11 @@
|
||||
id="flowPara5703-8-2">]] - click "next"-link on page</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara5707-0-7">{{ - like <flowSpan
|
||||
style="font-style:italic;-inkscape-font-specification:Sans Italic"
|
||||
style="font-style:italic;-inkscape-font-specification:'Sans Italic'"
|
||||
id="flowSpan6111">[[</flowSpan>, in new tab</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara5709-3-1">}} - like <flowSpan
|
||||
style="font-style:italic;-inkscape-font-specification:Sans Italic"
|
||||
style="font-style:italic;-inkscape-font-specification:'Sans Italic'"
|
||||
id="flowSpan6109">]]</flowSpan>, in new tab</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara5835"><Ctrl-A> - increment no. in URL</flowPara><flowPara
|
||||
@@ -2769,10 +2774,10 @@
|
||||
id="tspan4936-1-1-9-2"
|
||||
style="font-size:8px;fill:#ff0000">(3)</tspan></text>
|
||||
<flowRoot
|
||||
transform="translate(169.83695,87.823906)"
|
||||
transform="translate(169.83695,63.823906)"
|
||||
xml:space="preserve"
|
||||
id="flowRoot5691-4"
|
||||
style="font-size:40px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"><flowRegion
|
||||
style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"><flowRegion
|
||||
id="flowRegion5693-9"><rect
|
||||
id="rect5695-9"
|
||||
width="322.5"
|
||||
@@ -2781,8 +2786,8 @@
|
||||
y="448.75"
|
||||
style="fill:#000000" /></flowRegion><flowPara
|
||||
id="flowPara5697-3"
|
||||
style="font-size:10px;font-weight:bold;fill:#000000;-inkscape-font-specification:Sans Bold"><flowSpan
|
||||
style="font-weight:bold;fill:#ff0000;-inkscape-font-specification:Sans Bold"
|
||||
style="font-weight:bold;font-size:10px;-inkscape-font-specification:'Sans Bold';fill:#000000"><flowSpan
|
||||
style="font-weight:bold;-inkscape-font-specification:'Sans Bold';fill:#ff0000"
|
||||
id="flowSpan5705-0">(4)</flowSpan> scrolling:</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara5701-8"><Ctrl-F> - page down</flowPara><flowPara
|
||||
@@ -2792,59 +2797,59 @@
|
||||
id="flowPara5962"><Ctrl-D> - half page down</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara5711-7"><Ctrl-U> - half page up</flowPara></flowRoot> <flowRoot
|
||||
transform="translate(360.81663,-14.539167)"
|
||||
transform="translate(360.81663,-38.539167)"
|
||||
xml:space="preserve"
|
||||
id="flowRoot5691-4-9"
|
||||
style="font-size:40px;font-style:normal;font-weight:bold;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans;-inkscape-font-specification:Sans Bold"><flowRegion
|
||||
style="font-style:normal;font-weight:bold;font-size:40px;line-height:125%;font-family:Sans;-inkscape-font-specification:'Sans Bold';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"><flowRegion
|
||||
id="flowRegion5693-9-1"><rect
|
||||
id="rect5695-9-8"
|
||||
width="322.5"
|
||||
height="162.5"
|
||||
x="17.5"
|
||||
y="448.75"
|
||||
style="font-weight:bold;fill:#000000;-inkscape-font-specification:Sans Bold" /></flowRegion><flowPara
|
||||
style="font-size:10px;font-weight:bold;fill:#000000;-inkscape-font-specification:Sans Bold"
|
||||
style="font-weight:bold;-inkscape-font-specification:'Sans Bold';fill:#000000" /></flowRegion><flowPara
|
||||
style="font-weight:bold;font-size:10px;-inkscape-font-specification:'Sans Bold';fill:#000000"
|
||||
id="flowPara4171">in prompt mode:</flowPara><flowPara
|
||||
style="font-size:10px;font-weight:normal;fill:#000000;-inkscape-font-specification:Sans"
|
||||
style="font-weight:normal;font-size:10px;-inkscape-font-specification:Sans;fill:#000000"
|
||||
id="flowPara4175">Enter - accept prompt</flowPara><flowPara
|
||||
style="font-size:10px;font-weight:normal;fill:#000000;-inkscape-font-specification:Sans"
|
||||
style="font-weight:normal;font-size:10px;-inkscape-font-specification:Sans;fill:#000000"
|
||||
id="flowPara4177">y - answer yes to prompt</flowPara><flowPara
|
||||
style="font-size:10px;font-weight:normal;fill:#000000;-inkscape-font-specification:Sans"
|
||||
style="font-weight:normal;font-size:10px;-inkscape-font-specification:Sans;fill:#000000"
|
||||
id="flowPara4179">n - answer no to prompt</flowPara><flowPara
|
||||
style="font-size:10px;font-weight:normal;fill:#000000;-inkscape-font-specification:Sans"
|
||||
style="font-weight:normal;font-size:10px;-inkscape-font-specification:Sans;fill:#000000"
|
||||
id="flowPara6016" /></flowRoot> <flowRoot
|
||||
transform="translate(360.8264,40.645949)"
|
||||
transform="translate(360.8264,16.645949)"
|
||||
xml:space="preserve"
|
||||
id="flowRoot5691-0-9-9"
|
||||
style="font-size:40px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans;-inkscape-font-specification:Sans"><flowRegion
|
||||
style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:Sans;-inkscape-font-specification:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"><flowRegion
|
||||
id="flowRegion5693-7-0-2"><rect
|
||||
id="rect5695-0-5-6"
|
||||
width="322.5"
|
||||
height="162.5"
|
||||
x="17.5"
|
||||
y="448.75"
|
||||
style="font-style:normal;fill:#000000;-inkscape-font-specification:Sans" /></flowRegion><flowPara
|
||||
style="font-size:10px;font-style:normal;font-weight:bold;fill:#000000;-inkscape-font-specification:Sans Bold"
|
||||
style="font-style:normal;-inkscape-font-specification:Sans;fill:#000000" /></flowRegion><flowPara
|
||||
style="font-style:normal;font-weight:bold;font-size:10px;-inkscape-font-specification:'Sans Bold';fill:#000000"
|
||||
id="flowPara5701-9-6-8"><flowSpan
|
||||
style="font-style:normal;font-weight:bold;fill:#ff0000;-inkscape-font-specification:Sans Bold"
|
||||
style="font-style:normal;font-weight:bold;-inkscape-font-specification:'Sans Bold';fill:#ff0000"
|
||||
id="flowSpan5705-5-8-3">(6)</flowSpan> opening:</flowPara><flowPara
|
||||
style="font-size:10px;font-style:normal;fill:#000000;-inkscape-font-specification:Sans"
|
||||
style="font-style:normal;font-size:10px;-inkscape-font-specification:Sans;fill:#000000"
|
||||
id="flowPara5829-1">go - open based on cur. URL</flowPara><flowPara
|
||||
style="font-size:10px;font-style:normal;fill:#000000;-inkscape-font-specification:Sans"
|
||||
style="font-style:normal;font-size:10px;-inkscape-font-specification:Sans;fill:#000000"
|
||||
id="flowPara5703-8-2-8">gO - like <flowSpan
|
||||
style="font-style:italic;-inkscape-font-specification:Sans Italic"
|
||||
style="font-style:italic;-inkscape-font-specification:'Sans Italic'"
|
||||
id="flowSpan6132">go</flowSpan>, in new tab</flowPara><flowPara
|
||||
style="font-size:10px;font-style:normal;fill:#000000;-inkscape-font-specification:Sans"
|
||||
style="font-style:normal;font-size:10px;-inkscape-font-specification:Sans;fill:#000000"
|
||||
id="flowPara3581">xO - like <flowSpan
|
||||
style="font-style:italic;-inkscape-font-specification:Sans Italic"
|
||||
style="font-style:italic;-inkscape-font-specification:'Sans Italic'"
|
||||
id="flowSpan6134">go</flowSpan>, in bg. tab</flowPara><flowPara
|
||||
style="font-size:10px;font-style:normal;fill:#000000;-inkscape-font-specification:Sans"
|
||||
style="font-style:normal;font-size:10px;-inkscape-font-specification:Sans;fill:#000000"
|
||||
id="flowPara5709-3-1-6">xo - open in background tab</flowPara><flowPara
|
||||
style="font-size:10px;font-style:normal;fill:#000000;-inkscape-font-specification:Sans"
|
||||
style="font-style:normal;font-size:10px;-inkscape-font-specification:Sans;fill:#000000"
|
||||
id="flowPara5841-1">wo - open in new window</flowPara><flowPara
|
||||
style="font-size:10px;font-style:normal;fill:#000000;-inkscape-font-specification:Sans"
|
||||
style="font-style:normal;font-size:10px;-inkscape-font-specification:Sans;fill:#000000"
|
||||
id="flowPara5839-8" /><flowPara
|
||||
style="font-size:10px;font-style:normal;fill:#000000;-inkscape-font-specification:Sans"
|
||||
style="font-style:normal;font-size:10px;-inkscape-font-specification:Sans;fill:#000000"
|
||||
id="flowPara5711-1-8-7" /></flowRoot> <text
|
||||
xml:space="preserve"
|
||||
style="font-size:9px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:start;line-height:89.99999762%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;font-family:TlwgTypewriter"
|
||||
@@ -2899,10 +2904,10 @@
|
||||
id="tspan6219"
|
||||
style="font-size:8px">mode</tspan></text>
|
||||
<flowRoot
|
||||
transform="translate(361.29883,121.78408)"
|
||||
transform="translate(361.29883,97.78408)"
|
||||
xml:space="preserve"
|
||||
id="flowRoot5691-4-9-3"
|
||||
style="font-size:40px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"><flowRegion
|
||||
style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"><flowRegion
|
||||
id="flowRegion5693-9-1-7"><rect
|
||||
id="rect5695-9-8-7"
|
||||
width="322.5"
|
||||
@@ -2911,8 +2916,8 @@
|
||||
y="448.75"
|
||||
style="fill:#000000" /></flowRegion><flowPara
|
||||
id="flowPara5697-3-7-6"
|
||||
style="font-size:10px;font-weight:bold;fill:#000000;-inkscape-font-specification:Sans Bold"><flowSpan
|
||||
style="font-weight:bold;fill:#ff0000;-inkscape-font-specification:Sans Bold"
|
||||
style="font-weight:bold;font-size:10px;-inkscape-font-specification:'Sans Bold';fill:#000000"><flowSpan
|
||||
style="font-weight:bold;-inkscape-font-specification:'Sans Bold';fill:#ff0000"
|
||||
id="flowSpan5705-0-4-7">(7)</flowSpan> back/forward:</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara5701-8-5-8"><flowSpan
|
||||
@@ -2959,10 +2964,10 @@
|
||||
style="font-size:8px;fill:#ff0000"
|
||||
id="tspan3662">(9)</tspan></tspan></text>
|
||||
<flowRoot
|
||||
transform="translate(526.15723,-14.548933)"
|
||||
transform="translate(526.15723,-38.548933)"
|
||||
xml:space="preserve"
|
||||
id="flowRoot5691-4-9-3-6"
|
||||
style="font-size:40px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"><flowRegion
|
||||
style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"><flowRegion
|
||||
id="flowRegion5693-9-1-7-3"><rect
|
||||
id="rect5695-9-8-7-7"
|
||||
width="322.5"
|
||||
@@ -2971,15 +2976,15 @@
|
||||
y="448.75"
|
||||
style="fill:#000000" /></flowRegion><flowPara
|
||||
id="flowPara5697-3-7-6-8"
|
||||
style="font-size:10px;font-weight:bold;fill:#ff0000;-inkscape-font-specification:Sans Bold">(8)</flowPara><flowPara
|
||||
style="font-weight:bold;font-size:10px;-inkscape-font-specification:'Sans Bold';fill:#ff0000">(8)</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara3626-7">prefix with w - in new window</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara3725" /></flowRoot> <flowRoot
|
||||
transform="translate(525.65723,34.440325)"
|
||||
transform="translate(525.65723,10.440325)"
|
||||
xml:space="preserve"
|
||||
id="flowRoot5691-4-9-3-1"
|
||||
style="font-size:40px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"><flowRegion
|
||||
style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"><flowRegion
|
||||
id="flowRegion5693-9-1-7-1"><rect
|
||||
id="rect5695-9-8-7-5"
|
||||
width="322.5"
|
||||
@@ -2988,12 +2993,14 @@
|
||||
y="448.75"
|
||||
style="fill:#000000" /></flowRegion><flowPara
|
||||
id="flowPara5697-3-7-6-1"
|
||||
style="font-size:10px;font-weight:bold;fill:#000000;-inkscape-font-specification:Sans Bold"><flowSpan
|
||||
style="font-weight:bold;fill:#ff0000;-inkscape-font-specification:Sans Bold"
|
||||
style="font-weight:bold;font-size:10px;-inkscape-font-specification:'Sans Bold';fill:#000000"><flowSpan
|
||||
style="font-weight:bold;-inkscape-font-specification:'Sans Bold';fill:#ff0000"
|
||||
id="flowSpan5705-0-4-7-6">(9)</flowSpan> extended hint mode:</flowPara><flowPara
|
||||
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
|
||||
@@ -3003,7 +3010,7 @@
|
||||
id="flowPara3794">;o - put hinted URL in cmd. line</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara3796">;O - like <flowSpan
|
||||
style="font-style:italic;-inkscape-font-specification:Sans Italic"
|
||||
style="font-style:italic;-inkscape-font-specification:'Sans Italic'"
|
||||
id="flowSpan3798">;o</flowSpan>, in new tab</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara3800">;y - yank hinted URL to clipboard</flowPara><flowPara
|
||||
@@ -3013,24 +3020,24 @@
|
||||
id="flowPara3804">;r - rapid hinting</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara3806">;R - like <flowSpan
|
||||
style="font-style:italic;-inkscape-font-specification:Sans Italic"
|
||||
style="font-style:italic;-inkscape-font-specification:'Sans Italic'"
|
||||
id="flowSpan3810">;r</flowSpan>, in new window</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara3808">;d - download hinted URL</flowPara></flowRoot> <flowRoot
|
||||
transform="translate(706.84131,-14.539167)"
|
||||
transform="translate(706.84131,-38.539167)"
|
||||
xml:space="preserve"
|
||||
id="flowRoot5691-4-9-3-6-1"
|
||||
style="font-size:40px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"><flowRegion
|
||||
style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"><flowRegion
|
||||
id="flowRegion5693-9-1-7-3-5"><rect
|
||||
id="rect5695-9-8-7-7-0"
|
||||
width="148.08141"
|
||||
height="203.19766"
|
||||
width="154.90645"
|
||||
height="240.73535"
|
||||
x="17.5"
|
||||
y="448.75"
|
||||
style="fill:#000000" /></flowRegion><flowPara
|
||||
id="flowPara5697-3-7-6-8-2"
|
||||
style="font-size:10px;font-weight:bold;fill:#000000;-inkscape-font-specification:Sans Bold"><flowSpan
|
||||
style="font-weight:bold;fill:#ff0000;-inkscape-font-specification:Sans Bold"
|
||||
style="font-weight:bold;font-size:10px;-inkscape-font-specification:'Sans Bold';fill:#000000"><flowSpan
|
||||
style="font-weight:bold;-inkscape-font-specification:'Sans Bold';fill:#ff0000"
|
||||
id="flowSpan3852">(10)</flowSpan> misc. commands:</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara3725-0"><flowSpan
|
||||
@@ -3052,7 +3059,7 @@
|
||||
id="flowPara3915">gu - navigate up in URL</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara3917">gU - like <flowSpan
|
||||
style="font-style:italic;-inkscape-font-specification:Sans Italic"
|
||||
style="font-style:italic;-inkscape-font-specification:'Sans Italic'"
|
||||
id="flowSpan3923">gu</flowSpan>, in new tab</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara3921">sf - save config</flowPara><flowPara
|
||||
@@ -3072,10 +3079,16 @@
|
||||
id="flowPara4169"><flowSpan
|
||||
style="fill:#0000ff"
|
||||
id="flowSpan5438">ad</flowSpan> - cancel download</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara4077">co - close other tabs</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara4081">cd - clear downloads</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara3933" /><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara3935" /></flowRoot> <text
|
||||
id="flowPara3935" /><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara4079" /></flowRoot> <text
|
||||
sodipodi:linespacing="89.999998%"
|
||||
id="text9514-8-9-0-8"
|
||||
y="204.26315"
|
||||
@@ -3112,10 +3125,10 @@
|
||||
id="tspan4936-1-1-9-59-5"
|
||||
style="font-size:8px;fill:#ff0000">(10)</tspan></text>
|
||||
<flowRoot
|
||||
transform="translate(841.04351,-14.539167)"
|
||||
transform="translate(841.04351,-38.539167)"
|
||||
xml:space="preserve"
|
||||
id="flowRoot5691-4-9-3-6-1-2"
|
||||
style="font-size:40px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"><flowRegion
|
||||
style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"><flowRegion
|
||||
id="flowRegion5693-9-1-7-3-5-2"><rect
|
||||
id="rect5695-9-8-7-7-0-9"
|
||||
width="328.31396"
|
||||
@@ -3124,8 +3137,8 @@
|
||||
y="448.75"
|
||||
style="fill:#000000" /></flowRegion><flowPara
|
||||
id="flowPara5697-3-7-6-8-2-0"
|
||||
style="font-size:10px;font-weight:bold;fill:#000000;-inkscape-font-specification:Sans Bold"><flowSpan
|
||||
style="font-weight:bold;fill:#ff0000;-inkscape-font-specification:Sans Bold"
|
||||
style="font-weight:bold;font-size:10px;-inkscape-font-specification:'Sans Bold';fill:#000000"><flowSpan
|
||||
style="font-weight:bold;-inkscape-font-specification:'Sans Bold';fill:#ff0000"
|
||||
id="flowSpan3852-6">(11)</flowSpan> modifier commands:</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara3933-6"><Alt-num> - select tab</flowPara><flowPara
|
||||
@@ -3141,11 +3154,11 @@
|
||||
id="flowPara4138"><Ctrl-S> - stop loading</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara4140"><Ctrl-Alt-P> - print</flowPara><flowPara
|
||||
style="font-size:10px;font-weight:bold;fill:#000000;-inkscape-font-specification:Sans Bold"
|
||||
style="font-weight:bold;font-size:10px;-inkscape-font-specification:'Sans Bold';fill:#000000"
|
||||
id="flowPara4142">in insert mode:</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara4144"><Ctrl-E> - open editor</flowPara><flowPara
|
||||
style="font-size:10px;font-weight:bold;fill:#000000;-inkscape-font-specification:Sans Bold"
|
||||
style="font-weight:bold;font-size:10px;-inkscape-font-specification:'Sans Bold';fill:#000000"
|
||||
id="flowPara4146">in command mode:</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara4148"><Ctrl-P> - prev. history item</flowPara><flowPara
|
||||
@@ -3154,126 +3167,142 @@
|
||||
style="font-size:18px;fill:#eeeeec;fill-opacity:1;stroke:none"
|
||||
id="rect3764-9"
|
||||
width="60"
|
||||
height="60"
|
||||
height="45.993073"
|
||||
x="168.32558"
|
||||
y="362"
|
||||
ry="4.480969" />
|
||||
ry="3.4348924" />
|
||||
<rect
|
||||
style="font-size:18px;fill:#eeeeec;fill-opacity:1;stroke:none"
|
||||
id="rect3764-9-3"
|
||||
width="60"
|
||||
height="60"
|
||||
height="45.993073"
|
||||
x="47.906979"
|
||||
y="362"
|
||||
ry="4.480969" />
|
||||
ry="3.4348924" />
|
||||
<rect
|
||||
style="font-size:18px;fill:#eeeeec;fill-opacity:1;stroke:none"
|
||||
id="rect3764-9-1"
|
||||
width="60"
|
||||
height="60"
|
||||
height="45.993073"
|
||||
x="613.81396"
|
||||
y="362"
|
||||
ry="4.480969" />
|
||||
ry="3.4348924" />
|
||||
<rect
|
||||
style="font-size:18px;fill:#eeeeec;fill-opacity:1;stroke:none"
|
||||
id="rect3764-9-7"
|
||||
width="60"
|
||||
height="60"
|
||||
height="45.993073"
|
||||
x="730.46509"
|
||||
y="362"
|
||||
ry="4.480969" />
|
||||
<text
|
||||
id="text7358-8"
|
||||
y="395.78867"
|
||||
x="62.269463"
|
||||
style="font-size:12px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:DejaVu Sans Mono"
|
||||
xml:space="preserve"><tspan
|
||||
y="395.78867"
|
||||
ry="3.4348924" />
|
||||
<g
|
||||
id="g4049"
|
||||
transform="translate(1.3728676,-1.9658966)">
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-weight:normal;font-size:12px;font-family:'DejaVu Sans Mono';fill:#000000;fill-opacity:1;stroke:none"
|
||||
x="62.269463"
|
||||
id="tspan7360-1"
|
||||
sodipodi:role="line"
|
||||
style="font-size:12px;font-family:DejaVu Sans Mono">Ctrl</tspan></text>
|
||||
<text
|
||||
id="text7358-8-3"
|
||||
y="395.78867"
|
||||
x="745.17719"
|
||||
style="font-size:12px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:DejaVu Sans Mono"
|
||||
xml:space="preserve"><tspan
|
||||
y="395.78867"
|
||||
x="745.17719"
|
||||
id="tspan7360-1-7"
|
||||
sodipodi:role="line"
|
||||
style="font-size:12px;font-family:DejaVu Sans Mono">Ctrl</tspan></text>
|
||||
<text
|
||||
id="text7358-8-3-8"
|
||||
y="395.78867"
|
||||
x="627.75677"
|
||||
style="font-size:12px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:DejaVu Sans Mono"
|
||||
xml:space="preserve"><tspan
|
||||
y="395.78867"
|
||||
x="627.75677"
|
||||
id="tspan7360-1-7-0"
|
||||
sodipodi:role="line"
|
||||
style="font-size:12px;font-family:DejaVu Sans Mono">Alt</tspan></text>
|
||||
<text
|
||||
id="text7358-8-3-8-1"
|
||||
y="395.78867"
|
||||
x="186.34709"
|
||||
style="font-size:12px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:DejaVu Sans Mono"
|
||||
xml:space="preserve"><tspan
|
||||
y="395.78867"
|
||||
x="186.34709"
|
||||
id="tspan7360-1-7-0-2"
|
||||
sodipodi:role="line"
|
||||
style="font-size:12px;font-family:DejaVu Sans Mono">Alt</tspan></text>
|
||||
<text
|
||||
sodipodi:linespacing="89.999998%"
|
||||
id="text9514-8-9-0-8-4-0"
|
||||
y="410.26315"
|
||||
x="67.315361"
|
||||
style="font-size:8px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:start;line-height:89.99999762%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;font-family:TlwgTypewriter"
|
||||
xml:space="preserve"><tspan
|
||||
y="410.26315"
|
||||
y="385.78867"
|
||||
id="text7358-8"><tspan
|
||||
style="font-size:12px;font-family:'DejaVu Sans Mono'"
|
||||
sodipodi:role="line"
|
||||
id="tspan7360-1"
|
||||
x="62.269463"
|
||||
y="385.78867">Ctrl</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="67.315361"
|
||||
sodipodi:role="line"
|
||||
id="tspan4936-1-1-9-59-8-3"
|
||||
style="font-size:8px;fill:#ff0000">(11)</tspan></text>
|
||||
<text
|
||||
sodipodi:linespacing="89.999998%"
|
||||
id="text9514-8-9-0-8-4-0-8"
|
||||
y="410.26315"
|
||||
x="187.47893"
|
||||
style="font-size:8px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:start;line-height:89.99999762%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;font-family:TlwgTypewriter"
|
||||
xml:space="preserve"><tspan
|
||||
y="410.26315"
|
||||
y="400.26315"
|
||||
id="text9514-8-9-0-8-4-0"
|
||||
sodipodi:linespacing="89.999998%"><tspan
|
||||
style="font-size:8px;fill:#ff0000"
|
||||
id="tspan4936-1-1-9-59-8-3"
|
||||
sodipodi:role="line"
|
||||
x="67.315361"
|
||||
y="400.26315">(11)</tspan></text>
|
||||
</g>
|
||||
<g
|
||||
id="g4055"
|
||||
transform="translate(1.6278992,-11.965897)">
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-weight:normal;font-size:12px;font-family:'DejaVu Sans Mono';fill:#000000;fill-opacity:1;stroke:none"
|
||||
x="186.34709"
|
||||
y="395.78867"
|
||||
id="text7358-8-3-8-1"><tspan
|
||||
style="font-size:12px;font-family:'DejaVu Sans Mono'"
|
||||
sodipodi:role="line"
|
||||
id="tspan7360-1-7-0-2"
|
||||
x="186.34709"
|
||||
y="395.78867">Alt</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="187.47893"
|
||||
sodipodi:role="line"
|
||||
id="tspan4936-1-1-9-59-8-3-8"
|
||||
style="font-size:8px;fill:#ff0000">(11)</tspan></text>
|
||||
<text
|
||||
sodipodi:linespacing="89.999998%"
|
||||
id="text9514-8-9-0-8-4-0-7"
|
||||
y="410.26315"
|
||||
x="628.88861"
|
||||
style="font-size:8px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:start;line-height:89.99999762%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;font-family:TlwgTypewriter"
|
||||
xml:space="preserve"><tspan
|
||||
y="410.26315"
|
||||
id="text9514-8-9-0-8-4-0-8"
|
||||
sodipodi:linespacing="89.999998%"><tspan
|
||||
style="font-size:8px;fill:#ff0000"
|
||||
id="tspan4936-1-1-9-59-8-3-8"
|
||||
sodipodi:role="line"
|
||||
x="187.47893"
|
||||
y="410.26315">(11)</tspan></text>
|
||||
</g>
|
||||
<g
|
||||
id="g4065"
|
||||
transform="translate(5.706604,-11.965897)">
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-weight:normal;font-size:12px;font-family:'DejaVu Sans Mono';fill:#000000;fill-opacity:1;stroke:none"
|
||||
x="627.75677"
|
||||
y="395.78867"
|
||||
id="text7358-8-3-8"><tspan
|
||||
style="font-size:12px;font-family:'DejaVu Sans Mono'"
|
||||
sodipodi:role="line"
|
||||
id="tspan7360-1-7-0"
|
||||
x="627.75677"
|
||||
y="395.78867">Alt</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="628.88861"
|
||||
sodipodi:role="line"
|
||||
id="tspan4936-1-1-9-59-8-3-82"
|
||||
style="font-size:8px;fill:#ff0000">(11)</tspan></text>
|
||||
<text
|
||||
sodipodi:linespacing="89.999998%"
|
||||
id="text9514-8-9-0-8-4-0-3"
|
||||
y="410.26315"
|
||||
x="750.22308"
|
||||
style="font-size:8px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:start;line-height:89.99999762%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;font-family:TlwgTypewriter"
|
||||
xml:space="preserve"><tspan
|
||||
y="410.26315"
|
||||
id="text9514-8-9-0-8-4-0-7"
|
||||
sodipodi:linespacing="89.999998%"><tspan
|
||||
style="font-size:8px;fill:#ff0000"
|
||||
id="tspan4936-1-1-9-59-8-3-82"
|
||||
sodipodi:role="line"
|
||||
x="628.88861"
|
||||
y="410.26315">(11)</tspan></text>
|
||||
</g>
|
||||
<g
|
||||
id="g4071"
|
||||
transform="translate(1.0232544,-11.965897)">
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-weight:normal;font-size:12px;font-family:'DejaVu Sans Mono';fill:#000000;fill-opacity:1;stroke:none"
|
||||
x="745.17719"
|
||||
y="395.78867"
|
||||
id="text7358-8-3"><tspan
|
||||
style="font-size:12px;font-family:'DejaVu Sans Mono'"
|
||||
sodipodi:role="line"
|
||||
id="tspan7360-1-7"
|
||||
x="745.17719"
|
||||
y="395.78867">Ctrl</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="750.22308"
|
||||
sodipodi:role="line"
|
||||
id="tspan4936-1-1-9-59-8-3-4"
|
||||
style="font-size:8px;fill:#ff0000">(11)</tspan></text>
|
||||
y="410.26315"
|
||||
id="text9514-8-9-0-8-4-0-3"
|
||||
sodipodi:linespacing="89.999998%"><tspan
|
||||
style="font-size:8px;fill:#ff0000"
|
||||
id="tspan4936-1-1-9-59-8-3-4"
|
||||
sodipodi:role="line"
|
||||
x="750.22308"
|
||||
y="410.26315">(11)</tspan></text>
|
||||
</g>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-size:9px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:start;line-height:89.99999762%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;font-family:TlwgTypewriter"
|
||||
@@ -3297,27 +3326,15 @@
|
||||
style="font-size:8px">tab</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-size:8px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:start;line-height:89.99999762%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;font-family:TlwgTypewriter"
|
||||
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="tspan5327">other</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="267.67316"
|
||||
y="347.80524"
|
||||
id="tspan10562-12-5-98">tabs</tspan></text>
|
||||
x="274.21381"
|
||||
y="343.17578"
|
||||
id="tspan4052">(10)</tspan></text>
|
||||
<text
|
||||
sodipodi:linespacing="89.999998%"
|
||||
id="text10564-6-7-8-0"
|
||||
@@ -3398,10 +3415,10 @@
|
||||
id="tspan4936-1-1-9-59-5-6"
|
||||
style="font-size:8px;fill:#ff0000">(10)</tspan></text>
|
||||
<flowRoot
|
||||
transform="translate(838.55559,158.52236)"
|
||||
transform="translate(838.55559,134.52236)"
|
||||
xml:space="preserve"
|
||||
id="flowRoot5691-4-9-3-6-6"
|
||||
style="font-size:40px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"><flowRegion
|
||||
style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"><flowRegion
|
||||
id="flowRegion5693-9-1-7-3-8"><rect
|
||||
id="rect5695-9-8-7-7-6"
|
||||
width="322.5"
|
||||
@@ -3412,9 +3429,50 @@
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara3626-7-0"><flowSpan
|
||||
id="flowSpan5520"
|
||||
style="font-size:10px;font-weight:bold;fill:#0000ff;-inkscape-font-specification:Sans Bold">blue keys </flowSpan><flowSpan
|
||||
style="font-weight:bold;font-size:10px;-inkscape-font-specification:'Sans Bold';fill:#0000ff">blue keys </flowSpan><flowSpan
|
||||
style="fill:#0000ff"
|
||||
id="flowSpan5528">can be</flowSpan></flowPara><flowPara
|
||||
style="font-size:10px;fill:#0000ff"
|
||||
id="flowPara3725-9">prefixed by a count</flowPara></flowRoot> </g>
|
||||
id="flowPara3725-9">prefixed by a count</flowPara></flowRoot> <text
|
||||
xml:space="preserve"
|
||||
style="font-size:9px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:start;line-height:89.99999762%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;font-family:TlwgTypewriter"
|
||||
x="317.95987"
|
||||
y="155.85321"
|
||||
id="text7245-1-7"
|
||||
sodipodi:linespacing="89.999998%"><tspan
|
||||
sodipodi:role="line"
|
||||
x="317.95987"
|
||||
y="155.85321"
|
||||
id="tspan7366-3-3" /><tspan
|
||||
sodipodi:role="line"
|
||||
x="317.95987"
|
||||
y="163.23555"
|
||||
id="tspan5293-5"
|
||||
style="font-size:8px">reload </tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="317.95987"
|
||||
y="170.43555"
|
||||
style="font-size:8px"
|
||||
id="tspan3716">(bypass </tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="317.95987"
|
||||
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: 135 KiB After Width: | Height: | Size: 137 KiB |
7
misc/qt_menu.nib/README
Normal file
7
misc/qt_menu.nib/README
Normal file
@@ -0,0 +1,7 @@
|
||||
These files are copied from Qt's source tree in
|
||||
src/plugins/platforms/cocoa/qt_menu.nib at revision
|
||||
b8246f08e49eb672974fd3d3d972a5ff13c1524d.
|
||||
|
||||
http://code.qt.io/cgit/qt/qtbase.git/tree/src/plugins/platforms/cocoa/qt_menu.nib
|
||||
|
||||
They are needed for cx_Freeze and don't seem to be bundled with Qt anymore.
|
||||
59
misc/qt_menu.nib/classes.nib
generated
Normal file
59
misc/qt_menu.nib/classes.nib
generated
Normal file
@@ -0,0 +1,59 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IBClasses</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>ACTIONS</key>
|
||||
<dict>
|
||||
<key>hide</key>
|
||||
<string>id</string>
|
||||
<key>hideOtherApplications</key>
|
||||
<string>id</string>
|
||||
<key>orderFrontStandardAboutPanel</key>
|
||||
<string>id</string>
|
||||
<key>qtDispatcherToQPAMenuItem</key>
|
||||
<string>id</string>
|
||||
<key>terminate</key>
|
||||
<string>id</string>
|
||||
<key>unhideAllApplications</key>
|
||||
<string>id</string>
|
||||
</dict>
|
||||
<key>CLASS</key>
|
||||
<string>QCocoaMenuLoader</string>
|
||||
<key>LANGUAGE</key>
|
||||
<string>ObjC</string>
|
||||
<key>OUTLETS</key>
|
||||
<dict>
|
||||
<key>aboutItem</key>
|
||||
<string>NSMenuItem</string>
|
||||
<key>aboutQtItem</key>
|
||||
<string>NSMenuItem</string>
|
||||
<key>appMenu</key>
|
||||
<string>NSMenu</string>
|
||||
<key>hideItem</key>
|
||||
<string>NSMenuItem</string>
|
||||
<key>preferencesItem</key>
|
||||
<string>NSMenuItem</string>
|
||||
<key>quitItem</key>
|
||||
<string>NSMenuItem</string>
|
||||
<key>theMenu</key>
|
||||
<string>NSMenu</string>
|
||||
</dict>
|
||||
<key>SUPERCLASS</key>
|
||||
<string>NSResponder</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CLASS</key>
|
||||
<string>FirstResponder</string>
|
||||
<key>LANGUAGE</key>
|
||||
<string>ObjC</string>
|
||||
<key>SUPERCLASS</key>
|
||||
<string>NSObject</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>IBVersion</key>
|
||||
<string>1</string>
|
||||
</dict>
|
||||
</plist>
|
||||
18
misc/qt_menu.nib/info.nib
generated
Normal file
18
misc/qt_menu.nib/info.nib
generated
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IBFramework Version</key>
|
||||
<string>672</string>
|
||||
<key>IBOldestOS</key>
|
||||
<integer>5</integer>
|
||||
<key>IBOpenObjects</key>
|
||||
<array>
|
||||
<integer>57</integer>
|
||||
</array>
|
||||
<key>IBSystem Version</key>
|
||||
<string>9L31a</string>
|
||||
<key>targetFramework</key>
|
||||
<string>IBCocoaFramework</string>
|
||||
</dict>
|
||||
</plist>
|
||||
BIN
misc/qt_menu.nib/keyedobjects.nib
generated
Normal file
BIN
misc/qt_menu.nib/keyedobjects.nib
generated
Normal file
Binary file not shown.
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"
|
||||
|
||||
18
qutebrowser/test/utils/__init__.py → misc/userscripts/qutebrowser_viewsource
Normal file → Executable file
18
qutebrowser/test/utils/__init__.py → misc/userscripts/qutebrowser_viewsource
Normal file → Executable file
@@ -1,6 +1,6 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
#!/bin/bash
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2015 Zach-Button <zachrey.button@gmail.com>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
@@ -17,4 +17,16 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Tests for the qutebrowser.utils package."""
|
||||
#
|
||||
# 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"
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
@@ -24,11 +24,11 @@
|
||||
import os.path
|
||||
|
||||
__author__ = "Florian Bruhin"
|
||||
__copyright__ = "Copyright 2014 Florian Bruhin (The Compiler)"
|
||||
__copyright__ = "Copyright 2014-2015 Florian Bruhin (The Compiler)"
|
||||
__license__ = "GPL"
|
||||
__maintainer__ = __author__
|
||||
__email__ = "mail@qutebrowser.org"
|
||||
__version_info__ = (0, 1, 1)
|
||||
__version_info__ = (0, 3, 0)
|
||||
__version__ = '.'.join(map(str, __version_info__))
|
||||
__description__ = "A keyboard-driven, vim-like browser based on PyQt5 and QtWebKit."
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
|
||||
1350
qutebrowser/app.py
1350
qutebrowser/app.py
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
@@ -17,7 +17,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Functions related to adblocking."""
|
||||
"""Functions related to ad blocking."""
|
||||
|
||||
import io
|
||||
import os.path
|
||||
@@ -25,11 +25,9 @@ import functools
|
||||
import posixpath
|
||||
import zipfile
|
||||
|
||||
from PyQt5.QtCore import QStandardPaths
|
||||
|
||||
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):
|
||||
@@ -92,13 +90,18 @@ class HostBlocker:
|
||||
self.blocked_hosts = set()
|
||||
self._in_progress = []
|
||||
self._done_count = 0
|
||||
data_dir = standarddir.get(QStandardPaths.DataLocation)
|
||||
self._hosts_file = os.path.join(data_dir, '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:
|
||||
@@ -107,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:
|
||||
message.info('last-focused',
|
||||
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):
|
||||
@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')
|
||||
@@ -125,8 +132,10 @@ class HostBlocker:
|
||||
if url.scheme() == 'file':
|
||||
try:
|
||||
fileobj = open(url.path(), 'rb')
|
||||
except OSError:
|
||||
log.misc.exception("Failed to open block list!")
|
||||
except OSError as e:
|
||||
message.error(win_id, "adblock: Error while reading {}: "
|
||||
"{}".format(url.path(), e.strerror))
|
||||
continue
|
||||
download = FakeDownload(fileobj)
|
||||
self._in_progress.append(download)
|
||||
self.on_download_finished(download)
|
||||
@@ -154,9 +163,8 @@ class HostBlocker:
|
||||
f = get_fileobj(byte_io)
|
||||
except (OSError, UnicodeDecodeError, zipfile.BadZipFile,
|
||||
zipfile.LargeZipFile) as e:
|
||||
message.error('last-focused', "adblock: Error while reading {}: "
|
||||
"{} - {}".format(
|
||||
byte_io.name, e.__class__.__name__, e))
|
||||
message.error('current', "adblock: Error while reading {}: {} - "
|
||||
"{}".format(byte_io.name, e.__class__.__name__, e))
|
||||
return
|
||||
for line in f:
|
||||
line_count += 1
|
||||
@@ -184,17 +192,16 @@ class HostBlocker:
|
||||
self.blocked_hosts.add(host)
|
||||
log.misc.debug("{}: read {} lines".format(byte_io.name, line_count))
|
||||
if error_count > 0:
|
||||
message.error('last-focused', "adblock: {} read errors for "
|
||||
"{}".format(error_count, byte_io.name))
|
||||
message.error('current', "adblock: {} read errors for {}".format(
|
||||
error_count, byte_io.name))
|
||||
|
||||
def on_lists_downloaded(self):
|
||||
"""Install block lists after files have been downloaded."""
|
||||
with open(self._hosts_file, 'w', encoding='utf-8') as f:
|
||||
for host in sorted(self.blocked_hosts):
|
||||
f.write(host + '\n')
|
||||
message.info('last-focused', "adblock: Read {} hosts from {} "
|
||||
"sources.".format(len(self.blocked_hosts),
|
||||
self._done_count))
|
||||
message.info('current', "adblock: Read {} hosts from {} sources."
|
||||
.format(len(self.blocked_hosts), self._done_count))
|
||||
|
||||
@config.change_filter('content', 'host-block-lists')
|
||||
def on_config_changed(self):
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
import os.path
|
||||
|
||||
from PyQt5.QtCore import QStandardPaths
|
||||
from PyQt5.QtCore import pyqtSlot
|
||||
from PyQt5.QtNetwork import QNetworkDiskCache, QNetworkCacheMetaData
|
||||
|
||||
from qutebrowser.config import config
|
||||
@@ -30,24 +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)
|
||||
cache_dir = standarddir.get(QStandardPaths.CacheLocation)
|
||||
self.setCacheDirectory(os.path.join(cache_dir, '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.
|
||||
@@ -55,13 +72,13 @@ class DiskCache(QNetworkDiskCache):
|
||||
Return:
|
||||
An int.
|
||||
"""
|
||||
if objreg.get('general', 'private-browsing'):
|
||||
return 0
|
||||
else:
|
||||
if self._activated:
|
||||
return super().cacheSize()
|
||||
else:
|
||||
return 0
|
||||
|
||||
def fileMetaData(self, filename):
|
||||
"""Returns the QNetworkCacheMetaData for the cache file filename.
|
||||
"""Return the QNetworkCacheMetaData for the cache file filename.
|
||||
|
||||
Args:
|
||||
filename: The file name as a string.
|
||||
@@ -69,10 +86,10 @@ class DiskCache(QNetworkDiskCache):
|
||||
Return:
|
||||
A QNetworkCacheMetaData object.
|
||||
"""
|
||||
if objreg.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.
|
||||
@@ -83,10 +100,10 @@ class DiskCache(QNetworkDiskCache):
|
||||
return:
|
||||
A QIODevice or None.
|
||||
"""
|
||||
if objreg.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.
|
||||
@@ -94,10 +111,10 @@ class DiskCache(QNetworkDiskCache):
|
||||
Args:
|
||||
device: A QIODevice.
|
||||
"""
|
||||
if objreg.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.
|
||||
@@ -108,10 +125,10 @@ class DiskCache(QNetworkDiskCache):
|
||||
Return:
|
||||
A QNetworkCacheMetaData object.
|
||||
"""
|
||||
if objreg.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.
|
||||
@@ -122,10 +139,10 @@ class DiskCache(QNetworkDiskCache):
|
||||
Return:
|
||||
A QIODevice or None.
|
||||
"""
|
||||
if objreg.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.
|
||||
@@ -133,25 +150,25 @@ class DiskCache(QNetworkDiskCache):
|
||||
Return:
|
||||
True on success, False otherwise.
|
||||
"""
|
||||
if objreg.get('general', 'private-browsing'):
|
||||
return False
|
||||
else:
|
||||
if self._activated:
|
||||
return super().remove(url)
|
||||
else:
|
||||
return False
|
||||
|
||||
def updateMetaData(self, meta_data):
|
||||
"""Updates the cache meta date for the meta_data's url to meta_data.
|
||||
"""Update the cache meta date for the meta_data's url to meta_data.
|
||||
|
||||
Args:
|
||||
meta_data: A QNetworkCacheMetaData object.
|
||||
"""
|
||||
if objreg.get('general', 'private-browsing'):
|
||||
return
|
||||
else:
|
||||
if self._activated:
|
||||
super().updateMetaData(meta_data)
|
||||
else:
|
||||
return
|
||||
|
||||
def clear(self):
|
||||
"""Removes all items from the cache."""
|
||||
if objreg.get('general', 'private-browsing'):
|
||||
return
|
||||
else:
|
||||
"""Remove all items from the cache."""
|
||||
if self._activated:
|
||||
super().clear()
|
||||
else:
|
||||
return
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
@@ -20,16 +20,25 @@
|
||||
"""Handling of HTTP cookies."""
|
||||
|
||||
from PyQt5.QtNetwork import QNetworkCookie, QNetworkCookieJar
|
||||
from PyQt5.QtCore import QStandardPaths, QDateTime
|
||||
from PyQt5.QtCore import pyqtSignal, QDateTime
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.config.parsers import line as lineparser
|
||||
from qutebrowser.utils import utils, standarddir, objreg
|
||||
from qutebrowser.misc import lineparser
|
||||
|
||||
|
||||
class RAMCookieJar(QNetworkCookieJar):
|
||||
|
||||
"""An in-RAM cookie jar."""
|
||||
"""An in-RAM cookie jar.
|
||||
|
||||
Signals:
|
||||
changed: Emitted when the cookie store was changed.
|
||||
"""
|
||||
|
||||
changed = pyqtSignal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self, count=len(self.allCookies()))
|
||||
@@ -47,6 +56,7 @@ class RAMCookieJar(QNetworkCookieJar):
|
||||
if config.get('content', 'cookies-accept') == 'never':
|
||||
return False
|
||||
else:
|
||||
self.changed.emit()
|
||||
return super().setCookiesFromUrl(cookies, url)
|
||||
|
||||
|
||||
@@ -55,24 +65,26 @@ class CookieJar(RAMCookieJar):
|
||||
"""A cookie jar saving cookies to disk.
|
||||
|
||||
Attributes:
|
||||
_linecp: The LineConfigParser managing the cookies file.
|
||||
_lineparser: The LineParser managing the cookies file.
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
datadir = standarddir.get(QStandardPaths.DataLocation)
|
||||
self._linecp = lineparser.LineConfigParser(datadir, 'cookies',
|
||||
binary=True)
|
||||
self._lineparser = lineparser.LineParser(
|
||||
standarddir.data(), 'cookies', binary=True, parent=self)
|
||||
cookies = []
|
||||
for line in self._linecp:
|
||||
for line in self._lineparser:
|
||||
cookies += QNetworkCookie.parseCookies(line)
|
||||
self.setAllCookies(cookies)
|
||||
objreg.get('config').changed.connect(self.cookies_store_changed)
|
||||
objreg.get('save-manager').add_saveable(
|
||||
'cookies', self.save, self.changed,
|
||||
config_opt=('content', 'cookies-store'))
|
||||
|
||||
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]
|
||||
@@ -80,19 +92,18 @@ class CookieJar(RAMCookieJar):
|
||||
|
||||
def save(self):
|
||||
"""Save cookies to disk."""
|
||||
if not config.get('content', 'cookies-store'):
|
||||
return
|
||||
self.purge_old_cookies()
|
||||
lines = []
|
||||
for cookie in self.allCookies():
|
||||
if not cookie.isSessionCookie():
|
||||
lines.append(cookie.toRawForm())
|
||||
self._linecp.data = lines
|
||||
self._linecp.save()
|
||||
self._lineparser.data = lines
|
||||
self._lineparser.save()
|
||||
|
||||
@config.change_filter('content', 'cookies-store')
|
||||
def cookies_store_changed(self):
|
||||
"""Delete stored cookies if cookies-store changed."""
|
||||
if not config.get('content', 'cookies-store'):
|
||||
self._linecp.data = []
|
||||
self._linecp.save()
|
||||
self._lineparser.data = []
|
||||
self._lineparser.save()
|
||||
self.changed.emit()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
@@ -21,14 +21,14 @@
|
||||
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
import os.path
|
||||
import shutil
|
||||
import functools
|
||||
import collections
|
||||
|
||||
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, QObject, QTimer,
|
||||
QStandardPaths, Qt, QVariant, QAbstractListModel,
|
||||
QModelIndex, QUrl)
|
||||
Qt, QVariant, QAbstractListModel, QModelIndex, QUrl)
|
||||
from PyQt5.QtGui import QDesktopServices
|
||||
from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply
|
||||
# We need this import so PyQt can use it inside pyqtSlot
|
||||
@@ -49,6 +49,32 @@ ModelRole = usertypes.enum('ModelRole', ['item'], start=Qt.UserRole,
|
||||
RetryInfo = collections.namedtuple('RetryInfo', ['request', 'manager'])
|
||||
|
||||
|
||||
def _download_dir():
|
||||
"""Get the download directory to use."""
|
||||
directory = config.get('storage', 'download-directory')
|
||||
if directory is None:
|
||||
directory = standarddir.download()
|
||||
return directory
|
||||
|
||||
|
||||
def _path_suggestion(filename):
|
||||
"""Get the suggested file path.
|
||||
|
||||
Args:
|
||||
filename: The filename to use if included in the suggestion.
|
||||
"""
|
||||
suggestion = config.get('completion', 'download-path-suggestion')
|
||||
if suggestion == 'path':
|
||||
# add trailing '/' if not present
|
||||
return os.path.join(_download_dir(), '')
|
||||
elif suggestion == 'filename':
|
||||
return filename
|
||||
elif suggestion == 'both':
|
||||
return os.path.join(_download_dir(), filename)
|
||||
else:
|
||||
raise ValueError("Invalid suggestion value {}!".format(suggestion))
|
||||
|
||||
|
||||
class DownloadItemStats(QObject):
|
||||
|
||||
"""Statistics (bytes done, total bytes, time, etc.) about a download.
|
||||
@@ -122,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.
|
||||
@@ -132,7 +158,6 @@ class DownloadItemStats(QObject):
|
||||
bytes_total = None
|
||||
self.done = bytes_done
|
||||
self.total = bytes_total
|
||||
self.updated.emit()
|
||||
|
||||
|
||||
class DownloadItem(QObject):
|
||||
@@ -158,32 +183,33 @@ class DownloadItem(QObject):
|
||||
Attributes:
|
||||
done: Whether the download is finished.
|
||||
stats: A DownloadItemStats object.
|
||||
successful: Whether the download has completed sucessfully.
|
||||
index: The index of the download in the view.
|
||||
successful: Whether the download has completed successfully.
|
||||
error_msg: The current error message, or None
|
||||
autoclose: Whether to close the associated file if the download is
|
||||
done.
|
||||
fileobj: The file object to download the file to.
|
||||
reply: The QNetworkReply associated with this download.
|
||||
retry_info: A RetryInfo instance.
|
||||
_filename: The filename of the download.
|
||||
_redirects: How many time we were redirected already.
|
||||
_buffer: A BytesIO object to buffer incoming data until we know the
|
||||
target file.
|
||||
_read_timer: A QTimer which reads the QNetworkReply into self._buffer
|
||||
_read_timer: A Timer which reads the QNetworkReply into self._buffer
|
||||
periodically.
|
||||
_retry_info: A RetryInfo instance.
|
||||
_win_id: The window ID the DownloadItem runs in.
|
||||
|
||||
Signals:
|
||||
data_changed: The downloads metadata changed.
|
||||
finished: The download was finished.
|
||||
cancelled: The download was cancelled.
|
||||
error: An error with the download occured.
|
||||
error: An error with the download occurred.
|
||||
arg: The error message as string.
|
||||
redirected: Signal emitted when a download was redirected.
|
||||
arg 0: The new QNetworkRequest.
|
||||
arg 1: The old QNetworkReply.
|
||||
do_retry: Emitted when a request should be re-tried.
|
||||
arg: The QNetworkRequest to download.
|
||||
do_retry: Emitted when a download is retried.
|
||||
arg 0: The new DownloadItem
|
||||
"""
|
||||
|
||||
MAX_REDIRECTS = 10
|
||||
@@ -192,7 +218,7 @@ class DownloadItem(QObject):
|
||||
error = pyqtSignal(str)
|
||||
cancelled = pyqtSignal()
|
||||
redirected = pyqtSignal(QNetworkRequest, QNetworkReply)
|
||||
do_retry = pyqtSignal('QNetworkReply')
|
||||
do_retry = pyqtSignal(object) # DownloadItem
|
||||
|
||||
def __init__(self, reply, win_id, parent=None):
|
||||
"""Constructor.
|
||||
@@ -201,14 +227,15 @@ class DownloadItem(QObject):
|
||||
reply: The QNetworkReply to download.
|
||||
"""
|
||||
super().__init__(parent)
|
||||
self._retry_info = None
|
||||
self.retry_info = None
|
||||
self.done = False
|
||||
self.stats = DownloadItemStats(self)
|
||||
self.stats.updated.connect(self.data_changed)
|
||||
self.index = 0
|
||||
self.autoclose = True
|
||||
self.reply = None
|
||||
self._buffer = io.BytesIO()
|
||||
self._read_timer = QTimer()
|
||||
self._read_timer = usertypes.Timer(self, name='download-read-timer')
|
||||
self._read_timer.setInterval(500)
|
||||
self._read_timer.timeout.connect(self.on_read_timer_timeout)
|
||||
self._redirects = 0
|
||||
@@ -237,8 +264,9 @@ class DownloadItem(QObject):
|
||||
else:
|
||||
errmsg = " - {}".format(self.error_msg)
|
||||
if all(e is None for e in (perc, remaining, self.stats.total)):
|
||||
return ('{name} [{speed:>10}|{down}]{errmsg}'.format(
|
||||
name=self.basename, speed=speed, down=down, errmsg=errmsg))
|
||||
return ('{index}: {name} [{speed:>10}|{down}]{errmsg}'.format(
|
||||
index=self.index, name=self.basename, speed=speed,
|
||||
down=down, errmsg=errmsg))
|
||||
if perc is None:
|
||||
perc = '??'
|
||||
else:
|
||||
@@ -249,17 +277,18 @@ class DownloadItem(QObject):
|
||||
remaining = utils.format_seconds(remaining)
|
||||
total = utils.format_size(self.stats.total, suffix='B')
|
||||
if self.done:
|
||||
return ('{name} [{perc:>2}%|{total}]{errmsg}'.format(
|
||||
name=self.basename, perc=perc, total=total,
|
||||
errmsg=errmsg))
|
||||
return ('{index}: {name} [{perc:>2}%|{total}]{errmsg}'.format(
|
||||
index=self.index, name=self.basename, perc=perc,
|
||||
total=total, errmsg=errmsg))
|
||||
else:
|
||||
return ('{name} [{speed:>10}|{remaining:>5}|{perc:>2}%|'
|
||||
return ('{index}: {name} [{speed:>10}|{remaining:>5}|{perc:>2}%|'
|
||||
'{down}/{total}]{errmsg}'.format(
|
||||
name=self.basename, speed=speed, remaining=remaining,
|
||||
perc=perc, down=down, total=total, errmsg=errmsg))
|
||||
index=self.index, name=self.basename, speed=speed,
|
||||
remaining=remaining, perc=perc, down=down,
|
||||
total=total, errmsg=errmsg))
|
||||
|
||||
def _create_fileobj(self):
|
||||
"""Creates a file object using the internal filename."""
|
||||
"""Create a file object using the internal filename."""
|
||||
try:
|
||||
fileobj = open(self._filename, 'wb')
|
||||
except OSError as e:
|
||||
@@ -275,6 +304,8 @@ class DownloadItem(QObject):
|
||||
q.answered_yes.connect(self._create_fileobj)
|
||||
q.answered_no.connect(functools.partial(self.cancel, False))
|
||||
q.cancelled.connect(functools.partial(self.cancel, False))
|
||||
self.cancelled.connect(q.abort)
|
||||
self.error.connect(q.abort)
|
||||
message_bridge = objreg.get('message-bridge', scope='window',
|
||||
window=self._win_id)
|
||||
message_bridge.ask(q, blocking=False)
|
||||
@@ -290,7 +321,11 @@ class DownloadItem(QObject):
|
||||
self.error_msg = msg
|
||||
self.stats.finish()
|
||||
self.error.emit(msg)
|
||||
self.reply.abort()
|
||||
with log.hide_qt_warning('QNetworkReplyImplPrivate::error: Internal '
|
||||
'problem, this method must only be called '
|
||||
'once.'):
|
||||
# See https://codereview.qt-project.org/#/c/107863/
|
||||
self.reply.abort()
|
||||
self.reply.deleteLater()
|
||||
self.reply = None
|
||||
self.done = True
|
||||
@@ -310,8 +345,8 @@ class DownloadItem(QObject):
|
||||
reply.finished.connect(self.on_reply_finished)
|
||||
reply.error.connect(self.on_reply_error)
|
||||
reply.readyRead.connect(self.on_ready_read)
|
||||
self._retry_info = RetryInfo(request=reply.request(),
|
||||
manager=reply.manager())
|
||||
self.retry_info = RetryInfo(request=reply.request(),
|
||||
manager=reply.manager())
|
||||
if not self.fileobj:
|
||||
self._read_timer.start()
|
||||
# We could have got signals before we connected slots to them.
|
||||
@@ -320,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
|
||||
@@ -335,6 +377,7 @@ class DownloadItem(QObject):
|
||||
return utils.interpolate_color(
|
||||
start, stop, self.stats.percentage(), system)
|
||||
|
||||
@pyqtSlot()
|
||||
def cancel(self, remove_data=True):
|
||||
"""Cancel the download.
|
||||
|
||||
@@ -351,22 +394,32 @@ class DownloadItem(QObject):
|
||||
self.reply = None
|
||||
if self.fileobj is not None:
|
||||
self.fileobj.close()
|
||||
try:
|
||||
if (self._filename is not None and os.path.exists(self._filename)
|
||||
and remove_data):
|
||||
os.remove(self._filename)
|
||||
except OSError:
|
||||
log.downloads.exception("Failed to remove partial file")
|
||||
if remove_data:
|
||||
self.delete()
|
||||
self.done = True
|
||||
self.finished.emit()
|
||||
self.data_changed.emit()
|
||||
|
||||
def delete(self):
|
||||
"""Delete the downloaded file."""
|
||||
try:
|
||||
if self._filename is not None and os.path.exists(self._filename):
|
||||
os.remove(self._filename)
|
||||
except OSError:
|
||||
log.downloads.exception("Failed to remove partial file")
|
||||
|
||||
@pyqtSlot()
|
||||
def retry(self):
|
||||
"""Retry a failed download."""
|
||||
download_manager = objreg.get('download-manager', scope='window',
|
||||
window=self._win_id)
|
||||
new_reply = self.retry_info.manager.get(self.retry_info.request)
|
||||
new_download = download_manager.fetch(
|
||||
new_reply, suggested_filename=self.basename)
|
||||
self.do_retry.emit(new_download)
|
||||
self.cancel()
|
||||
new_reply = self._retry_info.manager.get(self._retry_info.request)
|
||||
self.do_retry.emit(new_reply)
|
||||
|
||||
@pyqtSlot()
|
||||
def open_file(self):
|
||||
"""Open the downloaded file."""
|
||||
assert self.successful
|
||||
@@ -385,24 +438,16 @@ class DownloadItem(QObject):
|
||||
"existing: {}, fileobj {}".format(
|
||||
filename, self._filename, self.fileobj))
|
||||
filename = os.path.expanduser(filename)
|
||||
if os.path.isabs(filename) and os.path.isdir(filename):
|
||||
# We got an absolute directory from the user, so we save it under
|
||||
# the default filename in that directory.
|
||||
self._filename = os.path.join(filename, self.basename)
|
||||
elif os.path.isabs(filename):
|
||||
# We got an absolute filename from the user, so we save it under
|
||||
# that filename.
|
||||
self._filename = filename
|
||||
self.basename = os.path.basename(self._filename)
|
||||
else:
|
||||
# We only got a filename (without directory) from the user, so we
|
||||
# save it under that filename in the default directory.
|
||||
download_dir = config.get('storage', 'download-directory')
|
||||
if download_dir is None:
|
||||
download_dir = standarddir.get(
|
||||
QStandardPaths.DownloadLocation)
|
||||
self._filename = os.path.join(download_dir, filename)
|
||||
self.basename = filename
|
||||
# Remove chars which can't be encoded in the filename encoding.
|
||||
# See https://github.com/The-Compiler/qutebrowser/issues/427
|
||||
encoding = sys.getfilesystemencoding()
|
||||
filename = utils.force_encoding(filename, encoding)
|
||||
if not self._create_full_filename(filename):
|
||||
# We only got a filename (without directory) or a relative path
|
||||
# from the user, so we append that to the default directory and
|
||||
# try again.
|
||||
self._create_full_filename(os.path.join(_download_dir(), filename))
|
||||
|
||||
log.downloads.debug("Setting filename to {}".format(filename))
|
||||
if os.path.isfile(self._filename):
|
||||
# The file already exists, so ask the user if it should be
|
||||
@@ -411,6 +456,25 @@ class DownloadItem(QObject):
|
||||
else:
|
||||
self._create_fileobj()
|
||||
|
||||
def _create_full_filename(self, filename):
|
||||
"""Try to create the full filename.
|
||||
|
||||
Return:
|
||||
True if the full filename was created, False otherwise.
|
||||
"""
|
||||
if os.path.isabs(filename) and os.path.isdir(filename):
|
||||
# We got an absolute directory from the user, so we save it under
|
||||
# the default filename in that directory.
|
||||
self._filename = os.path.join(filename, self.basename)
|
||||
return True
|
||||
elif os.path.isabs(filename):
|
||||
# We got an absolute filename from the user, so we save it under
|
||||
# that filename.
|
||||
self._filename = filename
|
||||
self.basename = os.path.basename(self._filename)
|
||||
return True
|
||||
return False
|
||||
|
||||
def set_fileobj(self, fileobj):
|
||||
""""Set the file object to write the download to.
|
||||
|
||||
@@ -557,7 +621,8 @@ class DownloadManager(QAbstractListModel):
|
||||
self._win_id = win_id
|
||||
self.downloads = []
|
||||
self.questions = []
|
||||
self._networkmanager = networkmanager.NetworkManager(win_id, self)
|
||||
self._networkmanager = networkmanager.NetworkManager(
|
||||
win_id, None, self)
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self, downloads=len(self.downloads))
|
||||
@@ -572,18 +637,6 @@ class DownloadManager(QAbstractListModel):
|
||||
self.questions.append(q)
|
||||
return q
|
||||
|
||||
@cmdutils.register(instance='download-manager', scope='window')
|
||||
def download(self, url, dest=None):
|
||||
"""Download a given URL, given as string.
|
||||
|
||||
Args:
|
||||
url: The URL to download
|
||||
dest: The file path to write the download to, or None to ask.
|
||||
"""
|
||||
url = urlutils.qurl_from_user_input(url)
|
||||
urlutils.raise_cmdexc_if_invalid(url)
|
||||
self.get(url, filename=dest)
|
||||
|
||||
@pyqtSlot('QUrl', 'QWebPage')
|
||||
def get(self, url, page=None, fileobj=None, filename=None,
|
||||
auto_remove=False):
|
||||
@@ -632,24 +685,31 @@ 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, filename, fileobj, page,
|
||||
auto_remove)
|
||||
return self.fetch_request(request, page, fileobj, filename,
|
||||
auto_remove, suggested_fn)
|
||||
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 = urlutils.filename_from_url(request.url())
|
||||
q.default = _path_suggestion(suggested_fn)
|
||||
message_bridge = objreg.get('message-bridge', scope='window',
|
||||
window=self._win_id)
|
||||
q.answered.connect(
|
||||
lambda fn: self.fetch_request(request, filename=fn, page=page,
|
||||
auto_remove=auto_remove))
|
||||
lambda fn: self.fetch_request(request, page, filename=fn,
|
||||
auto_remove=auto_remove,
|
||||
suggested_filename=suggested_fn))
|
||||
message_bridge.ask(q, blocking=False)
|
||||
return None
|
||||
|
||||
def fetch_request(self, request, page=None, fileobj=None, filename=None,
|
||||
auto_remove=False):
|
||||
auto_remove=False, suggested_filename=None):
|
||||
"""Download a QNetworkRequest to disk.
|
||||
|
||||
Args:
|
||||
@@ -668,10 +728,12 @@ class DownloadManager(QAbstractListModel):
|
||||
else:
|
||||
nam = page.networkAccessManager()
|
||||
reply = nam.get(request)
|
||||
return self.fetch(reply, fileobj, filename, auto_remove)
|
||||
return self.fetch(reply, fileobj, filename, auto_remove,
|
||||
suggested_filename)
|
||||
|
||||
@pyqtSlot('QNetworkReply')
|
||||
def fetch(self, reply, fileobj=None, filename=None, auto_remove=False):
|
||||
def fetch(self, reply, fileobj=None, filename=None, auto_remove=False,
|
||||
suggested_filename=None):
|
||||
"""Download a QNetworkReply to disk.
|
||||
|
||||
Args:
|
||||
@@ -686,12 +748,13 @@ class DownloadManager(QAbstractListModel):
|
||||
"""
|
||||
if fileobj is not None and filename is not None:
|
||||
raise TypeError("Only one of fileobj/filename may be given!")
|
||||
if filename is not None:
|
||||
suggested_filename = os.path.basename(filename)
|
||||
elif fileobj is not None and getattr(fileobj, 'name', None):
|
||||
suggested_filename = fileobj.name
|
||||
else:
|
||||
_inline, suggested_filename = http.parse_content_disposition(reply)
|
||||
if not suggested_filename:
|
||||
if filename is not None:
|
||||
suggested_filename = os.path.basename(filename)
|
||||
elif fileobj is not None and getattr(fileobj, 'name', None):
|
||||
suggested_filename = fileobj.name
|
||||
else:
|
||||
_, suggested_filename = http.parse_content_disposition(reply)
|
||||
log.downloads.debug("fetch: {} -> {}".format(reply.url(),
|
||||
suggested_filename))
|
||||
download = DownloadItem(reply, self._win_id, self)
|
||||
@@ -705,9 +768,9 @@ class DownloadManager(QAbstractListModel):
|
||||
download.error.connect(self.on_error)
|
||||
download.redirected.connect(
|
||||
functools.partial(self.on_redirect, download))
|
||||
download.do_retry.connect(self.fetch)
|
||||
download.basename = suggested_filename
|
||||
idx = len(self.downloads) + 1
|
||||
download.index = idx
|
||||
self.beginInsertRows(QModelIndex(), idx, idx)
|
||||
self.downloads.append(download)
|
||||
self.endInsertRows()
|
||||
@@ -719,7 +782,7 @@ class DownloadManager(QAbstractListModel):
|
||||
download.autoclose = False
|
||||
else:
|
||||
q = self._prepare_question()
|
||||
q.default = suggested_filename
|
||||
q.default = _path_suggestion(suggested_filename)
|
||||
q.answered.connect(download.set_filename)
|
||||
q.cancelled.connect(download.cancel)
|
||||
download.cancelled.connect(q.abort)
|
||||
@@ -730,20 +793,82 @@ class DownloadManager(QAbstractListModel):
|
||||
|
||||
return download
|
||||
|
||||
@cmdutils.register(instance='download-manager', scope='window')
|
||||
def cancel_download(self, count: {'special': 'count'}=1):
|
||||
def raise_no_download(self, count):
|
||||
"""Raise an exception that the download doesn't exist.
|
||||
|
||||
Args:
|
||||
count: The index of the download
|
||||
"""
|
||||
if not count:
|
||||
raise cmdexc.CommandError("There's no download!")
|
||||
raise cmdexc.CommandError("There's no download {}!".format(count))
|
||||
|
||||
@cmdutils.register(instance='download-manager', scope='window',
|
||||
count='count')
|
||||
def download_cancel(self, count=0):
|
||||
"""Cancel the last/[count]th download.
|
||||
|
||||
Args:
|
||||
count: The index of the download to cancel.
|
||||
"""
|
||||
try:
|
||||
download = self.downloads[count - 1]
|
||||
except IndexError:
|
||||
self.raise_no_download(count)
|
||||
if download.done:
|
||||
if not count:
|
||||
count = len(self.downloads)
|
||||
raise cmdexc.CommandError("Download {} is already done!"
|
||||
.format(count))
|
||||
download.cancel()
|
||||
|
||||
@cmdutils.register(instance='download-manager', scope='window',
|
||||
count='count')
|
||||
def download_delete(self, count=0):
|
||||
"""Delete the last/[count]th download from disk.
|
||||
|
||||
Args:
|
||||
count: The index of the download to cancel.
|
||||
"""
|
||||
try:
|
||||
download = self.downloads[count - 1]
|
||||
except IndexError:
|
||||
self.raise_no_download(count)
|
||||
if not download.successful:
|
||||
if not count:
|
||||
count = len(self.downloads)
|
||||
raise cmdexc.CommandError("Download {} is not done!".format(count))
|
||||
download.delete()
|
||||
self.remove_item(download)
|
||||
|
||||
@cmdutils.register(instance='download-manager', scope='window',
|
||||
deprecated="Use :download-cancel instead.",
|
||||
count='count')
|
||||
def cancel_download(self, count=1):
|
||||
"""Cancel the first/[count]th download.
|
||||
|
||||
Args:
|
||||
count: The index of the download to cancel.
|
||||
"""
|
||||
if count == 0:
|
||||
return
|
||||
self.download_cancel(count)
|
||||
|
||||
@cmdutils.register(instance='download-manager', scope='window',
|
||||
count='count')
|
||||
def download_open(self, count=0):
|
||||
"""Open the last/[count]th download.
|
||||
|
||||
Args:
|
||||
count: The index of the download to cancel.
|
||||
"""
|
||||
try:
|
||||
download = self.downloads[count - 1]
|
||||
except IndexError:
|
||||
raise cmdexc.CommandError("There's no download {}!".format(count))
|
||||
download.cancel()
|
||||
self.raise_no_download(count)
|
||||
if not download.successful:
|
||||
if not count:
|
||||
count = len(self.downloads)
|
||||
raise cmdexc.CommandError("Download {} is not done!".format(count))
|
||||
download.open_file()
|
||||
|
||||
@pyqtSlot(QNetworkRequest, QNetworkReply)
|
||||
def on_redirect(self, download, request, reply):
|
||||
@@ -785,21 +910,44 @@ class DownloadManager(QAbstractListModel):
|
||||
Return:
|
||||
A boolean.
|
||||
"""
|
||||
assert nam.adopted_downloads == 0
|
||||
for download in self.downloads:
|
||||
if download.reply is not None and download.reply.manager() is nam:
|
||||
return True
|
||||
return False
|
||||
running_download = (download.reply is not None and
|
||||
download.reply.manager() is nam)
|
||||
# user could request retry after tab is closed.
|
||||
failed_download = (download.done and (not download.successful) and
|
||||
download.retry_info.manager is nam)
|
||||
if running_download or failed_download:
|
||||
nam.adopt_download(download)
|
||||
return nam.adopted_downloads
|
||||
|
||||
def can_clear(self):
|
||||
"""Check if there are finished downloads to clear."""
|
||||
if self.downloads:
|
||||
return any(download.done for download in self.downloads)
|
||||
else:
|
||||
return False
|
||||
return any(download.done for download in self.downloads)
|
||||
|
||||
def clear(self):
|
||||
"""Remove all finished downloads."""
|
||||
self.remove_items(d for d in self.downloads if d.done)
|
||||
@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:
|
||||
all_: If given removes all finished downloads.
|
||||
count: The index of the download to cancel.
|
||||
"""
|
||||
if all_:
|
||||
finished_items = [d for d in self.downloads if d.done]
|
||||
self.remove_items(finished_items)
|
||||
else:
|
||||
try:
|
||||
download = self.downloads[count - 1]
|
||||
except IndexError:
|
||||
self.raise_no_download(count)
|
||||
if not download.done:
|
||||
if not count:
|
||||
count = len(self.downloads)
|
||||
raise cmdexc.CommandError("Download {} is not done!"
|
||||
.format(count))
|
||||
self.remove_item(download)
|
||||
|
||||
def last_index(self):
|
||||
"""Get the last index in the model.
|
||||
@@ -821,6 +969,7 @@ class DownloadManager(QAbstractListModel):
|
||||
del self.downloads[idx]
|
||||
self.endRemoveRows()
|
||||
download.deleteLater()
|
||||
self.update_indexes()
|
||||
|
||||
def remove_items(self, downloads):
|
||||
"""Remove an iterable of downloads."""
|
||||
@@ -850,6 +999,18 @@ class DownloadManager(QAbstractListModel):
|
||||
download.deleteLater()
|
||||
self.endRemoveRows()
|
||||
|
||||
def update_indexes(self):
|
||||
"""Update indexes of all DownloadItems."""
|
||||
first_idx = None
|
||||
for i, d in enumerate(self.downloads, 1):
|
||||
if first_idx is None and d.index != i:
|
||||
first_idx = i - 1
|
||||
d.index = i
|
||||
if first_idx is not None:
|
||||
model_idx = self.index(first_idx, 0)
|
||||
qtutils.ensure_valid(model_idx)
|
||||
self.dataChanged.emit(model_idx, self.last_index())
|
||||
|
||||
def headerData(self, section, orientation, role):
|
||||
"""Simple constant header."""
|
||||
if (section == 0 and orientation == Qt.Horizontal and
|
||||
@@ -868,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:
|
||||
@@ -886,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."""
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
@@ -31,9 +31,7 @@ from qutebrowser.utils import qtutils, utils, objreg
|
||||
|
||||
|
||||
def update_geometry(obj):
|
||||
"""WORKAROUND
|
||||
|
||||
This is a horrible workaround for some weird PyQt bug (probably).
|
||||
"""Weird WORKAROUND for some weird PyQt bug (probably).
|
||||
|
||||
This actually should be a method of DownloadView, but for some reason the
|
||||
rowsInserted/rowsRemoved signals don't get disconnected from this method
|
||||
@@ -44,7 +42,6 @@ def update_geometry(obj):
|
||||
Original bug: https://github.com/The-Compiler/qutebrowser/issues/167
|
||||
Workaround bug: https://github.com/The-Compiler/qutebrowser/issues/171
|
||||
"""
|
||||
|
||||
def _update_geometry():
|
||||
"""Actually update the geometry if the object still exists."""
|
||||
if sip.isdeleted(obj):
|
||||
@@ -126,7 +123,7 @@ class DownloadView(QListView):
|
||||
Return:
|
||||
A list of either:
|
||||
- (QAction, callable) tuples.
|
||||
- (None, None) for a seperator
|
||||
- (None, None) for a separator
|
||||
"""
|
||||
actions = []
|
||||
if item is None:
|
||||
@@ -142,7 +139,8 @@ class DownloadView(QListView):
|
||||
actions.append(("Cancel", item.cancel))
|
||||
if self.model().can_clear():
|
||||
actions.append((None, None))
|
||||
actions.append(("Remove all finished", self.model().clear))
|
||||
actions.append(("Remove all finished", functools.partial(
|
||||
self.model().download_remove, True)))
|
||||
return actions
|
||||
|
||||
@pyqtSlot('QPoint')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
@@ -21,28 +21,30 @@
|
||||
|
||||
import math
|
||||
import functools
|
||||
import subprocess
|
||||
import collections
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QEvent, Qt, QUrl
|
||||
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, QObject, QEvent, Qt, QUrl,
|
||||
QTimer)
|
||||
from PyQt5.QtGui import QMouseEvent, QClipboard
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PyQt5.QtWebKit import QWebElement
|
||||
from PyQt5.QtWebKitWidgets import QWebPage
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.keyinput import modeman, modeparsers
|
||||
from qutebrowser.browser import webelem
|
||||
from qutebrowser.commands import userscripts, cmdexc, cmdutils, runners
|
||||
from qutebrowser.utils import usertypes, log, qtutils, message, objreg
|
||||
from qutebrowser.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)
|
||||
@@ -60,10 +62,10 @@ class HintContext:
|
||||
frames: The QWebFrames to use.
|
||||
destroyed_frames: id()'s of QWebFrames which have been destroyed.
|
||||
(Workaround for https://github.com/The-Compiler/qutebrowser/issues/152)
|
||||
elems: A mapping from keystrings to (elem, label) namedtuples.
|
||||
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.
|
||||
@@ -73,6 +75,9 @@ class HintContext:
|
||||
spawn: Spawn a simple command.
|
||||
to_follow: The link to follow when enter is pressed.
|
||||
args: Custom arguments for userscript/spawn
|
||||
rapid: Whether to do rapid hinting.
|
||||
mainframe: The main QWebFrame where we started hinting in.
|
||||
group: The group of web elements to hint.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
@@ -80,9 +85,12 @@ class HintContext:
|
||||
self.target = None
|
||||
self.baseurl = None
|
||||
self.to_follow = None
|
||||
self.rapid = False
|
||||
self.frames = []
|
||||
self.destroyed_frames = []
|
||||
self.args = []
|
||||
self.mainframe = None
|
||||
self.group = None
|
||||
|
||||
def get_args(self, urlstr):
|
||||
"""Get the arguments, with {hint-url} replaced by the given URL."""
|
||||
@@ -108,28 +116,30 @@ class HintManager(QObject):
|
||||
Signals:
|
||||
mouse_event: Mouse event to be posted in the web view.
|
||||
arg: A QMouseEvent
|
||||
set_open_target: Set a new target to open the links in.
|
||||
start_hinting: Emitted when hinting starts, before a link is clicked.
|
||||
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_bg: "Follow hint in background tab...",
|
||||
Target.window: "Follow hint in new window...",
|
||||
Target.yank: "Yank hint to clipboard...",
|
||||
Target.yank_primary: "Yank hint to primary selection...",
|
||||
Target.run: "Run a command on a hint...",
|
||||
Target.fill: "Set hint in commandline...",
|
||||
Target.hover: "Hover over a hint...",
|
||||
Target.rapid: "Follow hint (rapid mode)...",
|
||||
Target.rapid_win: "Follow hint in new window (rapid mode)...",
|
||||
Target.download: "Download hint...",
|
||||
Target.userscript: "Call userscript via hint...",
|
||||
Target.spawn: "Spawn command via hint...",
|
||||
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",
|
||||
Target.yank_primary: "Yank hint to primary selection",
|
||||
Target.run: "Run a command on a hint",
|
||||
Target.fill: "Set hint in commandline",
|
||||
Target.hover: "Hover over a hint",
|
||||
Target.download: "Download hint",
|
||||
Target.userscript: "Call userscript via hint",
|
||||
Target.spawn: "Spawn command via hint",
|
||||
}
|
||||
|
||||
mouse_event = pyqtSignal('QMouseEvent')
|
||||
set_open_target = pyqtSignal(str)
|
||||
start_hinting = pyqtSignal(usertypes.ClickTarget)
|
||||
stop_hinting = pyqtSignal()
|
||||
|
||||
def __init__(self, win_id, tab_id, parent=None):
|
||||
"""Constructor."""
|
||||
@@ -141,6 +151,14 @@ class HintManager(QObject):
|
||||
window=win_id)
|
||||
mode_manager.left.connect(self.on_mode_left)
|
||||
|
||||
def _get_text(self):
|
||||
"""Get a hint text based on the current context."""
|
||||
text = self.HINT_TEXTS[self._context.target]
|
||||
if self._context.rapid:
|
||||
text += ' (rapid mode)'
|
||||
text += '...'
|
||||
return text
|
||||
|
||||
def _cleanup(self):
|
||||
"""Clean up after hinting."""
|
||||
for elem in self._context.elems.values():
|
||||
@@ -164,7 +182,7 @@ class HintManager(QObject):
|
||||
# See # https://github.com/The-Compiler/qutebrowser/issues/263
|
||||
pass
|
||||
log.hints.debug("Disconnected.")
|
||||
text = self.HINT_TEXTS[self._context.target]
|
||||
text = self._get_text()
|
||||
message_bridge = objreg.get('message-bridge', scope='window',
|
||||
window=self._win_id)
|
||||
message_bridge.maybe_reset_text(text)
|
||||
@@ -185,14 +203,32 @@ class HintManager(QObject):
|
||||
chars = '0123456789'
|
||||
else:
|
||||
chars = config.get('hints', 'chars')
|
||||
min_chars = config.get('hints', 'min-chars')
|
||||
if config.get('hints', 'scatter'):
|
||||
return self._hint_scattered(min_chars, chars, elems)
|
||||
else:
|
||||
return self._hint_linear(min_chars, chars, elems)
|
||||
|
||||
def _hint_scattered(self, min_chars, chars, elems):
|
||||
"""Produce scattered hint labels with variable length (like Vimium).
|
||||
|
||||
Args:
|
||||
min_chars: The minimum length of labels.
|
||||
chars: The alphabet to use for labels.
|
||||
elems: The elements to generate labels for.
|
||||
"""
|
||||
# Determine how many digits the link hints will require in the worst
|
||||
# case. Usually we do not need all of these digits for every link
|
||||
# single hint, so we can show shorter hints for a few of the links.
|
||||
needed = math.ceil(math.log(len(elems), len(chars)))
|
||||
needed = max(min_chars, math.ceil(math.log(len(elems), len(chars))))
|
||||
# Short hints are the number of hints we can possibly show which are
|
||||
# (needed - 1) digits in length.
|
||||
short_count = math.floor((len(chars) ** needed - len(elems)) /
|
||||
len(chars))
|
||||
if needed > min_chars:
|
||||
short_count = math.floor((len(chars) ** needed - len(elems)) /
|
||||
len(chars))
|
||||
else:
|
||||
short_count = 0
|
||||
|
||||
long_count = len(elems) - short_count
|
||||
|
||||
strings = []
|
||||
@@ -207,6 +243,20 @@ class HintManager(QObject):
|
||||
|
||||
return self._shuffle_hints(strings, len(chars))
|
||||
|
||||
def _hint_linear(self, min_chars, chars, elems):
|
||||
"""Produce linear hint labels with constant length (like dwb).
|
||||
|
||||
Args:
|
||||
min_chars: The minimum length of labels.
|
||||
chars: The alphabet to use for labels.
|
||||
elems: The elements to generate labels for.
|
||||
"""
|
||||
strings = []
|
||||
needed = max(min_chars, math.ceil(math.log(len(elems), len(chars))))
|
||||
for i in range(len(elems)):
|
||||
strings.append(self._number_to_hint_str(i, chars, needed))
|
||||
return strings
|
||||
|
||||
def _shuffle_hints(self, hints, length):
|
||||
"""Shuffle the given set of hints so that they're scattered.
|
||||
|
||||
@@ -267,6 +317,14 @@ class HintManager(QObject):
|
||||
display = elem.styleProperty('display', QWebElement.InlineStyle)
|
||||
return display == 'none'
|
||||
|
||||
def _show_elem(self, elem):
|
||||
"""Show a given element."""
|
||||
elem.setStyleProperty('display', 'inline !important')
|
||||
|
||||
def _hide_elem(self, elem):
|
||||
"""Hide a given element."""
|
||||
elem.setStyleProperty('display', 'none !important')
|
||||
|
||||
def _set_style_properties(self, elem, label):
|
||||
"""Set the hint CSS on the element given.
|
||||
|
||||
@@ -275,23 +333,23 @@ class HintManager(QObject):
|
||||
label: The label QWebElement.
|
||||
"""
|
||||
attrs = [
|
||||
('display', 'inline'),
|
||||
('z-index', '100000'),
|
||||
('pointer-events', 'none'),
|
||||
('position', 'absolute'),
|
||||
('color', config.get('colors', 'hints.fg')),
|
||||
('background', config.get('colors', 'hints.bg')),
|
||||
('font', config.get('fonts', 'hints')),
|
||||
('border', config.get('hints', 'border')),
|
||||
('opacity', str(config.get('hints', 'opacity'))),
|
||||
('display', 'inline !important'),
|
||||
('z-index', '{} !important'.format(int(2 ** 32 / 2 - 1))),
|
||||
('pointer-events', 'none !important'),
|
||||
('position', 'absolute !important'),
|
||||
('color', config.get('colors', 'hints.fg') + ' !important'),
|
||||
('background', config.get('colors', 'hints.bg') + ' !important'),
|
||||
('font', config.get('fonts', 'hints') + ' !important'),
|
||||
('border', config.get('hints', 'border') + ' !important'),
|
||||
('opacity', str(config.get('hints', 'opacity')) + ' !important'),
|
||||
]
|
||||
|
||||
# Make text uppercase if set in config
|
||||
if (config.get('hints', 'uppercase') and
|
||||
config.get('hints', 'mode') == 'letter'):
|
||||
attrs.append(('texttransform', 'uppercase'))
|
||||
attrs.append(('text-transform', 'uppercase !important'))
|
||||
else:
|
||||
attrs.append(('texttransform', 'none'))
|
||||
attrs.append(('text-transform', 'none !important'))
|
||||
|
||||
for k, v in attrs:
|
||||
label.setStyleProperty(k, v)
|
||||
@@ -313,8 +371,8 @@ class HintManager(QObject):
|
||||
top /= zoom
|
||||
log.hints.vdebug("Drawing label '{!r}' at {}/{} for element '{!r}', "
|
||||
"zoom level {}".format(label, left, top, elem, zoom))
|
||||
label.setStyleProperty('left', '{}px'.format(left))
|
||||
label.setStyleProperty('top', '{}px'.format(top))
|
||||
label.setStyleProperty('left', '{}px !important'.format(left))
|
||||
label.setStyleProperty('top', '{}px !important'.format(top))
|
||||
|
||||
def _draw_label(self, elem, string):
|
||||
"""Draw a hint label over an element.
|
||||
@@ -344,6 +402,11 @@ class HintManager(QObject):
|
||||
label.setPlainText(string)
|
||||
return label
|
||||
|
||||
def _show_url_error(self):
|
||||
"""Show an error because no link was found."""
|
||||
message.error(self._win_id, "No suitable link found for this element.",
|
||||
immediately=True)
|
||||
|
||||
def _click(self, elem, context):
|
||||
"""Click an element.
|
||||
|
||||
@@ -351,40 +414,57 @@ 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_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
|
||||
events = [
|
||||
QMouseEvent(QEvent.MouseMove, pos, Qt.NoButton, Qt.NoButton,
|
||||
Qt.NoModifier),
|
||||
]
|
||||
if target != Target.hover:
|
||||
self.set_open_target.emit(target.name)
|
||||
if context.target != Target.hover:
|
||||
events += [
|
||||
QMouseEvent(QEvent.MouseButtonPress, pos, Qt.LeftButton,
|
||||
Qt.NoButton, Qt.NoModifier),
|
||||
Qt.LeftButton, modifiers),
|
||||
QMouseEvent(QEvent.MouseButtonRelease, pos, Qt.LeftButton,
|
||||
Qt.NoButton, Qt.NoModifier),
|
||||
Qt.NoButton, modifiers),
|
||||
]
|
||||
for evt in events:
|
||||
self.mouse_event.emit(evt)
|
||||
if elem.is_text_input() and elem.is_editable():
|
||||
QTimer.singleShot(0, functools.partial(
|
||||
elem.webFrame().page().triggerAction,
|
||||
QWebPage.MoveToEndOfDocument))
|
||||
QTimer.singleShot(0, self.stop_hinting.emit)
|
||||
|
||||
def _yank(self, url, context):
|
||||
"""Yank an element to the clipboard or primary selection.
|
||||
|
||||
Args:
|
||||
url: The URL to open as a QURL.
|
||||
url: The URL to open as a QUrl.
|
||||
context: The HintContext to use.
|
||||
"""
|
||||
sel = context.target == Target.yank_primary
|
||||
@@ -432,24 +512,32 @@ class HintManager(QObject):
|
||||
"""
|
||||
url = self._resolve_url(elem, context.baseurl)
|
||||
if url is None:
|
||||
message.error(self._win_id,
|
||||
"No suitable link found for this element.",
|
||||
immediately=True)
|
||||
self._show_url_error()
|
||||
return
|
||||
download_manager = objreg.get('download-manager', scope='window',
|
||||
window=self._win_id)
|
||||
download_manager.get(url, elem.webFrame().page())
|
||||
|
||||
def _call_userscript(self, url, context):
|
||||
def _call_userscript(self, elem, context):
|
||||
"""Call an userscript from a hint.
|
||||
|
||||
Args:
|
||||
url: The URL to open as a QUrl.
|
||||
elem: The QWebElement to use in the userscript.
|
||||
context: The HintContext to use.
|
||||
"""
|
||||
cmd = context.args[0]
|
||||
args = context.args[1:]
|
||||
userscripts.run(cmd, *args, url=url, win_id=self._win_id)
|
||||
frame = context.mainframe
|
||||
env = {
|
||||
'QUTE_MODE': 'hints',
|
||||
'QUTE_SELECTED_TEXT': str(elem),
|
||||
'QUTE_SELECTED_HTML': elem.toOuterXml(),
|
||||
}
|
||||
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):
|
||||
"""Spawn a simple command from a hint.
|
||||
@@ -460,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.
|
||||
@@ -554,21 +640,17 @@ class HintManager(QObject):
|
||||
raise cmdexc.CommandError(
|
||||
"'args' is only allowed with target userscript/spawn.")
|
||||
|
||||
def _init_elements(self, mainframe, group):
|
||||
"""Initialize the elements and labels based on the context set.
|
||||
|
||||
Args:
|
||||
mainframe: The main QWebFrame.
|
||||
group: A Group enum member (which elements to find).
|
||||
"""
|
||||
def _init_elements(self):
|
||||
"""Initialize the elements and labels based on the context set."""
|
||||
elems = []
|
||||
for f in self._context.frames:
|
||||
elems += f.findAllElements(webelem.SELECTORS[group])
|
||||
elems = [e for e in elems if webelem.is_visible(e, mainframe)]
|
||||
elems += f.findAllElements(webelem.SELECTORS[self._context.group])
|
||||
elems = [e for e in elems
|
||||
if webelem.is_visible(e, self._context.mainframe)]
|
||||
# We wrap the elements late for performance reasons, as wrapping 1000s
|
||||
# of elements (with ~50 methods each) just takes too much time...
|
||||
elems = [webelem.WebElementWrapper(e) for e in elems]
|
||||
filterfunc = webelem.FILTERS.get(group, lambda e: True)
|
||||
filterfunc = webelem.FILTERS.get(self._context.group, lambda e: True)
|
||||
elems = [e for e in elems if filterfunc(e)]
|
||||
if not elems:
|
||||
raise cmdexc.CommandError("No elements found.")
|
||||
@@ -593,6 +675,7 @@ class HintManager(QObject):
|
||||
background: True to open in a background tab.
|
||||
window: True to open in a new window, False for the current one.
|
||||
"""
|
||||
from qutebrowser.mainwindow import mainwindow
|
||||
elem = self._find_prevnext(frame, prev)
|
||||
if elem is None:
|
||||
raise cmdexc.CommandError("No {} links found!".format(
|
||||
@@ -603,11 +686,10 @@ class HintManager(QObject):
|
||||
"prev" if prev else "forward"))
|
||||
qtutils.ensure_valid(url)
|
||||
if window:
|
||||
main_window = objreg.get('main-window', scope='window',
|
||||
window=self._win_id)
|
||||
win_id = main_window.spawn()
|
||||
new_window = mainwindow.MainWindow()
|
||||
new_window.show()
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=win_id)
|
||||
window=new_window.win_id)
|
||||
tabbed_browser.tabopen(url, background=False)
|
||||
elif tab:
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
@@ -618,12 +700,16 @@ class HintManager(QObject):
|
||||
tab=self._tab_id)
|
||||
webview.openurl(url)
|
||||
|
||||
@cmdutils.register(instance='hintmanager', scope='tab', name='hint')
|
||||
def start(self, group=webelem.Group.all, target=Target.normal,
|
||||
*args: {'nargs': '*'}):
|
||||
@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):
|
||||
"""Start hinting.
|
||||
|
||||
Args:
|
||||
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`.
|
||||
group: The hinting mode to use.
|
||||
|
||||
- `all`: All clickable elements.
|
||||
@@ -633,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.
|
||||
@@ -642,9 +730,6 @@ class HintManager(QObject):
|
||||
- `run`: Run the argument as command.
|
||||
- `fill`: Fill the commandline with the command given as
|
||||
argument.
|
||||
- `rapid`: Open the link in a new tab and stay in hinting mode.
|
||||
- `rapid-win`: Open the link in a new window and stay in
|
||||
hinting mode.
|
||||
- `download`: Download the link.
|
||||
- `userscript`: Call an userscript with `$QUTE_URL` set to the
|
||||
link.
|
||||
@@ -672,11 +757,28 @@ 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:
|
||||
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()
|
||||
self._context.target = target
|
||||
self._context.baseurl = tabbed_browser.current_url()
|
||||
self._context.rapid = rapid
|
||||
try:
|
||||
self._context.baseurl = tabbed_browser.current_url()
|
||||
except qtutils.QtValueError:
|
||||
raise cmdexc.CommandError("No URL set for this page yet!")
|
||||
self._context.frames = webelem.get_child_frames(mainframe)
|
||||
for frame in self._context.frames:
|
||||
# WORKAROUND for
|
||||
@@ -684,14 +786,40 @@ class HintManager(QObject):
|
||||
frame.destroyed.connect(functools.partial(
|
||||
self._context.destroyed_frames.append, id(frame)))
|
||||
self._context.args = args
|
||||
self._init_elements(mainframe, group)
|
||||
self._context.mainframe = mainframe
|
||||
self._context.group = group
|
||||
self._handle_old_rapid_targets(win_id)
|
||||
self._init_elements()
|
||||
message_bridge = objreg.get('message-bridge', scope='window',
|
||||
window=self._win_id)
|
||||
message_bridge.set_text(self.HINT_TEXTS[target])
|
||||
message_bridge.set_text(self._get_text())
|
||||
self._connect_frame_signals()
|
||||
modeman.enter(self._win_id, usertypes.KeyMode.hint,
|
||||
'HintManager.start')
|
||||
|
||||
def _handle_old_rapid_targets(self, win_id):
|
||||
"""Switch to the new way for rapid hinting with a rapid target.
|
||||
|
||||
Args:
|
||||
win_id: The window ID to display the warning in.
|
||||
|
||||
DEPRECATED.
|
||||
"""
|
||||
old_rapid_targets = {
|
||||
Target.rapid: Target.tab_bg,
|
||||
Target.rapid_win: Target.window,
|
||||
}
|
||||
target = self._context.target
|
||||
if target in old_rapid_targets:
|
||||
self._context.target = old_rapid_targets[target]
|
||||
self._context.rapid = True
|
||||
name = target.name.replace('_', '-')
|
||||
group_name = self._context.group.name.replace('_', '-')
|
||||
new_name = self._context.target.name.replace('_', '-')
|
||||
message.warning(
|
||||
win_id, ':hint with target {} is deprecated, use :hint '
|
||||
'--rapid {} {} instead!'.format(name, group_name, new_name))
|
||||
|
||||
def handle_partial_key(self, keystr):
|
||||
"""Handle a new partial keypress."""
|
||||
log.hints.debug("Handling new keystring: '{}'".format(keystr))
|
||||
@@ -705,11 +833,11 @@ class HintManager(QObject):
|
||||
'<font color="{}">{}</font>{}'.format(
|
||||
match_color, matched, rest))
|
||||
if self._is_hidden(elems.label):
|
||||
# hidden element which matches again -> unhide it
|
||||
elems.label.setStyleProperty('display', 'inline')
|
||||
# hidden element which matches again -> show it
|
||||
self._show_elem(elems.label)
|
||||
else:
|
||||
# element doesn't match anymore -> hide it
|
||||
elems.label.setStyleProperty('display', 'none')
|
||||
self._hide_elem(elems.label)
|
||||
except webelem.IsNullError:
|
||||
pass
|
||||
|
||||
@@ -724,17 +852,20 @@ class HintManager(QObject):
|
||||
if (filterstr is None or
|
||||
str(elems.elem).lower().startswith(filterstr)):
|
||||
if self._is_hidden(elems.label):
|
||||
# hidden element which matches again -> unhide it
|
||||
elems.label.setStyleProperty('display', 'inline')
|
||||
# hidden element which matches again -> show it
|
||||
self._show_elem(elems.label)
|
||||
else:
|
||||
# element doesn't match anymore -> hide it
|
||||
elems.label.setStyleProperty('display', 'none')
|
||||
self._hide_elem(elems.label)
|
||||
except webelem.IsNullError:
|
||||
pass
|
||||
visible = {}
|
||||
for k, e in self._context.elems.items():
|
||||
if not self._is_hidden(e.label):
|
||||
visible[k] = e
|
||||
try:
|
||||
if not self._is_hidden(e.label):
|
||||
visible[k] = e
|
||||
except webelem.IsNullError:
|
||||
pass
|
||||
if not visible:
|
||||
# Whoops, filtered all hints
|
||||
modeman.leave(self._win_id, usertypes.KeyMode.hint, 'all filtered')
|
||||
@@ -757,13 +888,13 @@ 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.rapid: self._click,
|
||||
Target.rapid_win: self._click,
|
||||
Target.hover: self._click,
|
||||
# _download needs a QWebElement to get the frame.
|
||||
Target.download: self._download,
|
||||
Target.userscript: self._call_userscript,
|
||||
}
|
||||
# Handlers which take a QUrl
|
||||
url_handlers = {
|
||||
@@ -771,25 +902,26 @@ class HintManager(QObject):
|
||||
Target.yank_primary: self._yank,
|
||||
Target.run: self._run_cmd,
|
||||
Target.fill: self._preset_cmd_text,
|
||||
Target.userscript: self._call_userscript,
|
||||
Target.spawn: self._spawn,
|
||||
}
|
||||
elem = self._context.elems[keystr].elem
|
||||
if elem.webFrame() is None:
|
||||
message.error(self._win_id, "This element has no webframe.",
|
||||
immediately=True)
|
||||
return
|
||||
if self._context.target in elem_handlers:
|
||||
handler = functools.partial(
|
||||
elem_handlers[self._context.target], elem, self._context)
|
||||
elif self._context.target in url_handlers:
|
||||
url = self._resolve_url(elem, self._context.baseurl)
|
||||
if url is None:
|
||||
message.error(self._win_id,
|
||||
"No suitable link found for this element.",
|
||||
immediately=True)
|
||||
self._show_url_error()
|
||||
return
|
||||
handler = functools.partial(
|
||||
url_handlers[self._context.target], url, self._context)
|
||||
else:
|
||||
raise ValueError("No suitable handler found!")
|
||||
if self._context.target not in (Target.rapid, Target.rapid_win):
|
||||
if not self._context.rapid:
|
||||
modeman.maybe_leave(self._win_id, usertypes.KeyMode.hint,
|
||||
'followed')
|
||||
else:
|
||||
|
||||
216
qutebrowser/browser/history.py
Normal file
216
qutebrowser/browser/history.py
Normal file
@@ -0,0 +1,216 @@
|
||||
# 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/>.
|
||||
|
||||
"""Simple history which gets written to disk."""
|
||||
|
||||
import time
|
||||
import collections
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, QUrl
|
||||
from PyQt5.QtWebKit import QWebHistoryInterface
|
||||
|
||||
from qutebrowser.utils import utils, objreg, standarddir, log
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.misc import lineparser
|
||||
|
||||
|
||||
class HistoryEntry:
|
||||
|
||||
"""A single entry in the web history.
|
||||
|
||||
Attributes:
|
||||
atime: The time the page was accessed.
|
||||
url: The URL which was accessed as QUrl.
|
||||
url_string: The URL which was accessed as string.
|
||||
"""
|
||||
|
||||
def __init__(self, atime, url):
|
||||
self.atime = float(atime)
|
||||
self.url = QUrl(url)
|
||||
self.url_string = url
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self, constructor=True, atime=self.atime,
|
||||
url=self.url.toDisplayString())
|
||||
|
||||
def __str__(self):
|
||||
return '{} {}'.format(int(self.atime), self.url_string)
|
||||
|
||||
@classmethod
|
||||
def from_str(cls, s):
|
||||
"""Get a history based on a 'TIME URL' string."""
|
||||
return cls(*s.split(' ', maxsplit=1))
|
||||
|
||||
|
||||
class WebHistory(QWebHistoryInterface):
|
||||
|
||||
"""A QWebHistoryInterface which supports being written to disk.
|
||||
|
||||
Attributes:
|
||||
_lineparser: The AppendLineParser used to save the history.
|
||||
_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:
|
||||
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.
|
||||
"""
|
||||
|
||||
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()
|
||||
self._temp_history = collections.OrderedDict()
|
||||
self._new_history = []
|
||||
self._saved_count = 0
|
||||
objreg.get('save-manager').add_saveable(
|
||||
'history', self.save, self.item_added)
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self, length=len(self))
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self._new_history[key]
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._history_dict.values())
|
||||
|
||||
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()
|
||||
return old + [str(e) for e in self._new_history]
|
||||
|
||||
def save(self):
|
||||
"""Save the history to disk."""
|
||||
new = (str(e) for e in self._new_history[self._saved_count:])
|
||||
self._lineparser.new_data = new
|
||||
self._lineparser.save()
|
||||
self._saved_count = len(self._new_history)
|
||||
|
||||
def addHistoryEntry(self, url_string):
|
||||
"""Called by WebKit when an URL should be added to the history.
|
||||
|
||||
Args:
|
||||
url_string: An url as string to add to the history.
|
||||
"""
|
||||
if not url_string:
|
||||
return
|
||||
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._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.
|
||||
|
||||
Args:
|
||||
url_string: The URL (as string) to check for.
|
||||
|
||||
Return:
|
||||
True if the url is in the history, False otherwise.
|
||||
"""
|
||||
return url_string in self._history_dict
|
||||
|
||||
|
||||
def init(parent=None):
|
||||
"""Initialize the web history.
|
||||
|
||||
Args:
|
||||
parent: The parent to use for WebHistory.
|
||||
"""
|
||||
history = WebHistory(parent)
|
||||
objreg.register('web-history', history)
|
||||
QWebHistoryInterface.setDefaultInterface(history)
|
||||
@@ -1,6 +1,6 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
@@ -17,7 +17,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Other utilities which don't fit anywhere else. """
|
||||
"""Other utilities which don't fit anywhere else."""
|
||||
|
||||
|
||||
import os.path
|
||||
@@ -50,7 +50,7 @@ def parse_content_disposition(reply):
|
||||
bytes(reply.rawHeader(content_disposition_header)))
|
||||
filename = content_disposition.filename()
|
||||
except UnicodeDecodeError:
|
||||
log.misc.exception("Error while decoding filename")
|
||||
log.rfc6266.exception("Error while decoding filename")
|
||||
else:
|
||||
is_inline = content_disposition.is_inline()
|
||||
# Then try to get filename from url
|
||||
|
||||
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.")
|
||||
@@ -1,6 +1,6 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
@@ -19,23 +19,45 @@
|
||||
|
||||
"""Our own QNetworkAccessManager."""
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot, pyqtSignal, PYQT_VERSION, QCoreApplication
|
||||
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkReply
|
||||
import collections
|
||||
|
||||
try:
|
||||
from PyQt5.QtNetwork import QSslSocket
|
||||
except ImportError:
|
||||
SSL_AVAILABLE = False
|
||||
else:
|
||||
SSL_AVAILABLE = QSslSocket.supportsSsl()
|
||||
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, PYQT_VERSION, QCoreApplication,
|
||||
QUrl)
|
||||
from PyQt5.QtNetwork import (QNetworkAccessManager, QNetworkReply, QSslError,
|
||||
QSslSocket)
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import message, log, usertypes, utils, objreg
|
||||
from qutebrowser.utils import (message, log, usertypes, utils, objreg, qtutils,
|
||||
urlutils)
|
||||
from qutebrowser.browser import cookies
|
||||
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 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):
|
||||
|
||||
"""A QSslError subclass which provides __hash__ on Qt < 5.4."""
|
||||
|
||||
def __hash__(self):
|
||||
try:
|
||||
# Qt >= 5.4
|
||||
return super().__hash__()
|
||||
except TypeError:
|
||||
return hash((self.certificate().toDer(), self.error()))
|
||||
|
||||
|
||||
class NetworkManager(QNetworkAccessManager):
|
||||
@@ -43,10 +65,18 @@ class NetworkManager(QNetworkAccessManager):
|
||||
"""Our own QNetworkAccessManager.
|
||||
|
||||
Attributes:
|
||||
adopted_downloads: If downloads are running with this QNAM but the
|
||||
associated tab gets closed already, the NAM gets
|
||||
reparented to the DownloadManager. This counts the
|
||||
still running downloads, so the QNAM can clean
|
||||
itself up when this reaches zero again.
|
||||
_requests: Pending requests.
|
||||
_scheme_handlers: A dictionary (scheme -> handler) of supported custom
|
||||
schemes.
|
||||
_win_id: The window ID this NetworkManager is associated with.
|
||||
_tab_id: The tab ID this NetworkManager is associated with.
|
||||
_rejected_ssl_errors: A {QUrl: [SslError]} dict of rejected errors.
|
||||
_accepted_ssl_errors: A {QUrl: [SslError]} dict of accepted errors.
|
||||
|
||||
Signals:
|
||||
shutting_down: Emitted when the QNAM is shutting down.
|
||||
@@ -54,22 +84,25 @@ class NetworkManager(QNetworkAccessManager):
|
||||
|
||||
shutting_down = pyqtSignal()
|
||||
|
||||
def __init__(self, win_id, parent=None):
|
||||
def __init__(self, win_id, tab_id, parent=None):
|
||||
log.init.debug("Initializing NetworkManager")
|
||||
with log.disable_qt_msghandler():
|
||||
# WORKAROUND for a hang when a message is printed - See:
|
||||
# http://www.riverbankcomputing.com/pipermail/pyqt/2014-November/035045.html
|
||||
super().__init__(parent)
|
||||
log.init.debug("NetworkManager init done")
|
||||
self.adopted_downloads = 0
|
||||
self._win_id = win_id
|
||||
self._tab_id = tab_id
|
||||
self._requests = []
|
||||
self._scheme_handlers = {
|
||||
'qute': qutescheme.QuteSchemeHandler(win_id),
|
||||
}
|
||||
self._set_cookiejar()
|
||||
self._set_cache()
|
||||
if SSL_AVAILABLE:
|
||||
self.sslErrors.connect(self.on_ssl_errors)
|
||||
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)
|
||||
@@ -105,11 +138,10 @@ class NetworkManager(QNetworkAccessManager):
|
||||
self.setCache(cache)
|
||||
cache.setParent(app)
|
||||
|
||||
def _ask(self, win_id, text, mode, owner=None):
|
||||
def _ask(self, text, mode, owner=None):
|
||||
"""Ask a blocking question in the statusbar.
|
||||
|
||||
Args:
|
||||
win_id: The ID of the window which is calling this function.
|
||||
text: The text to display to the user.
|
||||
mode: A PromptMode.
|
||||
owner: An object which will abort the question if destroyed, or
|
||||
@@ -124,21 +156,15 @@ class NetworkManager(QNetworkAccessManager):
|
||||
self.shutting_down.connect(q.abort)
|
||||
if owner is not None:
|
||||
owner.destroyed.connect(q.abort)
|
||||
bridge = objreg.get('message-bridge', scope='window', window=win_id)
|
||||
webview = objreg.get('webview', scope='tab', window=self._win_id,
|
||||
tab=self._tab_id)
|
||||
webview.loadStarted.connect(q.abort)
|
||||
bridge = objreg.get('message-bridge', scope='window',
|
||||
window=self._win_id)
|
||||
bridge.ask(q, blocking=True)
|
||||
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* explicitely 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)
|
||||
@@ -148,7 +174,7 @@ class NetworkManager(QNetworkAccessManager):
|
||||
self.shutting_down.emit()
|
||||
|
||||
@pyqtSlot('QNetworkReply*', 'QList<QSslError>')
|
||||
def on_ssl_errors(self, reply, errors):
|
||||
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.
|
||||
@@ -157,15 +183,37 @@ class NetworkManager(QNetworkAccessManager):
|
||||
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':
|
||||
err_string = '\n'.join('- ' + err.errorString() for err in errors)
|
||||
answer = self._ask(self._win_id,
|
||||
'SSL errors - continue?\n{}'.format(err_string),
|
||||
mode=usertypes.PromptMode.yesno,
|
||||
owner=reply)
|
||||
if answer:
|
||||
try:
|
||||
host_tpl = urlutils.host_tuple(reply.url())
|
||||
except ValueError:
|
||||
host_tpl = None
|
||||
is_accepted = False
|
||||
is_rejected = False
|
||||
else:
|
||||
is_accepted = set(errors).issubset(
|
||||
self._accepted_ssl_errors[host_tpl])
|
||||
is_rejected = set(errors).issubset(
|
||||
self._rejected_ssl_errors[host_tpl])
|
||||
if is_accepted:
|
||||
reply.ignoreSslErrors()
|
||||
elif is_rejected:
|
||||
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()
|
||||
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:
|
||||
@@ -176,21 +224,43 @@ class NetworkManager(QNetworkAccessManager):
|
||||
'SSL error: {}'.format(err.errorString()))
|
||||
reply.ignoreSslErrors()
|
||||
|
||||
@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
|
||||
|
||||
@pyqtSlot('QNetworkReply', 'QAuthenticator')
|
||||
def on_authentication_required(self, reply, authenticator):
|
||||
"""Called when a website needs authentication."""
|
||||
answer = self._ask(self._win_id,
|
||||
"Username ({}):".format(authenticator.realm()),
|
||||
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(self._win_id, "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):
|
||||
@@ -203,6 +273,28 @@ class NetworkManager(QNetworkAccessManager):
|
||||
# switched from private mode to normal mode
|
||||
self._set_cookiejar()
|
||||
|
||||
@pyqtSlot()
|
||||
def on_adopted_download_destroyed(self):
|
||||
"""Check if we can clean up if an adopted download was destroyed.
|
||||
|
||||
See the description for adopted_downloads for details.
|
||||
"""
|
||||
self.adopted_downloads -= 1
|
||||
log.downloads.debug("Adopted download destroyed, {} left.".format(
|
||||
self.adopted_downloads))
|
||||
assert self.adopted_downloads >= 0
|
||||
if self.adopted_downloads == 0:
|
||||
self.deleteLater()
|
||||
|
||||
@pyqtSlot(object) # DownloadItem
|
||||
def adopt_download(self, download):
|
||||
"""Adopt a new DownloadItem."""
|
||||
self.adopted_downloads += 1
|
||||
log.downloads.debug("Adopted download, {} adopted.".format(
|
||||
self.adopted_downloads))
|
||||
download.destroyed.connect(self.on_adopted_download_destroyed)
|
||||
download.do_retry.connect(self.adopt_download)
|
||||
|
||||
# WORKAROUND for:
|
||||
# http://www.riverbankcomputing.com/pipermail/pyqt/2014-September/034806.html
|
||||
#
|
||||
@@ -225,20 +317,20 @@ 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)
|
||||
|
||||
host_blocker = objreg.get('host-blocker')
|
||||
if (op == QNetworkAccessManager.GetOperation and
|
||||
req.url().host() in objreg.get('host-blocker').blocked_hosts):
|
||||
req.url().host() in host_blocker.blocked_hosts and
|
||||
config.get('content', 'host-blocking-enabled')):
|
||||
log.webview.info("Request to {} blocked by host blocker.".format(
|
||||
req.url().host()))
|
||||
return networkreply.ErrorNetworkReply(
|
||||
req, HOSTBLOCK_ERROR_STRING, QNetworkReply.ContentAccessDenied,
|
||||
self)
|
||||
|
||||
if config.get('network', 'do-not-track'):
|
||||
dnt = '1'.encode('ascii')
|
||||
else:
|
||||
@@ -262,5 +354,5 @@ class NetworkManager(QNetworkAccessManager):
|
||||
else:
|
||||
reply = super().createRequest(op, req, outgoing_data)
|
||||
self._requests.append(reply)
|
||||
reply.destroyed.connect(lambda obj: self._requests.remove(obj))
|
||||
reply.destroyed.connect(self._requests.remove)
|
||||
return reply
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# Based on the Eric5 helpviewer,
|
||||
# Copyright (c) 2009 - 2014 Detlev Offenbach <detlev@die-offenbachs.de>
|
||||
@@ -54,6 +54,7 @@ class FixedDataNetworkReply(QNetworkReply):
|
||||
self.setAttribute(QNetworkRequest.HttpReasonPhraseAttribute, 'OK')
|
||||
# For some reason, a segfault will be triggered if these lambdas aren't
|
||||
# there.
|
||||
# pylint: disable=unnecessary-lambda
|
||||
QTimer.singleShot(0, lambda: self.metaDataChanged.emit())
|
||||
QTimer.singleShot(0, lambda: self.readyRead.emit())
|
||||
QTimer.singleShot(0, lambda: self.finished.emit())
|
||||
@@ -112,6 +113,7 @@ class ErrorNetworkReply(QNetworkReply):
|
||||
self.setError(error, errorstring)
|
||||
# For some reason, a segfault will be triggered if these lambdas aren't
|
||||
# there.
|
||||
# pylint: disable=unnecessary-lambda
|
||||
QTimer.singleShot(0, lambda: self.error.emit(error))
|
||||
QTimer.singleShot(0, lambda: self.finished.emit())
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
@@ -19,21 +19,19 @@
|
||||
|
||||
"""Client for the pastebin."""
|
||||
|
||||
import functools
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, QObject, QUrl
|
||||
from PyQt5.QtNetwork import (QNetworkAccessManager, QNetworkRequest,
|
||||
QNetworkReply)
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QUrl
|
||||
|
||||
from qutebrowser.misc import httpclient
|
||||
|
||||
|
||||
class PastebinClient(QObject):
|
||||
|
||||
"""A client for http://p.cmpl.cc/ using QNetworkAccessManager.
|
||||
"""A client for http://p.cmpl.cc/ using HTTPClient.
|
||||
|
||||
Attributes:
|
||||
_nam: The QNetworkAccessManager used.
|
||||
_client: The HTTPClient used.
|
||||
|
||||
Class attributes:
|
||||
API_URL: The base API URL.
|
||||
@@ -51,7 +49,9 @@ class PastebinClient(QObject):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._nam = QNetworkAccessManager(self)
|
||||
self._client = httpclient.HTTPClient(self)
|
||||
self._client.error.connect(self.error)
|
||||
self._client.success.connect(self.on_client_success)
|
||||
|
||||
def paste(self, name, title, text, parent=None):
|
||||
"""Paste the text into a pastebin and return the URL.
|
||||
@@ -69,33 +69,17 @@ class PastebinClient(QObject):
|
||||
}
|
||||
if parent is not None:
|
||||
data['reply'] = parent
|
||||
encoded_data = urllib.parse.urlencode(data).encode('utf-8')
|
||||
create_url = urllib.parse.urljoin(self.API_URL, 'create')
|
||||
request = QNetworkRequest(QUrl(create_url))
|
||||
request.setHeader(QNetworkRequest.ContentTypeHeader,
|
||||
'application/x-www-form-urlencoded;charset=utf-8')
|
||||
reply = self._nam.post(request, encoded_data)
|
||||
if reply.isFinished():
|
||||
self.on_reply_finished(reply)
|
||||
else:
|
||||
reply.finished.connect(functools.partial(
|
||||
self.on_reply_finished, reply))
|
||||
url = QUrl(urllib.parse.urljoin(self.API_URL, 'create'))
|
||||
self._client.post(url, data)
|
||||
|
||||
def on_reply_finished(self, reply):
|
||||
"""Read the data and finish when the reply finished.
|
||||
@pyqtSlot(str)
|
||||
def on_client_success(self, data):
|
||||
"""Process the data and finish when the client finished.
|
||||
|
||||
Args:
|
||||
reply: The QNetworkReply which finished.
|
||||
data: A string with the received data.
|
||||
"""
|
||||
if reply.error() != QNetworkReply.NoError:
|
||||
self.error.emit(reply.errorString())
|
||||
return
|
||||
try:
|
||||
url = bytes(reply.readAll()).decode('utf-8')
|
||||
except UnicodeDecodeError:
|
||||
self.error.emit("Invalid UTF-8 data received in reply!")
|
||||
return
|
||||
if url.startswith('http://'):
|
||||
self.success.emit(url)
|
||||
if data.startswith('http://'):
|
||||
self.success.emit(data)
|
||||
else:
|
||||
self.error.emit("Invalid data received in reply!")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
@@ -19,7 +19,9 @@
|
||||
#
|
||||
# pylint complains when using .render() on jinja templates, so we make it shut
|
||||
# up for this whole module.
|
||||
# pylint: disable=maybe-no-member
|
||||
|
||||
# pylint: disable=no-member
|
||||
# https://bitbucket.org/logilab/pylint/issue/490/
|
||||
|
||||
"""Handler functions for different qute:... pages.
|
||||
|
||||
@@ -27,6 +29,7 @@ Module attributes:
|
||||
pyeval_output: The output of the last :pyeval command.
|
||||
"""
|
||||
|
||||
import functools
|
||||
import configparser
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot, QObject
|
||||
@@ -93,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:
|
||||
@@ -150,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='')
|
||||
@@ -168,9 +177,11 @@ def qute_help(win_id, request):
|
||||
|
||||
|
||||
def qute_settings(win_id, _request):
|
||||
"""Handler for qute:settings. View/change qute configuration"""
|
||||
"""Handler for qute:settings. View/change qute configuration."""
|
||||
config_getter = functools.partial(objreg.get('config').get, raw=True)
|
||||
html = jinja.env.get_template('settings.html').render(
|
||||
win_id=win_id, title='settings', config=configdata)
|
||||
win_id=win_id, title='settings', config=configdata,
|
||||
confget=config_getter)
|
||||
return html.encode('UTF-8', errors='xmlcharrefreplace')
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# Based on the Eric5 helpviewer,
|
||||
# Copyright (c) 2009 - 2014 Detlev Offenbach <detlev@die-offenbachs.de>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
@@ -24,14 +24,15 @@ OrderedDict. This is because we read them from a file at start and write them
|
||||
to a file on shutdown, so it makes sense to keep them as strings here.
|
||||
"""
|
||||
|
||||
import os.path
|
||||
import functools
|
||||
import collections
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, QStandardPaths, QUrl, QObject
|
||||
from PyQt5.QtCore import pyqtSignal, QUrl, QObject
|
||||
|
||||
from qutebrowser.utils import message, usertypes, urlutils, standarddir
|
||||
from qutebrowser.utils import message, usertypes, urlutils, standarddir, objreg
|
||||
from qutebrowser.commands import cmdexc, cmdutils
|
||||
from qutebrowser.config.parsers import line as lineparser
|
||||
from qutebrowser.misc import lineparser
|
||||
|
||||
|
||||
class QuickmarkManager(QObject):
|
||||
@@ -40,10 +41,21 @@ class QuickmarkManager(QObject):
|
||||
|
||||
Attributes:
|
||||
marks: An OrderedDict of all quickmarks.
|
||||
_linecp: The LineConfigParser used for the quickmarks.
|
||||
_lineparser: The LineParser used for the quickmarks, or None
|
||||
(when qutebrowser is started with -c '').
|
||||
|
||||
Signals:
|
||||
changed: Emitted when anything changed.
|
||||
added: Emitted when a new quickmark was added.
|
||||
arg 0: The name of the quickmark.
|
||||
arg 1: The URL of the quickmark, as string.
|
||||
removed: Emitted when an existing quickmark was removed.
|
||||
arg 0: The name of the quickmark.
|
||||
"""
|
||||
|
||||
changed = pyqtSignal()
|
||||
added = pyqtSignal(str, str)
|
||||
removed = pyqtSignal(str)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
"""Initialize and read quickmarks."""
|
||||
@@ -51,20 +63,32 @@ class QuickmarkManager(QObject):
|
||||
|
||||
self.marks = collections.OrderedDict()
|
||||
|
||||
confdir = standarddir.get(QStandardPaths.ConfigLocation)
|
||||
self._linecp = lineparser.LineConfigParser(confdir, 'quickmarks')
|
||||
for line in self._linecp:
|
||||
try:
|
||||
key, url = line.rsplit(maxsplit=1)
|
||||
except ValueError:
|
||||
message.error(0, "Invalid quickmark '{}'".format(line))
|
||||
else:
|
||||
self.marks[key] = url
|
||||
if standarddir.config() is None:
|
||||
self._lineparser = None
|
||||
else:
|
||||
self._lineparser = lineparser.LineParser(
|
||||
standarddir.config(), 'quickmarks', parent=self)
|
||||
for line in self._lineparser:
|
||||
if not line.strip():
|
||||
# Ignore empty or whitespace-only lines.
|
||||
continue
|
||||
try:
|
||||
key, url = line.rsplit(maxsplit=1)
|
||||
except ValueError:
|
||||
message.error(0, "Invalid quickmark '{}'".format(line))
|
||||
else:
|
||||
self.marks[key] = url
|
||||
filename = os.path.join(standarddir.config(), 'quickmarks')
|
||||
objreg.get('save-manager').add_saveable(
|
||||
'quickmark-manager', self.save, self.changed,
|
||||
filename=filename)
|
||||
|
||||
def save(self):
|
||||
"""Save the quickmarks to disk."""
|
||||
self._linecp.data = [' '.join(tpl) for tpl in self.marks.items()]
|
||||
self._linecp.save()
|
||||
if self._lineparser is not None:
|
||||
self._lineparser.data = [' '.join(tpl)
|
||||
for tpl in self.marks.items()]
|
||||
self._lineparser.save()
|
||||
|
||||
def prompt_save(self, win_id, url):
|
||||
"""Prompt for a new quickmark name to be added and add it.
|
||||
@@ -81,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:
|
||||
@@ -103,6 +127,7 @@ class QuickmarkManager(QObject):
|
||||
"""Really set the quickmark."""
|
||||
self.marks[name] = url
|
||||
self.changed.emit()
|
||||
self.added.emit(name, url)
|
||||
|
||||
if name in self.marks:
|
||||
message.confirm_async(
|
||||
@@ -124,6 +149,7 @@ class QuickmarkManager(QObject):
|
||||
raise cmdexc.CommandError("Quickmark '{}' not found!".format(name))
|
||||
else:
|
||||
self.changed.emit()
|
||||
self.removed.emit(name)
|
||||
|
||||
def get(self, name):
|
||||
"""Get the URL of the quickmark named name as a QUrl."""
|
||||
@@ -132,9 +158,12 @@ class QuickmarkManager(QObject):
|
||||
"Quickmark '{}' does not exist!".format(name))
|
||||
urlstr = self.marks[name]
|
||||
try:
|
||||
url = urlutils.fuzzy_url(urlstr)
|
||||
except urlutils.FuzzyUrlError:
|
||||
raise cmdexc.CommandError(
|
||||
"Invalid URL for quickmark {}: {} ({})".format(
|
||||
name, urlstr, url.errorString()))
|
||||
url = urlutils.fuzzy_url(urlstr, do_search=False)
|
||||
except urlutils.FuzzyUrlError as e:
|
||||
if e.url is None or not e.url.errorString():
|
||||
errstr = ''
|
||||
else:
|
||||
errstr = ' ({})'.format(e.url.errorString())
|
||||
raise cmdexc.CommandError("Invalid URL for quickmark {}: "
|
||||
"{}{}".format(name, urlstr, errstr))
|
||||
return url
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
@@ -17,15 +17,14 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""pyPEG parsing for the RFC 6266 (Content-Disposition) header. """
|
||||
"""pyPEG parsing for the RFC 6266 (Content-Disposition) header."""
|
||||
|
||||
import collections
|
||||
import urllib.parse
|
||||
import string
|
||||
import re
|
||||
|
||||
import pypeg2 as peg # pylint: disable=import-error
|
||||
# (fails on win7 in venv...)
|
||||
import pypeg2 as peg
|
||||
|
||||
from qutebrowser.utils import log, utils
|
||||
|
||||
@@ -122,6 +121,7 @@ class Language(str):
|
||||
FIXME: This grammar is not 100% correct yet.
|
||||
https://github.com/The-Compiler/qutebrowser/issues/105
|
||||
"""
|
||||
|
||||
grammar = re.compile('[A-Za-z0-9-]+')
|
||||
|
||||
|
||||
@@ -235,7 +235,7 @@ class ContentDisposition:
|
||||
"""
|
||||
|
||||
def __init__(self, disposition='inline', assocs=None):
|
||||
"""This constructor is used internally after parsing the header.
|
||||
"""Used internally after parsing the header.
|
||||
|
||||
Instances should generally be created from a factory
|
||||
function, such as parse_headers and its variants.
|
||||
@@ -265,7 +265,6 @@ class ContentDisposition:
|
||||
well, due to a certain browser using the part after the dot for
|
||||
mime-sniffing. Saving it to a database is fine by itself though.
|
||||
"""
|
||||
|
||||
if 'filename*' in self.assocs:
|
||||
return self.assocs['filename*']
|
||||
elif 'filename' in self.assocs:
|
||||
@@ -293,7 +292,9 @@ def normalize_ws(text):
|
||||
|
||||
def parse_headers(content_disposition):
|
||||
"""Build a ContentDisposition from header values."""
|
||||
# pylint: disable=maybe-no-member
|
||||
# https://bitbucket.org/logilab/pylint/issue/492/
|
||||
# pylint: disable=no-member
|
||||
|
||||
# We allow non-ascii here (it will only be parsed inside of qdtext, and
|
||||
# rejected by the grammar if it appears in other places), although parsing
|
||||
# it can be ambiguous. Parsing it ensures that a non-ambiguous filename*
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
@@ -41,7 +41,8 @@ class SignalFilter(QObject):
|
||||
BLACKLIST: List of signal names which should not be logged.
|
||||
"""
|
||||
|
||||
BLACKLIST = ['cur_scroll_perc_changed', 'cur_progress']
|
||||
BLACKLIST = ['cur_scroll_perc_changed', 'cur_progress',
|
||||
'cur_statusbar_message', 'cur_link_hovered']
|
||||
|
||||
def __init__(self, win_id, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
175
qutebrowser/browser/tabhistory.py
Normal file
175
qutebrowser/browser/tabhistory.py
Normal file
@@ -0,0 +1,175 @@
|
||||
# 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/>.
|
||||
|
||||
"""Utilities related to QWebHistory."""
|
||||
|
||||
|
||||
from PyQt5.QtCore import QByteArray, QDataStream, QIODevice, QUrl
|
||||
|
||||
from qutebrowser.utils import utils, qtutils
|
||||
|
||||
|
||||
HISTORY_STREAM_VERSION = 2
|
||||
BACK_FORWARD_TREE_VERSION = 2
|
||||
|
||||
|
||||
class TabHistoryItem:
|
||||
|
||||
"""A single item in the tab history.
|
||||
|
||||
Attributes:
|
||||
url: The QUrl of this item.
|
||||
title: The title as string of this item.
|
||||
active: Whether this item is the item currently navigated to.
|
||||
user_data: The user data for this item.
|
||||
"""
|
||||
|
||||
def __init__(self, url, original_url, title, active=False, user_data=None):
|
||||
self.url = url
|
||||
self.original_url = original_url
|
||||
self.title = title
|
||||
self.active = active
|
||||
self.user_data = user_data
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self, constructor=True, url=self.url,
|
||||
original_url=self.original_url, title=self.title,
|
||||
active=self.active, user_data=self.user_data)
|
||||
|
||||
|
||||
def _encode_url(url):
|
||||
"""Encode an QUrl suitable to pass to QWebHistory."""
|
||||
data = bytes(QUrl.toPercentEncoding(url.toString(), b':/#?&+=@%*'))
|
||||
return data.decode('ascii')
|
||||
|
||||
|
||||
def _serialize_item(i, item, stream):
|
||||
"""Serialize a single WebHistoryItem into a QDataStream.
|
||||
|
||||
Args:
|
||||
i: The index of the current item.
|
||||
item: The WebHistoryItem to write.
|
||||
stream: The QDataStream to write to.
|
||||
"""
|
||||
### Source/WebCore/history/qt/HistoryItemQt.cpp restoreState
|
||||
## urlString
|
||||
stream.writeQString(_encode_url(item.url))
|
||||
## title
|
||||
stream.writeQString(item.title)
|
||||
## originalURLString
|
||||
stream.writeQString(_encode_url(item.original_url))
|
||||
|
||||
### Source/WebCore/history/HistoryItem.cpp decodeBackForwardTree
|
||||
## backForwardTreeEncodingVersion
|
||||
stream.writeUInt32(BACK_FORWARD_TREE_VERSION)
|
||||
## size (recursion stack)
|
||||
stream.writeUInt64(0)
|
||||
## node->m_documentSequenceNumber
|
||||
# If two HistoryItems have the same document sequence number, then they
|
||||
# refer to the same instance of a document. Traversing history from one
|
||||
# such HistoryItem to another preserves the document.
|
||||
stream.writeInt64(i + 1)
|
||||
## size (node->m_documentState)
|
||||
stream.writeUInt64(0)
|
||||
## node->m_formContentType
|
||||
# info used to repost form data
|
||||
stream.writeQString(None)
|
||||
## hasFormData
|
||||
stream.writeBool(False)
|
||||
## node->m_itemSequenceNumber
|
||||
# If two HistoryItems have the same item sequence number, then they are
|
||||
# clones of one another. Traversing history from one such HistoryItem to
|
||||
# another is a no-op. HistoryItem clones are created for parent and
|
||||
# sibling frames when only a subframe navigates.
|
||||
stream.writeInt64(i + 1)
|
||||
## node->m_referrer
|
||||
stream.writeQString(None)
|
||||
## node->m_scrollPoint (x)
|
||||
try:
|
||||
stream.writeInt32(item.user_data['scroll-pos'].x())
|
||||
except (KeyError, TypeError):
|
||||
stream.writeInt32(0)
|
||||
## node->m_scrollPoint (y)
|
||||
try:
|
||||
stream.writeInt32(item.user_data['scroll-pos'].y())
|
||||
except (KeyError, TypeError):
|
||||
stream.writeInt32(0)
|
||||
## node->m_pageScaleFactor
|
||||
stream.writeFloat(1)
|
||||
## hasStateObject
|
||||
# Support for HTML5 History
|
||||
stream.writeBool(False)
|
||||
## node->m_target
|
||||
stream.writeQString(None)
|
||||
|
||||
### Source/WebCore/history/qt/HistoryItemQt.cpp restoreState
|
||||
## validUserData
|
||||
# We could restore the user data here, but we prefer to use the
|
||||
# QWebHistoryItem API for that.
|
||||
stream.writeBool(False)
|
||||
|
||||
|
||||
def serialize(items):
|
||||
"""Serialize a list of QWebHistoryItems to a data stream.
|
||||
|
||||
Args:
|
||||
items: An iterable of WebHistoryItems.
|
||||
|
||||
Return:
|
||||
A (stream, data, user_data) tuple.
|
||||
stream: The reseted QDataStream.
|
||||
data: The QByteArray with the raw data.
|
||||
user_data: A list with each item's user data.
|
||||
|
||||
Warning:
|
||||
If 'data' goes out of scope, reading from 'stream' will result in a
|
||||
segfault!
|
||||
"""
|
||||
data = QByteArray()
|
||||
stream = QDataStream(data, QIODevice.ReadWrite)
|
||||
user_data = []
|
||||
|
||||
current_idx = None
|
||||
|
||||
for i, item in enumerate(items):
|
||||
if item.active:
|
||||
if current_idx is not None:
|
||||
raise ValueError("Multiple active items ({} and {}) "
|
||||
"found!".format(current_idx, i))
|
||||
else:
|
||||
current_idx = i
|
||||
|
||||
if items:
|
||||
if current_idx is None:
|
||||
raise ValueError("No active item found!")
|
||||
else:
|
||||
current_idx = 0
|
||||
|
||||
### Source/WebKit/qt/Api/qwebhistory.cpp operator<<
|
||||
stream.writeInt(HISTORY_STREAM_VERSION)
|
||||
stream.writeInt(len(items))
|
||||
stream.writeInt(current_idx)
|
||||
|
||||
for i, item in enumerate(items):
|
||||
_serialize_item(i, item, stream)
|
||||
user_data.append(item.user_data)
|
||||
|
||||
stream.device().reset()
|
||||
qtutils.check_qdatastream(stream)
|
||||
return stream, data, user_data
|
||||
@@ -1,6 +1,6 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
@@ -277,6 +277,12 @@ class WebElementWrapper(collections.abc.MutableMapping):
|
||||
else:
|
||||
return False
|
||||
|
||||
def is_text_input(self):
|
||||
"""Check if this element is some kind of text box."""
|
||||
roles = ('combobox', 'textbox')
|
||||
tag = self._elem.tagName().lower()
|
||||
return self.get('role', None) in roles or tag in ('input', 'textarea')
|
||||
|
||||
def debug_text(self):
|
||||
"""Get a text based on an element suitable for debug output."""
|
||||
self._check_vanished()
|
||||
@@ -306,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.
|
||||
@@ -326,7 +332,7 @@ def get_child_frames(startframe):
|
||||
|
||||
|
||||
def focus_elem(frame):
|
||||
"""Get the focused element in a webframe.
|
||||
"""Get the focused element in a web frame.
|
||||
|
||||
FIXME: Add tests.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
@@ -21,7 +21,8 @@
|
||||
|
||||
import functools
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot, PYQT_VERSION, Qt, QUrl
|
||||
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, PYQT_VERSION, Qt, QUrl, QPoint,
|
||||
QTimer)
|
||||
from PyQt5.QtGui import QDesktopServices
|
||||
from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest
|
||||
from PyQt5.QtWidgets import QFileDialog
|
||||
@@ -29,10 +30,10 @@ from PyQt5.QtPrintSupport import QPrintDialog
|
||||
from PyQt5.QtWebKitWidgets import QWebPage
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.browser import http
|
||||
from qutebrowser.browser import http, tabhistory
|
||||
from qutebrowser.browser.network import networkmanager
|
||||
from qutebrowser.utils import (message, usertypes, log, jinja, qtutils, utils,
|
||||
objreg)
|
||||
objreg, debug)
|
||||
|
||||
|
||||
class BrowserPage(QWebPage):
|
||||
@@ -40,29 +41,52 @@ class BrowserPage(QWebPage):
|
||||
"""Our own QWebPage with advanced features.
|
||||
|
||||
Attributes:
|
||||
error_occured: Whether an error occured while loading.
|
||||
error_occurred: Whether an error occurred while loading.
|
||||
open_target: Where to open the next navigation request.
|
||||
("normal", "tab", "tab_bg")
|
||||
_hint_target: Override for open_target while hinting, or None.
|
||||
_extension_handlers: Mapping of QWebPage extensions to their handlers.
|
||||
_networkmnager: The NetworkManager used.
|
||||
_win_id: The window ID this BrowserPage is associated with.
|
||||
_ignore_load_started: Whether to ignore the next loadStarted signal.
|
||||
_is_shutting_down: Whether the page is currently shutting down.
|
||||
|
||||
Signals:
|
||||
shutting_down: Emitted when the page is currently shutting down.
|
||||
reloading: Emitted before a web page reloads.
|
||||
arg: The URL which gets reloaded.
|
||||
"""
|
||||
|
||||
def __init__(self, win_id, parent=None):
|
||||
shutting_down = pyqtSignal()
|
||||
reloading = pyqtSignal(QUrl)
|
||||
|
||||
def __init__(self, win_id, tab_id, parent=None):
|
||||
super().__init__(parent)
|
||||
self._win_id = win_id
|
||||
self._is_shutting_down = False
|
||||
self._extension_handlers = {
|
||||
QWebPage.ErrorPageExtension: self._handle_errorpage,
|
||||
QWebPage.ChooseMultipleFilesExtension: self._handle_multiple_files,
|
||||
}
|
||||
self._ignore_load_started = False
|
||||
self.error_occured = False
|
||||
self._networkmanager = networkmanager.NetworkManager(win_id, self)
|
||||
self.error_occurred = False
|
||||
self.open_target = usertypes.ClickTarget.normal
|
||||
self._hint_target = None
|
||||
self._networkmanager = networkmanager.NetworkManager(
|
||||
win_id, tab_id, self)
|
||||
self.setNetworkAccessManager(self._networkmanager)
|
||||
self.setForwardUnsupportedContent(True)
|
||||
self.reloading.connect(self._networkmanager.clear_rejected_ssl_errors)
|
||||
self.printRequested.connect(self.on_print_requested)
|
||||
self.downloadRequested.connect(self.on_download_requested)
|
||||
self.unsupportedContent.connect(self.on_unsupported_content)
|
||||
self.loadStarted.connect(self.on_load_started)
|
||||
self.featurePermissionRequested.connect(
|
||||
self.on_feature_permission_requested)
|
||||
self.saveFrameStateRequested.connect(
|
||||
self.on_save_frame_state_requested)
|
||||
self.restoreFrameStateRequested.connect(
|
||||
self.on_restore_frame_state_requested)
|
||||
|
||||
if PYQT_VERSION > 0x050300:
|
||||
# WORKAROUND (remove this when we bump the requirements to 5.3.1)
|
||||
@@ -72,8 +96,11 @@ class BrowserPage(QWebPage):
|
||||
|
||||
def javaScriptPrompt(self, _frame, msg, default):
|
||||
"""Override javaScriptPrompt to use the statusbar."""
|
||||
answer = message.ask(self._win_id, "js: {}".format(msg),
|
||||
usertypes.PromptMode.text, default)
|
||||
if (self._is_shutting_down or
|
||||
config.get('content', 'ignore-javascript-prompt')):
|
||||
return (False, "")
|
||||
answer = self._ask("js: {}".format(msg), usertypes.PromptMode.text,
|
||||
default)
|
||||
if answer is None:
|
||||
return (False, "")
|
||||
else:
|
||||
@@ -82,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:
|
||||
@@ -120,18 +147,29 @@ class BrowserPage(QWebPage):
|
||||
else:
|
||||
error_str = info.errorString
|
||||
if error_str == networkmanager.HOSTBLOCK_ERROR_STRING:
|
||||
# We don't set error_occurred in this case.
|
||||
error_str = "Request blocked by host blocker."
|
||||
# we don't set error_occured in this case.
|
||||
main_frame = info.frame.page().mainFrame()
|
||||
if info.frame != main_frame:
|
||||
# Content in an iframe -> Hide the frame so it doesn't use
|
||||
# any space. We can't hide the frame's documentElement
|
||||
# directly though.
|
||||
for elem in main_frame.documentElement().findAll('iframe'):
|
||||
if QUrl(elem.attribute('src')) == info.url:
|
||||
elem.setAttribute('style', 'display: none')
|
||||
return False
|
||||
else:
|
||||
self._ignore_load_started = True
|
||||
self.error_occured = True
|
||||
self.error_occurred = True
|
||||
log.webview.error("Error while loading {}: {}".format(
|
||||
urlstr, error_str))
|
||||
log.webview.debug("Error domain: {}, error code: {}".format(
|
||||
info.domain, info.error))
|
||||
title = "Error loading page: {}".format(urlstr)
|
||||
template = jinja.env.get_template('error.html')
|
||||
html = template.render( # pylint: disable=maybe-no-member
|
||||
# pylint: disable=no-member
|
||||
# https://bitbucket.org/logilab/pylint/issue/490/
|
||||
html = template.render(
|
||||
title=title, url=urlstr, error=error_str, icon='')
|
||||
errpage.content = html.encode('utf-8')
|
||||
errpage.encoding = 'utf-8'
|
||||
@@ -140,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.
|
||||
@@ -157,8 +195,60 @@ class BrowserPage(QWebPage):
|
||||
suggested_file)
|
||||
return True
|
||||
|
||||
def _ask(self, text, mode, default=None):
|
||||
"""Ask a blocking question in the statusbar.
|
||||
|
||||
Args:
|
||||
text: The text to display to the user.
|
||||
mode: A PromptMode.
|
||||
default: The default value to display.
|
||||
|
||||
Return:
|
||||
The answer the user gave or None if the prompt was cancelled.
|
||||
"""
|
||||
q = usertypes.Question()
|
||||
q.text = text
|
||||
q.mode = mode
|
||||
q.default = default
|
||||
self.loadStarted.connect(q.abort)
|
||||
self.shutting_down.connect(q.abort)
|
||||
bridge = objreg.get('message-bridge', scope='window',
|
||||
window=self._win_id)
|
||||
bridge.ask(q, blocking=True)
|
||||
q.deleteLater()
|
||||
return q.answer
|
||||
|
||||
def shutdown(self):
|
||||
"""Prepare the web page for being deleted."""
|
||||
self._is_shutting_down = True
|
||||
self.shutting_down.emit()
|
||||
download_manager = objreg.get('download-manager', scope='window',
|
||||
window=self._win_id)
|
||||
nam = self.networkAccessManager()
|
||||
if download_manager.has_downloads_with_nam(nam):
|
||||
nam.setParent(download_manager)
|
||||
else:
|
||||
nam.shutdown()
|
||||
|
||||
def load_history(self, entries):
|
||||
"""Load the history from a list of TabHistoryItem objects."""
|
||||
stream, _data, user_data = tabhistory.serialize(entries)
|
||||
history = self.history()
|
||||
qtutils.deserialize_stream(stream, history)
|
||||
for i, data in enumerate(user_data):
|
||||
history.itemAt(i).setUserData(data)
|
||||
cur_data = history.currentItem().userData()
|
||||
if cur_data is not None:
|
||||
frame = self.mainFrame()
|
||||
if 'zoom' in cur_data:
|
||||
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(
|
||||
frame.setScrollPosition, cur_data['scroll-pos']))
|
||||
|
||||
def display_content(self, reply, mimetype):
|
||||
"""Display a QNetworkReply with an explicitely set mimetype."""
|
||||
"""Display a QNetworkReply with an explicitly set mimetype."""
|
||||
self.mainFrame().setContent(reply.readAll(), mimetype, reply.url())
|
||||
reply.deleteLater()
|
||||
|
||||
@@ -196,12 +286,13 @@ class BrowserPage(QWebPage):
|
||||
At some point we might want to implement the MIME Sniffing standard
|
||||
here: http://mimesniff.spec.whatwg.org/
|
||||
"""
|
||||
inline, _suggested_filename = http.parse_content_disposition(reply)
|
||||
inline, suggested_filename = http.parse_content_disposition(reply)
|
||||
download_manager = objreg.get('download-manager', scope='window',
|
||||
window=self._win_id)
|
||||
if not inline:
|
||||
# Content-Disposition: attachment -> force download
|
||||
download_manager.fetch(reply)
|
||||
download_manager.fetch(reply,
|
||||
suggested_filename=suggested_filename)
|
||||
return
|
||||
mimetype, _rest = http.parse_content_type(reply)
|
||||
if mimetype == 'image/jpg':
|
||||
@@ -216,15 +307,136 @@ class BrowserPage(QWebPage):
|
||||
self.display_content, reply, 'image/jpeg'))
|
||||
else:
|
||||
# Unknown mimetype, so download anyways.
|
||||
download_manager.fetch(reply)
|
||||
download_manager.fetch(reply,
|
||||
suggested_filename=suggested_filename)
|
||||
|
||||
@pyqtSlot()
|
||||
def on_load_started(self):
|
||||
"""Reset error_occured when loading of a new page started."""
|
||||
"""Reset error_occurred when loading of a new page started."""
|
||||
if self._ignore_load_started:
|
||||
self._ignore_load_started = False
|
||||
else:
|
||||
self.error_occured = False
|
||||
self.error_occurred = False
|
||||
|
||||
@pyqtSlot('QWebFrame', 'QWebPage::Feature')
|
||||
def on_feature_permission_requested(self, frame, feature):
|
||||
"""Ask the user for approval for geolocation/notifications."""
|
||||
options = {
|
||||
QWebPage.Notifications: ('content', 'notifications'),
|
||||
QWebPage.Geolocation: ('content', 'geolocation'),
|
||||
}
|
||||
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)
|
||||
q.mode = usertypes.PromptMode.yesno
|
||||
|
||||
msgs = {
|
||||
QWebPage.Notifications: 'show notifications',
|
||||
QWebPage.Geolocation: 'access your location',
|
||||
}
|
||||
|
||||
host = frame.url().host()
|
||||
if host:
|
||||
q.text = "Allow the website at {} to {}?".format(
|
||||
frame.url().host(), msgs[feature])
|
||||
else:
|
||||
q.text = "Allow the website to {}?".format(msgs[feature])
|
||||
|
||||
yes_action = functools.partial(
|
||||
self.setFeaturePermission, frame, feature,
|
||||
QWebPage.PermissionGrantedByUser)
|
||||
q.answered_yes.connect(yes_action)
|
||||
|
||||
no_action = functools.partial(
|
||||
self.setFeaturePermission, frame, feature,
|
||||
QWebPage.PermissionDeniedByUser)
|
||||
q.answered_no.connect(no_action)
|
||||
q.cancelled.connect(no_action)
|
||||
|
||||
q.completed.connect(q.deleteLater)
|
||||
|
||||
self.featurePermissionRequestCanceled.connect(functools.partial(
|
||||
self.on_feature_permission_cancelled, q, frame, feature))
|
||||
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)
|
||||
|
||||
def on_feature_permission_cancelled(self, question, frame, feature,
|
||||
cancelled_frame, cancelled_feature):
|
||||
"""Slot invoked when a feature permission request was cancelled.
|
||||
|
||||
To be used with functools.partial.
|
||||
"""
|
||||
if frame is cancelled_frame and feature == cancelled_feature:
|
||||
try:
|
||||
question.abort()
|
||||
except RuntimeError:
|
||||
# The question could already be deleted, e.g. because it was
|
||||
# aborted after a loadStarted signal.
|
||||
pass
|
||||
|
||||
def on_save_frame_state_requested(self, frame, item):
|
||||
"""Save scroll position and zoom in history.
|
||||
|
||||
Args:
|
||||
frame: The QWebFrame which gets saved.
|
||||
item: The QWebHistoryItem to be saved.
|
||||
"""
|
||||
try:
|
||||
if frame != self.mainFrame():
|
||||
return
|
||||
except RuntimeError:
|
||||
# With Qt 5.2.1 (Ubuntu Trusty) we get this when closing a tab:
|
||||
# RuntimeError: wrapped C/C++ object of type BrowserPage has
|
||||
# been deleted
|
||||
# Since the information here isn't that important for closing web
|
||||
# views anyways, we ignore this error.
|
||||
return
|
||||
data = {
|
||||
'zoom': frame.zoomFactor(),
|
||||
'scroll-pos': frame.scrollPosition(),
|
||||
}
|
||||
item.setUserData(data)
|
||||
|
||||
def on_restore_frame_state_requested(self, frame):
|
||||
"""Restore scroll position and zoom from history.
|
||||
|
||||
Args:
|
||||
frame: The QWebFrame which gets restored.
|
||||
"""
|
||||
if frame != self.mainFrame():
|
||||
return
|
||||
data = self.history().currentItem().userData()
|
||||
if data is None:
|
||||
return
|
||||
if 'zoom' in data:
|
||||
frame.page().view().zoom_perc(data['zoom'] * 100)
|
||||
if 'scroll-pos' in data and frame.scrollPosition() == QPoint(0, 0):
|
||||
frame.setScrollPosition(data['scroll-pos'])
|
||||
|
||||
@pyqtSlot(str)
|
||||
def on_start_hinting(self, hint_target):
|
||||
"""Emitted before a hinting-click takes place.
|
||||
|
||||
Args:
|
||||
hint_target: A ClickTarget member to set self._hint_target to.
|
||||
"""
|
||||
log.webview.debug("Setting force target to {}".format(hint_target))
|
||||
self._hint_target = hint_target
|
||||
|
||||
@pyqtSlot()
|
||||
def on_stop_hinting(self):
|
||||
"""Emitted when hinting is finished."""
|
||||
log.webview.debug("Finishing hinting.")
|
||||
self._hint_target = None
|
||||
|
||||
def userAgentForUrl(self, url):
|
||||
"""Override QWebPage::userAgentForUrl to customize the user agent."""
|
||||
@@ -266,20 +478,33 @@ 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."""
|
||||
message.ask(self._win_id, "[js alert] {}".format(msg),
|
||||
usertypes.PromptMode.alert)
|
||||
log.js.debug("alert: {}".format(msg))
|
||||
if config.get('ui', 'modal-js-dialog'):
|
||||
return super().javaScriptAlert(frame, msg)
|
||||
|
||||
def javaScriptConfirm(self, _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):
|
||||
"""Override javaScriptConfirm to use the statusbar."""
|
||||
ans = message.ask(self._win_id, "[js confirm] {}".format(msg),
|
||||
usertypes.PromptMode.yesno)
|
||||
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),
|
||||
usertypes.PromptMode.yesno)
|
||||
return bool(ans)
|
||||
|
||||
def javaScriptConsoleMessage(self, msg, line, source):
|
||||
"""Override javaScriptConsoleMessage to use debug log."""
|
||||
log.js.debug("[{}:{}] {}".format(source, line, msg))
|
||||
if config.get('general', 'log-javascript-console'):
|
||||
log.js.debug("[{}:{}] {}".format(source, line, msg))
|
||||
|
||||
def chooseFile(self, _frame, suggested_file):
|
||||
"""Override QWebPage's chooseFile to be able to chose a file to upload.
|
||||
@@ -293,8 +518,8 @@ class BrowserPage(QWebPage):
|
||||
|
||||
def shouldInterruptJavaScript(self):
|
||||
"""Override shouldInterruptJavaScript to use the statusbar."""
|
||||
answer = message.ask(self._win_id, "Interrupt long-running "
|
||||
"javascript?", usertypes.PromptMode.yesno)
|
||||
answer = self._ask("Interrupt long-running javascript?",
|
||||
usertypes.PromptMode.yesno)
|
||||
if answer is None:
|
||||
answer = True
|
||||
return answer
|
||||
@@ -314,30 +539,41 @@ class BrowserPage(QWebPage):
|
||||
request: QNetworkRequest
|
||||
typ: QWebPage::NavigationType
|
||||
"""
|
||||
if typ != QWebPage.NavigationTypeLinkClicked:
|
||||
return True
|
||||
url = request.url()
|
||||
urlstr = url.toDisplayString()
|
||||
if typ == QWebPage.NavigationTypeReload:
|
||||
self.reloading.emit(url)
|
||||
if typ != QWebPage.NavigationTypeLinkClicked:
|
||||
return True
|
||||
if not url.isValid():
|
||||
message.error(self._win_id, "Invalid link {} clicked!".format(
|
||||
urlstr))
|
||||
log.webview.debug(url.errorString())
|
||||
self.open_target = usertypes.ClickTarget.normal
|
||||
return False
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=self._win_id)
|
||||
open_target = self.view().open_target
|
||||
if open_target == usertypes.ClickTarget.tab:
|
||||
log.webview.debug("acceptNavigationRequest, url {}, type {}, hint "
|
||||
"target {}, open_target {}".format(
|
||||
urlstr, debug.qenum_key(QWebPage, typ),
|
||||
self._hint_target, self.open_target))
|
||||
if self._hint_target is not None:
|
||||
target = self._hint_target
|
||||
else:
|
||||
target = self.open_target
|
||||
self.open_target = usertypes.ClickTarget.normal
|
||||
if target == usertypes.ClickTarget.tab:
|
||||
tabbed_browser.tabopen(url, False)
|
||||
return False
|
||||
elif open_target == usertypes.ClickTarget.tab_bg:
|
||||
elif target == usertypes.ClickTarget.tab_bg:
|
||||
tabbed_browser.tabopen(url, True)
|
||||
return False
|
||||
elif open_target == usertypes.ClickTarget.window:
|
||||
main_window = objreg.get('main-window', scope='window',
|
||||
window=self._win_id)
|
||||
win_id = main_window.spawn()
|
||||
elif target == usertypes.ClickTarget.window:
|
||||
from qutebrowser.mainwindow import mainwindow
|
||||
window = mainwindow.MainWindow()
|
||||
window.show()
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=win_id)
|
||||
window=window.win_id)
|
||||
tabbed_browser.tabopen(url, False)
|
||||
return False
|
||||
else:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
@@ -19,11 +19,13 @@
|
||||
|
||||
"""The main browser widgets."""
|
||||
|
||||
import sys
|
||||
import itertools
|
||||
import functools
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QTimer, QUrl
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PyQt5.QtGui import QPalette
|
||||
from PyQt5.QtWidgets import QApplication, QStyleFactory
|
||||
from PyQt5.QtWebKit import QWebSettings
|
||||
from PyQt5.QtWebKitWidgets import QWebView, QWebPage
|
||||
|
||||
@@ -31,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',
|
||||
@@ -51,23 +52,25 @@ class WebView(QWebView):
|
||||
hintmanager: The HintManager instance for this view.
|
||||
progress: loading progress of this page.
|
||||
scroll_pos: The current scroll position as (x%, y%) tuple.
|
||||
statusbar_message: The current javscript statusbar message.
|
||||
statusbar_message: The current javascript statusbar message.
|
||||
inspector: The QWebInspector used for this webview.
|
||||
load_status: loading status of this page (index into LoadStatus)
|
||||
open_target: Where to open the next tab ("normal", "tab", "tab_bg")
|
||||
viewing_source: Whether the webview is currently displaying source
|
||||
code.
|
||||
keep_icon: Whether the (e.g. cloned) icon should not be cleared on page
|
||||
load.
|
||||
registry: The ObjectRegistry associated with this tab.
|
||||
tab_id: The tab ID of the view.
|
||||
win_id: The window ID of the view.
|
||||
search_text: The text of the last search.
|
||||
search_flags: The search flags of the last search.
|
||||
_cur_url: The current URL (accessed via cur_url property).
|
||||
_has_ssl_errors: Whether SSL errors occured during loading.
|
||||
_has_ssl_errors: Whether SSL errors occurred during loading.
|
||||
_zoom: A NeighborList with the zoom levels.
|
||||
_old_scroll_pos: The old scroll position.
|
||||
_force_open_target: Override for open_target.
|
||||
_check_insertmode: If True, in mouseReleaseEvent we should check if we
|
||||
need to enter/leave insert mode.
|
||||
_default_zoom_changed: Whether the zoom was changed from the default.
|
||||
_win_id: The window ID of the view.
|
||||
|
||||
Signals:
|
||||
scroll_pos_changed: Scroll percentage of current tab changed.
|
||||
@@ -76,27 +79,36 @@ class WebView(QWebView):
|
||||
linkHovered: QWebPages linkHovered signal exposed.
|
||||
load_status_changed: The loading status changed
|
||||
url_text_changed: Current URL string changed.
|
||||
shutting_down: Emitted when the view is shutting down.
|
||||
"""
|
||||
|
||||
scroll_pos_changed = pyqtSignal(int, int)
|
||||
linkHovered = pyqtSignal(str, str, str)
|
||||
load_status_changed = pyqtSignal(str)
|
||||
url_text_changed = pyqtSignal(str)
|
||||
shutting_down = pyqtSignal()
|
||||
|
||||
def __init__(self, win_id, parent=None):
|
||||
super().__init__(parent)
|
||||
self._win_id = win_id
|
||||
if sys.platform == 'darwin' and qtutils.version_check('5.4'):
|
||||
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-42948
|
||||
# See https://github.com/The-Compiler/qutebrowser/issues/462
|
||||
self.setStyle(QStyleFactory.create('Fusion'))
|
||||
self.win_id = win_id
|
||||
self.load_status = LoadStatus.none
|
||||
self._check_insertmode = False
|
||||
self.inspector = None
|
||||
self.scroll_pos = (-1, -1)
|
||||
self.statusbar_message = ''
|
||||
self._old_scroll_pos = (-1, -1)
|
||||
self.open_target = usertypes.ClickTarget.normal
|
||||
self._force_open_target = None
|
||||
self._zoom = None
|
||||
self._has_ssl_errors = False
|
||||
self.keep_icon = False
|
||||
self.search_text = None
|
||||
self.search_flags = 0
|
||||
self.selection_enabled = False
|
||||
self.init_neighborlist()
|
||||
self._set_bg_color()
|
||||
cfg = objreg.get('config')
|
||||
cfg.changed.connect(self.init_neighborlist)
|
||||
# For some reason, this signal doesn't get disconnected automatically
|
||||
@@ -113,36 +125,44 @@ class WebView(QWebView):
|
||||
window=win_id)
|
||||
tab_registry[self.tab_id] = self
|
||||
objreg.register('webview', self, registry=self.registry)
|
||||
page = webpage.BrowserPage(win_id, self)
|
||||
self.setPage(page)
|
||||
page = self._init_page()
|
||||
hintmanager = hints.HintManager(win_id, self.tab_id, self)
|
||||
hintmanager.mouse_event.connect(self.on_mouse_event)
|
||||
hintmanager.set_open_target.connect(self.set_force_open_target)
|
||||
hintmanager.start_hinting.connect(page.on_start_hinting)
|
||||
hintmanager.stop_hinting.connect(page.on_stop_hinting)
|
||||
objreg.register('hintmanager', hintmanager, registry=self.registry)
|
||||
mode_manager = objreg.get('mode-manager', scope='window',
|
||||
window=win_id)
|
||||
mode_manager.entered.connect(self.on_mode_entered)
|
||||
mode_manager.left.connect(self.on_mode_left)
|
||||
page.linkHovered.connect(self.linkHovered)
|
||||
page.mainFrame().loadStarted.connect(self.on_load_started)
|
||||
self.urlChanged.connect(self.on_url_changed)
|
||||
page.mainFrame().loadFinished.connect(self.on_load_finished)
|
||||
self.loadProgress.connect(lambda p: setattr(self, 'progress', p))
|
||||
self.page().statusBarMessage.connect(
|
||||
lambda msg: setattr(self, 'statusbar_message', msg))
|
||||
self.page().networkAccessManager().sslErrors.connect(
|
||||
lambda *args: setattr(self, '_has_ssl_errors', True))
|
||||
self.viewing_source = False
|
||||
self.setZoomFactor(float(config.get('ui', 'default-zoom')) / 100)
|
||||
self._default_zoom_changed = False
|
||||
if config.get('input', 'rocker-gestures'):
|
||||
self.setContextMenuPolicy(Qt.PreventContextMenu)
|
||||
self.urlChanged.connect(self.on_url_changed)
|
||||
self.loadProgress.connect(lambda p: setattr(self, 'progress', p))
|
||||
objreg.get('config').changed.connect(self.on_config_changed)
|
||||
|
||||
def _init_page(self):
|
||||
"""Initialize the QWebPage used by this view."""
|
||||
page = webpage.BrowserPage(self.win_id, self.tab_id, self)
|
||||
self.setPage(page)
|
||||
page.linkHovered.connect(self.linkHovered)
|
||||
page.mainFrame().loadStarted.connect(self.on_load_started)
|
||||
page.mainFrame().loadFinished.connect(self.on_load_finished)
|
||||
page.statusBarMessage.connect(
|
||||
lambda msg: setattr(self, 'statusbar_message', msg))
|
||||
page.networkAccessManager().sslErrors.connect(
|
||||
lambda *args: setattr(self, '_has_ssl_errors', True))
|
||||
return page
|
||||
|
||||
def __repr__(self):
|
||||
url = utils.elide(self.url().toDisplayString(), 50)
|
||||
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
|
||||
@@ -162,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."""
|
||||
@@ -171,6 +200,13 @@ class WebView(QWebView):
|
||||
100)
|
||||
self._default_zoom_changed = False
|
||||
self.init_neighborlist()
|
||||
elif section == 'input' and option == 'rocker-gestures':
|
||||
if config.get('input', 'rocker-gestures'):
|
||||
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."""
|
||||
@@ -185,19 +221,19 @@ class WebView(QWebView):
|
||||
Args:
|
||||
e: The QMouseEvent.
|
||||
"""
|
||||
if e.button() == Qt.XButton1:
|
||||
# Back button on mice which have it.
|
||||
if e.button() in (Qt.XButton1, Qt.LeftButton):
|
||||
# Back button on mice which have it, or rocker gesture
|
||||
if self.page().history().canGoBack():
|
||||
self.back()
|
||||
else:
|
||||
message.error(self._win_id, "At beginning of history.",
|
||||
message.error(self.win_id, "At beginning of history.",
|
||||
immediately=True)
|
||||
elif e.button() == Qt.XButton2:
|
||||
# Forward button on mice which have it.
|
||||
elif e.button() in (Qt.XButton2, Qt.RightButton):
|
||||
# Forward button on mice which have it, or rocker gesture
|
||||
if self.page().history().canGoForward():
|
||||
self.forward()
|
||||
else:
|
||||
message.error(self._win_id, "At end of history.",
|
||||
message.error(self.win_id, "At end of history.",
|
||||
immediately=True)
|
||||
|
||||
def _mousepress_insertmode(self, e):
|
||||
@@ -220,7 +256,7 @@ class WebView(QWebView):
|
||||
# me, but it works this way.
|
||||
hitresult = frame.hitTestContent(pos)
|
||||
if hitresult.isNull():
|
||||
# For some reason, the whole hitresult can be null sometimes (e.g.
|
||||
# For some reason, the whole hit result can be null sometimes (e.g.
|
||||
# on doodle menu links). If this is the case, we schedule a check
|
||||
# later (in mouseReleaseEvent) which uses webelem.focus_elem.
|
||||
log.mouse.debug("Hitresult is null!")
|
||||
@@ -229,7 +265,7 @@ class WebView(QWebView):
|
||||
try:
|
||||
elem = webelem.WebElementWrapper(hitresult.element())
|
||||
except webelem.IsNullError:
|
||||
# For some reason, the hitresult element can be a null element
|
||||
# For some reason, the hit result element can be a null element
|
||||
# sometimes (e.g. when clicking the timetable fields on
|
||||
# http://www.sbb.ch/ ). If this is the case, we schedule a check
|
||||
# later (in mouseReleaseEvent) which uses webelem.focus_elem.
|
||||
@@ -239,12 +275,12 @@ class WebView(QWebView):
|
||||
if ((hitresult.isContentEditable() and elem.is_writable()) or
|
||||
elem.is_editable()):
|
||||
log.mouse.debug("Clicked editable element!")
|
||||
modeman.enter(self._win_id, usertypes.KeyMode.insert, 'click',
|
||||
modeman.enter(self.win_id, usertypes.KeyMode.insert, 'click',
|
||||
only_if_normal=True)
|
||||
else:
|
||||
log.mouse.debug("Clicked non-editable element!")
|
||||
if config.get('input', 'auto-leave-insert-mode'):
|
||||
modeman.maybe_leave(self._win_id, usertypes.KeyMode.insert,
|
||||
modeman.maybe_leave(self.win_id, usertypes.KeyMode.insert,
|
||||
'click')
|
||||
|
||||
def mouserelease_insertmode(self):
|
||||
@@ -254,17 +290,17 @@ class WebView(QWebView):
|
||||
self._check_insertmode = False
|
||||
try:
|
||||
elem = webelem.focus_elem(self.page().currentFrame())
|
||||
except webelem.IsNullError:
|
||||
log.mouse.warning("Element vanished!")
|
||||
except (webelem.IsNullError, RuntimeError):
|
||||
log.mouse.warning("Element/page vanished!")
|
||||
return
|
||||
if elem.is_editable():
|
||||
log.mouse.debug("Clicked editable element (delayed)!")
|
||||
modeman.enter(self._win_id, usertypes.KeyMode.insert,
|
||||
modeman.enter(self.win_id, usertypes.KeyMode.insert,
|
||||
'click-delayed', only_if_normal=True)
|
||||
else:
|
||||
log.mouse.debug("Clicked non-editable element (delayed)!")
|
||||
if config.get('input', 'auto-leave-insert-mode'):
|
||||
modeman.maybe_leave(self._win_id, usertypes.KeyMode.insert,
|
||||
modeman.maybe_leave(self.win_id, usertypes.KeyMode.insert,
|
||||
'click-delayed')
|
||||
|
||||
def _mousepress_opentarget(self, e):
|
||||
@@ -273,41 +309,30 @@ class WebView(QWebView):
|
||||
Args:
|
||||
e: The QMouseEvent.
|
||||
"""
|
||||
if self._force_open_target is not None:
|
||||
self.open_target = self._force_open_target
|
||||
self._force_open_target = None
|
||||
log.mouse.debug("Setting force target: {}".format(
|
||||
self.open_target))
|
||||
elif (e.button() == Qt.MidButton or
|
||||
e.modifiers() & Qt.ControlModifier):
|
||||
if e.button() == Qt.MidButton or e.modifiers() & Qt.ControlModifier:
|
||||
background_tabs = config.get('tabs', 'background-tabs')
|
||||
if e.modifiers() & Qt.ShiftModifier:
|
||||
background_tabs = not background_tabs
|
||||
if background_tabs:
|
||||
self.open_target = usertypes.ClickTarget.tab_bg
|
||||
target = usertypes.ClickTarget.tab_bg
|
||||
else:
|
||||
self.open_target = usertypes.ClickTarget.tab
|
||||
log.mouse.debug("Middle click, setting target: {}".format(
|
||||
self.open_target))
|
||||
target = usertypes.ClickTarget.tab
|
||||
self.page().open_target = target
|
||||
log.mouse.debug("Middle click, setting target: {}".format(target))
|
||||
else:
|
||||
self.open_target = usertypes.ClickTarget.normal
|
||||
self.page().open_target = usertypes.ClickTarget.normal
|
||||
log.mouse.debug("Normal click, setting normal target")
|
||||
|
||||
def shutdown(self):
|
||||
"""Shut down the webview."""
|
||||
self.shutting_down.emit()
|
||||
# We disable javascript because that prevents some segfaults when
|
||||
# quitting it seems.
|
||||
log.destroy.debug("Shutting down {!r}.".format(self))
|
||||
settings = self.settings()
|
||||
settings.setAttribute(QWebSettings.JavascriptEnabled, False)
|
||||
self.stop()
|
||||
download_manager = objreg.get('download-manager', scope='window',
|
||||
window=self._win_id)
|
||||
nam = self.page().networkAccessManager()
|
||||
if download_manager.has_downloads_with_nam(nam):
|
||||
nam.setParent(download_manager)
|
||||
else:
|
||||
nam.shutdown()
|
||||
self.page().shutdown()
|
||||
|
||||
def openurl(self, url):
|
||||
"""Open a URL in the browser.
|
||||
@@ -343,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):
|
||||
@@ -353,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):
|
||||
@@ -366,10 +394,12 @@ 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):
|
||||
"""Post a new mouseevent from a hintmanager."""
|
||||
"""Post a new mouse event from a hintmanager."""
|
||||
log.modes.debug("Hint triggered, focusing {!r}".format(self))
|
||||
self.setFocus()
|
||||
QApplication.postEvent(self, evt)
|
||||
@@ -384,23 +414,29 @@ 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.
|
||||
See https://github.com/The-Compiler/qutebrowser/issues/84
|
||||
"""
|
||||
ok = not self.page().error_occured
|
||||
ok = not self.page().error_occurred
|
||||
if ok and not self._has_ssl_errors:
|
||||
self._set_load_status(LoadStatus.success)
|
||||
elif ok:
|
||||
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',
|
||||
window=self._win_id)
|
||||
window=self.win_id)
|
||||
cur_mode = mode_manager.mode
|
||||
if cur_mode == usertypes.KeyMode.insert or not ok:
|
||||
return
|
||||
@@ -412,7 +448,7 @@ class WebView(QWebView):
|
||||
return
|
||||
log.modes.debug("focus element: {}".format(repr(elem)))
|
||||
if elem.is_editable():
|
||||
modeman.enter(self._win_id, usertypes.KeyMode.insert,
|
||||
modeman.enter(self.win_id, usertypes.KeyMode.insert,
|
||||
'load finished', only_if_normal=True)
|
||||
|
||||
@pyqtSlot(usertypes.KeyMode)
|
||||
@@ -423,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):
|
||||
@@ -431,18 +486,58 @@ 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)
|
||||
|
||||
@pyqtSlot(str)
|
||||
def set_force_open_target(self, target):
|
||||
"""Change the forced link target. Setter for _force_open_target.
|
||||
def search(self, text, flags):
|
||||
"""Search for text in the current page.
|
||||
|
||||
Args:
|
||||
target: A string to set self._force_open_target to.
|
||||
text: The text to search for.
|
||||
flags: The QWebPage::FindFlags.
|
||||
"""
|
||||
t = getattr(usertypes.ClickTarget, target)
|
||||
log.webview.debug("Setting force target to {}/{}".format(target, t))
|
||||
self._force_open_target = t
|
||||
log.webview.debug("Searching with text '{}' and flags "
|
||||
"0x{:04x}.".format(text, int(flags)))
|
||||
old_scroll_pos = self.scroll_pos
|
||||
flags = QWebPage.FindFlags(flags)
|
||||
found = self.findText(text, flags)
|
||||
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:
|
||||
message.info(self.win_id, "Search hit BOTTOM, continuing "
|
||||
"at TOP", immediately=True)
|
||||
elif backward and self.scroll_pos > old_scroll_pos:
|
||||
message.info(self.win_id, "Search hit TOP, continuing at "
|
||||
"BOTTOM", immediately=True)
|
||||
# We first want QWebPage to refresh.
|
||||
QTimer.singleShot(0, check_scroll_pos)
|
||||
|
||||
def createWindow(self, wintype):
|
||||
"""Called by Qt when a page wants to create a new window.
|
||||
@@ -467,7 +562,7 @@ class WebView(QWebView):
|
||||
log.webview.warning("WebModalDialog requested, but we don't "
|
||||
"support that!")
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=self._win_id)
|
||||
window=self.win_id)
|
||||
return tabbed_browser.tabopen(background=False)
|
||||
|
||||
def paintEvent(self, e):
|
||||
@@ -502,7 +597,7 @@ class WebView(QWebView):
|
||||
|
||||
This does the following things:
|
||||
- Check if a link was clicked with the middle button or Ctrl and
|
||||
set the open_target attribute accordingly.
|
||||
set the page's open_target attribute accordingly.
|
||||
- Emit the editable_elem_selected signal if an editable element was
|
||||
clicked.
|
||||
|
||||
@@ -512,7 +607,10 @@ class WebView(QWebView):
|
||||
Return:
|
||||
The superclass return value.
|
||||
"""
|
||||
if e.button() in (Qt.XButton1, Qt.XButton2):
|
||||
is_rocker_gesture = (config.get('input', 'rocker-gestures') and
|
||||
e.buttons() == Qt.LeftButton | Qt.RightButton)
|
||||
|
||||
if e.button() in (Qt.XButton1, Qt.XButton2) or is_rocker_gesture:
|
||||
self._mousepress_backforward(e)
|
||||
super().mousePressEvent(e)
|
||||
return
|
||||
@@ -526,3 +624,30 @@ class WebView(QWebView):
|
||||
# We want to make sure we check the focus element after the WebView is
|
||||
# updated completely.
|
||||
QTimer.singleShot(0, self.mouserelease_insertmode)
|
||||
|
||||
def contextMenuEvent(self, e):
|
||||
"""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):
|
||||
"""Zoom on Ctrl-Mousewheel.
|
||||
|
||||
Args:
|
||||
e: The QWheelEvent.
|
||||
"""
|
||||
if e.modifiers() & Qt.ControlModifier:
|
||||
e.accept()
|
||||
divider = config.get('input', 'mouse-zoom-divider')
|
||||
factor = self.zoomFactor() + e.angleDelta().y() / divider
|
||||
if factor < 0:
|
||||
return
|
||||
perc = int(100 * factor)
|
||||
message.info(self.win_id, "Zoom level: {}%".format(perc))
|
||||
self._zoom.fuzzyval = perc
|
||||
self.setZoomFactor(factor)
|
||||
self._default_zoom_changed = True
|
||||
else:
|
||||
super().wheelEvent(e)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
@@ -86,7 +86,6 @@ class ArgumentParser(argparse.ArgumentParser):
|
||||
|
||||
def enum_getter(enum):
|
||||
"""Function factory to get an enum getter."""
|
||||
|
||||
def _get_enum_item(key):
|
||||
"""Helper function to get an enum item.
|
||||
|
||||
@@ -104,7 +103,6 @@ def enum_getter(enum):
|
||||
|
||||
def multitype_conv(tpl):
|
||||
"""Function factory to get a type converter for a choice of types."""
|
||||
|
||||
def _convert(value):
|
||||
"""Convert a value according to an iterable of possible arg types."""
|
||||
for typ in set(tpl):
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
@@ -32,7 +32,7 @@ class CommandError(Exception):
|
||||
|
||||
class CommandMetaError(Exception):
|
||||
|
||||
"""Common base class for exceptions occuring before a command is run."""
|
||||
"""Common base class for exceptions occurring before a command is run."""
|
||||
|
||||
|
||||
class NoSuchCommandError(CommandMetaError):
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
@@ -23,7 +23,7 @@ Module attributes:
|
||||
cmd_dict: A mapping from command-strings to command objects.
|
||||
"""
|
||||
|
||||
from qutebrowser.utils import usertypes, qtutils, log
|
||||
from qutebrowser.utils import qtutils, log
|
||||
from qutebrowser.commands import command, cmdexc
|
||||
|
||||
cmd_dict = {}
|
||||
@@ -99,22 +99,11 @@ class register: # pylint: disable=invalid-name
|
||||
|
||||
Attributes:
|
||||
_instance: The object from the object registry to be used as "self".
|
||||
_scope: The scope to get _instance for.
|
||||
_name: The name (as string) or names (as list) of the command.
|
||||
_maxsplit: The maxium amounts of splits to do for the commandline, or
|
||||
None.
|
||||
_hide: Whether to hide the command or not.
|
||||
_completion: Which completion to use for arguments, as a list of
|
||||
strings.
|
||||
_modes/_not_modes: List of modes to use/not use.
|
||||
_needs_js: If javascript is needed for this command.
|
||||
_debug: Whether this is a debugging command (only shown with --debug).
|
||||
_ignore_args: Whether to ignore the arguments of the function.
|
||||
_kwargs: The arguments to pass to Command.
|
||||
"""
|
||||
|
||||
def __init__(self, instance=None, name=None, maxsplit=None, hide=False,
|
||||
completion=None, modes=None, not_modes=None, needs_js=False,
|
||||
debug=False, ignore_args=False, scope='global'):
|
||||
def __init__(self, *, instance=None, name=None, **kwargs):
|
||||
"""Save decorator arguments.
|
||||
|
||||
Gets called on parse-time with the decorator arguments.
|
||||
@@ -122,33 +111,14 @@ class register: # pylint: disable=invalid-name
|
||||
Args:
|
||||
See class attributes.
|
||||
"""
|
||||
# pylint: disable=too-many-arguments
|
||||
if modes is not None and not_modes is not None:
|
||||
raise ValueError("Only modes or not_modes can be given!")
|
||||
self._name = name
|
||||
self._maxsplit = maxsplit
|
||||
self._hide = hide
|
||||
self._instance = instance
|
||||
self._scope = scope
|
||||
self._completion = completion
|
||||
self._modes = modes
|
||||
self._not_modes = not_modes
|
||||
self._needs_js = needs_js
|
||||
self._debug = debug
|
||||
self._ignore_args = ignore_args
|
||||
if modes is not None:
|
||||
for m in modes:
|
||||
if not isinstance(m, usertypes.KeyMode):
|
||||
raise TypeError("Mode {} is no KeyMode member!".format(m))
|
||||
if not_modes is not None:
|
||||
for m in not_modes:
|
||||
if not isinstance(m, usertypes.KeyMode):
|
||||
raise TypeError("Mode {} is no KeyMode member!".format(m))
|
||||
self._name = name
|
||||
self._kwargs = kwargs
|
||||
|
||||
def _get_names(self, func):
|
||||
"""Get the name(s) which should be used for the current command.
|
||||
|
||||
If the name hasn't been overridden explicitely, the function name is
|
||||
If the name hasn't been overridden explicitly, the function name is
|
||||
transformed.
|
||||
|
||||
If it has been set, it can either be a string which is
|
||||
@@ -187,12 +157,8 @@ class register: # pylint: disable=invalid-name
|
||||
for name in names:
|
||||
if name in cmd_dict:
|
||||
raise ValueError("{} is already registered!".format(name))
|
||||
cmd = command.Command(
|
||||
name=names[0], maxsplit=self._maxsplit, hide=self._hide,
|
||||
instance=self._instance, scope=self._scope,
|
||||
completion=self._completion, modes=self._modes,
|
||||
not_modes=self._not_modes, needs_js=self._needs_js,
|
||||
is_debug=self._debug, ignore_args=self._ignore_args, handler=func)
|
||||
cmd = command.Command(name=names[0], instance=self._instance,
|
||||
handler=func, **self._kwargs)
|
||||
for name in names:
|
||||
cmd_dict[name] = cmd
|
||||
aliases += names[1:]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
@@ -25,7 +25,13 @@ import collections
|
||||
from PyQt5.QtWebKit import QWebSettings
|
||||
|
||||
from qutebrowser.commands import cmdexc, argparser
|
||||
from qutebrowser.utils import log, utils, message, debug, docutils, objreg
|
||||
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:
|
||||
@@ -37,16 +43,17 @@ class Command:
|
||||
maxsplit: The maximum amount of splits to do for the commandline, or
|
||||
None.
|
||||
hide: Whether to hide the arguments or not.
|
||||
deprecated: False, or a string to describe why a command is deprecated.
|
||||
desc: The description of the command.
|
||||
handler: The handler function to call.
|
||||
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.
|
||||
@@ -59,26 +66,45 @@ class Command:
|
||||
"""
|
||||
|
||||
AnnotationInfo = collections.namedtuple('AnnotationInfo',
|
||||
['kwargs', 'type', 'name', 'flag',
|
||||
'special'])
|
||||
['kwargs', 'type', 'flag', 'hide',
|
||||
'metavar'])
|
||||
|
||||
def __init__(self, name, maxsplit, hide, instance, completion, modes,
|
||||
not_modes, needs_js, is_debug, ignore_args,
|
||||
handler, scope):
|
||||
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',
|
||||
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:
|
||||
raise ValueError("Only modes or not_modes can be given!")
|
||||
if modes is not None:
|
||||
for m in modes:
|
||||
if not isinstance(m, usertypes.KeyMode):
|
||||
raise TypeError("Mode {} is no KeyMode member!".format(m))
|
||||
if not_modes is not None:
|
||||
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
|
||||
self.deprecated = deprecated
|
||||
self._instance = instance
|
||||
self.completion = completion
|
||||
self._modes = modes
|
||||
self._not_modes = not_modes
|
||||
self._scope = scope
|
||||
self._needs_js = needs_js
|
||||
self.debug = is_debug
|
||||
self.debug = debug
|
||||
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,
|
||||
@@ -91,12 +117,13 @@ 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 = {}
|
||||
self._inspect_func()
|
||||
count = self._inspect_func()
|
||||
if self.completion is not None and len(self.completion) > count:
|
||||
raise ValueError("Got {} completions, but only {} "
|
||||
"arguments!".format(len(self.completion), count))
|
||||
|
||||
def _check_prerequisites(self, win_id):
|
||||
"""Check if the command is permitted to run currently.
|
||||
@@ -121,6 +148,9 @@ class Command:
|
||||
QWebSettings.JavascriptEnabled):
|
||||
raise cmdexc.PrerequisitesError(
|
||||
"{}: This command needs javascript enabled.".format(self.name))
|
||||
if self.deprecated:
|
||||
message.warning(win_id, '{} is deprecated - {}'.format(
|
||||
self.name, self.deprecated))
|
||||
|
||||
def _check_func(self):
|
||||
"""Make sure the function parameters don't violate any rules."""
|
||||
@@ -151,84 +181,68 @@ 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 paraeter.
|
||||
|
||||
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.
|
||||
|
||||
Sets instance attributes (desc, type_conv, name_conv) based on the
|
||||
informations.
|
||||
|
||||
Return:
|
||||
How many user-visible arguments the command has.
|
||||
"""
|
||||
signature = inspect.signature(self.handler)
|
||||
doc = inspect.getdoc(self.handler)
|
||||
arg_count = 0
|
||||
if doc is not None:
|
||||
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.format_call(
|
||||
callsig = debug_utils.format_call(
|
||||
self.parser.add_argument, args, kwargs,
|
||||
full=False)
|
||||
log.commands.vdebug('Adding arg {} of type {} -> {}'.format(
|
||||
param.name, typ, callsig))
|
||||
self.parser.add_argument(*args, **kwargs)
|
||||
return arg_count
|
||||
|
||||
def _param_to_argparse_kwargs(self, param, annotation_info):
|
||||
"""Get argparse keyword arguments for a parameter.
|
||||
@@ -248,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:
|
||||
@@ -279,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,
|
||||
@@ -292,11 +308,11 @@ class Command:
|
||||
args.append(long_flag)
|
||||
args.append(short_flag)
|
||||
self.opt_args[param.name] = long_flag, short_flag
|
||||
if param.kind == inspect.Parameter.KEYWORD_ONLY:
|
||||
self.flags_with_args.append(param.name)
|
||||
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):
|
||||
@@ -313,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:
|
||||
@@ -348,10 +364,17 @@ class Command:
|
||||
args: The positional argument list. Gets modified directly.
|
||||
"""
|
||||
assert param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD
|
||||
if self._scope is not 'window':
|
||||
if self._scope == 'global':
|
||||
tab_id = None
|
||||
win_id = None
|
||||
obj = objreg.get(self._instance, scope=self._scope,
|
||||
window=win_id)
|
||||
elif self._scope == 'tab':
|
||||
tab_id = 'current'
|
||||
elif self._scope == 'window':
|
||||
tab_id = None
|
||||
else:
|
||||
raise ValueError("Invalid scope {}!".format(self._scope))
|
||||
obj = objreg.get(self._instance, scope=self._scope, window=win_id,
|
||||
tab=tab_id)
|
||||
args.append(obj)
|
||||
|
||||
def _get_count_arg(self, param, args, kwargs):
|
||||
@@ -391,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:
|
||||
@@ -412,7 +434,6 @@ class Command:
|
||||
Return:
|
||||
An (args, kwargs) tuple.
|
||||
"""
|
||||
|
||||
args = []
|
||||
kwargs = {}
|
||||
signature = inspect.signature(self.handler)
|
||||
@@ -428,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(
|
||||
@@ -479,5 +500,5 @@ class Command:
|
||||
posargs, kwargs = self._get_call_args(win_id)
|
||||
self._check_prerequisites(win_id)
|
||||
log.commands.debug('Calling {}'.format(
|
||||
debug.format_call(self.handler, posargs, kwargs)))
|
||||
debug_utils.format_call(self.handler, posargs, kwargs)))
|
||||
self.handler(*posargs, **kwargs)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
@@ -19,151 +19,52 @@
|
||||
|
||||
"""Module containing command managers (SearchRunner and CommandRunner)."""
|
||||
|
||||
import re
|
||||
import collections
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject, QUrl
|
||||
from PyQt5.QtWebKitWidgets import QWebPage
|
||||
from PyQt5.QtCore import pyqtSlot, QUrl, QObject
|
||||
|
||||
from qutebrowser.config import config, configexc
|
||||
from qutebrowser.commands import cmdexc, cmdutils
|
||||
from qutebrowser.utils import message, log, utils, objreg
|
||||
from qutebrowser.utils import message, log, objreg, qtutils
|
||||
from qutebrowser.misc import split
|
||||
|
||||
|
||||
ParseResult = collections.namedtuple('ParseResult', 'cmd, args, cmdline')
|
||||
|
||||
|
||||
def replace_variables(win_id, arglist):
|
||||
"""Utility function to replace variables like {url} in a list of args."""
|
||||
args = []
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=win_id)
|
||||
for arg in arglist:
|
||||
if arg == '{url}':
|
||||
# Note we have to do this in here as the user gets an error message
|
||||
# by current_url if no URL is open yet.
|
||||
if '{url}' in arglist:
|
||||
try:
|
||||
url = tabbed_browser.current_url().toString(QUrl.FullyEncoded |
|
||||
QUrl.RemovePassword)
|
||||
except qtutils.QtValueError as e:
|
||||
msg = "Current URL is invalid"
|
||||
if e.reason:
|
||||
msg += " ({})".format(e.reason)
|
||||
msg += "!"
|
||||
raise cmdexc.CommandError(msg)
|
||||
for arg in arglist:
|
||||
if arg == '{url}':
|
||||
args.append(url)
|
||||
else:
|
||||
args.append(arg)
|
||||
return args
|
||||
|
||||
|
||||
class SearchRunner(QObject):
|
||||
|
||||
"""Run searches on webpages.
|
||||
|
||||
Attributes:
|
||||
_text: The text from the last search.
|
||||
_flags: The flags from the last search.
|
||||
|
||||
Signals:
|
||||
do_search: Emitted when a search should be started.
|
||||
arg 1: Search string.
|
||||
arg 2: Flags to use.
|
||||
"""
|
||||
|
||||
do_search = pyqtSignal(str, 'QWebPage::FindFlags')
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._text = None
|
||||
self._flags = 0
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self, text=self._text, flags=self._flags)
|
||||
|
||||
def _search(self, text, rev=False):
|
||||
"""Search for a text on the current page.
|
||||
|
||||
Args:
|
||||
text: The text to search for.
|
||||
rev: Search direction, True if reverse, else False.
|
||||
"""
|
||||
if self._text is not None and self._text != text:
|
||||
# We first clear the marked text, then the highlights
|
||||
self.do_search.emit('', 0)
|
||||
self.do_search.emit('', QWebPage.HighlightAllOccurrences)
|
||||
self._text = text
|
||||
self._flags = 0
|
||||
ignore_case = config.get('general', 'ignore-case')
|
||||
if ignore_case == 'smart':
|
||||
if not text.islower():
|
||||
self._flags |= QWebPage.FindCaseSensitively
|
||||
elif not ignore_case:
|
||||
self._flags |= QWebPage.FindCaseSensitively
|
||||
if config.get('general', 'wrap-search'):
|
||||
self._flags |= QWebPage.FindWrapsAroundDocument
|
||||
if rev:
|
||||
self._flags |= QWebPage.FindBackward
|
||||
# We actually search *twice* - once to highlight everything, then again
|
||||
# to get a mark so we can navigate.
|
||||
self.do_search.emit(self._text, self._flags)
|
||||
self.do_search.emit(self._text, self._flags |
|
||||
QWebPage.HighlightAllOccurrences)
|
||||
|
||||
@pyqtSlot(str)
|
||||
def search(self, text):
|
||||
"""Search for a text on a website.
|
||||
|
||||
Args:
|
||||
text: The text to search for.
|
||||
"""
|
||||
self._search(text)
|
||||
|
||||
@pyqtSlot(str)
|
||||
def search_rev(self, text):
|
||||
"""Search for a text on a website in reverse direction.
|
||||
|
||||
Args:
|
||||
text: The text to search for.
|
||||
"""
|
||||
self._search(text, rev=True)
|
||||
|
||||
@cmdutils.register(instance='search-runner', hide=True, scope='window')
|
||||
def search_next(self, count: {'special': 'count'}=1):
|
||||
"""Continue the search to the ([count]th) next term.
|
||||
|
||||
Args:
|
||||
count: How many elements to ignore.
|
||||
"""
|
||||
if self._text is not None:
|
||||
for _ in range(count):
|
||||
self.do_search.emit(self._text, self._flags)
|
||||
|
||||
@cmdutils.register(instance='search-runner', hide=True, scope='window')
|
||||
def search_prev(self, count: {'special': 'count'}=1):
|
||||
"""Continue the search to the ([count]th) previous term.
|
||||
|
||||
Args:
|
||||
count: How many elements to ignore.
|
||||
"""
|
||||
if self._text is None:
|
||||
return
|
||||
# The int() here serves as a QFlags constructor to create a copy of the
|
||||
# QFlags instance rather as a reference. I don't know why it works this
|
||||
# way, but it does.
|
||||
flags = int(self._flags)
|
||||
if flags & QWebPage.FindBackward:
|
||||
flags &= ~QWebPage.FindBackward
|
||||
else:
|
||||
flags |= QWebPage.FindBackward
|
||||
for _ in range(count):
|
||||
self.do_search.emit(self._text, flags)
|
||||
|
||||
|
||||
class CommandRunner(QObject):
|
||||
|
||||
"""Parse and run qutebrowser commandline commands.
|
||||
|
||||
Attributes:
|
||||
_cmd: The command which was parsed.
|
||||
_args: The arguments which were parsed.
|
||||
_win_id: The window this CommandRunner is associated with.
|
||||
"""
|
||||
|
||||
def __init__(self, win_id, parent=None):
|
||||
super().__init__(parent)
|
||||
self._cmd = None
|
||||
self._args = []
|
||||
self._win_id = win_id
|
||||
|
||||
def _get_alias(self, text):
|
||||
@@ -189,7 +90,34 @@ class CommandRunner(QObject):
|
||||
new_cmd += ' '
|
||||
return new_cmd
|
||||
|
||||
def parse(self, text, aliases=True, fallback=False, keep=False):
|
||||
def parse_all(self, text, *args, **kwargs):
|
||||
"""Split a command on ;; and parse all parts.
|
||||
|
||||
If the first command in the commandline is a non-split one, it only
|
||||
returns that.
|
||||
|
||||
Args:
|
||||
text: Text to parse.
|
||||
*args/**kwargs: Passed to parse().
|
||||
|
||||
Yields:
|
||||
ParseResult tuples.
|
||||
"""
|
||||
if ';;' in text:
|
||||
# Get the first command and check if it doesn't want to have ;;
|
||||
# split.
|
||||
first = text.split(';;')[0]
|
||||
result = self.parse(first, *args, **kwargs)
|
||||
if result.cmd.no_cmd_split:
|
||||
sub_texts = [text]
|
||||
else:
|
||||
sub_texts = [e.strip() for e in text.split(';;')]
|
||||
else:
|
||||
sub_texts = [text]
|
||||
for sub in sub_texts:
|
||||
yield self.parse(sub, *args, **kwargs)
|
||||
|
||||
def parse(self, text, *, aliases=True, fallback=False, keep=False):
|
||||
"""Split the commandline text into command and arguments.
|
||||
|
||||
Args:
|
||||
@@ -200,7 +128,7 @@ class CommandRunner(QObject):
|
||||
keep: Whether to keep special chars and whitespace
|
||||
|
||||
Return:
|
||||
A split string commandline, e.g ['open', 'www.google.com']
|
||||
A (cmd, args, cmdline) ParseResult tuple.
|
||||
"""
|
||||
cmdstr, sep, argstr = text.partition(' ')
|
||||
if not cmdstr and not fallback:
|
||||
@@ -212,29 +140,34 @@ class CommandRunner(QObject):
|
||||
return self.parse(new_cmd, aliases=False, fallback=fallback,
|
||||
keep=keep)
|
||||
try:
|
||||
self._cmd = cmdutils.cmd_dict[cmdstr]
|
||||
cmd = cmdutils.cmd_dict[cmdstr]
|
||||
except KeyError:
|
||||
if fallback and keep:
|
||||
cmdstr, sep, argstr = text.partition(' ')
|
||||
return [cmdstr, sep] + argstr.split()
|
||||
elif fallback:
|
||||
return text.split()
|
||||
if fallback:
|
||||
cmd = None
|
||||
args = None
|
||||
if keep:
|
||||
cmdstr, sep, argstr = text.partition(' ')
|
||||
cmdline = [cmdstr, sep] + argstr.split()
|
||||
else:
|
||||
cmdline = text.split()
|
||||
else:
|
||||
raise cmdexc.NoSuchCommandError(
|
||||
'{}: no such command'.format(cmdstr))
|
||||
self._split_args(argstr, keep)
|
||||
retargs = self._args[:]
|
||||
if keep and retargs:
|
||||
return [cmdstr, sep + retargs[0]] + retargs[1:]
|
||||
elif keep:
|
||||
return [cmdstr, sep]
|
||||
raise cmdexc.NoSuchCommandError('{}: no such command'.format(
|
||||
cmdstr))
|
||||
else:
|
||||
return [cmdstr] + retargs
|
||||
args = self._split_args(cmd, argstr, keep)
|
||||
if keep and args:
|
||||
cmdline = [cmdstr, sep + args[0]] + args[1:]
|
||||
elif keep:
|
||||
cmdline = [cmdstr, sep]
|
||||
else:
|
||||
cmdline = [cmdstr] + args[:]
|
||||
return ParseResult(cmd=cmd, args=args, cmdline=cmdline)
|
||||
|
||||
def _split_args(self, argstr, keep):
|
||||
def _split_args(self, cmd, argstr, keep):
|
||||
"""Split the arguments from an arg string.
|
||||
|
||||
Args:
|
||||
cmd: The command we're currently handling.
|
||||
argstr: An argument string.
|
||||
keep: Whether to keep special chars and whitespace
|
||||
|
||||
@@ -242,9 +175,9 @@ class CommandRunner(QObject):
|
||||
A list containing the splitted strings.
|
||||
"""
|
||||
if not argstr:
|
||||
self._args = []
|
||||
elif self._cmd.maxsplit is None:
|
||||
self._args = split.split(argstr, keep=keep)
|
||||
return []
|
||||
elif cmd.maxsplit is None:
|
||||
return split.split(argstr, keep=keep)
|
||||
else:
|
||||
# If split=False, we still want to split the flags, but not
|
||||
# everything after that.
|
||||
@@ -262,23 +195,16 @@ class CommandRunner(QObject):
|
||||
for i, arg in enumerate(split_args):
|
||||
arg = arg.strip()
|
||||
if arg.startswith('-'):
|
||||
if arg.lstrip('-') in self._cmd.flags_with_args:
|
||||
if arg in cmd.flags_with_args:
|
||||
flag_arg_count += 1
|
||||
else:
|
||||
self._args = []
|
||||
maxsplit = i + self._cmd.maxsplit + flag_arg_count
|
||||
args = split.simple_split(argstr, keep=keep,
|
||||
maxsplit = i + cmd.maxsplit + flag_arg_count
|
||||
return split.simple_split(argstr, keep=keep,
|
||||
maxsplit=maxsplit)
|
||||
for s in args:
|
||||
# remove quotes and replace \" by "
|
||||
s = re.sub(r"""(^|[^\\])["']""", r'\1', s)
|
||||
s = re.sub(r"""\\(["'])""", r'\1', s)
|
||||
self._args.append(s)
|
||||
break
|
||||
else:
|
||||
else: # pylint: disable=useless-else-on-loop
|
||||
# If there are only flags, we got it right on the first try
|
||||
# already.
|
||||
self._args = split_args
|
||||
return split_args
|
||||
|
||||
def run(self, text, count=None):
|
||||
"""Parse a command from a line of text and run it.
|
||||
@@ -287,16 +213,12 @@ class CommandRunner(QObject):
|
||||
text: The text to parse.
|
||||
count: The count to pass to the command.
|
||||
"""
|
||||
if ';;' in text:
|
||||
for sub in text.split(';;'):
|
||||
self.run(sub, count)
|
||||
return
|
||||
self.parse(text)
|
||||
args = replace_variables(self._win_id, self._args)
|
||||
if count is not None:
|
||||
self._cmd.run(self._win_id, args, count=count)
|
||||
else:
|
||||
self._cmd.run(self._win_id, args)
|
||||
for result in self.parse_all(text):
|
||||
args = replace_variables(self._win_id, result.args)
|
||||
if count is not None:
|
||||
result.cmd.run(self._win_id, args, count=count)
|
||||
else:
|
||||
result.cmd.run(self._win_id, args)
|
||||
|
||||
@pyqtSlot(str, int)
|
||||
def run_safely(self, text, count=None):
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
@@ -22,67 +22,46 @@
|
||||
import os
|
||||
import os.path
|
||||
import tempfile
|
||||
import select
|
||||
|
||||
from PyQt5.QtCore import (pyqtSignal, QObject, QThread, QStandardPaths,
|
||||
QProcessEnvironment, QProcess, QUrl)
|
||||
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 _BlockingFIFOReader(QObject):
|
||||
class _QtFIFOReader(QObject):
|
||||
|
||||
"""A worker which reads commands from a FIFO endlessly.
|
||||
|
||||
This is intended to be run in a separate QThread. It reads from the given
|
||||
FIFO even across EOF so an userscript can write to it multiple times.
|
||||
|
||||
It uses select() so it can timeout once per second, checking if termination
|
||||
was requested.
|
||||
|
||||
Attributes:
|
||||
_filepath: The filename of the FIFO to read.
|
||||
fifo: The file object which is being read.
|
||||
|
||||
Signals:
|
||||
got_line: Emitted when a new line arrived.
|
||||
finished: Emitted when the read loop realized it should terminate and
|
||||
is about to do so.
|
||||
"""
|
||||
"""A FIFO reader based on a QSocketNotifier."""
|
||||
|
||||
got_line = pyqtSignal(str)
|
||||
finished = pyqtSignal()
|
||||
|
||||
def __init__(self, filepath, parent=None):
|
||||
super().__init__(parent)
|
||||
self._filepath = filepath
|
||||
self.fifo = None
|
||||
# We open as R/W so we never get EOF and have to reopen the pipe.
|
||||
# See http://www.outflux.net/blog/archives/2008/03/09/using-select-on-a-fifo/
|
||||
# We also use os.open and os.fdopen rather than built-in open so we
|
||||
# can add O_NONBLOCK.
|
||||
fd = os.open(filepath, os.O_RDWR |
|
||||
os.O_NONBLOCK) # pylint: disable=no-member
|
||||
self.fifo = os.fdopen(fd, 'r')
|
||||
self._notifier = QSocketNotifier(fd, QSocketNotifier.Read, self)
|
||||
self._notifier.activated.connect(self.read_line)
|
||||
|
||||
def read(self):
|
||||
"""Blocking read loop which emits got_line when a new line arrived."""
|
||||
try:
|
||||
# We open as R/W so we never get EOF and have to reopen the pipe.
|
||||
# See http://www.outflux.net/blog/archives/2008/03/09/using-select-on-a-fifo/
|
||||
# We also use os.open and os.fdopen rather than built-in open so we
|
||||
# can add O_NONBLOCK.
|
||||
fd = os.open(self._filepath, os.O_RDWR |
|
||||
os.O_NONBLOCK) # pylint: disable=no-member
|
||||
self.fifo = os.fdopen(fd, 'r')
|
||||
except OSError:
|
||||
log.procs.exception("Failed to read FIFO")
|
||||
self.finished.emit()
|
||||
return
|
||||
while True:
|
||||
log.procs.debug("thread loop")
|
||||
ready_r, _ready_w, _ready_e = select.select([self.fifo], [], [], 1)
|
||||
if ready_r:
|
||||
log.procs.debug("reading data")
|
||||
for line in self.fifo:
|
||||
self.got_line.emit(line.rstrip())
|
||||
if QThread.currentThread().isInterruptionRequested():
|
||||
self.finished.emit()
|
||||
return
|
||||
@pyqtSlot()
|
||||
def read_line(self):
|
||||
"""(Try to) read a line from the FIFO."""
|
||||
log.procs.debug("QSocketNotifier triggered!")
|
||||
self._notifier.setEnabled(False)
|
||||
for line in self.fifo:
|
||||
self.got_line.emit(line.rstrip('\r\n'))
|
||||
self._notifier.setEnabled(True)
|
||||
|
||||
def cleanup(self):
|
||||
"""Clean up so the FIFO can be closed."""
|
||||
self._notifier.setEnabled(False)
|
||||
|
||||
|
||||
class _BaseUserscriptRunner(QObject):
|
||||
@@ -91,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.
|
||||
@@ -106,149 +81,129 @@ 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."""
|
||||
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))
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class _POSIXUserscriptRunner(_BaseUserscriptRunner):
|
||||
|
||||
"""Userscript runner to be used on POSIX. Uses _BlockingFIFOReader.
|
||||
"""Userscript runner to be used on POSIX. Uses _QtFIFOReader.
|
||||
|
||||
The OS must have support for named pipes and select(). Commands are
|
||||
executed immediately when they arrive in the FIFO.
|
||||
Commands are executed immediately when they arrive in the FIFO.
|
||||
|
||||
Attributes:
|
||||
_reader: The _BlockingFIFOReader instance.
|
||||
_thread: The QThread where reader runs.
|
||||
_reader: The _QtFIFOReader instance.
|
||||
"""
|
||||
|
||||
def __init__(self, win_id, parent=None):
|
||||
super().__init__(win_id, parent)
|
||||
self._reader = None
|
||||
self._thread = None
|
||||
|
||||
def run(self, cmd, *args, env=None):
|
||||
rundir = standarddir.get(QStandardPaths.RuntimeLocation)
|
||||
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
|
||||
# create a directory and place the FIFO there, which sucks. Since
|
||||
# os.kfifo will raise an exception anyways when the path doesn't
|
||||
# os.mkfifo will raise an exception anyways when the path doesn't
|
||||
# exist, it shouldn't be a big issue.
|
||||
self._filepath = tempfile.mktemp(prefix='userscript-', dir=rundir)
|
||||
self._filepath = tempfile.mktemp(prefix='qutebrowser-userscript-',
|
||||
dir=standarddir.runtime())
|
||||
os.mkfifo(self._filepath) # pylint: disable=no-member
|
||||
except OSError as e:
|
||||
message.error(self._win_id, "Error while creating FIFO: {}".format(
|
||||
e))
|
||||
return
|
||||
|
||||
self._reader = _BlockingFIFOReader(self._filepath)
|
||||
self._thread = QThread(self)
|
||||
self._reader.moveToThread(self._thread)
|
||||
self._reader = _QtFIFOReader(self._filepath)
|
||||
self._reader.got_line.connect(self.got_cmd)
|
||||
self._thread.started.connect(self._reader.read)
|
||||
self._reader.finished.connect(self.on_reader_finished)
|
||||
self._thread.finished.connect(self.on_thread_finished)
|
||||
|
||||
self._run_process(cmd, *args, env=env)
|
||||
self._thread.start()
|
||||
self._run_process(cmd, *args, env=env, verbose=verbose)
|
||||
|
||||
def on_proc_finished(self):
|
||||
"""Interrupt the reader when the process finished."""
|
||||
log.procs.debug("proc finished")
|
||||
self._thread.requestInterruption()
|
||||
self.finish()
|
||||
|
||||
def on_proc_error(self, error):
|
||||
"""Interrupt the reader when the process had an error."""
|
||||
super().on_proc_error(error)
|
||||
self._thread.requestInterruption()
|
||||
self.finish()
|
||||
|
||||
def on_reader_finished(self):
|
||||
def finish(self):
|
||||
"""Quit the thread and clean up when the reader finished."""
|
||||
log.procs.debug("reader finished")
|
||||
self._thread.quit()
|
||||
log.procs.debug("Cleaning up")
|
||||
self._reader.cleanup()
|
||||
self._reader.fifo.close()
|
||||
self._reader.deleteLater()
|
||||
self._reader = None
|
||||
super()._cleanup()
|
||||
self.finished.emit()
|
||||
|
||||
def on_thread_finished(self):
|
||||
"""Clean up the QThread object when the thread finished."""
|
||||
log.procs.debug("thread finished")
|
||||
self._thread.deleteLater()
|
||||
|
||||
|
||||
class _WindowsUserscriptRunner(_BaseUserscriptRunner):
|
||||
|
||||
@@ -281,7 +236,6 @@ class _WindowsUserscriptRunner(_BaseUserscriptRunner):
|
||||
|
||||
def on_proc_finished(self):
|
||||
"""Read back the commands when the process finished."""
|
||||
log.procs.debug("proc finished")
|
||||
try:
|
||||
with open(self._filepath, 'r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
@@ -293,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:
|
||||
@@ -320,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!")
|
||||
@@ -337,16 +291,58 @@ else:
|
||||
UserscriptRunner = _DummyUserscriptRunner
|
||||
|
||||
|
||||
def run(cmd, *args, url, win_id):
|
||||
"""Convenience method to run an userscript."""
|
||||
def store_source(frame):
|
||||
"""Store HTML/plaintext in files.
|
||||
|
||||
This writes files containing the HTML/plaintext source of the page, and
|
||||
returns a dict with the paths as QUTE_HTML/QUTE_TEXT.
|
||||
|
||||
Args:
|
||||
frame: The QWebFrame to get the info from, or None to do nothing.
|
||||
|
||||
Return:
|
||||
A dictionary with the needed environment variables.
|
||||
|
||||
Warning:
|
||||
The caller is responsible to delete the files after using them!
|
||||
"""
|
||||
if frame is None:
|
||||
return {}
|
||||
env = {}
|
||||
with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8',
|
||||
suffix='.html',
|
||||
delete=False) as html_file:
|
||||
html_file.write(frame.toHtml())
|
||||
env['QUTE_HTML'] = html_file.name
|
||||
with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8',
|
||||
suffix='.txt',
|
||||
delete=False) as txt_file:
|
||||
txt_file.write(frame.toPlainText())
|
||||
env['QUTE_TEXT'] = txt_file.name
|
||||
return env
|
||||
|
||||
|
||||
def run(cmd, *args, win_id, env, verbose=False):
|
||||
"""Convenience method to run an userscript.
|
||||
|
||||
Args:
|
||||
cmd: The userscript binary to run.
|
||||
*args: The arguments to pass to the userscript.
|
||||
win_id: The window id the userscript is executed in.
|
||||
env: A dictionary of variables to add to the process environment.
|
||||
verbose: Show notifications when the command started/exited.
|
||||
"""
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=win_id)
|
||||
# We don't remove the password in the URL here, as it's probably safe to
|
||||
# pass via env variable..
|
||||
urlstr = url.toString(QUrl.FullyEncoded)
|
||||
commandrunner = runners.CommandRunner(win_id, tabbed_browser)
|
||||
runner = UserscriptRunner(win_id, tabbed_browser)
|
||||
runner.got_cmd.connect(
|
||||
lambda cmd: log.commands.debug("Got userscript command: {}".format(
|
||||
cmd)))
|
||||
runner.got_cmd.connect(commandrunner.run_safely)
|
||||
runner.run(cmd, *args, env={'QUTE_URL': urlstr})
|
||||
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, verbose=verbose)
|
||||
runner.finished.connect(commandrunner.deleteLater)
|
||||
runner.finished.connect(runner.deleteLater)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
@@ -19,14 +19,12 @@
|
||||
|
||||
"""Completer attached to a CompletionView."""
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot, QObject, QTimer
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QTimer
|
||||
|
||||
from qutebrowser.config import config, configdata
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.commands import cmdutils, runners
|
||||
from qutebrowser.utils import usertypes, log, objreg, utils
|
||||
from qutebrowser.completion.models import completion as models
|
||||
from qutebrowser.completion.models.sortfilter import (
|
||||
CompletionFilterModel as CFM)
|
||||
from qutebrowser.completion.models import instances
|
||||
|
||||
|
||||
class Completer(QObject):
|
||||
@@ -34,7 +32,6 @@ class Completer(QObject):
|
||||
"""Completer which manages completions in a CompletionView.
|
||||
|
||||
Attributes:
|
||||
models: dict of available completion models.
|
||||
_cmd: The statusbar Command object this completer belongs to.
|
||||
_ignore_change: Whether to ignore the next completion update.
|
||||
_win_id: The window ID this completer is in.
|
||||
@@ -43,24 +40,24 @@ 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._models = {
|
||||
usertypes.Completion.option: {},
|
||||
usertypes.Completion.value: {},
|
||||
}
|
||||
self._init_static_completions()
|
||||
self._init_setting_completions()
|
||||
self.init_quickmark_completions()
|
||||
self._timer = QTimer()
|
||||
self._timer.setSingleShot(True)
|
||||
self._timer.setInterval(0)
|
||||
@@ -69,51 +66,69 @@ 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',
|
||||
window=self._win_id)
|
||||
return completion.model()
|
||||
|
||||
def _init_static_completions(self):
|
||||
"""Initialize the static completion models."""
|
||||
self._models[usertypes.Completion.command] = CFM(
|
||||
models.CommandCompletionModel(self), self)
|
||||
self._models[usertypes.Completion.helptopic] = CFM(
|
||||
models.HelpCompletionModel(self), self)
|
||||
|
||||
def _init_setting_completions(self):
|
||||
"""Initialize setting completion models."""
|
||||
self._models[usertypes.Completion.section] = CFM(
|
||||
models.SettingSectionCompletionModel(self), self)
|
||||
self._models[usertypes.Completion.option] = {}
|
||||
self._models[usertypes.Completion.value] = {}
|
||||
for sectname in configdata.DATA:
|
||||
model = models.SettingOptionCompletionModel(sectname, self)
|
||||
self._models[usertypes.Completion.option][sectname] = CFM(
|
||||
model, self)
|
||||
self._models[usertypes.Completion.value][sectname] = {}
|
||||
for opt in configdata.DATA[sectname].keys():
|
||||
model = models.SettingValueCompletionModel(sectname, opt, self)
|
||||
self._models[usertypes.Completion.value][sectname][opt] = CFM(
|
||||
model, self)
|
||||
|
||||
@pyqtSlot()
|
||||
def init_quickmark_completions(self):
|
||||
"""Initialize quickmark completion models."""
|
||||
try:
|
||||
self._models[usertypes.Completion.quickmark_by_url].deleteLater()
|
||||
self._models[usertypes.Completion.quickmark_by_name].deleteLater()
|
||||
except KeyError:
|
||||
pass
|
||||
self._models[usertypes.Completion.quickmark_by_url] = CFM(
|
||||
models.QuickmarkCompletionModel('url', self), self)
|
||||
self._models[usertypes.Completion.quickmark_by_name] = CFM(
|
||||
models.QuickmarkCompletionModel('name', self), self)
|
||||
|
||||
def _get_completion_model(self, completion, parts, cursor_part):
|
||||
"""Get a completion model based on an enum member.
|
||||
|
||||
@@ -127,17 +142,17 @@ class Completer(QObject):
|
||||
"""
|
||||
if completion == usertypes.Completion.option:
|
||||
section = parts[cursor_part - 1]
|
||||
model = self._models[completion].get(section)
|
||||
model = instances.get(completion).get(section)
|
||||
elif completion == usertypes.Completion.value:
|
||||
section = parts[cursor_part - 2]
|
||||
option = parts[cursor_part - 1]
|
||||
try:
|
||||
model = self._models[completion][section][option]
|
||||
model = instances.get(completion)[section][option]
|
||||
except KeyError:
|
||||
# No completion model for this section/option.
|
||||
model = None
|
||||
else:
|
||||
model = self._models.get(completion)
|
||||
model = instances.get(completion)
|
||||
return model
|
||||
|
||||
def _filter_cmdline_parts(self, parts, cursor_part):
|
||||
@@ -187,7 +202,7 @@ class Completer(QObject):
|
||||
"{}".format(parts, cursor_part))
|
||||
if cursor_part == 0:
|
||||
# '|' or 'set|'
|
||||
return self._models[usertypes.Completion.command]
|
||||
return instances.get(usertypes.Completion.command)
|
||||
# delegate completion to command
|
||||
try:
|
||||
completions = cmdutils.cmd_dict[parts[0]].completion
|
||||
@@ -243,7 +258,13 @@ class Completer(QObject):
|
||||
data = model.data(indexes[0])
|
||||
if data is None:
|
||||
return
|
||||
data = self._quote(data)
|
||||
parts = self.split()
|
||||
try:
|
||||
needs_quoting = cmdutils.cmd_dict[parts[0]].maxsplit is None
|
||||
except KeyError:
|
||||
needs_quoting = True
|
||||
if needs_quoting:
|
||||
data = self._quote(data)
|
||||
if model.count() == 1 and config.get('completion', 'quick-complete'):
|
||||
# If we only have one item, we want to apply it immediately
|
||||
# and go on to the next part.
|
||||
@@ -292,7 +313,7 @@ class Completer(QObject):
|
||||
if self._cmd.prefix() != ':':
|
||||
# This is a search or gibberish, so we don't need to complete
|
||||
# anything (yet)
|
||||
# FIXME complete searchs
|
||||
# FIXME complete searches
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/32
|
||||
completion.hide()
|
||||
return
|
||||
@@ -313,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(
|
||||
@@ -343,7 +364,8 @@ class Completer(QObject):
|
||||
# the whitespace.
|
||||
return [text]
|
||||
runner = runners.CommandRunner(self._win_id)
|
||||
parts = runner.parse(text, fallback=True, aliases=aliases, keep=keep)
|
||||
result = runner.parse(text, fallback=True, aliases=aliases, keep=keep)
|
||||
parts = result.cmdline
|
||||
if self._empty_item_idx is not None:
|
||||
log.completion.debug("Empty element queued at {}, "
|
||||
"inserting.".format(self._empty_item_idx))
|
||||
@@ -368,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
|
||||
@@ -390,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:
|
||||
@@ -441,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)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
@@ -22,7 +22,9 @@
|
||||
We use this to be able to highlight parts of the text.
|
||||
"""
|
||||
|
||||
import re
|
||||
import html
|
||||
|
||||
from PyQt5.QtWidgets import QStyle, QStyleOptionViewItem, QStyledItemDelegate
|
||||
from PyQt5.QtCore import QRectF, QSize, Qt
|
||||
from PyQt5.QtGui import (QIcon, QPalette, QTextDocument, QTextOption,
|
||||
@@ -143,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:
|
||||
@@ -195,9 +196,9 @@ class CompletionItemDelegate(QStyledItemDelegate):
|
||||
if index.parent().isValid():
|
||||
pattern = index.model().pattern
|
||||
if index.column() == 0 and pattern:
|
||||
text = self._opt.text.replace(
|
||||
pattern,
|
||||
'<span class="highlight">{}</span>'.format(pattern))
|
||||
repl = r'<span class="highlight">\g<0></span>'
|
||||
text = re.sub(re.escape(pattern), repl, self._opt.text,
|
||||
flags=re.IGNORECASE)
|
||||
self._doc.setHtml(text)
|
||||
else:
|
||||
self._doc.setPlainText(self._opt.text)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
@@ -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):
|
||||
@@ -59,6 +58,7 @@ class CompletionView(QTreeView):
|
||||
QTreeView {
|
||||
{{ font['completion'] }}
|
||||
{{ color['completion.bg'] }}
|
||||
alternate-background-color: {{ color['completion.alternate-bg'] }};
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
@@ -95,18 +95,20 @@ 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)
|
||||
style.set_register_stylesheet(self)
|
||||
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Minimum)
|
||||
self.setHeaderHidden(True)
|
||||
self.setAlternatingRowColors(True)
|
||||
self.setIndentation(0)
|
||||
self.setItemsExpandable(False)
|
||||
self.setExpandsOnDoubleClick(False)
|
||||
@@ -149,7 +151,10 @@ class CompletionView(QTreeView):
|
||||
idx = self.selectionModel().currentIndex()
|
||||
if not idx.isValid():
|
||||
# No item selected yet
|
||||
return self.model().first_item()
|
||||
if upwards:
|
||||
return self.model().last_item()
|
||||
else:
|
||||
return self.model().first_item()
|
||||
while True:
|
||||
idx = self.indexAbove(idx) if upwards else self.indexBelow(idx)
|
||||
# wrap around if we arrived at beginning/end
|
||||
@@ -163,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.
|
||||
"""
|
||||
@@ -193,10 +201,20 @@ class CompletionView(QTreeView):
|
||||
self.setModel(model)
|
||||
if sel_model is not None:
|
||||
sel_model.deleteLater()
|
||||
self.expandAll()
|
||||
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()
|
||||
@@ -218,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)
|
||||
@@ -241,3 +247,11 @@ class CompletionView(QTreeView):
|
||||
"""Extend resizeEvent to adjust column size."""
|
||||
super().resizeEvent(e)
|
||||
self._resize_columns()
|
||||
|
||||
def showEvent(self, e):
|
||||
"""Adjust the completion size and scroll when it's freshly shown."""
|
||||
self.resize_completion.emit()
|
||||
scrollbar = self.verticalScrollBar()
|
||||
if scrollbar is not None:
|
||||
scrollbar.setValue(scrollbar.minimum())
|
||||
super().showEvent(e)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
@@ -29,7 +29,8 @@ from PyQt5.QtGui import QStandardItemModel, QStandardItem
|
||||
from qutebrowser.utils import usertypes, qtutils
|
||||
|
||||
|
||||
Role = usertypes.enum('Role', ['sort'], start=Qt.UserRole, is_int=True)
|
||||
Role = usertypes.enum('Role', ['sort', 'userdata'], start=Qt.UserRole,
|
||||
is_int=True)
|
||||
|
||||
|
||||
class BaseCompletionModel(QStandardItemModel):
|
||||
@@ -60,7 +61,8 @@ class BaseCompletionModel(QStandardItemModel):
|
||||
self.appendRow(cat)
|
||||
return cat
|
||||
|
||||
def new_item(self, cat, name, desc='', misc=None):
|
||||
def new_item(self, cat, name, desc='', misc=None, sort=None,
|
||||
userdata=None):
|
||||
"""Add a new item to a category.
|
||||
|
||||
Args:
|
||||
@@ -68,10 +70,15 @@ class BaseCompletionModel(QStandardItemModel):
|
||||
name: The name of the item.
|
||||
desc: The description of the item.
|
||||
misc: Misc text to display.
|
||||
sort: Data for the sort role (int).
|
||||
userdata: User data to be added for the first column.
|
||||
|
||||
Return:
|
||||
A (nameitem, descitem, miscitem) tuple.
|
||||
"""
|
||||
assert not isinstance(name, int)
|
||||
assert not isinstance(desc, int)
|
||||
assert not isinstance(misc, int)
|
||||
nameitem = QStandardItem(name)
|
||||
descitem = QStandardItem(desc)
|
||||
if misc is None:
|
||||
@@ -82,6 +89,10 @@ class BaseCompletionModel(QStandardItemModel):
|
||||
cat.setChild(idx, 0, nameitem)
|
||||
cat.setChild(idx, 1, descitem)
|
||||
cat.setChild(idx, 2, miscitem)
|
||||
if sort is not None:
|
||||
nameitem.setData(sort, Role.sort)
|
||||
if userdata is not None:
|
||||
nameitem.setData(userdata, Role.userdata)
|
||||
return nameitem, descitem, miscitem
|
||||
|
||||
def flags(self, index):
|
||||
@@ -98,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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
@@ -17,13 +17,12 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""CompletionModels for different usages."""
|
||||
"""CompletionModels for the config."""
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot, Qt
|
||||
|
||||
from qutebrowser.config import config, configdata
|
||||
from qutebrowser.utils import log, qtutils, objreg
|
||||
from qutebrowser.commands import cmdutils
|
||||
from qutebrowser.completion.models import base
|
||||
|
||||
|
||||
@@ -110,12 +109,16 @@ class SettingValueCompletionModel(base.BaseCompletionModel):
|
||||
self._section = section
|
||||
self._option = option
|
||||
objreg.get('config').changed.connect(self.update_current_value)
|
||||
cur_cat = self.new_category("Current", sort=0)
|
||||
cur_cat = self.new_category("Current/Default", sort=0)
|
||||
value = config.get(section, option, raw=True)
|
||||
if not value:
|
||||
value = '""'
|
||||
self.cur_item, _descitem, _miscitem = self.new_item(cur_cat, value,
|
||||
"Current value")
|
||||
default_value = configdata.DATA[section][option].default()
|
||||
if not default_value:
|
||||
default_value = '""'
|
||||
self.new_item(cur_cat, default_value, "Default value")
|
||||
if hasattr(configdata.DATA[section], 'valtype'):
|
||||
# Same type for all values (ValueList)
|
||||
vals = configdata.DATA[section].valtype.complete()
|
||||
@@ -126,7 +129,7 @@ class SettingValueCompletionModel(base.BaseCompletionModel):
|
||||
# Different type for each value (KeyValue)
|
||||
vals = configdata.DATA[section][option].typ.complete()
|
||||
if vals is not None:
|
||||
cat = self.new_category("Allowed", sort=1)
|
||||
cat = self.new_category("Completions", sort=1)
|
||||
for (val, desc) in vals:
|
||||
self.new_item(cat, val, desc)
|
||||
|
||||
@@ -144,89 +147,3 @@ class SettingValueCompletionModel(base.BaseCompletionModel):
|
||||
if not ok:
|
||||
raise ValueError("Setting data failed! (section: {}, option: {}, "
|
||||
"value: {})".format(section, option, value))
|
||||
|
||||
|
||||
class CommandCompletionModel(base.BaseCompletionModel):
|
||||
|
||||
"""A CompletionModel filled with all commands and descriptions."""
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
assert cmdutils.cmd_dict
|
||||
cmdlist = []
|
||||
for obj in set(cmdutils.cmd_dict.values()):
|
||||
if obj.hide or (obj.debug and not objreg.get('args').debug):
|
||||
pass
|
||||
else:
|
||||
cmdlist.append((obj.name, obj.desc))
|
||||
for name, cmd in config.section('aliases').items():
|
||||
cmdlist.append((name, "Alias for '{}'".format(cmd)))
|
||||
cat = self.new_category("Commands")
|
||||
for (name, desc) in sorted(cmdlist):
|
||||
self.new_item(cat, name, desc)
|
||||
|
||||
|
||||
class HelpCompletionModel(base.BaseCompletionModel):
|
||||
|
||||
"""A CompletionModel filled with help topics."""
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._init_commands()
|
||||
self._init_settings()
|
||||
|
||||
def _init_commands(self):
|
||||
"""Fill completion with :command entries."""
|
||||
assert cmdutils.cmd_dict
|
||||
cmdlist = []
|
||||
for obj in set(cmdutils.cmd_dict.values()):
|
||||
if obj.hide or (obj.debug and not objreg.get('args').debug):
|
||||
pass
|
||||
else:
|
||||
cmdlist.append((':' + obj.name, obj.desc))
|
||||
cat = self.new_category("Commands")
|
||||
for (name, desc) in sorted(cmdlist):
|
||||
self.new_item(cat, name, desc)
|
||||
|
||||
def _init_settings(self):
|
||||
"""Fill completion with section->option entries."""
|
||||
cat = self.new_category("Settings")
|
||||
for sectname, sectdata in configdata.DATA.items():
|
||||
for optname in sectdata.keys():
|
||||
try:
|
||||
desc = sectdata.descriptions[optname]
|
||||
except (KeyError, AttributeError):
|
||||
# Some stuff (especially ValueList items) don't have a
|
||||
# description.
|
||||
desc = ""
|
||||
else:
|
||||
desc = desc.splitlines()[0]
|
||||
name = '{}->{}'.format(sectname, optname)
|
||||
self.new_item(cat, name, desc)
|
||||
|
||||
|
||||
class QuickmarkCompletionModel(base.BaseCompletionModel):
|
||||
|
||||
"""A CompletionModel filled with all quickmarks."""
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
|
||||
def __init__(self, match_field='url', parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
cat = self.new_category("Quickmarks")
|
||||
quickmarks = objreg.get('quickmark-manager').marks.items()
|
||||
|
||||
if match_field == 'url':
|
||||
for qm_name, qm_url in quickmarks:
|
||||
self.new_item(cat, qm_url, qm_name)
|
||||
elif match_field == 'name':
|
||||
for qm_name, qm_url in quickmarks:
|
||||
self.new_item(cat, qm_name, qm_url)
|
||||
else:
|
||||
raise ValueError("Invalid value '{}' for match_field!".format(
|
||||
match_field))
|
||||
175
qutebrowser/completion/models/instances.py
Normal file
175
qutebrowser/completion/models/instances.py
Normal file
@@ -0,0 +1,175 @@
|
||||
# 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/>.
|
||||
|
||||
"""Global instances of the completion models.
|
||||
|
||||
Module attributes:
|
||||
_instances: An dict of available completions.
|
||||
INITIALIZERS: A {usertypes.Completion: callable} dict of functions to
|
||||
initialize completions.
|
||||
"""
|
||||
|
||||
import functools
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot, Qt
|
||||
|
||||
from qutebrowser.completion.models import miscmodels, urlmodel, configmodel
|
||||
from qutebrowser.completion.models.sortfilter import CompletionFilterModel
|
||||
from qutebrowser.utils import objreg, usertypes, log, debug
|
||||
from qutebrowser.config import configdata
|
||||
|
||||
|
||||
_instances = {}
|
||||
|
||||
|
||||
def _init_model(klass, *args, dumb_sort=None, **kwargs):
|
||||
"""Helper to initialize a model.
|
||||
|
||||
Args:
|
||||
klass: The class of the model to initialize.
|
||||
*args: Arguments to pass to the model.
|
||||
**kwargs: Keyword arguments to pass to the model.
|
||||
dumb_sort: Passed to CompletionFilterModel.
|
||||
"""
|
||||
app = objreg.get('app')
|
||||
return CompletionFilterModel(klass(*args, parent=app, **kwargs),
|
||||
dumb_sort=dumb_sort, parent=app)
|
||||
|
||||
|
||||
def _init_command_completion():
|
||||
"""Initialize the command completion model."""
|
||||
log.completion.debug("Initializing command completion.")
|
||||
model = _init_model(miscmodels.CommandCompletionModel)
|
||||
_instances[usertypes.Completion.command] = model
|
||||
|
||||
|
||||
def _init_helptopic_completion():
|
||||
"""Initialize the helptopic completion model."""
|
||||
log.completion.debug("Initializing helptopic completion.")
|
||||
model = _init_model(miscmodels.HelpCompletionModel)
|
||||
_instances[usertypes.Completion.helptopic] = model
|
||||
|
||||
|
||||
def _init_url_completion():
|
||||
"""Initialize the URL completion model."""
|
||||
log.completion.debug("Initializing URL completion.")
|
||||
with debug.log_time(log.completion, 'URL completion init'):
|
||||
model = _init_model(urlmodel.UrlCompletionModel,
|
||||
dumb_sort=Qt.DescendingOrder)
|
||||
_instances[usertypes.Completion.url] = model
|
||||
|
||||
|
||||
def _init_setting_completions():
|
||||
"""Initialize setting completion models."""
|
||||
log.completion.debug("Initializing setting completion.")
|
||||
_instances[usertypes.Completion.section] = _init_model(
|
||||
configmodel.SettingSectionCompletionModel)
|
||||
_instances[usertypes.Completion.option] = {}
|
||||
_instances[usertypes.Completion.value] = {}
|
||||
for sectname in configdata.DATA:
|
||||
model = _init_model(configmodel.SettingOptionCompletionModel, sectname)
|
||||
_instances[usertypes.Completion.option][sectname] = model
|
||||
_instances[usertypes.Completion.value][sectname] = {}
|
||||
for opt in configdata.DATA[sectname].keys():
|
||||
model = _init_model(configmodel.SettingValueCompletionModel,
|
||||
sectname, opt)
|
||||
_instances[usertypes.Completion.value][sectname][opt] = model
|
||||
|
||||
|
||||
@pyqtSlot()
|
||||
def init_quickmark_completions():
|
||||
"""Initialize quickmark completion models."""
|
||||
log.completion.debug("Initializing quickmark completion.")
|
||||
try:
|
||||
_instances[usertypes.Completion.quickmark_by_url].deleteLater()
|
||||
_instances[usertypes.Completion.quickmark_by_name].deleteLater()
|
||||
except KeyError:
|
||||
pass
|
||||
model = _init_model(miscmodels.QuickmarkCompletionModel, 'url')
|
||||
_instances[usertypes.Completion.quickmark_by_url] = model
|
||||
model = _init_model(miscmodels.QuickmarkCompletionModel, 'name')
|
||||
_instances[usertypes.Completion.quickmark_by_name] = model
|
||||
|
||||
|
||||
@pyqtSlot()
|
||||
def init_session_completion():
|
||||
"""Initialize session completion model."""
|
||||
log.completion.debug("Initializing session completion.")
|
||||
try:
|
||||
_instances[usertypes.Completion.sessions].deleteLater()
|
||||
except KeyError:
|
||||
pass
|
||||
model = _init_model(miscmodels.SessionCompletionModel)
|
||||
_instances[usertypes.Completion.sessions] = model
|
||||
|
||||
|
||||
INITIALIZERS = {
|
||||
usertypes.Completion.command: _init_command_completion,
|
||||
usertypes.Completion.helptopic: _init_helptopic_completion,
|
||||
usertypes.Completion.url: _init_url_completion,
|
||||
usertypes.Completion.section: _init_setting_completions,
|
||||
usertypes.Completion.option: _init_setting_completions,
|
||||
usertypes.Completion.value: _init_setting_completions,
|
||||
usertypes.Completion.quickmark_by_url: init_quickmark_completions,
|
||||
usertypes.Completion.quickmark_by_name: init_quickmark_completions,
|
||||
usertypes.Completion.sessions: init_session_completion,
|
||||
}
|
||||
|
||||
|
||||
def get(completion):
|
||||
"""Get a certain completion. Initializes the completion if needed."""
|
||||
try:
|
||||
return _instances[completion]
|
||||
except KeyError:
|
||||
if completion in INITIALIZERS:
|
||||
INITIALIZERS[completion]()
|
||||
return _instances[completion]
|
||||
else:
|
||||
raise
|
||||
|
||||
|
||||
def update(completions):
|
||||
"""Update an already existing completion.
|
||||
|
||||
Args:
|
||||
completions: An iterable of usertypes.Completions.
|
||||
"""
|
||||
did_run = []
|
||||
for completion in completions:
|
||||
if completion in _instances:
|
||||
func = INITIALIZERS[completion]
|
||||
if func not in did_run:
|
||||
func()
|
||||
did_run.append(func)
|
||||
|
||||
|
||||
def init():
|
||||
"""Initialize completions. Note this only connects signals."""
|
||||
quickmark_manager = objreg.get('quickmark-manager')
|
||||
quickmark_manager.changed.connect(
|
||||
functools.partial(update, [usertypes.Completion.quickmark_by_url,
|
||||
usertypes.Completion.quickmark_by_name]))
|
||||
|
||||
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]))
|
||||
128
qutebrowser/completion/models/miscmodels.py
Normal file
128
qutebrowser/completion/models/miscmodels.py
Normal file
@@ -0,0 +1,128 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
"""Misc. CompletionModels."""
|
||||
|
||||
from qutebrowser.config import config, configdata
|
||||
from qutebrowser.utils import objreg, log
|
||||
from qutebrowser.commands import cmdutils
|
||||
from qutebrowser.completion.models import base
|
||||
|
||||
|
||||
class CommandCompletionModel(base.BaseCompletionModel):
|
||||
|
||||
"""A CompletionModel filled with all commands and descriptions."""
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
assert cmdutils.cmd_dict
|
||||
cmdlist = []
|
||||
for obj in set(cmdutils.cmd_dict.values()):
|
||||
if (obj.hide or (obj.debug and not objreg.get('args').debug) or
|
||||
obj.deprecated):
|
||||
pass
|
||||
else:
|
||||
cmdlist.append((obj.name, obj.desc))
|
||||
for name, cmd in config.section('aliases').items():
|
||||
cmdlist.append((name, "Alias for '{}'".format(cmd)))
|
||||
cat = self.new_category("Commands")
|
||||
for (name, desc) in sorted(cmdlist):
|
||||
self.new_item(cat, name, desc)
|
||||
|
||||
|
||||
class HelpCompletionModel(base.BaseCompletionModel):
|
||||
|
||||
"""A CompletionModel filled with help topics."""
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._init_commands()
|
||||
self._init_settings()
|
||||
|
||||
def _init_commands(self):
|
||||
"""Fill completion with :command entries."""
|
||||
assert cmdutils.cmd_dict
|
||||
cmdlist = []
|
||||
for obj in set(cmdutils.cmd_dict.values()):
|
||||
if (obj.hide or (obj.debug and not objreg.get('args').debug) or
|
||||
obj.deprecated):
|
||||
pass
|
||||
else:
|
||||
cmdlist.append((':' + obj.name, obj.desc))
|
||||
cat = self.new_category("Commands")
|
||||
for (name, desc) in sorted(cmdlist):
|
||||
self.new_item(cat, name, desc)
|
||||
|
||||
def _init_settings(self):
|
||||
"""Fill completion with section->option entries."""
|
||||
cat = self.new_category("Settings")
|
||||
for sectname, sectdata in configdata.DATA.items():
|
||||
for optname in sectdata.keys():
|
||||
try:
|
||||
desc = sectdata.descriptions[optname]
|
||||
except (KeyError, AttributeError):
|
||||
# Some stuff (especially ValueList items) don't have a
|
||||
# description.
|
||||
desc = ""
|
||||
else:
|
||||
desc = desc.splitlines()[0]
|
||||
name = '{}->{}'.format(sectname, optname)
|
||||
self.new_item(cat, name, desc)
|
||||
|
||||
|
||||
class QuickmarkCompletionModel(base.BaseCompletionModel):
|
||||
|
||||
"""A CompletionModel filled with all quickmarks."""
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
|
||||
def __init__(self, match_field='url', parent=None):
|
||||
super().__init__(parent)
|
||||
cat = self.new_category("Quickmarks")
|
||||
quickmarks = objreg.get('quickmark-manager').marks.items()
|
||||
if match_field == 'url':
|
||||
for qm_name, qm_url in quickmarks:
|
||||
self.new_item(cat, qm_url, qm_name)
|
||||
elif match_field == 'name':
|
||||
for qm_name, qm_url in quickmarks:
|
||||
self.new_item(cat, qm_name, qm_url)
|
||||
else:
|
||||
raise ValueError("Invalid value '{}' for match_field!".format(
|
||||
match_field))
|
||||
|
||||
|
||||
class SessionCompletionModel(base.BaseCompletionModel):
|
||||
|
||||
"""A CompletionModel filled with session names."""
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
cat = self.new_category("Sessions")
|
||||
try:
|
||||
for name in objreg.get('session-manager').list_sessions():
|
||||
if not name.startswith('_'):
|
||||
self.new_item(cat, name)
|
||||
except OSError:
|
||||
log.completion.exception("Failed to list sessions!")
|
||||
@@ -1,6 +1,6 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
@@ -23,9 +23,9 @@ Contains:
|
||||
CompletionFilterModel -- A QSortFilterProxyModel subclass for completions.
|
||||
"""
|
||||
|
||||
from PyQt5.QtCore import QSortFilterProxyModel, QModelIndex
|
||||
from PyQt5.QtCore import QSortFilterProxyModel, QModelIndex, Qt
|
||||
|
||||
from qutebrowser.utils import log, qtutils
|
||||
from qutebrowser.utils import log, qtutils, debug
|
||||
from qutebrowser.completion.models import base as completion
|
||||
|
||||
|
||||
@@ -38,13 +38,21 @@ class CompletionFilterModel(QSortFilterProxyModel):
|
||||
srcmodel: The current source model.
|
||||
Kept as attribute because calling `sourceModel` takes quite
|
||||
a long time for some reason.
|
||||
_sort_order: The order to use for sorting if using dumb_sort.
|
||||
"""
|
||||
|
||||
def __init__(self, source, parent=None):
|
||||
def __init__(self, source, parent=None, *, dumb_sort=None):
|
||||
super().__init__(parent)
|
||||
super().setSourceModel(source)
|
||||
self.srcmodel = source
|
||||
self.pattern = ''
|
||||
if dumb_sort is None:
|
||||
# pylint: disable=invalid-name
|
||||
self.lessThan = self.intelligentLessThan
|
||||
self._sort_order = Qt.AscendingOrder
|
||||
else:
|
||||
self.setSortRole(completion.Role.sort)
|
||||
self._sort_order = dumb_sort
|
||||
|
||||
def set_pattern(self, val):
|
||||
"""Setter for pattern.
|
||||
@@ -57,14 +65,15 @@ class CompletionFilterModel(QSortFilterProxyModel):
|
||||
Args:
|
||||
val: The value to set.
|
||||
"""
|
||||
self.pattern = val
|
||||
self.invalidateFilter()
|
||||
sortcol = 0
|
||||
try:
|
||||
self.srcmodel.sort(sortcol)
|
||||
except NotImplementedError:
|
||||
self.sort(sortcol)
|
||||
self.invalidate()
|
||||
with debug.log_time(log.completion, 'Setting filter pattern'):
|
||||
self.pattern = val
|
||||
self.invalidateFilter()
|
||||
sortcol = 0
|
||||
try:
|
||||
self.srcmodel.sort(sortcol)
|
||||
except NotImplementedError:
|
||||
self.sort(sortcol)
|
||||
self.invalidate()
|
||||
|
||||
def count(self):
|
||||
"""Get the count of non-toplevel items currently visible.
|
||||
@@ -124,14 +133,18 @@ class CompletionFilterModel(QSortFilterProxyModel):
|
||||
if parent == QModelIndex():
|
||||
return True
|
||||
idx = self.srcmodel.index(row, 0, parent)
|
||||
qtutils.ensure_valid(idx)
|
||||
if not idx.isValid():
|
||||
# No entries in parent model
|
||||
return False
|
||||
data = self.srcmodel.data(idx)
|
||||
# TODO more sophisticated filtering
|
||||
if not self.pattern:
|
||||
return True
|
||||
if not data:
|
||||
return False
|
||||
return self.pattern.casefold() in data.casefold()
|
||||
|
||||
def lessThan(self, lindex, rindex):
|
||||
def intelligentLessThan(self, lindex, rindex):
|
||||
"""Custom sorting implementation.
|
||||
|
||||
Prefers all items which start with self.pattern. Other than that, uses
|
||||
@@ -167,3 +180,9 @@ class CompletionFilterModel(QSortFilterProxyModel):
|
||||
return False
|
||||
else:
|
||||
return left < right
|
||||
|
||||
def sort(self, column, order=None):
|
||||
"""Extend sort to respect self._sort_order if no order was given."""
|
||||
if order is None:
|
||||
order = self._sort_order
|
||||
super().sort(column, order)
|
||||
|
||||
128
qutebrowser/completion/models/urlmodel.py
Normal file
128
qutebrowser/completion/models/urlmodel.py
Normal file
@@ -0,0 +1,128 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
"""CompletionModels for URLs."""
|
||||
|
||||
import datetime
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot, Qt
|
||||
|
||||
from qutebrowser.utils import objreg, utils
|
||||
from qutebrowser.completion.models import base
|
||||
from qutebrowser.config import config
|
||||
|
||||
|
||||
class UrlCompletionModel(base.BaseCompletionModel):
|
||||
|
||||
"""A model which combines quickmarks and web history URLs.
|
||||
|
||||
Used for the `open` command."""
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self._quickmark_cat = self.new_category("Quickmarks")
|
||||
self._history_cat = self.new_category("History")
|
||||
|
||||
quickmark_manager = objreg.get('quickmark-manager')
|
||||
quickmarks = quickmark_manager.marks.items()
|
||||
for qm_name, qm_url in quickmarks:
|
||||
self._add_quickmark_entry(qm_name, qm_url)
|
||||
quickmark_manager.added.connect(self.on_quickmark_added)
|
||||
quickmark_manager.removed.connect(self.on_quickmark_removed)
|
||||
|
||||
self._history = objreg.get('web-history')
|
||||
max_history = config.get('completion', 'web-history-max-items')
|
||||
history = utils.newest_slice(self._history, max_history)
|
||||
for entry in history:
|
||||
self._add_history_entry(entry)
|
||||
self._history.add_completion_item.connect(
|
||||
self.on_history_item_added)
|
||||
|
||||
objreg.get('config').changed.connect(self.reformat_timestamps)
|
||||
|
||||
def _fmt_atime(self, atime):
|
||||
"""Format an atime to a human-readable string."""
|
||||
fmt = config.get('completion', 'timestamp-format')
|
||||
if fmt is None:
|
||||
return ''
|
||||
return datetime.datetime.fromtimestamp(atime).strftime(fmt)
|
||||
|
||||
def _add_history_entry(self, entry):
|
||||
"""Add a new history entry to the completion."""
|
||||
self.new_item(self._history_cat, entry.url.toDisplayString(), "",
|
||||
self._fmt_atime(entry.atime), sort=int(entry.atime),
|
||||
userdata=entry.url)
|
||||
|
||||
def _add_quickmark_entry(self, name, url):
|
||||
"""Add a new quickmark entry to the completion.
|
||||
|
||||
Args:
|
||||
name: The name of the new quickmark.
|
||||
url: The URL of the new quickmark.
|
||||
"""
|
||||
self.new_item(self._quickmark_cat, url, name)
|
||||
|
||||
@config.change_filter('completion', 'timestamp-format')
|
||||
def reformat_timestamps(self):
|
||||
"""Reformat the timestamps if the config option was changed."""
|
||||
for i in range(self._history_cat.rowCount()):
|
||||
name_item = self._history_cat.child(i, 0)
|
||||
atime_item = self._history_cat.child(i, 2)
|
||||
atime = name_item.data(base.Role.sort)
|
||||
atime_item.setText(self._fmt_atime(atime))
|
||||
|
||||
@pyqtSlot(object)
|
||||
def on_history_item_added(self, entry):
|
||||
"""Slot called when a new history item was added."""
|
||||
for i in range(self._history_cat.rowCount()):
|
||||
name_item = self._history_cat.child(i, 0)
|
||||
atime_item = self._history_cat.child(i, 2)
|
||||
url = name_item.data(base.Role.userdata)
|
||||
if url == entry.url:
|
||||
atime_item.setText(self._fmt_atime(entry.atime))
|
||||
name_item.setData(int(entry.atime), base.Role.sort)
|
||||
break
|
||||
else:
|
||||
self._add_history_entry(entry)
|
||||
|
||||
@pyqtSlot(str, str)
|
||||
def on_quickmark_added(self, name, url):
|
||||
"""Called when a quickmark has been added by the user.
|
||||
|
||||
Args:
|
||||
name: The name of the new quickmark.
|
||||
url: The url of the new quickmark, as string.
|
||||
"""
|
||||
self._add_quickmark_entry(name, url)
|
||||
|
||||
@pyqtSlot(str)
|
||||
def on_quickmark_removed(self, name):
|
||||
"""Called when a quickmark has been removed by the user.
|
||||
|
||||
Args:
|
||||
name: The name of the quickmark which has been removed.
|
||||
"""
|
||||
for i in range(self._quickmark_cat.rowCount()):
|
||||
name_item = self._quickmark_cat.child(i, 1)
|
||||
if name_item.data(Qt.DisplayRole) == name:
|
||||
self._quickmark_cat.removeRow(i)
|
||||
break
|
||||
@@ -1,6 +1,6 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
@@ -20,8 +20,8 @@
|
||||
"""Configuration storage and config-related utilities.
|
||||
|
||||
This borrows a lot of ideas from configparser, but also has some things that
|
||||
are fundamentally different. This is why nothing inherts from configparser, but
|
||||
we borrow some methods and classes from there where it makes sense.
|
||||
are fundamentally different. This is why nothing inherits from configparser,
|
||||
but we borrow some methods and classes from there where it makes sense.
|
||||
"""
|
||||
|
||||
import os
|
||||
@@ -32,19 +32,19 @@ import configparser
|
||||
import collections
|
||||
import collections.abc
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QStandardPaths, QUrl
|
||||
from PyQt5.QtWidgets import QMessageBox
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QUrl, QSettings
|
||||
|
||||
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
|
||||
|
||||
|
||||
class change_filter: # pylint: disable=invalid-name
|
||||
|
||||
"""Decorator to register a new command handler.
|
||||
"""Decorator to filter calls based on a config section/option matching.
|
||||
|
||||
This could also be a function, but as a class (with a "wrong" name) it's
|
||||
much cleaner to implement.
|
||||
@@ -52,15 +52,18 @@ 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.
|
||||
|
||||
Args:
|
||||
See class attributes.
|
||||
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)
|
||||
@@ -68,9 +71,10 @@ 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):
|
||||
"""Register the command before running the function.
|
||||
"""Filter calls to the decorated function.
|
||||
|
||||
Gets called when a function should be decorated.
|
||||
|
||||
@@ -85,20 +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
|
||||
|
||||
@@ -113,17 +131,17 @@ def section(sect):
|
||||
return objreg.get('config')[sect]
|
||||
|
||||
|
||||
def init(args):
|
||||
"""Initialize the config.
|
||||
def _init_main_config(parent=None):
|
||||
"""Initialize the main config.
|
||||
|
||||
Args:
|
||||
args: The argparse namespace.
|
||||
parent: The parent to pass to ConfigManager.
|
||||
"""
|
||||
confdir = standarddir.get(QStandardPaths.ConfigLocation, args)
|
||||
args = objreg.get('args')
|
||||
try:
|
||||
app = objreg.get('app')
|
||||
config_obj = ConfigManager(confdir, 'qutebrowser.conf', app)
|
||||
except (configexc.Error, configparser.Error) as e:
|
||||
config_obj = ConfigManager(standarddir.config(), 'qutebrowser.conf',
|
||||
args.relaxed_config, parent=parent)
|
||||
except (configexc.Error, configparser.Error, UnicodeDecodeError) as e:
|
||||
log.init.exception(e)
|
||||
errstr = "Error while reading config:"
|
||||
try:
|
||||
@@ -131,38 +149,126 @@ def init(args):
|
||||
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:
|
||||
filename = os.path.join(standarddir.config(), 'qutebrowser.conf')
|
||||
save_manager = objreg.get('save-manager')
|
||||
save_manager.add_saveable(
|
||||
'config', config_obj.save, config_obj.changed,
|
||||
config_opt=('general', 'auto-save-config'), filename=filename)
|
||||
for sect in config_obj.sections.values():
|
||||
for opt in sect.values.values():
|
||||
if opt.values['conf'] is None:
|
||||
# Option added to built-in defaults but not in user's
|
||||
# config yet
|
||||
save_manager.save('config', explicit=True, force=True)
|
||||
return
|
||||
|
||||
|
||||
def _init_key_config(parent):
|
||||
"""Initialize the key config.
|
||||
|
||||
Args:
|
||||
parent: The parent to use for the KeyConfigParser.
|
||||
"""
|
||||
args = objreg.get('args')
|
||||
try:
|
||||
key_config = keyconf.KeyConfigParser(confdir, 'keys.conf')
|
||||
except keyconf.KeyConfigError as e:
|
||||
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:
|
||||
save_manager = objreg.get('save-manager')
|
||||
filename = os.path.join(standarddir.config(), 'keys.conf')
|
||||
save_manager.add_saveable(
|
||||
'key-config', key_config.save, key_config.config_dirty,
|
||||
config_opt=('general', 'auto-save-config'), filename=filename,
|
||||
dirty=key_config.is_dirty)
|
||||
|
||||
datadir = standarddir.get(QStandardPaths.DataLocation, args)
|
||||
state_config = ini.ReadWriteConfigParser(datadir, 'state')
|
||||
|
||||
def _init_misc():
|
||||
"""Initialize misc. config-related files."""
|
||||
save_manager = objreg.get('save-manager')
|
||||
state_config = ini.ReadWriteConfigParser(standarddir.data(), 'state')
|
||||
for sect in ('general', 'geometry'):
|
||||
try:
|
||||
state_config.add_section(sect)
|
||||
except configparser.DuplicateSectionError:
|
||||
pass
|
||||
# See commit a98060e020a4ba83b663813a4b9404edb47f28ad.
|
||||
state_config['general'].pop('fooled', None)
|
||||
objreg.register('state-config', state_config)
|
||||
save_manager.add_saveable('state-config', state_config.save)
|
||||
|
||||
# We need to import this here because lineparser needs config.
|
||||
from qutebrowser.config.parsers import line
|
||||
command_history = line.LineConfigParser(datadir, 'cmd-history',
|
||||
('completion', 'history-length'))
|
||||
from qutebrowser.misc import lineparser
|
||||
command_history = lineparser.LimitLineParser(
|
||||
standarddir.data(), 'cmd-history',
|
||||
limit=('completion', 'cmd-history-max-items'),
|
||||
parent=objreg.get('config'))
|
||||
objreg.register('command-history', command_history)
|
||||
save_manager.add_saveable('command-history', command_history.save,
|
||||
command_history.changed)
|
||||
|
||||
# Set the QSettings path to something like
|
||||
# ~/.config/qutebrowser/qsettings/qutebrowser/qutebrowser.conf so it
|
||||
# doesn't overwrite our config.
|
||||
#
|
||||
# This fixes one of the corruption issues here:
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/515
|
||||
|
||||
if standarddir.config() is None:
|
||||
path = os.devnull
|
||||
else:
|
||||
path = os.path.join(standarddir.config(), 'qsettings')
|
||||
for fmt in (QSettings.NativeFormat, QSettings.IniFormat):
|
||||
QSettings.setPath(fmt, QSettings.UserScope, path)
|
||||
|
||||
|
||||
def init(parent=None):
|
||||
"""Initialize the config.
|
||||
|
||||
Args:
|
||||
parent: The parent to pass to QObjects which get initialized.
|
||||
"""
|
||||
_init_main_config(parent)
|
||||
_init_key_config(parent)
|
||||
_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):
|
||||
@@ -176,6 +282,11 @@ 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:
|
||||
sections: The configuration data as an OrderedDict.
|
||||
@@ -208,16 +319,27 @@ class ConfigManager(QObject):
|
||||
('colors', 'tab.indicator.stop'): 'tabs.indicator.stop',
|
||||
('colors', 'tab.indicator.error'): 'tabs.indicator.error',
|
||||
('colors', 'tab.indicator.system'): 'tabs.indicator.system',
|
||||
('colors', 'tab.seperator'): 'tabs.seperator',
|
||||
('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)
|
||||
|
||||
def __init__(self, configdir, fname, parent=None):
|
||||
def __init__(self, configdir, fname, relaxed=False, parent=None):
|
||||
super().__init__(parent)
|
||||
self._initialized = False
|
||||
self.sections = configdata.DATA
|
||||
self.sections = configdata.data()
|
||||
self._interpolation = configparser.ExtendedInterpolation()
|
||||
self._proxies = {}
|
||||
for sectname in self.sections.keys():
|
||||
@@ -229,7 +351,7 @@ class ConfigManager(QObject):
|
||||
else:
|
||||
self._configdir = configdir
|
||||
parser = ini.ReadConfigParser(configdir, fname)
|
||||
self._from_cp(parser)
|
||||
self._from_cp(parser, relaxed)
|
||||
self._initialized = True
|
||||
self._validate_all()
|
||||
|
||||
@@ -279,7 +401,7 @@ class ConfigManager(QObject):
|
||||
try:
|
||||
desc = self.sections[sectname].descriptions[optname]
|
||||
except KeyError:
|
||||
log.misc.exception("No description for {}.{}!".format(
|
||||
log.config.exception("No description for {}.{}!".format(
|
||||
sectname, optname))
|
||||
continue
|
||||
for descline in desc.splitlines():
|
||||
@@ -338,27 +460,52 @@ class ConfigManager(QObject):
|
||||
else:
|
||||
return None
|
||||
|
||||
def _from_cp(self, cp):
|
||||
def _from_cp(self, cp, relaxed=False):
|
||||
"""Read the config from a configparser instance.
|
||||
|
||||
Args:
|
||||
cp: The configparser instance to read the values from.
|
||||
relaxed: Whether to ignore inexistent sections/options.
|
||||
"""
|
||||
for sectname in cp:
|
||||
if sectname in self.RENAMED_SECTIONS:
|
||||
sectname = self.RENAMED_SECTIONS[sectname]
|
||||
if sectname is not 'DEFAULT' and sectname not in self.sections:
|
||||
raise configexc.NoSectionError(sectname)
|
||||
if not relaxed:
|
||||
raise configexc.NoSectionError(sectname)
|
||||
for sectname in self.sections:
|
||||
real_sectname = self._get_real_sectname(cp, sectname)
|
||||
if real_sectname is None:
|
||||
continue
|
||||
for k, v in cp[real_sectname].items():
|
||||
if k.startswith(self.ESCAPE_CHAR):
|
||||
k = k[1:]
|
||||
if (sectname, k) in self.RENAMED_OPTIONS:
|
||||
k = self.RENAMED_OPTIONS[sectname, k]
|
||||
self._from_cp_section(sectname, cp, relaxed)
|
||||
|
||||
def _from_cp_section(self, sectname, cp, relaxed):
|
||||
"""Read a single section from a configparser instance.
|
||||
|
||||
Args:
|
||||
sectname: The name of the section to read.
|
||||
cp: The configparser instance to read the values from.
|
||||
relaxed: Whether to ignore inexistent options.
|
||||
"""
|
||||
real_sectname = self._get_real_sectname(cp, sectname)
|
||||
if real_sectname is None:
|
||||
return
|
||||
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
|
||||
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:
|
||||
if relaxed:
|
||||
pass
|
||||
else:
|
||||
raise
|
||||
|
||||
def _validate_all(self):
|
||||
"""Validate all values set in self._from_cp."""
|
||||
@@ -376,7 +523,7 @@ class ConfigManager(QObject):
|
||||
|
||||
def _changed(self, sectname, optname):
|
||||
"""Notify other objects the config has changed."""
|
||||
log.misc.debug("Config option changed: {} -> {}".format(
|
||||
log.config.debug("Config option changed: {} -> {}".format(
|
||||
sectname, optname))
|
||||
if sectname in ('colors', 'fonts'):
|
||||
self.style_changed.emit(sectname, optname)
|
||||
@@ -400,7 +547,7 @@ class ConfigManager(QObject):
|
||||
def items(self, sectname, raw=True):
|
||||
"""Get a list of (optname, value) tuples for a section.
|
||||
|
||||
Implemented for configparser interpolation compatbility.
|
||||
Implemented for configparser interpolation compatibility
|
||||
|
||||
Args:
|
||||
sectname: The name of the section to get.
|
||||
@@ -463,7 +610,7 @@ class ConfigManager(QObject):
|
||||
The value of the option.
|
||||
"""
|
||||
if not self._initialized:
|
||||
raise Exception("get got called before initialisation was "
|
||||
raise Exception("get got called before initialization was "
|
||||
"complete!")
|
||||
try:
|
||||
sect = self.sections[sectname]
|
||||
@@ -482,50 +629,66 @@ 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):
|
||||
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
|
||||
instead.
|
||||
|
||||
If the option name ends with '!' and it is a boolean value, toggle it.
|
||||
|
||||
//
|
||||
|
||||
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_: 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
|
||||
try:
|
||||
if optname.endswith('?'):
|
||||
val = self.get(sectname, optname[:-1], transformed=False)
|
||||
message.info(win_id, "{} {} = {}".format(
|
||||
sectname, optname[:-1], val), immediately=True)
|
||||
else:
|
||||
if value is None:
|
||||
|
||||
if option.endswith('?'):
|
||||
option = option[:-1]
|
||||
print_ = True
|
||||
else:
|
||||
try:
|
||||
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, 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, section_, option, value)
|
||||
else:
|
||||
raise cmdexc.CommandError("set: The following arguments "
|
||||
"are required: value")
|
||||
layer = 'temp' if temp else 'conf'
|
||||
self.set(layer, sectname, optname, value)
|
||||
except (configexc.Error, configparser.Error) as e:
|
||||
raise cmdexc.CommandError("set: {} - {}".format(
|
||||
e.__class__.__name__, e))
|
||||
except (configexc.Error, configparser.Error) as e:
|
||||
raise cmdexc.CommandError("set: {} - {}".format(
|
||||
e.__class__.__name__, e))
|
||||
|
||||
if print_:
|
||||
val = self.get(section_, option, transformed=False)
|
||||
message.info(win_id, "{} {} = {}".format(
|
||||
section_, option, val), immediately=True)
|
||||
|
||||
def set(self, layer, sectname, optname, value, validate=True):
|
||||
"""Set an option.
|
||||
@@ -560,14 +723,6 @@ class ConfigManager(QObject):
|
||||
if self._initialized:
|
||||
self._after_set(sectname, optname)
|
||||
|
||||
@cmdutils.register(instance='config', name='save')
|
||||
def save_command(self):
|
||||
"""Save the config file."""
|
||||
try:
|
||||
self.save()
|
||||
except OSError as e:
|
||||
raise cmdexc.CommandError("Could not save config: {}".format(e))
|
||||
|
||||
def save(self):
|
||||
"""Save the config file."""
|
||||
if self._configdir is None:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
@@ -32,9 +32,9 @@ class ValidationError(Error):
|
||||
"""Raised when a value for a config type was invalid.
|
||||
|
||||
Attributes:
|
||||
section: Section in which the error occured (added when catching and
|
||||
section: Section in which the error occurred (added when catching and
|
||||
re-raising the exception).
|
||||
option: Option in which the error occured.
|
||||
option: Option in which the error occurred.
|
||||
"""
|
||||
|
||||
def __init__(self, value, msg):
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
@@ -25,6 +25,7 @@ import base64
|
||||
import codecs
|
||||
import os.path
|
||||
import sre_constants
|
||||
import itertools
|
||||
|
||||
from PyQt5.QtCore import QUrl
|
||||
from PyQt5.QtGui import QColor, QFont
|
||||
@@ -33,10 +34,15 @@ 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
|
||||
|
||||
# Taken from configparser
|
||||
BOOLEAN_STATES = {'1': True, 'yes': True, 'true': True, 'on': True,
|
||||
'0': False, 'no': False, 'false': False, 'off': False}
|
||||
|
||||
|
||||
class ValidValues:
|
||||
|
||||
@@ -220,25 +226,17 @@ class List(BaseType):
|
||||
|
||||
class Bool(BaseType):
|
||||
|
||||
"""Base class for a boolean setting.
|
||||
|
||||
Class attributes:
|
||||
_BOOLEAN_STATES: A dictionary of strings mapped to their bool meanings.
|
||||
"""
|
||||
"""Base class for a boolean setting."""
|
||||
|
||||
typestr = 'bool'
|
||||
|
||||
# Taken from configparser
|
||||
_BOOLEAN_STATES = {'1': True, 'yes': True, 'true': True, 'on': True,
|
||||
'0': False, 'no': False, 'false': False, 'off': False}
|
||||
|
||||
valid_values = ValidValues('true', 'false')
|
||||
|
||||
def transform(self, value):
|
||||
if not value:
|
||||
return None
|
||||
else:
|
||||
return Bool._BOOLEAN_STATES[value.lower()]
|
||||
return BOOLEAN_STATES[value.lower()]
|
||||
|
||||
def validate(self, value):
|
||||
if not value:
|
||||
@@ -246,7 +244,7 @@ class Bool(BaseType):
|
||||
return
|
||||
else:
|
||||
raise configexc.ValidationError(value, "may not be empty!")
|
||||
if value.lower() not in Bool._BOOLEAN_STATES:
|
||||
if value.lower() not in BOOLEAN_STATES:
|
||||
raise configexc.ValidationError(value, "must be a boolean!")
|
||||
|
||||
|
||||
@@ -661,9 +659,9 @@ class Font(BaseType):
|
||||
) |
|
||||
# size (<float>pt | <int>px)
|
||||
(?P<size>[0-9]+((\.[0-9]+)?[pP][tT]|[pP][xX]))
|
||||
)\ # size/weight/style are space-separated
|
||||
)* # 0-inf size/weight/style tags
|
||||
(?P<family>[A-Za-z, "-]*)$ # mandatory font family""", re.VERBOSE)
|
||||
)\ # size/weight/style are space-separated
|
||||
)* # 0-inf size/weight/style tags
|
||||
(?P<family>[A-Za-z0-9, "-]*)$ # mandatory font family""", re.VERBOSE)
|
||||
|
||||
def validate(self, value):
|
||||
if not value:
|
||||
@@ -675,9 +673,28 @@ class Font(BaseType):
|
||||
raise configexc.ValidationError(value, "must be a valid font")
|
||||
|
||||
|
||||
class FontFamily(Font):
|
||||
|
||||
"""A Qt font family."""
|
||||
|
||||
def validate(self, value):
|
||||
if not value:
|
||||
if self._none_ok:
|
||||
return
|
||||
else:
|
||||
raise configexc.ValidationError(value, "may not be empty!")
|
||||
match = self.font_regex.match(value)
|
||||
if not match:
|
||||
raise configexc.ValidationError(value, "must be a valid font")
|
||||
for group in 'style', 'weight', 'namedweight', 'size':
|
||||
if match.group(group):
|
||||
raise configexc.ValidationError(value, "may not include a "
|
||||
"{}!".format(group))
|
||||
|
||||
|
||||
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:
|
||||
@@ -782,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:
|
||||
@@ -789,15 +817,25 @@ class File(BaseType):
|
||||
else:
|
||||
raise configexc.ValidationError(value, "may not be empty!")
|
||||
value = os.path.expanduser(value)
|
||||
if not os.path.isfile(value):
|
||||
raise configexc.ValidationError(value, "must be a valid file!")
|
||||
if not os.path.isabs(value):
|
||||
raise configexc.ValidationError(value, "must be an absolute path!")
|
||||
|
||||
def transform(self, value):
|
||||
if not value:
|
||||
return None
|
||||
return os.path.expanduser(value)
|
||||
value = os.path.expandvars(value)
|
||||
try:
|
||||
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 a valid file!")
|
||||
except UnicodeEncodeError as e:
|
||||
raise configexc.ValidationError(value, e)
|
||||
|
||||
|
||||
class Directory(BaseType):
|
||||
@@ -812,19 +850,51 @@ class Directory(BaseType):
|
||||
return
|
||||
else:
|
||||
raise configexc.ValidationError(value, "may not be empty!")
|
||||
value = os.path.expandvars(value)
|
||||
value = os.path.expanduser(value)
|
||||
if not os.path.isdir(value):
|
||||
raise configexc.ValidationError(value, "must be a valid "
|
||||
"directory!")
|
||||
if not os.path.isabs(value):
|
||||
raise configexc.ValidationError(value, "must be an absolute path!")
|
||||
try:
|
||||
if not os.path.isdir(value):
|
||||
raise configexc.ValidationError(
|
||||
value, "must be a valid directory!")
|
||||
if not os.path.isabs(value):
|
||||
raise configexc.ValidationError(
|
||||
value, "must be an absolute path!")
|
||||
except UnicodeEncodeError as e:
|
||||
raise configexc.ValidationError(value, e)
|
||||
|
||||
def transform(self, value):
|
||||
if not value:
|
||||
return None
|
||||
value = os.path.expandvars(value)
|
||||
return os.path.expanduser(value)
|
||||
|
||||
|
||||
class FormatString(BaseType):
|
||||
|
||||
"""A string with '{foo}'-placeholders."""
|
||||
|
||||
typestr = 'format-string'
|
||||
|
||||
def __init__(self, fields, none_ok=False):
|
||||
super().__init__(none_ok)
|
||||
self.fields = fields
|
||||
|
||||
def validate(self, value):
|
||||
if not value:
|
||||
if self._none_ok:
|
||||
return
|
||||
else:
|
||||
raise configexc.ValidationError(value, "may not be empty!")
|
||||
s = self.transform(value)
|
||||
try:
|
||||
return s.format(**{k: '' for k in self.fields})
|
||||
except KeyError as e:
|
||||
raise configexc.ValidationError(value, "Invalid placeholder "
|
||||
"{}".format(e))
|
||||
except ValueError as e:
|
||||
raise configexc.ValidationError(value, str(e))
|
||||
|
||||
|
||||
class WebKitBytes(BaseType):
|
||||
|
||||
"""A size with an optional suffix.
|
||||
@@ -937,13 +1007,13 @@ class ShellCommand(BaseType):
|
||||
return
|
||||
else:
|
||||
raise configexc.ValidationError(value, "may not be empty!")
|
||||
if self.placeholder and '{}' not in self.transform(value):
|
||||
raise configexc.ValidationError(value, "needs to contain a "
|
||||
"{}-placeholder.")
|
||||
try:
|
||||
shlex.split(value)
|
||||
except ValueError as e:
|
||||
raise configexc.ValidationError(value, str(e))
|
||||
if self.placeholder and '{}' not in self.transform(value):
|
||||
raise configexc.ValidationError(value, "needs to contain a "
|
||||
"{}-placeholder.")
|
||||
|
||||
def transform(self, value):
|
||||
if not value:
|
||||
@@ -1040,14 +1110,45 @@ 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(
|
||||
url.errorString()))
|
||||
|
||||
|
||||
class FuzzyUrl(BaseType):
|
||||
|
||||
"""A single URL."""
|
||||
|
||||
def validate(self, value):
|
||||
from qutebrowser.utils import urlutils
|
||||
if not value:
|
||||
if self._none_ok:
|
||||
return
|
||||
else:
|
||||
raise configexc.ValidationError(value, "may not be empty!")
|
||||
try:
|
||||
self.transform(value)
|
||||
except urlutils.FuzzyUrlError as e:
|
||||
raise configexc.ValidationError(value, str(e))
|
||||
|
||||
def transform(self, value):
|
||||
from qutebrowser.utils import urlutils
|
||||
if not value:
|
||||
return None
|
||||
else:
|
||||
return urlutils.fuzzy_url(value, do_search=False)
|
||||
|
||||
|
||||
class Encoding(BaseType):
|
||||
|
||||
"""Setting for a python encoding."""
|
||||
@@ -1075,35 +1176,36 @@ 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:
|
||||
return
|
||||
else:
|
||||
raise configexc.ValidationError(value, "may not be empty!")
|
||||
value = os.path.expandvars(value)
|
||||
value = os.path.expanduser(value)
|
||||
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:
|
||||
value.encode('utf-8')
|
||||
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!")
|
||||
|
||||
def transform(self, value):
|
||||
path = os.path.expanduser(value)
|
||||
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))
|
||||
|
||||
|
||||
class AutoSearch(BaseType):
|
||||
@@ -1155,6 +1257,13 @@ class Position(BaseType):
|
||||
return self.MAPPING[value]
|
||||
|
||||
|
||||
class VerticalPosition(BaseType):
|
||||
|
||||
"""The position of the download bar."""
|
||||
|
||||
valid_values = ValidValues('north', 'south')
|
||||
|
||||
|
||||
class UrlList(List):
|
||||
|
||||
"""A list of URLs."""
|
||||
@@ -1185,6 +1294,22 @@ class UrlList(List):
|
||||
"{}".format(val.errorString()))
|
||||
|
||||
|
||||
class SessionName(BaseType):
|
||||
|
||||
"""The name of a session."""
|
||||
|
||||
typestr = 'session'
|
||||
|
||||
def validate(self, value):
|
||||
if not value:
|
||||
if self._none_ok:
|
||||
return
|
||||
else:
|
||||
raise configexc.ValidationError(value, "may not be empty!")
|
||||
if value.startswith('_'):
|
||||
raise configexc.ValidationError(value, "may not start with '_'!")
|
||||
|
||||
|
||||
class SelectOnRemove(BaseType):
|
||||
|
||||
"""Which tab to select when the focused tab is removed."""
|
||||
@@ -1208,29 +1333,77 @@ 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."),
|
||||
('startpage', "Load the start page."),
|
||||
('default-page', "Load the default page."),
|
||||
('close', "Close the window."))
|
||||
|
||||
|
||||
class AcceptCookies(BaseType):
|
||||
|
||||
"""Whether to accept a cookie."""
|
||||
"""Control which cookies to accept."""
|
||||
|
||||
valid_values = ValidValues(('default', "Default QtWebKit behaviour."),
|
||||
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."))
|
||||
|
||||
|
||||
class ConfirmQuit(BaseType):
|
||||
class ConfirmQuit(List):
|
||||
|
||||
"""Whether to display a confirmation when the window is closed."""
|
||||
|
||||
typestr = 'string-list'
|
||||
|
||||
valid_values = ValidValues(('always', "Always show a confirmation."),
|
||||
('multiple-tabs', "Show a confirmation if "
|
||||
"multiple tabs are opened."),
|
||||
('downloads', "Show a confirmation if "
|
||||
"downloads are running"),
|
||||
('never', "Never show a confirmation."))
|
||||
# Values that can be combined with commas
|
||||
combinable_values = ('multiple-tabs', 'downloads')
|
||||
|
||||
def validate(self, value):
|
||||
values = self.transform(value)
|
||||
# Never can't be set with other options
|
||||
if 'never' in values and len(values) > 1:
|
||||
raise configexc.ValidationError(
|
||||
value, "List cannot contain never!")
|
||||
# Always can't be set with other options
|
||||
elif 'always' in values and len(values) > 1:
|
||||
raise configexc.ValidationError(
|
||||
value, "List cannot contain always!")
|
||||
# Values have to be valid
|
||||
elif not set(values).issubset(set(self.valid_values.values)):
|
||||
raise configexc.ValidationError(
|
||||
value, "List contains invalid values!")
|
||||
# List can't have duplicates
|
||||
elif len(set(values)) != len(values):
|
||||
raise configexc.ValidationError(
|
||||
value, "List contains duplicate values!")
|
||||
|
||||
def complete(self):
|
||||
combinations = []
|
||||
# Generate combinations of the options that can be combined
|
||||
for size in range(2, len(self.combinable_values) + 1):
|
||||
combinations += list(
|
||||
itertools.combinations(self.combinable_values, size))
|
||||
out = []
|
||||
# Add valid single values
|
||||
for val in self.valid_values:
|
||||
out.append((val, self.valid_values.descriptions[val]))
|
||||
# Add combinations to list of options
|
||||
for val in combinations:
|
||||
desc = ''
|
||||
out.append((','.join(val), desc))
|
||||
return out
|
||||
|
||||
|
||||
class ForwardUnboundKeys(BaseType):
|
||||
@@ -1289,8 +1462,95 @@ 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 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 the "
|
||||
"window."),
|
||||
('window', "Open in a new window."))
|
||||
|
||||
|
||||
class DownloadPathSuggestion(BaseType):
|
||||
|
||||
"""How to format the question when downloading."""
|
||||
|
||||
valid_values = ValidValues(('path', "Show only the download path."),
|
||||
('filename', "Show only download filename."),
|
||||
('both', "Show download path and filename."))
|
||||
|
||||
|
||||
class UserAgent(BaseType):
|
||||
|
||||
"""The user agent to use."""
|
||||
|
||||
typestr = 'user-agent'
|
||||
|
||||
def __init__(self, none_ok=False):
|
||||
super().__init__(none_ok)
|
||||
|
||||
def validate(self, value):
|
||||
if not value:
|
||||
if self._none_ok:
|
||||
return
|
||||
else:
|
||||
raise configexc.ValidationError(value, "may not be empty!")
|
||||
|
||||
def complete(self):
|
||||
"""Complete a list of common user agents."""
|
||||
out = [
|
||||
('Mozilla/5.0 (Windows NT 6.1; WOW64; rv:35.0) Gecko/20100101 '
|
||||
'Firefox/35.0',
|
||||
"Firefox 35.0 Win7 64-bit"),
|
||||
('Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:35.0) Gecko/20100101 '
|
||||
'Firefox/35.0',
|
||||
"Firefox 35.0 Ubuntu"),
|
||||
('Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:35.0) '
|
||||
'Gecko/20100101 Firefox/35.0',
|
||||
"Firefox 35.0 MacOSX"),
|
||||
|
||||
('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) '
|
||||
'AppleWebKit/600.3.18 (KHTML, like Gecko) Version/8.0.3 '
|
||||
'Safari/600.3.18',
|
||||
"Safari 8.0 MacOSX"),
|
||||
|
||||
('Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, '
|
||||
'like Gecko) Chrome/40.0.2214.111 Safari/537.36',
|
||||
"Chrome 40.0 Win7 64-bit"),
|
||||
('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) '
|
||||
'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.111 '
|
||||
'Safari/537.36',
|
||||
"Chrome 40.0 MacOSX"),
|
||||
('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 '
|
||||
'(KHTML, like Gecko) Chrome/40.0.2214.111 Safari/537.36',
|
||||
"Chrome 40.0 Linux"),
|
||||
|
||||
('Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like '
|
||||
'Gecko',
|
||||
"IE 11.0 Win7 64-bit"),
|
||||
|
||||
('Mozilla/5.0 (iPhone; CPU iPhone OS 8_1_2 like Mac OS X) '
|
||||
'AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 '
|
||||
'Mobile/12B440 Safari/600.1.4',
|
||||
"Mobile Safari 8.0 iOS"),
|
||||
('Mozilla/5.0 (Android; Mobile; rv:35.0) Gecko/35.0 Firefox/35.0',
|
||||
"Firefox 35, Android"),
|
||||
('Mozilla/5.0 (Linux; Android 5.0.2; One Build/KTU84L.H4) '
|
||||
'AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 '
|
||||
'Chrome/37.0.0.0 Mobile Safari/537.36',
|
||||
"Android Browser"),
|
||||
|
||||
('Mozilla/5.0 (compatible; Googlebot/2.1; '
|
||||
'+http://www.google.com/bot.html',
|
||||
"Google Bot"),
|
||||
('Wget/1.16.1 (linux-gnu)',
|
||||
"wget 1.16.1"),
|
||||
('curl/7.40.0',
|
||||
"curl 7.40.0")
|
||||
]
|
||||
return out
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
@@ -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,
|
||||
@@ -60,10 +64,12 @@ class ReadConfigParser(configparser.ConfigParser):
|
||||
|
||||
class ReadWriteConfigParser(ReadConfigParser):
|
||||
|
||||
"""ConfigParser subclass used for auxillary config files."""
|
||||
"""ConfigParser subclass used for auxiliary config files."""
|
||||
|
||||
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))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
import collections
|
||||
import os.path
|
||||
import itertools
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, QObject
|
||||
|
||||
@@ -34,7 +35,7 @@ class KeyConfigError(Exception):
|
||||
"""Raised on errors with the key config.
|
||||
|
||||
Attributes:
|
||||
lineno: The config line in which the exception occured.
|
||||
lineno: The config line in which the exception occurred.
|
||||
"""
|
||||
|
||||
def __init__(self, msg=None):
|
||||
@@ -42,6 +43,15 @@ class KeyConfigError(Exception):
|
||||
self.lineno = None
|
||||
|
||||
|
||||
class DuplicateKeychainError(KeyConfigError):
|
||||
|
||||
"""Error raised when there's a duplicate key binding."""
|
||||
|
||||
def __init__(self, keychain):
|
||||
super().__init__("Duplicate key chain {}!".format(keychain))
|
||||
self.keychain = keychain
|
||||
|
||||
|
||||
class KeyConfigParser(QObject):
|
||||
|
||||
"""Parser for the keybind config.
|
||||
@@ -50,25 +60,34 @@ class KeyConfigParser(QObject):
|
||||
_configfile: The filename of the config or None.
|
||||
_cur_section: The section currently being processed by _read().
|
||||
_cur_command: The command currently being processed by _read().
|
||||
is_dirty: Whether the config is currently dirty.
|
||||
|
||||
Class attributes:
|
||||
UNBOUND_COMMAND: The special command used for unbound keybindings.
|
||||
|
||||
Signals:
|
||||
changed: Emitted when the config has changed.
|
||||
changed: Emitted when the internal data has changed.
|
||||
arg: Name of the mode which was changed.
|
||||
config_dirty: Emitted when the config should be re-saved.
|
||||
"""
|
||||
|
||||
changed = pyqtSignal(str)
|
||||
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
|
||||
self._cur_section = None
|
||||
self._cur_command = None
|
||||
# Mapping of section name(s) to keybinding -> command dicts.
|
||||
# Mapping of section name(s) to key binding -> command dicts.
|
||||
self.keybindings = collections.OrderedDict()
|
||||
if configdir is None:
|
||||
self._configfile = None
|
||||
@@ -77,7 +96,8 @@ 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))
|
||||
|
||||
def __str__(self):
|
||||
@@ -130,8 +150,8 @@ class KeyConfigParser(QObject):
|
||||
data = str(self)
|
||||
f.write(data)
|
||||
|
||||
@cmdutils.register(instance='key-config', maxsplit=1)
|
||||
def bind(self, key, command, *, mode=None):
|
||||
@cmdutils.register(instance='key-config', maxsplit=1, no_cmd_split=True)
|
||||
def bind(self, key, command, *, mode=None, force=False):
|
||||
"""Bind a key to a command.
|
||||
|
||||
Args:
|
||||
@@ -139,6 +159,7 @@ class KeyConfigParser(QObject):
|
||||
command: The command to execute, with optional args.
|
||||
mode: A comma-separated list of modes to bind the key in
|
||||
(default: `normal`).
|
||||
force: Rebind the key if it is already bound.
|
||||
"""
|
||||
if mode is None:
|
||||
mode = 'normal'
|
||||
@@ -146,16 +167,20 @@ class KeyConfigParser(QObject):
|
||||
for m in mode.split(','):
|
||||
if m not in configdata.KEY_DATA:
|
||||
raise cmdexc.CommandError("Invalid mode {}!".format(m))
|
||||
split_cmd = command.split()
|
||||
if split_cmd[0] not in cmdutils.cmd_dict:
|
||||
raise cmdexc.CommandError("Invalid command {}!".format(
|
||||
split_cmd[0]))
|
||||
try:
|
||||
self._add_binding(mode, key, command)
|
||||
self._validate_command(command)
|
||||
except KeyConfigError as e:
|
||||
raise cmdexc.CommandError(str(e))
|
||||
try:
|
||||
self._add_binding(mode, key, command, force=force)
|
||||
except DuplicateKeychainError as e:
|
||||
raise cmdexc.CommandError("Duplicate keychain {} - use --force to "
|
||||
"override!".format(str(e.keychain)))
|
||||
except KeyConfigError as e:
|
||||
raise cmdexc.CommandError(e)
|
||||
for m in mode.split(','):
|
||||
self.changed.emit(m)
|
||||
self._mark_config_dirty()
|
||||
|
||||
@cmdutils.register(instance='key-config')
|
||||
def unbind(self, key, mode=None):
|
||||
@@ -183,8 +208,15 @@ class KeyConfigParser(QObject):
|
||||
raise cmdexc.CommandError("Can't find binding '{}' in section "
|
||||
"'{}'!".format(key, mode))
|
||||
else:
|
||||
if key in itertools.chain.from_iterable(
|
||||
configdata.KEY_DATA[mode].values()):
|
||||
try:
|
||||
self._add_binding(mode, key, self.UNBOUND_COMMAND)
|
||||
except DuplicateKeychainError:
|
||||
pass
|
||||
for m in mode.split(','):
|
||||
self.changed.emit(m)
|
||||
self._mark_config_dirty()
|
||||
|
||||
def _normalize_sectname(self, s):
|
||||
"""Normalize a section string like 'foo, bar,baz' to 'bar,baz,foo'."""
|
||||
@@ -198,20 +230,60 @@ class KeyConfigParser(QObject):
|
||||
sections = '!' + sections
|
||||
return sections
|
||||
|
||||
def _load_default(self):
|
||||
"""Load the built-in default keybindings."""
|
||||
def _load_default(self, *, only_new=False):
|
||||
"""Load the built-in default key bindings.
|
||||
|
||||
Args:
|
||||
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:
|
||||
self.keybindings[sectname] = collections.OrderedDict()
|
||||
if not only_new:
|
||||
self.keybindings[sectname] = collections.OrderedDict()
|
||||
else:
|
||||
for command, keychains in sect.items():
|
||||
for e in keychains:
|
||||
self._add_binding(sectname, e, command)
|
||||
for keychain, command in sect.items():
|
||||
self._add_binding(sectname, keychain, command)
|
||||
self.changed.emit(sectname)
|
||||
|
||||
def _read(self):
|
||||
"""Read the config file from disk and parse it."""
|
||||
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.
|
||||
"""
|
||||
try:
|
||||
bindings = self.keybindings[sectname]
|
||||
except KeyError:
|
||||
return True
|
||||
if keychain in bindings:
|
||||
return False
|
||||
elif command in bindings.values():
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
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):
|
||||
@@ -230,45 +302,86 @@ 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 keybindings!")
|
||||
log.keyboard.exception("Failed to read key bindings!")
|
||||
for sectname in self.keybindings:
|
||||
self.changed.emit(sectname)
|
||||
|
||||
def _mark_config_dirty(self):
|
||||
"""Mark the config as dirty."""
|
||||
self.is_dirty = True
|
||||
self.config_dirty.emit()
|
||||
|
||||
def _validate_command(self, line):
|
||||
"""Check if a given command is valid."""
|
||||
if line == self.UNBOUND_COMMAND:
|
||||
return
|
||||
commands = line.split(';;')
|
||||
try:
|
||||
first_cmd = commands[0].split(maxsplit=1)[0].strip()
|
||||
cmd = cmdutils.cmd_dict[first_cmd]
|
||||
if cmd.no_cmd_split:
|
||||
commands = [line]
|
||||
except (KeyError, IndexError):
|
||||
pass
|
||||
|
||||
for cmd in commands:
|
||||
if not cmd.strip():
|
||||
raise KeyConfigError("Got empty command (line: {!r})!".format(
|
||||
line))
|
||||
commands = [c.split(maxsplit=1)[0].strip() for c in commands]
|
||||
for cmd in commands:
|
||||
if cmd not in cmdutils.cmd_dict:
|
||||
raise KeyConfigError("Invalid command '{}'!".format(cmd))
|
||||
|
||||
def _read_command(self, line):
|
||||
"""Read a command from a line."""
|
||||
if self._cur_section is None:
|
||||
raise KeyConfigError("Got command '{}' without getting a "
|
||||
"section!".format(line))
|
||||
else:
|
||||
command = line.split(maxsplit=1)[0]
|
||||
if command not in cmdutils.cmd_dict:
|
||||
raise KeyConfigError("Invalid command '{}'!".format(command))
|
||||
self._validate_command(line)
|
||||
for rgx, repl in configdata.CHANGED_KEY_COMMANDS:
|
||||
if rgx.match(line):
|
||||
line = rgx.sub(repl, line)
|
||||
self._mark_config_dirty()
|
||||
break
|
||||
self._cur_command = line
|
||||
|
||||
def _read_keybinding(self, line):
|
||||
"""Read a keybinding from a line."""
|
||||
"""Read a key binding from a line."""
|
||||
if self._cur_command is None:
|
||||
raise KeyConfigError("Got keybinding '{}' without getting a "
|
||||
raise KeyConfigError("Got key binding '{}' without getting a "
|
||||
"command!".format(line))
|
||||
else:
|
||||
assert self._cur_section is not None
|
||||
self._add_binding(self._cur_section, line, self._cur_command)
|
||||
|
||||
def _add_binding(self, sectname, keychain, command):
|
||||
def _add_binding(self, sectname, keychain, command, *, force=False):
|
||||
"""Add a new binding from keychain to command in section sectname."""
|
||||
log.keyboard.debug("Adding binding {} -> {} in mode {}.".format(
|
||||
keychain, command, sectname))
|
||||
if sectname not in self.keybindings:
|
||||
self.keybindings[sectname] = collections.OrderedDict()
|
||||
if keychain in self.get_bindings_for(sectname):
|
||||
raise KeyConfigError("Duplicate keychain '{}'!".format(keychain))
|
||||
if force or command == self.UNBOUND_COMMAND:
|
||||
self.unbind(keychain, mode=sectname)
|
||||
else:
|
||||
raise DuplicateKeychainError(keychain)
|
||||
section = self.keybindings[sectname]
|
||||
if (command != self.UNBOUND_COMMAND and
|
||||
section.get(keychain, None) == self.UNBOUND_COMMAND):
|
||||
# re-binding an unbound keybinding
|
||||
del section[keychain]
|
||||
self.keybindings[sectname][keychain] = command
|
||||
|
||||
def get_bindings_for(self, section):
|
||||
"""Get a dict with all merged keybindings for a section."""
|
||||
"""Get a dict with all merged key bindings for a section."""
|
||||
bindings = {}
|
||||
for sectstring, d in self.keybindings.items():
|
||||
if sectstring.startswith('!'):
|
||||
@@ -284,4 +397,6 @@ class KeyConfigParser(QObject):
|
||||
bindings.update(self.keybindings['all'])
|
||||
except KeyError:
|
||||
pass
|
||||
bindings = {k: v for k, v in bindings.items()
|
||||
if v != self.UNBOUND_COMMAND}
|
||||
return bindings
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 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/>.
|
||||
|
||||
"""Parser for line-based configurations like histories."""
|
||||
|
||||
import os
|
||||
import os.path
|
||||
import collections
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot
|
||||
|
||||
from qutebrowser.utils import log, utils, objreg, qtutils
|
||||
from qutebrowser.config import config
|
||||
|
||||
|
||||
class LineConfigParser(collections.UserList):
|
||||
|
||||
"""Parser for configuration files which are simply line-based.
|
||||
|
||||
Attributes:
|
||||
data: A list of lines.
|
||||
_configdir: The directory to read the config from.
|
||||
_configfile: The config file path.
|
||||
_fname: Filename of the config.
|
||||
_binary: Whether to open the file in binary mode.
|
||||
_limit: The config section/option used to limit the maximum number of
|
||||
lines.
|
||||
"""
|
||||
|
||||
def __init__(self, configdir, fname, limit=None, binary=False):
|
||||
"""Config constructor.
|
||||
|
||||
Args:
|
||||
configdir: Directory to read the config from.
|
||||
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__()
|
||||
self._configdir = configdir
|
||||
self._configfile = os.path.join(self._configdir, fname)
|
||||
self._fname = fname
|
||||
self._limit = limit
|
||||
self._binary = binary
|
||||
if not os.path.isfile(self._configfile):
|
||||
self.data = []
|
||||
else:
|
||||
log.init.debug("Reading config from {}".format(self._configfile))
|
||||
self.read(self._configfile)
|
||||
if limit is not None:
|
||||
objreg.get('config').changed.connect(self.cleanup_file)
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self, constructor=True,
|
||||
configdir=self._configdir, fname=self._fname,
|
||||
limit=self._limit, binary=self._binary)
|
||||
|
||||
def read(self, filename):
|
||||
"""Read the data from a file."""
|
||||
if self._binary:
|
||||
with open(filename, 'rb') as f:
|
||||
self.data = [line.rstrip(b'\n') for line in f.readlines()]
|
||||
else:
|
||||
with open(filename, 'r', encoding='utf-8') as f:
|
||||
self.data = [line.rstrip('\n') for line in f.readlines()]
|
||||
|
||||
def write(self, fp, limit=-1):
|
||||
"""Write the data to a file.
|
||||
|
||||
Args:
|
||||
fp: A file object to write the data to.
|
||||
limit: How many lines to write, or -1 for no limit.
|
||||
"""
|
||||
if limit == -1:
|
||||
data = self.data
|
||||
else:
|
||||
data = self.data[-limit:]
|
||||
if self._binary:
|
||||
fp.write(b'\n'.join(data))
|
||||
else:
|
||||
fp.write('\n'.join(data))
|
||||
|
||||
def save(self):
|
||||
"""Save the config file."""
|
||||
limit = -1 if self._limit is None else config.get(*self._limit)
|
||||
if limit == 0:
|
||||
return
|
||||
if not os.path.exists(self._configdir):
|
||||
os.makedirs(self._configdir, 0o755)
|
||||
log.destroy.debug("Saving config to {}".format(self._configfile))
|
||||
with qtutils.savefile_open(self._configfile, self._binary) as f:
|
||||
self.write(f, limit)
|
||||
|
||||
@pyqtSlot(str, str)
|
||||
def cleanup_file(self, section, option):
|
||||
"""Delete the file if the limit was changed to 0."""
|
||||
if (section, option) != self._limit:
|
||||
return
|
||||
value = config.get(section, option)
|
||||
if value == 0:
|
||||
if os.path.exists(self._configfile):
|
||||
os.remove(self._configfile)
|
||||
@@ -1,6 +1,6 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
@@ -29,6 +29,7 @@ class Section:
|
||||
"""Base class for KeyValue/ValueList sections.
|
||||
|
||||
Attributes:
|
||||
_readonly: Whether this section is read-only.
|
||||
values: An OrderedDict with key as index and value as value.
|
||||
key: string
|
||||
value: SettingValue
|
||||
@@ -38,6 +39,7 @@ class Section:
|
||||
def __init__(self):
|
||||
self.values = None
|
||||
self.descriptions = {}
|
||||
self._readonly = False
|
||||
|
||||
def __getitem__(self, key):
|
||||
"""Get the value for key.
|
||||
@@ -99,13 +101,15 @@ class KeyValue(Section):
|
||||
set of keys.
|
||||
"""
|
||||
|
||||
def __init__(self, *defaults):
|
||||
def __init__(self, *defaults, readonly=False):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
*defaults: A (key, value, description) list of defaults.
|
||||
readonly: Whether this config is readonly.
|
||||
"""
|
||||
super().__init__()
|
||||
self._readonly = readonly
|
||||
if not defaults:
|
||||
return
|
||||
self.values = collections.OrderedDict()
|
||||
@@ -115,6 +119,8 @@ class KeyValue(Section):
|
||||
self.descriptions[k] = desc
|
||||
|
||||
def setv(self, layer, key, value, interpolated):
|
||||
if self._readonly:
|
||||
raise ValueError("Trying to modify a read-only config!")
|
||||
self.values[key].setv(layer, value, interpolated)
|
||||
|
||||
def dump_userconfig(self):
|
||||
@@ -133,7 +139,7 @@ class ValueList(Section):
|
||||
"""This class represents a section with a list key-value settings.
|
||||
|
||||
These are settings inside sections which don't have fixed keys, but instead
|
||||
have a dynamic list of "key = value" pairs, like keybindings or
|
||||
have a dynamic list of "key = value" pairs, like key bindings or
|
||||
searchengines.
|
||||
|
||||
They basically consist of two different SettingValues.
|
||||
@@ -143,17 +149,20 @@ class ValueList(Section):
|
||||
keytype: The type to use for the key (only used for validating)
|
||||
valtype: The type to use for the value.
|
||||
_ordered_value_cache: A ChainMap-like OrderedDict of all values.
|
||||
_readonly: Whether this section is read-only.
|
||||
"""
|
||||
|
||||
def __init__(self, keytype, valtype, *defaults):
|
||||
def __init__(self, keytype, valtype, *defaults, readonly=False):
|
||||
"""Wrap types over default values. Take care when overriding this.
|
||||
|
||||
Args:
|
||||
keytype: The type instance to be used for keys.
|
||||
valtype: The type instance to be used for values.
|
||||
*defaults: A (key, value) list of default values.
|
||||
readonly: Whether this config is readonly.
|
||||
"""
|
||||
super().__init__()
|
||||
self._readonly = readonly
|
||||
self._ordered_value_cache = None
|
||||
self.keytype = keytype
|
||||
self.valtype = valtype
|
||||
@@ -182,6 +191,8 @@ class ValueList(Section):
|
||||
return self._ordered_value_cache
|
||||
|
||||
def setv(self, layer, key, value, interpolated):
|
||||
if self._readonly:
|
||||
raise ValueError("Trying to modify a read-only config!")
|
||||
self.keytype.validate(key)
|
||||
if key in self.layers[layer]:
|
||||
self.layers[layer][key].setv(layer, value, interpolated)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
@@ -56,7 +56,7 @@ def set_register_stylesheet(obj):
|
||||
Must have a STYLESHEET attribute.
|
||||
"""
|
||||
qss = get_stylesheet(obj.STYLESHEET)
|
||||
log.style.vdebug("stylesheet for {}: {}".format(
|
||||
log.config.vdebug("stylesheet for {}: {}".format(
|
||||
obj.__class__.__name__, qss))
|
||||
obj.setStyleSheet(qss)
|
||||
objreg.get('config').changed.connect(
|
||||
@@ -91,10 +91,10 @@ class ColorDict(dict):
|
||||
try:
|
||||
val = super().__getitem__(key)
|
||||
except KeyError:
|
||||
log.style.exception("No color defined for {}!")
|
||||
log.config.exception("No color defined for {}!")
|
||||
return ''
|
||||
if isinstance(val, QColor):
|
||||
# This could happen when accidentaly declarding something as
|
||||
# This could happen when accidentally declaring something as
|
||||
# QtColor instead of Color in the config, and it'd go unnoticed as
|
||||
# the CSS is invalid then.
|
||||
raise TypeError("QColor passed to ColorDict!")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
@@ -26,7 +26,7 @@ class SettingValue:
|
||||
|
||||
"""Base class for setting values.
|
||||
|
||||
Intended to be subclassed by config value "types".
|
||||
Intended to be sub-classed by config value "types".
|
||||
|
||||
Attributes:
|
||||
typ: A BaseType subclass instance.
|
||||
@@ -79,6 +79,7 @@ class SettingValue:
|
||||
if val is not None:
|
||||
return val
|
||||
else: # pylint: disable=useless-else-on-loop
|
||||
# https://bitbucket.org/logilab/pylint/issue/489/
|
||||
raise ValueError("No valid config value found!")
|
||||
|
||||
def transformed(self):
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
@@ -22,189 +22,421 @@
|
||||
Module attributes:
|
||||
ATTRIBUTES: A mapping from internal setting names to QWebSetting enum
|
||||
constants.
|
||||
SETTERS: A mapping from setting names to QWebSetting setter method names.
|
||||
settings: The global QWebSettings singleton instance.
|
||||
"""
|
||||
|
||||
import os.path
|
||||
|
||||
from PyQt5.QtWebKit import QWebSettings
|
||||
from PyQt5.QtCore import QStandardPaths
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import usertypes, standarddir, objreg
|
||||
from qutebrowser.utils import standarddir, objreg, log, utils, debug
|
||||
|
||||
MapType = usertypes.enum('MapType', ['attribute', 'setter', 'setter_none',
|
||||
'static_setter'])
|
||||
UNSET = object()
|
||||
|
||||
|
||||
class Base:
|
||||
|
||||
"""Base class for QWebSetting wrappers.
|
||||
|
||||
Attributes:
|
||||
_default: The default value of this setting.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._default = UNSET
|
||||
|
||||
def _get_qws(self, qws):
|
||||
"""Get the QWebSettings object to use.
|
||||
|
||||
Args:
|
||||
qws: The QWebSettings instance to use, or None to use the global
|
||||
instance.
|
||||
"""
|
||||
if qws is None:
|
||||
return QWebSettings.globalSettings()
|
||||
else:
|
||||
return qws
|
||||
|
||||
def save_default(self, qws=None):
|
||||
"""Save the default value based on the currently set one.
|
||||
|
||||
This does nothing if no getter is configured for this setting.
|
||||
|
||||
Args:
|
||||
qws: The QWebSettings instance to use, or None to use the global
|
||||
instance.
|
||||
|
||||
Return:
|
||||
The saved default value.
|
||||
"""
|
||||
try:
|
||||
self._default = self.get(qws)
|
||||
return self._default
|
||||
except AttributeError:
|
||||
return None
|
||||
|
||||
def restore_default(self, qws=None):
|
||||
"""Restore the default value from the saved one.
|
||||
|
||||
This does nothing if the default has never been set.
|
||||
|
||||
Args:
|
||||
qws: The QWebSettings instance to use, or None to use the global
|
||||
instance.
|
||||
"""
|
||||
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):
|
||||
"""Get the value of this setting.
|
||||
|
||||
Must be overridden by subclasses.
|
||||
|
||||
Args:
|
||||
qws: The QWebSettings instance to use, or None to use the global
|
||||
instance.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def set(self, value, qws=None):
|
||||
"""Set the value of this setting.
|
||||
|
||||
Args:
|
||||
value: The value to set.
|
||||
qws: The QWebSettings instance to use, or None to use the global
|
||||
instance.
|
||||
"""
|
||||
if value is None:
|
||||
self.restore_default(qws)
|
||||
else:
|
||||
self._set(value, qws=qws)
|
||||
|
||||
def _set(self, value, qws):
|
||||
"""Inner function to set the value of this setting.
|
||||
|
||||
Must be overridden by subclasses.
|
||||
|
||||
Args:
|
||||
value: The value to set.
|
||||
qws: The QWebSettings instance to use, or None to use the global
|
||||
instance.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class Attribute(Base):
|
||||
|
||||
"""A setting set via QWebSettings::setAttribute.
|
||||
|
||||
Attributes:
|
||||
self._attribute: A QWebSettings::WebAttribute instance.
|
||||
"""
|
||||
|
||||
def __init__(self, attribute):
|
||||
super().__init__()
|
||||
self._attribute = attribute
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(
|
||||
self, attribute=debug.qenum_key(QWebSettings, self._attribute),
|
||||
constructor=True)
|
||||
|
||||
def get(self, qws=None):
|
||||
return self._get_qws(qws).attribute(self._attribute)
|
||||
|
||||
def _set(self, value, qws=None):
|
||||
self._get_qws(qws).setAttribute(self._attribute, value)
|
||||
|
||||
|
||||
class Setter(Base):
|
||||
|
||||
"""A setting set via QWebSettings getter/setter methods.
|
||||
|
||||
This will pass the QWebSettings instance ("self") as first argument to the
|
||||
methods, so self._getter/self._setter are the *unbound* methods.
|
||||
|
||||
Attributes:
|
||||
_getter: The unbound QWebSettings method to get this value, or None.
|
||||
_setter: The unbound QWebSettings method to set this value.
|
||||
_args: An iterable of the arguments to pass to the setter/getter
|
||||
(before the value, for the setter).
|
||||
_unpack: Whether to unpack args (True) or pass them directly (False).
|
||||
"""
|
||||
|
||||
def __init__(self, getter, setter, args=(), unpack=False):
|
||||
super().__init__()
|
||||
self._getter = getter
|
||||
self._setter = setter
|
||||
self._args = args
|
||||
self._unpack = unpack
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self, getter=self._getter, setter=self._setter,
|
||||
args=self._args, unpack=self._unpack,
|
||||
constructor=True)
|
||||
|
||||
def get(self, qws=None):
|
||||
if self._getter is None:
|
||||
raise AttributeError("No getter set!")
|
||||
return self._getter(self._get_qws(qws), *self._args)
|
||||
|
||||
def _set(self, value, qws=None):
|
||||
args = [self._get_qws(qws)]
|
||||
args.extend(self._args)
|
||||
if self._unpack:
|
||||
args.extend(value)
|
||||
else:
|
||||
args.append(value)
|
||||
self._setter(*args)
|
||||
|
||||
|
||||
class NullStringSetter(Setter):
|
||||
|
||||
"""A setter for settings requiring a null QString as default.
|
||||
|
||||
This overrides save_default so None is saved for an empty string. This is
|
||||
needed for the CSS media type, because it returns an empty Python string
|
||||
when getting the value, but setting it to the default requires passing None
|
||||
(a null QString) instead of an empty string.
|
||||
"""
|
||||
|
||||
def save_default(self, qws=None):
|
||||
try:
|
||||
val = self.get(qws)
|
||||
except AttributeError:
|
||||
return None
|
||||
if val == '':
|
||||
self._set(None, qws=qws)
|
||||
else:
|
||||
self._set(val, qws=qws)
|
||||
return val
|
||||
|
||||
|
||||
class GlobalSetter(Setter):
|
||||
|
||||
"""A setting set via static QWebSettings getter/setter methods.
|
||||
|
||||
self._getter/self._setter are the *bound* methods.
|
||||
"""
|
||||
|
||||
def get(self, qws=None):
|
||||
if qws is not None:
|
||||
raise ValueError("qws may not be set with GlobalSetters!")
|
||||
if self._getter is None:
|
||||
raise AttributeError("No getter set!")
|
||||
return self._getter(*self._args)
|
||||
|
||||
def _set(self, value, qws=None):
|
||||
if qws is not None:
|
||||
raise ValueError("qws may not be set with GlobalSetters!")
|
||||
args = list(self._args)
|
||||
if self._unpack:
|
||||
args.extend(value)
|
||||
else:
|
||||
args.append(value)
|
||||
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':
|
||||
(MapType.attribute, QWebSettings.AutoLoadImages),
|
||||
Attribute(QWebSettings.AutoLoadImages),
|
||||
'allow-javascript':
|
||||
(MapType.attribute, QWebSettings.JavascriptEnabled),
|
||||
Attribute(QWebSettings.JavascriptEnabled),
|
||||
'javascript-can-open-windows':
|
||||
(MapType.attribute, QWebSettings.JavascriptCanOpenWindows),
|
||||
Attribute(QWebSettings.JavascriptCanOpenWindows),
|
||||
'javascript-can-close-windows':
|
||||
(MapType.attribute, QWebSettings.JavascriptCanCloseWindows),
|
||||
Attribute(QWebSettings.JavascriptCanCloseWindows),
|
||||
'javascript-can-access-clipboard':
|
||||
(MapType.attribute, QWebSettings.JavascriptCanAccessClipboard),
|
||||
Attribute(QWebSettings.JavascriptCanAccessClipboard),
|
||||
#'allow-java':
|
||||
# (MapType.attribute, QWebSettings.JavaEnabled),
|
||||
# Attribute(QWebSettings.JavaEnabled),
|
||||
'allow-plugins':
|
||||
(MapType.attribute, QWebSettings.PluginsEnabled),
|
||||
Attribute(QWebSettings.PluginsEnabled),
|
||||
'webgl':
|
||||
Attribute(QWebSettings.WebGLEnabled),
|
||||
'css-regions':
|
||||
Attribute(QWebSettings.CSSRegionsEnabled),
|
||||
'hyperlink-auditing':
|
||||
Attribute(QWebSettings.HyperlinkAuditingEnabled),
|
||||
'local-content-can-access-remote-urls':
|
||||
(MapType.attribute, QWebSettings.LocalContentCanAccessRemoteUrls),
|
||||
Attribute(QWebSettings.LocalContentCanAccessRemoteUrls),
|
||||
'local-content-can-access-file-urls':
|
||||
(MapType.attribute, QWebSettings.LocalContentCanAccessFileUrls),
|
||||
Attribute(QWebSettings.LocalContentCanAccessFileUrls),
|
||||
'cookies-accept':
|
||||
CookiePolicy(),
|
||||
},
|
||||
'network': {
|
||||
'dns-prefetch':
|
||||
(MapType.attribute, QWebSettings.DnsPrefetchEnabled),
|
||||
Attribute(QWebSettings.DnsPrefetchEnabled),
|
||||
},
|
||||
'input': {
|
||||
'spatial-navigation':
|
||||
(MapType.attribute, QWebSettings.SpatialNavigationEnabled),
|
||||
Attribute(QWebSettings.SpatialNavigationEnabled),
|
||||
'links-included-in-focus-chain':
|
||||
(MapType.attribute, QWebSettings.LinksIncludedInFocusChain),
|
||||
Attribute(QWebSettings.LinksIncludedInFocusChain),
|
||||
},
|
||||
'fonts': {
|
||||
'web-family-standard':
|
||||
(MapType.setter_none, lambda qws, v:
|
||||
qws.setFontFamily(QWebSettings.StandardFont, v)),
|
||||
Setter(getter=QWebSettings.fontFamily,
|
||||
setter=QWebSettings.setFontFamily,
|
||||
args=[QWebSettings.StandardFont]),
|
||||
'web-family-fixed':
|
||||
(MapType.setter_none, lambda qws, v:
|
||||
qws.setFontFamily(QWebSettings.FixedFont, v)),
|
||||
Setter(getter=QWebSettings.fontFamily,
|
||||
setter=QWebSettings.setFontFamily,
|
||||
args=[QWebSettings.FixedFont]),
|
||||
'web-family-serif':
|
||||
(MapType.setter_none, lambda qws, v:
|
||||
qws.setFontFamily(QWebSettings.SerifFont, v)),
|
||||
Setter(getter=QWebSettings.fontFamily,
|
||||
setter=QWebSettings.setFontFamily,
|
||||
args=[QWebSettings.SerifFont]),
|
||||
'web-family-sans-serif':
|
||||
(MapType.setter_none, lambda qws, v:
|
||||
qws.setFontFamily(QWebSettings.SansSerifFont, v)),
|
||||
Setter(getter=QWebSettings.fontFamily,
|
||||
setter=QWebSettings.setFontFamily,
|
||||
args=[QWebSettings.SansSerifFont]),
|
||||
'web-family-cursive':
|
||||
(MapType.setter_none, lambda qws, v:
|
||||
qws.setFontFamily(QWebSettings.CursiveFont, v)),
|
||||
Setter(getter=QWebSettings.fontFamily,
|
||||
setter=QWebSettings.setFontFamily,
|
||||
args=[QWebSettings.CursiveFont]),
|
||||
'web-family-fantasy':
|
||||
(MapType.setter_none, lambda qws, v:
|
||||
qws.setFontFamily(QWebSettings.FantasyFont, v)),
|
||||
Setter(getter=QWebSettings.fontFamily,
|
||||
setter=QWebSettings.setFontFamily,
|
||||
args=[QWebSettings.FantasyFont]),
|
||||
'web-size-minimum':
|
||||
(MapType.setter, lambda qws, v:
|
||||
qws.setFontSize(QWebSettings.MinimumFontSize, v)),
|
||||
Setter(getter=QWebSettings.fontSize,
|
||||
setter=QWebSettings.setFontSize,
|
||||
args=[QWebSettings.MinimumFontSize]),
|
||||
'web-size-minimum-logical':
|
||||
(MapType.setter, lambda qws, v:
|
||||
qws.setFontSize(QWebSettings.MinimumLogicalFontSize, v)),
|
||||
Setter(getter=QWebSettings.fontSize,
|
||||
setter=QWebSettings.setFontSize,
|
||||
args=[QWebSettings.MinimumLogicalFontSize]),
|
||||
'web-size-default':
|
||||
(MapType.setter, lambda qws, v:
|
||||
qws.setFontSize(QWebSettings.DefaultFontSize, v)),
|
||||
Setter(getter=QWebSettings.fontSize,
|
||||
setter=QWebSettings.setFontSize,
|
||||
args=[QWebSettings.DefaultFontSize]),
|
||||
'web-size-default-fixed':
|
||||
(MapType.setter, lambda qws, v:
|
||||
qws.setFontSize(QWebSettings.DefaultFixedFontSize, v)),
|
||||
Setter(getter=QWebSettings.fontSize,
|
||||
setter=QWebSettings.setFontSize,
|
||||
args=[QWebSettings.DefaultFixedFontSize]),
|
||||
},
|
||||
'ui': {
|
||||
'zoom-text-only':
|
||||
(MapType.attribute, QWebSettings.ZoomTextOnly),
|
||||
Attribute(QWebSettings.ZoomTextOnly),
|
||||
'frame-flattening':
|
||||
(MapType.attribute, QWebSettings.FrameFlatteningEnabled),
|
||||
Attribute(QWebSettings.FrameFlatteningEnabled),
|
||||
'user-stylesheet':
|
||||
(MapType.setter_none, lambda qws, v: qws.setUserStyleSheetUrl(v)),
|
||||
Setter(getter=QWebSettings.userStyleSheetUrl,
|
||||
setter=QWebSettings.setUserStyleSheetUrl),
|
||||
'css-media-type':
|
||||
(MapType.setter, lambda qws, v: qws.setCSSMediaType(v)),
|
||||
NullStringSetter(getter=QWebSettings.cssMediaType,
|
||||
setter=QWebSettings.setCSSMediaType),
|
||||
'smooth-scrolling':
|
||||
Attribute(QWebSettings.ScrollAnimatorEnabled),
|
||||
#'accelerated-compositing':
|
||||
# (MapType.attribute, QWebSettings.AcceleratedCompositingEnabled),
|
||||
# Attribute(QWebSettings.AcceleratedCompositingEnabled),
|
||||
#'tiled-backing-store':
|
||||
# (MapType.attribute, QWebSettings.TiledBackingStoreEnabled),
|
||||
# Attribute(QWebSettings.TiledBackingStoreEnabled),
|
||||
},
|
||||
'storage': {
|
||||
'offline-storage-database':
|
||||
(MapType.attribute, QWebSettings.OfflineStorageDatabaseEnabled),
|
||||
Attribute(QWebSettings.OfflineStorageDatabaseEnabled),
|
||||
'offline-web-application-storage':
|
||||
(MapType.attribute,
|
||||
QWebSettings.OfflineWebApplicationCacheEnabled),
|
||||
Attribute(QWebSettings.OfflineWebApplicationCacheEnabled),
|
||||
'local-storage':
|
||||
(MapType.attribute, QWebSettings.LocalStorageEnabled),
|
||||
Attribute(QWebSettings.LocalStorageEnabled),
|
||||
'maximum-pages-in-cache':
|
||||
(MapType.static_setter, lambda v:
|
||||
QWebSettings.setMaximumPagesInCache(v)),
|
||||
GlobalSetter(getter=QWebSettings.maximumPagesInCache,
|
||||
setter=QWebSettings.setMaximumPagesInCache),
|
||||
'object-cache-capacities':
|
||||
(MapType.static_setter, lambda v:
|
||||
QWebSettings.setObjectCacheCapacities(*v)),
|
||||
GlobalSetter(getter=None,
|
||||
setter=QWebSettings.setObjectCacheCapacities,
|
||||
unpack=True),
|
||||
'offline-storage-default-quota':
|
||||
(MapType.static_setter, lambda v:
|
||||
QWebSettings.setOfflineStorageDefaultQuota(v)),
|
||||
GlobalSetter(getter=QWebSettings.offlineStorageDefaultQuota,
|
||||
setter=QWebSettings.setOfflineStorageDefaultQuota),
|
||||
'offline-web-application-cache-quota':
|
||||
(MapType.static_setter, lambda v:
|
||||
QWebSettings.setOfflineWebApplicationCacheQuota(v)),
|
||||
GlobalSetter(
|
||||
getter=QWebSettings.offlineWebApplicationCacheQuota,
|
||||
setter=QWebSettings.setOfflineWebApplicationCacheQuota),
|
||||
},
|
||||
'general': {
|
||||
'private-browsing':
|
||||
(MapType.attribute, QWebSettings.PrivateBrowsingEnabled),
|
||||
Attribute(QWebSettings.PrivateBrowsingEnabled),
|
||||
'developer-extras':
|
||||
(MapType.attribute, QWebSettings.DeveloperExtrasEnabled),
|
||||
Attribute(QWebSettings.DeveloperExtrasEnabled),
|
||||
'print-element-backgrounds':
|
||||
(MapType.attribute, QWebSettings.PrintElementBackgrounds),
|
||||
Attribute(QWebSettings.PrintElementBackgrounds),
|
||||
'xss-auditing':
|
||||
(MapType.attribute, QWebSettings.XSSAuditingEnabled),
|
||||
Attribute(QWebSettings.XSSAuditingEnabled),
|
||||
'site-specific-quirks':
|
||||
(MapType.attribute, QWebSettings.SiteSpecificQuirksEnabled),
|
||||
Attribute(QWebSettings.SiteSpecificQuirksEnabled),
|
||||
'default-encoding':
|
||||
(MapType.setter_none, lambda qws, v:
|
||||
qws.setDefaultTextEncoding(v)),
|
||||
Setter(getter=QWebSettings.defaultTextEncoding,
|
||||
setter=QWebSettings.setDefaultTextEncoding),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
settings = None
|
||||
|
||||
|
||||
def _set_setting(typ, arg, value):
|
||||
"""Set a QWebSettings setting.
|
||||
|
||||
Args:
|
||||
typ: The type of the item.
|
||||
arg: The argument (attribute/handler)
|
||||
value: The value to set.
|
||||
"""
|
||||
if not isinstance(typ, MapType):
|
||||
raise TypeError("Type {} is no MapType member!".format(typ))
|
||||
if typ == MapType.attribute:
|
||||
settings.setAttribute(arg, value)
|
||||
elif typ == MapType.setter_none:
|
||||
if value is None:
|
||||
value = ""
|
||||
arg(settings, value)
|
||||
elif typ == MapType.setter and value is not None:
|
||||
arg(settings, value)
|
||||
elif typ == MapType.static_setter and value is not None:
|
||||
arg(value)
|
||||
|
||||
|
||||
def init():
|
||||
"""Initialize the global QWebSettings."""
|
||||
cachedir = standarddir.get(QStandardPaths.CacheLocation)
|
||||
QWebSettings.setIconDatabasePath(cachedir)
|
||||
QWebSettings.setOfflineWebApplicationCachePath(
|
||||
os.path.join(cachedir, 'application-cache'))
|
||||
datadir = standarddir.get(QStandardPaths.DataLocation)
|
||||
QWebSettings.globalSettings().setLocalStoragePath(
|
||||
os.path.join(datadir, 'local-storage'))
|
||||
QWebSettings.setOfflineStoragePath(
|
||||
os.path.join(datadir, 'offline-storage'))
|
||||
cache_path = standarddir.cache()
|
||||
data_path = standarddir.data()
|
||||
if config.get('general', 'private-browsing') or cache_path is None:
|
||||
QWebSettings.setIconDatabasePath('')
|
||||
else:
|
||||
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'))
|
||||
|
||||
global settings
|
||||
settings = QWebSettings.globalSettings()
|
||||
for sectname, section in MAPPINGS.items():
|
||||
for optname, (typ, arg) in section.items():
|
||||
for optname, mapping in section.items():
|
||||
default = mapping.save_default()
|
||||
log.config.vdebug("Saved default for {} -> {}: {!r}".format(
|
||||
sectname, optname, default))
|
||||
value = config.get(sectname, optname)
|
||||
_set_setting(typ, arg, value)
|
||||
log.config.vdebug("Setting {} -> {} to {!r}".format(
|
||||
sectname, optname, value))
|
||||
mapping.set(value)
|
||||
objreg.get('config').changed.connect(update_settings)
|
||||
|
||||
|
||||
def update_settings(section, option):
|
||||
"""Update global settings when qwebsettings changed."""
|
||||
try:
|
||||
typ, arg = MAPPINGS[section][option]
|
||||
except KeyError:
|
||||
return
|
||||
value = config.get(section, option)
|
||||
_set_setting(typ, arg, value)
|
||||
cache_path = standarddir.cache()
|
||||
if (section, option) == ('general', 'private-browsing'):
|
||||
if config.get('general', 'private-browsing') or cache_path is None:
|
||||
QWebSettings.setIconDatabasePath('')
|
||||
else:
|
||||
QWebSettings.setIconDatabasePath(cache_path)
|
||||
else:
|
||||
try:
|
||||
mapping = MAPPINGS[section][option]
|
||||
except KeyError:
|
||||
return
|
||||
value = config.get(section, option)
|
||||
mapping.set(value)
|
||||
|
||||
@@ -14,21 +14,23 @@ 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 %}
|
||||
<tr><th colspan="2"><h3>{{ section }}</h3><pre>{{ config.SECTION_DESC.get(section)|wordwrap(width=120) }}</pre></th></tr>
|
||||
{% for d, e in config.DATA.get(section).items() %}
|
||||
<tr>
|
||||
<td>{{ d }} (Current: {{ e.value()|truncate(100) }})</td>
|
||||
<td>{{ d }} (Current: {{ confget(section, d)|truncate(100) }})</td>
|
||||
<td>
|
||||
<input type="input"
|
||||
onblur="cset('{{ section }}', '{{ d }}', this)"
|
||||
value="{{ e.value() }}">
|
||||
value="{{ confget(section, d) }}">
|
||||
</input>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
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);
|
||||
}
|
||||
})();
|
||||
@@ -1,6 +1,6 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
@@ -17,13 +17,13 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Base class for vim-like keysequence parser."""
|
||||
"""Base class for vim-like key sequence parser."""
|
||||
|
||||
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
|
||||
@@ -44,20 +44,22 @@ class BaseKeyParser(QObject):
|
||||
ambiguous: There are both a partial and a definitive match.
|
||||
none: No more matches possible.
|
||||
|
||||
Types: type of a keybinding.
|
||||
chain: execute() was called via a chain-like keybinding
|
||||
special: execute() was called via a special keybinding
|
||||
Types: type of a key binding.
|
||||
chain: execute() was called via a chain-like key binding
|
||||
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 keybindings
|
||||
bindings: Bound key bindings
|
||||
special_bindings: Bound special bindings (<Foo>).
|
||||
_win_id: The window ID this keyparser is associated with.
|
||||
_warn_on_keychains: Whether a warning should be logged when binding
|
||||
keychains in a section which does not support them.
|
||||
_keystring: The currently entered key sequence
|
||||
_timer: Timer for delayed execution.
|
||||
_ambiguous_timer: Timer for delayed execution with ambiguous bindings.
|
||||
_modename: The name of the input mode associated with this keyparser.
|
||||
_supports_count: Whether count is supported
|
||||
_supports_chains: Whether keychains are supported
|
||||
@@ -69,16 +71,18 @@ class BaseKeyParser(QObject):
|
||||
|
||||
keystring_updated = pyqtSignal(str)
|
||||
do_log = True
|
||||
passthrough = False
|
||||
|
||||
Match = usertypes.enum('Match', ['partial', 'definitive', 'ambiguous',
|
||||
'none'])
|
||||
'other', 'none'])
|
||||
Type = usertypes.enum('Type', ['chain', 'special'])
|
||||
|
||||
def __init__(self, win_id, parent=None, supports_count=None,
|
||||
supports_chains=False):
|
||||
super().__init__(parent)
|
||||
self._win_id = win_id
|
||||
self._timer = None
|
||||
self._ambiguous_timer = usertypes.Timer(self, 'ambiguous-match')
|
||||
self._ambiguous_timer.setSingleShot(True)
|
||||
self._modename = None
|
||||
self._keystring = ''
|
||||
if supports_count is None:
|
||||
@@ -136,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
|
||||
@@ -152,30 +159,30 @@ class BaseKeyParser(QObject):
|
||||
e: the KeyPressEvent from Qt.
|
||||
|
||||
Return:
|
||||
True if event has been handled, False otherwise.
|
||||
A self.Match member.
|
||||
"""
|
||||
txt = e.text()
|
||||
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
|
||||
if len(txt) == 1:
|
||||
category = unicodedata.category(txt)
|
||||
is_control_char = (category == 'Cc')
|
||||
else:
|
||||
is_control_char = False
|
||||
|
||||
if (not txt) or unicodedata.category(txt) == 'Cc': # control chars
|
||||
if (not txt) or is_control_char:
|
||||
self._debug_log("Ignoring, no text char")
|
||||
return False
|
||||
return self.Match.none
|
||||
|
||||
self._stop_delayed_exec()
|
||||
self._stop_timers()
|
||||
self._keystring += txt
|
||||
|
||||
count, cmd_input = self._split_count()
|
||||
|
||||
if not cmd_input:
|
||||
# Only a count, no command yet, but we handled it
|
||||
return True
|
||||
return self.Match.other
|
||||
|
||||
match, binding = self._match_key(cmd_input)
|
||||
|
||||
@@ -188,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:
|
||||
@@ -198,8 +205,7 @@ class BaseKeyParser(QObject):
|
||||
self._debug_log("Giving up with '{}', no matches".format(
|
||||
self._keystring))
|
||||
self._keystring = ''
|
||||
return False
|
||||
return True
|
||||
return match
|
||||
|
||||
def _match_key(self, cmd_input):
|
||||
"""Try to match a given keystring with any bound keychain.
|
||||
@@ -240,13 +246,16 @@ class BaseKeyParser(QObject):
|
||||
else:
|
||||
return (self.Match.none, None)
|
||||
|
||||
def _stop_delayed_exec(self):
|
||||
def _stop_timers(self):
|
||||
"""Stop a delayed execution if any is running."""
|
||||
if self._timer is not None:
|
||||
if self.do_log:
|
||||
log.keyboard.debug("Stopping delayed execution.")
|
||||
self._timer.stop()
|
||||
self._timer = None
|
||||
if self._ambiguous_timer.isActive() and self.do_log:
|
||||
log.keyboard.debug("Stopping delayed execution.")
|
||||
self._ambiguous_timer.stop()
|
||||
try:
|
||||
self._ambiguous_timer.timeout.disconnect()
|
||||
except TypeError:
|
||||
# no connections
|
||||
pass
|
||||
|
||||
def _handle_ambiguous_match(self, binding, count):
|
||||
"""Handle an ambiguous match.
|
||||
@@ -265,12 +274,10 @@ class BaseKeyParser(QObject):
|
||||
# execute in `time' ms
|
||||
self._debug_log("Scheduling execution of {} in {}ms".format(
|
||||
binding, time))
|
||||
self._timer = usertypes.Timer(self, 'ambigious_match')
|
||||
self._timer.setSingleShot(True)
|
||||
self._timer.setInterval(time)
|
||||
self._timer.timeout.connect(
|
||||
self._ambiguous_timer.setInterval(time)
|
||||
self._ambiguous_timer.timeout.connect(
|
||||
functools.partial(self.delayed_exec, binding, count))
|
||||
self._timer.start()
|
||||
self._ambiguous_timer.start()
|
||||
|
||||
def delayed_exec(self, command, count):
|
||||
"""Execute a delayed command.
|
||||
@@ -279,7 +286,6 @@ class BaseKeyParser(QObject):
|
||||
command/count: As if passed to self.execute()
|
||||
"""
|
||||
self._debug_log("Executing delayed command now!")
|
||||
self._timer = None
|
||||
self._keystring = ''
|
||||
self.keystring_updated.emit(self._keystring)
|
||||
self.execute(command, self.Type.chain, count)
|
||||
@@ -289,13 +295,17 @@ class BaseKeyParser(QObject):
|
||||
|
||||
Args:
|
||||
e: the KeyPressEvent from Qt
|
||||
|
||||
Return:
|
||||
True if the event was handled, False otherwise.
|
||||
"""
|
||||
handled = self._handle_special_key(e)
|
||||
|
||||
if handled or not self._supports_chains:
|
||||
return handled
|
||||
handled = self._handle_single_key(e)
|
||||
match = self._handle_single_key(e)
|
||||
self.keystring_updated.emit(self._keystring)
|
||||
return handled
|
||||
return match != self.Match.none
|
||||
|
||||
def read_config(self, modename=None):
|
||||
"""Read the configuration.
|
||||
@@ -341,9 +351,15 @@ class BaseKeyParser(QObject):
|
||||
|
||||
@pyqtSlot(str)
|
||||
def on_keyconfig_changed(self, mode):
|
||||
"""Re-read the config if a keybinding was changed."""
|
||||
"""Re-read the config if a key binding was changed."""
|
||||
if self._modename is None:
|
||||
raise AttributeError("on_keyconfig_changed called but no section "
|
||||
"defined!")
|
||||
if mode == self._modename:
|
||||
self.read_config()
|
||||
|
||||
def clear_keystring(self):
|
||||
"""Clear the currently entered key sequence."""
|
||||
self._debug_log("discarding keystring '{}'.".format(self._keystring))
|
||||
self._keystring = ''
|
||||
self.keystring_updated.emit(self._keystring)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
@@ -55,6 +55,7 @@ class PassthroughKeyParser(CommandKeyParser):
|
||||
"""
|
||||
|
||||
do_log = False
|
||||
passthrough = True
|
||||
|
||||
def __init__(self, win_id, mode, parent=None, warn=True):
|
||||
"""Constructor.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
@@ -17,17 +17,13 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Mode manager singleton which handles the current keyboard mode.
|
||||
|
||||
Module attributes:
|
||||
manager: The ModeManager instance.
|
||||
"""
|
||||
"""Mode manager singleton which handles the current keyboard mode."""
|
||||
|
||||
import functools
|
||||
|
||||
from PyQt5.QtGui import QWindow
|
||||
from PyQt5.QtCore import pyqtSignal, QObject, QEvent
|
||||
from PyQt5.QtCore import pyqtSignal, Qt, QObject, QEvent
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PyQt5.QtWebKitWidgets import QWebView
|
||||
|
||||
from qutebrowser.keyinput import modeparsers, keyparser
|
||||
from qutebrowser.config import config
|
||||
@@ -35,6 +31,33 @@ from qutebrowser.commands import cmdexc, cmdutils
|
||||
from qutebrowser.utils import usertypes, log, objreg, utils
|
||||
|
||||
|
||||
class KeyEvent:
|
||||
|
||||
"""A small wrapper over a QKeyEvent storing its data.
|
||||
|
||||
This is needed because Qt apparently mutates existing events with new data.
|
||||
It doesn't store the modifiers because they can be different for a key
|
||||
press/release.
|
||||
|
||||
Attributes:
|
||||
key: A Qt.Key member (QKeyEvent::key).
|
||||
text: A string (QKeyEvent::text).
|
||||
"""
|
||||
|
||||
def __init__(self, keyevent):
|
||||
self.key = keyevent.key()
|
||||
self.text = keyevent.text()
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self, key=self.key, text=self.text)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.key == other.key and self.text == other.text
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.key, self.text))
|
||||
|
||||
|
||||
class NotInModeError(Exception):
|
||||
|
||||
"""Exception raised when we want to leave a mode we're not in."""
|
||||
@@ -55,84 +78,51 @@ 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))
|
||||
|
||||
|
||||
class EventFilter(QObject):
|
||||
|
||||
"""Event filter which passes the event to the corrent ModeManager."""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._activated = True
|
||||
|
||||
def eventFilter(self, obj, event):
|
||||
"""Forward events to the correct modeman."""
|
||||
if not self._activated:
|
||||
return False
|
||||
try:
|
||||
modeman = objreg.get('mode-manager', scope='window',
|
||||
window='current')
|
||||
return modeman.eventFilter(obj, event)
|
||||
except objreg.RegistryUnavailableError:
|
||||
# No window available yet, or not a MainWindow
|
||||
return False
|
||||
except:
|
||||
# If there is an exception in here and we leave the eventfilter
|
||||
# activated, we'll get an infinite loop and a stack overflow.
|
||||
self._activated = False
|
||||
raise
|
||||
|
||||
|
||||
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 list of keys where the keyPressEvent was
|
||||
_releaseevents_to_pass: A set of KeyEvents where the keyPressEvent was
|
||||
passed through, so the release event should as
|
||||
well.
|
||||
|
||||
@@ -152,17 +142,15 @@ class ModeManager(QObject):
|
||||
def __init__(self, win_id, parent=None):
|
||||
super().__init__(parent)
|
||||
self._win_id = win_id
|
||||
self._handlers = {}
|
||||
self.passthrough = []
|
||||
self.mode = usertypes.KeyMode.none
|
||||
self._releaseevents_to_pass = []
|
||||
self._parsers = {}
|
||||
self.mode = usertypes.KeyMode.normal
|
||||
self._releaseevents_to_pass = set()
|
||||
self._forward_unbound_keys = config.get(
|
||||
'input', 'forward-unbound-keys')
|
||||
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.
|
||||
@@ -174,17 +162,21 @@ 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()
|
||||
is_tab = event.key() in (Qt.Key_Tab, Qt.Key_Backtab)
|
||||
|
||||
if handled:
|
||||
filter_this = True
|
||||
elif (curmode in self.passthrough or
|
||||
elif is_tab and not isinstance(focus_widget, QWebView):
|
||||
filter_this = True
|
||||
elif (parser.passthrough or
|
||||
self._forward_unbound_keys == 'all' or
|
||||
(self._forward_unbound_keys == 'auto' and is_non_alnum)):
|
||||
filter_this = False
|
||||
@@ -192,16 +184,15 @@ class ModeManager(QObject):
|
||||
filter_this = True
|
||||
|
||||
if not filter_this:
|
||||
self._releaseevents_to_pass.append(event)
|
||||
self._releaseevents_to_pass.add(KeyEvent(event))
|
||||
|
||||
if curmode != usertypes.KeyMode.insert:
|
||||
log.modes.debug("handled: {}, forward-unbound-keys: {}, "
|
||||
"passthrough: {}, is_non_alnum: {} --> filter: "
|
||||
"{} (focused: {!r})".format(
|
||||
"passthrough: {}, is_non_alnum: {}, is_tab {} --> "
|
||||
"filter: {} (focused: {!r})".format(
|
||||
handled, self._forward_unbound_keys,
|
||||
curmode in self.passthrough,
|
||||
is_non_alnum, filter_this,
|
||||
QApplication.instance().focusWidget()))
|
||||
parser.passthrough, is_non_alnum, is_tab,
|
||||
filter_this, focus_widget))
|
||||
return filter_this
|
||||
|
||||
def _eventFilter_keyrelease(self, event):
|
||||
@@ -214,10 +205,9 @@ class ModeManager(QObject):
|
||||
True if event should be filtered, False otherwise.
|
||||
"""
|
||||
# handle like matching KeyPress
|
||||
if event in self._releaseevents_to_pass:
|
||||
# remove all occurences
|
||||
self._releaseevents_to_pass = [
|
||||
e for e in self._releaseevents_to_pass if e != event]
|
||||
keyevent = KeyEvent(event)
|
||||
if keyevent in self._releaseevents_to_pass:
|
||||
self._releaseevents_to_pass.remove(keyevent)
|
||||
filter_this = False
|
||||
else:
|
||||
filter_this = True
|
||||
@@ -225,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 keybindings 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.
|
||||
@@ -252,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):
|
||||
@@ -313,7 +299,7 @@ class ModeManager(QObject):
|
||||
self._forward_unbound_keys = config.get(
|
||||
'input', 'forward-unbound-keys')
|
||||
|
||||
def eventFilter(self, obj, event):
|
||||
def eventFilter(self, event):
|
||||
"""Filter all events based on the currently set mode.
|
||||
|
||||
Also calls the real keypress handler.
|
||||
@@ -327,21 +313,12 @@ class ModeManager(QObject):
|
||||
if self.mode is None:
|
||||
# We got events before mode is set, so just pass them through.
|
||||
return False
|
||||
typ = event.type()
|
||||
if typ not in [QEvent.KeyPress, QEvent.KeyRelease]:
|
||||
# We're not interested in non-key-events so we pass them through.
|
||||
return False
|
||||
if not isinstance(obj, QWindow):
|
||||
# We already handled this same event at some point earlier, so
|
||||
# we're not interested in it anymore.
|
||||
return False
|
||||
if (QApplication.instance().activeWindow() not in
|
||||
objreg.window_registry.values()):
|
||||
# Some other window (print dialog, etc.) is focused so we pass
|
||||
# the event through.
|
||||
return False
|
||||
|
||||
if typ == QEvent.KeyPress:
|
||||
if event.type() == QEvent.KeyPress:
|
||||
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()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
@@ -37,12 +37,18 @@ LastPress = usertypes.enum('LastPress', ['none', 'filtertext', 'keystring'])
|
||||
|
||||
class NormalKeyParser(keyparser.CommandKeyParser):
|
||||
|
||||
"""KeyParser for normalmode with added STARTCHARS detection."""
|
||||
"""KeyParser for normal mode with added STARTCHARS detection and more.
|
||||
|
||||
Attributes:
|
||||
_partial_timer: Timer to clear partial keypresses.
|
||||
"""
|
||||
|
||||
def __init__(self, win_id, parent=None):
|
||||
super().__init__(win_id, parent, supports_count=True,
|
||||
supports_chains=True)
|
||||
self.read_config('normal')
|
||||
self._partial_timer = usertypes.Timer(self, 'partial-match')
|
||||
self._partial_timer.setSingleShot(True)
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self)
|
||||
@@ -54,13 +60,38 @@ class NormalKeyParser(keyparser.CommandKeyParser):
|
||||
e: the KeyPressEvent from Qt.
|
||||
|
||||
Return:
|
||||
True if event has been handled, False otherwise.
|
||||
A self.Match member.
|
||||
"""
|
||||
txt = e.text().strip()
|
||||
if not self._keystring and any(txt == c for c in STARTCHARS):
|
||||
message.set_cmd_text(self._win_id, txt)
|
||||
return True
|
||||
return super()._handle_single_key(e)
|
||||
return self.Match.definitive
|
||||
match = super()._handle_single_key(e)
|
||||
if match == self.Match.partial:
|
||||
timeout = config.get('input', 'partial-timeout')
|
||||
if timeout != 0:
|
||||
self._partial_timer.setInterval(timeout)
|
||||
self._partial_timer.timeout.connect(self._clear_partial_match)
|
||||
self._partial_timer.start()
|
||||
return match
|
||||
|
||||
@pyqtSlot()
|
||||
def _clear_partial_match(self):
|
||||
"""Clear a partial keystring after a timeout."""
|
||||
self._debug_log("Clearing partial keystring {}".format(
|
||||
self._keystring))
|
||||
self._keystring = ''
|
||||
self.keystring_updated.emit(self._keystring)
|
||||
|
||||
@pyqtSlot()
|
||||
def _stop_timers(self):
|
||||
super()._stop_timers()
|
||||
self._partial_timer.stop()
|
||||
try:
|
||||
self._partial_timer.timeout.disconnect(self._clear_partial_match)
|
||||
except TypeError:
|
||||
# no connections
|
||||
pass
|
||||
|
||||
|
||||
class PromptKeyParser(keyparser.CommandKeyParser):
|
||||
@@ -140,21 +171,25 @@ class HintKeyParser(keyparser.CommandKeyParser):
|
||||
|
||||
Args:
|
||||
e: the KeyPressEvent from Qt
|
||||
|
||||
Returns:
|
||||
True if the match has been handled, False otherwise.
|
||||
"""
|
||||
handled = self._handle_single_key(e)
|
||||
if handled and self._keystring:
|
||||
# A key has been added to the keystring (Match.partial)
|
||||
match = self._handle_single_key(e)
|
||||
if match == self.Match.partial:
|
||||
self.keystring_updated.emit(self._keystring)
|
||||
self._last_press = LastPress.keystring
|
||||
return handled
|
||||
elif handled:
|
||||
# We handled the key but the keystring is empty. This happens when
|
||||
# match is Match.definitive, so a keychain has been completed.
|
||||
return True
|
||||
elif match == self.Match.definitive:
|
||||
self._last_press = LastPress.none
|
||||
return handled
|
||||
else:
|
||||
return True
|
||||
elif match == self.Match.other:
|
||||
pass
|
||||
elif match == self.Match.none:
|
||||
# We couldn't find a keychain so we check if it's a special key.
|
||||
return self._handle_special_key(e)
|
||||
else:
|
||||
raise ValueError("Got invalid match type {}!".format(match))
|
||||
|
||||
def execute(self, cmdstr, keytype, count=None):
|
||||
"""Handle a completed keychain."""
|
||||
@@ -183,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')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
@@ -25,7 +25,7 @@ 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
|
||||
@@ -34,92 +34,149 @@ 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.
|
||||
|
||||
Adds all needed components to a vbox, initializes subwidgets and connects
|
||||
Adds all needed components to a vbox, initializes sub-widgets and connects
|
||||
signals.
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
def __init__(self, win_id, parent=None):
|
||||
def __init__(self, geometry=None, parent=None):
|
||||
"""Create a new main window.
|
||||
|
||||
Args:
|
||||
geometry: The geometry to load, as a bytes-object (or None).
|
||||
parent: The parent the window should get.
|
||||
"""
|
||||
super().__init__(parent)
|
||||
self.setAttribute(Qt.WA_DeleteOnClose)
|
||||
self._commandrunner = None
|
||||
self.win_id = win_id
|
||||
self.win_id = next(win_id_gen)
|
||||
self.registry = objreg.ObjectRegistry()
|
||||
objreg.window_registry[win_id] = self
|
||||
objreg.register('main-window', self, scope='window', window=win_id)
|
||||
objreg.window_registry[self.win_id] = self
|
||||
objreg.register('main-window', self, scope='window',
|
||||
window=self.win_id)
|
||||
tab_registry = objreg.ObjectRegistry()
|
||||
objreg.register('tab-registry', tab_registry, scope='window',
|
||||
window=win_id)
|
||||
window=self.win_id)
|
||||
|
||||
message_bridge = message.MessageBridge(self)
|
||||
objreg.register('message-bridge', message_bridge, scope='window',
|
||||
window=win_id)
|
||||
window=self.win_id)
|
||||
|
||||
self.setWindowTitle('qutebrowser')
|
||||
if win_id == 0:
|
||||
self._load_geometry()
|
||||
else:
|
||||
self._set_default_geometry()
|
||||
log.init.debug("Initial mainwindow geometry: {}".format(
|
||||
self.geometry()))
|
||||
self._vbox = QVBoxLayout(self)
|
||||
self._vbox.setContentsMargins(0, 0, 0, 0)
|
||||
self._vbox.setSpacing(0)
|
||||
|
||||
log.init.debug("Initializing downloads...")
|
||||
download_manager = downloads.DownloadManager(win_id, self)
|
||||
download_manager = downloads.DownloadManager(self.win_id, self)
|
||||
objreg.register('download-manager', download_manager, scope='window',
|
||||
window=win_id)
|
||||
window=self.win_id)
|
||||
|
||||
self._downloadview = downloadview.DownloadView(win_id)
|
||||
self._vbox.addWidget(self._downloadview)
|
||||
self._downloadview = downloadview.DownloadView(self.win_id)
|
||||
|
||||
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
|
||||
# window.
|
||||
self.status = bar.StatusBar(self.win_id, parent=self)
|
||||
|
||||
self._add_widgets()
|
||||
self._downloadview.show()
|
||||
|
||||
self._tabbed_browser = tabbedbrowser.TabbedBrowser(win_id)
|
||||
objreg.register('tabbed-browser', self._tabbed_browser, scope='window',
|
||||
window=win_id)
|
||||
self._vbox.addWidget(self._tabbed_browser)
|
||||
self._completion = completionwidget.CompletionView(self.win_id, self)
|
||||
|
||||
self.status = bar.StatusBar(win_id)
|
||||
self._vbox.addWidget(self.status)
|
||||
|
||||
self._completion = completionwidget.CompletionView(win_id, self)
|
||||
|
||||
self._commandrunner = runners.CommandRunner(win_id)
|
||||
|
||||
log.init.debug("Initializing search...")
|
||||
search_runner = runners.SearchRunner(self)
|
||||
objreg.register('search-runner', search_runner, scope='window',
|
||||
window=win_id)
|
||||
self._commandrunner = runners.CommandRunner(self.win_id)
|
||||
|
||||
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()
|
||||
QTimer.singleShot(0, functools.partial(
|
||||
modeman.enter, win_id, usertypes.KeyMode.normal, 'init'))
|
||||
|
||||
# When we're here the statusbar might not even really exist yet, so
|
||||
# resizing will fail. Therefore, we use singleShot QTimers to make sure
|
||||
# we defer this until everything else is initialized.
|
||||
QTimer.singleShot(0, self._connect_resize_completion)
|
||||
objreg.get('config').changed.connect(self.on_config_changed)
|
||||
|
||||
if config.get('ui', 'hide-mouse-cursor'):
|
||||
self.setCursor(Qt.BlankCursor)
|
||||
|
||||
#self.retranslateUi(MainWindow)
|
||||
#self.tabWidget.setCurrentIndex(0)
|
||||
#QtCore.QMetaObject.connectSlotsByName(MainWindow)
|
||||
@@ -132,29 +189,30 @@ class MainWindow(QWidget):
|
||||
"""Resize the completion if related config options changed."""
|
||||
if section == 'completion' and option in ('height', 'shrink'):
|
||||
self.resize_completion()
|
||||
elif section == 'ui' and option == 'downloads-position':
|
||||
self._add_widgets()
|
||||
|
||||
@classmethod
|
||||
def spawn(cls, show=True):
|
||||
"""Create a new main window.
|
||||
def _add_widgets(self):
|
||||
"""Add or readd all widgets to the VBox."""
|
||||
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)
|
||||
elif position == 'south':
|
||||
self._vbox.addWidget(self.tabbed_browser)
|
||||
self._vbox.addWidget(self._downloadview)
|
||||
else:
|
||||
raise ValueError("Invalid position {}!".format(position))
|
||||
self._vbox.addWidget(self.status)
|
||||
|
||||
Args:
|
||||
show: Show the window after creating.
|
||||
|
||||
Return:
|
||||
The new window id.
|
||||
"""
|
||||
win_id = next(win_id_gen)
|
||||
win = MainWindow(win_id)
|
||||
if show:
|
||||
win.show()
|
||||
return win_id
|
||||
|
||||
def _load_geometry(self):
|
||||
def _load_state_geometry(self):
|
||||
"""Load the geometry from the state file."""
|
||||
state_config = objreg.get('state-config')
|
||||
try:
|
||||
data = state_config['geometry']['mainwindow']
|
||||
log.init.debug("Restoring mainwindow from {}".format(data))
|
||||
geom = base64.b64decode(data, validate=True)
|
||||
except KeyError:
|
||||
# First start
|
||||
@@ -163,14 +221,25 @@ class MainWindow(QWidget):
|
||||
log.init.exception("Error while reading geometry")
|
||||
self._set_default_geometry()
|
||||
else:
|
||||
try:
|
||||
ok = self.restoreGeometry(geom)
|
||||
except KeyError:
|
||||
log.init.exception("Error while restoring geometry.")
|
||||
self._set_default_geometry()
|
||||
if not ok:
|
||||
log.init.warning("Error while restoring geometry.")
|
||||
self._set_default_geometry()
|
||||
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.
|
||||
|
||||
If loading fails, loads default geometry.
|
||||
"""
|
||||
log.init.debug("Loading mainwindow from {}".format(geom))
|
||||
ok = self.restoreGeometry(geom)
|
||||
if not ok:
|
||||
log.init.warning("Error while loading geometry.")
|
||||
self._set_default_geometry()
|
||||
|
||||
def _connect_resize_completion(self):
|
||||
"""Connect the resize_completion signal and resize it once."""
|
||||
@@ -187,7 +256,7 @@ class MainWindow(QWidget):
|
||||
|
||||
def _connect_signals(self):
|
||||
"""Connect all mainwindow signals."""
|
||||
# pylint: disable=too-many-locals,too-many-statements
|
||||
# pylint: disable=too-many-statements
|
||||
key_config = objreg.get('key-config')
|
||||
|
||||
status = self._get_object('statusbar')
|
||||
@@ -195,14 +264,12 @@ class MainWindow(QWidget):
|
||||
completion_obj = self._get_object('completion')
|
||||
tabs = self._get_object('tabbed-browser')
|
||||
cmd = self._get_object('status-command')
|
||||
completer = self._get_object('completer')
|
||||
search_runner = self._get_object('search-runner')
|
||||
message_bridge = self._get_object('message-bridge')
|
||||
mode_manager = self._get_object('mode-manager')
|
||||
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
|
||||
@@ -215,10 +282,7 @@ class MainWindow(QWidget):
|
||||
keyparsers[usertypes.KeyMode.normal].keystring_updated.connect(
|
||||
status.keystring.setText)
|
||||
cmd.got_cmd.connect(self._commandrunner.run_safely)
|
||||
cmd.got_search.connect(search_runner.search)
|
||||
cmd.got_search_rev.connect(search_runner.search_rev)
|
||||
cmd.returnPressed.connect(tabs.on_cmd_return_pressed)
|
||||
search_runner.do_search.connect(tabs.search)
|
||||
tabs.got_cmd.connect(self._commandrunner.run_safely)
|
||||
|
||||
# config
|
||||
@@ -227,6 +291,7 @@ class MainWindow(QWidget):
|
||||
|
||||
# messages
|
||||
message_bridge.s_error.connect(status.disp_error)
|
||||
message_bridge.s_warning.connect(status.disp_warning)
|
||||
message_bridge.s_info.connect(status.disp_temp_text)
|
||||
message_bridge.s_set_text.connect(status.set_text)
|
||||
message_bridge.s_maybe_reset_text.connect(status.txt.maybe_reset_text)
|
||||
@@ -246,6 +311,8 @@ class MainWindow(QWidget):
|
||||
tabs.current_tab_changed.connect(status.percentage.on_tab_changed)
|
||||
tabs.cur_scroll_perc_changed.connect(status.percentage.set_perc)
|
||||
|
||||
tabs.tab_index_changed.connect(status.tabindex.on_tab_index_changed)
|
||||
|
||||
tabs.current_tab_changed.connect(status.txt.on_tab_changed)
|
||||
tabs.cur_statusbar_message.connect(status.txt.on_statusbar_message)
|
||||
tabs.cur_load_started.connect(status.txt.on_load_started)
|
||||
@@ -261,13 +328,13 @@ class MainWindow(QWidget):
|
||||
completion_obj.on_clear_completion_selection)
|
||||
cmd.hide_completion.connect(completion_obj.hide)
|
||||
|
||||
# quickmark completion
|
||||
quickmark_manager = objreg.get('quickmark-manager')
|
||||
quickmark_manager.changed.connect(completer.init_quickmark_completions)
|
||||
|
||||
@pyqtSlot()
|
||||
def resize_completion(self):
|
||||
"""Adjust completion according to config."""
|
||||
if not self._completion.isVisible():
|
||||
# It doesn't make sense to resize the completion as long as it's
|
||||
# not shown anyways.
|
||||
return
|
||||
# Get the configured height/percentage.
|
||||
confheight = str(config.get('completion', 'height'))
|
||||
if confheight.endswith('%'):
|
||||
@@ -291,6 +358,7 @@ class MainWindow(QWidget):
|
||||
topleft = QPoint(0, topleft_y)
|
||||
bottomright = self.status.geometry().topRight()
|
||||
rect = QRect(topleft, bottomright)
|
||||
log.misc.debug('completion rect: {}'.format(rect))
|
||||
if rect.isValid():
|
||||
self._completion.setGeometry(rect)
|
||||
|
||||
@@ -305,6 +373,14 @@ class MainWindow(QWidget):
|
||||
"""
|
||||
super().close()
|
||||
|
||||
@cmdutils.register(instance='main-window', scope='window')
|
||||
def fullscreen(self):
|
||||
"""Toggle fullscreen mode."""
|
||||
if self.isFullScreen():
|
||||
self.showNormal()
|
||||
else:
|
||||
self.showFullScreen()
|
||||
|
||||
def resizeEvent(self, e):
|
||||
"""Extend resizewindow's resizeEvent to adjust completion.
|
||||
|
||||
@@ -314,27 +390,42 @@ 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')
|
||||
count = self._tabbed_browser.count()
|
||||
if confirm_quit == 'never':
|
||||
tab_count = self.tabbed_browser.count()
|
||||
download_manager = objreg.get('download-manager', scope='window',
|
||||
window=self.win_id)
|
||||
download_count = download_manager.rowCount()
|
||||
quit_texts = []
|
||||
# Close if set to never ask for confirmation
|
||||
if 'never' in confirm_quit:
|
||||
pass
|
||||
elif confirm_quit == 'multiple-tabs' and count <= 1:
|
||||
pass
|
||||
else:
|
||||
text = "Close {} {}?".format(
|
||||
count, "tab" if count == 1 else "tabs")
|
||||
# Ask if multiple-tabs are open
|
||||
if 'multiple-tabs' in confirm_quit and tab_count > 1:
|
||||
quit_texts.append("{} {} open.".format(
|
||||
tab_count, "tab is" if tab_count == 1 else "tabs are"))
|
||||
# Ask if multiple downloads running
|
||||
if 'downloads' in confirm_quit and download_count > 0:
|
||||
quit_texts.append("{} {} running.".format(
|
||||
tab_count,
|
||||
"download is" if tab_count == 1 else "downloads are"))
|
||||
# Process all quit messages that user must confirm
|
||||
if quit_texts or 'always' in confirm_quit:
|
||||
text = '\n'.join(['Really quit?'] + quit_texts)
|
||||
confirmed = message.ask(self.win_id, text,
|
||||
usertypes.PromptMode.yesno, default=True)
|
||||
usertypes.PromptMode.yesno,
|
||||
default=True)
|
||||
# Stop asking if the user cancels
|
||||
if not confirmed:
|
||||
log.destroy.debug("Cancelling losing of window {}".format(
|
||||
log.destroy.debug("Cancelling closing of window {}".format(
|
||||
self.win_id))
|
||||
e.ignore()
|
||||
return
|
||||
e.accept()
|
||||
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()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user