Merge remote-tracking branch 'upstream/master' into perdomainstylesheets
This commit is contained in:
commit
b4dd94b6e9
@ -5,15 +5,15 @@ cache:
|
||||
build: off
|
||||
environment:
|
||||
PYTHONUNBUFFERED: 1
|
||||
PYTHON: C:\Python36\python.exe
|
||||
PYTHON: C:\Python36-x64\python.exe
|
||||
matrix:
|
||||
- TESTENV: py36-pyqt510
|
||||
- TESTENV: py36-pyqt511
|
||||
- TESTENV: pylint
|
||||
|
||||
install:
|
||||
- '%PYTHON% -m pip install -U pip'
|
||||
- '%PYTHON% -m pip install -r misc\requirements\requirements-tox.txt'
|
||||
- 'set PATH=%PATH%;C:\Python36'
|
||||
- 'set PATH=C:\Python36-x64;%PATH'
|
||||
|
||||
test_script:
|
||||
- '%PYTHON% -m tox -e %TESTENV%'
|
||||
|
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
||||
doc/changelog.asciidoc merge=union
|
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@ -1,5 +1,5 @@
|
||||
qutebrowser/browser/history.py @rcorre
|
||||
qutebrowser/completion/* @rcorre
|
||||
qutebrowser/completion/** @rcorre
|
||||
qutebrowser/misc/sql.py @rcorre
|
||||
tests/end2end/features/completion.feature @rcorre
|
||||
tests/end2end/features/test_completion_bdd.py @rcorre
|
||||
|
13
.github/CONTRIBUTING.asciidoc
vendored
13
.github/CONTRIBUTING.asciidoc
vendored
@ -1,3 +1,11 @@
|
||||
IMPORTANT: *Currently, bigger changes are going on in qutebrowser, as
|
||||
part of a
|
||||
https://lists.schokokeks.org/pipermail/qutebrowser-announce/2018-September/000051.html[student research project]
|
||||
about adding a plugin API to qutebrowser and moving a lot of code from the code
|
||||
into plugins.* Due to that, bandwidth for pull request review is currently
|
||||
very limited, and contributions might lead to merge conflicts due to
|
||||
ongoing refactorings.
|
||||
|
||||
- Before you start to work on something, please leave a comment on the relevant
|
||||
issue (or open one). This makes sure there is no duplicate work done.
|
||||
|
||||
@ -7,6 +15,5 @@
|
||||
- If you are stuck somewhere or have questions,
|
||||
https://github.com/qutebrowser/qutebrowser#getting-help[please ask]!
|
||||
|
||||
See the full contribution documentation for details and other useful hints:
|
||||
|
||||
include::../doc/contributing.asciidoc[]
|
||||
See the link:../doc/contributing.asciidoc[full contribution documentation] for
|
||||
details and other useful hints.
|
||||
|
2
.github/ISSUE_TEMPLATE.md
vendored
2
.github/ISSUE_TEMPLATE.md
vendored
@ -1,2 +0,0 @@
|
||||
<!-- If this is a bug report, please remember to mention your version info from
|
||||
`:open qute:version` or `qutebrowser --version` -->
|
15
.github/ISSUE_TEMPLATE/1_Bug_report.md
vendored
Normal file
15
.github/ISSUE_TEMPLATE/1_Bug_report.md
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
---
|
||||
name: 🐛 Bug Report
|
||||
about: Report errors and problems
|
||||
|
||||
---
|
||||
|
||||
**Version info (see `:version`)**:
|
||||
|
||||
**Does the bug happen if you start with `--temp-basedir`?** (if applicable):
|
||||
|
||||
**Description**
|
||||
|
||||
**How to reproduce**
|
||||
<!-- Link to the affected site, or steps to reproduce the issue
|
||||
(if possible/applicable). -->
|
5
.github/ISSUE_TEMPLATE/2_Feature_request.md
vendored
Normal file
5
.github/ISSUE_TEMPLATE/2_Feature_request.md
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
name: 🚀 Feature Request
|
||||
about: Ideas for new features and improvements
|
||||
|
||||
---
|
12
.github/ISSUE_TEMPLATE/3_Support_question.md
vendored
Normal file
12
.github/ISSUE_TEMPLATE/3_Support_question.md
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
---
|
||||
name: ❓ Support Question
|
||||
about: It's okay to ask questions via GitHub, but IRC/Reddit/Mailinglist might be better.
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
While it's fine to ask questions here, check the documentation for better
|
||||
ways to get help:
|
||||
|
||||
https://github.com/qutebrowser/qutebrowser#getting-help
|
||||
-->
|
11
.github/ISSUE_TEMPLATE/4_Security_issue.md
vendored
Normal file
11
.github/ISSUE_TEMPLATE/4_Security_issue.md
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
---
|
||||
name: ⛔ Security Issue
|
||||
about: Contact mail@qutebrowser.org for security issues.
|
||||
|
||||
---
|
||||
|
||||
⚠ PLEASE DON'T DISCLOSE SECURITY-RELATED ISSUES PUBLICLY, SEE BELOW.
|
||||
|
||||
If you have found a security issue in qutebrowser, please send the details to
|
||||
mail [at] qutebrowser.org and don't disclose it publicly until we can provide a
|
||||
fix for it
|
BIN
.github/img/macstadium.png
vendored
Normal file
BIN
.github/img/macstadium.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 54 KiB |
1
.gitignore
vendored
1
.gitignore
vendored
@ -16,7 +16,6 @@ __pycache__
|
||||
/doc/*.html
|
||||
/README.html
|
||||
/qutebrowser/html/doc/
|
||||
/qutebrowser/html/*.html
|
||||
/.venv*
|
||||
/.coverage
|
||||
/htmlcov
|
||||
|
@ -54,7 +54,7 @@ no-docstring-rgx=(^_|^main$)
|
||||
|
||||
[FORMAT]
|
||||
max-line-length=79
|
||||
ignore-long-lines=(<?https?://|^# Copyright 201\d)
|
||||
ignore-long-lines=(<?https?://|^# Copyright 201\d|link:)
|
||||
expected-line-ending-format=LF
|
||||
|
||||
[VARIABLES]
|
||||
|
27
.travis.yml
27
.travis.yml
@ -20,18 +20,21 @@ matrix:
|
||||
- os: linux
|
||||
env: TESTENV=py36-pyqt59
|
||||
- os: linux
|
||||
env: TESTENV=py36-pyqt510-cov
|
||||
# We need a newer Xvfb as a WORKAROUND for:
|
||||
# https://bugreports.qt.io/browse/QTBUG-64928
|
||||
sudo: required
|
||||
env: TESTENV=py36-pyqt510
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- sourceline: "deb http://us.archive.ubuntu.com/ubuntu/ xenial main universe"
|
||||
packages:
|
||||
- xvfb
|
||||
apt:
|
||||
packages:
|
||||
- xfonts-base
|
||||
- os: linux
|
||||
env: TESTENV=py36-pyqt511-cov
|
||||
# https://github.com/travis-ci/travis-ci/issues/9069
|
||||
- os: linux
|
||||
python: 3.7
|
||||
sudo: required
|
||||
dist: xenial
|
||||
env: TESTENV=py37-pyqt511
|
||||
- os: osx
|
||||
env: TESTENV=py36 OSX=sierra
|
||||
env: TESTENV=py37 OSX=sierra
|
||||
osx_image: xcode9.2
|
||||
language: generic
|
||||
# https://github.com/qutebrowser/qutebrowser/issues/2013
|
||||
@ -66,6 +69,10 @@ matrix:
|
||||
env: TESTENV=shellcheck
|
||||
services: docker
|
||||
fast_finish: true
|
||||
allow_failures:
|
||||
# https://github.com/qutebrowser/qutebrowser/issues/4055
|
||||
- os: linux
|
||||
env: TESTENV=py36-pyqt510
|
||||
|
||||
cache:
|
||||
directories:
|
||||
|
@ -59,7 +59,7 @@ Getting help
|
||||
|
||||
You can get help in the IRC channel
|
||||
irc://irc.freenode.org/#qutebrowser[`#qutebrowser`] on
|
||||
http://freenode.net/[Freenode]
|
||||
https://freenode.net/[Freenode]
|
||||
(https://webchat.freenode.net/?channels=#qutebrowser[webchat]), or by writing a
|
||||
message to the
|
||||
https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser[mailinglist] at
|
||||
@ -96,24 +96,25 @@ Requirements
|
||||
|
||||
The following software and libraries are required to run qutebrowser:
|
||||
|
||||
* http://www.python.org/[Python] 3.5 or newer (3.6 recommended)
|
||||
* http://qt.io/[Qt] 5.7.1 or newer (5.10 recommended) with the following modules:
|
||||
* https://www.python.org/[Python] 3.5 or newer (3.6 recommended)
|
||||
* https://www.qt.io/[Qt] 5.7.1 or newer (5.11 recommended, support for < 5.9
|
||||
will be dropped soon) with the following modules:
|
||||
- QtCore / qtbase
|
||||
- QtQuick (part of qtbase in some distributions)
|
||||
- QtSQL (part of qtbase in some distributions)
|
||||
- QtOpenGL
|
||||
- QtWebEngine, or
|
||||
- QtWebKit - only the
|
||||
link:https://github.com/annulen/webkit/wiki[updated fork] (5.212) is
|
||||
supported
|
||||
* http://www.riverbankcomputing.com/software/pyqt/intro[PyQt] 5.7.0 or newer
|
||||
(5.10 recommended) for Python 3
|
||||
- alternatively QtWebKit - support for QtWebKit will be dropped soon, and
|
||||
only the link:https://github.com/annulen/webkit/wiki[updated fork] (5.212)
|
||||
is supported
|
||||
* https://www.riverbankcomputing.com/software/pyqt/intro[PyQt] 5.7.0 or newer
|
||||
(5.11 recommended, support for < 5.9 will be dropped soon) for Python 3
|
||||
* https://pypi.python.org/pypi/setuptools/[pkg_resources/setuptools]
|
||||
* http://fdik.org/pyPEG/[pyPEG2]
|
||||
* https://fdik.org/pyPEG/[pyPEG2]
|
||||
* http://jinja.pocoo.org/[jinja2]
|
||||
* http://pygments.org/[pygments]
|
||||
* https://github.com/yaml/pyyaml[PyYAML]
|
||||
* http://www.attrs.org/[attrs]
|
||||
* https://www.attrs.org/[attrs]
|
||||
|
||||
The following libraries are optional:
|
||||
|
||||
@ -143,6 +144,18 @@ get in touch!
|
||||
* PayPal: me@the-compiler.org
|
||||
* Bitcoin: link:bitcoin:1PMzbcetAHfpxoXww8Bj5XqquHtVvMjJtE[1PMzbcetAHfpxoXww8Bj5XqquHtVvMjJtE]
|
||||
|
||||
Sponsors
|
||||
--------
|
||||
|
||||
Thanks a lot to https://www.macstadium.com/[MacStadium] for supporting
|
||||
qutebrowser with a free hosted Mac Mini via their
|
||||
https://www.macstadium.com/opensource[Open Source Project].
|
||||
|
||||
(They don't require including this here - I've just been very happy with their
|
||||
offer, and without them, no macOS releases or tests would exist)
|
||||
|
||||
image:.github/img/macstadium.png["powered by MacStadium",width=200,link="https://www.macstadium.com/"]
|
||||
|
||||
Authors
|
||||
-------
|
||||
|
||||
@ -152,7 +165,7 @@ https://github.com/qutebrowser/qutebrowser/graphs/contributors[hundreds of contr
|
||||
|
||||
Additionally, the following people have contributed graphics:
|
||||
|
||||
* Jad/link:http://yelostudio.com[yelo] (new icon)
|
||||
* Jad/link:https://yelostudio.com[yelo] (new icon)
|
||||
* WOFall (original icon)
|
||||
* regines (key binding cheatsheet)
|
||||
|
||||
@ -170,18 +183,16 @@ Active
|
||||
|
||||
* https://fanglingsu.github.io/vimb/[vimb] (C, GTK+ with WebKit2)
|
||||
* https://luakit.github.io/luakit/[luakit] (C/Lua, GTK+ with WebKit2)
|
||||
* http://surf.suckless.org/[surf] (C, GTK+ with WebKit1/WebKit2)
|
||||
* http://www.uzbl.org/[uzbl] (C, GTK+ with WebKit1/WebKit2)
|
||||
* https://surf.suckless.org/[surf] (C, GTK+ with WebKit1/WebKit2)
|
||||
* https://github.com/next-browser/next/[next] (Lisp, Emacs-like, GTK+ with WebKit)
|
||||
* https://github.com/parkouss/webmacs/[webmacs] (Python, Emacs-like with QtWebEngine)
|
||||
* Chrome/Chromium addons:
|
||||
https://github.com/1995eaton/chromium-vim[cVim],
|
||||
http://vimium.github.io/[Vimium],
|
||||
https://vimium.github.io/[Vimium],
|
||||
https://github.com/brookhong/Surfingkeys[Surfingkeys],
|
||||
https://key.saka.io/[Saka Key]
|
||||
* Firefox addons (based on WebExtensions):
|
||||
https://addons.mozilla.org/en-GB/firefox/addon/vimium-ff/[Vimium-FF] (experimental),
|
||||
https://key.saka.io[Saka Key],
|
||||
https://github.com/ueokande/vim-vixen[Vim Vixen],
|
||||
https://github.com/shinglyu/QuantumVim[QuantumVim],
|
||||
https://github.com/amedama41/vvimpulation[VVimpulation],
|
||||
https://github.com/cmcaine/tridactyl[Tridactyl] (working
|
||||
on a https://bugzilla.mozilla.org/show_bug.cgi?id=1215061[better API] for
|
||||
keyboard integration in Firefox).
|
||||
@ -192,17 +203,23 @@ Inactive
|
||||
* https://bitbucket.org/portix/dwb[dwb] (C, GTK+ with WebKit1,
|
||||
https://bitbucket.org/portix/dwb/pull-requests/22/several-cleanups-to-increase-portability/diff[unmaintained] -
|
||||
main inspiration for qutebrowser)
|
||||
* http://sourceforge.net/p/vimprobable/wiki/Home/[vimprobable] (C, GTK+ with
|
||||
* https://sourceforge.net/p/vimprobable/wiki/Home/[vimprobable] (C, GTK+ with
|
||||
WebKit1)
|
||||
* http://pwmt.org/projects/jumanji/[jumanji] (C, GTK+ with WebKit1)
|
||||
* https://wiki.archlinux.org/index.php?title=Jumanji[jumanji] (C, GTK+ with WebKit1,
|
||||
original site is gone but Arch Linux has some data)
|
||||
* http://conkeror.org/[conkeror] (Javascript, Emacs-like, XULRunner/Gecko)
|
||||
* https://www.uzbl.org/[uzbl] (C, GTK+ with WebKit1/WebKit2)
|
||||
* Firefox addons (not based on WebExtensions or no recent activity):
|
||||
http://www.vimperator.org/[Vimperator],
|
||||
http://5digits.org/pentadactyl/[Pentadactyl],
|
||||
http://bug.5digits.org/pentadactyl/index[Pentadactyl],
|
||||
https://github.com/akhodakivskiy/VimFx[VimFx],
|
||||
https://key.saka.io[Saka Key],
|
||||
https://github.com/shinglyu/QuantumVim[QuantumVim],
|
||||
* Chrome/Chromium addons:
|
||||
https://chrome.google.com/webstore/detail/vichrome/gghkfhpblkcmlkmpcpgaajbbiikbhpdi?hl=en[ViChrome],
|
||||
https://github.com/jinzhu/vrome[Vrome]
|
||||
https://key.saka.io[Saka Key],
|
||||
https://github.com/1995eaton/chromium-vim[cVim],
|
||||
|
||||
License
|
||||
-------
|
||||
@ -229,4 +246,4 @@ display PDF files in the browser. Windows releases come with a bundled pdf.js.
|
||||
pdf.js is distributed under the terms of the Apache License. You can
|
||||
find a copy of the license in `qutebrowser/3rdparty/pdfjs/LICENSE` (in the
|
||||
Windows release or after running `scripts/dev/update_3rdparty.py`), or online
|
||||
http://www.apache.org/licenses/LICENSE-2.0.html[here].
|
||||
https://www.apache.org/licenses/LICENSE-2.0.html[here].
|
||||
|
@ -15,12 +15,219 @@ breaking changes (such as renamed commands) can happen in minor releases.
|
||||
// `Fixed` for any bug fixes.
|
||||
// `Security` to invite users to upgrade in case of vulnerabilities.
|
||||
|
||||
v1.4.0 (unreleased)
|
||||
v1.6.0 (unreleased)
|
||||
-------------------
|
||||
|
||||
Added
|
||||
~~~~~
|
||||
|
||||
- New `tabs.new_position.stacking` setting which controls whether new tabs
|
||||
opened from a page should stack on each other or not.
|
||||
- New `completion.open_categories` setting which allows to configure which
|
||||
categories are shown in the `:open` completion, and how they are ordered.
|
||||
- New config manipulation commands:
|
||||
* `:config-dict-add` and `:config-list-add` to a new element to a dict/list
|
||||
setting.
|
||||
* `:config-dict-remove` and `:config-list-remove` to remove an element from a
|
||||
dict/list setting.
|
||||
- New `hints.selectors` setting which allows to configure what CSS selectors
|
||||
are used for hints, and also allows adding custom hint groups.
|
||||
|
||||
Changed
|
||||
~~~~~~~
|
||||
|
||||
- `:q` now closes current window instead of quitting qutebrowser completely
|
||||
(`:close`), while `:qa` quits (`:quit`). The behavior of `:wq` remains
|
||||
unchanged (`:quit --save`), as closing a window while saving the session
|
||||
doesn't make sense.
|
||||
- Completion highlighting is now done differently (using QSyntaxHighlither),
|
||||
which should fix some highlighting corner-cases.
|
||||
- The `QtColor` config type now also understands colors like `rgb(...)`.
|
||||
- `:yank` now has a `--quiet` option which causes it to not display a message.
|
||||
- The `:open` completion now also shows search engines by default.
|
||||
- The `content.host_blocking.enabled` setting now supports URL patterns, so the
|
||||
adblocker can be disabled on a given page.
|
||||
- Elements with a `tabindex` attribute now also get hints by default.
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
|
||||
- Invalid world IDs now get rejected for `:jseval` and GreaseMonkey scripts.
|
||||
- When websites suggest download filenames with invalid characters, those are
|
||||
now correctly replaced.
|
||||
|
||||
v1.5.1
|
||||
------
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
|
||||
- Flickering when opening/closing tabs (as soon as more than 10 are open) on
|
||||
some pages.
|
||||
- PDF.js is now bundled again with the macOS/Windows release.
|
||||
- PDF.js is now searched in the correct path (if not installed system-wide)
|
||||
instead of hardcoding `~/.local/share/qutebrowser`.
|
||||
- Improved logging for PDF.js resources which fail to load.
|
||||
- Crash when closing a tab after doing a search.
|
||||
- Tabs appearing when hidden after e.g. closing tabs.
|
||||
|
||||
v1.5.0
|
||||
------
|
||||
|
||||
Added
|
||||
~~~~~
|
||||
|
||||
- Rewritten PDF.js support:
|
||||
* PDF.js support and the `content.pdfjs` setting are now also available with
|
||||
QtWebEngine.
|
||||
* Opening a PDF file now doesn't start a second request anymore.
|
||||
* Opening PDFs on https:// sites now works properly.
|
||||
* New `--pdfjs` flag for `prompt-open-download`, so PDFs can be opened in
|
||||
PDF.js with `<Ctrl-P>` in the download prompt.
|
||||
- New settings:
|
||||
* `content.mouse_lock` to handle HTML5 pointer locking.
|
||||
* `completion.web_history.exclude` which hides a list of URL patterns from
|
||||
the completion.
|
||||
* `qt.process_model` which can be used to change Chromium's process model.
|
||||
* `qt.low_end_device_mode` which turns on Chromium's low-end device mode.
|
||||
This mode uses less RAM, but the expense of performance.
|
||||
* `content.webrtc_ip_handling_policy`, which allows more
|
||||
fine-grained/restrictive control about which IPs are exposed via WebRTC.
|
||||
* `tabs.max_width` which allows to have a more "normal" look for tabs.
|
||||
* `content.mute` which allows to mute pages (or all tabs) by default.
|
||||
- Running qutebrowser with QtWebKit or Qt < 5.9 now shows a warning (only
|
||||
once), as support for those is going to be removed in a future release.
|
||||
- New t[iI][hHu] default bindings (similar to `tsh` etc.) to toggle images.
|
||||
- The qute-pass userscript now has optional OTP support.
|
||||
- When `:spawn --userscript` is called with a count, that count is now
|
||||
passed to userscripts as `$QUTE_COUNT`.
|
||||
|
||||
Changed
|
||||
~~~~~~~
|
||||
|
||||
- Windows and macOS releases now bundle Python 3.7, PyQt 5.11.3 and Qt 5.11.2.
|
||||
QtWebEngine includes security fixes up to Chromium 68.0.3440.75 and
|
||||
http://code.qt.io/cgit/qt/qtwebengine.git/tree/dist/changes-5.11.2/?h=v5.11.2[various other fixes].
|
||||
- Various performance improvements when many tabs are opened.
|
||||
- The `content.headers.referer` setting now works on QtWebEngine.
|
||||
- The `:repeat` command now takes a count which is multiplied with the given
|
||||
"times" argument.
|
||||
- The default keybinding to leave passthrough mode was changed from `<Ctrl-V>`
|
||||
to `<Shift-Escape>`, which makes pasting from the clipboard easier in
|
||||
passthrough mode and is also unlikely to conflict with webpage bindings.
|
||||
- The `app_id` is now set to `qutebrowser` for Wayland.
|
||||
- `Command` or `Cmd` can now be used (instead of `Meta`) to map the Command key
|
||||
on macOS.
|
||||
- Using `:set option` now shows the value of the setting (like `:set option?`
|
||||
already did).
|
||||
- The `completion.web_history_max_items` setting got renamed to
|
||||
`completion.web_history.max_items`.
|
||||
- The Makefile shipped with qutebrowser now supports overriding variables
|
||||
`DATADIR` and `MANDIR`.
|
||||
- Regenerating completion history now shows a progress dialog.
|
||||
- The `content.autoplay` setting now supports URL patterns on Qt >= 5.11.
|
||||
- The `content.host_blocking.whitelist` setting now takes a list of URL
|
||||
patterns instead of globs.
|
||||
- In passthrough mode, Ctrl + Mousewheel now also gets passed through to the
|
||||
page instead of zooming.
|
||||
- Editing text in an external editor now simulates a JS "input" event, which
|
||||
improves compatibility with websites reacting via JS to input.
|
||||
- The `qute://settings` page is now properly sorted on Python 3.5.
|
||||
- `:zoom`, `:zoom-in` and `:zoom-out` now have a `--quiet` switch which causes
|
||||
them to not display a message.
|
||||
- The `scrolling.bar` setting now takes three values instead of being a
|
||||
boolean: `always`, `never`, and `when-searching` (which only displays it
|
||||
while a search is active).
|
||||
- '@@' now repeats the last run macro.
|
||||
- The `content.host_blocking.lists` setting now accepts a `file://` URL to a
|
||||
directory, and reads all files in that directory.
|
||||
- The `:tab-give` and `:tab-take` command now have a new flag `--keep` which
|
||||
causes them to keep the old tab around.
|
||||
- `:navigate` now clears the URL query.
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
|
||||
- `qute://` pages now work properly on Qt 5.11.2
|
||||
- Error when passing a substring with spaces to `:tab-take`.
|
||||
- Greasemonkey scripts which start with an UTF-8 BOM are now handled correctly.
|
||||
- When no documentation has been generated, the plaintext documentation now can
|
||||
be shown for more files such as `qute://help/userscripts.html`.
|
||||
- Crash when doing initial run on Wayland without XWayland.
|
||||
- Crash when trying to load an empty session file.
|
||||
- `:hint` with an invalid `--mode=` value now shows a proper error.
|
||||
- Rare crash on Qt 5.11.2 when clicking on `<select>` elements.
|
||||
- Rare crash related to the completion.
|
||||
|
||||
Removed
|
||||
~~~~~~~
|
||||
|
||||
- Support for importing pre-v1.0.0 history files has been removed.
|
||||
- The `content.webrtc_public_interfaces_only` setting has been removed and
|
||||
replaced by `content.webrtc_ip_handling_policy`.
|
||||
|
||||
v1.4.2
|
||||
------
|
||||
|
||||
Changed
|
||||
~~~~~~~
|
||||
|
||||
- The `content.xss_auditing` setting is now enabled by default, to mirror
|
||||
Chromium's rather than Qt's default behavior.
|
||||
- Long URLs in the statusbar are now elided at the end rather than in the
|
||||
middle, to make sure the hostname is completely visible whenever possible.
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
|
||||
- Crash in Qt 5.7.1 when a website uses `window.print()`.
|
||||
- The workaround for Nouveau graphic drivers now works properly again.
|
||||
- Crash when using `:follow-selected` with a link which is outside of the view.
|
||||
- Workaround for windows not showing as urgent with some window managers
|
||||
(like i3).
|
||||
- Crash when opening URLs with some unicode characters (IDNA 2008). Those URLs
|
||||
still won't open though, due to missing support in Qt.
|
||||
- Crash when a download directory which can't be created is configured.
|
||||
- Crash in the `importer.py` script when importing Chrome bookmarks from newer Chrome versions.
|
||||
- The `content.webrtc_public_interfaces_only` option didn't work on Qt 5.11 previously (it now does).
|
||||
Note it still does not work on Qt 5.10 (due to a Qt bug) and Qt < 5.9.2.
|
||||
- Repeated escaping of entries in `qute://log` when refreshing page.
|
||||
- The host blocker doesn't block 0.0.0.0 anymore.
|
||||
- Crash when using :// as URL pattern.
|
||||
- The `:buffer` completion now sorts tabs with indices >= 10 correctly again.
|
||||
|
||||
v1.4.1
|
||||
------
|
||||
|
||||
Security
|
||||
~~~~~~~~
|
||||
|
||||
- CVE-2018-10895: Fix CSRF issue on the qute://settings page, leading to
|
||||
possible arbitrary code execution. See the related GitHub issue for details:
|
||||
https://github.com/qutebrowser/qutebrowser/issues/4060
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
|
||||
- Rare crash when an error occurs in downloads.
|
||||
- Newlines are now stripped from the :version pastebin URL.
|
||||
- There's a new `mkvenv-pypi-old` environment in `tox.ini` which installs an
|
||||
older Qt, which is needed on Ubuntu 16.04.
|
||||
- Worked around a Qt issue which redirects to a `chrome-error://` page when
|
||||
trying to use U2F.
|
||||
- The `link_pyqt.py` script now works correctly with PyQt 5.11.
|
||||
- The Windows installer now uninstalls the old version before installing the
|
||||
new one, fixing issues with qutebrowser not starting after installing v1.4.0
|
||||
over v1.3.3.
|
||||
|
||||
v1.4.0
|
||||
------
|
||||
|
||||
Added
|
||||
~~~~~
|
||||
|
||||
- Support for the bundled `sip` module in PyQt 5.11 and other changes in
|
||||
Qt/PyQt 5.11.x.
|
||||
- New `--debug-flag log-requests` to log requests to the debug log for
|
||||
debugging.
|
||||
- New `--first` flag for `:hint` (bound to `gi` for inputs) which automatically
|
||||
@ -40,11 +247,14 @@ Added
|
||||
* Support for requesting persistent storage via
|
||||
`navigator.webkitPersistentStorage.requestQuota` with a new
|
||||
`content.persistent_storage` setting (requires Qt 5.11).
|
||||
This setting also supports URL patterns.
|
||||
* Support for registering custom protocol handlers via
|
||||
`navigator.registerProtocolHandler` with a new
|
||||
`content.register_protocol_handler` setting (requires Qt 5.11).
|
||||
This setting also supports URL patterns.
|
||||
* Support for WebRTC screen sharing with a new `content.desktop_capture`
|
||||
setting (requires Qt 5.10).
|
||||
This setting also supports URL patterns.
|
||||
* New `content.autoplay` setting to enable/disable automatic video playback
|
||||
(requires Qt 5.10).
|
||||
* New `content.webrtc_public_interfaces_only` setting to only expose public
|
||||
@ -55,8 +265,17 @@ Added
|
||||
Changed
|
||||
~~~~~~~
|
||||
|
||||
- The following settings now support URL patterns:
|
||||
* `content.headers.do_not_track`
|
||||
* `content.headers.custom`
|
||||
* `content.headers.accept_language`
|
||||
* `content.headers.user_agent`
|
||||
* `content.ssl_strict`
|
||||
* `content.geolocation`
|
||||
* `content.notifications`
|
||||
* `content.media_capture`
|
||||
- The Windows/macOS releases now bundle Qt 5.11.1 which is based on
|
||||
Chromium 65.0.3325.151 with security fixes up to Chromium 67.0.3396.79.
|
||||
Chromium 65.0.3325.151 with security fixes up to Chromium 67.0.3396.87.
|
||||
- New short flags for commandline arguments: `-B` and `-T` for `--basedir` and
|
||||
`--temp-basedir`; `-d` and `-D` for `--debug` and `--debug-flag`.
|
||||
- Deleting history items via `:history-clear` or `:completion-item-del` now
|
||||
@ -85,21 +304,60 @@ Changed
|
||||
- Improved error messages when a setting needs a newer Qt version.
|
||||
- QtWebEngine: Various improvements to make the cursor more visible in caret
|
||||
browsing.
|
||||
- When a prompt is opened in insert/passthrough mode, the mode is restored
|
||||
after closing the prompt.
|
||||
- On Qt 5.10 or newer, dictionaries are now read from the qutebrowser data
|
||||
directory (e.g. `~/.local/share/qutebrowser`) instead of `/usr/share/qt`.
|
||||
Existing dictionaries are copied over.
|
||||
- If an error while parsing `~/.netrc` occurs, the cause of the error is now
|
||||
logged.
|
||||
- On Qt 5.9 or newer, certificate errors now show Chromium's detailed error
|
||||
page.
|
||||
- Greasemonkey scripts now support a "@qute-js-world" tag to run them in a
|
||||
different JavaScript context.
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
|
||||
- Various subtle keyboard focus issues.
|
||||
- The security fix in v1.3.3 caused URLs with ampersands
|
||||
(`www.example.com?one=1&two=2`) to send the wrong arguments when clicked on
|
||||
the `qute://history` page.
|
||||
- Crash when opening a PDF page with PDF.js enabled (on QtWebKit), but no
|
||||
PDF.js installed.
|
||||
- Crash when closing a tab shortly after opening it.
|
||||
|
||||
Removed
|
||||
~~~~~~~
|
||||
|
||||
- No prebuilt binaries for 32-bit Windows are supplied anymore. This is due to
|
||||
Qt removing QtWebEngine support for those upstream. It might be possible to
|
||||
distribute 32-bit binaries again with Qt 5.12 in December, but that will only
|
||||
happen if it turns out enough people actually need 32-bit support.
|
||||
- `:tab-detach` which has been deprecated in v1.1.0 has been removed.
|
||||
- The `content.developer_extras` setting got removed. On QtWebKit, developer
|
||||
extras are now automatically enabled when opening the inspector.
|
||||
|
||||
v1.3.3 (unreleased)
|
||||
-------------------
|
||||
v1.3.3
|
||||
------
|
||||
|
||||
Security
|
||||
~~~~~~~~
|
||||
|
||||
- An XSS vulnerability on the `qute://history` page allowed websites to inject
|
||||
HTML into the page via a crafted title tag. This could allow them to steal
|
||||
your browsing history. If you're currently unable to upgrade, avoid using
|
||||
`:history`. A CVE request for this issue is pending, see
|
||||
https://github.com/qutebrowser/qutebrowser/issues/4011[#4011] for updates.
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
|
||||
- Crash in a workaround for a Qt 5.11 bug in rare circumstances.
|
||||
- Workaround for a Qt bug which preserves searches between page loads.
|
||||
- In v1.3.2 a dependency on the `PyQt5.QtQuickWidgets` module was accidentally
|
||||
introduced. Since that module isn't packaged everywhere, it's been removed
|
||||
again.
|
||||
|
||||
v1.3.2
|
||||
------
|
||||
@ -1479,7 +1737,7 @@ Changed
|
||||
`tabs.bg/fg.selected.odd/even`.
|
||||
- `:spawn --userscript` and `:hint` with the `userscript` target now look up
|
||||
relative paths in `~/.local/share/qutebrowser/userscripts` or
|
||||
`$XDG_DATA_DIR`. Using a binary in `$PATH` won't work anymore with
|
||||
`$XDG_DATA_HOME`. Using a binary in `$PATH` won't work anymore with
|
||||
`--userscript`.
|
||||
- New design for error pages
|
||||
- Link filtering for hints now checks if the text is contained anywhere in
|
||||
|
@ -5,6 +5,14 @@ The Compiler <mail@qutebrowser.org>
|
||||
:data-uri:
|
||||
:toc:
|
||||
|
||||
IMPORTANT: *Currently, bigger changes are going on in qutebrowser, as
|
||||
part of a
|
||||
https://lists.schokokeks.org/pipermail/qutebrowser-announce/2018-September/000051.html[student research project]
|
||||
about adding a plugin API to qutebrowser and moving a lot of code from the code
|
||||
into plugins.* Due to that, bandwidth for pull request review is currently
|
||||
very limited, and contributions might lead to merge conflicts due to
|
||||
ongoing refactorings.
|
||||
|
||||
I `<3` footnote:[Of course, that says `<3` in HTML.] contributors!
|
||||
|
||||
This document contains guidelines for contributing to qutebrowser, as well as
|
||||
@ -88,7 +96,7 @@ git format-patch origin/master <1>
|
||||
Running qutebrowser
|
||||
-------------------
|
||||
|
||||
After link:install.asciidoc#tox[installing qutebrowser via tox], you can run
|
||||
After link:install{outfilesuffix}#tox[installing qutebrowser via tox], you can run
|
||||
`.venv/bin/qutebrowser --debug --temp-basedir` to test your changes with debug
|
||||
logging enabled and without affecting existing running instances.
|
||||
|
||||
@ -689,8 +697,6 @@ New PyQt release
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
* See above.
|
||||
* Install new PyQt in Windows VM (32- and 64-bit).
|
||||
* Download new installer and update PyQt installer path in `ci_install.py`.
|
||||
* Update `tox.ini`/`.travis.yml`/`.appveyor.yml` to test new versions.
|
||||
|
||||
qutebrowser release
|
||||
@ -712,7 +718,7 @@ qutebrowser release
|
||||
as closed.
|
||||
|
||||
* Linux: Run `git checkout v1.$x.$y && ./.venv/bin/python3 scripts/dev/build_release.py --upload v1.$x.$y`.
|
||||
* Windows: Run `git checkout v1.X.Y; C:\Python36-32\python scripts\dev\build_release.py --asciidoc C:\Python27\python C:\asciidoc-8.6.9\asciidoc.py --upload v1.X.Y` (replace X/Y by hand).
|
||||
* Windows: Run `git checkout v1.X.Y; py -3 scripts\dev\build_release.py --asciidoc C:\Python27\python %userprofile%\bin\asciidoc-8.6.10\asciidoc.py --upload v1.X.Y` (replace X/Y by hand).
|
||||
* macOS: Run `git checkout v1.X.Y && python3 scripts/dev/build_release.py --upload v1.X.Y` (replace X/Y by hand).
|
||||
* On server:
|
||||
- Run `python3 scripts/dev/download_release.py v1.X.Y` (replace X/Y by hand).
|
||||
|
@ -5,10 +5,10 @@ The Compiler <mail@qutebrowser.org>
|
||||
|
||||
[qanda]
|
||||
What is qutebrowser based on?::
|
||||
qutebrowser uses http://www.python.org/[Python], http://qt.io/[Qt] and
|
||||
http://www.riverbankcomputing.com/software/pyqt/intro[PyQt].
|
||||
qutebrowser uses https://www.python.org/[Python], https://www.qt.io/[Qt] and
|
||||
https://www.riverbankcomputing.com/software/pyqt/intro[PyQt].
|
||||
+
|
||||
The concept of it is largely inspired by http://portix.bitbucket.org/dwb/[dwb]
|
||||
The concept of it is largely inspired by https://bitbucket.org/portix/dwb/[dwb]
|
||||
and http://www.vimperator.org/vimperator[Vimperator]. Many actions and
|
||||
key bindings are similar to dwb.
|
||||
|
||||
@ -16,34 +16,34 @@ 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.io/[Qt]/link:http://www.riverbankcomputing.com/software/pyqt/intro[PyQt].
|
||||
link:https://www.qt.io/[Qt]/link:https://www.riverbankcomputing.com/software/pyqt/intro[PyQt].
|
||||
+
|
||||
Read the next few questions to find out why I was unhappy with existing
|
||||
software.
|
||||
|
||||
What's wrong with link:http://portix.bitbucket.org/dwb/[dwb]/link:http://sourceforge.net/projects/vimprobable/[vimprobable]/link:https://mason-larobina.github.io/luakit/[luakit]/link:http://pwmt.org/projects/jumanji/[jumanji]/... (projects based on WebKitGTK)?::
|
||||
Most of them are based on the http://webkitgtk.org/[WebKitGTK+]
|
||||
http://webkitgtk.org/reference/webkitgtk/stable/index.html[WebKit1] API,
|
||||
What's wrong with link:https://bitbucket.org/portix/dwb/[dwb]/link:https://sourceforge.net/projects/vimprobable/[vimprobable]/link:https://mason-larobina.github.io/luakit/[luakit]/jumanji/... (projects based on WebKitGTK)?::
|
||||
Most of them are based on the https://webkitgtk.org/[WebKitGTK+]
|
||||
https://webkitgtk.org/reference/webkitgtk/stable/index.html[WebKit1] API,
|
||||
which causes a lot of crashes. As the GTK API using WebKit1 is
|
||||
https://lists.webkit.org/pipermail/webkit-gtk/2014-March/001821.html[deprecated],
|
||||
these bugs are never going to be fixed.
|
||||
+
|
||||
When qutebrowser was created, the newer
|
||||
http://webkitgtk.org/reference/webkit2gtk/stable/index.html[WebKit2 API] lacked
|
||||
https://webkitgtk.org/reference/webkit2gtk/stable/index.html[WebKit2 API] lacked
|
||||
basic features like proxy support, and almost no projects have started porting
|
||||
to WebKit2. In the meantime, this situation has improved a bit, but there are
|
||||
still only a few projects which have some kind of WebKit2 support (see the
|
||||
https://github.com/qutebrowser/qutebrowser#similar-projects[list of
|
||||
alternatives]).
|
||||
+
|
||||
qutebrowser uses http://qt.io/[Qt] and
|
||||
http://wiki.qt.io/QtWebEngine[QtWebEngine] by default (and supports
|
||||
http://wiki.qt.io/QtWebKit[QtWebKit] optionally). QtWebEngine is based on
|
||||
qutebrowser uses https://www.qt.io/[Qt] and
|
||||
https://wiki.qt.io/QtWebEngine[QtWebEngine] by default (and supports
|
||||
https://wiki.qt.io/QtWebKit[QtWebKit] optionally). QtWebEngine is based on
|
||||
Google's https://www.chromium.org/Home[Chromium]. With an up-to-date Qt, it has
|
||||
much more man-power behind it than WebKitGTK+ has, and thus supports more modern
|
||||
web features - it's also arguably more secure.
|
||||
|
||||
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]?::
|
||||
What's wrong with https://www.mozilla.org/en-US/firefox/new/[Firefox] and link:http://bug.5digits.org/pentadactyl/[Pentadactyl]/link:http://www.vimperator.org/vimperator[Vimperator]?::
|
||||
Firefox likes to break compatibility with addons on each upgrade, gets
|
||||
slower and more bloated with every upgrade, and has some
|
||||
https://blog.mozilla.org/advancingcontent/2014/02/11/publisher-transformation-with-users-at-the-center/[horrible
|
||||
@ -51,20 +51,20 @@ What's wrong with https://www.mozilla.org/en-US/firefox/new/[Firefox] and link:h
|
||||
+
|
||||
Also, developing addons for it is a nightmare.
|
||||
|
||||
What's wrong with http://www.chromium.org/Home[Chromium] and https://vimium.github.io/[Vimium]?::
|
||||
What's wrong with https://www.chromium.org/Home[Chromium] and https://vimium.github.io/[Vimium]?::
|
||||
The Chrome plugin API doesn't seem to allow much freedom for plugin
|
||||
writers, which results in Vimium not really having all the features you'd
|
||||
expect from a proper minimal, vim-like browser.
|
||||
|
||||
Why Python?::
|
||||
I enjoy writing Python since 2011, which made it one of the possible
|
||||
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
|
||||
choices. I wanted to use https://www.qt.io/[Qt] because of
|
||||
https://wiki.qt.io/QtWebKit[QtWebKit] so I didn't have
|
||||
https://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.]
|
||||
https://www.infoworld.com/d/application-development/van-rossum-python-not-too-slow-188715[No.]
|
||||
I believe efficiency while coding is a lot more important than efficiency
|
||||
while running. Also, most of the heavy lifting of qutebrowser is done by Qt
|
||||
and WebKit in C++, with the
|
||||
@ -74,7 +74,7 @@ Is qutebrowser secure?::
|
||||
Most security issues are in the backend (which handles networking,
|
||||
rendering, JavaScript, etc.) and not qutebrowser itself.
|
||||
+
|
||||
qutebrowser uses http://wiki.qt.io/QtWebEngine[QtWebEngine] by default.
|
||||
qutebrowser uses https://wiki.qt.io/QtWebEngine[QtWebEngine] by default.
|
||||
QtWebEngine is based on Google's https://www.chromium.org/Home[Chromium]. While
|
||||
Qt only updates to a new Chromium release on every minor Qt release (all ~6
|
||||
months), every patch release backports security fixes from newer Chromium
|
||||
@ -84,26 +84,41 @@ do anything. Chromium's process isolation and
|
||||
https://chromium.googlesource.com/chromium/src/+/master/docs/design/sandbox.md[sandboxing]
|
||||
features are also enabled as a second line of defense.
|
||||
+
|
||||
http://wiki.qt.io/QtWebKit[QtWebKit] is also supported as an alternative
|
||||
https://wiki.qt.io/QtWebKit[QtWebKit] is also supported as an alternative
|
||||
backend, but hasn't seen new releases
|
||||
https://github.com/annulen/webkit/releases[in a while]. It also doesn't have any
|
||||
process isolation or sandboxing.
|
||||
process isolation or sandboxing. See
|
||||
https://github.com/qutebrowser/qutebrowser/issues/4039[#4039] for more details.
|
||||
+
|
||||
Security issues in qutebrowser's code happen very rarely (as per March 2018,
|
||||
there has been one security issue caused by qutebrowser in over four years) and
|
||||
are fixed timely. To report security bugs, please contact me directly at
|
||||
mail@qutebrowser.org, GPG ID
|
||||
Security issues in qutebrowser's code happen very rarely (as per July 2018,
|
||||
there have been three security issues caused by qutebrowser in over 4.5 years).
|
||||
Those were handled appropriately
|
||||
(http://seclists.org/oss-sec/2018/q3/29[example]) and fixed timely. To report
|
||||
security bugs, please contact me directly at mail@qutebrowser.org, GPG ID
|
||||
https://www.the-compiler.org/pubkey.asc[0x916eb0c8fd55a072].
|
||||
|
||||
Is there an adblocker?::
|
||||
There is a host-based adblocker which takes /etc/hosts-like lists. A "real"
|
||||
adblocker has a
|
||||
http://www.reddit.com/r/programming/comments/25j41u/adblock_pluss_effect_on_firefoxs_memory_usage/chhpomw[big
|
||||
https://www.reddit.com/r/programming/comments/25j41u/adblock_pluss_effect_on_firefoxs_memory_usage/chhpomw[big
|
||||
impact] on browsing speed and
|
||||
https://blog.mozilla.org/nnethercote/2014/05/14/adblock-pluss-effect-on-firefoxs-memory-usage/[RAM
|
||||
usage], so implementing support for AdBlockPlus-like lists is currently not
|
||||
a priority.
|
||||
|
||||
How can I get No-Script-like behavior?::
|
||||
To disable JavaScript by default:
|
||||
+
|
||||
----
|
||||
:set content.javascript.enabled false
|
||||
----
|
||||
+
|
||||
The basic command for enabling JavaScript for the current host is `tsh`.
|
||||
This will allow JavaScript execution for the current session.
|
||||
Use `S` instead of `s` to make the exception permanent.
|
||||
With `H` instead of `h`, subdomains are included.
|
||||
With `u` instead of `h`, only the current URL is whitelisted (not the whole host).
|
||||
|
||||
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:
|
||||
@ -132,13 +147,14 @@ It also works nicely with rapid hints:
|
||||
----
|
||||
|
||||
How do I use qutebrowser with mutt?::
|
||||
Due to a Qt limitation, local files without `.html` extensions are
|
||||
"downloaded" instead of displayed, see
|
||||
https://github.com/qutebrowser/qutebrowser/issues/566[#566]. You can work
|
||||
around this by using this in your `mailcap`:
|
||||
For security reasons, local files without `.html` extensions aren't
|
||||
rendered as HTML, see
|
||||
https://bugs.chromium.org/p/chromium/issues/detail?id=777737[this Chromium issue]
|
||||
for details. You can do this in your `mailcap` file to get a proper
|
||||
extension:
|
||||
+
|
||||
----
|
||||
text/html; mv %s %s.html && qutebrowser %s.html >/dev/null 2>/dev/null; needsterminal;
|
||||
text/html; qutebrowser %s; nametemplate=%s.html
|
||||
----
|
||||
|
||||
What is the difference between bookmarks and quickmarks?::
|
||||
@ -251,7 +267,7 @@ Unable to view flash content.::
|
||||
to use the flash plugin. Using the command `:set content.plugins true`
|
||||
in qutebrowser will enable plugins. Packages for flash should
|
||||
be provided for your platform or it can be obtained from
|
||||
http://get.adobe.com/flashplayer/[Adobe].
|
||||
https://get.adobe.com/flashplayer/[Adobe].
|
||||
|
||||
Experiencing freezing on sites like duckduckgo and youtube.::
|
||||
This issue could be caused by stale plugin files installed by `mozplugger`
|
||||
@ -295,10 +311,19 @@ Lastly, set your `qt.args` to point to that directory and restart qutebrowser:
|
||||
:restart
|
||||
----
|
||||
|
||||
Unable to use `spawn` on MacOS.::
|
||||
When running qutebrowser from the prebuilt binary (`qutebrowser.app`) it *will
|
||||
not* read any files that would alter your `$PATH` (e.g. `.profile`, `.bashrc`,
|
||||
etc). This is not a bug, just that `.profile` is not propogated to GUI
|
||||
applications in MacOS.
|
||||
+
|
||||
See https://github.com/qutebrowser/qutebrowser/issues/4273[Issue #4273] for
|
||||
details and potential workarounds.
|
||||
|
||||
My issue is not listed.::
|
||||
If you experience any segfaults or crashes, you can report the issue in
|
||||
https://github.com/qutebrowser/qutebrowser/issues[the issue tracker] or
|
||||
using the `:report` command.
|
||||
If you are reporting a segfault, make sure you read the
|
||||
link:stacktrace.asciidoc[guide] on how to report them with all needed
|
||||
link:stacktrace{outfilesuffix}[guide] on how to report them with all needed
|
||||
information.
|
||||
|
@ -38,7 +38,11 @@ It is possible to run or bind multiple commands by separating them with `;;`.
|
||||
|<<close,close>>|Close the current window.
|
||||
|<<config-clear,config-clear>>|Set all settings back to their default.
|
||||
|<<config-cycle,config-cycle>>|Cycle an option between multiple values.
|
||||
|<<config-dict-add,config-dict-add>>|Add a key/value pair to a dictionary option.
|
||||
|<<config-dict-remove,config-dict-remove>>|Remove a key from a dict.
|
||||
|<<config-edit,config-edit>>|Open the config.py file in the editor.
|
||||
|<<config-list-add,config-list-add>>|Append a value to a config option that is a list.
|
||||
|<<config-list-remove,config-list-remove>>|Remove a value from a list.
|
||||
|<<config-source,config-source>>|Read a config.py file.
|
||||
|<<config-unset,config-unset>>|Unset an option.
|
||||
|<<config-write-py,config-write-py>>|Write the current configuration to a config.py file.
|
||||
@ -292,6 +296,35 @@ Cycle an option between multiple values.
|
||||
* +*-t*+, +*--temp*+: Set value temporarily until qutebrowser is closed.
|
||||
* +*-p*+, +*--print*+: Print the value after setting.
|
||||
|
||||
[[config-dict-add]]
|
||||
=== config-dict-add
|
||||
Syntax: +:config-dict-add [*--temp*] [*--replace*] 'option' 'key' 'value'+
|
||||
|
||||
Add a key/value pair to a dictionary option.
|
||||
|
||||
==== positional arguments
|
||||
* +'option'+: The name of the option.
|
||||
* +'key'+: The key to use.
|
||||
* +'value'+: The value to place in the dictionary.
|
||||
|
||||
==== optional arguments
|
||||
* +*-t*+, +*--temp*+: Add value temporarily until qutebrowser is closed.
|
||||
* +*-r*+, +*--replace*+: Replace existing values. By default, existing values are not overwritten.
|
||||
|
||||
|
||||
[[config-dict-remove]]
|
||||
=== config-dict-remove
|
||||
Syntax: +:config-dict-remove [*--temp*] 'option' 'key'+
|
||||
|
||||
Remove a key from a dict.
|
||||
|
||||
==== positional arguments
|
||||
* +'option'+: The name of the option.
|
||||
* +'key'+: The key to remove from the dict.
|
||||
|
||||
==== optional arguments
|
||||
* +*-t*+, +*--temp*+: Remove value temporarily until qutebrowser is closed.
|
||||
|
||||
[[config-edit]]
|
||||
=== config-edit
|
||||
Syntax: +:config-edit [*--no-source*]+
|
||||
@ -301,6 +334,32 @@ Open the config.py file in the editor.
|
||||
==== optional arguments
|
||||
* +*-n*+, +*--no-source*+: Don't re-source the config file after editing.
|
||||
|
||||
[[config-list-add]]
|
||||
=== config-list-add
|
||||
Syntax: +:config-list-add [*--temp*] 'option' 'value'+
|
||||
|
||||
Append a value to a config option that is a list.
|
||||
|
||||
==== positional arguments
|
||||
* +'option'+: The name of the option.
|
||||
* +'value'+: The value to append to the end of the list.
|
||||
|
||||
==== optional arguments
|
||||
* +*-t*+, +*--temp*+: Add value temporarily until qutebrowser is closed.
|
||||
|
||||
[[config-list-remove]]
|
||||
=== config-list-remove
|
||||
Syntax: +:config-list-remove [*--temp*] 'option' 'value'+
|
||||
|
||||
Remove a value from a list.
|
||||
|
||||
==== positional arguments
|
||||
* +'option'+: The name of the option.
|
||||
* +'value'+: The value to remove from the list.
|
||||
|
||||
==== optional arguments
|
||||
* +*-t*+, +*--temp*+: Remove value temporarily until qutebrowser is closed.
|
||||
|
||||
[[config-source]]
|
||||
=== config-source
|
||||
Syntax: +:config-source [*--clear*] ['filename']+
|
||||
@ -326,7 +385,7 @@ This sets an option back to its default and removes it from autoconfig.yml.
|
||||
* +'option'+: The name of the option.
|
||||
|
||||
==== optional arguments
|
||||
* +*-t*+, +*--temp*+: Don't touch autoconfig.yml.
|
||||
* +*-t*+, +*--temp*+: Set value temporarily until qutebrowser is closed.
|
||||
|
||||
[[config-write-py]]
|
||||
=== config-write-py
|
||||
@ -545,6 +604,10 @@ Start hinting.
|
||||
- `inputs`: Only input fields.
|
||||
|
||||
|
||||
Custom groups can be added via the `hints.selectors` setting
|
||||
and also used here.
|
||||
|
||||
|
||||
|
||||
* +'target'+: What to do with the selected element.
|
||||
|
||||
@ -576,7 +639,7 @@ Start hinting.
|
||||
- With `userscript`: The userscript to execute. Either store
|
||||
the userscript in
|
||||
`~/.local/share/qutebrowser/userscripts`
|
||||
(or `$XDG_DATA_DIR`), or use an absolute
|
||||
(or `$XDG_DATA_HOME`), or use an absolute
|
||||
path.
|
||||
- With `fill`: The command to fill the statusbar with.
|
||||
`{hint-url}` will get replaced by the selected
|
||||
@ -758,11 +821,11 @@ This tries to automatically click on typical _Previous Page_ or _Next Page_ link
|
||||
- `up`: Go up a level in the current URL.
|
||||
- `increment`: Increment the last number in the URL.
|
||||
Uses the
|
||||
link:settings.html#url.incdec_segments[url.incdec_segments]
|
||||
link:settings{outsuffix}#url.incdec_segments[url.incdec_segments]
|
||||
config option.
|
||||
- `decrement`: Decrement the last number in the URL.
|
||||
Uses the
|
||||
link:settings.html#url.incdec_segments[url.incdec_segments]
|
||||
link:settings{outsuffix}#url.incdec_segments[url.incdec_segments]
|
||||
config option.
|
||||
|
||||
|
||||
@ -918,6 +981,9 @@ Repeat a given command.
|
||||
* +'times'+: How many times to repeat.
|
||||
* +'command'+: The command to run, with optional args.
|
||||
|
||||
==== count
|
||||
Multiplies with 'times' when given.
|
||||
|
||||
==== 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.
|
||||
@ -1136,7 +1202,7 @@ Syntax: +:set [*--temp*] [*--print*] [*--pattern* 'pattern'] ['option'] ['value'
|
||||
|
||||
Set an option.
|
||||
|
||||
If the option name ends with '?', the value of the option is shown instead. Using :set without any arguments opens a page where settings can be changed interactively.
|
||||
If the option name ends with '?' or no value is provided, the value of the option is shown instead. Using :set without any arguments opens a page where settings can be changed interactively.
|
||||
|
||||
==== positional arguments
|
||||
* +'option'+: The name of the option.
|
||||
@ -1190,13 +1256,16 @@ Spawn a command in a shell.
|
||||
* +*-u*+, +*--userscript*+: Run the command as a userscript. You can use an absolute path, or store the userscript in one of those
|
||||
locations:
|
||||
- `~/.local/share/qutebrowser/userscripts`
|
||||
(or `$XDG_DATA_DIR`)
|
||||
(or `$XDG_DATA_HOME`)
|
||||
- `/usr/share/qutebrowser/userscripts`
|
||||
|
||||
* +*-v*+, +*--verbose*+: Show notifications when the command started/exited.
|
||||
* +*-o*+, +*--output*+: Whether the output should be shown in a new tab.
|
||||
* +*-d*+, +*--detach*+: Whether the command should be detached from qutebrowser.
|
||||
|
||||
==== count
|
||||
Given to userscripts as $QUTE_COUNT.
|
||||
|
||||
==== note
|
||||
* This command does not split arguments after the last argument and handles quotes literally.
|
||||
|
||||
@ -1255,7 +1324,7 @@ The tab index to focus, starting with 1.
|
||||
|
||||
[[tab-give]]
|
||||
=== tab-give
|
||||
Syntax: +:tab-give ['win-id']+
|
||||
Syntax: +:tab-give [*--keep*] ['win-id']+
|
||||
|
||||
Give the current tab to a new or existing window if win_id given.
|
||||
|
||||
@ -1264,6 +1333,9 @@ If no win_id is given, the tab will get detached into a new window.
|
||||
==== positional arguments
|
||||
* +'win-id'+: The window ID of the window to give the current tab to.
|
||||
|
||||
==== optional arguments
|
||||
* +*-k*+, +*--keep*+: If given, keep the old tab around.
|
||||
|
||||
==== count
|
||||
Overrides win_id (index starts at 1 for win_id=0).
|
||||
|
||||
@ -1328,7 +1400,7 @@ How many tabs to switch back.
|
||||
|
||||
[[tab-take]]
|
||||
=== tab-take
|
||||
Syntax: +:tab-take 'index'+
|
||||
Syntax: +:tab-take [*--keep*] 'index'+
|
||||
|
||||
Take a tab from another window.
|
||||
|
||||
@ -1336,6 +1408,12 @@ Take a tab from another window.
|
||||
* +'index'+: The [win_id/]index of the tab to take. Or a substring in which case the closest match will be taken.
|
||||
|
||||
|
||||
==== optional arguments
|
||||
* +*-k*+, +*--keep*+: If given, keep the old tab around.
|
||||
|
||||
==== note
|
||||
* This command does not split arguments after the last argument and handles quotes literally.
|
||||
|
||||
[[unbind]]
|
||||
=== unbind
|
||||
Syntax: +:unbind [*--mode* 'mode'] 'key'+
|
||||
@ -1382,7 +1460,7 @@ Close all windows except for the current one.
|
||||
|
||||
[[yank]]
|
||||
=== yank
|
||||
Syntax: +:yank [*--sel*] [*--keep*] ['what']+
|
||||
Syntax: +:yank [*--sel*] [*--keep*] [*--quiet*] ['what']+
|
||||
|
||||
Yank something to the clipboard or primary selection.
|
||||
|
||||
@ -1401,10 +1479,11 @@ Yank something to the clipboard or primary selection.
|
||||
==== optional arguments
|
||||
* +*-s*+, +*--sel*+: Use the primary selection instead of the clipboard.
|
||||
* +*-k*+, +*--keep*+: Stay in visual mode after yanking the selection.
|
||||
* +*-q*+, +*--quiet*+: Don't show an information message.
|
||||
|
||||
[[zoom]]
|
||||
=== zoom
|
||||
Syntax: +:zoom ['zoom']+
|
||||
Syntax: +:zoom [*--quiet*] ['zoom']+
|
||||
|
||||
Set the zoom level for the current tab.
|
||||
|
||||
@ -1413,20 +1492,33 @@ The zoom can be given as argument or as [count]. If neither is given, the zoom i
|
||||
==== positional arguments
|
||||
* +'zoom'+: The zoom percentage to set.
|
||||
|
||||
==== optional arguments
|
||||
* +*-q*+, +*--quiet*+: Don't show a zoom level message.
|
||||
|
||||
==== count
|
||||
The zoom percentage to set.
|
||||
|
||||
[[zoom-in]]
|
||||
=== zoom-in
|
||||
Syntax: +:zoom-in [*--quiet*]+
|
||||
|
||||
Increase the zoom level for the current tab.
|
||||
|
||||
==== optional arguments
|
||||
* +*-q*+, +*--quiet*+: Don't show a zoom level message.
|
||||
|
||||
==== count
|
||||
How many steps to zoom in.
|
||||
|
||||
[[zoom-out]]
|
||||
=== zoom-out
|
||||
Syntax: +:zoom-out [*--quiet*]+
|
||||
|
||||
Decrease the zoom level for the current tab.
|
||||
|
||||
==== optional arguments
|
||||
* +*-q*+, +*--quiet*+: Don't show a zoom level message.
|
||||
|
||||
==== count
|
||||
How many steps to zoom out.
|
||||
|
||||
@ -1657,7 +1749,7 @@ Shift the focus of the prompt file completion menu to another item.
|
||||
|
||||
[[prompt-open-download]]
|
||||
=== prompt-open-download
|
||||
Syntax: +:prompt-open-download ['cmdline']+
|
||||
Syntax: +:prompt-open-download [*--pdfjs*] ['cmdline']+
|
||||
|
||||
Immediately open a download.
|
||||
|
||||
@ -1669,6 +1761,9 @@ If no specific command is given, this will use the system's default application
|
||||
cmdline.
|
||||
|
||||
|
||||
==== optional arguments
|
||||
* +*-p*+, +*--pdfjs*+: Open the download via PDF.js.
|
||||
|
||||
==== note
|
||||
* This command does not split arguments after the last argument and handles quotes literally.
|
||||
|
||||
|
@ -44,7 +44,7 @@ If you want to customize many settings, you can open the link:qute://settings[]
|
||||
page by running `:set` without any arguments, where all settings are listed and
|
||||
customizable.
|
||||
|
||||
Using the link:commands.html#set[`:set`] command and command completion, you
|
||||
Using the link:commands{outfilesuffix}#set[`:set`] command and command completion, you
|
||||
can quickly set settings interactively, for example `:set tabs.position left`.
|
||||
|
||||
Some settings are also customizable for a given
|
||||
@ -53,8 +53,8 @@ https://developer.chrome.com/apps/match_patterns[URL pattern] by doing e.g.
|
||||
|
||||
To get more help about a setting, use e.g. `:help tabs.position`.
|
||||
|
||||
To bind and unbind keys, you can use the link:commands.html#bind[`:bind`] and
|
||||
link:commands.html#unbind[`:unbind`] commands:
|
||||
To bind and unbind keys, you can use the link:commands{outfilesuffix}#bind[`:bind`] and
|
||||
link:commands{outfilesuffix}#unbind[`:unbind`] commands:
|
||||
|
||||
- Binding the key chain `,v` to the `:spawn mpv {url}` command:
|
||||
`:bind ,v spawn mpv {url}`
|
||||
@ -67,9 +67,9 @@ See the help pages linked above (or `:help :bind`, `:help :unbind`) for more
|
||||
information.
|
||||
|
||||
Other useful commands for config manipulation are
|
||||
link:commands.html#config-unset[`:config-unset`] to reset a value to its default,
|
||||
link:commands.html#config-clear[`:config-clear`] to reset the entire configuration,
|
||||
and link:commands.html#config-cycle[`:config-cycle`] to cycle a setting between
|
||||
link:commands{outfilesuffix}#config-unset[`:config-unset`] to reset a value to its default,
|
||||
link:commands{outfilesuffix}#config-clear[`:config-clear`] to reset the entire configuration,
|
||||
and link:commands{outfilesuffix}#config-cycle[`:config-cycle`] to cycle a setting between
|
||||
different values.
|
||||
|
||||
[[configpy]]
|
||||
@ -111,7 +111,7 @@ Note that qutebrowser does some Python magic so it's able to warn you about
|
||||
mistyped config settings. As an example, if you do `c.tabs.possition = "left"`,
|
||||
you'll get an error when starting.
|
||||
|
||||
See the link:settings.html[settings help page] for all available settings. The
|
||||
See the link:settings{outfilesuffix}[settings help page] for all available settings. The
|
||||
accepted values depend on the type of the option. Commonly used are:
|
||||
|
||||
- Strings: `c.tabs.position = "left"`
|
||||
@ -187,7 +187,7 @@ preferred to use the `config.bind` command. Doing so ensures the commands are
|
||||
valid and normalizes different expressions which map to the same key.
|
||||
|
||||
For details on how to specify keys and the available modes, see the
|
||||
link:settings.html#bindings.commands[documentation] for the `bindings.commands`
|
||||
link:settings{outfilesuffix}#bindings.commands[documentation] for the `bindings.commands`
|
||||
setting.
|
||||
|
||||
To bind a key:
|
||||
@ -395,6 +395,7 @@ Pre-built colorschemes
|
||||
|
||||
- A collection of https://github.com/chriskempson/base16[base16] color-schemes can be found in https://github.com/theova/base16-qutebrowser[base16-qutebrowser] and used with https://github.com/AuditeMarlow/base16-manager[base16-manager].
|
||||
- Two implementations of the https://github.com/arcticicestudio/nord[Nord] colorscheme for qutebrowser exist: https://github.com/Linuus/nord-qutebrowser[Linuus], https://github.com/KnownAsDon/QuteBrowser-Nord-Theme[KnownAsDon]
|
||||
- https://github.com/evannagle/qutebrowser-dracula-theme[Dracula]
|
||||
|
||||
Avoiding flake8 errors
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
@ -452,7 +453,7 @@ or always navigate through command history with
|
||||
:bind -m command <Down> command-history-next
|
||||
----
|
||||
|
||||
- The default for `completion.web_history_max_items` is now set to `-1`, showing
|
||||
- The default for `completion.web_history.max_items` is now set to `-1`, showing
|
||||
an unlimited number of items in the completion for `:open` as the new
|
||||
sqlite-based completion is much faster. If the `:open` completion is too slow
|
||||
on your machine, set an appropriate limit again.
|
||||
|
@ -6,14 +6,14 @@ Documentation
|
||||
|
||||
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:configuring.html[Configuring qutebrowser]
|
||||
* link:settings.html[Documentation of settings]
|
||||
* link:../userscripts.html[How to write userscripts]
|
||||
* link:../contributing.html[Contributing to qutebrowser]
|
||||
* link:../quickstart{outfilesuffix}[Quick start guide]
|
||||
* link:../faq{outfilesuffix}[Frequently asked questions]
|
||||
* link:../changelog{outfilesuffix}[Change Log]
|
||||
* link:commands{outfilesuffix}[Documentation of commands]
|
||||
* link:configuring{outfilesuffix}[Configuring qutebrowser]
|
||||
* link:settings{outfilesuffix}[Documentation of settings]
|
||||
* link:../userscripts{outfilesuffix}[How to write userscripts]
|
||||
* link:../contributing{outfilesuffix}[Contributing to qutebrowser]
|
||||
|
||||
Getting help
|
||||
------------
|
||||
|
@ -100,6 +100,7 @@
|
||||
|<<completion.delay,completion.delay>>|Delay (in milliseconds) before updating completions after typing a character.
|
||||
|<<completion.height,completion.height>>|Height (in pixels or as percentage of the window) of the completion.
|
||||
|<<completion.min_chars,completion.min_chars>>|Minimum amount of characters needed to update completions.
|
||||
|<<completion.open_categories,completion.open_categories>>|Which categories to show (in which order) in the :open completion.
|
||||
|<<completion.quick,completion.quick>>|Move on to the next part when there's only one possible completion left.
|
||||
|<<completion.scrollbar.padding,completion.scrollbar.padding>>|Padding (in pixels) of the scrollbar handle in the completion window.
|
||||
|<<completion.scrollbar.width,completion.scrollbar.width>>|Width (in pixels) of the scrollbar in the completion window.
|
||||
@ -107,7 +108,8 @@
|
||||
|<<completion.shrink,completion.shrink>>|Shrink the completion to be smaller than the configured size if there are no scrollbars.
|
||||
|<<completion.timestamp_format,completion.timestamp_format>>|Format of timestamps (e.g. for the history completion).
|
||||
|<<completion.use_best_match,completion.use_best_match>>|Execute the best-matching command on a partial match.
|
||||
|<<completion.web_history_max_items,completion.web_history_max_items>>|Number of URLs to show in the web history.
|
||||
|<<completion.web_history.exclude,completion.web_history.exclude>>|A list of patterns which should not be shown in the history.
|
||||
|<<completion.web_history.max_items,completion.web_history.max_items>>|Number of URLs to show in the web history.
|
||||
|<<confirm_quit,confirm_quit>>|Require a confirmation before quitting the application.
|
||||
|<<content.autoplay,content.autoplay>>|Automatically start playing `<video>` elements.
|
||||
|<<content.cache.appcache,content.cache.appcache>>|Enable support for the HTML 5 web application cache feature.
|
||||
@ -128,7 +130,7 @@
|
||||
|<<content.headers.user_agent,content.headers.user_agent>>|User agent to send. Unset to send the default.
|
||||
|<<content.host_blocking.enabled,content.host_blocking.enabled>>|Enable host blocking.
|
||||
|<<content.host_blocking.lists,content.host_blocking.lists>>|List of URLs of lists which contain hosts to block.
|
||||
|<<content.host_blocking.whitelist,content.host_blocking.whitelist>>|List of domains that should always be loaded, despite being ad-blocked.
|
||||
|<<content.host_blocking.whitelist,content.host_blocking.whitelist>>|A list of patterns that should always be loaded, despite being ad-blocked.
|
||||
|<<content.hyperlink_auditing,content.hyperlink_auditing>>|Enable hyperlink auditing (`<a ping>`).
|
||||
|<<content.images,content.images>>|Load images automatically in web pages.
|
||||
|<<content.javascript.alert,content.javascript.alert>>|Show javascript alerts.
|
||||
@ -143,6 +145,8 @@
|
||||
|<<content.local_content_can_access_remote_urls,content.local_content_can_access_remote_urls>>|Allow locally loaded documents to access remote URLs.
|
||||
|<<content.local_storage,content.local_storage>>|Enable support for HTML 5 local storage and Web SQL.
|
||||
|<<content.media_capture,content.media_capture>>|Allow websites to record audio/video.
|
||||
|<<content.mouse_lock,content.mouse_lock>>|Allow websites to lock your mouse pointer.
|
||||
|<<content.mute,content.mute>>|Automatically mute tabs.
|
||||
|<<content.netrc_file,content.netrc_file>>|Netrc-file for HTTP authentication.
|
||||
|<<content.notifications,content.notifications>>|Allow websites to show notifications.
|
||||
|<<content.pdfjs,content.pdfjs>>|Allow pdf.js to view PDF files in the browser.
|
||||
@ -156,7 +160,7 @@
|
||||
|<<content.ssl_strict,content.ssl_strict>>|Validate SSL handshakes.
|
||||
|<<content.user_stylesheets,content.user_stylesheets>>|List of user stylesheet filenames to use.
|
||||
|<<content.webgl,content.webgl>>|Enable WebGL.
|
||||
|<<content.webrtc_public_interfaces_only,content.webrtc_public_interfaces_only>>|Only expose public interfaces via WebRTC.
|
||||
|<<content.webrtc_ip_handling_policy,content.webrtc_ip_handling_policy>>|Which interfaces to expose via WebRTC.
|
||||
|<<content.windowed_fullscreen,content.windowed_fullscreen>>|Limit fullscreen to the browser window (does not expand to fill the screen).
|
||||
|<<content.xss_auditing,content.xss_auditing>>|Monitor load requests for cross-site scripting attempts.
|
||||
|<<downloads.location.directory,downloads.location.directory>>|Directory to save downloads to.
|
||||
@ -203,6 +207,7 @@
|
||||
|<<hints.next_regexes,hints.next_regexes>>|Comma-separated list of regular expressions to use for 'next' links.
|
||||
|<<hints.prev_regexes,hints.prev_regexes>>|Comma-separated list of regular expressions to use for 'prev' links.
|
||||
|<<hints.scatter,hints.scatter>>|Scatter hint key chains (like Vimium) or not (like dwb).
|
||||
|<<hints.selectors,hints.selectors>>|CSS selectors used to determine which elements on a page should have hints.
|
||||
|<<hints.uppercase,hints.uppercase>>|Make characters in hint strings uppercase.
|
||||
|<<history_gap_interval,history_gap_interval>>|Maximum time (in minutes) between two history items for them to be considered being from the same browsing session.
|
||||
|<<input.escape_quits_reporter,input.escape_quits_reporter>>|Allow Escape to quit the crash reporter.
|
||||
@ -227,7 +232,9 @@
|
||||
|<<qt.force_platform,qt.force_platform>>|Force a Qt platform to use.
|
||||
|<<qt.force_software_rendering,qt.force_software_rendering>>|Force software rendering for QtWebEngine.
|
||||
|<<qt.highdpi,qt.highdpi>>|Turn on Qt HighDPI scaling.
|
||||
|<<scrolling.bar,scrolling.bar>>|Show a scrollbar.
|
||||
|<<qt.low_end_device_mode,qt.low_end_device_mode>>|When to use Chromium's low-end device mode.
|
||||
|<<qt.process_model,qt.process_model>>|Which Chromium process model to use.
|
||||
|<<scrolling.bar,scrolling.bar>>|When to show the scrollbar.
|
||||
|<<scrolling.smooth,scrolling.smooth>>|Enable smooth scrolling for web pages.
|
||||
|<<search.ignore_case,search.ignore_case>>|When to find text on a page case-insensitively.
|
||||
|<<search.incremental,search.incremental>>|Find text on a page incrementally, renewing the search for each typed character.
|
||||
@ -246,11 +253,13 @@
|
||||
|<<tabs.indicator.padding,tabs.indicator.padding>>|Padding (in pixels) for tab indicators.
|
||||
|<<tabs.indicator.width,tabs.indicator.width>>|Width (in pixels) of the progress indicator (0 to disable).
|
||||
|<<tabs.last_close,tabs.last_close>>|How to behave when the last tab is closed.
|
||||
|<<tabs.max_width,tabs.max_width>>|Maximum width (in pixels) of tabs (-1 for no maximum).
|
||||
|<<tabs.min_width,tabs.min_width>>|Minimum width (in pixels) of tabs (-1 for the default minimum size behavior).
|
||||
|<<tabs.mode_on_change,tabs.mode_on_change>>|When switching tabs, what input mode is applied.
|
||||
|<<tabs.mousewheel_switching,tabs.mousewheel_switching>>|Switch between tabs using the mouse wheel.
|
||||
|<<tabs.new_position.related,tabs.new_position.related>>|Position of new tabs opened from another tab.
|
||||
|<<tabs.new_position.unrelated,tabs.new_position.unrelated>>|Position of new tabs which aren't opened from another tab.
|
||||
|<<tabs.new_position.stacking,tabs.new_position.stacking>>|Stack related tabs on top of each other when opened consecutively.
|
||||
|<<tabs.new_position.unrelated,tabs.new_position.unrelated>>|Position of new tabs which are not opened from another tab.
|
||||
|<<tabs.padding,tabs.padding>>|Padding (in pixels) around text for tabs.
|
||||
|<<tabs.pinned.shrink,tabs.pinned.shrink>>|Shrink pinned tabs down to their contents.
|
||||
|<<tabs.position,tabs.position>>|Position of the tab bar.
|
||||
@ -287,9 +296,11 @@ Type: <<types,Dict>>
|
||||
|
||||
Default:
|
||||
|
||||
- +pass:[q]+: +pass:[quit]+
|
||||
- +pass:[q]+: +pass:[close]+
|
||||
- +pass:[qa]+: +pass:[quit]+
|
||||
- +pass:[w]+: +pass:[session-save]+
|
||||
- +pass:[wq]+: +pass:[quit --save]+
|
||||
- +pass:[wqa]+: +pass:[quit --save]+
|
||||
|
||||
[[auto_save.interval]]
|
||||
=== auto_save.interval
|
||||
@ -562,6 +573,7 @@ Default:
|
||||
* +pass:[g0]+: +pass:[tab-focus 1]+
|
||||
* +pass:[gB]+: +pass:[set-cmd-text -s :bookmark-load -t]+
|
||||
* +pass:[gC]+: +pass:[tab-clone]+
|
||||
* +pass:[gD]+: +pass:[tab-give]+
|
||||
* +pass:[gO]+: +pass:[set-cmd-text :open -t -r {url:pretty}]+
|
||||
* +pass:[gU]+: +pass:[navigate up -t]+
|
||||
* +pass:[g^]+: +pass:[tab-focus 1]+
|
||||
@ -593,6 +605,9 @@ Default:
|
||||
* +pass:[sk]+: +pass:[set-cmd-text -s :bind]+
|
||||
* +pass:[sl]+: +pass:[set-cmd-text -s :set -t]+
|
||||
* +pass:[ss]+: +pass:[set-cmd-text -s :set]+
|
||||
* +pass:[tIH]+: +pass:[config-cycle -p -u *://*.{url:host}/* content.images ;; reload]+
|
||||
* +pass:[tIh]+: +pass:[config-cycle -p -u *://{url:host}/* content.images ;; reload]+
|
||||
* +pass:[tIu]+: +pass:[config-cycle -p -u {url} content.images ;; reload]+
|
||||
* +pass:[tPH]+: +pass:[config-cycle -p -u *://*.{url:host}/* content.plugins ;; reload]+
|
||||
* +pass:[tPh]+: +pass:[config-cycle -p -u *://{url:host}/* content.plugins ;; reload]+
|
||||
* +pass:[tPu]+: +pass:[config-cycle -p -u {url} content.plugins ;; reload]+
|
||||
@ -600,6 +615,9 @@ Default:
|
||||
* +pass:[tSh]+: +pass:[config-cycle -p -u *://{url:host}/* content.javascript.enabled ;; reload]+
|
||||
* +pass:[tSu]+: +pass:[config-cycle -p -u {url} content.javascript.enabled ;; reload]+
|
||||
* +pass:[th]+: +pass:[back -t]+
|
||||
* +pass:[tiH]+: +pass:[config-cycle -p -t -u *://*.{url:host}/* content.images ;; reload]+
|
||||
* +pass:[tih]+: +pass:[config-cycle -p -t -u *://{url:host}/* content.images ;; reload]+
|
||||
* +pass:[tiu]+: +pass:[config-cycle -p -t -u {url} content.images ;; reload]+
|
||||
* +pass:[tl]+: +pass:[forward -t]+
|
||||
* +pass:[tpH]+: +pass:[config-cycle -p -t -u *://*.{url:host}/* content.plugins ;; reload]+
|
||||
* +pass:[tph]+: +pass:[config-cycle -p -t -u *://{url:host}/* content.plugins ;; reload]+
|
||||
@ -633,7 +651,7 @@ Default:
|
||||
* +pass:[}}]+: +pass:[navigate next -t]+
|
||||
- +pass:[passthrough]+:
|
||||
|
||||
* +pass:[<Ctrl-V>]+: +pass:[leave-mode]+
|
||||
* +pass:[<Shift-Escape>]+: +pass:[leave-mode]+
|
||||
- +pass:[prompt]+:
|
||||
|
||||
* +pass:[<Alt-B>]+: +pass:[rl-backward-word]+
|
||||
@ -649,6 +667,7 @@ Default:
|
||||
* +pass:[<Ctrl-F>]+: +pass:[rl-forward-char]+
|
||||
* +pass:[<Ctrl-H>]+: +pass:[rl-backward-delete-char]+
|
||||
* +pass:[<Ctrl-K>]+: +pass:[rl-kill-line]+
|
||||
* +pass:[<Ctrl-P>]+: +pass:[prompt-open-download --pdfjs]+
|
||||
* +pass:[<Ctrl-U>]+: +pass:[rl-unix-line-discard]+
|
||||
* +pass:[<Ctrl-W>]+: +pass:[rl-unix-word-rubout]+
|
||||
* +pass:[<Ctrl-X>]+: +pass:[prompt-open-download]+
|
||||
@ -780,7 +799,7 @@ Default: +pass:[black]+
|
||||
=== colors.completion.match.fg
|
||||
Foreground color of the matched text in the completion.
|
||||
|
||||
Type: <<types,QssColor>>
|
||||
Type: <<types,QtColor>>
|
||||
|
||||
Default: +pass:[#ff4444]+
|
||||
|
||||
@ -1383,6 +1402,26 @@ Type: <<types,Int>>
|
||||
|
||||
Default: +pass:[1]+
|
||||
|
||||
[[completion.open_categories]]
|
||||
=== completion.open_categories
|
||||
Which categories to show (in which order) in the :open completion.
|
||||
|
||||
Type: <<types,FlagList>>
|
||||
|
||||
Valid values:
|
||||
|
||||
* +searchengines+
|
||||
* +quickmarks+
|
||||
* +bookmarks+
|
||||
* +history+
|
||||
|
||||
Default:
|
||||
|
||||
- +pass:[searchengines]+
|
||||
- +pass:[quickmarks]+
|
||||
- +pass:[bookmarks]+
|
||||
- +pass:[history]+
|
||||
|
||||
[[completion.quick]]
|
||||
=== completion.quick
|
||||
Move on to the next part when there's only one possible completion left.
|
||||
@ -1445,8 +1484,19 @@ Type: <<types,Bool>>
|
||||
|
||||
Default: +pass:[false]+
|
||||
|
||||
[[completion.web_history_max_items]]
|
||||
=== completion.web_history_max_items
|
||||
[[completion.web_history.exclude]]
|
||||
=== completion.web_history.exclude
|
||||
A list of patterns which should not be shown in the history.
|
||||
This only affects the completion. Matching URLs are still saved in the history (and visible on the qute://history page), but hidden in the completion.
|
||||
Changing this setting will cause the completion history to be regenerated on the next start, which will take a short while.
|
||||
This setting requires a restart.
|
||||
|
||||
Type: <<types,List of UrlPattern>>
|
||||
|
||||
Default: empty
|
||||
|
||||
[[completion.web_history.max_items]]
|
||||
=== completion.web_history.max_items
|
||||
Number of URLs to show in the web history.
|
||||
0: no history / -1: unlimited
|
||||
|
||||
@ -1474,7 +1524,9 @@ Default:
|
||||
[[content.autoplay]]
|
||||
=== content.autoplay
|
||||
Automatically start playing `<video>` elements.
|
||||
Note this option needs a restart with QtWebEngine on Qt < 5.11.
|
||||
Note: On Qt < 5.11, this option needs a restart and does not support URL patterns.
|
||||
|
||||
This setting supports URL patterns.
|
||||
|
||||
Type: <<types,Bool>>
|
||||
|
||||
@ -1570,6 +1622,8 @@ Default: +pass:[iso-8859-1]+
|
||||
Allow websites to share screen content.
|
||||
On Qt < 5.10, a dialog box is always displayed, even if this is set to "true".
|
||||
|
||||
This setting supports URL patterns.
|
||||
|
||||
Type: <<types,BoolAsk>>
|
||||
|
||||
Valid values:
|
||||
@ -1609,6 +1663,8 @@ This setting is only available with the QtWebKit backend.
|
||||
=== content.geolocation
|
||||
Allow websites to request geolocations.
|
||||
|
||||
This setting supports URL patterns.
|
||||
|
||||
Type: <<types,BoolAsk>>
|
||||
|
||||
Valid values:
|
||||
@ -1622,6 +1678,9 @@ Default: +pass:[ask]+
|
||||
[[content.headers.accept_language]]
|
||||
=== content.headers.accept_language
|
||||
Value to send in the `Accept-Language` header.
|
||||
Note that the value read from JavaScript is always the global value.
|
||||
|
||||
This setting supports URL patterns.
|
||||
|
||||
Type: <<types,String>>
|
||||
|
||||
@ -1631,6 +1690,8 @@ Default: +pass:[en-US,en]+
|
||||
=== content.headers.custom
|
||||
Custom headers for qutebrowser HTTP requests.
|
||||
|
||||
This setting supports URL patterns.
|
||||
|
||||
Type: <<types,Dict>>
|
||||
|
||||
Default: empty
|
||||
@ -1640,6 +1701,8 @@ Default: empty
|
||||
Value to send in the `DNT` header.
|
||||
When this is set to true, qutebrowser asks websites to not track your identity. If set to null, the DNT header is not sent at all.
|
||||
|
||||
This setting supports URL patterns.
|
||||
|
||||
Type: <<types,Bool>>
|
||||
|
||||
Default: +pass:[true]+
|
||||
@ -1648,6 +1711,8 @@ Default: +pass:[true]+
|
||||
=== content.headers.referer
|
||||
When to send the Referer header.
|
||||
The Referer header tells websites from which website you were coming from when visiting them.
|
||||
No restart is needed with QtWebKit.
|
||||
This setting requires a restart.
|
||||
|
||||
Type: <<types,String>>
|
||||
|
||||
@ -1655,15 +1720,16 @@ Valid values:
|
||||
|
||||
* +always+: Always send the Referer.
|
||||
* +never+: Never send the Referer. This is not recommended, as some sites may break.
|
||||
* +same-domain+: Only send the Referer for the same domain. This will still protect your privacy, but shouldn't break any sites.
|
||||
* +same-domain+: Only send the Referer for the same domain. This will still protect your privacy, but shouldn't break any sites. With QtWebEngine, the referer will still be sent for other domains, but with stripped path information.
|
||||
|
||||
Default: +pass:[same-domain]+
|
||||
|
||||
This setting is only available with the QtWebKit backend.
|
||||
|
||||
[[content.headers.user_agent]]
|
||||
=== content.headers.user_agent
|
||||
User agent to send. Unset to send the default.
|
||||
Note that the value read from JavaScript is always the global value.
|
||||
|
||||
This setting supports URL patterns.
|
||||
|
||||
Type: <<types,String>>
|
||||
|
||||
@ -1673,6 +1739,8 @@ Default: empty
|
||||
=== content.host_blocking.enabled
|
||||
Enable host blocking.
|
||||
|
||||
This setting supports URL patterns.
|
||||
|
||||
Type: <<types,Bool>>
|
||||
|
||||
Default: +pass:[true]+
|
||||
@ -1688,6 +1756,11 @@ The file can be in one of the following formats:
|
||||
- A zip-file of any of the above, with either only one file, or a file
|
||||
named `hosts` (with any extension).
|
||||
|
||||
It's also possible to add a local file or directory via a `file://` URL. In
|
||||
case of a directory, all files in the directory are read as adblock lists.
|
||||
|
||||
The file `~/.config/qutebrowser/blocked-hosts` is always read if it exists.
|
||||
|
||||
|
||||
Type: <<types,List of Url>>
|
||||
|
||||
@ -1697,11 +1770,11 @@ Default:
|
||||
|
||||
[[content.host_blocking.whitelist]]
|
||||
=== content.host_blocking.whitelist
|
||||
List of domains that should always be loaded, despite being ad-blocked.
|
||||
Domains may contain * and ? wildcards and are otherwise required to exactly match the requested domain.
|
||||
A list of patterns that should always be loaded, despite being ad-blocked.
|
||||
Note this whitelists blocked hosts, not first-party URLs. As an example, if `example.org` loads an ad from `ads.example.org`, the whitelisted host should be `ads.example.org`. If you want to disable the adblocker on a given page, use the `content.host_blocking.enabled` setting with a URL pattern instead.
|
||||
Local domains are always exempt from hostblocking.
|
||||
|
||||
Type: <<types,List of String>>
|
||||
Type: <<types,List of UrlPattern>>
|
||||
|
||||
Default:
|
||||
|
||||
@ -1843,6 +1916,8 @@ Default: +pass:[true]+
|
||||
=== content.media_capture
|
||||
Allow websites to record audio/video.
|
||||
|
||||
This setting supports URL patterns.
|
||||
|
||||
Type: <<types,BoolAsk>>
|
||||
|
||||
Valid values:
|
||||
@ -1855,6 +1930,37 @@ Default: +pass:[ask]+
|
||||
|
||||
This setting is only available with the QtWebEngine backend.
|
||||
|
||||
[[content.mouse_lock]]
|
||||
=== content.mouse_lock
|
||||
Allow websites to lock your mouse pointer.
|
||||
|
||||
This setting supports URL patterns.
|
||||
|
||||
Type: <<types,BoolAsk>>
|
||||
|
||||
Valid values:
|
||||
|
||||
* +true+
|
||||
* +false+
|
||||
* +ask+
|
||||
|
||||
Default: +pass:[ask]+
|
||||
|
||||
On QtWebEngine, this setting requires Qt 5.8 or newer.
|
||||
|
||||
On QtWebKit, this setting is unavailable.
|
||||
|
||||
[[content.mute]]
|
||||
=== content.mute
|
||||
Automatically mute tabs.
|
||||
Note that if the `:tab-mute` command is used, the mute status for the affected tab is now controlled manually, and this setting doesn't have any effect.
|
||||
|
||||
This setting supports URL patterns.
|
||||
|
||||
Type: <<types,Bool>>
|
||||
|
||||
Default: +pass:[false]+
|
||||
|
||||
[[content.netrc_file]]
|
||||
=== content.netrc_file
|
||||
Netrc-file for HTTP authentication.
|
||||
@ -1868,6 +1974,8 @@ Default: empty
|
||||
=== content.notifications
|
||||
Allow websites to show notifications.
|
||||
|
||||
This setting supports URL patterns.
|
||||
|
||||
Type: <<types,BoolAsk>>
|
||||
|
||||
Valid values:
|
||||
@ -1889,12 +1997,12 @@ Type: <<types,Bool>>
|
||||
|
||||
Default: +pass:[false]+
|
||||
|
||||
This setting is only available with the QtWebKit backend.
|
||||
|
||||
[[content.persistent_storage]]
|
||||
=== content.persistent_storage
|
||||
Allow websites to request persistent storage quota via `navigator.webkitPersistentStorage.requestQuota`.
|
||||
|
||||
This setting supports URL patterns.
|
||||
|
||||
Type: <<types,BoolAsk>>
|
||||
|
||||
Valid values:
|
||||
@ -1967,6 +2075,8 @@ This setting is only available with the QtWebKit backend.
|
||||
=== content.register_protocol_handler
|
||||
Allow websites to register protocol handlers via `navigator.registerProtocolHandler`.
|
||||
|
||||
This setting supports URL patterns.
|
||||
|
||||
Type: <<types,BoolAsk>>
|
||||
|
||||
Valid values:
|
||||
@ -1985,6 +2095,8 @@ On QtWebKit, this setting is unavailable.
|
||||
=== content.ssl_strict
|
||||
Validate SSL handshakes.
|
||||
|
||||
This setting supports URL patterns.
|
||||
|
||||
Type: <<types,BoolAsk>>
|
||||
|
||||
Valid values:
|
||||
@ -2013,14 +2125,22 @@ Type: <<types,Bool>>
|
||||
|
||||
Default: +pass:[true]+
|
||||
|
||||
[[content.webrtc_public_interfaces_only]]
|
||||
=== content.webrtc_public_interfaces_only
|
||||
Only expose public interfaces via WebRTC.
|
||||
On Qt 5.9, this option requires a restart. On Qt 5.10, this option doesn't work at all because of a Qt bug. On Qt >= 5.11, no restart is required.
|
||||
[[content.webrtc_ip_handling_policy]]
|
||||
=== content.webrtc_ip_handling_policy
|
||||
Which interfaces to expose via WebRTC.
|
||||
On Qt 5.10, this option doesn't work because of a Qt bug.
|
||||
This setting requires a restart.
|
||||
|
||||
Type: <<types,Bool>>
|
||||
Type: <<types,String>>
|
||||
|
||||
Default: +pass:[false]+
|
||||
Valid values:
|
||||
|
||||
* +all-interfaces+: WebRTC has the right to enumerate all interfaces and bind them to discover public interfaces.
|
||||
* +default-public-and-private-interfaces+: WebRTC should only use the default route used by http. This also exposes the associated default private address. Default route is the route chosen by the OS on a multi-homed endpoint.
|
||||
* +default-public-interface-only+: WebRTC should only use the default route used by http. This doesn't expose any local addresses.
|
||||
* +disable-non-proxied-udp+: WebRTC should only use TCP to contact peers or servers unless the proxy server supports UDP. This doesn't expose any local addresses either.
|
||||
|
||||
Default: +pass:[all-interfaces]+
|
||||
|
||||
On QtWebEngine, this setting requires Qt 5.9.2 or newer.
|
||||
|
||||
@ -2037,13 +2157,13 @@ Default: +pass:[false]+
|
||||
[[content.xss_auditing]]
|
||||
=== content.xss_auditing
|
||||
Monitor load requests for cross-site scripting attempts.
|
||||
Suspicious scripts will be blocked and reported in the inspector's JavaScript console. Enabling this feature might have an impact on performance.
|
||||
Suspicious scripts will be blocked and reported in the inspector's JavaScript console.
|
||||
|
||||
This setting supports URL patterns.
|
||||
|
||||
Type: <<types,Bool>>
|
||||
|
||||
Default: +pass:[false]+
|
||||
Default: +pass:[true]+
|
||||
|
||||
[[downloads.location.directory]]
|
||||
=== downloads.location.directory
|
||||
@ -2455,6 +2575,76 @@ Type: <<types,Bool>>
|
||||
|
||||
Default: +pass:[true]+
|
||||
|
||||
[[hints.selectors]]
|
||||
=== hints.selectors
|
||||
CSS selectors used to determine which elements on a page should have hints.
|
||||
|
||||
This setting supports URL patterns.
|
||||
|
||||
This setting can only be set in config.py.
|
||||
|
||||
Type: <<types,Dict>>
|
||||
|
||||
Default:
|
||||
|
||||
- +pass:[all]+:
|
||||
|
||||
* +pass:[a]+
|
||||
* +pass:[area]+
|
||||
* +pass:[textarea]+
|
||||
* +pass:[select]+
|
||||
* +pass:[input:not([type="hidden"])]+
|
||||
* +pass:[button]+
|
||||
* +pass:[frame]+
|
||||
* +pass:[iframe]+
|
||||
* +pass:[img]+
|
||||
* +pass:[link]+
|
||||
* +pass:[summary]+
|
||||
* +pass:[[onclick]]+
|
||||
* +pass:[[onmousedown]]+
|
||||
* +pass:[[role="link"]]+
|
||||
* +pass:[[role="option"]]+
|
||||
* +pass:[[role="button"]]+
|
||||
* +pass:[[ng-click]]+
|
||||
* +pass:[[ngClick]]+
|
||||
* +pass:[[data-ng-click]]+
|
||||
* +pass:[[x-ng-click]]+
|
||||
* +pass:[[tabindex]]+
|
||||
- +pass:[images]+:
|
||||
|
||||
* +pass:[img]+
|
||||
- +pass:[inputs]+:
|
||||
|
||||
* +pass:[input[type="text"]]+
|
||||
* +pass:[input[type="date"]]+
|
||||
* +pass:[input[type="datetime-local"]]+
|
||||
* +pass:[input[type="email"]]+
|
||||
* +pass:[input[type="month"]]+
|
||||
* +pass:[input[type="number"]]+
|
||||
* +pass:[input[type="password"]]+
|
||||
* +pass:[input[type="search"]]+
|
||||
* +pass:[input[type="tel"]]+
|
||||
* +pass:[input[type="time"]]+
|
||||
* +pass:[input[type="url"]]+
|
||||
* +pass:[input[type="week"]]+
|
||||
* +pass:[input:not([type])]+
|
||||
* +pass:[textarea]+
|
||||
- +pass:[links]+:
|
||||
|
||||
* +pass:[a[href]]+
|
||||
* +pass:[area[href]]+
|
||||
* +pass:[link[href]]+
|
||||
* +pass:[[role="link"][href]]+
|
||||
- +pass:[media]+:
|
||||
|
||||
* +pass:[audio]+
|
||||
* +pass:[img]+
|
||||
* +pass:[video]+
|
||||
- +pass:[url]+:
|
||||
|
||||
* +pass:[[src]]+
|
||||
* +pass:[[href]]+
|
||||
|
||||
[[hints.uppercase]]
|
||||
=== hints.uppercase
|
||||
Make characters in hint strings uppercase.
|
||||
@ -2699,13 +2889,59 @@ Type: <<types,Bool>>
|
||||
|
||||
Default: +pass:[false]+
|
||||
|
||||
[[qt.low_end_device_mode]]
|
||||
=== qt.low_end_device_mode
|
||||
When to use Chromium's low-end device mode.
|
||||
This improves the RAM usage of renderer processes, at the expense of performance.
|
||||
This setting requires a restart.
|
||||
|
||||
Type: <<types,String>>
|
||||
|
||||
Valid values:
|
||||
|
||||
* +always+: Always use low-end device mode.
|
||||
* +auto+: Decide automatically (uses low-end mode with < 1 GB available RAM).
|
||||
* +never+: Never use low-end device mode.
|
||||
|
||||
Default: +pass:[auto]+
|
||||
|
||||
This setting is only available with the QtWebEngine backend.
|
||||
|
||||
[[qt.process_model]]
|
||||
=== qt.process_model
|
||||
Which Chromium process model to use.
|
||||
Alternative process models use less resources, but decrease security and robustness.
|
||||
See the following pages for more details:
|
||||
|
||||
- https://www.chromium.org/developers/design-documents/process-models
|
||||
- https://doc.qt.io/qt-5/qtwebengine-features.html#process-models
|
||||
This setting requires a restart.
|
||||
|
||||
Type: <<types,String>>
|
||||
|
||||
Valid values:
|
||||
|
||||
* +process-per-site-instance+: Pages from separate sites are put into separate processes and separate visits to the same site are also isolated.
|
||||
* +process-per-site+: Pages from separate sites are put into separate processes. Unlike Process per Site Instance, all visits to the same site will share an OS process. The benefit of this model is reduced memory consumption, because more web pages will share processes. The drawbacks include reduced security, robustness, and responsiveness.
|
||||
* +single-process+: Run all tabs in a single process. This should be used for debugging purposes only, and it disables `:open --private`.
|
||||
|
||||
Default: +pass:[process-per-site-instance]+
|
||||
|
||||
This setting is only available with the QtWebEngine backend.
|
||||
|
||||
[[scrolling.bar]]
|
||||
=== scrolling.bar
|
||||
Show a scrollbar.
|
||||
When to show the scrollbar.
|
||||
|
||||
Type: <<types,Bool>>
|
||||
Type: <<types,String>>
|
||||
|
||||
Default: +pass:[false]+
|
||||
Valid values:
|
||||
|
||||
* +always+: Always show the scrollbar.
|
||||
* +never+: Never show the scrollbar.
|
||||
* +when-searching+: Show the scrollbar when searching for text in the webpage. With the QtWebKit backend, this is equal to `never`.
|
||||
|
||||
Default: +pass:[when-searching]+
|
||||
|
||||
[[scrolling.smooth]]
|
||||
=== scrolling.smooth
|
||||
@ -2972,6 +3208,17 @@ Valid values:
|
||||
|
||||
Default: +pass:[ignore]+
|
||||
|
||||
[[tabs.max_width]]
|
||||
=== tabs.max_width
|
||||
Maximum width (in pixels) of tabs (-1 for no maximum).
|
||||
This setting only applies when tabs are horizontal.
|
||||
This setting does not apply to pinned tabs, unless `tabs.pinned.shrink` is False.
|
||||
This setting may not apply properly if max_width is smaller than the minimum size of tab contents, or smaller than tabs.min_width.
|
||||
|
||||
Type: <<types,Int>>
|
||||
|
||||
Default: +pass:[-1]+
|
||||
|
||||
[[tabs.min_width]]
|
||||
=== tabs.min_width
|
||||
Minimum width (in pixels) of tabs (-1 for the default minimum size behavior).
|
||||
@ -3007,6 +3254,7 @@ Default: +pass:[true]+
|
||||
[[tabs.new_position.related]]
|
||||
=== tabs.new_position.related
|
||||
Position of new tabs opened from another tab.
|
||||
See `tabs.new_position.stacking` for controlling stacking behavior.
|
||||
|
||||
Type: <<types,NewTabPosition>>
|
||||
|
||||
@ -3019,9 +3267,19 @@ Valid values:
|
||||
|
||||
Default: +pass:[next]+
|
||||
|
||||
[[tabs.new_position.stacking]]
|
||||
=== tabs.new_position.stacking
|
||||
Stack related tabs on top of each other when opened consecutively.
|
||||
Only applies for `next` and `prev` values of `tabs.new_position.related` and `tabs.new_position.unrelated`.
|
||||
|
||||
Type: <<types,Bool>>
|
||||
|
||||
Default: +pass:[true]+
|
||||
|
||||
[[tabs.new_position.unrelated]]
|
||||
=== tabs.new_position.unrelated
|
||||
Position of new tabs which aren't opened from another tab.
|
||||
Position of new tabs which are not opened from another tab.
|
||||
See `tabs.new_position.stacking` for controlling stacking behavior.
|
||||
|
||||
Type: <<types,NewTabPosition>>
|
||||
|
||||
@ -3381,7 +3639,7 @@ When setting from a string, pass a json-like list, e.g. `["one", "two"]`.
|
||||
A value can be in one of the following formats: * `#RGB`/`#RRGGBB`/`#RRRGGGBBB`/`#RRRRGGGGBBBB` * An SVG color name as specified in http://www.w3.org/TR/SVG/types.html#ColorKeywords[the W3C specification]. * transparent (no color) * `rgb(r, g, b)` / `rgba(r, g, b, a)` (values 0-255 or percentages) * `hsv(h, s, v)` / `hsva(h, s, v, a)` (values 0-255, hue 0-359) * A gradient as explained in http://doc.qt.io/qt-5/stylesheet-reference.html#list-of-property-types[the Qt documentation] under ``Gradient''
|
||||
|QtColor|A color value.
|
||||
|
||||
A value can be in one of the following formats: * `#RGB`/`#RRGGBB`/`#RRRGGGBBB`/`#RRRRGGGGBBBB` * An SVG color name as specified in http://www.w3.org/TR/SVG/types.html#ColorKeywords[the W3C specification]. * transparent (no color)
|
||||
A value can be in one of the following formats: * `#RGB`/`#RRGGBB`/`#RRRGGGBBB`/`#RRRRGGGGBBBB` * An SVG color name as specified in http://www.w3.org/TR/SVG/types.html#ColorKeywords[the W3C specification]. * transparent (no color) * `rgb(r, g, b)` / `rgba(r, g, b, a)` (values 0-255 or percentages) * `hsv(h, s, v)` / `hsva(h, s, v, a)` (values 0-255, hue 0-359)
|
||||
|QtFont|A font family, with optional style/weight/size.
|
||||
|
||||
* Style: `normal`/`italic`/`oblique` * Weight: `normal`, `bold`, `100`..`900` * Size: _number_ `px`/`pt`
|
||||
@ -3403,5 +3661,8 @@ See the setting's valid values for more information on allowed values.
|
||||
See https://sqlite.org/lang_datefunc.html for reference.
|
||||
|UniqueCharString|A string which may not contain duplicate chars.
|
||||
|Url|A URL as a string.
|
||||
|UrlPattern|A match pattern for a URL.
|
||||
|
||||
See https://developer.chrome.com/apps/match_patterns for the allowed syntax.
|
||||
|VerticalPosition|The position of the download bar.
|
||||
|==============
|
||||
|
@ -222,6 +222,38 @@ There are prebuilt RPMs available at https://software.opensuse.org/download.html
|
||||
|
||||
To use the QtWebEngine backend, install `libqt5-qtwebengine`.
|
||||
|
||||
On Slackware
|
||||
------------
|
||||
|
||||
qutebrowser is available in the 3rd party repository at http://slackbuilds.org[slackbuilds.org]
|
||||
|
||||
An easy way to install it is with sbopkg (frontend for slackbuilds.org) available at http://sbopkg.org[sbopkg.org]
|
||||
|
||||
sbopkg can be run with a dialog screen interface, or via command line options.
|
||||
|
||||
After installing the latest sbopkg package, choose your release version, and sync the repo.
|
||||
|
||||
----
|
||||
sbopkg -V 14.2
|
||||
sbopkg -r
|
||||
----
|
||||
|
||||
The pyPEG2 and MarkupSafe dependencies both need building for python3. You can either set PYTHON3=yes in the shell or set those as options in the dialog menu for each.
|
||||
|
||||
Generate a queue file for qutebrowser and dependencies:
|
||||
|
||||
----
|
||||
sqg -p qutebrowser
|
||||
----
|
||||
|
||||
Then load the queue in the dialog queue menu or via:
|
||||
|
||||
----
|
||||
PYTHON3=yes sbopkg -i qutebrowser
|
||||
----
|
||||
|
||||
If you use the dialog screen you can deselect any already-installed packages that you don't need/want to rebuild before starting the build process.
|
||||
|
||||
On OpenBSD
|
||||
----------
|
||||
|
||||
@ -407,6 +439,14 @@ caveats:
|
||||
(`export LD_LIBRARY_PATH=/usr/lib/openssl-1.0` on Archlinux) before starting
|
||||
qutebrowser if you want SSL to work in certain downloads (e.g. for
|
||||
`:adblock-update` or `:download`).
|
||||
* On Ubuntu (tested on 18.04), you will need to install the `libssl1.0.0`
|
||||
package (`apt install libssl1.0.0`). Then, in the qutebrowser git
|
||||
repository, create a directory named `libssl` (`mkdir libssl`), and link
|
||||
`libcrypto.so.1.0.0` and `libssl.so.1.0.0` into it without the versioning
|
||||
part in their names (`ln -s /usr/lib/x86_64-linux-gnu/libcrypto.so.1.0.0
|
||||
libssl/libcrypto.so` and `ln -s /usr/lib/x86_64-linux-gnu/libssl.so.1.0.0
|
||||
libssl/libssl.so`). Now you can start qutebrowser issuing `export
|
||||
LD_LIBRARY_PATH=$(pwd)/libssl` beforehand.
|
||||
- It comes with a QtWebEngine compiled without proprietary codec support (such
|
||||
as h.264).
|
||||
|
||||
|
@ -45,12 +45,13 @@ 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_COUNT`: The `count` from the spawn command running the userscript.
|
||||
|
||||
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.
|
||||
- `QUTE_SELECTED_HTML`: The HTML of the element selected via hints.
|
||||
|
||||
Sending commands
|
||||
----------------
|
||||
|
@ -1,7 +1,9 @@
|
||||
PYTHON = python3
|
||||
PREFIX = /usr/local
|
||||
DESTDIR =
|
||||
PREFIX ?= /usr/local
|
||||
ICONSIZES = 16 24 32 48 64 128 256 512
|
||||
DATAROOTDIR = $(PREFIX)/share
|
||||
DATADIR ?= $(DATAROOTDIR)
|
||||
MANDIR ?= $(DATAROOTDIR)/man
|
||||
|
||||
SETUPTOOLSOPTIONS =
|
||||
ifdef DESTDIR
|
||||
@ -16,18 +18,18 @@ doc/qutebrowser.1.html:
|
||||
install: doc/qutebrowser.1.html
|
||||
$(PYTHON) setup.py install --prefix="$(PREFIX)" --optimize=1 $(SETUPTOOLSOPTS)
|
||||
install -Dm644 misc/qutebrowser.appdata.xml \
|
||||
"$(DESTDIR)$(PREFIX)/share/metainfo/qutebrowser.appdata.xml"
|
||||
"$(DESTDIR)$(DATADIR)/metainfo/qutebrowser.appdata.xml"
|
||||
install -Dm644 doc/qutebrowser.1 \
|
||||
"$(DESTDIR)$(PREFIX)/share/man/man1/qutebrowser.1"
|
||||
"$(DESTDIR)$(MANDIR)/man1/qutebrowser.1"
|
||||
install -Dm644 misc/qutebrowser.desktop \
|
||||
"$(DESTDIR)$(PREFIX)/share/applications/qutebrowser.desktop"
|
||||
"$(DESTDIR)$(DATADIR)/applications/qutebrowser.desktop"
|
||||
$(foreach i,$(ICONSIZES),install -Dm644 "icons/qutebrowser-$(i)x$(i).png" \
|
||||
"$(DESTDIR)$(PREFIX)/share/icons/hicolor/$(i)x$(i)/apps/qutebrowser.png";)
|
||||
"$(DESTDIR)$(DATADIR)/icons/hicolor/$(i)x$(i)/apps/qutebrowser.png";)
|
||||
install -Dm644 icons/qutebrowser.svg \
|
||||
"$(DESTDIR)$(PREFIX)/share/icons/hicolor/scalable/apps/qutebrowser.svg"
|
||||
install -Dm755 -t "$(DESTDIR)$(PREFIX)/share/qutebrowser/userscripts/" \
|
||||
"$(DESTDIR)$(DATADIR)/icons/hicolor/scalable/apps/qutebrowser.svg"
|
||||
install -Dm755 -t "$(DESTDIR)$(DATADIR)/qutebrowser/userscripts/" \
|
||||
$(wildcard misc/userscripts/*)
|
||||
install -Dm755 -t "$(DESTDIR)$(PREFIX)/share/qutebrowser/scripts/" \
|
||||
install -Dm755 -t "$(DESTDIR)$(DATADIR)/qutebrowser/scripts/" \
|
||||
$(filter-out scripts/__init__.py scripts/__pycache__ scripts/dev \
|
||||
scripts/testbrowser scripts/asciidoc2html.py scripts/setupcommon.py \
|
||||
scripts/link_pyqt.py,$(wildcard scripts/*))
|
||||
|
@ -13,7 +13,7 @@
|
||||
height="682.66669"
|
||||
id="svg2"
|
||||
sodipodi:version="0.32"
|
||||
inkscape:version="0.92.2 5c3e80d, 2017-08-06"
|
||||
inkscape:version="0.92.2 2405546, 2018-03-11"
|
||||
version="1.0"
|
||||
sodipodi:docname="cheatsheet.svg"
|
||||
inkscape:output_extension="org.inkscape.output.svg.inkscape"
|
||||
@ -33,7 +33,7 @@
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="1.7536248"
|
||||
inkscape:cx="430.72917"
|
||||
inkscape:cx="613.20834"
|
||||
inkscape:cy="268.64059"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
@ -3085,7 +3085,9 @@
|
||||
style="font-size:10.66666698px;line-height:1.25;font-family:sans-serif;fill:#000000;stroke-width:1.06666672"
|
||||
id="flowPara4056"> (to index/left/right)</flowPara><flowPara
|
||||
style="font-size:10.66666698px;line-height:1.25;font-family:sans-serif;fill:#000000;stroke-width:1.06666672"
|
||||
id="flowPara3858">gC - clone tab </flowPara><flowPara
|
||||
id="flowPara3858">gC - clone tab</flowPara><flowPara
|
||||
style="font-size:10.66666698px;line-height:1.25;font-family:sans-serif;fill:#000000;stroke-width:1.06666672"
|
||||
id="flowPara6098">gD - detach tab </flowPara><flowPara
|
||||
style="font-size:10.66666698px;line-height:1.25;font-family:sans-serif;fill:#000000;stroke-width:1.06666672"
|
||||
id="flowPara3860">gf - view page source</flowPara><flowPara
|
||||
style="font-size:10.66666698px;line-height:1.25;font-family:sans-serif;fill:#000000;stroke-width:1.06666672"
|
||||
@ -3097,9 +3099,9 @@
|
||||
style="font-size:10.66666698px;line-height:1.25;font-family:sans-serif;fill:#000000;stroke-width:1.06666672"
|
||||
id="flowPara3921">sf - save config</flowPara><flowPara
|
||||
style="font-size:10.66666698px;line-height:1.25;font-family:sans-serif;fill:#000000;stroke-width:1.06666672"
|
||||
id="flowPara3925">ss - set setting</flowPara><flowPara
|
||||
id="flowPara3925">ss - set setting (sl: temp)</flowPara><flowPara
|
||||
style="font-size:10.66666698px;line-height:1.25;font-family:sans-serif;fill:#000000;stroke-width:1.06666672"
|
||||
id="flowPara3927">sl - set temp. setting</flowPara><flowPara
|
||||
id="flowPara3927" /><flowPara
|
||||
style="font-size:10.66666698px;line-height:1.25;font-family:sans-serif;fill:#000000;stroke-width:1.06666672"
|
||||
id="flowPara3929">sk - bind key</flowPara><flowPara
|
||||
style="font-size:10.66666698px;line-height:1.25;font-family:sans-serif;fill:#000000;stroke-width:1.06666672"
|
||||
|
Before Width: | Height: | Size: 181 KiB After Width: | Height: | Size: 181 KiB |
@ -1,6 +1,7 @@
|
||||
[Desktop Entry]
|
||||
Name=qutebrowser
|
||||
GenericName=Web Browser
|
||||
Comment=A keyboard-driven, vim-like browser based on PyQt5
|
||||
Icon=qutebrowser
|
||||
Type=Application
|
||||
Categories=Network;WebBrowser;
|
||||
|
@ -40,6 +40,9 @@ Section "Install"
|
||||
; Uninstall old versions
|
||||
ExecWait 'MsiExec.exe /quiet /qn /norestart /X{633F41F9-FE9B-42D1-9CC4-718CBD01EE11}'
|
||||
ExecWait 'MsiExec.exe /quiet /qn /norestart /X{9331D947-AC86-4542-A755-A833429C6E69}'
|
||||
IfFileExists "$INSTDIR\uninst.exe" 0 +2
|
||||
ExecWait "$INSTDIR\uninst.exe /S _?=$INSTDIR"
|
||||
CreateDirectory "$INSTDIR"
|
||||
|
||||
SetOutPath "$INSTDIR"
|
||||
|
||||
|
@ -19,10 +19,10 @@ def get_data_files():
|
||||
('../qutebrowser/config/configdata.yml', 'config'),
|
||||
]
|
||||
|
||||
# if os.path.exists(os.path.join('qutebrowser', '3rdparty', 'pdfjs')):
|
||||
# data_files.append(('../qutebrowser/3rdparty/pdfjs', '3rdparty/pdfjs'))
|
||||
# else:
|
||||
# print("Warning: excluding pdfjs as it's not present!")
|
||||
if os.path.exists(os.path.join('qutebrowser', '3rdparty', 'pdfjs')):
|
||||
data_files.append(('../qutebrowser/3rdparty/pdfjs', '3rdparty/pdfjs'))
|
||||
else:
|
||||
print("Warning: excluding pdfjs as it's not present!")
|
||||
|
||||
return data_files
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
certifi==2018.4.16
|
||||
certifi==2018.8.24
|
||||
chardet==3.0.4
|
||||
codecov==2.0.15
|
||||
coverage==4.5.1
|
||||
idna==2.7
|
||||
requests==2.18.4
|
||||
urllib3==1.22
|
||||
requests==2.19.1
|
||||
urllib3==1.23
|
||||
|
@ -1,15 +1,15 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
attrs==18.1.0
|
||||
attrs==18.2.0
|
||||
flake8==3.5.0
|
||||
flake8-bugbear==18.2.0
|
||||
flake8-bugbear==18.8.0
|
||||
flake8-builtins==1.4.1 # rq.filter: != 1.4.0
|
||||
flake8-comprehensions==1.4.1
|
||||
flake8-copyright==0.2.0
|
||||
flake8-debugger==3.1.0
|
||||
flake8-deprecated==1.3
|
||||
flake8-docstrings==1.3.0
|
||||
flake8-future-import==0.4.4
|
||||
flake8-future-import==0.4.5
|
||||
flake8-mock==0.3
|
||||
flake8-per-file-ignores==0.6
|
||||
flake8-polyfill==1.0.2
|
||||
@ -24,4 +24,4 @@ pydocstyle==2.1.1
|
||||
pyflakes==2.0.0
|
||||
six==1.11.0
|
||||
snowballstemmer==1.2.1
|
||||
typing==3.6.4
|
||||
typing==3.6.6
|
||||
|
@ -1,8 +1,8 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
appdirs==1.4.3
|
||||
packaging==17.1
|
||||
pyparsing==2.2.0
|
||||
setuptools==39.2.0
|
||||
packaging==18.0
|
||||
pyparsing==2.2.2
|
||||
setuptools==40.4.3
|
||||
six==1.11.0
|
||||
wheel==0.31.1
|
||||
wheel==0.32.1
|
||||
|
@ -1,7 +1,7 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
altgraph==0.15
|
||||
altgraph==0.16.1
|
||||
future==0.16.0
|
||||
macholib==1.9
|
||||
pefile==2017.11.5
|
||||
PyInstaller==3.3.1
|
||||
macholib==1.11
|
||||
pefile==2018.8.8
|
||||
-e git+https://github.com/pyinstaller/pyinstaller.git@develop#egg=PyInstaller
|
||||
|
@ -1 +1,4 @@
|
||||
PyInstaller
|
||||
-e git+https://github.com/pyinstaller/pyinstaller.git@develop#egg=PyInstaller
|
||||
|
||||
# remove @commit-id for scm installs
|
||||
#@ replace: @.*# @develop#
|
||||
|
@ -1,18 +0,0 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
-e git+https://github.com/PyCQA/astroid.git#egg=astroid
|
||||
certifi==2018.4.16
|
||||
chardet==3.0.4
|
||||
github3.py==1.1.0
|
||||
idna==2.7
|
||||
isort==4.3.4
|
||||
lazy-object-proxy==1.3.1
|
||||
mccabe==0.6.1
|
||||
-e git+https://github.com/PyCQA/pylint.git#egg=pylint
|
||||
python-dateutil==2.7.3
|
||||
./scripts/dev/pylint_checkers
|
||||
requests==2.18.4
|
||||
six==1.11.0
|
||||
uritemplate==3.0.0
|
||||
urllib3==1.22
|
||||
wrapt==1.10.11
|
@ -1,11 +0,0 @@
|
||||
-e git+https://github.com/PyCQA/astroid.git#egg=astroid
|
||||
-e git+https://github.com/PyCQA/pylint.git#egg=pylint
|
||||
./scripts/dev/pylint_checkers
|
||||
requests
|
||||
github3.py
|
||||
|
||||
# remove @commit-id for scm installs
|
||||
#@ replace: @.*# #
|
||||
|
||||
# fix qute-pylint location
|
||||
#@ replace: qute-pylint==.* ./scripts/dev/pylint_checkers
|
@ -1,18 +1,23 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
astroid==1.6.5
|
||||
certifi==2018.4.16
|
||||
asn1crypto==0.24.0
|
||||
astroid==2.0.4
|
||||
certifi==2018.8.24
|
||||
cffi==1.11.5
|
||||
chardet==3.0.4
|
||||
github3.py==1.1.0
|
||||
cryptography==2.3.1
|
||||
github3.py==1.2.0
|
||||
idna==2.7
|
||||
isort==4.3.4
|
||||
jwcrypto==0.5.0
|
||||
lazy-object-proxy==1.3.1
|
||||
mccabe==0.6.1
|
||||
pylint==1.9.2
|
||||
pycparser==2.19
|
||||
pylint==2.1.1
|
||||
python-dateutil==2.7.3
|
||||
./scripts/dev/pylint_checkers
|
||||
requests==2.18.4
|
||||
requests==2.19.1
|
||||
six==1.11.0
|
||||
uritemplate==3.0.0
|
||||
urllib3==1.22
|
||||
urllib3==1.23
|
||||
wrapt==1.10.11
|
||||
|
@ -1,4 +0,0 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
PyQt5==5.10 # rq.filter: != 5.10.1
|
||||
sip==4.19.8
|
@ -1,2 +0,0 @@
|
||||
PyQt5==5.10.0
|
||||
#@ filter: PyQt5 != 5.10.1
|
@ -1,4 +1,4 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
PyQt5==5.10.1
|
||||
sip==4.19.8
|
||||
PyQt5==5.11.3
|
||||
PyQt5-sip==4.19.13
|
||||
|
@ -1,4 +1,4 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
docutils==0.14
|
||||
pyroma==2.3.1
|
||||
pyroma==2.4
|
||||
|
@ -35,8 +35,4 @@ git+https://github.com/pallets/markupsafe.git
|
||||
hg+http://bitbucket.org/birkenfeld/pygments-main
|
||||
hg+https://bitbucket.org/fdik/pypeg
|
||||
git+https://github.com/python-attrs/attrs.git
|
||||
|
||||
# Fails to build:
|
||||
# gcc: error: ext/_yaml.c: No such file or directory
|
||||
# hg+https://bitbucket.org/xi/pyyaml
|
||||
PyYAML==3.12
|
||||
git+https://github.com/yaml/pyyaml.git
|
||||
|
@ -1,9 +1,11 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
attrs==18.1.0
|
||||
beautifulsoup4==4.6.0
|
||||
cheroot==6.3.1
|
||||
click==6.7
|
||||
atomicwrites==1.2.1
|
||||
attrs==18.2.0
|
||||
backports.functools-lru-cache==1.5
|
||||
beautifulsoup4==4.6.3
|
||||
cheroot==6.5.2
|
||||
click==7.0
|
||||
# colorama==0.3.9
|
||||
coverage==4.5.1
|
||||
EasyProcess==0.2.3
|
||||
@ -11,30 +13,30 @@ fields==5.0.0
|
||||
Flask==1.0.2
|
||||
glob2==0.6
|
||||
hunter==2.0.2
|
||||
hypothesis==3.57.0
|
||||
hypothesis==3.74.3
|
||||
itsdangerous==0.24
|
||||
# Jinja2==2.10
|
||||
Mako==1.0.7
|
||||
# MarkupSafe==1.0
|
||||
more-itertools==4.2.0
|
||||
parse==1.8.4
|
||||
more-itertools==4.3.0
|
||||
parse==1.9.0
|
||||
parse-type==0.4.2
|
||||
pluggy==0.6.0
|
||||
py==1.5.3
|
||||
pluggy==0.7.1
|
||||
py==1.6.0
|
||||
py-cpuinfo==4.0.0
|
||||
pytest==3.6.1
|
||||
pytest==3.6.4 # rq.filter: <3.7
|
||||
pytest-bdd==2.21.0
|
||||
pytest-benchmark==3.1.1
|
||||
pytest-cov==2.5.1
|
||||
pytest-cov==2.6.0
|
||||
pytest-faulthandler==1.5.0
|
||||
pytest-instafail==0.4.0
|
||||
pytest-mock==1.10.0
|
||||
pytest-qt==2.4.0
|
||||
pytest-repeat==0.4.1
|
||||
pytest-rerunfailures==4.1
|
||||
pytest-qt==3.2.1
|
||||
pytest-repeat==0.7.0
|
||||
pytest-rerunfailures==4.2
|
||||
pytest-travis-fold==1.3.0
|
||||
pytest-xvfb==1.1.0
|
||||
PyVirtualDisplay==0.2.1
|
||||
six==1.11.0
|
||||
vulture==0.27
|
||||
vulture==0.29
|
||||
Werkzeug==0.14.1
|
||||
|
@ -4,7 +4,7 @@ coverage
|
||||
Flask
|
||||
hunter
|
||||
hypothesis
|
||||
pytest
|
||||
pytest<3.7
|
||||
pytest-bdd
|
||||
pytest-benchmark
|
||||
pytest-cov
|
||||
@ -19,3 +19,4 @@ pytest-xvfb
|
||||
vulture
|
||||
|
||||
#@ ignore: Jinja2, MarkupSafe, colorama
|
||||
#@ filter: pytest <3.7
|
||||
|
@ -1,7 +1,8 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
pluggy==0.6.0
|
||||
py==1.5.3
|
||||
pluggy==0.7.1
|
||||
py==1.6.0
|
||||
six==1.11.0
|
||||
tox==3.0.0
|
||||
toml==0.10.0
|
||||
tox==3.5.1
|
||||
virtualenv==16.0.0
|
||||
|
@ -1,3 +1,3 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
vulture==0.27
|
||||
vulture==0.29
|
||||
|
61
misc/userscripts/README.md
Normal file
61
misc/userscripts/README.md
Normal file
@ -0,0 +1,61 @@
|
||||
# Userscripts
|
||||
|
||||
The following userscripts are included in the current directory.
|
||||
|
||||
- [cast](./cast): Cast content on your Chromecast using [castnow][]. Only
|
||||
[youtube-dl][] downloadable content.
|
||||
- [dmenu_qutebrowser](./dmenu_qutebrowser): Pipes history, quickmarks, and URL into dmenu.
|
||||
- [format_json](./format_json): Pretty prints current page's JSON code in other
|
||||
tab.
|
||||
- [getbib](./getbib): Scraping the current web page for DOIs and downloading
|
||||
corresponding bibtex information.
|
||||
- [open_download](./open_download): Opens a rofi menu with
|
||||
all files from the download directory and opens the selected file.
|
||||
- [openfeeds](./openfeeds): Opens all links to feeds defined in the head of a site.
|
||||
- [password_fill](./password_fill): Find a username/password entry and fill it
|
||||
with credentials given by the configured backend (currently only pass) for the
|
||||
current website.
|
||||
- [qute-keepass](./qute-keepass): Insertion of usernames and passwords from keepass
|
||||
databases using pykeepass.
|
||||
- [qute-pass](./qute-pass): Insert login information using pass and a
|
||||
dmenu-compatible application (e.g. dmenu, rofi -dmenu, ...).
|
||||
- [qutedmenu](./qutedmenu): Handle open -s && open -t with bemenu.
|
||||
- [readability](./readability): Executes python-readability on current page and
|
||||
opens the summary as new tab.
|
||||
- [ripbang](./ripbang): Adds DuckDuckGo bang as searchengine.
|
||||
- [rss](./rss): Keeps track of URLs in RSS feeds and opens new ones.
|
||||
- [taskadd](./taskadd): Adds a task to taskwarrior.
|
||||
- [tor_identity](./tor_identity): Change your tor identity.
|
||||
- [view_in_mpv](./view_in_mpv): Views the current web page in mpv using
|
||||
sensible mpv-flags.
|
||||
|
||||
[castnow]: https://github.com/xat/castnow
|
||||
[youtube-dl]: https://rg3.github.io/youtube-dl/
|
||||
|
||||
## Others
|
||||
|
||||
The following userscripts can be found on their own repositories.
|
||||
|
||||
- [qurlshare](https://github.com/sim590/qurlshare): *secure* sharing of an URL between qutebrowser
|
||||
instances using a distributed hash table.
|
||||
- [qutebrowser-userscripts](https://github.com/cryzed/qutebrowser-userscripts):
|
||||
a small pack of userscripts.
|
||||
- [qutebrowser-zotero](https://github.com/parchd-1/qutebrowser-zotero): connects
|
||||
qutebrowser to [Zotero][] standalone.
|
||||
- [qute.match](https://github.com/bziur/qute.match): execute script based on
|
||||
visisted url.
|
||||
- [qutepocket](https://github.com/kepi/qutepocket): Add URL to your [Pocket][]
|
||||
bookmark manager.
|
||||
- [qb-scripts](https://github.com/peterjschroeder/qb-scripts): a small pack of
|
||||
userscripts.
|
||||
- [instapaper.zsh](https://github.com/dmcgrady/instapaper.zsh): Add URL to
|
||||
your [Instapaper][] bookmark manager.
|
||||
- [qtb.us](https://github.com/Chinggis6/qtb.us): small pack of userscripts.
|
||||
- [pinboard.zsh](https://github.com/dmix/pinboard.zsh): Add URL to your
|
||||
[Pinboard][] bookmark manager.
|
||||
|
||||
[Zotero]: https://www.zotero.org/
|
||||
[Pocket]: https://getpocket.com/
|
||||
[Instapaper]: https://www.instapaper.com/
|
||||
[Pinboard]: https://pinboard.in/
|
||||
|
@ -25,9 +25,17 @@ demonstration can be seen here: https://i.imgur.com/KN3XuZP.gif.
|
||||
USAGE = """The domain of the site has to appear as a segment in the pass path, for example: "github.com/cryzed" or
|
||||
"websites/github.com". How the username and password are determined is freely configurable using the CLI arguments. The
|
||||
login information is inserted by emulating key events using qutebrowser's fake-key command in this manner:
|
||||
[USERNAME]<Tab>[PASSWORD], which is compatible with almost all login forms."""
|
||||
[USERNAME]<Tab>[PASSWORD], which is compatible with almost all login forms.
|
||||
|
||||
EPILOG = """Dependencies: tldextract (Python 3 module), pass.
|
||||
Suggested bindings similar to Uzbl's `formfiller` script:
|
||||
|
||||
config.bind('<z><l>', 'spawn --userscript qute-pass')
|
||||
config.bind('<z><u><l>', 'spawn --userscript qute-pass --username-only')
|
||||
config.bind('<z><p><l>', 'spawn --userscript qute-pass --password-only')
|
||||
config.bind('<z><o><l>', 'spawn --userscript qute-pass --otp-only')
|
||||
"""
|
||||
|
||||
EPILOG = """Dependencies: tldextract (Python 3 module), pass, pass-otp (optional).
|
||||
For issues and feedback please use: https://github.com/cryzed/qutebrowser-userscripts.
|
||||
|
||||
WARNING: The login details are viewable as plaintext in qutebrowser's debug log (qute://log) and might be shared if
|
||||
@ -66,6 +74,7 @@ argument_parser.add_argument('--merge-candidates', '-m', action='store_true',
|
||||
group = argument_parser.add_mutually_exclusive_group()
|
||||
group.add_argument('--username-only', '-e', action='store_true', help='Only insert username')
|
||||
group.add_argument('--password-only', '-w', action='store_true', help='Only insert password')
|
||||
group.add_argument('--otp-only', '-o', action='store_true', help='Only insert OTP code')
|
||||
|
||||
stderr = functools.partial(print, file=sys.stderr)
|
||||
|
||||
@ -87,7 +96,7 @@ def qute_command(command):
|
||||
|
||||
def find_pass_candidates(domain, password_store_path):
|
||||
candidates = []
|
||||
for path, directories, file_names in os.walk(password_store_path):
|
||||
for path, directories, file_names in os.walk(password_store_path, followlinks=True):
|
||||
if directories or domain not in path.split(os.path.sep):
|
||||
continue
|
||||
|
||||
@ -98,11 +107,19 @@ def find_pass_candidates(domain, password_store_path):
|
||||
return candidates
|
||||
|
||||
|
||||
def pass_(path, encoding):
|
||||
process = subprocess.run(['pass', path], stdout=subprocess.PIPE)
|
||||
def _run_pass(command, encoding):
|
||||
process = subprocess.run(command, stdout=subprocess.PIPE)
|
||||
return process.stdout.decode(encoding).strip()
|
||||
|
||||
|
||||
def pass_(path, encoding):
|
||||
return _run_pass(['pass', path], encoding)
|
||||
|
||||
|
||||
def pass_otp(path, encoding):
|
||||
return _run_pass(['pass', 'otp', path], encoding)
|
||||
|
||||
|
||||
def dmenu(items, invocation, encoding):
|
||||
command = shlex.split(invocation)
|
||||
process = subprocess.run(command, input='\n'.join(items).encode(encoding), stdout=subprocess.PIPE)
|
||||
@ -152,7 +169,7 @@ def main(arguments):
|
||||
|
||||
# Match username
|
||||
target = selection if arguments.username_target == 'path' else secret
|
||||
match = re.match(arguments.username_pattern, target)
|
||||
match = re.search(arguments.username_pattern, target, re.MULTILINE)
|
||||
if not match:
|
||||
stderr('Failed to match username pattern on {}!'.format(arguments.username_target))
|
||||
return ExitCodes.COULD_NOT_MATCH_USERNAME
|
||||
@ -169,6 +186,9 @@ def main(arguments):
|
||||
fake_key_raw(username)
|
||||
elif arguments.password_only:
|
||||
fake_key_raw(password)
|
||||
elif arguments.otp_only:
|
||||
otp = pass_otp(selection, arguments.io_encoding)
|
||||
fake_key_raw(otp)
|
||||
else:
|
||||
# Enter username and password using fake-key and <Tab> (which seems to work almost universally), then switch
|
||||
# back into insert-mode, so the form can be directly submitted by hitting enter afterwards
|
||||
|
@ -63,4 +63,8 @@ qt_log_ignore =
|
||||
^inotify_add_watch\(".*"\) failed: "No space left on device"
|
||||
^QSettings::value: Empty key passed
|
||||
^Icon theme ".*" not found
|
||||
^Error receiving trust for a CA certificate
|
||||
xfail_strict = true
|
||||
filterwarnings =
|
||||
# This happens in many qutebrowser dependencies...
|
||||
ignore:Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated, and in 3.8 it will stop working:DeprecationWarning
|
||||
|
@ -26,7 +26,7 @@ __copyright__ = "Copyright 2014-2018 Florian Bruhin (The Compiler)"
|
||||
__license__ = "GPL"
|
||||
__maintainer__ = __author__
|
||||
__email__ = "mail@qutebrowser.org"
|
||||
__version_info__ = (1, 3, 2)
|
||||
__version_info__ = (1, 5, 1)
|
||||
__version__ = '.'.join(str(e) for e in __version_info__)
|
||||
__description__ = "A keyboard-driven, vim-like browser based on PyQt5."
|
||||
|
||||
|
@ -59,7 +59,6 @@ except ImportError:
|
||||
|
||||
import qutebrowser
|
||||
import qutebrowser.resources
|
||||
from qutebrowser.completion import completiondelegate
|
||||
from qutebrowser.completion.models import miscmodels
|
||||
from qutebrowser.commands import cmdutils, runners, cmdexc
|
||||
from qutebrowser.config import config, websettings, configfiles, configinit
|
||||
@ -72,9 +71,9 @@ from qutebrowser.keyinput import macros
|
||||
from qutebrowser.mainwindow import mainwindow, prompt
|
||||
from qutebrowser.misc import (readline, ipc, savemanager, sessions,
|
||||
crashsignal, earlyinit, sql, cmdhistory,
|
||||
backendproblem)
|
||||
backendproblem, objects)
|
||||
from qutebrowser.utils import (log, version, message, utils, urlutils, objreg,
|
||||
usertypes, standarddir, error)
|
||||
usertypes, standarddir, error, qtutils)
|
||||
# pylint: disable=unused-import
|
||||
# We import those to run the cmdutils.register decorators.
|
||||
from qutebrowser.mainwindow.statusbar import command
|
||||
@ -104,6 +103,7 @@ def run(args):
|
||||
qApp = Application(args)
|
||||
qApp.setOrganizationName("qutebrowser")
|
||||
qApp.setApplicationName("qutebrowser")
|
||||
qApp.setDesktopFileName("qutebrowser")
|
||||
qApp.setApplicationVersion(qutebrowser.__version__)
|
||||
qApp.lastWindowClosed.connect(quitter.on_last_window_closed)
|
||||
|
||||
@ -129,6 +129,9 @@ def run(args):
|
||||
sys.exit(usertypes.Exit.err_ipc)
|
||||
|
||||
if server is None:
|
||||
if args.backend is not None:
|
||||
log.init.warning(
|
||||
"Backend from the running instance will be used")
|
||||
sys.exit(usertypes.Exit.ok)
|
||||
else:
|
||||
server.got_args.connect(lambda args, target_arg, cwd:
|
||||
@ -181,8 +184,6 @@ def init(args, crash_handler):
|
||||
QDesktopServices.setUrlHandler('https', open_desktopservices_url)
|
||||
QDesktopServices.setUrlHandler('qute', open_desktopservices_url)
|
||||
|
||||
objreg.get('web-history').import_txt()
|
||||
|
||||
log.init.debug("Init done!")
|
||||
crash_handler.raise_crashdlg()
|
||||
|
||||
@ -349,10 +350,6 @@ def _open_startpage(win_id=None):
|
||||
def _open_special_pages(args):
|
||||
"""Open special notification pages which are only shown once.
|
||||
|
||||
Currently this is:
|
||||
- Quickstart page if it's the first start.
|
||||
- Legacy QtWebKit warning if needed.
|
||||
|
||||
Args:
|
||||
args: The argparse namespace.
|
||||
"""
|
||||
@ -364,25 +361,30 @@ def _open_special_pages(args):
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window='last-focused')
|
||||
|
||||
# Quickstart page
|
||||
pages = [
|
||||
# state, condition, URL
|
||||
('quickstart-done',
|
||||
True,
|
||||
'https://www.qutebrowser.org/quickstart.html'),
|
||||
|
||||
quickstart_done = general_sect.get('quickstart-done') == '1'
|
||||
('config-migration-shown',
|
||||
os.path.exists(os.path.join(standarddir.config(),
|
||||
'qutebrowser.conf')),
|
||||
'qute://help/configuring.html'),
|
||||
|
||||
if not quickstart_done:
|
||||
tabbed_browser.tabopen(
|
||||
QUrl('https://www.qutebrowser.org/quickstart.html'))
|
||||
general_sect['quickstart-done'] = '1'
|
||||
('webkit-warning-shown',
|
||||
objects.backend == usertypes.Backend.QtWebKit,
|
||||
'qute://warning/webkit'),
|
||||
|
||||
# Setting migration page
|
||||
('old-qt-warning-shown',
|
||||
not qtutils.version_check('5.9'),
|
||||
'qute://warning/old-qt'),
|
||||
]
|
||||
|
||||
needs_migration = os.path.exists(
|
||||
os.path.join(standarddir.config(), 'qutebrowser.conf'))
|
||||
migration_shown = general_sect.get('config-migration-shown') == '1'
|
||||
|
||||
if needs_migration and not migration_shown:
|
||||
tabbed_browser.tabopen(QUrl('qute://help/configuring.html'),
|
||||
background=False)
|
||||
general_sect['config-migration-shown'] = '1'
|
||||
for state, condition, url in pages:
|
||||
if general_sect.get(state) != '1' and condition:
|
||||
tabbed_browser.tabopen(QUrl(url), background=False)
|
||||
general_sect[state] = '1'
|
||||
|
||||
|
||||
def on_focus_changed(_old, new):
|
||||
@ -445,16 +447,10 @@ def _init_modules(args, crash_handler):
|
||||
|
||||
log.init.debug("Initializing web history...")
|
||||
history.init(qApp)
|
||||
except sql.SqlError as e:
|
||||
if e.environmental:
|
||||
error.handle_fatal_exc(e, args, 'Error initializing SQL',
|
||||
pre_text='Error initializing SQL')
|
||||
sys.exit(usertypes.Exit.err_init)
|
||||
else:
|
||||
raise
|
||||
|
||||
log.init.debug("Initializing completion...")
|
||||
completiondelegate.init()
|
||||
except sql.SqlEnvironmentError as e:
|
||||
error.handle_fatal_exc(e, args, 'Error initializing SQL',
|
||||
pre_text='Error initializing SQL')
|
||||
sys.exit(usertypes.Exit.err_init)
|
||||
|
||||
log.init.debug("Initializing command history...")
|
||||
cmdhistory.init()
|
||||
|
@ -24,7 +24,6 @@ import os.path
|
||||
import functools
|
||||
import posixpath
|
||||
import zipfile
|
||||
import fnmatch
|
||||
|
||||
from qutebrowser.browser import downloads
|
||||
from qutebrowser.config import config
|
||||
@ -32,7 +31,7 @@ from qutebrowser.utils import objreg, standarddir, log, message
|
||||
from qutebrowser.commands import cmdutils
|
||||
|
||||
|
||||
def guess_zip_filename(zf):
|
||||
def _guess_zip_filename(zf):
|
||||
"""Guess which file to use inside a zip file.
|
||||
|
||||
Args:
|
||||
@ -54,26 +53,26 @@ def get_fileobj(byte_io):
|
||||
if zipfile.is_zipfile(byte_io):
|
||||
byte_io.seek(0) # rewind what zipfile.is_zipfile did
|
||||
zf = zipfile.ZipFile(byte_io)
|
||||
filename = guess_zip_filename(zf)
|
||||
filename = _guess_zip_filename(zf)
|
||||
byte_io = zf.open(filename, mode='r')
|
||||
else:
|
||||
byte_io.seek(0) # rewind what zipfile.is_zipfile did
|
||||
return byte_io
|
||||
|
||||
|
||||
def is_whitelisted_host(host):
|
||||
"""Check if the given host is on the adblock whitelist.
|
||||
def _is_whitelisted_url(url):
|
||||
"""Check if the given URL is on the adblock whitelist.
|
||||
|
||||
Args:
|
||||
host: The host of the request as string.
|
||||
url: The URL to check as QUrl.
|
||||
"""
|
||||
for pattern in config.val.content.host_blocking.whitelist:
|
||||
if fnmatch.fnmatch(host, pattern.lower()):
|
||||
if pattern.matches(url):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class FakeDownload:
|
||||
class _FakeDownload:
|
||||
|
||||
"""A download stub to use on_download_finished with local files."""
|
||||
|
||||
@ -111,14 +110,18 @@ class HostBlocker:
|
||||
|
||||
config.instance.changed.connect(self._update_files)
|
||||
|
||||
def is_blocked(self, url):
|
||||
def is_blocked(self, url, first_party_url=None):
|
||||
"""Check if the given URL (as QUrl) is blocked."""
|
||||
if not config.val.content.host_blocking.enabled:
|
||||
if first_party_url is not None and not first_party_url.isValid():
|
||||
first_party_url = None
|
||||
if not config.instance.get('content.host_blocking.enabled',
|
||||
url=first_party_url):
|
||||
return False
|
||||
|
||||
host = url.host()
|
||||
return ((host in self._blocked_hosts or
|
||||
host in self._config_blocked_hosts) and
|
||||
not is_whitelisted_host(host))
|
||||
not _is_whitelisted_url(url))
|
||||
|
||||
def _read_hosts_file(self, filename, target):
|
||||
"""Read hosts from the given filename.
|
||||
@ -174,15 +177,12 @@ class HostBlocker:
|
||||
for url in config.val.content.host_blocking.lists:
|
||||
if url.scheme() == 'file':
|
||||
filename = url.toLocalFile()
|
||||
try:
|
||||
fileobj = open(filename, 'rb')
|
||||
except OSError as e:
|
||||
message.error("adblock: Error while reading {}: {}".format(
|
||||
filename, e.strerror))
|
||||
continue
|
||||
download = FakeDownload(fileobj)
|
||||
self._in_progress.append(download)
|
||||
self.on_download_finished(download)
|
||||
if os.path.isdir(filename):
|
||||
for entry in os.scandir(filename):
|
||||
if entry.is_file():
|
||||
self._import_local(entry.path)
|
||||
else:
|
||||
self._import_local(filename)
|
||||
else:
|
||||
fobj = io.BytesIO()
|
||||
fobj.name = 'adblock: ' + url.host()
|
||||
@ -191,7 +191,23 @@ class HostBlocker:
|
||||
auto_remove=True)
|
||||
self._in_progress.append(download)
|
||||
download.finished.connect(
|
||||
functools.partial(self.on_download_finished, download))
|
||||
functools.partial(self._on_download_finished, download))
|
||||
|
||||
def _import_local(self, filename):
|
||||
"""Adds the contents of a file to the blocklist.
|
||||
|
||||
Args:
|
||||
filename: path to a local file to import.
|
||||
"""
|
||||
try:
|
||||
fileobj = open(filename, 'rb')
|
||||
except OSError as e:
|
||||
message.error("adblock: Error while reading {}: {}".format(
|
||||
filename, e.strerror))
|
||||
return
|
||||
download = _FakeDownload(fileobj)
|
||||
self._in_progress.append(download)
|
||||
self._on_download_finished(download)
|
||||
|
||||
def _parse_line(self, line):
|
||||
"""Parse a line from a host file.
|
||||
@ -234,7 +250,9 @@ class HostBlocker:
|
||||
hosts = parts[1:]
|
||||
|
||||
for host in hosts:
|
||||
if '.' in host and not host.endswith('.localdomain'):
|
||||
if ('.' in host and
|
||||
not host.endswith('.localdomain') and
|
||||
host != '0.0.0.0'):
|
||||
self._blocked_hosts.add(host)
|
||||
|
||||
return True
|
||||
@ -269,7 +287,7 @@ class HostBlocker:
|
||||
message.error("adblock: {} read errors for {}".format(
|
||||
error_count, byte_io.name))
|
||||
|
||||
def on_lists_downloaded(self):
|
||||
def _on_lists_downloaded(self):
|
||||
"""Install block lists after files have been downloaded."""
|
||||
with open(self._local_hosts_file, 'w', encoding='utf-8') as f:
|
||||
for host in sorted(self._blocked_hosts):
|
||||
@ -288,7 +306,7 @@ class HostBlocker:
|
||||
except OSError as e:
|
||||
log.misc.exception("Failed to delete hosts file: {}".format(e))
|
||||
|
||||
def on_download_finished(self, download):
|
||||
def _on_download_finished(self, download):
|
||||
"""Check if all downloads are finished and if so, trigger reading.
|
||||
|
||||
Arguments:
|
||||
@ -303,6 +321,6 @@ class HostBlocker:
|
||||
download.fileobj.close()
|
||||
if not self._in_progress:
|
||||
try:
|
||||
self.on_lists_downloaded()
|
||||
self._on_lists_downloaded()
|
||||
except OSError:
|
||||
log.misc.exception("Failed to write host block list!")
|
||||
|
@ -22,11 +22,11 @@
|
||||
import enum
|
||||
import itertools
|
||||
|
||||
import sip
|
||||
import attr
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QUrl, QObject, QSizeF, Qt
|
||||
from PyQt5.QtGui import QIcon
|
||||
from PyQt5.QtWidgets import QWidget, QApplication
|
||||
from PyQt5.QtWidgets import QWidget, QApplication, QDialog
|
||||
from PyQt5.QtPrintSupport import QPrintDialog
|
||||
|
||||
import pygments
|
||||
import pygments.lexers
|
||||
@ -38,6 +38,7 @@ from qutebrowser.utils import (utils, objreg, usertypes, log, qtutils,
|
||||
urlutils, message)
|
||||
from qutebrowser.misc import miscwidgets, objects
|
||||
from qutebrowser.browser import mouse, hints
|
||||
from qutebrowser.qt import sip
|
||||
|
||||
|
||||
tab_id_gen = itertools.count(0)
|
||||
@ -187,8 +188,9 @@ class AbstractPrinting:
|
||||
|
||||
"""Attribute of AbstractTab for printing the page."""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, tab):
|
||||
self._widget = None
|
||||
self._tab = tab
|
||||
|
||||
def check_pdf_support(self):
|
||||
raise NotImplementedError
|
||||
@ -212,6 +214,29 @@ class AbstractPrinting:
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def show_dialog(self):
|
||||
"""Print with a QPrintDialog."""
|
||||
self.check_printer_support()
|
||||
|
||||
def print_callback(ok):
|
||||
"""Called when printing finished."""
|
||||
if not ok:
|
||||
message.error("Printing failed!")
|
||||
diag.deleteLater()
|
||||
|
||||
def do_print():
|
||||
"""Called when the dialog was closed."""
|
||||
self.to_printer(diag.printer(), print_callback)
|
||||
|
||||
diag = QPrintDialog(self._tab)
|
||||
if utils.is_mac:
|
||||
# For some reason we get a segfault when using open() on macOS
|
||||
ret = diag.exec_()
|
||||
if ret == QDialog.Accepted:
|
||||
do_print()
|
||||
else:
|
||||
diag.open(do_print)
|
||||
|
||||
|
||||
class AbstractSearch(QObject):
|
||||
|
||||
@ -223,10 +248,19 @@ class AbstractSearch(QObject):
|
||||
this view.
|
||||
_flags: The flags of the last search (needs to be set by subclasses).
|
||||
_widget: The underlying WebView widget.
|
||||
|
||||
Signals:
|
||||
finished: Emitted when a search was finished.
|
||||
arg: True if the text was found, False otherwise.
|
||||
cleared: Emitted when an existing search was cleared.
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
finished = pyqtSignal(bool)
|
||||
cleared = pyqtSignal()
|
||||
|
||||
def __init__(self, tab, parent=None):
|
||||
super().__init__(parent)
|
||||
self._tab = tab
|
||||
self._widget = None
|
||||
self.text = None
|
||||
self.search_displayed = False
|
||||
@ -370,9 +404,11 @@ class AbstractCaret(QObject):
|
||||
Signals:
|
||||
selection_toggled: Emitted when the selection was toggled.
|
||||
arg: Whether the selection is now active.
|
||||
follow_selected_done: Emitted when a follow_selection action is done.
|
||||
"""
|
||||
|
||||
selection_toggled = pyqtSignal(bool)
|
||||
follow_selected_done = pyqtSignal()
|
||||
|
||||
def __init__(self, tab, mode_manager, parent=None):
|
||||
super().__init__(parent)
|
||||
@ -596,6 +632,9 @@ class AbstractElements:
|
||||
def find_css(self, selector, callback, *, only_visible=False):
|
||||
"""Find all HTML elements matching a given selector async.
|
||||
|
||||
If there's an error, the callback is called with a webelem.Error
|
||||
instance.
|
||||
|
||||
Args:
|
||||
callback: The callback to be called when the search finished.
|
||||
selector: The CSS selector to search for.
|
||||
@ -641,20 +680,27 @@ class AbstractAudio(QObject):
|
||||
muted_changed = pyqtSignal(bool)
|
||||
recently_audible_changed = pyqtSignal(bool)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
def __init__(self, tab, parent=None):
|
||||
super().__init__(parent)
|
||||
self._widget = None
|
||||
self._tab = tab
|
||||
|
||||
def set_muted(self, muted: bool):
|
||||
"""Set this tab as muted or not."""
|
||||
def set_muted(self, muted: bool, override: bool = False):
|
||||
"""Set this tab as muted or not.
|
||||
|
||||
Arguments:
|
||||
override: If set to True, muting/unmuting was done manually and
|
||||
overrides future automatic mute/unmute changes based on
|
||||
the URL.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def is_muted(self):
|
||||
"""Whether this tab is muted."""
|
||||
raise NotImplementedError
|
||||
|
||||
def toggle_muted(self):
|
||||
self.set_muted(not self.is_muted())
|
||||
def toggle_muted(self, *, override: bool = False):
|
||||
self.set_muted(not self.is_muted(), override=override)
|
||||
|
||||
def is_recently_audible(self):
|
||||
"""Whether this tab has had audio playing recently."""
|
||||
@ -829,12 +875,20 @@ class AbstractTab(QWidget):
|
||||
navigation.navigation_type,
|
||||
navigation.is_main_frame))
|
||||
|
||||
if (navigation.navigation_type == navigation.Type.link_clicked and
|
||||
not navigation.url.isValid()):
|
||||
msg = urlutils.get_errstring(navigation.url,
|
||||
"Invalid link clicked")
|
||||
message.error(msg)
|
||||
self.data.open_target = usertypes.ClickTarget.normal
|
||||
if not navigation.url.isValid():
|
||||
# Also a WORKAROUND for missing IDNA 2008 support in QUrl, see
|
||||
# https://bugreports.qt.io/browse/QTBUG-60364
|
||||
|
||||
if navigation.navigation_type == navigation.Type.link_clicked:
|
||||
msg = urlutils.get_errstring(navigation.url,
|
||||
"Invalid link clicked")
|
||||
message.error(msg)
|
||||
self.data.open_target = usertypes.ClickTarget.normal
|
||||
|
||||
log.webview.debug("Ignoring invalid URL {} in "
|
||||
"acceptNavigationRequest: {}".format(
|
||||
navigation.url.toDisplayString(),
|
||||
navigation.url.errorString()))
|
||||
navigation.accepted = False
|
||||
|
||||
def handle_auto_insert_mode(self, ok):
|
||||
@ -893,10 +947,6 @@ class AbstractTab(QWidget):
|
||||
self._progress = perc
|
||||
self.load_progress.emit(perc)
|
||||
|
||||
@pyqtSlot()
|
||||
def _on_ssl_errors(self):
|
||||
self._has_ssl_errors = True
|
||||
|
||||
def url(self, requested=False):
|
||||
raise NotImplementedError
|
||||
|
||||
|
@ -25,9 +25,9 @@ import shlex
|
||||
import functools
|
||||
import typing
|
||||
|
||||
from PyQt5.QtWidgets import QApplication, QTabBar, QDialog
|
||||
from PyQt5.QtWidgets import QApplication, QTabBar
|
||||
from PyQt5.QtCore import pyqtSlot, Qt, QUrl, QEvent, QUrlQuery
|
||||
from PyQt5.QtPrintSupport import QPrintDialog, QPrintPreviewDialog
|
||||
from PyQt5.QtPrintSupport import QPrintPreviewDialog
|
||||
|
||||
from qutebrowser.commands import userscripts, cmdexc, cmdutils, runners
|
||||
from qutebrowser.config import config, configdata
|
||||
@ -66,6 +66,11 @@ class CommandDispatcher:
|
||||
|
||||
def _new_tabbed_browser(self, private):
|
||||
"""Get a tabbed-browser from a new window."""
|
||||
args = QApplication.instance().arguments()
|
||||
if private and '--single-process' in args:
|
||||
raise cmdexc.CommandError("Private windows are unavailable with "
|
||||
"the single-process process model.")
|
||||
|
||||
new_window = mainwindow.MainWindow(private=private)
|
||||
new_window.show()
|
||||
return new_window.tabbed_browser
|
||||
@ -415,27 +420,6 @@ class CommandDispatcher:
|
||||
tab.printing.to_pdf(filename)
|
||||
log.misc.debug("Print to file: {}".format(filename))
|
||||
|
||||
def _print(self, tab):
|
||||
"""Print with a QPrintDialog."""
|
||||
def print_callback(ok):
|
||||
"""Called when printing finished."""
|
||||
if not ok:
|
||||
message.error("Printing failed!")
|
||||
diag.deleteLater()
|
||||
|
||||
def do_print():
|
||||
"""Called when the dialog was closed."""
|
||||
tab.printing.to_printer(diag.printer(), print_callback)
|
||||
|
||||
diag = QPrintDialog(tab)
|
||||
if utils.is_mac:
|
||||
# For some reason we get a segfault when using open() on macOS
|
||||
ret = diag.exec_()
|
||||
if ret == QDialog.Accepted:
|
||||
do_print()
|
||||
else:
|
||||
diag.open(do_print)
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', name='print',
|
||||
scope='window')
|
||||
@cmdutils.argument('count', count=True)
|
||||
@ -453,22 +437,15 @@ class CommandDispatcher:
|
||||
return
|
||||
|
||||
try:
|
||||
if pdf:
|
||||
tab.printing.check_pdf_support()
|
||||
else:
|
||||
tab.printing.check_printer_support()
|
||||
if preview:
|
||||
tab.printing.check_preview_support()
|
||||
self._print_preview(tab)
|
||||
elif pdf:
|
||||
self._print_pdf(tab, pdf)
|
||||
else:
|
||||
tab.printing.show_dialog()
|
||||
except browsertab.WebTabError as e:
|
||||
raise cmdexc.CommandError(e)
|
||||
|
||||
if preview:
|
||||
self._print_preview(tab)
|
||||
elif pdf:
|
||||
self._print_pdf(tab, pdf)
|
||||
else:
|
||||
self._print(tab)
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window')
|
||||
def tab_clone(self, bg=False, window=False):
|
||||
"""Duplicate the current tab.
|
||||
@ -513,14 +490,16 @@ class CommandDispatcher:
|
||||
new_tabbed_browser.widget.set_tab_pinned(newtab, curtab.data.pinned)
|
||||
return newtab
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window')
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window',
|
||||
maxsplit=0)
|
||||
@cmdutils.argument('index', completion=miscmodels.other_buffer)
|
||||
def tab_take(self, index):
|
||||
def tab_take(self, index, keep=False):
|
||||
"""Take a tab from another window.
|
||||
|
||||
Args:
|
||||
index: The [win_id/]index of the tab to take. Or a substring
|
||||
in which case the closest match will be taken.
|
||||
keep: If given, keep the old tab around.
|
||||
"""
|
||||
tabbed_browser, tab = self._resolve_buffer_index(index)
|
||||
|
||||
@ -528,18 +507,20 @@ class CommandDispatcher:
|
||||
raise cmdexc.CommandError("Can't take a tab from the same window")
|
||||
|
||||
self._open(tab.url(), tab=True)
|
||||
tabbed_browser.close_tab(tab, add_undo=False)
|
||||
if not keep:
|
||||
tabbed_browser.close_tab(tab, add_undo=False)
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window')
|
||||
@cmdutils.argument('win_id', completion=miscmodels.window)
|
||||
@cmdutils.argument('count', count=True)
|
||||
def tab_give(self, win_id: int = None, count=None):
|
||||
def tab_give(self, win_id: int = None, keep=False, count=None):
|
||||
"""Give the current tab to a new or existing window if win_id given.
|
||||
|
||||
If no win_id is given, the tab will get detached into a new window.
|
||||
|
||||
Args:
|
||||
win_id: The window ID of the window to give the current tab to.
|
||||
keep: If given, keep the old tab around.
|
||||
count: Overrides win_id (index starts at 1 for win_id=0).
|
||||
"""
|
||||
if count is not None:
|
||||
@ -549,7 +530,7 @@ class CommandDispatcher:
|
||||
raise cmdexc.CommandError("Can't give a tab to the same window")
|
||||
|
||||
if win_id is None:
|
||||
if self._count() < 2:
|
||||
if self._count() < 2 and not keep:
|
||||
raise cmdexc.CommandError("Cannot detach from a window with "
|
||||
"only one tab")
|
||||
|
||||
@ -564,7 +545,9 @@ class CommandDispatcher:
|
||||
window=win_id)
|
||||
|
||||
tabbed_browser.tabopen(self._current_url())
|
||||
self._tabbed_browser.close_tab(self._current_widget(), add_undo=False)
|
||||
if not keep:
|
||||
self._tabbed_browser.close_tab(self._current_widget(),
|
||||
add_undo=False)
|
||||
|
||||
def _back_forward(self, tab, bg, window, count, forward):
|
||||
"""Helper function for :back/:forward."""
|
||||
@ -634,11 +617,11 @@ class CommandDispatcher:
|
||||
- `up`: Go up a level in the current URL.
|
||||
- `increment`: Increment the last number in the URL.
|
||||
Uses the
|
||||
link:settings.html#url.incdec_segments[url.incdec_segments]
|
||||
link:settings{outsuffix}#url.incdec_segments[url.incdec_segments]
|
||||
config option.
|
||||
- `decrement`: Decrement the last number in the URL.
|
||||
Uses the
|
||||
link:settings.html#url.incdec_segments[url.incdec_segments]
|
||||
link:settings{outsuffix}#url.incdec_segments[url.incdec_segments]
|
||||
config option.
|
||||
|
||||
tab: Open in a new tab.
|
||||
@ -652,7 +635,8 @@ class CommandDispatcher:
|
||||
|
||||
cmdutils.check_exclusive((tab, bg, window), 'tbw')
|
||||
widget = self._current_widget()
|
||||
url = self._current_url().adjusted(QUrl.RemoveFragment)
|
||||
url = self._current_url()
|
||||
url = url.adjusted(QUrl.RemoveFragment | QUrl.RemoveQuery)
|
||||
|
||||
handlers = {
|
||||
'prev': functools.partial(navigate.prevnext, prev=True),
|
||||
@ -833,7 +817,7 @@ class CommandDispatcher:
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window')
|
||||
@cmdutils.argument('what', choices=['selection', 'url', 'pretty-url',
|
||||
'title', 'domain'])
|
||||
def yank(self, what='url', sel=False, keep=False):
|
||||
def yank(self, what='url', sel=False, keep=False, quiet=False):
|
||||
"""Yank something to the clipboard or primary selection.
|
||||
|
||||
Args:
|
||||
@ -847,6 +831,7 @@ class CommandDispatcher:
|
||||
|
||||
sel: Use the primary selection instead of the clipboard.
|
||||
keep: Stay in visual mode after yanking the selection.
|
||||
quiet: Don't show an information message.
|
||||
"""
|
||||
if what == 'title':
|
||||
s = self._tabbed_browser.widget.page_title(self._current_index())
|
||||
@ -860,10 +845,10 @@ class CommandDispatcher:
|
||||
what = 'URL' # For printing
|
||||
elif what == 'selection':
|
||||
def _selection_callback(s):
|
||||
if not s:
|
||||
if not s and not quiet:
|
||||
message.info("Nothing to yank")
|
||||
return
|
||||
self._yank_to_target(s, sel, what, keep)
|
||||
self._yank_to_target(s, sel, what, keep, quiet)
|
||||
|
||||
caret = self._current_widget().caret
|
||||
caret.selection(callback=_selection_callback)
|
||||
@ -871,9 +856,9 @@ class CommandDispatcher:
|
||||
else: # pragma: no cover
|
||||
raise ValueError("Invalid value {!r} for `what'.".format(what))
|
||||
|
||||
self._yank_to_target(s, sel, what, keep)
|
||||
self._yank_to_target(s, sel, what, keep, quiet)
|
||||
|
||||
def _yank_to_target(self, s, sel, what, keep):
|
||||
def _yank_to_target(self, s, sel, what, keep, quiet):
|
||||
if sel and utils.supports_selection():
|
||||
target = "primary selection"
|
||||
else:
|
||||
@ -882,47 +867,53 @@ class CommandDispatcher:
|
||||
|
||||
utils.set_clipboard(s, selection=sel)
|
||||
if what != 'selection':
|
||||
message.info("Yanked {} to {}: {}".format(what, target, s))
|
||||
if not quiet:
|
||||
message.info("Yanked {} to {}: {}".format(what, target, s))
|
||||
else:
|
||||
message.info("{} {} yanked to {}".format(
|
||||
len(s), "char" if len(s) == 1 else "chars", target))
|
||||
if not quiet:
|
||||
message.info("{} {} yanked to {}".format(
|
||||
len(s), "char" if len(s) == 1 else "chars", target))
|
||||
if not keep:
|
||||
modeman.leave(self._win_id, KeyMode.caret, "yank selected",
|
||||
maybe=True)
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window')
|
||||
@cmdutils.argument('count', count=True)
|
||||
def zoom_in(self, count=1):
|
||||
def zoom_in(self, count=1, quiet=False):
|
||||
"""Increase the zoom level for the current tab.
|
||||
|
||||
Args:
|
||||
count: How many steps to zoom in.
|
||||
quiet: Don't show a zoom level message.
|
||||
"""
|
||||
tab = self._current_widget()
|
||||
try:
|
||||
perc = tab.zoom.offset(count)
|
||||
except ValueError as e:
|
||||
raise cmdexc.CommandError(e)
|
||||
message.info("Zoom level: {}%".format(int(perc)), replace=True)
|
||||
if not quiet:
|
||||
message.info("Zoom level: {}%".format(int(perc)), replace=True)
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window')
|
||||
@cmdutils.argument('count', count=True)
|
||||
def zoom_out(self, count=1):
|
||||
def zoom_out(self, count=1, quiet=False):
|
||||
"""Decrease the zoom level for the current tab.
|
||||
|
||||
Args:
|
||||
count: How many steps to zoom out.
|
||||
quiet: Don't show a zoom level message.
|
||||
"""
|
||||
tab = self._current_widget()
|
||||
try:
|
||||
perc = tab.zoom.offset(-count)
|
||||
except ValueError as e:
|
||||
raise cmdexc.CommandError(e)
|
||||
message.info("Zoom level: {}%".format(int(perc)), replace=True)
|
||||
if not quiet:
|
||||
message.info("Zoom level: {}%".format(int(perc)), replace=True)
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window')
|
||||
@cmdutils.argument('count', count=True)
|
||||
def zoom(self, zoom=None, count=None):
|
||||
def zoom(self, zoom=None, count=None, quiet=False):
|
||||
"""Set the zoom level for the current tab.
|
||||
|
||||
The zoom can be given as argument or as [count]. If neither is
|
||||
@ -932,6 +923,7 @@ class CommandDispatcher:
|
||||
Args:
|
||||
zoom: The zoom percentage to set.
|
||||
count: The zoom percentage to set.
|
||||
quiet: Don't show a zoom level message.
|
||||
"""
|
||||
if zoom is not None:
|
||||
try:
|
||||
@ -949,7 +941,8 @@ class CommandDispatcher:
|
||||
tab.zoom.set_factor(float(level) / 100)
|
||||
except ValueError:
|
||||
raise cmdexc.CommandError("Can't zoom {}%!".format(level))
|
||||
message.info("Zoom level: {}%".format(int(level)), replace=True)
|
||||
if not quiet:
|
||||
message.info("Zoom level: {}%".format(int(level)), replace=True)
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window')
|
||||
def tab_only(self, prev=False, next_=False, force=False):
|
||||
@ -1138,9 +1131,6 @@ class CommandDispatcher:
|
||||
if index == 'last':
|
||||
self._tab_focus_last()
|
||||
return
|
||||
elif not no_last and index == self._current_index() + 1:
|
||||
self._tab_focus_last(show_error=False)
|
||||
return
|
||||
elif index is None:
|
||||
self.tab_next()
|
||||
return
|
||||
@ -1148,6 +1138,10 @@ class CommandDispatcher:
|
||||
if index < 0:
|
||||
index = self._count() + index + 1
|
||||
|
||||
if not no_last and index == self._current_index() + 1:
|
||||
self._tab_focus_last(show_error=False)
|
||||
return
|
||||
|
||||
if 1 <= index <= self._count():
|
||||
self._set_current_index(index - 1)
|
||||
else:
|
||||
@ -1201,8 +1195,9 @@ class CommandDispatcher:
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window',
|
||||
maxsplit=0, no_replace_variables=True)
|
||||
@cmdutils.argument('count', count=True)
|
||||
def spawn(self, cmdline, userscript=False, verbose=False,
|
||||
output=False, detach=False):
|
||||
output=False, detach=False, count=None):
|
||||
"""Spawn a command in a shell.
|
||||
|
||||
Args:
|
||||
@ -1210,12 +1205,13 @@ class CommandDispatcher:
|
||||
absolute path, or store the userscript in one of those
|
||||
locations:
|
||||
- `~/.local/share/qutebrowser/userscripts`
|
||||
(or `$XDG_DATA_DIR`)
|
||||
(or `$XDG_DATA_HOME`)
|
||||
- `/usr/share/qutebrowser/userscripts`
|
||||
verbose: Show notifications when the command started/exited.
|
||||
output: Whether the output should be shown in a new tab.
|
||||
detach: Whether the command should be detached from qutebrowser.
|
||||
cmdline: The commandline to execute.
|
||||
count: Given to userscripts as $QUTE_COUNT.
|
||||
"""
|
||||
cmdutils.check_exclusive((userscript, detach), 'ud')
|
||||
try:
|
||||
@ -1239,7 +1235,7 @@ class CommandDispatcher:
|
||||
if userscript:
|
||||
def _selection_callback(s):
|
||||
try:
|
||||
runner = self._run_userscript(s, cmd, args, verbose)
|
||||
runner = self._run_userscript(s, cmd, args, verbose, count)
|
||||
runner.finished.connect(_on_proc_finished)
|
||||
except cmdexc.CommandError as e:
|
||||
message.error(str(e))
|
||||
@ -1266,19 +1262,23 @@ class CommandDispatcher:
|
||||
"""Open main startpage in current tab."""
|
||||
self.openurl(config.val.url.start_pages[0])
|
||||
|
||||
def _run_userscript(self, selection, cmd, args, verbose):
|
||||
def _run_userscript(self, selection, cmd, args, verbose, count):
|
||||
"""Run a userscript given as argument.
|
||||
|
||||
Args:
|
||||
cmd: The userscript to run.
|
||||
args: Arguments to pass to the userscript.
|
||||
verbose: Show notifications when the command started/exited.
|
||||
count: Exposed to the userscript.
|
||||
"""
|
||||
env = {
|
||||
'QUTE_MODE': 'command',
|
||||
'QUTE_SELECTED_TEXT': selection,
|
||||
}
|
||||
|
||||
if count is not None:
|
||||
env['QUTE_COUNT'] = str(count)
|
||||
|
||||
idx = self._current_index()
|
||||
if idx != -1:
|
||||
env['QUTE_TITLE'] = self._tabbed_browser.widget.page_title(idx)
|
||||
@ -1455,6 +1455,7 @@ class CommandDispatcher:
|
||||
if tab.data.inspector is None:
|
||||
tab.data.inspector = inspector.create()
|
||||
tab.data.inspector.inspect(page)
|
||||
tab.data.inspector.show()
|
||||
else:
|
||||
tab.data.inspector.toggle(page)
|
||||
except inspector.WebInspectorError as e:
|
||||
@ -1673,6 +1674,8 @@ class CommandDispatcher:
|
||||
"""
|
||||
try:
|
||||
elem.set_value(text)
|
||||
# Kick off js handlers to trick them into thinking there was input.
|
||||
elem.dispatch_event("input", bubbles=True)
|
||||
except webelem.OrphanedError:
|
||||
message.error('Edited element vanished')
|
||||
ed.backup()
|
||||
@ -2106,7 +2109,10 @@ class CommandDispatcher:
|
||||
raise cmdexc.CommandError(str(e))
|
||||
|
||||
widget = self._current_widget()
|
||||
widget.run_js_async(js_code, callback=jseval_cb, world=world)
|
||||
try:
|
||||
widget.run_js_async(js_code, callback=jseval_cb, world=world)
|
||||
except browsertab.WebTabError as e:
|
||||
raise cmdexc.CommandError(str(e))
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window')
|
||||
def fake_key(self, keystring, global_=False):
|
||||
@ -2243,6 +2249,6 @@ class CommandDispatcher:
|
||||
if tab is None:
|
||||
return
|
||||
try:
|
||||
tab.audio.toggle_muted()
|
||||
tab.audio.toggle_muted(override=True)
|
||||
except browsertab.WebTabError as e:
|
||||
raise cmdexc.CommandError(e)
|
||||
|
@ -29,14 +29,15 @@ import pathlib
|
||||
import tempfile
|
||||
import enum
|
||||
|
||||
import sip
|
||||
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, Qt, QObject, QModelIndex,
|
||||
QTimer, QAbstractListModel, QUrl)
|
||||
|
||||
from qutebrowser.browser import pdfjs
|
||||
from qutebrowser.commands import cmdexc, cmdutils
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import (usertypes, standarddir, utils, message, log,
|
||||
qtutils)
|
||||
qtutils, objreg)
|
||||
from qutebrowser.qt import sip
|
||||
|
||||
|
||||
ModelRole = enum.IntEnum('ModelRole', ['item'], start=Qt.UserRole)
|
||||
@ -80,9 +81,9 @@ def download_dir():
|
||||
ddir = directory
|
||||
|
||||
try:
|
||||
os.makedirs(ddir)
|
||||
except FileExistsError:
|
||||
pass
|
||||
os.makedirs(ddir, exist_ok=True)
|
||||
except OSError as e:
|
||||
message.error("Failed to create download directory: {}".format(e))
|
||||
|
||||
return ddir
|
||||
|
||||
@ -134,11 +135,12 @@ def create_full_filename(basename, filename):
|
||||
Return:
|
||||
The full absolute path, or None if filename creation was not possible.
|
||||
"""
|
||||
basename = utils.sanitize_filename(basename)
|
||||
# Filename can be a full path so don't use sanitize_filename on it.
|
||||
# Remove chars which can't be encoded in the filename encoding.
|
||||
# See https://github.com/qutebrowser/qutebrowser/issues/427
|
||||
encoding = sys.getfilesystemencoding()
|
||||
filename = utils.force_encoding(filename, encoding)
|
||||
basename = utils.force_encoding(basename, encoding)
|
||||
if os.path.isabs(filename) and (os.path.isdir(filename) or
|
||||
filename.endswith(os.sep)):
|
||||
# We got an absolute directory from the user, so we save it under
|
||||
@ -159,8 +161,7 @@ def get_filename_question(*, suggested_filename, url, parent=None):
|
||||
url: The URL the download originated from.
|
||||
parent: The parent of the question (a QObject).
|
||||
"""
|
||||
encoding = sys.getfilesystemencoding()
|
||||
suggested_filename = utils.force_encoding(suggested_filename, encoding)
|
||||
suggested_filename = utils.sanitize_filename(suggested_filename)
|
||||
|
||||
q = usertypes.Question(parent)
|
||||
q.title = "Save file to:"
|
||||
@ -224,9 +225,6 @@ class _DownloadTarget:
|
||||
|
||||
"""Abstract base class for different download targets."""
|
||||
|
||||
def __init__(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def suggested_filename(self):
|
||||
"""Get the suggested filename for this download target."""
|
||||
raise NotImplementedError
|
||||
@ -243,7 +241,6 @@ class FileDownloadTarget(_DownloadTarget):
|
||||
"""
|
||||
|
||||
def __init__(self, filename, force_overwrite=False):
|
||||
# pylint: disable=super-init-not-called
|
||||
self.filename = filename
|
||||
self.force_overwrite = force_overwrite
|
||||
|
||||
@ -263,7 +260,6 @@ class FileObjDownloadTarget(_DownloadTarget):
|
||||
"""
|
||||
|
||||
def __init__(self, fileobj):
|
||||
# pylint: disable=super-init-not-called
|
||||
self.fileobj = fileobj
|
||||
|
||||
def suggested_filename(self):
|
||||
@ -290,7 +286,6 @@ class OpenFileDownloadTarget(_DownloadTarget):
|
||||
"""
|
||||
|
||||
def __init__(self, cmdline=None):
|
||||
# pylint: disable=super-init-not-called
|
||||
self.cmdline = cmdline
|
||||
|
||||
def suggested_filename(self):
|
||||
@ -300,6 +295,17 @@ class OpenFileDownloadTarget(_DownloadTarget):
|
||||
return 'temporary file'
|
||||
|
||||
|
||||
class PDFJSDownloadTarget(_DownloadTarget):
|
||||
|
||||
"""Open the download via PDF.js."""
|
||||
|
||||
def suggested_filename(self):
|
||||
raise NoFilenameError
|
||||
|
||||
def __str__(self):
|
||||
return 'temporary PDF.js file'
|
||||
|
||||
|
||||
class DownloadItemStats(QObject):
|
||||
|
||||
"""Statistics (bytes done, total bytes, time, etc.) about a download.
|
||||
@ -405,6 +411,8 @@ class AbstractDownloadItem(QObject):
|
||||
arg: The error message as string.
|
||||
remove_requested: Emitted when the removal of this download was
|
||||
requested.
|
||||
pdfjs_requested: Emitted when PDF.js should be opened with the given
|
||||
filename.
|
||||
"""
|
||||
|
||||
data_changed = pyqtSignal()
|
||||
@ -412,6 +420,7 @@ class AbstractDownloadItem(QObject):
|
||||
error = pyqtSignal(str)
|
||||
cancelled = pyqtSignal()
|
||||
remove_requested = pyqtSignal()
|
||||
pdfjs_requested = pyqtSignal(str)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
@ -692,9 +701,7 @@ class AbstractDownloadItem(QObject):
|
||||
global last_used_directory
|
||||
|
||||
try:
|
||||
os.makedirs(os.path.dirname(self._filename))
|
||||
except FileExistsError:
|
||||
pass
|
||||
os.makedirs(os.path.dirname(self._filename), exist_ok=True)
|
||||
except OSError as e:
|
||||
self._die(e.strerror)
|
||||
|
||||
@ -732,6 +739,19 @@ class AbstractDownloadItem(QObject):
|
||||
return
|
||||
self.open_file(cmdline)
|
||||
|
||||
def _pdfjs_if_successful(self):
|
||||
"""Open the file via PDF.js if downloading was successful."""
|
||||
if not self.successful:
|
||||
log.downloads.debug("{} finished but not successful, not opening!"
|
||||
.format(self))
|
||||
return
|
||||
|
||||
filename = self._get_open_filename()
|
||||
if filename is None: # pragma: no cover
|
||||
log.downloads.error("No filename to open the download!")
|
||||
return
|
||||
self.pdfjs_requested.emit(os.path.basename(filename))
|
||||
|
||||
def set_target(self, target):
|
||||
"""Set the target for a given download.
|
||||
|
||||
@ -743,7 +763,7 @@ class AbstractDownloadItem(QObject):
|
||||
elif isinstance(target, FileDownloadTarget):
|
||||
self._set_filename(
|
||||
target.filename, force_overwrite=target.force_overwrite)
|
||||
elif isinstance(target, OpenFileDownloadTarget):
|
||||
elif isinstance(target, (OpenFileDownloadTarget, PDFJSDownloadTarget)):
|
||||
try:
|
||||
fobj = temp_download_manager.get_tmpfile(self.basename)
|
||||
except OSError as exc:
|
||||
@ -751,8 +771,15 @@ class AbstractDownloadItem(QObject):
|
||||
message.error(msg)
|
||||
self.cancel()
|
||||
return
|
||||
self.finished.connect(
|
||||
functools.partial(self._open_if_successful, target.cmdline))
|
||||
|
||||
if isinstance(target, OpenFileDownloadTarget):
|
||||
self.finished.connect(functools.partial(
|
||||
self._open_if_successful, target.cmdline))
|
||||
elif isinstance(target, PDFJSDownloadTarget):
|
||||
self.finished.connect(self._pdfjs_if_successful)
|
||||
else:
|
||||
raise utils.Unreachable
|
||||
|
||||
self._set_tempfile(fobj)
|
||||
else: # pragma: no cover
|
||||
raise ValueError("Unsupported download target: {}".format(target))
|
||||
@ -799,6 +826,13 @@ class AbstractDownloadManager(QObject):
|
||||
dl.stats.update_speed()
|
||||
self.data_changed.emit(-1)
|
||||
|
||||
@pyqtSlot(str)
|
||||
def _on_pdfjs_requested(self, filename):
|
||||
"""Open PDF.js when a download requests it."""
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window='last-focused')
|
||||
tabbed_browser.tabopen(pdfjs.get_main_url(filename), background=False)
|
||||
|
||||
def _init_item(self, download, auto_remove, suggested_filename):
|
||||
"""Initialize a newly created DownloadItem."""
|
||||
download.cancelled.connect(download.remove)
|
||||
@ -815,6 +849,8 @@ class AbstractDownloadManager(QObject):
|
||||
download.data_changed.connect(
|
||||
functools.partial(self._on_data_changed, download))
|
||||
download.error.connect(self._on_error)
|
||||
download.pdfjs_requested.connect(self._on_pdfjs_requested)
|
||||
|
||||
download.basename = suggested_filename
|
||||
idx = len(self.downloads)
|
||||
download.index = idx + 1 # "Human readable" index
|
||||
@ -1197,7 +1233,7 @@ class TempDownloadManager:
|
||||
"directory")
|
||||
self._tmpdir = None
|
||||
|
||||
def _get_tmpdir(self):
|
||||
def get_tmpdir(self):
|
||||
"""Return the temporary directory that is used for downloads.
|
||||
|
||||
The directory is created lazily on first access.
|
||||
@ -1223,13 +1259,12 @@ class TempDownloadManager:
|
||||
Return:
|
||||
A tempfile.NamedTemporaryFile that should be used to save the file.
|
||||
"""
|
||||
tmpdir = self._get_tmpdir()
|
||||
encoding = sys.getfilesystemencoding()
|
||||
suggested_name = utils.force_encoding(suggested_name, encoding)
|
||||
tmpdir = self.get_tmpdir()
|
||||
suggested_name = utils.sanitize_filename(suggested_name)
|
||||
# Make sure that the filename is not too long
|
||||
suggested_name = utils.elide_filename(suggested_name, 50)
|
||||
fobj = tempfile.NamedTemporaryFile(dir=tmpdir.name, delete=False,
|
||||
suffix=suggested_name)
|
||||
suffix='_' + suggested_name)
|
||||
self.files.append(fobj)
|
||||
return fobj
|
||||
|
||||
|
@ -21,13 +21,13 @@
|
||||
|
||||
import functools
|
||||
|
||||
import sip
|
||||
from PyQt5.QtCore import pyqtSlot, QSize, Qt, QTimer
|
||||
from PyQt5.QtWidgets import QListView, QSizePolicy, QMenu, QStyleFactory
|
||||
|
||||
from qutebrowser.browser import downloads
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import qtutils, utils, objreg
|
||||
from qutebrowser.qt import sip
|
||||
|
||||
|
||||
def update_geometry(obj):
|
||||
|
@ -46,7 +46,8 @@ class GreasemonkeyScript:
|
||||
|
||||
"""Container class for userscripts, parses metadata blocks."""
|
||||
|
||||
def __init__(self, properties, code):
|
||||
def __init__(self, properties, code, # noqa: C901 pragma: no mccabe
|
||||
filename=None):
|
||||
self._code = code
|
||||
self.includes = []
|
||||
self.matches = []
|
||||
@ -58,6 +59,7 @@ class GreasemonkeyScript:
|
||||
self.run_at = None
|
||||
self.script_meta = None
|
||||
self.runs_on_sub_frames = True
|
||||
self.jsworld = "main"
|
||||
for name, value in properties:
|
||||
if name == 'name':
|
||||
self.name = value
|
||||
@ -77,12 +79,22 @@ class GreasemonkeyScript:
|
||||
self.runs_on_sub_frames = False
|
||||
elif name == 'require':
|
||||
self.requires.append(value)
|
||||
elif name == 'qute-js-world':
|
||||
self.jsworld = value
|
||||
|
||||
if not self.name:
|
||||
if filename:
|
||||
self.name = filename
|
||||
else:
|
||||
raise ValueError(
|
||||
"@name key required or pass filename to init."
|
||||
)
|
||||
|
||||
HEADER_REGEX = r'// ==UserScript==|\n+// ==/UserScript==\n'
|
||||
PROPS_REGEX = r'// @(?P<prop>[^\s]+)\s*(?P<val>.*)'
|
||||
|
||||
@classmethod
|
||||
def parse(cls, source):
|
||||
def parse(cls, source, filename=None):
|
||||
"""GreasemonkeyScript factory.
|
||||
|
||||
Takes a userscript source and returns a GreasemonkeyScript.
|
||||
@ -94,7 +106,11 @@ class GreasemonkeyScript:
|
||||
_head, props, _code = matches
|
||||
except ValueError:
|
||||
props = ""
|
||||
script = cls(re.findall(cls.PROPS_REGEX, props), source)
|
||||
script = cls(
|
||||
re.findall(cls.PROPS_REGEX, props),
|
||||
source,
|
||||
filename=filename
|
||||
)
|
||||
script.script_meta = props
|
||||
if not script.includes and not script.matches:
|
||||
script.includes = ['*']
|
||||
@ -118,7 +134,7 @@ class GreasemonkeyScript:
|
||||
scriptName=javascript.string_escape(
|
||||
"/".join([self.namespace or '', self.name])),
|
||||
scriptInfo=self._meta_json(),
|
||||
scriptMeta=javascript.string_escape(self.script_meta),
|
||||
scriptMeta=javascript.string_escape(self.script_meta or ''),
|
||||
scriptSource=self._code,
|
||||
use_proxy=use_proxy)
|
||||
|
||||
@ -142,7 +158,7 @@ class GreasemonkeyScript:
|
||||
|
||||
|
||||
@attr.s
|
||||
class MatchingScripts(object):
|
||||
class MatchingScripts:
|
||||
|
||||
"""All userscripts registered to run on a particular url."""
|
||||
|
||||
@ -231,8 +247,9 @@ class GreasemonkeyManager(QObject):
|
||||
if not os.path.isfile(script_filename):
|
||||
continue
|
||||
script_path = os.path.join(scripts_dir, script_filename)
|
||||
with open(script_path, encoding='utf-8') as script_file:
|
||||
script = GreasemonkeyScript.parse(script_file.read())
|
||||
with open(script_path, encoding='utf-8-sig') as script_file:
|
||||
script = GreasemonkeyScript.parse(script_file.read(),
|
||||
script_filename)
|
||||
if not script.name:
|
||||
script.name = script_filename
|
||||
self.add_script(script, force)
|
||||
|
@ -32,7 +32,7 @@ import attr
|
||||
from PyQt5.QtCore import pyqtSlot, QObject, Qt, QUrl
|
||||
from PyQt5.QtWidgets import QLabel
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.config import config, configexc
|
||||
from qutebrowser.keyinput import modeman, modeparsers
|
||||
from qutebrowser.browser import webelem
|
||||
from qutebrowser.commands import userscripts, cmdexc, cmdutils, runners
|
||||
@ -601,8 +601,12 @@ class HintManager(QObject):
|
||||
return
|
||||
|
||||
if elems is None:
|
||||
message.error("There was an error while getting hint elements")
|
||||
message.error("Unknown error while getting hint elements.")
|
||||
return
|
||||
elif isinstance(elems, webelem.Error):
|
||||
message.error(str(elems))
|
||||
return
|
||||
|
||||
if not elems:
|
||||
message.error("No elements found.")
|
||||
return
|
||||
@ -637,9 +641,8 @@ class HintManager(QObject):
|
||||
star_args_optional=True, maxsplit=2)
|
||||
@cmdutils.argument('win_id', win_id=True)
|
||||
def start(self, # pylint: disable=keyword-arg-before-vararg
|
||||
group=webelem.Group.all, target=Target.normal,
|
||||
*args, win_id, mode=None, add_history=False, rapid=False,
|
||||
first=False):
|
||||
group='all', target=Target.normal, *args, win_id, mode=None,
|
||||
add_history=False, rapid=False, first=False):
|
||||
"""Start hinting.
|
||||
|
||||
Args:
|
||||
@ -658,6 +661,9 @@ class HintManager(QObject):
|
||||
- `images`: Only images.
|
||||
- `inputs`: Only input fields.
|
||||
|
||||
Custom groups can be added via the `hints.selectors` setting
|
||||
and also used here.
|
||||
|
||||
target: What to do with the selected element.
|
||||
|
||||
- `normal`: Open the link.
|
||||
@ -693,7 +699,7 @@ class HintManager(QObject):
|
||||
- With `userscript`: The userscript to execute. Either store
|
||||
the userscript in
|
||||
`~/.local/share/qutebrowser/userscripts`
|
||||
(or `$XDG_DATA_DIR`), or use an absolute
|
||||
(or `$XDG_DATA_HOME`), or use an absolute
|
||||
path.
|
||||
- With `fill`: The command to fill the statusbar with.
|
||||
`{hint-url}` will get replaced by the selected
|
||||
@ -724,15 +730,12 @@ class HintManager(QObject):
|
||||
raise cmdexc.CommandError("Rapid hinting makes no sense with "
|
||||
"target {}!".format(name))
|
||||
|
||||
if mode is None:
|
||||
mode = config.val.hints.mode
|
||||
|
||||
self._check_args(target, *args)
|
||||
self._context = HintContext()
|
||||
self._context.tab = tab
|
||||
self._context.target = target
|
||||
self._context.rapid = rapid
|
||||
self._context.hint_mode = mode
|
||||
self._context.hint_mode = self._get_hint_mode(mode)
|
||||
self._context.add_history = add_history
|
||||
self._context.first = first
|
||||
try:
|
||||
@ -741,10 +744,28 @@ class HintManager(QObject):
|
||||
raise cmdexc.CommandError("No URL set for this page yet!")
|
||||
self._context.args = args
|
||||
self._context.group = group
|
||||
selector = webelem.SELECTORS[self._context.group]
|
||||
|
||||
try:
|
||||
selector = webelem.css_selector(self._context.group,
|
||||
self._context.baseurl)
|
||||
except webelem.Error as e:
|
||||
raise cmdexc.CommandError(str(e))
|
||||
|
||||
self._context.tab.elements.find_css(selector, self._start_cb,
|
||||
only_visible=True)
|
||||
|
||||
def _get_hint_mode(self, mode):
|
||||
"""Get the hinting mode to use based on a mode argument."""
|
||||
if mode is None:
|
||||
return config.val.hints.mode
|
||||
|
||||
opt = config.instance.get_opt('hints.mode')
|
||||
try:
|
||||
opt.typ.to_py(mode)
|
||||
except configexc.ValidationError as e:
|
||||
raise cmdexc.CommandError("Invalid mode: {}".format(e))
|
||||
return mode
|
||||
|
||||
def current_mode(self):
|
||||
"""Return the currently active hinting mode (or None otherwise)."""
|
||||
if self._context is None:
|
||||
|
@ -23,11 +23,12 @@ import os
|
||||
import time
|
||||
import contextlib
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot, QUrl, QTimer, pyqtSignal
|
||||
from PyQt5.QtCore import pyqtSlot, QUrl, pyqtSignal
|
||||
from PyQt5.QtWidgets import QProgressDialog, QApplication
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.commands import cmdutils, cmdexc
|
||||
from qutebrowser.utils import (utils, objreg, log, usertypes, message,
|
||||
debug, standarddir, qtutils)
|
||||
from qutebrowser.utils import utils, objreg, log, usertypes, message, qtutils
|
||||
from qutebrowser.misc import objects, sql
|
||||
|
||||
|
||||
@ -35,6 +36,77 @@ from qutebrowser.misc import objects, sql
|
||||
_USER_VERSION = 2
|
||||
|
||||
|
||||
class HistoryProgress:
|
||||
|
||||
"""Progress dialog for history imports/conversions.
|
||||
|
||||
This makes WebHistory simpler as it can call methods of this class even
|
||||
when we don't want to show a progress dialog (for very small imports). This
|
||||
means tick() and finish() can be called even when start() wasn't.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._progress = None
|
||||
self._value = 0
|
||||
|
||||
def start(self, text, maximum):
|
||||
"""Start showing a progress dialog."""
|
||||
self._progress = QProgressDialog()
|
||||
self._progress.setMinimumDuration(500)
|
||||
self._progress.setLabelText(text)
|
||||
self._progress.setMaximum(maximum)
|
||||
self._progress.setCancelButton(None)
|
||||
self._progress.show()
|
||||
QApplication.processEvents()
|
||||
|
||||
def tick(self):
|
||||
"""Increase the displayed progress value."""
|
||||
self._value += 1
|
||||
if self._progress is not None:
|
||||
self._progress.setValue(self._value)
|
||||
QApplication.processEvents()
|
||||
|
||||
def finish(self):
|
||||
"""Finish showing the progress dialog."""
|
||||
if self._progress is not None:
|
||||
self._progress.hide()
|
||||
|
||||
|
||||
class CompletionMetaInfo(sql.SqlTable):
|
||||
|
||||
"""Table containing meta-information for the completion."""
|
||||
|
||||
KEYS = {
|
||||
'force_rebuild': False,
|
||||
}
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__("CompletionMetaInfo", ['key', 'value'],
|
||||
constraints={'key': 'PRIMARY KEY'})
|
||||
for key, default in self.KEYS.items():
|
||||
if key not in self:
|
||||
self[key] = default
|
||||
|
||||
def _check_key(self, key):
|
||||
if key not in self.KEYS:
|
||||
raise KeyError(key)
|
||||
|
||||
def __contains__(self, key):
|
||||
self._check_key(key)
|
||||
query = self.contains_query('key')
|
||||
return query.run(val=key).value()
|
||||
|
||||
def __getitem__(self, key):
|
||||
self._check_key(key)
|
||||
query = sql.Query('SELECT value FROM CompletionMetaInfo '
|
||||
'WHERE key = :key')
|
||||
return query.run(key=key).value()
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
self._check_key(key)
|
||||
self.insert({'key': key, 'value': value}, replace=True)
|
||||
|
||||
|
||||
class CompletionHistory(sql.SqlTable):
|
||||
|
||||
"""History which only has the newest entry for each URL."""
|
||||
@ -50,26 +122,46 @@ class CompletionHistory(sql.SqlTable):
|
||||
|
||||
class WebHistory(sql.SqlTable):
|
||||
|
||||
"""The global history of visited pages."""
|
||||
"""The global history of visited pages.
|
||||
|
||||
Attributes:
|
||||
completion: A CompletionHistory instance.
|
||||
metainfo: A CompletionMetaInfo instance.
|
||||
_progress: A HistoryProgress instance.
|
||||
|
||||
Class attributes:
|
||||
_PROGRESS_THRESHOLD: When to start showing progress dialogs.
|
||||
"""
|
||||
|
||||
# All web history cleared
|
||||
history_cleared = pyqtSignal()
|
||||
# one url cleared
|
||||
url_cleared = pyqtSignal(QUrl)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
_PROGRESS_THRESHOLD = 1000
|
||||
|
||||
def __init__(self, progress, parent=None):
|
||||
super().__init__("History", ['url', 'title', 'atime', 'redirect'],
|
||||
constraints={'url': 'NOT NULL',
|
||||
'title': 'NOT NULL',
|
||||
'atime': 'NOT NULL',
|
||||
'redirect': 'NOT NULL'},
|
||||
parent=parent)
|
||||
self._progress = progress
|
||||
|
||||
self.completion = CompletionHistory(parent=self)
|
||||
self.metainfo = CompletionMetaInfo(parent=self)
|
||||
|
||||
if sql.Query('pragma user_version').run().value() < _USER_VERSION:
|
||||
self.completion.delete_all()
|
||||
if self.metainfo['force_rebuild']:
|
||||
self.completion.delete_all()
|
||||
self.metainfo['force_rebuild'] = False
|
||||
|
||||
if not self.completion:
|
||||
# either the table is out-of-date or the user wiped it manually
|
||||
self._rebuild_completion()
|
||||
|
||||
self.create_index('HistoryIndex', 'url')
|
||||
self.create_index('HistoryAtimeIndex', 'atime')
|
||||
self._contains_query = self.contains_query('url')
|
||||
@ -87,21 +179,29 @@ class WebHistory(sql.SqlTable):
|
||||
'ORDER BY atime desc '
|
||||
'limit :limit offset :offset')
|
||||
|
||||
config.instance.changed.connect(self._on_config_changed)
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self, length=len(self))
|
||||
|
||||
def __contains__(self, url):
|
||||
return self._contains_query.run(val=url).value()
|
||||
|
||||
@config.change_filter('completion.web_history.exclude')
|
||||
def _on_config_changed(self):
|
||||
self.metainfo['force_rebuild'] = True
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _handle_sql_errors(self):
|
||||
try:
|
||||
yield
|
||||
except sql.SqlError as e:
|
||||
if e.environmental:
|
||||
message.error("Failed to write history: {}".format(e.text()))
|
||||
else:
|
||||
raise
|
||||
except sql.SqlEnvironmentError as e:
|
||||
message.error("Failed to write history: {}".format(e.text()))
|
||||
|
||||
def _is_excluded(self, url):
|
||||
"""Check if the given URL is excluded from the completion."""
|
||||
patterns = config.cache['completion.web_history.exclude']
|
||||
return any(pattern.matches(url) for pattern in patterns)
|
||||
|
||||
def _rebuild_completion(self):
|
||||
data = {'url': [], 'title': [], 'last_atime': []}
|
||||
@ -109,10 +209,23 @@ class WebHistory(sql.SqlTable):
|
||||
q = sql.Query('SELECT url, title, max(atime) AS atime FROM History '
|
||||
'WHERE NOT redirect and url NOT LIKE "qute://back%" '
|
||||
'GROUP BY url ORDER BY atime asc')
|
||||
for entry in q.run():
|
||||
data['url'].append(self._format_completion_url(QUrl(entry.url)))
|
||||
entries = list(q.run())
|
||||
|
||||
if len(entries) > self._PROGRESS_THRESHOLD:
|
||||
self._progress.start("Rebuilding completion...", len(entries))
|
||||
|
||||
for entry in entries:
|
||||
self._progress.tick()
|
||||
|
||||
url = QUrl(entry.url)
|
||||
if self._is_excluded(url):
|
||||
continue
|
||||
data['url'].append(self._format_completion_url(url))
|
||||
data['title'].append(entry.title)
|
||||
data['last_atime'].append(entry.atime)
|
||||
|
||||
self._progress.finish()
|
||||
|
||||
self.completion.insert_batch(data, replace=True)
|
||||
sql.Query('pragma user_version = {}'.format(_USER_VERSION)).run()
|
||||
|
||||
@ -218,114 +331,15 @@ class WebHistory(sql.SqlTable):
|
||||
'title': title,
|
||||
'atime': atime,
|
||||
'redirect': redirect})
|
||||
if not redirect:
|
||||
self.completion.insert({
|
||||
'url': self._format_completion_url(url),
|
||||
'title': title,
|
||||
'last_atime': atime
|
||||
}, replace=True)
|
||||
|
||||
def _parse_entry(self, line):
|
||||
"""Parse a history line like '12345 http://example.com title'."""
|
||||
if not line or line.startswith('#'):
|
||||
return None
|
||||
data = line.split(maxsplit=2)
|
||||
if len(data) == 2:
|
||||
atime, url = data
|
||||
title = ""
|
||||
elif len(data) == 3:
|
||||
atime, url, title = data
|
||||
else:
|
||||
raise ValueError("2 or 3 fields expected")
|
||||
if redirect or self._is_excluded(url):
|
||||
return
|
||||
|
||||
# http://xn--pple-43d.com/ with
|
||||
# https://bugreports.qt.io/browse/QTBUG-60364
|
||||
if url in ['http://.com/', 'https://.com/',
|
||||
'http://www..com/', 'https://www..com/']:
|
||||
return None
|
||||
|
||||
url = QUrl(url)
|
||||
if not url.isValid():
|
||||
raise ValueError("Invalid URL: {}".format(url.errorString()))
|
||||
|
||||
# https://github.com/qutebrowser/qutebrowser/issues/2646
|
||||
if url.scheme() == 'data':
|
||||
return None
|
||||
|
||||
# https://github.com/qutebrowser/qutebrowser/issues/670
|
||||
atime = atime.lstrip('\0')
|
||||
|
||||
if '-' in atime:
|
||||
atime, flags = atime.split('-')
|
||||
else:
|
||||
flags = ''
|
||||
|
||||
if not set(flags).issubset('r'):
|
||||
raise ValueError("Invalid flags {!r}".format(flags))
|
||||
|
||||
redirect = 'r' in flags
|
||||
return (url, title, int(atime), redirect)
|
||||
|
||||
def import_txt(self):
|
||||
"""Import a history text file into sqlite if it exists.
|
||||
|
||||
In older versions of qutebrowser, history was stored in a text format.
|
||||
This converts that file into the new sqlite format and moves it to a
|
||||
backup location.
|
||||
"""
|
||||
path = os.path.join(standarddir.data(), 'history')
|
||||
if not os.path.isfile(path):
|
||||
return
|
||||
|
||||
def action():
|
||||
"""Actually run the import."""
|
||||
with debug.log_time(log.init, 'Import old history file to sqlite'):
|
||||
try:
|
||||
self._read(path)
|
||||
except ValueError as ex:
|
||||
message.error('Failed to import history: {}'.format(ex))
|
||||
else:
|
||||
self._write_backup(path)
|
||||
|
||||
# delay to give message time to appear before locking down for import
|
||||
message.info('Converting {} to sqlite...'.format(path))
|
||||
QTimer.singleShot(100, action)
|
||||
|
||||
def _read(self, path):
|
||||
"""Import a text file into the sql database."""
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
data = {'url': [], 'title': [], 'atime': [], 'redirect': []}
|
||||
completion_data = {'url': [], 'title': [], 'last_atime': []}
|
||||
for (i, line) in enumerate(f):
|
||||
try:
|
||||
parsed = self._parse_entry(line.strip())
|
||||
if parsed is None:
|
||||
continue
|
||||
url, title, atime, redirect = parsed
|
||||
data['url'].append(self._format_url(url))
|
||||
data['title'].append(title)
|
||||
data['atime'].append(atime)
|
||||
data['redirect'].append(redirect)
|
||||
if not redirect:
|
||||
completion_data['url'].append(
|
||||
self._format_completion_url(url))
|
||||
completion_data['title'].append(title)
|
||||
completion_data['last_atime'].append(atime)
|
||||
except ValueError as ex:
|
||||
raise ValueError('Failed to parse line #{} of {}: "{}"'
|
||||
.format(i, path, ex))
|
||||
self.insert_batch(data)
|
||||
self.completion.insert_batch(completion_data, replace=True)
|
||||
|
||||
def _write_backup(self, path):
|
||||
bak = path + '.bak'
|
||||
message.info('History import complete. Appending {} to {}'
|
||||
.format(path, bak))
|
||||
with open(path, 'r', encoding='utf-8') as infile:
|
||||
with open(bak, 'a', encoding='utf-8') as outfile:
|
||||
for line in infile:
|
||||
outfile.write('\n' + line)
|
||||
os.remove(path)
|
||||
self.completion.insert({
|
||||
'url': self._format_completion_url(url),
|
||||
'title': title,
|
||||
'last_atime': atime
|
||||
}, replace=True)
|
||||
|
||||
def _format_url(self, url):
|
||||
return url.toString(QUrl.RemovePassword | QUrl.FullyEncoded)
|
||||
@ -360,7 +374,8 @@ def init(parent=None):
|
||||
Args:
|
||||
parent: The parent to use for WebHistory.
|
||||
"""
|
||||
history = WebHistory(parent=parent)
|
||||
progress = HistoryProgress()
|
||||
history = WebHistory(progress=progress, parent=parent)
|
||||
objreg.register('web-history', history)
|
||||
|
||||
if objects.backend == usertypes.Backend.QtWebKit: # pragma: no cover
|
||||
|
@ -22,7 +22,7 @@
|
||||
from PyQt5.QtCore import QObject, QEvent, Qt, QTimer
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import message, log, usertypes, qtutils
|
||||
from qutebrowser.utils import message, log, usertypes, qtutils, objreg
|
||||
from qutebrowser.keyinput import modeman
|
||||
|
||||
|
||||
@ -40,11 +40,12 @@ class ChildEventFilter(QObject):
|
||||
_widget: The widget expected to send out childEvents.
|
||||
"""
|
||||
|
||||
def __init__(self, eventfilter, widget, parent=None):
|
||||
def __init__(self, eventfilter, widget, win_id, parent=None):
|
||||
super().__init__(parent)
|
||||
self._filter = eventfilter
|
||||
assert widget is not None
|
||||
self._widget = widget
|
||||
self._win_id = win_id
|
||||
|
||||
def eventFilter(self, obj, event):
|
||||
"""Act on ChildAdded events."""
|
||||
@ -57,7 +58,22 @@ class ChildEventFilter(QObject):
|
||||
|
||||
if qtutils.version_check('5.11', compiled=False, exact=True):
|
||||
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-68076
|
||||
QTimer.singleShot(0, self._widget.setFocus)
|
||||
pass_modes = [usertypes.KeyMode.command,
|
||||
usertypes.KeyMode.prompt,
|
||||
usertypes.KeyMode.yesno]
|
||||
if modeman.instance(self._win_id).mode not in pass_modes:
|
||||
tabbed_browser = objreg.get('tabbed-browser',
|
||||
scope='window',
|
||||
window=self._win_id)
|
||||
current_index = tabbed_browser.widget.currentIndex()
|
||||
try:
|
||||
widget_index = tabbed_browser.widget.indexOf(
|
||||
self._widget.parent())
|
||||
except RuntimeError:
|
||||
widget_index = -1
|
||||
if current_index == widget_index:
|
||||
QTimer.singleShot(0, self._widget.setFocus)
|
||||
|
||||
elif event.type() == QEvent.ChildRemoved:
|
||||
child = event.child()
|
||||
log.mouse.debug("{}: removed child {}".format(obj, child))
|
||||
@ -100,9 +116,13 @@ class MouseEventFilter(QObject):
|
||||
|
||||
self._ignore_wheel_event = True
|
||||
|
||||
pos = e.pos()
|
||||
if pos.x() < 0 or pos.y() < 0:
|
||||
log.mouse.warning("Ignoring invalid click at {}".format(pos))
|
||||
return False
|
||||
|
||||
if e.button() != Qt.NoButton:
|
||||
self._tab.elements.find_at_pos(e.pos(),
|
||||
self._mousepress_insertmode_cb)
|
||||
self._tab.elements.find_at_pos(pos, self._mousepress_insertmode_cb)
|
||||
|
||||
return False
|
||||
|
||||
@ -125,6 +145,10 @@ class MouseEventFilter(QObject):
|
||||
return True
|
||||
|
||||
if e.modifiers() & Qt.ControlModifier:
|
||||
mode = modeman.instance(self._tab.win_id).mode
|
||||
if mode == usertypes.KeyMode.passthrough:
|
||||
return False
|
||||
|
||||
divider = config.val.zoom.mouse_divider
|
||||
if divider == 0:
|
||||
return False
|
||||
|
@ -117,7 +117,10 @@ def prevnext(*, browsertab, win_id, baseurl, prev=False,
|
||||
"""
|
||||
def _prevnext_cb(elems):
|
||||
if elems is None:
|
||||
message.error("There was an error while getting hint elements")
|
||||
message.error("Unknown error while getting hint elements")
|
||||
return
|
||||
elif isinstance(elems, webelem.Error):
|
||||
message.error(str(elems))
|
||||
return
|
||||
|
||||
elem = _find_prevnext(prev, elems)
|
||||
@ -147,5 +150,9 @@ def prevnext(*, browsertab, win_id, baseurl, prev=False,
|
||||
else:
|
||||
browsertab.openurl(url)
|
||||
|
||||
browsertab.elements.find_css(webelem.SELECTORS[webelem.Group.links],
|
||||
_prevnext_cb)
|
||||
try:
|
||||
link_selector = webelem.css_selector('links', baseurl)
|
||||
except webelem.Error as e:
|
||||
raise Error(str(e))
|
||||
|
||||
browsertab.elements.find_css(link_selector, _prevnext_cb)
|
||||
|
@ -22,9 +22,12 @@
|
||||
|
||||
import os
|
||||
|
||||
from PyQt5.QtCore import QUrl
|
||||
from PyQt5.QtCore import QUrl, QUrlQuery
|
||||
|
||||
from qutebrowser.utils import utils, javascript
|
||||
from qutebrowser.utils import (utils, javascript, jinja, qtutils, usertypes,
|
||||
standarddir, log)
|
||||
from qutebrowser.misc import objects
|
||||
from qutebrowser.config import config
|
||||
|
||||
|
||||
class PDFJSNotFound(Exception):
|
||||
@ -41,73 +44,73 @@ class PDFJSNotFound(Exception):
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
def generate_pdfjs_page(url):
|
||||
"""Return the html content of a page that displays url with pdfjs.
|
||||
def generate_pdfjs_page(filename, url):
|
||||
"""Return the html content of a page that displays a file with pdfjs.
|
||||
|
||||
Returns a string.
|
||||
|
||||
Args:
|
||||
url: The url of the pdf as QUrl.
|
||||
filename: The filename of the PDF to open.
|
||||
url: The URL being opened.
|
||||
"""
|
||||
viewer = get_pdfjs_res('web/viewer.html').decode('utf-8')
|
||||
script = _generate_pdfjs_script(url)
|
||||
html_page = viewer.replace('</body>',
|
||||
'</body><script>{}</script>'.format(script))
|
||||
return html_page
|
||||
if not is_available():
|
||||
pdfjs_dir = os.path.join(standarddir.data(), 'pdfjs')
|
||||
return jinja.render('no_pdfjs.html',
|
||||
url=url.toDisplayString(),
|
||||
title="PDF.js not found",
|
||||
pdfjs_dir=pdfjs_dir)
|
||||
html = get_pdfjs_res('web/viewer.html').decode('utf-8')
|
||||
|
||||
script = _generate_pdfjs_script(filename)
|
||||
html = html.replace('</body>',
|
||||
'</body><script>{}</script>'.format(script))
|
||||
# WORKAROUND for the fact that PDF.js tries to use the Fetch API even with
|
||||
# qute:// URLs.
|
||||
pdfjs_script = '<script src="../build/pdf.js"></script>'
|
||||
html = html.replace(pdfjs_script,
|
||||
'<script>window.Response = undefined;</script>\n' +
|
||||
pdfjs_script)
|
||||
return html
|
||||
|
||||
|
||||
def _generate_pdfjs_script(url):
|
||||
def _generate_pdfjs_script(filename):
|
||||
"""Generate the script that shows the pdf with pdf.js.
|
||||
|
||||
Args:
|
||||
url: The url of the pdf page as QUrl.
|
||||
filename: The name of the file to open.
|
||||
"""
|
||||
return (
|
||||
'document.addEventListener("DOMContentLoaded", function() {{\n'
|
||||
' PDFJS.verbosity = PDFJS.VERBOSITY_LEVELS.info;\n'
|
||||
' (window.PDFView || window.PDFViewerApplication).open("{url}");\n'
|
||||
'}});\n'
|
||||
).format(url=javascript.string_escape(url.toString(QUrl.FullyEncoded)))
|
||||
url = QUrl('qute://pdfjs/file')
|
||||
url_query = QUrlQuery()
|
||||
url_query.addQueryItem('filename', filename)
|
||||
url.setQuery(url_query)
|
||||
|
||||
return jinja.js_environment.from_string("""
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
if (typeof window.PDFJS !== 'undefined') {
|
||||
// v1.x
|
||||
{% if disable_create_object_url %}
|
||||
window.PDFJS.disableCreateObjectURL = true;
|
||||
{% endif %}
|
||||
window.PDFJS.verbosity = window.PDFJS.VERBOSITY_LEVELS.info;
|
||||
} else {
|
||||
// v2.x
|
||||
const options = window.PDFViewerApplicationOptions;
|
||||
{% if disable_create_object_url %}
|
||||
options.set('disableCreateObjectURL', true);
|
||||
{% endif %}
|
||||
options.set('verbosity', pdfjsLib.VerbosityLevel.INFOS);
|
||||
}
|
||||
|
||||
def fix_urls(asset):
|
||||
"""Take an html page and replace each relative URL with an absolute.
|
||||
|
||||
This is specialized for pdf.js files and not a general purpose function.
|
||||
|
||||
Args:
|
||||
asset: js file or html page as string.
|
||||
"""
|
||||
new_urls = [
|
||||
('viewer.css', 'qute://pdfjs/web/viewer.css'),
|
||||
('compatibility.js', 'qute://pdfjs/web/compatibility.js'),
|
||||
('locale/locale.properties',
|
||||
'qute://pdfjs/web/locale/locale.properties'),
|
||||
('l10n.js', 'qute://pdfjs/web/l10n.js'),
|
||||
('../build/pdf.js', 'qute://pdfjs/build/pdf.js'),
|
||||
('debugger.js', 'qute://pdfjs/web/debugger.js'),
|
||||
('viewer.js', 'qute://pdfjs/web/viewer.js'),
|
||||
('compressed.tracemonkey-pldi-09.pdf', ''),
|
||||
('./images/', 'qute://pdfjs/web/images/'),
|
||||
('../build/pdf.worker.js', 'qute://pdfjs/build/pdf.worker.js'),
|
||||
('../web/cmaps/', 'qute://pdfjs/web/cmaps/'),
|
||||
]
|
||||
for original, new in new_urls:
|
||||
asset = asset.replace(original, new)
|
||||
return asset
|
||||
|
||||
|
||||
SYSTEM_PDFJS_PATHS = [
|
||||
# Debian pdf.js-common
|
||||
# Arch Linux pdfjs (AUR)
|
||||
'/usr/share/pdf.js/',
|
||||
# Arch Linux pdf.js (AUR)
|
||||
'/usr/share/javascript/pdf.js/',
|
||||
# Debian libjs-pdf
|
||||
'/usr/share/javascript/pdf/',
|
||||
# fallback
|
||||
os.path.expanduser('~/.local/share/qutebrowser/pdfjs/'),
|
||||
]
|
||||
const viewer = window.PDFView || window.PDFViewerApplication;
|
||||
viewer.open({{ url }});
|
||||
});
|
||||
""").render(
|
||||
url=javascript.to_js(url.toString(QUrl.FullyEncoded)),
|
||||
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-70420
|
||||
disable_create_object_url=(
|
||||
not qtutils.version_check('5.12') and
|
||||
not qtutils.version_check('5.7.1', exact=True, compiled=False) and
|
||||
objects.backend == usertypes.Backend.QtWebEngine))
|
||||
|
||||
|
||||
def get_pdfjs_res_and_path(path):
|
||||
@ -124,11 +127,25 @@ def get_pdfjs_res_and_path(path):
|
||||
content = None
|
||||
file_path = None
|
||||
|
||||
system_paths = [
|
||||
# Debian pdf.js-common
|
||||
# Arch Linux pdfjs (AUR)
|
||||
'/usr/share/pdf.js/',
|
||||
# Arch Linux pdf.js (AUR)
|
||||
'/usr/share/javascript/pdf.js/',
|
||||
# Debian libjs-pdf
|
||||
'/usr/share/javascript/pdf/',
|
||||
# fallback
|
||||
os.path.join(standarddir.data(), 'pdfjs'),
|
||||
# hardcoded fallback for --temp-basedir
|
||||
os.path.expanduser('~/.local/share/qutebrowser/pdfjs/'),
|
||||
]
|
||||
|
||||
# First try a system wide installation
|
||||
# System installations might strip off the 'build/' or 'web/' prefixes.
|
||||
# qute expects them, so we need to adjust for it.
|
||||
names_to_try = [path, _remove_prefix(path)]
|
||||
for system_path in SYSTEM_PDFJS_PATHS:
|
||||
for system_path in system_paths:
|
||||
content, file_path = _read_from_system(system_path, names_to_try)
|
||||
if content is not None:
|
||||
break
|
||||
@ -140,14 +157,11 @@ def get_pdfjs_res_and_path(path):
|
||||
content = utils.read_file(res_path, binary=True)
|
||||
except FileNotFoundError:
|
||||
raise PDFJSNotFound(path) from None
|
||||
except OSError as e:
|
||||
log.misc.warning("OSError while reading PDF.js file: {}".format(e))
|
||||
raise PDFJSNotFound(path) from None
|
||||
|
||||
try:
|
||||
# Might be script/html or might be binary
|
||||
text_content = content.decode('utf-8')
|
||||
except UnicodeDecodeError:
|
||||
return (content, file_path)
|
||||
text_content = fix_urls(text_content)
|
||||
return (text_content.encode('utf-8'), file_path)
|
||||
return content, file_path
|
||||
|
||||
|
||||
def get_pdfjs_res(path):
|
||||
@ -193,7 +207,10 @@ def _read_from_system(system_path, names):
|
||||
full_path = os.path.join(system_path, name)
|
||||
with open(full_path, 'rb') as f:
|
||||
return (f.read(), full_path)
|
||||
except OSError:
|
||||
except FileNotFoundError:
|
||||
continue
|
||||
except OSError as e:
|
||||
log.misc.warning("OSError while reading PDF.js file: {}".format(e))
|
||||
continue
|
||||
return (None, None)
|
||||
|
||||
@ -206,3 +223,22 @@ def is_available():
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def should_use_pdfjs(mimetype, url):
|
||||
"""Check whether PDF.js should be used."""
|
||||
# e.g. 'blob:qute%3A///b45250b3-787e-44d1-a8d8-c2c90f81f981'
|
||||
is_download_url = (url.scheme() == 'blob' and
|
||||
QUrl(url.path()).scheme() == 'qute')
|
||||
is_pdf = mimetype in ['application/pdf', 'application/x-pdf']
|
||||
return is_pdf and not is_download_url and config.val.content.pdfjs
|
||||
|
||||
|
||||
def get_main_url(filename):
|
||||
"""Get the URL to be opened to view a local PDF."""
|
||||
url = QUrl('qute://pdfjs/web/viewer.html')
|
||||
query = QUrlQuery()
|
||||
query.addQueryItem('filename', filename) # read from our JS
|
||||
query.addQueryItem('file', '') # to avoid pdfjs opening the default PDF
|
||||
url.setQuery(query)
|
||||
return url
|
||||
|
@ -29,7 +29,7 @@ from PyQt5.QtCore import pyqtSlot, pyqtSignal, QTimer
|
||||
from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import message, usertypes, log, urlutils, utils
|
||||
from qutebrowser.utils import message, usertypes, log, urlutils, utils, debug
|
||||
from qutebrowser.browser import downloads
|
||||
from qutebrowser.browser.webkit import http
|
||||
from qutebrowser.browser.webkit.network import networkmanager
|
||||
@ -307,7 +307,14 @@ class DownloadItem(downloads.AbstractDownloadItem):
|
||||
"""Handle QNetworkReply errors."""
|
||||
if code == QNetworkReply.OperationCanceledError:
|
||||
return
|
||||
self._die(self._reply.errorString())
|
||||
|
||||
if self._reply is None:
|
||||
error = "Unknown error: {}".format(
|
||||
debug.qenum_key(QNetworkReply, code))
|
||||
else:
|
||||
error = self._reply.errorString()
|
||||
|
||||
self._die(error)
|
||||
|
||||
@pyqtSlot()
|
||||
def _on_read_timer_timeout(self):
|
||||
|
@ -24,62 +24,72 @@ Module attributes:
|
||||
_HANDLERS: The handlers registered via decorators.
|
||||
"""
|
||||
|
||||
import html
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
import textwrap
|
||||
import mimetypes
|
||||
import urllib
|
||||
import collections
|
||||
import base64
|
||||
|
||||
import pkg_resources
|
||||
import sip
|
||||
from PyQt5.QtCore import QUrlQuery, QUrl
|
||||
try:
|
||||
import secrets
|
||||
except ImportError:
|
||||
# New in Python 3.6
|
||||
secrets = None
|
||||
|
||||
from PyQt5.QtCore import QUrlQuery, QUrl, qVersion
|
||||
|
||||
import qutebrowser
|
||||
from qutebrowser.browser import pdfjs, downloads
|
||||
from qutebrowser.config import config, configdata, configexc, configdiff
|
||||
from qutebrowser.utils import (version, utils, jinja, log, message, docutils,
|
||||
objreg, urlutils)
|
||||
from qutebrowser.misc import objects
|
||||
from qutebrowser.qt import sip
|
||||
|
||||
|
||||
pyeval_output = ":pyeval was never called"
|
||||
spawn_output = ":spawn was never called"
|
||||
csrf_token = None
|
||||
|
||||
|
||||
_HANDLERS = {}
|
||||
|
||||
|
||||
class NoHandlerFound(Exception):
|
||||
class Error(Exception):
|
||||
|
||||
"""Raised when no handler was found for the given URL."""
|
||||
"""Exception for generic errors on a qute:// page."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class QuteSchemeOSError(Exception):
|
||||
class NotFoundError(Error):
|
||||
|
||||
"""Called when there was an OSError inside a handler."""
|
||||
"""Raised when the given URL was not found."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class QuteSchemeError(Exception):
|
||||
class SchemeOSError(Error):
|
||||
|
||||
"""Exception to signal that a handler should return an ErrorReply.
|
||||
"""Raised when there was an OSError inside a handler."""
|
||||
|
||||
Attributes correspond to the arguments in
|
||||
networkreply.ErrorNetworkReply.
|
||||
pass
|
||||
|
||||
Attributes:
|
||||
errorstring: Error string to print.
|
||||
error: Numerical error value.
|
||||
"""
|
||||
|
||||
def __init__(self, errorstring, error):
|
||||
self.errorstring = errorstring
|
||||
self.error = error
|
||||
super().__init__(errorstring)
|
||||
class UrlInvalidError(Error):
|
||||
|
||||
"""Raised when an invalid URL was opened."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class RequestDeniedError(Error):
|
||||
|
||||
"""Raised when the request is forbidden."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class Redirect(Exception):
|
||||
@ -101,12 +111,10 @@ class add_handler: # noqa: N801,N806 pylint: disable=invalid-name
|
||||
|
||||
Attributes:
|
||||
_name: The 'foo' part of qute://foo
|
||||
backend: Limit which backends the handler can run with.
|
||||
"""
|
||||
|
||||
def __init__(self, name, backend=None):
|
||||
def __init__(self, name):
|
||||
self._name = name
|
||||
self._backend = backend
|
||||
self._function = None
|
||||
|
||||
def __call__(self, function):
|
||||
@ -116,19 +124,7 @@ class add_handler: # noqa: N801,N806 pylint: disable=invalid-name
|
||||
|
||||
def wrapper(self, *args, **kwargs):
|
||||
"""Call the underlying function."""
|
||||
if self._backend is not None and objects.backend != self._backend:
|
||||
return self.wrong_backend_handler(*args, **kwargs)
|
||||
else:
|
||||
return self._function(*args, **kwargs)
|
||||
|
||||
def wrong_backend_handler(self, url):
|
||||
"""Show an error page about using the invalid backend."""
|
||||
html = jinja.render('error.html',
|
||||
title="Error while opening qute://url",
|
||||
url=url.toDisplayString(),
|
||||
error='{} is not available with this '
|
||||
'backend'.format(url.toDisplayString()))
|
||||
return 'text/html', html
|
||||
return self._function(*args, **kwargs)
|
||||
|
||||
|
||||
def data_for_url(url):
|
||||
@ -170,15 +166,13 @@ def data_for_url(url):
|
||||
try:
|
||||
handler = _HANDLERS[host]
|
||||
except KeyError:
|
||||
raise NoHandlerFound(url)
|
||||
raise NotFoundError("No handler found for {}".format(
|
||||
url.toDisplayString()))
|
||||
|
||||
try:
|
||||
mimetype, data = handler(url)
|
||||
except OSError as e:
|
||||
# FIXME:qtwebengine how to handle this?
|
||||
raise QuteSchemeOSError(e)
|
||||
except QuteSchemeError:
|
||||
raise
|
||||
raise SchemeOSError(e)
|
||||
|
||||
assert mimetype is not None, url
|
||||
if mimetype == 'text/html' and isinstance(data, str):
|
||||
@ -196,11 +190,11 @@ def qute_bookmarks(_url):
|
||||
quickmarks = sorted(objreg.get('quickmark-manager').marks.items(),
|
||||
key=lambda x: x[0]) # Sort by name
|
||||
|
||||
html = jinja.render('bookmarks.html',
|
||||
title='Bookmarks',
|
||||
bookmarks=bookmarks,
|
||||
quickmarks=quickmarks)
|
||||
return 'text/html', html
|
||||
src = jinja.render('bookmarks.html',
|
||||
title='Bookmarks',
|
||||
bookmarks=bookmarks,
|
||||
quickmarks=quickmarks)
|
||||
return 'text/html', src
|
||||
|
||||
|
||||
@add_handler('tabs')
|
||||
@ -218,10 +212,10 @@ def qute_tabs(_url):
|
||||
urlstr = tab.url().toDisplayString()
|
||||
tabs[str(win_id)].append((tab.title(), urlstr))
|
||||
|
||||
html = jinja.render('tabs.html',
|
||||
title='Tabs',
|
||||
tab_list_by_window=tabs)
|
||||
return 'text/html', html
|
||||
src = jinja.render('tabs.html',
|
||||
title='Tabs',
|
||||
tab_list_by_window=tabs)
|
||||
return 'text/html', src
|
||||
|
||||
|
||||
def history_data(start_time, offset=None):
|
||||
@ -241,8 +235,9 @@ def history_data(start_time, offset=None):
|
||||
end_time = start_time - 24*60*60
|
||||
entries = hist.entries_between(end_time, start_time)
|
||||
|
||||
return [{"url": e.url, "title": e.title or e.url, "time": e.atime}
|
||||
for e in entries]
|
||||
return [{"url": e.url,
|
||||
"title": html.escape(e.title) or html.escape(e.url),
|
||||
"time": e.atime} for e in entries]
|
||||
|
||||
|
||||
@add_handler('history')
|
||||
@ -252,14 +247,14 @@ def qute_history(url):
|
||||
try:
|
||||
offset = QUrlQuery(url).queryItemValue("offset")
|
||||
offset = int(offset) if offset else None
|
||||
except ValueError as e:
|
||||
raise QuteSchemeError("Query parameter offset is invalid", e)
|
||||
except ValueError:
|
||||
raise UrlInvalidError("Query parameter offset is invalid")
|
||||
# Use start_time in query or current time.
|
||||
try:
|
||||
start_time = QUrlQuery(url).queryItemValue("start_time")
|
||||
start_time = float(start_time) if start_time else time.time()
|
||||
except ValueError as e:
|
||||
raise QuteSchemeError("Query parameter start_time is invalid", e)
|
||||
except ValueError:
|
||||
raise UrlInvalidError("Query parameter start_time is invalid")
|
||||
|
||||
return 'text/html', json.dumps(history_data(start_time, offset))
|
||||
else:
|
||||
@ -281,31 +276,31 @@ def qute_javascript(url):
|
||||
path = "javascript" + os.sep.join(path.split('/'))
|
||||
return 'text/html', utils.read_file(path, binary=False)
|
||||
else:
|
||||
raise QuteSchemeError("No file specified", ValueError())
|
||||
raise UrlInvalidError("No file specified")
|
||||
|
||||
|
||||
@add_handler('pyeval')
|
||||
def qute_pyeval(_url):
|
||||
"""Handler for qute://pyeval."""
|
||||
html = jinja.render('pre.html', title='pyeval', content=pyeval_output)
|
||||
return 'text/html', html
|
||||
src = jinja.render('pre.html', title='pyeval', content=pyeval_output)
|
||||
return 'text/html', src
|
||||
|
||||
|
||||
@add_handler('spawn-output')
|
||||
def qute_spawn_output(_url):
|
||||
"""Handler for qute://spawn-output."""
|
||||
html = jinja.render('pre.html', title='spawn output', content=spawn_output)
|
||||
return 'text/html', html
|
||||
src = jinja.render('pre.html', title='spawn output', content=spawn_output)
|
||||
return 'text/html', src
|
||||
|
||||
|
||||
@add_handler('version')
|
||||
@add_handler('verizon')
|
||||
def qute_version(_url):
|
||||
"""Handler for qute://version."""
|
||||
html = jinja.render('version.html', title='Version info',
|
||||
version=version.version(),
|
||||
copyright=qutebrowser.__copyright__)
|
||||
return 'text/html', html
|
||||
src = jinja.render('version.html', title='Version info',
|
||||
version=version.version(),
|
||||
copyright=qutebrowser.__copyright__)
|
||||
return 'text/html', src
|
||||
|
||||
|
||||
@add_handler('plainlog')
|
||||
@ -323,8 +318,8 @@ def qute_plainlog(url):
|
||||
if not level:
|
||||
level = 'vdebug'
|
||||
text = log.ram_handler.dump_log(html=False, level=level)
|
||||
html = jinja.render('pre.html', title='log', content=text)
|
||||
return 'text/html', html
|
||||
src = jinja.render('pre.html', title='log', content=text)
|
||||
return 'text/html', src
|
||||
|
||||
|
||||
@add_handler('log')
|
||||
@ -343,8 +338,8 @@ def qute_log(url):
|
||||
level = 'vdebug'
|
||||
html_log = log.ram_handler.dump_log(html=True, level=level)
|
||||
|
||||
html = jinja.render('log.html', title='log', content=html_log)
|
||||
return 'text/html', html
|
||||
src = jinja.render('log.html', title='log', content=html_log)
|
||||
return 'text/html', src
|
||||
|
||||
|
||||
@add_handler('gpl')
|
||||
@ -353,6 +348,23 @@ def qute_gpl(_url):
|
||||
return 'text/html', utils.read_file('html/license.html')
|
||||
|
||||
|
||||
def _asciidoc_fallback_path(html_path):
|
||||
"""Fall back to plaintext asciidoc if the HTML is unavailable."""
|
||||
asciidoc_path = html_path.replace('.html', '.asciidoc')
|
||||
asciidoc_paths = [asciidoc_path]
|
||||
if asciidoc_path.startswith('html/doc/'):
|
||||
asciidoc_paths += [asciidoc_path.replace('html/doc/', '../doc/help/'),
|
||||
asciidoc_path.replace('html/doc/', '../doc/')]
|
||||
|
||||
for path in asciidoc_paths:
|
||||
try:
|
||||
return utils.read_file(path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@add_handler('help')
|
||||
def qute_help(url):
|
||||
"""Handler for qute://help."""
|
||||
@ -370,23 +382,14 @@ def qute_help(url):
|
||||
try:
|
||||
bdata = utils.read_file(path, binary=True)
|
||||
except OSError as e:
|
||||
raise QuteSchemeOSError(e)
|
||||
mimetype, _encoding = mimetypes.guess_type(urlpath)
|
||||
assert mimetype is not None, url
|
||||
raise SchemeOSError(e)
|
||||
mimetype = utils.guess_mimetype(urlpath)
|
||||
return mimetype, bdata
|
||||
|
||||
try:
|
||||
data = utils.read_file(path)
|
||||
except OSError:
|
||||
# No .html around, let's see if we find the asciidoc
|
||||
asciidoc_path = path.replace('.html', '.asciidoc')
|
||||
if asciidoc_path.startswith('html/doc/'):
|
||||
asciidoc_path = asciidoc_path.replace('html/doc/', '../doc/help/')
|
||||
|
||||
try:
|
||||
asciidoc = utils.read_file(asciidoc_path)
|
||||
except OSError:
|
||||
asciidoc = None
|
||||
asciidoc = _asciidoc_fallback_path(path)
|
||||
|
||||
if asciidoc is None:
|
||||
raise
|
||||
@ -412,17 +415,6 @@ def qute_help(url):
|
||||
return 'text/html', data
|
||||
|
||||
|
||||
@add_handler('backend-warning')
|
||||
def qute_backend_warning(_url):
|
||||
"""Handler for qute://backend-warning."""
|
||||
html = jinja.render('backend-warning.html',
|
||||
distribution=version.distribution(),
|
||||
Distribution=version.Distribution,
|
||||
version=pkg_resources.parse_version,
|
||||
title="Legacy backend warning")
|
||||
return 'text/html', html
|
||||
|
||||
|
||||
def _qute_settings_set(url):
|
||||
"""Handler for qute://settings/set."""
|
||||
query = QUrlQuery(url)
|
||||
@ -447,13 +439,29 @@ def _qute_settings_set(url):
|
||||
@add_handler('settings')
|
||||
def qute_settings(url):
|
||||
"""Handler for qute://settings. View/change qute configuration."""
|
||||
global csrf_token
|
||||
|
||||
if url.path() == '/set':
|
||||
if url.password() != csrf_token:
|
||||
message.error("Invalid CSRF token for qute://settings!")
|
||||
raise RequestDeniedError("Invalid CSRF token!")
|
||||
return _qute_settings_set(url)
|
||||
|
||||
html = jinja.render('settings.html', title='settings',
|
||||
configdata=configdata,
|
||||
confget=config.instance.get_str)
|
||||
return 'text/html', html
|
||||
# Requests to qute://settings/set should only be allowed from
|
||||
# qute://settings. As an additional security precaution, we generate a CSRF
|
||||
# token to use here.
|
||||
if secrets:
|
||||
csrf_token = secrets.token_urlsafe()
|
||||
else:
|
||||
# On Python < 3.6, from secrets.py
|
||||
token = base64.urlsafe_b64encode(os.urandom(32))
|
||||
csrf_token = token.rstrip(b'=').decode('ascii')
|
||||
|
||||
src = jinja.render('settings.html', title='settings',
|
||||
configdata=configdata,
|
||||
confget=config.instance.get_str,
|
||||
csrf_token=csrf_token)
|
||||
return 'text/html', src
|
||||
|
||||
|
||||
@add_handler('bindings')
|
||||
@ -467,9 +475,9 @@ def qute_bindings(_url):
|
||||
for mode in modes:
|
||||
bindings[mode] = config.key_instance.get_bindings_for(mode)
|
||||
|
||||
html = jinja.render('bindings.html', title='Bindings',
|
||||
bindings=bindings)
|
||||
return 'text/html', html
|
||||
src = jinja.render('bindings.html', title='Bindings',
|
||||
bindings=bindings)
|
||||
return 'text/html', src
|
||||
|
||||
|
||||
@add_handler('back')
|
||||
@ -478,10 +486,10 @@ def qute_back(url):
|
||||
|
||||
Simple page to free ram / lazy load a site, goes back on focusing the tab.
|
||||
"""
|
||||
html = jinja.render(
|
||||
src = jinja.render(
|
||||
'back.html',
|
||||
title='Suspended: ' + urllib.parse.unquote(url.fragment()))
|
||||
return 'text/html', html
|
||||
return 'text/html', src
|
||||
|
||||
|
||||
@add_handler('configdiff')
|
||||
@ -504,3 +512,59 @@ def qute_pastebin_version(_url):
|
||||
"""Handler that pastebins the version string."""
|
||||
version.pastebin_version()
|
||||
return 'text/plain', b'Paste called.'
|
||||
|
||||
|
||||
@add_handler('pdfjs')
|
||||
def qute_pdfjs(url):
|
||||
"""Handler for qute://pdfjs. Return the pdf.js viewer."""
|
||||
if url.path() == '/file':
|
||||
filename = QUrlQuery(url).queryItemValue('filename')
|
||||
if not filename:
|
||||
raise UrlInvalidError("Missing filename")
|
||||
if '/' in filename or os.sep in filename:
|
||||
raise RequestDeniedError("Path separator in filename.")
|
||||
|
||||
path = os.path.join(downloads.temp_download_manager.get_tmpdir().name,
|
||||
filename)
|
||||
|
||||
with open(path, 'rb') as f:
|
||||
data = f.read()
|
||||
|
||||
mimetype = utils.guess_mimetype(filename, fallback=True)
|
||||
return mimetype, data
|
||||
|
||||
if url.path() == '/web/viewer.html':
|
||||
filename = QUrlQuery(url).queryItemValue("filename")
|
||||
if not filename:
|
||||
raise UrlInvalidError("Missing filename")
|
||||
data = pdfjs.generate_pdfjs_page(filename, url)
|
||||
return 'text/html', data
|
||||
|
||||
try:
|
||||
data = pdfjs.get_pdfjs_res(url.path())
|
||||
except pdfjs.PDFJSNotFound as e:
|
||||
# Logging as the error might get lost otherwise since we're not showing
|
||||
# the error page if a single asset is missing. This way we don't lose
|
||||
# information, as the failed pdfjs requests are still in the log.
|
||||
log.misc.warning(
|
||||
"pdfjs resource requested but not found: {}".format(e.path))
|
||||
raise NotFoundError("Can't find pdfjs resource '{}'".format(e.path))
|
||||
else:
|
||||
mimetype = utils.guess_mimetype(url.fileName(), fallback=True)
|
||||
return mimetype, data
|
||||
|
||||
|
||||
@add_handler('warning')
|
||||
def qute_warning(url):
|
||||
"""Handler for qute://warning."""
|
||||
path = url.path()
|
||||
if path == '/old-qt':
|
||||
src = jinja.render('warning-old-qt.html',
|
||||
title='Old Qt warning',
|
||||
qt_version=qVersion())
|
||||
elif path == '/webkit':
|
||||
src = jinja.render('warning-webkit.html',
|
||||
title='QtWebKit backend warning')
|
||||
else:
|
||||
raise NotFoundError("Invalid warning page {}".format(path))
|
||||
return 'text/html', src
|
||||
|
@ -34,21 +34,22 @@ class CallSuper(Exception):
|
||||
"""Raised when the caller should call the superclass instead."""
|
||||
|
||||
|
||||
def custom_headers():
|
||||
def custom_headers(url):
|
||||
"""Get the combined custom headers."""
|
||||
headers = {}
|
||||
|
||||
dnt_config = config.val.content.headers.do_not_track
|
||||
dnt_config = config.instance.get('content.headers.do_not_track', url=url)
|
||||
if dnt_config is not None:
|
||||
dnt = b'1' if dnt_config else b'0'
|
||||
headers[b'DNT'] = dnt
|
||||
headers[b'X-Do-Not-Track'] = dnt
|
||||
|
||||
conf_headers = config.val.content.headers.custom
|
||||
conf_headers = config.instance.get('content.headers.custom', url=url)
|
||||
for header, value in conf_headers.items():
|
||||
headers[header.encode('ascii')] = value.encode('ascii')
|
||||
|
||||
accept_language = config.val.content.headers.accept_language
|
||||
accept_language = config.instance.get('content.headers.accept_language',
|
||||
url=url)
|
||||
if accept_language is not None:
|
||||
headers[b'Accept-Language'] = accept_language.encode('ascii')
|
||||
|
||||
@ -156,7 +157,7 @@ def ignore_certificate_errors(url, errors, abort_on):
|
||||
Return:
|
||||
True if the error should be ignored, False otherwise.
|
||||
"""
|
||||
ssl_strict = config.val.content.ssl_strict
|
||||
ssl_strict = config.instance.get('content.ssl_strict', url=url)
|
||||
log.webview.debug("Certificate errors {!r}, strict {}".format(
|
||||
errors, ssl_strict))
|
||||
|
||||
@ -213,7 +214,7 @@ def feature_permission(url, option, msg, yes_action, no_action, abort_on,
|
||||
The Question object if a question was asked (and blocking=False),
|
||||
None otherwise.
|
||||
"""
|
||||
config_val = config.instance.get(option)
|
||||
config_val = config.instance.get(option, url=url)
|
||||
if config_val == 'ask':
|
||||
if url.isValid():
|
||||
urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded)
|
||||
@ -273,7 +274,7 @@ def get_tab(win_id, target):
|
||||
return tabbed_browser.tabopen(url=None, background=bg_tab)
|
||||
|
||||
|
||||
def get_user_stylesheet(url=None):
|
||||
def get_user_stylesheet(searching=False, url=None):
|
||||
"""Get the combined user-stylesheet.
|
||||
|
||||
If `url` is given and there's no overridden stylesheet, return
|
||||
@ -289,7 +290,8 @@ def get_user_stylesheet(url=None):
|
||||
with open(filename, 'r', encoding='utf-8') as f:
|
||||
css += f.read()
|
||||
|
||||
if not config.val.scrolling.bar:
|
||||
if (config.val.scrolling.bar == 'never' or
|
||||
config.val.scrolling.bar == 'when-searching' and not searching):
|
||||
css += '\nhtml > ::-webkit-scrollbar { width: 0px; height: 0px; }'
|
||||
|
||||
return css
|
||||
@ -319,10 +321,10 @@ def netrc_authentication(url, authenticator):
|
||||
(user, _account, password) = authenticators
|
||||
except FileNotFoundError:
|
||||
log.misc.debug("No .netrc file found")
|
||||
except OSError:
|
||||
log.misc.exception("Unable to read the netrc file")
|
||||
except netrc.NetrcParseError:
|
||||
log.misc.exception("Error when parsing the netrc file")
|
||||
except OSError as e:
|
||||
log.misc.exception("Unable to read the netrc file: {}".format(e))
|
||||
except netrc.NetrcParseError as e:
|
||||
log.misc.exception("Error when parsing the netrc file: {}".format(e))
|
||||
|
||||
if user is None:
|
||||
return False
|
||||
|
@ -240,8 +240,7 @@ class BookmarkManager(UrlMarkManager):
|
||||
|
||||
def _init_lineparser(self):
|
||||
bookmarks_directory = os.path.join(standarddir.config(), 'bookmarks')
|
||||
if not os.path.isdir(bookmarks_directory):
|
||||
os.makedirs(bookmarks_directory)
|
||||
os.makedirs(bookmarks_directory, exist_ok=True)
|
||||
|
||||
bookmarks_subdir = os.path.join('bookmarks', 'urls')
|
||||
self._lineparser = lineparser.LineParser(
|
||||
|
@ -17,14 +17,8 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Generic web element related code.
|
||||
"""Generic web element related code."""
|
||||
|
||||
Module attributes:
|
||||
Group: Enum for different kinds of groups.
|
||||
SELECTORS: CSS selectors for different groups of elements.
|
||||
"""
|
||||
|
||||
import enum
|
||||
import collections.abc
|
||||
|
||||
from PyQt5.QtCore import QUrl, Qt, QEvent, QTimer
|
||||
@ -36,25 +30,6 @@ from qutebrowser.mainwindow import mainwindow
|
||||
from qutebrowser.utils import log, usertypes, utils, qtutils, objreg
|
||||
|
||||
|
||||
Group = enum.Enum('Group', ['all', 'links', 'images', 'url', 'inputs'])
|
||||
|
||||
|
||||
SELECTORS = {
|
||||
Group.all: ('a, area, textarea, select, input:not([type=hidden]), button, '
|
||||
'frame, iframe, link, summary, [onclick], [onmousedown], '
|
||||
'[role=link], [role=option], [role=button], img, '
|
||||
# Angular 1 selectors
|
||||
'[ng-click], [ngClick], [data-ng-click], [x-ng-click]'),
|
||||
Group.links: 'a[href], area[href], link[href], [role=link][href]',
|
||||
Group.images: 'img',
|
||||
Group.url: '[src], [href]',
|
||||
Group.inputs: ('input[type=text], input[type=email], input[type=url], '
|
||||
'input[type=tel], input[type=number], '
|
||||
'input[type=password], input[type=search], '
|
||||
'input:not([type]), textarea'),
|
||||
}
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
|
||||
"""Base class for WebElement errors."""
|
||||
@ -69,6 +44,18 @@ class OrphanedError(Error):
|
||||
pass
|
||||
|
||||
|
||||
def css_selector(group, url):
|
||||
"""Get a CSS selector for the given group/URL."""
|
||||
selectors = config.instance.get('hints.selectors', url)
|
||||
if group not in selectors:
|
||||
selectors = config.val.hints.selectors
|
||||
|
||||
if group not in selectors:
|
||||
raise Error("Undefined hinting group {!r}".format(group))
|
||||
|
||||
return ','.join(selectors[group])
|
||||
|
||||
|
||||
class AbstractWebElement(collections.abc.MutableMapping):
|
||||
|
||||
"""A wrapper around QtWebKit/QtWebEngine web element.
|
||||
@ -139,6 +126,18 @@ class AbstractWebElement(collections.abc.MutableMapping):
|
||||
"""Set the element value."""
|
||||
raise NotImplementedError
|
||||
|
||||
def dispatch_event(self, event, bubbles=False,
|
||||
cancelable=False, composed=False):
|
||||
"""Dispatch an event to the element.
|
||||
|
||||
Args:
|
||||
bubbles: Whether this event should bubble.
|
||||
cancelable: Whether this event can be cancelled.
|
||||
composed: Whether the event will trigger listeners outside of a
|
||||
shadow root.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def insert_text(self, text):
|
||||
"""Insert the given text into the element."""
|
||||
raise NotImplementedError
|
||||
|
@ -28,6 +28,10 @@ class CertificateErrorWrapper(usertypes.AbstractCertificateErrorWrapper):
|
||||
|
||||
"""A wrapper over a QWebEngineCertificateError."""
|
||||
|
||||
def __init__(self, error):
|
||||
super().__init__(error)
|
||||
self.ignore = False
|
||||
|
||||
def __str__(self):
|
||||
return self._error.errorDescription()
|
||||
|
||||
@ -37,5 +41,8 @@ class CertificateErrorWrapper(usertypes.AbstractCertificateErrorWrapper):
|
||||
self._error.error()),
|
||||
string=str(self))
|
||||
|
||||
def url(self):
|
||||
return self._error.url()
|
||||
|
||||
def is_overridable(self):
|
||||
return self._error.isOverridable()
|
||||
|
@ -19,6 +19,7 @@
|
||||
|
||||
"""A request interceptor taking care of adblocking and custom headers."""
|
||||
|
||||
from PyQt5.QtCore import QUrl
|
||||
from PyQt5.QtWebEngineCore import (QWebEngineUrlRequestInterceptor,
|
||||
QWebEngineUrlRequestInfo)
|
||||
|
||||
@ -68,15 +69,29 @@ class RequestInterceptor(QWebEngineUrlRequestInterceptor):
|
||||
info.firstPartyUrl().toDisplayString(),
|
||||
resource_type, navigation_type))
|
||||
|
||||
url = info.requestUrl()
|
||||
first_party = info.firstPartyUrl()
|
||||
|
||||
if ((url.scheme(), url.host(), url.path()) ==
|
||||
('qute', 'settings', '/set')):
|
||||
if (first_party != QUrl('qute://settings/') or
|
||||
info.resourceType() !=
|
||||
QWebEngineUrlRequestInfo.ResourceTypeXhr):
|
||||
log.webview.warning("Blocking malicious request from {} to {}"
|
||||
.format(first_party.toDisplayString(),
|
||||
url.toDisplayString()))
|
||||
info.block(True)
|
||||
return
|
||||
|
||||
# FIXME:qtwebengine only block ads for NavigationTypeOther?
|
||||
if self._host_blocker.is_blocked(info.requestUrl()):
|
||||
if self._host_blocker.is_blocked(url, first_party):
|
||||
log.webview.info("Request to {} blocked by host blocker.".format(
|
||||
info.requestUrl().host()))
|
||||
url.host()))
|
||||
info.block(True)
|
||||
|
||||
for header, value in shared.custom_headers():
|
||||
for header, value in shared.custom_headers(url=url):
|
||||
info.setHttpHeader(header, value)
|
||||
|
||||
user_agent = config.val.content.headers.user_agent
|
||||
user_agent = config.instance.get('content.headers.user_agent', url=url)
|
||||
if user_agent is not None:
|
||||
info.setHttpHeader(b'User-Agent', user_agent.encode('ascii'))
|
||||
|
@ -21,10 +21,12 @@
|
||||
|
||||
import glob
|
||||
import os
|
||||
import os.path
|
||||
import re
|
||||
import shutil
|
||||
|
||||
from PyQt5.QtCore import QLibraryInfo
|
||||
from qutebrowser.utils import log, message
|
||||
from qutebrowser.utils import log, message, standarddir, qtutils
|
||||
|
||||
dict_version_re = re.compile(r".+-(?P<version>[0-9]+-[0-9]+?)\.bdic")
|
||||
|
||||
@ -39,9 +41,12 @@ def version(filename):
|
||||
return tuple(int(n) for n in match.group('version').split('-'))
|
||||
|
||||
|
||||
def dictionary_dir():
|
||||
def dictionary_dir(old=False):
|
||||
"""Return the path (str) to the QtWebEngine's dictionaries directory."""
|
||||
datapath = QLibraryInfo.location(QLibraryInfo.DataPath)
|
||||
if qtutils.version_check('5.10', compiled=False) and not old:
|
||||
datapath = standarddir.data()
|
||||
else:
|
||||
datapath = QLibraryInfo.location(QLibraryInfo.DataPath)
|
||||
return os.path.join(datapath, 'qtwebengine_dictionaries')
|
||||
|
||||
|
||||
@ -73,3 +78,16 @@ def local_filename(code):
|
||||
"""
|
||||
all_installed = local_files(code)
|
||||
return os.path.splitext(all_installed[0])[0] if all_installed else None
|
||||
|
||||
|
||||
def init():
|
||||
"""Initialize the dictionary path if supported."""
|
||||
if qtutils.version_check('5.10', compiled=False):
|
||||
new_dir = dictionary_dir()
|
||||
old_dir = dictionary_dir(old=True)
|
||||
os.environ['QTWEBENGINE_DICTIONARIES_PATH'] = new_dir
|
||||
try:
|
||||
if os.path.exists(old_dir) and not os.path.exists(new_dir):
|
||||
shutil.copytree(old_dir, new_dir)
|
||||
except OSError:
|
||||
log.misc.exception("Failed to copy old dictionaries")
|
||||
|
@ -27,7 +27,7 @@ import functools
|
||||
from PyQt5.QtCore import pyqtSlot, Qt
|
||||
from PyQt5.QtWebEngineWidgets import QWebEngineDownloadItem
|
||||
|
||||
from qutebrowser.browser import downloads
|
||||
from qutebrowser.browser import downloads, pdfjs
|
||||
from qutebrowser.utils import debug, usertypes, message, log, qtutils
|
||||
|
||||
|
||||
@ -212,15 +212,19 @@ class DownloadManager(downloads.AbstractDownloadManager):
|
||||
def handle_download(self, qt_item):
|
||||
"""Start a download coming from a QWebEngineProfile."""
|
||||
suggested_filename = _get_suggested_filename(qt_item.path())
|
||||
use_pdfjs = pdfjs.should_use_pdfjs(qt_item.mimeType(), qt_item.url())
|
||||
|
||||
download = DownloadItem(qt_item)
|
||||
self._init_item(download, auto_remove=False,
|
||||
self._init_item(download, auto_remove=use_pdfjs,
|
||||
suggested_filename=suggested_filename)
|
||||
|
||||
if self._mhtml_target is not None:
|
||||
download.set_target(self._mhtml_target)
|
||||
self._mhtml_target = None
|
||||
return
|
||||
if use_pdfjs:
|
||||
download.set_target(downloads.PDFJSDownloadTarget())
|
||||
return
|
||||
|
||||
filename = downloads.immediate_download_path()
|
||||
if filename is not None:
|
||||
|
@ -135,6 +135,10 @@ class WebEngineElement(webelem.AbstractWebElement):
|
||||
def set_value(self, value):
|
||||
self._js_call('set_value', value)
|
||||
|
||||
def dispatch_event(self, event, bubbles=False,
|
||||
cancelable=False, composed=False):
|
||||
self._js_call('dispatch_event', event, bubbles, cancelable, composed)
|
||||
|
||||
def caret_position(self):
|
||||
"""Get the text caret position for the current element.
|
||||
|
||||
@ -203,6 +207,8 @@ class WebEngineElement(webelem.AbstractWebElement):
|
||||
url = self.resolve_url(baseurl)
|
||||
if url is None:
|
||||
return True
|
||||
if baseurl.scheme() == url.scheme(): # e.g. a qute:// link
|
||||
return False
|
||||
return url.scheme() not in urlutils.WEBENGINE_SCHEMES
|
||||
|
||||
def _click_editable(self, click_target):
|
||||
|
@ -19,7 +19,7 @@
|
||||
|
||||
"""QtWebEngine specific qute://* handlers and glue code."""
|
||||
|
||||
from PyQt5.QtCore import QBuffer, QIODevice
|
||||
from PyQt5.QtCore import QBuffer, QIODevice, QUrl
|
||||
from PyQt5.QtWebEngineCore import (QWebEngineUrlSchemeHandler,
|
||||
QWebEngineUrlRequestJob)
|
||||
|
||||
@ -37,6 +37,38 @@ class QuteSchemeHandler(QWebEngineUrlSchemeHandler):
|
||||
if qtutils.version_check('5.11', compiled=False):
|
||||
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-63378
|
||||
profile.installUrlSchemeHandler(b'chrome-error', self)
|
||||
profile.installUrlSchemeHandler(b'chrome-extension', self)
|
||||
|
||||
def _check_initiator(self, job):
|
||||
"""Check whether the initiator of the job should be allowed.
|
||||
|
||||
Only the browser itself or qute:// pages should access any of those
|
||||
URLs. The request interceptor further locks down qute://settings/set.
|
||||
|
||||
Args:
|
||||
job: QWebEngineUrlRequestJob
|
||||
|
||||
Return:
|
||||
True if the initiator is allowed, False if it was blocked.
|
||||
"""
|
||||
try:
|
||||
initiator = job.initiator()
|
||||
except AttributeError:
|
||||
# Added in Qt 5.11
|
||||
return True
|
||||
|
||||
if initiator == QUrl('null') and not qtutils.version_check('5.12'):
|
||||
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-70421
|
||||
return True
|
||||
|
||||
if initiator.isValid() and initiator.scheme() != 'qute':
|
||||
log.misc.warning("Blocking malicious request from {} to {}".format(
|
||||
initiator.toDisplayString(),
|
||||
job.requestUrl().toDisplayString()))
|
||||
job.fail(QWebEngineUrlRequestJob.RequestDenied)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def requestStarted(self, job):
|
||||
"""Handle a request for a qute: scheme.
|
||||
@ -49,28 +81,40 @@ class QuteSchemeHandler(QWebEngineUrlSchemeHandler):
|
||||
"""
|
||||
url = job.requestUrl()
|
||||
|
||||
if url.scheme() == 'chrome-error':
|
||||
if url.scheme() in ['chrome-error', 'chrome-extension']:
|
||||
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-63378
|
||||
job.fail(QWebEngineUrlRequestJob.UrlInvalid)
|
||||
return
|
||||
|
||||
assert job.requestMethod() == b'GET'
|
||||
if not self._check_initiator(job):
|
||||
return
|
||||
|
||||
if job.requestMethod() != b'GET':
|
||||
job.fail(QWebEngineUrlRequestJob.RequestDenied)
|
||||
return
|
||||
|
||||
assert url.scheme() == 'qute'
|
||||
|
||||
log.misc.debug("Got request for {}".format(url.toDisplayString()))
|
||||
try:
|
||||
mimetype, data = qutescheme.data_for_url(url)
|
||||
except qutescheme.NoHandlerFound:
|
||||
log.misc.debug("No handler found for {}".format(
|
||||
url.toDisplayString()))
|
||||
job.fail(QWebEngineUrlRequestJob.UrlNotFound)
|
||||
except qutescheme.QuteSchemeOSError:
|
||||
# FIXME:qtwebengine how do we show a better error here?
|
||||
log.misc.exception("OSError while handling qute://* URL")
|
||||
job.fail(QWebEngineUrlRequestJob.UrlNotFound)
|
||||
except qutescheme.QuteSchemeError:
|
||||
# FIXME:qtwebengine how do we show a better error here?
|
||||
log.misc.exception("Error while handling qute://* URL")
|
||||
job.fail(QWebEngineUrlRequestJob.RequestFailed)
|
||||
except qutescheme.Error as e:
|
||||
errors = {
|
||||
qutescheme.NotFoundError:
|
||||
QWebEngineUrlRequestJob.UrlNotFound,
|
||||
qutescheme.UrlInvalidError:
|
||||
QWebEngineUrlRequestJob.UrlInvalid,
|
||||
qutescheme.RequestDeniedError:
|
||||
QWebEngineUrlRequestJob.RequestDenied,
|
||||
qutescheme.SchemeOSError:
|
||||
QWebEngineUrlRequestJob.UrlNotFound,
|
||||
qutescheme.Error:
|
||||
QWebEngineUrlRequestJob.RequestFailed,
|
||||
}
|
||||
exctype = type(e)
|
||||
log.misc.error("{} while handling qute://* URL".format(
|
||||
exctype.__name__))
|
||||
job.fail(errors[exctype])
|
||||
except qutescheme.Redirect as e:
|
||||
qtutils.ensure_valid(e.url)
|
||||
job.redirect(e.url)
|
||||
|
@ -298,6 +298,8 @@ def init(args):
|
||||
not hasattr(QWebEnginePage, 'setInspectedPage')): # only Qt < 5.11
|
||||
os.environ['QTWEBENGINE_REMOTE_DEBUGGING'] = str(utils.random_port())
|
||||
|
||||
spell.init()
|
||||
|
||||
_init_profiles()
|
||||
config.instance.changed.connect(_update_settings)
|
||||
|
||||
|
@ -21,27 +21,26 @@
|
||||
|
||||
import math
|
||||
import functools
|
||||
import sys
|
||||
import re
|
||||
import html as html_utils
|
||||
|
||||
import sip
|
||||
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, Qt, QEvent, QPoint, QPointF,
|
||||
QUrl, QTimer, QObject, qVersion)
|
||||
QUrl, QTimer, QObject)
|
||||
from PyQt5.QtGui import QKeyEvent, QIcon
|
||||
from PyQt5.QtNetwork import QAuthenticator
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PyQt5.QtWebEngineWidgets import QWebEnginePage, QWebEngineScript
|
||||
|
||||
from qutebrowser.config import configdata, config, configutils
|
||||
from qutebrowser.browser import browsertab, mouse, shared
|
||||
from qutebrowser.browser import browsertab, mouse, shared, webelem
|
||||
from qutebrowser.browser.webengine import (webview, webengineelem, tabhistory,
|
||||
interceptor, webenginequtescheme,
|
||||
cookies, webenginedownloads,
|
||||
webenginesettings)
|
||||
webenginesettings, certificateerror)
|
||||
from qutebrowser.misc import miscwidgets
|
||||
from qutebrowser.utils import (usertypes, qtutils, log, javascript, utils,
|
||||
message, objreg, jinja, debug)
|
||||
from qutebrowser.qt import sip
|
||||
|
||||
|
||||
_qute_scheme_handler = None
|
||||
@ -163,8 +162,8 @@ class WebEngineSearch(browsertab.AbstractSearch):
|
||||
back yet.
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
def __init__(self, tab, parent=None):
|
||||
super().__init__(tab, parent)
|
||||
self._flags = QWebEnginePage.FindFlags(0)
|
||||
self._pending_searches = 0
|
||||
|
||||
@ -184,6 +183,13 @@ class WebEngineSearch(browsertab.AbstractSearch):
|
||||
self._pending_searches))
|
||||
return
|
||||
|
||||
if sip.isdeleted(self._widget):
|
||||
# This happens when starting a search, and closing the tab
|
||||
# before results arrive.
|
||||
log.webview.debug("Ignoring finished search for deleted "
|
||||
"widget")
|
||||
return
|
||||
|
||||
found_text = 'found' if found else "didn't find"
|
||||
if flags:
|
||||
flag_text = 'with flags {}'.format(debug.qflags_key(
|
||||
@ -192,8 +198,11 @@ class WebEngineSearch(browsertab.AbstractSearch):
|
||||
flag_text = ''
|
||||
log.webview.debug(' '.join([caller, found_text, text, flag_text])
|
||||
.strip())
|
||||
|
||||
if callback is not None:
|
||||
callback(found)
|
||||
self.finished.emit(found)
|
||||
|
||||
self._widget.findText(text, flags, wrapped_callback)
|
||||
|
||||
def search(self, text, *, ignore_case='never', reverse=False,
|
||||
@ -214,6 +223,8 @@ class WebEngineSearch(browsertab.AbstractSearch):
|
||||
self._find(text, self._flags, result_cb, 'search')
|
||||
|
||||
def clear(self):
|
||||
if self.search_displayed:
|
||||
self.cleared.emit()
|
||||
self.search_displayed = False
|
||||
self._widget.findText('')
|
||||
|
||||
@ -234,6 +245,15 @@ class WebEngineCaret(browsertab.AbstractCaret):
|
||||
|
||||
"""QtWebEngine implementations related to moving the cursor/selection."""
|
||||
|
||||
def _flags(self):
|
||||
"""Get flags to pass to JS."""
|
||||
flags = set()
|
||||
if qtutils.version_check('5.7.1', compiled=False):
|
||||
flags.add('filter-prefix')
|
||||
if utils.is_windows:
|
||||
flags.add('windows')
|
||||
return list(flags)
|
||||
|
||||
@pyqtSlot(usertypes.KeyMode)
|
||||
def _on_mode_entered(self, mode):
|
||||
if mode != usertypes.KeyMode.caret:
|
||||
@ -246,9 +266,9 @@ class WebEngineCaret(browsertab.AbstractCaret):
|
||||
self._tab.search.clear()
|
||||
|
||||
self._tab.run_js_async(
|
||||
javascript.assemble('caret',
|
||||
'setPlatform', sys.platform, qVersion()))
|
||||
self._js_call('setInitialCursor', self._selection_cb)
|
||||
javascript.assemble('caret', 'setFlags', self._flags()))
|
||||
|
||||
self._js_call('setInitialCursor', callback=self._selection_cb)
|
||||
|
||||
def _selection_cb(self, enabled):
|
||||
"""Emit selection_toggled based on setInitialCursor."""
|
||||
@ -266,32 +286,25 @@ class WebEngineCaret(browsertab.AbstractCaret):
|
||||
self._js_call('disableCaret')
|
||||
|
||||
def move_to_next_line(self, count=1):
|
||||
for _ in range(count):
|
||||
self._js_call('moveDown')
|
||||
self._js_call('moveDown', count)
|
||||
|
||||
def move_to_prev_line(self, count=1):
|
||||
for _ in range(count):
|
||||
self._js_call('moveUp')
|
||||
self._js_call('moveUp', count)
|
||||
|
||||
def move_to_next_char(self, count=1):
|
||||
for _ in range(count):
|
||||
self._js_call('moveRight')
|
||||
self._js_call('moveRight', count)
|
||||
|
||||
def move_to_prev_char(self, count=1):
|
||||
for _ in range(count):
|
||||
self._js_call('moveLeft')
|
||||
self._js_call('moveLeft', count)
|
||||
|
||||
def move_to_end_of_word(self, count=1):
|
||||
for _ in range(count):
|
||||
self._js_call('moveToEndOfWord')
|
||||
self._js_call('moveToEndOfWord', count)
|
||||
|
||||
def move_to_next_word(self, count=1):
|
||||
for _ in range(count):
|
||||
self._js_call('moveToNextWord')
|
||||
self._js_call('moveToNextWord', count)
|
||||
|
||||
def move_to_prev_word(self, count=1):
|
||||
for _ in range(count):
|
||||
self._js_call('moveToPreviousWord')
|
||||
self._js_call('moveToPreviousWord', count)
|
||||
|
||||
def move_to_start_of_line(self):
|
||||
self._js_call('moveToStartOfLine')
|
||||
@ -300,20 +313,16 @@ class WebEngineCaret(browsertab.AbstractCaret):
|
||||
self._js_call('moveToEndOfLine')
|
||||
|
||||
def move_to_start_of_next_block(self, count=1):
|
||||
for _ in range(count):
|
||||
self._js_call('moveToStartOfNextBlock')
|
||||
self._js_call('moveToStartOfNextBlock', count)
|
||||
|
||||
def move_to_start_of_prev_block(self, count=1):
|
||||
for _ in range(count):
|
||||
self._js_call('moveToStartOfPrevBlock')
|
||||
self._js_call('moveToStartOfPrevBlock', count)
|
||||
|
||||
def move_to_end_of_next_block(self, count=1):
|
||||
for _ in range(count):
|
||||
self._js_call('moveToEndOfNextBlock')
|
||||
self._js_call('moveToEndOfNextBlock', count)
|
||||
|
||||
def move_to_end_of_prev_block(self, count=1):
|
||||
for _ in range(count):
|
||||
self._js_call('moveToEndOfPrevBlock')
|
||||
self._js_call('moveToEndOfPrevBlock', count)
|
||||
|
||||
def move_to_start_of_document(self):
|
||||
self._js_call('moveToStartOfDocument')
|
||||
@ -322,7 +331,7 @@ class WebEngineCaret(browsertab.AbstractCaret):
|
||||
self._js_call('moveToEndOfDocument')
|
||||
|
||||
def toggle_selection(self):
|
||||
self._js_call('toggleSelection', self.selection_toggled.emit)
|
||||
self._js_call('toggleSelection', callback=self.selection_toggled.emit)
|
||||
|
||||
def drop_selection(self):
|
||||
self._js_call('dropSelection')
|
||||
@ -335,7 +344,13 @@ class WebEngineCaret(browsertab.AbstractCaret):
|
||||
self._tab.run_js_async(javascript.assemble('caret', 'getSelection'),
|
||||
callback)
|
||||
|
||||
def _follow_selected_cb(self, js_elem, tab=False):
|
||||
def _follow_selected_cb_wrapped(self, js_elem, tab):
|
||||
try:
|
||||
self._follow_selected_cb(js_elem, tab)
|
||||
finally:
|
||||
self.follow_selected_done.emit()
|
||||
|
||||
def _follow_selected_cb(self, js_elem, tab):
|
||||
"""Callback for javascript which clicks the selected element.
|
||||
|
||||
Args:
|
||||
@ -344,6 +359,7 @@ class WebEngineCaret(browsertab.AbstractCaret):
|
||||
"""
|
||||
if js_elem is None:
|
||||
return
|
||||
|
||||
if js_elem == "focused":
|
||||
# we had a focused element, not a selected one. Just send <enter>
|
||||
self._follow_enter(tab)
|
||||
@ -360,7 +376,10 @@ class WebEngineCaret(browsertab.AbstractCaret):
|
||||
if elem.is_link():
|
||||
log.webview.debug("Found link in selection, clicking. ClickTarget "
|
||||
"{}, elem {}".format(click_type, elem))
|
||||
elem.click(click_type)
|
||||
try:
|
||||
elem.click(click_type)
|
||||
except webelem.Error as e:
|
||||
message.error(str(e))
|
||||
|
||||
def follow_selected(self, *, tab=False):
|
||||
if self._tab.search.search_displayed:
|
||||
@ -376,11 +395,13 @@ class WebEngineCaret(browsertab.AbstractCaret):
|
||||
# click an existing blue selection
|
||||
js_code = javascript.assemble('webelem',
|
||||
'find_selected_focused_link')
|
||||
self._tab.run_js_async(js_code, lambda jsret:
|
||||
self._follow_selected_cb(jsret, tab))
|
||||
self._tab.run_js_async(
|
||||
js_code,
|
||||
lambda jsret: self._follow_selected_cb_wrapped(jsret, tab))
|
||||
|
||||
def _js_call(self, command, callback=None):
|
||||
self._tab.run_js_async(javascript.assemble('caret', command), callback)
|
||||
def _js_call(self, command, *args, callback=None):
|
||||
code = javascript.assemble('caret', command, *args)
|
||||
self._tab.run_js_async(code, callback)
|
||||
|
||||
|
||||
class WebEngineScroller(browsertab.AbstractScroller):
|
||||
@ -575,9 +596,12 @@ class WebEngineElements(browsertab.AbstractElements):
|
||||
if js_elems is None:
|
||||
callback(None)
|
||||
return
|
||||
elif not js_elems['success']:
|
||||
callback(webelem.Error(js_elems['error']))
|
||||
return
|
||||
|
||||
elems = []
|
||||
for js_elem in js_elems:
|
||||
for js_elem in js_elems['result']:
|
||||
elem = webengineelem.WebEngineElement(js_elem, tab=self._tab)
|
||||
elems.append(elem)
|
||||
callback(elems)
|
||||
@ -617,8 +641,8 @@ class WebEngineElements(browsertab.AbstractElements):
|
||||
self._tab.run_js_async(js_code, js_cb)
|
||||
|
||||
def find_at_pos(self, pos, callback):
|
||||
assert pos.x() >= 0
|
||||
assert pos.y() >= 0
|
||||
assert pos.x() >= 0, pos
|
||||
assert pos.y() >= 0, pos
|
||||
pos /= self._tab.zoom.factor()
|
||||
js_code = javascript.assemble('webelem', 'find_at_pos',
|
||||
pos.x(), pos.y())
|
||||
@ -628,14 +652,26 @@ class WebEngineElements(browsertab.AbstractElements):
|
||||
|
||||
class WebEngineAudio(browsertab.AbstractAudio):
|
||||
|
||||
"""QtWebEngine implemementations related to audio/muting."""
|
||||
"""QtWebEngine implemementations related to audio/muting.
|
||||
|
||||
Attributes:
|
||||
_overridden: Whether the user toggled muting manually.
|
||||
If that's the case, we leave it alone.
|
||||
"""
|
||||
|
||||
def __init__(self, tab, parent=None):
|
||||
super().__init__(tab, parent)
|
||||
self._overridden = False
|
||||
|
||||
def _connect_signals(self):
|
||||
page = self._widget.page()
|
||||
page.audioMutedChanged.connect(self.muted_changed)
|
||||
page.recentlyAudibleChanged.connect(self.recently_audible_changed)
|
||||
self._tab.url_changed.connect(self._on_url_changed)
|
||||
config.instance.changed.connect(self._on_config_changed)
|
||||
|
||||
def set_muted(self, muted: bool):
|
||||
def set_muted(self, muted: bool, override: bool = False):
|
||||
self._overridden = override
|
||||
page = self._widget.page()
|
||||
page.setAudioMuted(muted)
|
||||
|
||||
@ -647,6 +683,17 @@ class WebEngineAudio(browsertab.AbstractAudio):
|
||||
page = self._widget.page()
|
||||
return page.recentlyAudible()
|
||||
|
||||
@pyqtSlot(QUrl)
|
||||
def _on_url_changed(self, url):
|
||||
if self._overridden:
|
||||
return
|
||||
mute = config.instance.get('content.mute', url=url)
|
||||
self.set_muted(mute)
|
||||
|
||||
@config.change_filter('content.mute')
|
||||
def _on_config_changed(self):
|
||||
self._on_url_changed(self._tab.url())
|
||||
|
||||
|
||||
class _WebEnginePermissions(QObject):
|
||||
|
||||
@ -703,6 +750,18 @@ class _WebEnginePermissions(QObject):
|
||||
QWebEnginePage.MediaVideoCapture: 'record video',
|
||||
QWebEnginePage.MediaAudioVideoCapture: 'record audio/video',
|
||||
}
|
||||
try:
|
||||
options.update({
|
||||
QWebEnginePage.MouseLock:
|
||||
'content.mouse_lock',
|
||||
})
|
||||
messages.update({
|
||||
QWebEnginePage.MouseLock:
|
||||
'hide your mouse pointer',
|
||||
})
|
||||
except AttributeError:
|
||||
# Added in Qt 5.8
|
||||
pass
|
||||
try:
|
||||
options.update({
|
||||
QWebEnginePage.DesktopVideoCapture:
|
||||
@ -788,12 +847,18 @@ class _WebEngineScripts(QObject):
|
||||
super().__init__(parent)
|
||||
self._tab = tab
|
||||
self._widget = None
|
||||
self._greasemonkey = objreg.get('greasemonkey')
|
||||
|
||||
def connect_signals(self):
|
||||
"""Connect signals to our private slots."""
|
||||
config.instance.changed.connect(self._on_config_changed)
|
||||
self._tab.url_changed.connect(self._update_stylesheet)
|
||||
self._tab.load_finished.connect(self._on_load_finished)
|
||||
|
||||
self._tab.search.cleared.connect(functools.partial(
|
||||
self._update_stylesheet, searching=False))
|
||||
self._tab.search.finished.connect(self._on_load_finished)
|
||||
|
||||
@pyqtSlot(str)
|
||||
def _on_config_changed(self, option):
|
||||
if option in ['scrolling.bar', 'content.user_stylesheets']:
|
||||
@ -805,14 +870,14 @@ class _WebEngineScripts(QObject):
|
||||
self._update_stylesheet(url)
|
||||
|
||||
@pyqtSlot(QUrl)
|
||||
def _update_stylesheet(self, url, force=False):
|
||||
def _update_stylesheet(self, url, searching=False, force=False):
|
||||
"""Update the custom stylesheet in existing tabs.
|
||||
|
||||
Arguments:
|
||||
url: The url to get the stylesheet for.
|
||||
force: Also update the global stylesheet.
|
||||
"""
|
||||
css = shared.get_user_stylesheet(url=url)
|
||||
css = shared.get_user_stylesheet(searching=searching, url=url)
|
||||
if css is configutils.UNSET and force:
|
||||
css = shared.get_user_stylesheet(url=None)
|
||||
|
||||
@ -869,9 +934,16 @@ class _WebEngineScripts(QObject):
|
||||
self._inject_early_js('js', js_code, subframes=True)
|
||||
self._init_stylesheet()
|
||||
|
||||
greasemonkey = objreg.get('greasemonkey')
|
||||
greasemonkey.scripts_reloaded.connect(self._inject_userscripts)
|
||||
self._inject_userscripts()
|
||||
# The Greasemonkey metadata block support in QtWebEngine only starts at
|
||||
# Qt 5.8. With 5.7.1, we need to inject the scripts ourselves in
|
||||
# response to urlChanged.
|
||||
if not qtutils.version_check('5.8'):
|
||||
self._tab.url_changed.connect(
|
||||
self._inject_greasemonkey_scripts_for_url)
|
||||
else:
|
||||
self._greasemonkey.scripts_reloaded.connect(
|
||||
self._inject_all_greasemonkey_scripts)
|
||||
self._inject_all_greasemonkey_scripts()
|
||||
|
||||
def _init_stylesheet(self):
|
||||
"""Initialize custom stylesheets.
|
||||
@ -888,40 +960,90 @@ class _WebEngineScripts(QObject):
|
||||
)
|
||||
self._inject_early_js('stylesheet', js_code, subframes=True)
|
||||
|
||||
def _inject_userscripts(self):
|
||||
"""Register user JavaScript files with the global profiles."""
|
||||
# The Greasemonkey metadata block support in QtWebEngine only starts at
|
||||
# Qt 5.8. With 5.7.1, we need to inject the scripts ourselves in
|
||||
# response to urlChanged.
|
||||
if not qtutils.version_check('5.8'):
|
||||
return
|
||||
@pyqtSlot(QUrl)
|
||||
def _inject_greasemonkey_scripts_for_url(self, url):
|
||||
matching_scripts = self._greasemonkey.scripts_for(url)
|
||||
self._inject_greasemonkey_scripts(
|
||||
matching_scripts.start, QWebEngineScript.DocumentCreation, True)
|
||||
self._inject_greasemonkey_scripts(
|
||||
matching_scripts.end, QWebEngineScript.DocumentReady, False)
|
||||
self._inject_greasemonkey_scripts(
|
||||
matching_scripts.idle, QWebEngineScript.Deferred, False)
|
||||
|
||||
# Since we are inserting scripts into profile.scripts they won't
|
||||
# just get replaced by new gm scripts like if we were injecting them
|
||||
# ourselves so we need to remove all gm scripts, while not removing
|
||||
# any other stuff that might have been added. Like the one for
|
||||
# stylesheets.
|
||||
greasemonkey = objreg.get('greasemonkey')
|
||||
scripts = self._widget.page().scripts()
|
||||
for script in scripts.toList():
|
||||
@pyqtSlot()
|
||||
def _inject_all_greasemonkey_scripts(self):
|
||||
scripts = self._greasemonkey.all_scripts()
|
||||
self._inject_greasemonkey_scripts(scripts)
|
||||
|
||||
def _remove_all_greasemonkey_scripts(self):
|
||||
page_scripts = self._widget.page().scripts()
|
||||
for script in page_scripts.toList():
|
||||
if script.name().startswith("GM-"):
|
||||
log.greasemonkey.debug('Removing script: {}'
|
||||
.format(script.name()))
|
||||
removed = scripts.remove(script)
|
||||
removed = page_scripts.remove(script)
|
||||
assert removed, script.name()
|
||||
|
||||
# Then add the new scripts.
|
||||
for script in greasemonkey.all_scripts():
|
||||
# @run-at (and @include/@exclude/@match) is parsed by
|
||||
# QWebEngineScript.
|
||||
def _inject_greasemonkey_scripts(self, scripts=None, injection_point=None,
|
||||
remove_first=True):
|
||||
"""Register user JavaScript files with the current tab.
|
||||
|
||||
Args:
|
||||
scripts: A list of GreasemonkeyScripts, or None to add all
|
||||
known by the Greasemonkey subsystem.
|
||||
injection_point: The QWebEngineScript::InjectionPoint stage
|
||||
to inject the script into, None to use
|
||||
auto-detection.
|
||||
remove_first: Whether to remove all previously injected
|
||||
scripts before adding these ones.
|
||||
"""
|
||||
if sip.isdeleted(self._widget):
|
||||
return
|
||||
|
||||
# Since we are inserting scripts into a per-tab collection,
|
||||
# rather than just injecting scripts on page load, we need to
|
||||
# make sure we replace existing scripts, not just add new ones.
|
||||
# While, taking care not to remove any other scripts that might
|
||||
# have been added elsewhere, like the one for stylesheets.
|
||||
page_scripts = self._widget.page().scripts()
|
||||
if remove_first:
|
||||
self._remove_all_greasemonkey_scripts()
|
||||
|
||||
if not scripts:
|
||||
return
|
||||
|
||||
for script in scripts:
|
||||
new_script = QWebEngineScript()
|
||||
new_script.setWorldId(QWebEngineScript.MainWorld)
|
||||
try:
|
||||
world = int(script.jsworld)
|
||||
if not 0 <= world <= qtutils.MAX_WORLD_ID:
|
||||
log.greasemonkey.error(
|
||||
"script {} has invalid value for '@qute-js-world'"
|
||||
": {}, should be between 0 and {}"
|
||||
.format(
|
||||
script.name,
|
||||
script.jsworld,
|
||||
qtutils.MAX_WORLD_ID))
|
||||
continue
|
||||
except ValueError:
|
||||
try:
|
||||
world = _JS_WORLD_MAP[usertypes.JsWorld[
|
||||
script.jsworld.lower()]]
|
||||
except KeyError:
|
||||
log.greasemonkey.error(
|
||||
"script {} has invalid value for '@qute-js-world'"
|
||||
": {}".format(script.name, script.jsworld))
|
||||
continue
|
||||
new_script.setWorldId(world)
|
||||
new_script.setSourceCode(script.code())
|
||||
new_script.setName("GM-{}".format(script.name))
|
||||
new_script.setRunsOnSubFrames(script.runs_on_sub_frames)
|
||||
# Override the @run-at value parsed by QWebEngineScript if desired.
|
||||
if injection_point:
|
||||
new_script.setInjectionPoint(injection_point)
|
||||
log.greasemonkey.debug('adding script: {}'
|
||||
.format(new_script.name()))
|
||||
scripts.insert(new_script)
|
||||
page_scripts.insert(new_script)
|
||||
|
||||
|
||||
class WebEngineTab(browsertab.AbstractTab):
|
||||
@ -941,16 +1063,16 @@ class WebEngineTab(browsertab.AbstractTab):
|
||||
private=private, parent=parent)
|
||||
widget = webview.WebEngineView(tabdata=self.data, win_id=win_id,
|
||||
private=private)
|
||||
self.history = WebEngineHistory(self)
|
||||
self.scroller = WebEngineScroller(self, parent=self)
|
||||
self.history = WebEngineHistory(tab=self)
|
||||
self.scroller = WebEngineScroller(tab=self, parent=self)
|
||||
self.caret = WebEngineCaret(mode_manager=mode_manager,
|
||||
tab=self, parent=self)
|
||||
self.zoom = WebEngineZoom(tab=self, parent=self)
|
||||
self.search = WebEngineSearch(parent=self)
|
||||
self.printing = WebEnginePrinting()
|
||||
self.search = WebEngineSearch(tab=self, parent=self)
|
||||
self.printing = WebEnginePrinting(tab=self)
|
||||
self.elements = WebEngineElements(tab=self)
|
||||
self.action = WebEngineAction(tab=self)
|
||||
self.audio = WebEngineAudio(parent=self)
|
||||
self.audio = WebEngineAudio(tab=self, parent=self)
|
||||
self._permissions = _WebEnginePermissions(tab=self, parent=self)
|
||||
self._scripts = _WebEngineScripts(tab=self, parent=self)
|
||||
# We're assigning settings in _set_widget
|
||||
@ -975,7 +1097,7 @@ class WebEngineTab(browsertab.AbstractTab):
|
||||
fp.installEventFilter(self._mouse_event_filter)
|
||||
self._child_event_filter = mouse.ChildEventFilter(
|
||||
eventfilter=self._mouse_event_filter, widget=self._widget,
|
||||
parent=self)
|
||||
win_id=self.win_id, parent=self)
|
||||
self._widget.installEventFilter(self._child_event_filter)
|
||||
|
||||
@pyqtSlot()
|
||||
@ -995,6 +1117,9 @@ class WebEngineTab(browsertab.AbstractTab):
|
||||
url: The QUrl to open.
|
||||
predict: If set to False, predicted_navigation is not emitted.
|
||||
"""
|
||||
if sip.isdeleted(self._widget):
|
||||
# https://github.com/qutebrowser/qutebrowser/issues/3896
|
||||
return
|
||||
self._saved_zoom = self.zoom.factor()
|
||||
self._openurl_prepare(url, predict=predict)
|
||||
self._widget.load(url)
|
||||
@ -1017,6 +1142,10 @@ class WebEngineTab(browsertab.AbstractTab):
|
||||
world_id = QWebEngineScript.ApplicationWorld
|
||||
elif isinstance(world, int):
|
||||
world_id = world
|
||||
if not 0 <= world_id <= qtutils.MAX_WORLD_ID:
|
||||
raise browsertab.WebTabError(
|
||||
"World ID should be between 0 and {}"
|
||||
.format(qtutils.MAX_WORLD_ID))
|
||||
else:
|
||||
world_id = _JS_WORLD_MAP[world]
|
||||
|
||||
@ -1153,11 +1282,10 @@ class WebEngineTab(browsertab.AbstractTab):
|
||||
@pyqtSlot()
|
||||
def _on_load_started(self):
|
||||
"""Clear search when a new load is started if needed."""
|
||||
if (qtutils.version_check('5.9', compiled=False) and
|
||||
not qtutils.version_check('5.9.2', compiled=False)):
|
||||
# WORKAROUND for
|
||||
# https://bugreports.qt.io/browse/QTBUG-61506
|
||||
self.search.clear()
|
||||
# WORKAROUND for
|
||||
# https://bugreports.qt.io/browse/QTBUG-61506
|
||||
# (seems to be back in later Qt versions as well)
|
||||
self.search.clear()
|
||||
super()._on_load_started()
|
||||
self.data.netrc_used = False
|
||||
|
||||
@ -1241,6 +1369,34 @@ class WebEngineTab(browsertab.AbstractTab):
|
||||
# the old icon is still displayed.
|
||||
self.icon_changed.emit(QIcon())
|
||||
|
||||
@pyqtSlot(certificateerror.CertificateErrorWrapper)
|
||||
def _on_ssl_errors(self, error):
|
||||
self._has_ssl_errors = True
|
||||
|
||||
url = error.url()
|
||||
log.webview.debug("Certificate error: {}".format(error))
|
||||
|
||||
if error.is_overridable():
|
||||
error.ignore = shared.ignore_certificate_errors(
|
||||
url, [error], abort_on=[self.shutting_down, self.load_started])
|
||||
else:
|
||||
log.webview.error("Non-overridable certificate error: "
|
||||
"{}".format(error))
|
||||
|
||||
log.webview.debug("ignore {}, URL {}, requested {}".format(
|
||||
error.ignore, url, self.url(requested=True)))
|
||||
|
||||
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-56207
|
||||
# We can't really know when to show an error page, as the error might
|
||||
# have happened when loading some resource.
|
||||
# However, self.url() is not available yet and the requested URL
|
||||
# might not match the URL we get from the error - so we just apply a
|
||||
# heuristic here.
|
||||
if (not qtutils.version_check('5.9') and
|
||||
not error.ignore and
|
||||
url.matches(self.url(requested=True), QUrl.RemoveScheme)):
|
||||
self._show_error_page(url, str(error))
|
||||
|
||||
@pyqtSlot(QUrl)
|
||||
def _on_predicted_navigation(self, url):
|
||||
"""If we know we're going to visit an URL soon, change the settings.
|
||||
@ -1256,10 +1412,10 @@ class WebEngineTab(browsertab.AbstractTab):
|
||||
super()._on_navigation_request(navigation)
|
||||
|
||||
if navigation.url == QUrl('qute://print'):
|
||||
command_dispatcher = objreg.get('command-dispatcher',
|
||||
scope='window',
|
||||
window=self.win_id)
|
||||
command_dispatcher.printpage()
|
||||
try:
|
||||
self.printing.show_dialog()
|
||||
except browsertab.WebTabError as e:
|
||||
message.error(str(e))
|
||||
navigation.accepted = False
|
||||
|
||||
if not navigation.accepted or not navigation.is_main_frame:
|
||||
|
@ -19,18 +19,17 @@
|
||||
|
||||
"""The main browser widget for QtWebEngine."""
|
||||
|
||||
import sip
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QUrl, PYQT_VERSION
|
||||
from PyQt5.QtCore import pyqtSignal, QUrl, PYQT_VERSION
|
||||
from PyQt5.QtGui import QPalette
|
||||
from PyQt5.QtQuickWidgets import QQuickWidget
|
||||
from PyQt5.QtWebEngineWidgets import (QWebEngineView, QWebEnginePage,
|
||||
QWebEngineScript)
|
||||
from PyQt5.QtWidgets import QWidget
|
||||
from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage
|
||||
|
||||
from qutebrowser.browser import shared
|
||||
from qutebrowser.browser.webengine import certificateerror, webenginesettings
|
||||
from qutebrowser.browser.webengine import webenginesettings, certificateerror
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import log, debug, usertypes, jinja, objreg, qtutils
|
||||
from qutebrowser.utils import log, debug, usertypes, objreg, qtutils
|
||||
from qutebrowser.misc import miscwidgets
|
||||
from qutebrowser.qt import sip
|
||||
|
||||
|
||||
class WebEngineView(QWebEngineView):
|
||||
@ -71,10 +70,10 @@ class WebEngineView(QWebEngineView):
|
||||
if proxy is not None:
|
||||
return proxy
|
||||
|
||||
# This should only find the RenderWidgetHostViewQtDelegateWidget,
|
||||
# but not e.g. a QMenu
|
||||
children = [c for c in self.findChildren(QQuickWidget)
|
||||
if c.isVisible()]
|
||||
# We don't want e.g. a QMenu.
|
||||
rwhv_class = 'QtWebEngineCore::RenderWidgetHostViewQtDelegateWidget'
|
||||
children = [c for c in self.findChildren(QWidget)
|
||||
if c.isVisible() and c.inherits(rwhv_class)]
|
||||
|
||||
log.webview.debug("Found possibly lost focusProxy: {}"
|
||||
.format(children))
|
||||
@ -152,11 +151,13 @@ class WebEnginePage(QWebEnginePage):
|
||||
|
||||
Signals:
|
||||
certificate_error: Emitted on certificate errors.
|
||||
Needs to be directly connected to a slot setting the
|
||||
'ignore' attribute.
|
||||
shutting_down: Emitted when the page is shutting down.
|
||||
navigation_request: Emitted on acceptNavigationRequest.
|
||||
"""
|
||||
|
||||
certificate_error = pyqtSignal()
|
||||
certificate_error = pyqtSignal(certificateerror.CertificateErrorWrapper)
|
||||
shutting_down = pyqtSignal()
|
||||
navigation_request = pyqtSignal(usertypes.NavigationRequest)
|
||||
|
||||
@ -166,7 +167,6 @@ class WebEnginePage(QWebEnginePage):
|
||||
self._theme_color = theme_color
|
||||
self._set_bg_color()
|
||||
config.instance.changed.connect(self._set_bg_color)
|
||||
self.urlChanged.connect(self._inject_userjs)
|
||||
|
||||
@config.change_filter('colors.webpage.bg')
|
||||
def _set_bg_color(self):
|
||||
@ -181,36 +181,9 @@ class WebEnginePage(QWebEnginePage):
|
||||
|
||||
def certificateError(self, error):
|
||||
"""Handle certificate errors coming from Qt."""
|
||||
self.certificate_error.emit()
|
||||
url = error.url()
|
||||
error = certificateerror.CertificateErrorWrapper(error)
|
||||
log.webview.debug("Certificate error: {}".format(error))
|
||||
|
||||
url_string = url.toDisplayString()
|
||||
error_page = jinja.render(
|
||||
'error.html', title="Error loading page: {}".format(url_string),
|
||||
url=url_string, error=str(error))
|
||||
|
||||
if error.is_overridable():
|
||||
ignore = shared.ignore_certificate_errors(
|
||||
url, [error], abort_on=[self.loadStarted, self.shutting_down])
|
||||
else:
|
||||
log.webview.error("Non-overridable certificate error: "
|
||||
"{}".format(error))
|
||||
ignore = False
|
||||
|
||||
# We can't really know when to show an error page, as the error might
|
||||
# have happened when loading some resource.
|
||||
# However, self.url() is not available yet and self.requestedUrl()
|
||||
# might not match the URL we get from the error - so we just apply a
|
||||
# heuristic here.
|
||||
# See https://bugreports.qt.io/browse/QTBUG-56207
|
||||
log.webview.debug("ignore {}, URL {}, requested {}".format(
|
||||
ignore, url, self.requestedUrl()))
|
||||
if not ignore and url.matches(self.requestedUrl(), QUrl.RemoveScheme):
|
||||
self.setHtml(error_page)
|
||||
|
||||
return ignore
|
||||
self.certificate_error.emit(error)
|
||||
return error.ignore
|
||||
|
||||
def javaScriptConfirm(self, url, js_msg):
|
||||
"""Override javaScriptConfirm to use qutebrowser prompts."""
|
||||
@ -288,43 +261,3 @@ class WebEnginePage(QWebEnginePage):
|
||||
is_main_frame=is_main_frame)
|
||||
self.navigation_request.emit(navigation)
|
||||
return navigation.accepted
|
||||
|
||||
@pyqtSlot('QUrl')
|
||||
def _inject_userjs(self, url):
|
||||
"""Inject userscripts registered for `url` into the current page."""
|
||||
if qtutils.version_check('5.8'):
|
||||
# Handled in webenginetab with the builtin Greasemonkey
|
||||
# support.
|
||||
return
|
||||
|
||||
# Using QWebEnginePage.scripts() to hold the user scripts means
|
||||
# we don't have to worry ourselves about where to inject the
|
||||
# page but also means scripts hang around for the tab lifecycle.
|
||||
# So clear them here.
|
||||
scripts = self.scripts()
|
||||
for script in scripts.toList():
|
||||
if script.name().startswith("GM-"):
|
||||
log.greasemonkey.debug("Removing script: {}"
|
||||
.format(script.name()))
|
||||
removed = scripts.remove(script)
|
||||
assert removed, script.name()
|
||||
|
||||
def _add_script(script, injection_point):
|
||||
new_script = QWebEngineScript()
|
||||
new_script.setInjectionPoint(injection_point)
|
||||
new_script.setWorldId(QWebEngineScript.MainWorld)
|
||||
new_script.setSourceCode(script.code())
|
||||
new_script.setName("GM-{}".format(script.name))
|
||||
new_script.setRunsOnSubFrames(script.runs_on_sub_frames)
|
||||
log.greasemonkey.debug("Adding script: {}"
|
||||
.format(new_script.name()))
|
||||
scripts.insert(new_script)
|
||||
|
||||
greasemonkey = objreg.get('greasemonkey')
|
||||
matching_scripts = greasemonkey.scripts_for(url)
|
||||
for script in matching_scripts.start:
|
||||
_add_script(script, QWebEngineScript.DocumentCreation)
|
||||
for script in matching_scripts.end:
|
||||
_add_script(script, QWebEngineScript.DocumentReady)
|
||||
for script in matching_scripts.idle:
|
||||
_add_script(script, QWebEngineScript.Deferred)
|
||||
|
@ -312,9 +312,9 @@ class _Downloader:
|
||||
for style in styles:
|
||||
style = webkitelem.WebKitElement(style, tab=self.tab)
|
||||
# The Mozilla Developer Network says:
|
||||
# type: This attribute defines the styling language as a MIME type
|
||||
# (charset should not be specified). This attribute is optional and
|
||||
# default to text/css if it's missing.
|
||||
# > type: This attribute defines the styling language as a MIME
|
||||
# > type (charset should not be specified). This attribute is
|
||||
# > optional and default to text/css if it's missing.
|
||||
# https://developer.mozilla.org/en/docs/Web/HTML/Element/style
|
||||
if 'type' in style and style['type'] != 'text/css':
|
||||
continue
|
||||
|
@ -111,11 +111,13 @@ def dirbrowser_html(path):
|
||||
return html.encode('UTF-8', errors='xmlcharrefreplace')
|
||||
|
||||
|
||||
def handler(request):
|
||||
def handler(request, _operation, _current_url):
|
||||
"""Handler for a file:// URL.
|
||||
|
||||
Args:
|
||||
request: QNetworkRequest to answer to.
|
||||
_operation: The HTTP operation being done.
|
||||
_current_url: The page we're on currently.
|
||||
|
||||
Return:
|
||||
A QNetworkReply for directories, None for files.
|
||||
|
@ -373,24 +373,9 @@ class NetworkManager(QNetworkAccessManager):
|
||||
req, proxy_error, QNetworkReply.UnknownProxyError,
|
||||
self)
|
||||
|
||||
scheme = req.url().scheme()
|
||||
if scheme in self._scheme_handlers:
|
||||
result = self._scheme_handlers[scheme](req)
|
||||
if result is not None:
|
||||
result.setParent(self)
|
||||
return result
|
||||
|
||||
for header, value in shared.custom_headers():
|
||||
for header, value in shared.custom_headers(url=req.url()):
|
||||
req.setRawHeader(header, value)
|
||||
|
||||
host_blocker = objreg.get('host-blocker')
|
||||
if host_blocker.is_blocked(req.url()):
|
||||
log.webview.info("Request to {} blocked by host blocker.".format(
|
||||
req.url().host()))
|
||||
return networkreply.ErrorNetworkReply(
|
||||
req, HOSTBLOCK_ERROR_STRING, QNetworkReply.ContentAccessDenied,
|
||||
self)
|
||||
|
||||
# There are some scenarios where we can't figure out current_url:
|
||||
# - There's a generic NetworkManager, e.g. for downloads
|
||||
# - The download was in a tab which is now closed.
|
||||
@ -408,6 +393,14 @@ class NetworkManager(QNetworkAccessManager):
|
||||
# the webpage shutdown here.
|
||||
current_url = QUrl()
|
||||
|
||||
host_blocker = objreg.get('host-blocker')
|
||||
if host_blocker.is_blocked(req.url(), current_url):
|
||||
log.webview.info("Request to {} blocked by host blocker.".format(
|
||||
req.url().host()))
|
||||
return networkreply.ErrorNetworkReply(
|
||||
req, HOSTBLOCK_ERROR_STRING, QNetworkReply.ContentAccessDenied,
|
||||
self)
|
||||
|
||||
if 'log-requests' in self._args.debug_flags:
|
||||
operation = debug.qenum_key(QNetworkAccessManager, op)
|
||||
operation = operation.replace('Operation', '').upper()
|
||||
@ -416,5 +409,12 @@ class NetworkManager(QNetworkAccessManager):
|
||||
req.url().toDisplayString(),
|
||||
current_url.toDisplayString()))
|
||||
|
||||
scheme = req.url().scheme()
|
||||
if scheme in self._scheme_handlers:
|
||||
result = self._scheme_handlers[scheme](req, op, current_url)
|
||||
if result is not None:
|
||||
result.setParent(self)
|
||||
return result
|
||||
|
||||
self.set_referer(req, current_url)
|
||||
return super().createRequest(op, req, outgoing_data)
|
||||
|
@ -19,58 +19,63 @@
|
||||
|
||||
"""QtWebKit specific qute://* handlers and glue code."""
|
||||
|
||||
import mimetypes
|
||||
from PyQt5.QtCore import QUrl
|
||||
from PyQt5.QtNetwork import QNetworkReply, QNetworkAccessManager
|
||||
|
||||
from PyQt5.QtNetwork import QNetworkReply
|
||||
|
||||
from qutebrowser.browser import pdfjs, qutescheme
|
||||
from qutebrowser.browser import qutescheme
|
||||
from qutebrowser.browser.webkit.network import networkreply
|
||||
from qutebrowser.utils import log, usertypes, qtutils
|
||||
from qutebrowser.utils import log, qtutils
|
||||
|
||||
|
||||
def handler(request):
|
||||
def handler(request, operation, current_url):
|
||||
"""Scheme handler for qute:// URLs.
|
||||
|
||||
Args:
|
||||
request: QNetworkRequest to answer to.
|
||||
operation: The HTTP operation being done.
|
||||
current_url: The page we're on currently.
|
||||
|
||||
Return:
|
||||
A QNetworkReply.
|
||||
"""
|
||||
if operation != QNetworkAccessManager.GetOperation:
|
||||
return networkreply.ErrorNetworkReply(
|
||||
request, "Unsupported request type",
|
||||
QNetworkReply.ContentOperationNotPermittedError)
|
||||
|
||||
url = request.url()
|
||||
|
||||
if ((url.scheme(), url.host(), url.path()) ==
|
||||
('qute', 'settings', '/set')):
|
||||
if current_url != QUrl('qute://settings/'):
|
||||
log.webview.warning("Blocking malicious request from {} to {}"
|
||||
.format(current_url.toDisplayString(),
|
||||
url.toDisplayString()))
|
||||
return networkreply.ErrorNetworkReply(
|
||||
request, "Invalid qute://settings request",
|
||||
QNetworkReply.ContentAccessDenied)
|
||||
|
||||
try:
|
||||
mimetype, data = qutescheme.data_for_url(request.url())
|
||||
except qutescheme.NoHandlerFound:
|
||||
errorstr = "No handler found for {}!".format(
|
||||
request.url().toDisplayString())
|
||||
return networkreply.ErrorNetworkReply(
|
||||
request, errorstr, QNetworkReply.ContentNotFoundError)
|
||||
except qutescheme.QuteSchemeOSError as e:
|
||||
return networkreply.ErrorNetworkReply(
|
||||
request, str(e), QNetworkReply.ContentNotFoundError)
|
||||
except qutescheme.QuteSchemeError as e:
|
||||
return networkreply.ErrorNetworkReply(request, e.errorstring, e.error)
|
||||
mimetype, data = qutescheme.data_for_url(url)
|
||||
except qutescheme.Error as e:
|
||||
errors = {
|
||||
qutescheme.NotFoundError:
|
||||
QNetworkReply.ContentNotFoundError,
|
||||
qutescheme.UrlInvalidError:
|
||||
QNetworkReply.ContentOperationNotPermittedError,
|
||||
qutescheme.RequestDeniedError:
|
||||
QNetworkReply.ContentAccessDenied,
|
||||
qutescheme.SchemeOSError:
|
||||
QNetworkReply.ContentNotFoundError,
|
||||
qutescheme.Error:
|
||||
QNetworkReply.InternalServerError,
|
||||
}
|
||||
exctype = type(e)
|
||||
log.misc.error("{} while handling qute://* URL".format(
|
||||
exctype.__name__))
|
||||
return networkreply.ErrorNetworkReply(request, str(e), errors[exctype])
|
||||
except qutescheme.Redirect as e:
|
||||
qtutils.ensure_valid(e.url)
|
||||
return networkreply.RedirectNetworkReply(e.url)
|
||||
|
||||
return networkreply.FixedDataNetworkReply(request, data, mimetype)
|
||||
|
||||
|
||||
@qutescheme.add_handler('pdfjs', backend=usertypes.Backend.QtWebKit)
|
||||
def qute_pdfjs(url):
|
||||
"""Handler for qute://pdfjs. Return the pdf.js viewer."""
|
||||
try:
|
||||
data = pdfjs.get_pdfjs_res(url.path())
|
||||
except pdfjs.PDFJSNotFound as e:
|
||||
# Logging as the error might get lost otherwise since we're not showing
|
||||
# the error page if a single asset is missing. This way we don't lose
|
||||
# information, as the failed pdfjs requests are still in the log.
|
||||
log.misc.warning(
|
||||
"pdfjs resource requested but not found: {}".format(e.path))
|
||||
raise qutescheme.QuteSchemeError("Can't find pdfjs resource "
|
||||
"'{}'".format(e.path),
|
||||
QNetworkReply.ContentNotFoundError)
|
||||
else:
|
||||
mimetype, _encoding = mimetypes.guess_type(url.fileName())
|
||||
assert mimetype is not None, url
|
||||
return mimetype, data
|
||||
|
@ -125,8 +125,20 @@ class WebKitElement(webelem.AbstractWebElement):
|
||||
self._elem.setPlainText(value)
|
||||
else:
|
||||
log.webelem.debug("Filling {!r} via javascript.".format(self))
|
||||
value = javascript.string_escape(value)
|
||||
self._elem.evaluateJavaScript("this.value='{}'".format(value))
|
||||
value = javascript.to_js(value)
|
||||
self._elem.evaluateJavaScript("this.value={}".format(value))
|
||||
|
||||
def dispatch_event(self, event, bubbles=False,
|
||||
cancelable=False, composed=False):
|
||||
self._check_vanished()
|
||||
log.webelem.debug("Firing event on {!r} via javascript.".format(self))
|
||||
self._elem.evaluateJavaScript(
|
||||
"this.dispatchEvent(new Event({}, "
|
||||
"{{'bubbles': {}, 'cancelable': {}, 'composed': {}}}))"
|
||||
.format(javascript.to_js(event),
|
||||
javascript.to_js(bubbles),
|
||||
javascript.to_js(cancelable),
|
||||
javascript.to_js(composed)))
|
||||
|
||||
def caret_position(self):
|
||||
"""Get the text caret position for the current element."""
|
||||
@ -142,11 +154,11 @@ class WebKitElement(webelem.AbstractWebElement):
|
||||
raise webelem.Error("Element is not editable!")
|
||||
log.webelem.debug("Inserting text into element {!r}".format(self))
|
||||
self._elem.evaluateJavaScript("""
|
||||
var text = "{}";
|
||||
var text = {};
|
||||
var event = document.createEvent("TextEvent");
|
||||
event.initTextEvent("textInput", true, true, null, text);
|
||||
this.dispatchEvent(event);
|
||||
""".format(javascript.string_escape(text)))
|
||||
""".format(javascript.to_js(text)))
|
||||
|
||||
def _parent(self):
|
||||
"""Get the parent element of this element."""
|
||||
|
@ -23,7 +23,6 @@ import re
|
||||
import functools
|
||||
import xml.etree.ElementTree
|
||||
|
||||
import sip
|
||||
from PyQt5.QtCore import (pyqtSlot, Qt, QEvent, QUrl, QPoint, QTimer, QSizeF,
|
||||
QSize)
|
||||
from PyQt5.QtGui import QKeyEvent, QIcon
|
||||
@ -35,6 +34,7 @@ from qutebrowser.browser import browsertab, shared
|
||||
from qutebrowser.browser.webkit import (webview, tabhistory, webkitelem,
|
||||
webkitsettings)
|
||||
from qutebrowser.utils import qtutils, usertypes, utils, log, debug
|
||||
from qutebrowser.qt import sip
|
||||
|
||||
|
||||
class WebKitAction(browsertab.AbstractAction):
|
||||
@ -84,8 +84,8 @@ class WebKitSearch(browsertab.AbstractSearch):
|
||||
|
||||
"""QtWebKit implementations related to searching on the page."""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
def __init__(self, tab, parent=None):
|
||||
super().__init__(tab, parent)
|
||||
self._flags = QWebPage.FindFlags(0)
|
||||
|
||||
def _call_cb(self, callback, found, text, flags, caller):
|
||||
@ -115,7 +115,11 @@ class WebKitSearch(browsertab.AbstractSearch):
|
||||
if callback is not None:
|
||||
QTimer.singleShot(0, functools.partial(callback, found))
|
||||
|
||||
self.finished.emit(found)
|
||||
|
||||
def clear(self):
|
||||
if self.search_displayed:
|
||||
self.cleared.emit()
|
||||
self.search_displayed = False
|
||||
# We first clear the marked text, then the highlights
|
||||
self._widget.findText('')
|
||||
@ -348,7 +352,7 @@ class WebKitCaret(browsertab.AbstractCaret):
|
||||
def selection(self, callback):
|
||||
callback(self._widget.selectedText())
|
||||
|
||||
def follow_selected(self, *, tab=False):
|
||||
def _follow_selected(self, *, tab=False):
|
||||
if QWebSettings.globalSettings().testAttribute(
|
||||
QWebSettings.JavascriptEnabled):
|
||||
if tab:
|
||||
@ -389,6 +393,12 @@ class WebKitCaret(browsertab.AbstractCaret):
|
||||
else:
|
||||
self._tab.openurl(url)
|
||||
|
||||
def follow_selected(self, *, tab=False):
|
||||
try:
|
||||
self._follow_selected(tab=tab)
|
||||
finally:
|
||||
self.follow_selected_done.emit()
|
||||
|
||||
|
||||
class WebKitZoom(browsertab.AbstractZoom):
|
||||
|
||||
@ -631,7 +641,7 @@ class WebKitAudio(browsertab.AbstractAudio):
|
||||
|
||||
"""Dummy handling of audio status for QtWebKit."""
|
||||
|
||||
def set_muted(self, muted: bool):
|
||||
def set_muted(self, muted: bool, override: bool = False):
|
||||
raise browsertab.WebTabError('Muting is not supported on QtWebKit!')
|
||||
|
||||
def is_muted(self):
|
||||
@ -652,16 +662,16 @@ class WebKitTab(browsertab.AbstractTab):
|
||||
private=private, tab=self)
|
||||
if private:
|
||||
self._make_private(widget)
|
||||
self.history = WebKitHistory(self)
|
||||
self.scroller = WebKitScroller(self, parent=self)
|
||||
self.history = WebKitHistory(tab=self)
|
||||
self.scroller = WebKitScroller(tab=self, parent=self)
|
||||
self.caret = WebKitCaret(mode_manager=mode_manager,
|
||||
tab=self, parent=self)
|
||||
self.zoom = WebKitZoom(tab=self, parent=self)
|
||||
self.search = WebKitSearch(parent=self)
|
||||
self.printing = WebKitPrinting()
|
||||
self.search = WebKitSearch(tab=self, parent=self)
|
||||
self.printing = WebKitPrinting(tab=self)
|
||||
self.elements = WebKitElements(tab=self)
|
||||
self.action = WebKitAction(tab=self)
|
||||
self.audio = WebKitAudio(parent=self)
|
||||
self.audio = WebKitAudio(tab=self, parent=self)
|
||||
# We're assigning settings in _set_widget
|
||||
self.settings = webkitsettings.WebKitSettings(settings=None)
|
||||
self._set_widget(widget)
|
||||
@ -808,6 +818,10 @@ class WebKitTab(browsertab.AbstractTab):
|
||||
if navigation.is_main_frame:
|
||||
self.settings.update_for_url(navigation.url)
|
||||
|
||||
@pyqtSlot()
|
||||
def _on_ssl_errors(self):
|
||||
self._has_ssl_errors = True
|
||||
|
||||
def _connect_signals(self):
|
||||
view = self._widget
|
||||
page = view.page()
|
||||
|
@ -22,7 +22,6 @@
|
||||
import html
|
||||
import functools
|
||||
|
||||
import sip
|
||||
from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QUrl, QPoint
|
||||
from PyQt5.QtGui import QDesktopServices
|
||||
from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest
|
||||
@ -31,10 +30,11 @@ from PyQt5.QtPrintSupport import QPrintDialog
|
||||
from PyQt5.QtWebKitWidgets import QWebPage, QWebFrame
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.browser import pdfjs, shared
|
||||
from qutebrowser.browser import pdfjs, shared, downloads
|
||||
from qutebrowser.browser.webkit import http
|
||||
from qutebrowser.browser.webkit.network import networkmanager
|
||||
from qutebrowser.utils import message, usertypes, log, jinja, objreg
|
||||
from qutebrowser.qt import sip
|
||||
|
||||
|
||||
class BrowserPage(QWebPage):
|
||||
@ -206,17 +206,6 @@ class BrowserPage(QWebPage):
|
||||
suggested_file)
|
||||
return True
|
||||
|
||||
def _show_pdfjs(self, reply):
|
||||
"""Show the reply with pdfjs."""
|
||||
try:
|
||||
page = pdfjs.generate_pdfjs_page(reply.url())
|
||||
except pdfjs.PDFJSNotFound:
|
||||
page = jinja.render('no_pdfjs.html',
|
||||
url=reply.url().toDisplayString())
|
||||
self.mainFrame().setContent(page.encode('utf-8'), 'text/html',
|
||||
reply.url())
|
||||
reply.deleteLater()
|
||||
|
||||
def shutdown(self):
|
||||
"""Prepare the web page for being deleted."""
|
||||
self._is_shutting_down = True
|
||||
@ -279,10 +268,10 @@ class BrowserPage(QWebPage):
|
||||
else:
|
||||
reply.finished.connect(functools.partial(
|
||||
self.display_content, reply, 'image/jpeg'))
|
||||
elif (mimetype in ['application/pdf', 'application/x-pdf'] and
|
||||
config.val.content.pdfjs):
|
||||
# Use pdf.js to display the page
|
||||
self._show_pdfjs(reply)
|
||||
elif pdfjs.should_use_pdfjs(mimetype, reply.url()):
|
||||
download_manager.fetch(reply,
|
||||
target=downloads.PDFJSDownloadTarget(),
|
||||
auto_remove=True)
|
||||
else:
|
||||
# Unknown mimetype, so download anyways.
|
||||
download_manager.fetch(reply,
|
||||
@ -415,7 +404,7 @@ class BrowserPage(QWebPage):
|
||||
|
||||
def userAgentForUrl(self, url):
|
||||
"""Override QWebPage::userAgentForUrl to customize the user agent."""
|
||||
ua = config.val.content.headers.user_agent
|
||||
ua = config.instance.get('content.headers.user_agent', url=url)
|
||||
if ua is None:
|
||||
return super().userAgentForUrl(url)
|
||||
else:
|
||||
|
@ -19,7 +19,7 @@
|
||||
|
||||
"""The main browser widgets."""
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QUrl
|
||||
from PyQt5.QtCore import pyqtSignal, Qt, QUrl
|
||||
from PyQt5.QtGui import QPalette
|
||||
from PyQt5.QtWidgets import QStyleFactory
|
||||
from PyQt5.QtWebKit import QWebSettings
|
||||
@ -78,10 +78,6 @@ class WebView(QWebView):
|
||||
|
||||
self.setPage(page)
|
||||
|
||||
mode_manager = objreg.get('mode-manager', scope='window',
|
||||
window=win_id)
|
||||
mode_manager.entered.connect(self.on_mode_entered)
|
||||
mode_manager.left.connect(self.on_mode_left)
|
||||
config.instance.changed.connect(self._set_bg_color)
|
||||
|
||||
def __repr__(self):
|
||||
@ -130,32 +126,6 @@ class WebView(QWebView):
|
||||
"""
|
||||
self.load(url)
|
||||
|
||||
@pyqtSlot(usertypes.KeyMode)
|
||||
def on_mode_entered(self, mode):
|
||||
"""Ignore attempts to focus the widget if in any status-input mode.
|
||||
|
||||
FIXME:qtwebengine
|
||||
For QtWebEngine, doing the same has no effect, so we do it in here.
|
||||
"""
|
||||
if mode in [usertypes.KeyMode.command, usertypes.KeyMode.prompt,
|
||||
usertypes.KeyMode.yesno]:
|
||||
log.webview.debug("Ignoring focus because mode {} was "
|
||||
"entered.".format(mode))
|
||||
self.setFocusPolicy(Qt.NoFocus)
|
||||
|
||||
@pyqtSlot(usertypes.KeyMode)
|
||||
def on_mode_left(self, mode):
|
||||
"""Restore focus policy if status-input modes were left.
|
||||
|
||||
FIXME:qtwebengine
|
||||
For QtWebEngine, doing the same has no effect, so we do it in here.
|
||||
"""
|
||||
if mode in [usertypes.KeyMode.command, usertypes.KeyMode.prompt,
|
||||
usertypes.KeyMode.yesno]:
|
||||
log.webview.debug("Restoring focus policy because mode {} was "
|
||||
"left.".format(mode))
|
||||
self.setFocusPolicy(Qt.WheelFocus)
|
||||
|
||||
def createWindow(self, wintype):
|
||||
"""Called by Qt when a page wants to create a new window.
|
||||
|
||||
|
@ -432,7 +432,7 @@ def run_async(tab, cmd, *args, win_id, env, verbose=False):
|
||||
cmd_path = os.path.expanduser(cmd)
|
||||
|
||||
# if cmd is not given as an absolute path, look it up
|
||||
# ~/.local/share/qutebrowser/userscripts (or $XDG_DATA_DIR)
|
||||
# ~/.local/share/qutebrowser/userscripts (or $XDG_DATA_HOME)
|
||||
if not os.path.isabs(cmd_path):
|
||||
log.misc.debug("{} is no absolute path".format(cmd_path))
|
||||
cmd_path = _lookup_path(cmd)
|
||||
|
@ -28,13 +28,27 @@ import html
|
||||
from PyQt5.QtWidgets import QStyle, QStyleOptionViewItem, QStyledItemDelegate
|
||||
from PyQt5.QtCore import QRectF, QSize, Qt
|
||||
from PyQt5.QtGui import (QIcon, QPalette, QTextDocument, QTextOption,
|
||||
QAbstractTextDocumentLayout)
|
||||
QAbstractTextDocumentLayout, QSyntaxHighlighter,
|
||||
QTextCharFormat)
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import qtutils, jinja
|
||||
from qutebrowser.utils import qtutils
|
||||
|
||||
|
||||
_cached_stylesheet = None
|
||||
class _Highlighter(QSyntaxHighlighter):
|
||||
|
||||
def __init__(self, doc, pattern, color):
|
||||
super().__init__(doc)
|
||||
self._format = QTextCharFormat()
|
||||
self._format.setForeground(color)
|
||||
self._pattern = pattern
|
||||
|
||||
def highlightBlock(self, text):
|
||||
"""Override highlightBlock for custom highlighting."""
|
||||
for match in re.finditer(self._pattern, text, re.IGNORECASE):
|
||||
start, end = match.span()
|
||||
length = end - start
|
||||
self.setFormat(start, length, self._format)
|
||||
|
||||
|
||||
class CompletionItemDelegate(QStyledItemDelegate):
|
||||
@ -194,21 +208,15 @@ class CompletionItemDelegate(QStyledItemDelegate):
|
||||
self._doc.setDefaultTextOption(text_option)
|
||||
self._doc.setDocumentMargin(2)
|
||||
|
||||
assert _cached_stylesheet is not None
|
||||
self._doc.setDefaultStyleSheet(_cached_stylesheet)
|
||||
|
||||
if index.parent().isValid():
|
||||
view = self.parent()
|
||||
pattern = view.pattern
|
||||
columns_to_filter = index.model().columns_to_filter(index)
|
||||
self._doc.setPlainText(self._opt.text)
|
||||
if index.column() in columns_to_filter and pattern:
|
||||
repl = r'<span class="highlight">\g<0></span>'
|
||||
pat = html.escape(re.escape(pattern)).replace(r'\ ', r'|')
|
||||
txt = html.escape(self._opt.text)
|
||||
text = re.sub(pat, repl, txt, flags=re.IGNORECASE)
|
||||
self._doc.setHtml(text)
|
||||
else:
|
||||
self._doc.setPlainText(self._opt.text)
|
||||
pat = re.escape(pattern).replace(r'\ ', r'|')
|
||||
_Highlighter(self._doc, pat,
|
||||
config.val.colors.completion.match.fg)
|
||||
else:
|
||||
self._doc.setHtml(
|
||||
'<span style="font: {};">{}</span>'.format(
|
||||
@ -283,24 +291,3 @@ class CompletionItemDelegate(QStyledItemDelegate):
|
||||
self._draw_focus_rect()
|
||||
|
||||
self._painter.restore()
|
||||
|
||||
|
||||
@config.change_filter('colors.completion.match.fg', function=True)
|
||||
def _update_stylesheet():
|
||||
"""Update the cached stylesheet."""
|
||||
stylesheet = """
|
||||
.highlight {
|
||||
color: {{ conf.colors.completion.match.fg }};
|
||||
}
|
||||
"""
|
||||
with jinja.environment.no_autoescape():
|
||||
template = jinja.environment.from_string(stylesheet)
|
||||
|
||||
global _cached_stylesheet
|
||||
_cached_stylesheet = template.render(conf=config.val)
|
||||
|
||||
|
||||
def init():
|
||||
"""Initialize the cached stylesheet."""
|
||||
_update_stylesheet()
|
||||
config.instance.changed.connect(_update_stylesheet)
|
||||
|
@ -27,12 +27,7 @@ from qutebrowser.keyinput import keyutils
|
||||
|
||||
def option(*, info):
|
||||
"""A CompletionModel filled with settings and their descriptions."""
|
||||
model = completionmodel.CompletionModel(column_widths=(20, 70, 10))
|
||||
options = ((opt.name, opt.description, info.config.get_str(opt.name))
|
||||
for opt in configdata.DATA.values()
|
||||
if not opt.no_autoconfig)
|
||||
model.add_category(listcategory.ListCategory("Options", options))
|
||||
return model
|
||||
return _option(info, "Options", lambda opt: not opt.no_autoconfig)
|
||||
|
||||
|
||||
def customized_option(*, info):
|
||||
@ -47,6 +42,37 @@ def customized_option(*, info):
|
||||
return model
|
||||
|
||||
|
||||
def list_option(*, info):
|
||||
"""A CompletionModel filled with settings whose values are lists."""
|
||||
predicate = lambda opt: (isinstance(info.config.get_obj(opt.name),
|
||||
list) and not opt.no_autoconfig)
|
||||
return _option(info, "List options", predicate)
|
||||
|
||||
|
||||
def dict_option(*, info):
|
||||
"""A CompletionModel filled with settings whose values are dicts."""
|
||||
predicate = lambda opt: (isinstance(info.config.get_obj(opt.name),
|
||||
dict) and not opt.no_autoconfig)
|
||||
return _option(info, "Dict options", predicate)
|
||||
|
||||
|
||||
def _option(info, title, predicate):
|
||||
"""A CompletionModel that is generated for several option sets.
|
||||
|
||||
Args:
|
||||
info: The config info that can be passed through.
|
||||
title: The title of the options.
|
||||
predicate: The function for filtering out the options. Takes a single
|
||||
argument.
|
||||
"""
|
||||
model = completionmodel.CompletionModel(column_widths=(20, 70, 10))
|
||||
options = ((opt.name, opt.description, info.config.get_str(opt.name))
|
||||
for opt in configdata.DATA.values()
|
||||
if predicate(opt))
|
||||
model.add_category(listcategory.ListCategory(title, options))
|
||||
return model
|
||||
|
||||
|
||||
def value(optname, *values, info):
|
||||
"""A CompletionModel filled with setting values.
|
||||
|
||||
|
@ -42,7 +42,7 @@ class HistoryCategory(QSqlQueryModel):
|
||||
|
||||
def _atime_expr(self):
|
||||
"""If max_items is set, return an expression to limit the query."""
|
||||
max_items = config.val.completion.web_history_max_items
|
||||
max_items = config.val.completion.web_history.max_items
|
||||
# HistoryCategory should not be added to the completion in that case.
|
||||
assert max_items != 0
|
||||
|
||||
@ -84,7 +84,7 @@ class HistoryCategory(QSqlQueryModel):
|
||||
timefmt = ("strftime('{}', last_atime, 'unixepoch', 'localtime')"
|
||||
.format(timestamp_format.replace("'", "`")))
|
||||
|
||||
if not self._query or len(words) != len(self._query.boundValues()):
|
||||
if not self._query or len(words) != len(self._query.bound_values()):
|
||||
# if the number of words changed, we need to generate a new query
|
||||
# otherwise, we can reuse the prepared query for performance
|
||||
self._query = sql.Query(' '.join([
|
||||
@ -100,14 +100,14 @@ class HistoryCategory(QSqlQueryModel):
|
||||
with debug.log_time('sql', 'Running completion query'):
|
||||
self._query.run(**{
|
||||
str(i): w for i, w in enumerate(words)})
|
||||
self.setQuery(self._query)
|
||||
self.setQuery(self._query.query)
|
||||
|
||||
def removeRows(self, row, _count, _parent=None):
|
||||
"""Override QAbstractItemModel::removeRows to re-run sql query."""
|
||||
# re-run query to reload updated table
|
||||
with debug.log_time('sql', 'Re-running completion query post-delete'):
|
||||
self._query.run()
|
||||
self.setQuery(self._query)
|
||||
self.setQuery(self._query.query)
|
||||
while self.rowCount() < row:
|
||||
self.fetchMore()
|
||||
return True
|
||||
|
@ -24,7 +24,7 @@ import re
|
||||
from PyQt5.QtCore import Qt, QSortFilterProxyModel, QRegExp
|
||||
from PyQt5.QtGui import QStandardItem, QStandardItemModel
|
||||
|
||||
from qutebrowser.utils import qtutils
|
||||
from qutebrowser.utils import qtutils, log
|
||||
|
||||
|
||||
class ListCategory(QSortFilterProxyModel):
|
||||
@ -80,6 +80,13 @@ class ListCategory(QSortFilterProxyModel):
|
||||
left = self.srcmodel.data(lindex)
|
||||
right = self.srcmodel.data(rindex)
|
||||
|
||||
if left is None or right is None: # pragma: no cover
|
||||
log.completion.warning("Got unexpected None value, "
|
||||
"left={!r} right={!r} "
|
||||
"lindex={!r} rindex={!r}"
|
||||
.format(left, right, lindex, rindex))
|
||||
return False
|
||||
|
||||
leftstart = left.startswith(self._pattern)
|
||||
rightstart = right.startswith(self._pattern)
|
||||
|
||||
|
@ -122,8 +122,8 @@ def _buffer(skip_win_id=None):
|
||||
tabs.append(("{}/{}".format(win_id, idx + 1),
|
||||
tab.url().toDisplayString(),
|
||||
tabbed_browser.widget.page_title(idx)))
|
||||
cat = listcategory.ListCategory("{}".format(win_id), tabs,
|
||||
delete_func=delete_buffer)
|
||||
cat = listcategory.ListCategory(
|
||||
str(win_id), tabs, delete_func=delete_buffer, sort=False)
|
||||
model.add_category(cat)
|
||||
|
||||
return model
|
||||
|
@ -22,6 +22,7 @@
|
||||
from qutebrowser.completion.models import (completionmodel, listcategory,
|
||||
histcategory)
|
||||
from qutebrowser.utils import log, objreg
|
||||
from qutebrowser.config import config
|
||||
|
||||
|
||||
_URLCOL = 0
|
||||
@ -50,25 +51,48 @@ def _delete_quickmark(data):
|
||||
|
||||
|
||||
def url(*, info):
|
||||
"""A model which combines bookmarks, quickmarks and web history URLs.
|
||||
"""A model which combines various URLs.
|
||||
|
||||
This combines:
|
||||
- bookmarks
|
||||
- quickmarks
|
||||
- search engines
|
||||
- web history URLs
|
||||
|
||||
Used for the `open` command.
|
||||
"""
|
||||
model = completionmodel.CompletionModel(column_widths=(40, 50, 10))
|
||||
|
||||
# pylint: disable=bad-config-option
|
||||
quickmarks = [(url, name) for (name, url)
|
||||
in objreg.get('quickmark-manager').marks.items()]
|
||||
bookmarks = objreg.get('bookmark-manager').marks.items()
|
||||
searchengines = [(k, v) for k, v
|
||||
in sorted(config.val.url.searchengines.items())
|
||||
if k != 'DEFAULT']
|
||||
# pylint: enable=bad-config-option
|
||||
categories = config.val.completion.open_categories
|
||||
models = {}
|
||||
|
||||
if quickmarks:
|
||||
model.add_category(listcategory.ListCategory(
|
||||
if searchengines and 'searchengines' in categories:
|
||||
models['searchengines'] = listcategory.ListCategory(
|
||||
'Search engines', searchengines, sort=False)
|
||||
|
||||
if quickmarks and 'quickmarks' in categories:
|
||||
models['quickmarks'] = listcategory.ListCategory(
|
||||
'Quickmarks', quickmarks, delete_func=_delete_quickmark,
|
||||
sort=False))
|
||||
if bookmarks:
|
||||
model.add_category(listcategory.ListCategory(
|
||||
'Bookmarks', bookmarks, delete_func=_delete_bookmark, sort=False))
|
||||
sort=False)
|
||||
if bookmarks and 'bookmarks' in categories:
|
||||
models['bookmarks'] = listcategory.ListCategory(
|
||||
'Bookmarks', bookmarks, delete_func=_delete_bookmark, sort=False)
|
||||
|
||||
if info.config.get('completion.web_history_max_items') != 0:
|
||||
history_disabled = info.config.get('completion.web_history.max_items') == 0
|
||||
if not history_disabled and 'history' in categories:
|
||||
hist_cat = histcategory.HistoryCategory(delete_func=_delete_history)
|
||||
model.add_category(hist_cat)
|
||||
models['history'] = hist_cat
|
||||
|
||||
for category in categories:
|
||||
if category in models:
|
||||
model.add_category(models[category])
|
||||
|
||||
return model
|
||||
|
@ -34,6 +34,7 @@ from qutebrowser.keyinput import keyutils
|
||||
val = None
|
||||
instance = None
|
||||
key_instance = None
|
||||
cache = None
|
||||
|
||||
# Keeping track of all change filters to validate them later.
|
||||
change_filters = []
|
||||
|
50
qutebrowser/config/configcache.py
Normal file
50
qutebrowser/config/configcache.py
Normal file
@ -0,0 +1,50 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2018 Jay Kamat <jaygkamat@gmail.com>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
# qutebrowser is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# qutebrowser is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
"""Implementation of a basic config cache."""
|
||||
|
||||
from qutebrowser.config import config
|
||||
|
||||
|
||||
class ConfigCache:
|
||||
|
||||
"""A 'high-performance' cache for the config system.
|
||||
|
||||
Useful for areas which call out to the config system very frequently, DO
|
||||
NOT modify the value returned, DO NOT require per-url settings, do not
|
||||
change frequently, and do not require partially 'expanded' config paths.
|
||||
|
||||
If any of these requirements are broken, you will get incorrect or slow
|
||||
behavior.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._cache = {}
|
||||
config.instance.changed.connect(self._on_config_changed)
|
||||
|
||||
def _on_config_changed(self, attr: str) -> None:
|
||||
if attr in self._cache:
|
||||
self._cache[attr] = config.instance.get(attr)
|
||||
|
||||
def __getitem__(self, attr: str):
|
||||
if attr not in self._cache:
|
||||
assert not config.instance.get_opt(attr).supports_pattern
|
||||
self._cache[attr] = config.instance.get(attr)
|
||||
return self._cache[attr]
|
@ -85,8 +85,8 @@ class ConfigCommands:
|
||||
*, pattern=None):
|
||||
"""Set an option.
|
||||
|
||||
If the option name ends with '?', the value of the option is shown
|
||||
instead.
|
||||
If the option name ends with '?' or no value is provided, the
|
||||
value of the option is shown instead.
|
||||
|
||||
Using :set without any arguments opens a page where settings can be
|
||||
changed interactively.
|
||||
@ -116,8 +116,7 @@ class ConfigCommands:
|
||||
|
||||
with self._handle_config_error():
|
||||
if value is None:
|
||||
raise cmdexc.CommandError("set: The following arguments "
|
||||
"are required: value")
|
||||
self._print_value(option, pattern=pattern)
|
||||
else:
|
||||
self._config.set_str(option, value, pattern=pattern,
|
||||
save_yaml=not temp)
|
||||
@ -246,11 +245,114 @@ class ConfigCommands:
|
||||
|
||||
Args:
|
||||
option: The name of the option.
|
||||
temp: Don't touch autoconfig.yml.
|
||||
temp: Set value temporarily until qutebrowser is closed.
|
||||
"""
|
||||
with self._handle_config_error():
|
||||
self._config.unset(option, save_yaml=not temp)
|
||||
|
||||
@cmdutils.register(instance='config-commands')
|
||||
@cmdutils.argument('option', completion=configmodel.list_option)
|
||||
def config_list_add(self, option, value, temp=False):
|
||||
"""Append a value to a config option that is a list.
|
||||
|
||||
Args:
|
||||
option: The name of the option.
|
||||
value: The value to append to the end of the list.
|
||||
temp: Add value temporarily until qutebrowser is closed.
|
||||
"""
|
||||
opt = self._config.get_opt(option)
|
||||
valid_list_types = (configtypes.List, configtypes.ListOrValue)
|
||||
if not isinstance(opt.typ, valid_list_types):
|
||||
raise cmdexc.CommandError(":config-list-add can only be used for "
|
||||
"lists")
|
||||
|
||||
with self._handle_config_error():
|
||||
option_value = self._config.get_mutable_obj(option)
|
||||
option_value.append(value)
|
||||
self._config.update_mutables(save_yaml=not temp)
|
||||
|
||||
@cmdutils.register(instance='config-commands')
|
||||
@cmdutils.argument('option', completion=configmodel.dict_option)
|
||||
def config_dict_add(self, option, key, value, temp=False, replace=False):
|
||||
"""Add a key/value pair to a dictionary option.
|
||||
|
||||
Args:
|
||||
option: The name of the option.
|
||||
key: The key to use.
|
||||
value: The value to place in the dictionary.
|
||||
temp: Add value temporarily until qutebrowser is closed.
|
||||
replace: Replace existing values. By default, existing values are
|
||||
not overwritten.
|
||||
"""
|
||||
opt = self._config.get_opt(option)
|
||||
if not isinstance(opt.typ, configtypes.Dict):
|
||||
raise cmdexc.CommandError(":config-dict-add can only be used for "
|
||||
"dicts")
|
||||
|
||||
with self._handle_config_error():
|
||||
option_value = self._config.get_mutable_obj(option)
|
||||
|
||||
if key in option_value and not replace:
|
||||
raise cmdexc.CommandError("{} already exists in {} - use "
|
||||
"--replace to overwrite!"
|
||||
.format(key, option))
|
||||
|
||||
option_value[key] = value
|
||||
self._config.update_mutables(save_yaml=not temp)
|
||||
|
||||
@cmdutils.register(instance='config-commands')
|
||||
@cmdutils.argument('option', completion=configmodel.list_option)
|
||||
def config_list_remove(self, option, value, temp=False):
|
||||
"""Remove a value from a list.
|
||||
|
||||
Args:
|
||||
option: The name of the option.
|
||||
value: The value to remove from the list.
|
||||
temp: Remove value temporarily until qutebrowser is closed.
|
||||
"""
|
||||
opt = self._config.get_opt(option)
|
||||
valid_list_types = (configtypes.List, configtypes.ListOrValue)
|
||||
if not isinstance(opt.typ, valid_list_types):
|
||||
raise cmdexc.CommandError(":config-list-remove can only be used "
|
||||
"for lists")
|
||||
|
||||
with self._handle_config_error():
|
||||
option_value = self._config.get_mutable_obj(option)
|
||||
|
||||
if value not in option_value:
|
||||
raise cmdexc.CommandError("{} is not in {}!".format(value,
|
||||
option))
|
||||
|
||||
option_value.remove(value)
|
||||
|
||||
self._config.update_mutables(save_yaml=not temp)
|
||||
|
||||
@cmdutils.register(instance='config-commands')
|
||||
@cmdutils.argument('option', completion=configmodel.dict_option)
|
||||
def config_dict_remove(self, option, key, temp=False):
|
||||
"""Remove a key from a dict.
|
||||
|
||||
Args:
|
||||
option: The name of the option.
|
||||
key: The key to remove from the dict.
|
||||
temp: Remove value temporarily until qutebrowser is closed.
|
||||
"""
|
||||
opt = self._config.get_opt(option)
|
||||
if not isinstance(opt.typ, configtypes.Dict):
|
||||
raise cmdexc.CommandError(":config-dict-remove can only be used "
|
||||
"for dicts")
|
||||
|
||||
with self._handle_config_error():
|
||||
option_value = self._config.get_mutable_obj(option)
|
||||
|
||||
if key not in option_value:
|
||||
raise cmdexc.CommandError("{} is not in {}!".format(key,
|
||||
option))
|
||||
|
||||
del option_value[key]
|
||||
|
||||
self._config.update_mutables(save_yaml=not temp)
|
||||
|
||||
@cmdutils.register(instance='config-commands')
|
||||
def config_clear(self, save=False):
|
||||
"""Set all settings back to their default.
|
||||
|
@ -56,7 +56,7 @@ class Option:
|
||||
@attr.s
|
||||
class Migrations:
|
||||
|
||||
"""Nigrated options in configdata.yml.
|
||||
"""Migrated options in configdata.yml.
|
||||
|
||||
Attributes:
|
||||
renamed: A dict mapping old option names to new names.
|
||||
|
@ -3,8 +3,10 @@
|
||||
aliases:
|
||||
default:
|
||||
w: session-save
|
||||
q: quit
|
||||
q: close
|
||||
qa: quit
|
||||
wq: quit --save
|
||||
wqa: quit --save
|
||||
type:
|
||||
name: Dict
|
||||
keytype:
|
||||
@ -181,6 +183,51 @@ qt.force_platform:
|
||||
This sets the `QT_QPA_PLATFORM` environment variable and is useful to force
|
||||
using the XCB plugin when running QtWebEngine on Wayland.
|
||||
|
||||
qt.process_model:
|
||||
type:
|
||||
name: String
|
||||
valid_values:
|
||||
- process-per-site-instance: Pages from separate sites are put into
|
||||
separate processes and separate visits to the same site are also
|
||||
isolated.
|
||||
- process-per-site: Pages from separate sites are put into separate
|
||||
processes. Unlike Process per Site Instance, all visits to the same
|
||||
site will share an OS process. The benefit of this model is reduced
|
||||
memory consumption, because more web pages will share processes.
|
||||
The drawbacks include reduced security, robustness, and
|
||||
responsiveness.
|
||||
- single-process: Run all tabs in a single process. This should be used
|
||||
for debugging purposes only, and it disables `:open --private`.
|
||||
default: process-per-site-instance
|
||||
backend: QtWebEngine
|
||||
restart: true
|
||||
desc: >-
|
||||
Which Chromium process model to use.
|
||||
|
||||
Alternative process models use less resources, but decrease security and
|
||||
robustness.
|
||||
|
||||
See the following pages for more details:
|
||||
|
||||
- https://www.chromium.org/developers/design-documents/process-models
|
||||
- https://doc.qt.io/qt-5/qtwebengine-features.html#process-models
|
||||
|
||||
qt.low_end_device_mode:
|
||||
type:
|
||||
name: String
|
||||
valid_values:
|
||||
- always: Always use low-end device mode.
|
||||
- auto: Decide automatically (uses low-end mode with < 1 GB available
|
||||
RAM).
|
||||
- never: Never use low-end device mode.
|
||||
default: auto
|
||||
backend: QtWebEngine
|
||||
restart: true
|
||||
desc: >-
|
||||
When to use Chromium's low-end device mode.
|
||||
|
||||
This improves the RAM usage of renderer processes, at the expense of
|
||||
performance.
|
||||
|
||||
qt.highdpi:
|
||||
type: Bool
|
||||
@ -220,10 +267,12 @@ content.autoplay:
|
||||
backend:
|
||||
QtWebEngine: Qt 5.10
|
||||
QtWebKit: false
|
||||
supports_pattern: true
|
||||
desc: >-
|
||||
Automatically start playing `<video>` elements.
|
||||
|
||||
Note this option needs a restart with QtWebEngine on Qt < 5.11.
|
||||
Note: On Qt < 5.11, this option needs a restart and does not support URL
|
||||
patterns.
|
||||
|
||||
content.cache.size:
|
||||
default: null
|
||||
@ -321,6 +370,7 @@ content.windowed_fullscreen:
|
||||
content.desktop_capture:
|
||||
type: BoolAsk
|
||||
default: ask
|
||||
supports_pattern: true
|
||||
desc: >-
|
||||
Allow websites to share screen content.
|
||||
|
||||
@ -350,14 +400,28 @@ content.frame_flattening:
|
||||
content.geolocation:
|
||||
default: ask
|
||||
type: BoolAsk
|
||||
supports_pattern: true
|
||||
desc: Allow websites to request geolocations.
|
||||
|
||||
content.mouse_lock:
|
||||
default: ask
|
||||
type: BoolAsk
|
||||
supports_pattern: true
|
||||
backend:
|
||||
QtWebKit: false
|
||||
QtWebEngine: Qt 5.8
|
||||
desc: Allow websites to lock your mouse pointer.
|
||||
|
||||
content.headers.accept_language:
|
||||
type:
|
||||
name: String
|
||||
none_ok: true
|
||||
supports_pattern: true
|
||||
default: en-US,en
|
||||
desc: Value to send in the `Accept-Language` header.
|
||||
desc: >-
|
||||
Value to send in the `Accept-Language` header.
|
||||
|
||||
Note that the value read from JavaScript is always the global value.
|
||||
|
||||
content.headers.custom:
|
||||
default: {}
|
||||
@ -370,6 +434,7 @@ content.headers.custom:
|
||||
name: String
|
||||
encoding: ascii
|
||||
none_ok: true
|
||||
supports_pattern: true
|
||||
desc: Custom headers for qutebrowser HTTP requests.
|
||||
|
||||
content.headers.do_not_track:
|
||||
@ -377,6 +442,7 @@ content.headers.do_not_track:
|
||||
name: Bool
|
||||
none_ok: true
|
||||
default: true
|
||||
supports_pattern: true
|
||||
desc: >-
|
||||
Value to send in the `DNT` header.
|
||||
|
||||
@ -392,14 +458,18 @@ content.headers.referer:
|
||||
- never: "Never send the Referer. This is not recommended, as some sites
|
||||
may break."
|
||||
- same-domain: "Only send the Referer for the same domain. This will
|
||||
still protect your privacy, but shouldn't break any sites."
|
||||
backend: QtWebKit
|
||||
still protect your privacy, but shouldn't break any sites. With
|
||||
QtWebEngine, the referer will still be sent for other domains, but
|
||||
with stripped path information."
|
||||
restart: true
|
||||
desc: >-
|
||||
When to send the Referer header.
|
||||
|
||||
The Referer header tells websites from which website you were coming from
|
||||
when visiting them.
|
||||
|
||||
No restart is needed with QtWebKit.
|
||||
|
||||
content.headers.user_agent:
|
||||
default: null
|
||||
type:
|
||||
@ -451,10 +521,15 @@ content.headers.user_agent:
|
||||
Gecko"
|
||||
- IE 11.0 for Desktop Win7 64-bit
|
||||
|
||||
desc: User agent to send. Unset to send the default.
|
||||
supports_pattern: true
|
||||
desc: >-
|
||||
User agent to send. Unset to send the default.
|
||||
|
||||
Note that the value read from JavaScript is always the global value.
|
||||
|
||||
content.host_blocking.enabled:
|
||||
default: true
|
||||
supports_pattern: true
|
||||
type: Bool
|
||||
desc: Enable host blocking.
|
||||
|
||||
@ -475,18 +550,26 @@ content.host_blocking.lists:
|
||||
- A zip-file of any of the above, with either only one file, or a file
|
||||
named `hosts` (with any extension).
|
||||
|
||||
It's also possible to add a local file or directory via a `file://` URL. In
|
||||
case of a directory, all files in the directory are read as adblock lists.
|
||||
|
||||
The file `~/.config/qutebrowser/blocked-hosts` is always read if it exists.
|
||||
|
||||
content.host_blocking.whitelist:
|
||||
default:
|
||||
- piwik.org
|
||||
type:
|
||||
name: List
|
||||
valtype: String
|
||||
valtype: UrlPattern
|
||||
none_ok: true
|
||||
desc: >-
|
||||
List of domains that should always be loaded, despite being ad-blocked.
|
||||
A list of patterns that should always be loaded, despite being ad-blocked.
|
||||
|
||||
Domains may contain * and ? wildcards and are otherwise required to exactly
|
||||
match the requested domain.
|
||||
Note this whitelists blocked hosts, not first-party URLs. As an example, if
|
||||
`example.org` loads an ad from `ads.example.org`, the whitelisted host
|
||||
should be `ads.example.org`. If you want to disable the adblocker on a
|
||||
given page, use the `content.host_blocking.enabled` setting with a URL
|
||||
pattern instead.
|
||||
|
||||
Local domains are always exempt from hostblocking.
|
||||
|
||||
@ -594,6 +677,7 @@ content.local_storage:
|
||||
content.media_capture:
|
||||
default: ask
|
||||
type: BoolAsk
|
||||
supports_pattern: true
|
||||
backend: QtWebEngine
|
||||
desc: Allow websites to record audio/video.
|
||||
|
||||
@ -610,13 +694,13 @@ content.netrc_file:
|
||||
content.notifications:
|
||||
default: ask
|
||||
type: BoolAsk
|
||||
supports_pattern: true
|
||||
backend: QtWebKit
|
||||
desc: Allow websites to show notifications.
|
||||
|
||||
content.pdfjs:
|
||||
default: false
|
||||
type: Bool
|
||||
backend: QtWebKit
|
||||
desc: >-
|
||||
Allow pdf.js to view PDF files in the browser.
|
||||
|
||||
@ -626,6 +710,7 @@ content.pdfjs:
|
||||
content.persistent_storage:
|
||||
default: ask
|
||||
type: BoolAsk
|
||||
supports_pattern: true
|
||||
backend:
|
||||
QtWebKit: false
|
||||
QtWebEngine: Qt 5.11
|
||||
@ -672,6 +757,7 @@ content.proxy_dns_requests:
|
||||
content.register_protocol_handler:
|
||||
default: ask
|
||||
type: BoolAsk
|
||||
supports_pattern: true
|
||||
backend:
|
||||
QtWebKit: false
|
||||
QtWebEngine: Qt 5.11
|
||||
@ -681,6 +767,7 @@ content.register_protocol_handler:
|
||||
content.ssl_strict:
|
||||
default: ask
|
||||
type: BoolAsk
|
||||
supports_pattern: true
|
||||
desc: Validate SSL handshakes.
|
||||
|
||||
content.user_stylesheets:
|
||||
@ -698,29 +785,52 @@ content.webgl:
|
||||
supports_pattern: true
|
||||
desc: Enable WebGL.
|
||||
|
||||
content.webrtc_public_interfaces_only:
|
||||
default: false
|
||||
type: Bool
|
||||
content.webrtc_ip_handling_policy:
|
||||
default: all-interfaces
|
||||
type:
|
||||
name: String
|
||||
valid_values:
|
||||
- all-interfaces: WebRTC has the right to enumerate all interfaces and
|
||||
bind them to discover public interfaces.
|
||||
- default-public-and-private-interfaces: WebRTC should only use the
|
||||
default route used by http. This also exposes the associated
|
||||
default private address. Default route is the route chosen by the
|
||||
OS on a multi-homed endpoint.
|
||||
- default-public-interface-only: WebRTC should only use the default route
|
||||
used by http. This doesn't expose any local addresses.
|
||||
- disable-non-proxied-udp: WebRTC should only use TCP to contact peers or
|
||||
servers unless the proxy server supports UDP. This doesn't expose
|
||||
any local addresses either.
|
||||
default: all-interfaces
|
||||
backend:
|
||||
QtWebKit: false
|
||||
QtWebEngine: Qt 5.9.2
|
||||
restart: true
|
||||
desc: >-
|
||||
Only expose public interfaces via WebRTC.
|
||||
Which interfaces to expose via WebRTC.
|
||||
|
||||
On Qt 5.9, this option requires a restart.
|
||||
On Qt 5.10, this option doesn't work at all because of a Qt bug.
|
||||
On Qt >= 5.11, no restart is required.
|
||||
On Qt 5.10, this option doesn't work because of a Qt bug.
|
||||
|
||||
content.xss_auditing:
|
||||
type: Bool
|
||||
default: false
|
||||
default: true
|
||||
supports_pattern: true
|
||||
desc: >-
|
||||
Monitor load requests for cross-site scripting attempts.
|
||||
|
||||
Suspicious scripts will be blocked and reported in the inspector's
|
||||
JavaScript console. Enabling this feature might have an impact on
|
||||
performance.
|
||||
JavaScript console.
|
||||
|
||||
content.mute:
|
||||
default: false
|
||||
type: Bool
|
||||
supports_pattern: true
|
||||
desc: >-
|
||||
Automatically mute tabs.
|
||||
|
||||
Note that if the `:tab-mute` command is used, the mute status for the
|
||||
affected tab is now controlled manually, and this setting doesn't have any
|
||||
effect.
|
||||
|
||||
# emacs: '
|
||||
|
||||
@ -788,7 +898,27 @@ completion.timestamp_format:
|
||||
default: '%Y-%m-%d'
|
||||
desc: Format of timestamps (e.g. for the history completion).
|
||||
|
||||
completion.web_history.exclude:
|
||||
type:
|
||||
name: List
|
||||
valtype: UrlPattern
|
||||
none_ok: true
|
||||
default: []
|
||||
restart: true
|
||||
desc: >-
|
||||
A list of patterns which should not be shown in the history.
|
||||
|
||||
This only affects the completion. Matching URLs are still saved in the
|
||||
history (and visible on the qute://history page), but hidden in the
|
||||
completion.
|
||||
|
||||
Changing this setting will cause the completion history to be regenerated
|
||||
on the next start, which will take a short while.
|
||||
|
||||
completion.web_history_max_items:
|
||||
renamed: completion.web_history.max_items
|
||||
|
||||
completion.web_history.max_items:
|
||||
default: -1
|
||||
type:
|
||||
name: Int
|
||||
@ -854,6 +984,18 @@ downloads.location.suggestion:
|
||||
- both: Show download path and filename.
|
||||
desc: What to display in the download filename input.
|
||||
|
||||
completion.open_categories:
|
||||
type:
|
||||
name: FlagList
|
||||
valid_values: [searchengines, quickmarks, bookmarks, history]
|
||||
none_ok: true
|
||||
default:
|
||||
- searchengines
|
||||
- quickmarks
|
||||
- bookmarks
|
||||
- history
|
||||
desc: Which categories to show (in which order) in the :open completion.
|
||||
|
||||
downloads.open_dispatcher:
|
||||
type:
|
||||
name: String
|
||||
@ -1021,6 +1163,71 @@ hints.scatter:
|
||||
|
||||
Ignored for number hints.
|
||||
|
||||
hints.selectors:
|
||||
no_autoconfig: true
|
||||
default:
|
||||
all:
|
||||
- 'a'
|
||||
- 'area'
|
||||
- 'textarea'
|
||||
- 'select'
|
||||
- 'input:not([type="hidden"])'
|
||||
- 'button'
|
||||
- 'frame'
|
||||
- 'iframe'
|
||||
- 'img'
|
||||
- 'link'
|
||||
- 'summary'
|
||||
- '[onclick]'
|
||||
- '[onmousedown]'
|
||||
- '[role="link"]'
|
||||
- '[role="option"]'
|
||||
- '[role="button"]'
|
||||
- '[ng-click]'
|
||||
- '[ngClick]'
|
||||
- '[data-ng-click]'
|
||||
- '[x-ng-click]'
|
||||
- '[tabindex]'
|
||||
links:
|
||||
- 'a[href]'
|
||||
- 'area[href]'
|
||||
- 'link[href]'
|
||||
- '[role="link"][href]'
|
||||
images:
|
||||
- 'img'
|
||||
media:
|
||||
- 'audio'
|
||||
- 'img'
|
||||
- 'video'
|
||||
url:
|
||||
- '[src]'
|
||||
- '[href]'
|
||||
inputs:
|
||||
- 'input[type="text"]'
|
||||
- 'input[type="date"]'
|
||||
- 'input[type="datetime-local"]'
|
||||
- 'input[type="email"]'
|
||||
- 'input[type="month"]'
|
||||
- 'input[type="number"]'
|
||||
- 'input[type="password"]'
|
||||
- 'input[type="search"]'
|
||||
- 'input[type="tel"]'
|
||||
- 'input[type="time"]'
|
||||
- 'input[type="url"]'
|
||||
- 'input[type="week"]'
|
||||
- 'input:not([type])'
|
||||
- 'textarea'
|
||||
type:
|
||||
name: Dict
|
||||
keytype: String
|
||||
valtype:
|
||||
name: List
|
||||
none_ok: true
|
||||
valtype: String
|
||||
supports_pattern: true
|
||||
desc: CSS selectors used to determine which elements on a page should have
|
||||
hints.
|
||||
|
||||
hints.uppercase:
|
||||
default: false
|
||||
type: Bool
|
||||
@ -1167,9 +1374,15 @@ prompt.radius:
|
||||
## scrolling
|
||||
|
||||
scrolling.bar:
|
||||
type: Bool
|
||||
default: false
|
||||
desc: Show a scrollbar.
|
||||
type:
|
||||
name: String
|
||||
valid_values:
|
||||
- always: Always show the scrollbar.
|
||||
- never: Never show the scrollbar.
|
||||
- when-searching: Show the scrollbar when searching for text in the
|
||||
webpage. With the QtWebKit backend, this is equal to `never`.
|
||||
default: when-searching
|
||||
desc: When to show the scrollbar.
|
||||
|
||||
scrolling.smooth:
|
||||
type: Bool
|
||||
@ -1349,12 +1562,27 @@ tabs.mousewheel_switching:
|
||||
tabs.new_position.related:
|
||||
default: next
|
||||
type: NewTabPosition
|
||||
desc: Position of new tabs opened from another tab.
|
||||
desc: >-
|
||||
Position of new tabs opened from another tab.
|
||||
|
||||
See `tabs.new_position.stacking` for controlling stacking behavior.
|
||||
|
||||
tabs.new_position.unrelated:
|
||||
default: last
|
||||
type: NewTabPosition
|
||||
desc: "Position of new tabs which aren't opened from another tab."
|
||||
desc: >-
|
||||
Position of new tabs which are not opened from another tab.
|
||||
|
||||
See `tabs.new_position.stacking` for controlling stacking behavior.
|
||||
|
||||
tabs.new_position.stacking:
|
||||
default: true
|
||||
type: Bool
|
||||
desc: >-
|
||||
Stack related tabs on top of each other when opened consecutively.
|
||||
|
||||
Only applies for `next` and `prev` values of `tabs.new_position.related`
|
||||
and `tabs.new_position.unrelated`.
|
||||
|
||||
tabs.padding:
|
||||
default:
|
||||
@ -1498,6 +1726,23 @@ tabs.min_width:
|
||||
|
||||
This setting does not apply to pinned tabs, unless `tabs.pinned.shrink` is False.
|
||||
|
||||
tabs.max_width:
|
||||
default: -1
|
||||
type:
|
||||
name: Int
|
||||
minval: -1
|
||||
maxval: maxint
|
||||
desc: >-
|
||||
Maximum width (in pixels) of tabs (-1 for no maximum).
|
||||
|
||||
This setting only applies when tabs are horizontal.
|
||||
|
||||
This setting does not apply to pinned tabs, unless `tabs.pinned.shrink` is
|
||||
False.
|
||||
|
||||
This setting may not apply properly if max_width is smaller than the
|
||||
minimum size of tab contents, or smaller than tabs.min_width.
|
||||
|
||||
tabs.width.indicator:
|
||||
renamed: tabs.indicator.width
|
||||
|
||||
@ -1753,7 +1998,7 @@ colors.completion.item.selected.border.bottom:
|
||||
|
||||
colors.completion.match.fg:
|
||||
default: '#ff4444'
|
||||
type: QssColor
|
||||
type: QtColor
|
||||
desc: Foreground color of the matched text in the completion.
|
||||
|
||||
colors.completion.scrollbar.fg:
|
||||
@ -2440,6 +2685,7 @@ bindings.default:
|
||||
.: repeat-command
|
||||
<Ctrl-p>: tab-pin
|
||||
<Alt-m>: tab-mute
|
||||
gD: tab-give
|
||||
q: record-macro
|
||||
"@": run-macro
|
||||
tsh: config-cycle -p -t -u *://{url:host}/* content.javascript.enabled ;; reload
|
||||
@ -2454,6 +2700,12 @@ bindings.default:
|
||||
tPH: config-cycle -p -u *://*.{url:host}/* content.plugins ;; reload
|
||||
tpu: config-cycle -p -t -u {url} content.plugins ;; reload
|
||||
tPu: config-cycle -p -u {url} content.plugins ;; reload
|
||||
tih: config-cycle -p -t -u *://{url:host}/* content.images ;; reload
|
||||
tIh: config-cycle -p -u *://{url:host}/* content.images ;; reload
|
||||
tiH: config-cycle -p -t -u *://*.{url:host}/* content.images ;; reload
|
||||
tIH: config-cycle -p -u *://*.{url:host}/* content.images ;; reload
|
||||
tiu: config-cycle -p -t -u {url} content.images ;; reload
|
||||
tIu: config-cycle -p -u {url} content.images ;; reload
|
||||
insert:
|
||||
<Ctrl-E>: open-editor
|
||||
<Shift-Ins>: insert-text {primary}
|
||||
@ -2465,7 +2717,7 @@ bindings.default:
|
||||
<Ctrl-B>: hint all tab-bg
|
||||
<Escape>: leave-mode
|
||||
passthrough:
|
||||
<Ctrl-V>: leave-mode
|
||||
<Shift-Escape>: leave-mode
|
||||
command:
|
||||
<Ctrl-P>: command-history-prev
|
||||
<Ctrl-N>: command-history-next
|
||||
@ -2499,6 +2751,7 @@ bindings.default:
|
||||
prompt:
|
||||
<Return>: prompt-accept
|
||||
<Ctrl-X>: prompt-open-download
|
||||
<Ctrl-P>: prompt-open-download --pdfjs
|
||||
<Shift-Tab>: prompt-item-focus prev
|
||||
<Up>: prompt-item-focus prev
|
||||
<Tab>: prompt-item-focus next
|
||||
|
@ -276,7 +276,24 @@ class YamlConfig(QObject):
|
||||
del settings['bindings.default']
|
||||
self._mark_changed()
|
||||
|
||||
# content.webrtc_public_interfaces_only got merged into
|
||||
# content.webrtc_ip_handling_policy.
|
||||
old = 'content.webrtc_public_interfaces_only'
|
||||
new = 'content.webrtc_ip_handling_policy'
|
||||
if old in settings:
|
||||
settings[new] = {}
|
||||
for scope, val in settings[old].items():
|
||||
if val:
|
||||
settings[new][scope] = 'default-public-interface-only'
|
||||
else:
|
||||
settings[new][scope] = 'all-interfaces'
|
||||
|
||||
del settings[old]
|
||||
self._mark_changed()
|
||||
|
||||
self._migrate_bool(settings, 'tabs.favicons.show', 'always', 'never')
|
||||
self._migrate_bool(settings, 'scrolling.bar',
|
||||
'when-searching', 'never')
|
||||
self._migrate_bool(settings, 'qt.force_software_rendering',
|
||||
'software-opengl', 'none')
|
||||
|
||||
|
@ -28,6 +28,7 @@ from qutebrowser.config import (config, configdata, configfiles, configtypes,
|
||||
configexc, configcommands)
|
||||
from qutebrowser.utils import (objreg, usertypes, log, standarddir, message,
|
||||
qtutils)
|
||||
from qutebrowser.config import configcache
|
||||
from qutebrowser.misc import msgbox, objects
|
||||
|
||||
|
||||
@ -44,6 +45,7 @@ def early_init(args):
|
||||
config.instance = config.Config(yaml_config=yaml_config)
|
||||
config.val = config.ConfigContainer(config.instance)
|
||||
config.key_instance = config.KeyConfig(config.instance)
|
||||
config.cache = configcache.ConfigCache()
|
||||
yaml_config.setParent(config.instance)
|
||||
|
||||
for cf in config.change_filters:
|
||||
@ -89,6 +91,8 @@ def _init_envvars():
|
||||
os.environ['QT_XCB_FORCE_SOFTWARE_OPENGL'] = '1'
|
||||
elif software_rendering == 'qt-quick':
|
||||
os.environ['QT_QUICK_BACKEND'] = 'software'
|
||||
elif software_rendering == 'chromium':
|
||||
os.environ['QT_WEBENGINE_DISABLE_NOUVEAU_WORKAROUND'] = '1'
|
||||
|
||||
if config.val.qt.force_platform is not None:
|
||||
os.environ['QT_QPA_PLATFORM'] = config.val.qt.force_platform
|
||||
@ -167,24 +171,67 @@ def qt_args(namespace):
|
||||
argv += ['--' + arg for arg in config.val.qt.args]
|
||||
|
||||
if objects.backend == usertypes.Backend.QtWebEngine:
|
||||
if not qtutils.version_check('5.11', compiled=False):
|
||||
# WORKAROUND equivalent to
|
||||
# https://codereview.qt-project.org/#/c/217932/
|
||||
# Needed for Qt < 5.9.5 and < 5.10.1
|
||||
argv.append('--disable-shared-workers')
|
||||
|
||||
if config.val.qt.force_software_rendering == 'chromium':
|
||||
argv.append('--disable-gpu')
|
||||
|
||||
if not config.val.content.canvas_reading:
|
||||
argv.append('--disable-reading-from-canvas')
|
||||
|
||||
if not qtutils.version_check('5.11'):
|
||||
# On Qt 5.11, we can control this via QWebEngineSettings
|
||||
if not config.val.content.autoplay:
|
||||
argv.append('--autoplay-policy=user-gesture-required')
|
||||
if config.val.content.webrtc_public_interfaces_only:
|
||||
argv.append('--force-webrtc-ip-handling-policy='
|
||||
'default_public_interface_only')
|
||||
argv += list(_qtwebengine_args())
|
||||
|
||||
return argv
|
||||
|
||||
|
||||
def _qtwebengine_args():
|
||||
"""Get the QtWebEngine arguments to use based on the config."""
|
||||
if not qtutils.version_check('5.11', compiled=False):
|
||||
# WORKAROUND equivalent to
|
||||
# https://codereview.qt-project.org/#/c/217932/
|
||||
# Needed for Qt < 5.9.5 and < 5.10.1
|
||||
yield '--disable-shared-workers'
|
||||
|
||||
settings = {
|
||||
'qt.force_software_rendering': {
|
||||
'software-opengl': None,
|
||||
'qt-quick': None,
|
||||
'chromium': '--disable-gpu',
|
||||
'none': None,
|
||||
},
|
||||
'content.canvas_reading': {
|
||||
True: None,
|
||||
False: '--disable-reading-from-canvas',
|
||||
},
|
||||
'content.webrtc_ip_handling_policy': {
|
||||
'all-interfaces': None,
|
||||
'default-public-and-private-interfaces':
|
||||
'--force-webrtc-ip-handling-policy='
|
||||
'default_public_and_private_interfaces',
|
||||
'default-public-interface-only':
|
||||
'--force-webrtc-ip-handling-policy='
|
||||
'default_public_interface_only',
|
||||
'disable-non-proxied-udp':
|
||||
'--force-webrtc-ip-handling-policy='
|
||||
'disable_non_proxied_udp',
|
||||
},
|
||||
'qt.process_model': {
|
||||
'process-per-site-instance': None,
|
||||
'process-per-site': '--process-per-site',
|
||||
'single-process': '--single-process',
|
||||
},
|
||||
'qt.low_end_device_mode': {
|
||||
'auto': None,
|
||||
'always': '--enable-low-end-device-mode',
|
||||
'never': '--disable-low-end-device-mode',
|
||||
},
|
||||
'content.headers.referer': {
|
||||
'always': None,
|
||||
'never': '--no-referrers',
|
||||
'same-domain': '--reduced-referrer-granularity',
|
||||
}
|
||||
}
|
||||
|
||||
if not qtutils.version_check('5.11'):
|
||||
# On Qt 5.11, we can control this via QWebEngineSettings
|
||||
settings['content.autoplay'] = {
|
||||
True: None,
|
||||
False: '--autoplay-policy=user-gesture-required',
|
||||
}
|
||||
|
||||
for setting, args in sorted(settings.items()):
|
||||
arg = args[config.instance.get(setting)]
|
||||
if arg is not None:
|
||||
yield arg
|
||||
|
@ -60,8 +60,8 @@ from PyQt5.QtGui import QColor, QFont
|
||||
from PyQt5.QtWidgets import QTabWidget, QTabBar
|
||||
|
||||
from qutebrowser.commands import cmdutils
|
||||
from qutebrowser.config import configexc
|
||||
from qutebrowser.utils import standarddir, utils, qtutils, urlutils
|
||||
from qutebrowser.config import configexc, configutils
|
||||
from qutebrowser.utils import standarddir, utils, qtutils, urlutils, urlmatch
|
||||
from qutebrowser.keyinput import keyutils
|
||||
|
||||
|
||||
@ -123,7 +123,7 @@ class BaseType:
|
||||
"""A type used for a setting value.
|
||||
|
||||
Attributes:
|
||||
none_ok: Whether to convert to None for an empty string.
|
||||
none_ok: Whether to allow None (or an empty string for :set) as value.
|
||||
|
||||
Class attributes:
|
||||
valid_values: Possible values if they can be expressed as a fixed
|
||||
@ -149,6 +149,9 @@ class BaseType:
|
||||
value: The value to check.
|
||||
pytype: A Python type to check the value against.
|
||||
"""
|
||||
if value is configutils.UNSET:
|
||||
return
|
||||
|
||||
if (value is None or (pytype == list and value == []) or
|
||||
(pytype == dict and value == {})):
|
||||
if not self.none_ok:
|
||||
@ -309,7 +312,9 @@ class MappingType(BaseType):
|
||||
|
||||
def to_py(self, value):
|
||||
self._basic_py_validation(value, str)
|
||||
if not value:
|
||||
if value is configutils.UNSET:
|
||||
return value
|
||||
elif not value:
|
||||
return None
|
||||
self._validate_valid_values(value.lower())
|
||||
return self.MAPPING[value.lower()]
|
||||
@ -367,7 +372,9 @@ class String(BaseType):
|
||||
|
||||
def to_py(self, value):
|
||||
self._basic_py_validation(value, str)
|
||||
if not value:
|
||||
if value is configutils.UNSET:
|
||||
return value
|
||||
elif not value:
|
||||
return None
|
||||
|
||||
self._validate_encoding(value)
|
||||
@ -399,7 +406,9 @@ class UniqueCharString(String):
|
||||
|
||||
def to_py(self, value):
|
||||
value = super().to_py(value)
|
||||
if not value:
|
||||
if value is configutils.UNSET:
|
||||
return value
|
||||
elif not value:
|
||||
return None
|
||||
|
||||
# Check for duplicate values
|
||||
@ -455,7 +464,9 @@ class List(BaseType):
|
||||
|
||||
def to_py(self, value):
|
||||
self._basic_py_validation(value, list)
|
||||
if not value:
|
||||
if value is configutils.UNSET:
|
||||
return value
|
||||
elif not value:
|
||||
return []
|
||||
|
||||
for val in value:
|
||||
@ -534,6 +545,9 @@ class ListOrValue(BaseType):
|
||||
return value
|
||||
|
||||
def to_py(self, value):
|
||||
if value is configutils.UNSET:
|
||||
return value
|
||||
|
||||
try:
|
||||
return [self.valtype.to_py(value)]
|
||||
except configexc.ValidationError:
|
||||
@ -577,7 +591,8 @@ class FlagList(List):
|
||||
|
||||
def to_py(self, value):
|
||||
vals = super().to_py(value)
|
||||
self._check_duplicates(vals)
|
||||
if vals is not configutils.UNSET:
|
||||
self._check_duplicates(vals)
|
||||
return vals
|
||||
|
||||
def complete(self):
|
||||
@ -764,7 +779,9 @@ class Perc(_Numeric):
|
||||
|
||||
def to_py(self, value):
|
||||
self._basic_py_validation(value, (float, int, str))
|
||||
if not value:
|
||||
if value is configutils.UNSET:
|
||||
return value
|
||||
elif not value:
|
||||
return None
|
||||
|
||||
if isinstance(value, str):
|
||||
@ -903,13 +920,49 @@ class QtColor(BaseType):
|
||||
* An SVG color name as specified in
|
||||
http://www.w3.org/TR/SVG/types.html#ColorKeywords[the W3C specification].
|
||||
* transparent (no color)
|
||||
* `rgb(r, g, b)` / `rgba(r, g, b, a)` (values 0-255 or percentages)
|
||||
* `hsv(h, s, v)` / `hsva(h, s, v, a)` (values 0-255, hue 0-359)
|
||||
"""
|
||||
|
||||
def _parse_value(self, val):
|
||||
try:
|
||||
return int(val)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
mult = 255.0
|
||||
if val.endswith('%'):
|
||||
val = val[:-1]
|
||||
mult = 255.0 / 100
|
||||
|
||||
try:
|
||||
return int(float(val) * mult)
|
||||
except ValueError:
|
||||
raise configexc.ValidationError(val, "must be a valid color value")
|
||||
|
||||
def to_py(self, value):
|
||||
self._basic_py_validation(value, str)
|
||||
if not value:
|
||||
if value is configutils.UNSET:
|
||||
return value
|
||||
elif not value:
|
||||
return None
|
||||
|
||||
if '(' in value and value.endswith(')'):
|
||||
openparen = value.index('(')
|
||||
kind = value[:openparen]
|
||||
vals = value[openparen+1:-1].split(',')
|
||||
vals = [self._parse_value(v) for v in vals]
|
||||
if kind == 'rgba' and len(vals) == 4:
|
||||
return QColor.fromRgb(*vals)
|
||||
elif kind == 'rgb' and len(vals) == 3:
|
||||
return QColor.fromRgb(*vals)
|
||||
elif kind == 'hsva' and len(vals) == 4:
|
||||
return QColor.fromHsv(*vals)
|
||||
elif kind == 'hsv' and len(vals) == 3:
|
||||
return QColor.fromHsv(*vals)
|
||||
else:
|
||||
raise configexc.ValidationError(value, "must be a valid color")
|
||||
|
||||
color = QColor(value)
|
||||
if color.isValid():
|
||||
return color
|
||||
@ -936,7 +989,9 @@ class QssColor(BaseType):
|
||||
|
||||
def to_py(self, value):
|
||||
self._basic_py_validation(value, str)
|
||||
if not value:
|
||||
if value is configutils.UNSET:
|
||||
return value
|
||||
elif not value:
|
||||
return None
|
||||
|
||||
functions = ['rgb', 'rgba', 'hsv', 'hsva', 'qlineargradient',
|
||||
@ -981,7 +1036,9 @@ class Font(BaseType):
|
||||
|
||||
def to_py(self, value):
|
||||
self._basic_py_validation(value, str)
|
||||
if not value:
|
||||
if value is configutils.UNSET:
|
||||
return value
|
||||
elif not value:
|
||||
return None
|
||||
|
||||
if not self.font_regex.fullmatch(value): # pragma: no cover
|
||||
@ -1000,7 +1057,9 @@ class FontFamily(Font):
|
||||
|
||||
def to_py(self, value):
|
||||
self._basic_py_validation(value, str)
|
||||
if not value:
|
||||
if value is configutils.UNSET:
|
||||
return value
|
||||
elif not value:
|
||||
return None
|
||||
|
||||
match = self.font_regex.fullmatch(value)
|
||||
@ -1024,7 +1083,9 @@ class QtFont(Font):
|
||||
|
||||
def to_py(self, value):
|
||||
self._basic_py_validation(value, str)
|
||||
if not value:
|
||||
if value is configutils.UNSET:
|
||||
return value
|
||||
elif not value:
|
||||
return None
|
||||
|
||||
style_map = {
|
||||
@ -1136,7 +1197,9 @@ class Regex(BaseType):
|
||||
def to_py(self, value):
|
||||
"""Get a compiled regex from either a string or a regex object."""
|
||||
self._basic_py_validation(value, (str, self._regex_type))
|
||||
if not value:
|
||||
if value is configutils.UNSET:
|
||||
return value
|
||||
elif not value:
|
||||
return None
|
||||
elif isinstance(value, str):
|
||||
return self._compile_regex(value)
|
||||
@ -1214,7 +1277,9 @@ class Dict(BaseType):
|
||||
|
||||
def to_py(self, value):
|
||||
self._basic_py_validation(value, dict)
|
||||
if not value:
|
||||
if value is configutils.UNSET:
|
||||
return value
|
||||
elif not value:
|
||||
return self._fill_fixed_keys({})
|
||||
|
||||
self._validate_keys(value)
|
||||
@ -1230,7 +1295,7 @@ class Dict(BaseType):
|
||||
if not value:
|
||||
# An empty Dict is treated just like None -> empty string
|
||||
return ''
|
||||
return json.dumps(value)
|
||||
return json.dumps(value, sort_keys=True)
|
||||
|
||||
def to_doc(self, value, indent=0):
|
||||
if not value:
|
||||
@ -1256,7 +1321,9 @@ class File(BaseType):
|
||||
|
||||
def to_py(self, value):
|
||||
self._basic_py_validation(value, str)
|
||||
if not value:
|
||||
if value is configutils.UNSET:
|
||||
return value
|
||||
elif not value:
|
||||
return None
|
||||
|
||||
value = os.path.expanduser(value)
|
||||
@ -1282,7 +1349,9 @@ class Directory(BaseType):
|
||||
|
||||
def to_py(self, value):
|
||||
self._basic_py_validation(value, str)
|
||||
if not value:
|
||||
if value is configutils.UNSET:
|
||||
return value
|
||||
elif not value:
|
||||
return None
|
||||
value = os.path.expandvars(value)
|
||||
value = os.path.expanduser(value)
|
||||
@ -1309,7 +1378,9 @@ class FormatString(BaseType):
|
||||
|
||||
def to_py(self, value):
|
||||
self._basic_py_validation(value, str)
|
||||
if not value:
|
||||
if value is configutils.UNSET:
|
||||
return value
|
||||
elif not value:
|
||||
return None
|
||||
|
||||
try:
|
||||
@ -1341,8 +1412,10 @@ class ShellCommand(List):
|
||||
|
||||
def to_py(self, value):
|
||||
value = super().to_py(value)
|
||||
if not value:
|
||||
if value is configutils.UNSET:
|
||||
return value
|
||||
elif not value:
|
||||
return []
|
||||
|
||||
if (self.placeholder and
|
||||
'{}' not in ' '.join(value) and
|
||||
@ -1365,7 +1438,9 @@ class Proxy(BaseType):
|
||||
|
||||
def to_py(self, value):
|
||||
self._basic_py_validation(value, str)
|
||||
if not value:
|
||||
if value is configutils.UNSET:
|
||||
return value
|
||||
elif not value:
|
||||
return None
|
||||
|
||||
try:
|
||||
@ -1401,7 +1476,9 @@ class SearchEngineUrl(BaseType):
|
||||
|
||||
def to_py(self, value):
|
||||
self._basic_py_validation(value, str)
|
||||
if not value:
|
||||
if value is configutils.UNSET:
|
||||
return value
|
||||
elif not value:
|
||||
return None
|
||||
|
||||
if not ('{}' in value or '{0}' in value):
|
||||
@ -1429,7 +1506,9 @@ class FuzzyUrl(BaseType):
|
||||
|
||||
def to_py(self, value):
|
||||
self._basic_py_validation(value, str)
|
||||
if not value:
|
||||
if value is configutils.UNSET:
|
||||
return value
|
||||
elif not value:
|
||||
return None
|
||||
|
||||
try:
|
||||
@ -1463,6 +1542,9 @@ class Padding(Dict):
|
||||
|
||||
def to_py(self, value):
|
||||
d = super().to_py(value)
|
||||
if d is configutils.UNSET:
|
||||
return d
|
||||
|
||||
return PaddingValues(**d)
|
||||
|
||||
|
||||
@ -1472,7 +1554,9 @@ class Encoding(BaseType):
|
||||
|
||||
def to_py(self, value):
|
||||
self._basic_py_validation(value, str)
|
||||
if not value:
|
||||
if value is configutils.UNSET:
|
||||
return value
|
||||
elif not value:
|
||||
return None
|
||||
try:
|
||||
codecs.lookup(value)
|
||||
@ -1529,7 +1613,9 @@ class Url(BaseType):
|
||||
|
||||
def to_py(self, value):
|
||||
self._basic_py_validation(value, str)
|
||||
if not value:
|
||||
if value is configutils.UNSET:
|
||||
return value
|
||||
elif not value:
|
||||
return None
|
||||
|
||||
qurl = QUrl.fromUserInput(value)
|
||||
@ -1545,7 +1631,9 @@ class SessionName(BaseType):
|
||||
|
||||
def to_py(self, value):
|
||||
self._basic_py_validation(value, str)
|
||||
if not value:
|
||||
if value is configutils.UNSET:
|
||||
return value
|
||||
elif not value:
|
||||
return None
|
||||
if value.startswith('_'):
|
||||
raise configexc.ValidationError(value, "may not start with '_'!")
|
||||
@ -1593,8 +1681,10 @@ class ConfirmQuit(FlagList):
|
||||
|
||||
def to_py(self, value):
|
||||
values = super().to_py(value)
|
||||
if not values:
|
||||
if values is configutils.UNSET:
|
||||
return values
|
||||
elif not values:
|
||||
return []
|
||||
|
||||
# Never can't be set with other options
|
||||
if 'never' in values and len(values) > 1:
|
||||
@ -1630,7 +1720,9 @@ class TimestampTemplate(BaseType):
|
||||
|
||||
def to_py(self, value):
|
||||
self._basic_py_validation(value, str)
|
||||
if not value:
|
||||
if value is configutils.UNSET:
|
||||
return value
|
||||
elif not value:
|
||||
return None
|
||||
|
||||
try:
|
||||
@ -1654,10 +1746,33 @@ class Key(BaseType):
|
||||
|
||||
def to_py(self, value):
|
||||
self._basic_py_validation(value, str)
|
||||
if not value:
|
||||
if value is configutils.UNSET:
|
||||
return value
|
||||
elif not value:
|
||||
return None
|
||||
|
||||
try:
|
||||
return keyutils.KeySequence.parse(value)
|
||||
except keyutils.KeyParseError as e:
|
||||
raise configexc.ValidationError(value, str(e))
|
||||
|
||||
|
||||
class UrlPattern(BaseType):
|
||||
|
||||
"""A match pattern for a URL.
|
||||
|
||||
See https://developer.chrome.com/apps/match_patterns for the allowed
|
||||
syntax.
|
||||
"""
|
||||
|
||||
def to_py(self, value):
|
||||
self._basic_py_validation(value, str)
|
||||
if value is configutils.UNSET:
|
||||
return value
|
||||
elif not value:
|
||||
return None
|
||||
|
||||
try:
|
||||
return urlmatch.UrlPattern(value)
|
||||
except urlmatch.ParseError as e:
|
||||
raise configexc.ValidationError(value, str(e))
|
||||
|
@ -111,7 +111,7 @@ li {
|
||||
<li>
|
||||
You can manually download the pdf.js archive
|
||||
<a href="https://mozilla.github.io/pdf.js/getting_started/#download">here</a>
|
||||
and extract it to <code>~/.local/share/qutebrowser/pdfjs</code>
|
||||
and extract it to <code>{{ pdfjs_dir }}</code>
|
||||
<br>
|
||||
<span class="warning">Warning:</span> Using this method you are
|
||||
responsible for yourself to keep the installation updated! If a
|
||||
|
@ -3,7 +3,8 @@
|
||||
{% block script %}
|
||||
var cset = function(option, value) {
|
||||
// FIXME:conf we might want some error handling here?
|
||||
var url = "qute://settings/set?option=" + encodeURIComponent(option);
|
||||
var url = "qute://user:{{csrf_token}}@settings/set"
|
||||
url += "?option=" + encodeURIComponent(option);
|
||||
url += "&value=" + encodeURIComponent(value);
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open("GET", url);
|
||||
@ -33,7 +34,7 @@ input { width: 98%; }
|
||||
<th>Setting</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
{% for option in configdata.DATA.values() if not option.no_autoconfig %}
|
||||
{% for option in configdata.DATA.values()|sort(attribute='name') if not option.no_autoconfig %}
|
||||
<tr>
|
||||
<!-- FIXME: convert to string properly -->
|
||||
<td class="setting">{{ option.name }} (Current: {{ confget(option.name) | string |truncate(100) }})
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user