Merge remote-tracking branch 'upstream/master' into perdomainstylesheets

This commit is contained in:
Jay Kamat 2018-10-14 15:26:09 -07:00
commit b4dd94b6e9
No known key found for this signature in database
GPG Key ID: 5D2E399600F4F7B5
228 changed files with 7244 additions and 2994 deletions

View File

@ -5,15 +5,15 @@ cache:
build: off build: off
environment: environment:
PYTHONUNBUFFERED: 1 PYTHONUNBUFFERED: 1
PYTHON: C:\Python36\python.exe PYTHON: C:\Python36-x64\python.exe
matrix: matrix:
- TESTENV: py36-pyqt510 - TESTENV: py36-pyqt511
- TESTENV: pylint - TESTENV: pylint
install: install:
- '%PYTHON% -m pip install -U pip' - '%PYTHON% -m pip install -U pip'
- '%PYTHON% -m pip install -r misc\requirements\requirements-tox.txt' - '%PYTHON% -m pip install -r misc\requirements\requirements-tox.txt'
- 'set PATH=%PATH%;C:\Python36' - 'set PATH=C:\Python36-x64;%PATH'
test_script: test_script:
- '%PYTHON% -m tox -e %TESTENV%' - '%PYTHON% -m tox -e %TESTENV%'

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
doc/changelog.asciidoc merge=union

2
.github/CODEOWNERS vendored
View File

@ -1,5 +1,5 @@
qutebrowser/browser/history.py @rcorre qutebrowser/browser/history.py @rcorre
qutebrowser/completion/* @rcorre qutebrowser/completion/** @rcorre
qutebrowser/misc/sql.py @rcorre qutebrowser/misc/sql.py @rcorre
tests/end2end/features/completion.feature @rcorre tests/end2end/features/completion.feature @rcorre
tests/end2end/features/test_completion_bdd.py @rcorre tests/end2end/features/test_completion_bdd.py @rcorre

View File

@ -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 - 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. 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, - If you are stuck somewhere or have questions,
https://github.com/qutebrowser/qutebrowser#getting-help[please ask]! https://github.com/qutebrowser/qutebrowser#getting-help[please ask]!
See the full contribution documentation for details and other useful hints: See the link:../doc/contributing.asciidoc[full contribution documentation] for
details and other useful hints.
include::../doc/contributing.asciidoc[]

View File

@ -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
View 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). -->

View File

@ -0,0 +1,5 @@
---
name: 🚀 Feature Request
about: Ideas for new features and improvements
---

View 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
-->

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

1
.gitignore vendored
View File

@ -16,7 +16,6 @@ __pycache__
/doc/*.html /doc/*.html
/README.html /README.html
/qutebrowser/html/doc/ /qutebrowser/html/doc/
/qutebrowser/html/*.html
/.venv* /.venv*
/.coverage /.coverage
/htmlcov /htmlcov

View File

@ -54,7 +54,7 @@ no-docstring-rgx=(^_|^main$)
[FORMAT] [FORMAT]
max-line-length=79 max-line-length=79
ignore-long-lines=(<?https?://|^# Copyright 201\d) ignore-long-lines=(<?https?://|^# Copyright 201\d|link:)
expected-line-ending-format=LF expected-line-ending-format=LF
[VARIABLES] [VARIABLES]

View File

@ -20,18 +20,21 @@ matrix:
- os: linux - os: linux
env: TESTENV=py36-pyqt59 env: TESTENV=py36-pyqt59
- os: linux - os: linux
env: TESTENV=py36-pyqt510-cov env: TESTENV=py36-pyqt510
# We need a newer Xvfb as a WORKAROUND for:
# https://bugreports.qt.io/browse/QTBUG-64928
sudo: required
addons: addons:
apt: apt:
sources: packages:
- sourceline: "deb http://us.archive.ubuntu.com/ubuntu/ xenial main universe" - xfonts-base
packages: - os: linux
- xvfb 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 - os: osx
env: TESTENV=py36 OSX=sierra env: TESTENV=py37 OSX=sierra
osx_image: xcode9.2 osx_image: xcode9.2
language: generic language: generic
# https://github.com/qutebrowser/qutebrowser/issues/2013 # https://github.com/qutebrowser/qutebrowser/issues/2013
@ -66,6 +69,10 @@ matrix:
env: TESTENV=shellcheck env: TESTENV=shellcheck
services: docker services: docker
fast_finish: true fast_finish: true
allow_failures:
# https://github.com/qutebrowser/qutebrowser/issues/4055
- os: linux
env: TESTENV=py36-pyqt510
cache: cache:
directories: directories:

View File

@ -59,7 +59,7 @@ Getting help
You can get help in the IRC channel You can get help in the IRC channel
irc://irc.freenode.org/#qutebrowser[`#qutebrowser`] on 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 (https://webchat.freenode.net/?channels=#qutebrowser[webchat]), or by writing a
message to the message to the
https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser[mailinglist] at 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: The following software and libraries are required to run qutebrowser:
* http://www.python.org/[Python] 3.5 or newer (3.6 recommended) * https://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.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 - QtCore / qtbase
- QtQuick (part of qtbase in some distributions) - QtQuick (part of qtbase in some distributions)
- QtSQL (part of qtbase in some distributions) - QtSQL (part of qtbase in some distributions)
- QtOpenGL - QtOpenGL
- QtWebEngine, or - QtWebEngine, or
- QtWebKit - only the - alternatively QtWebKit - support for QtWebKit will be dropped soon, and
link:https://github.com/annulen/webkit/wiki[updated fork] (5.212) is only the link:https://github.com/annulen/webkit/wiki[updated fork] (5.212)
supported is supported
* http://www.riverbankcomputing.com/software/pyqt/intro[PyQt] 5.7.0 or newer * https://www.riverbankcomputing.com/software/pyqt/intro[PyQt] 5.7.0 or newer
(5.10 recommended) for Python 3 (5.11 recommended, support for < 5.9 will be dropped soon) for Python 3
* https://pypi.python.org/pypi/setuptools/[pkg_resources/setuptools] * 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://jinja.pocoo.org/[jinja2]
* http://pygments.org/[pygments] * http://pygments.org/[pygments]
* https://github.com/yaml/pyyaml[PyYAML] * https://github.com/yaml/pyyaml[PyYAML]
* http://www.attrs.org/[attrs] * https://www.attrs.org/[attrs]
The following libraries are optional: The following libraries are optional:
@ -143,6 +144,18 @@ get in touch!
* PayPal: me@the-compiler.org * PayPal: me@the-compiler.org
* Bitcoin: link:bitcoin:1PMzbcetAHfpxoXww8Bj5XqquHtVvMjJtE[1PMzbcetAHfpxoXww8Bj5XqquHtVvMjJtE] * 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 Authors
------- -------
@ -152,7 +165,7 @@ https://github.com/qutebrowser/qutebrowser/graphs/contributors[hundreds of contr
Additionally, the following people have contributed graphics: 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) * WOFall (original icon)
* regines (key binding cheatsheet) * regines (key binding cheatsheet)
@ -170,18 +183,16 @@ Active
* https://fanglingsu.github.io/vimb/[vimb] (C, GTK+ with WebKit2) * https://fanglingsu.github.io/vimb/[vimb] (C, GTK+ with WebKit2)
* https://luakit.github.io/luakit/[luakit] (C/Lua, GTK+ with WebKit2) * https://luakit.github.io/luakit/[luakit] (C/Lua, GTK+ with WebKit2)
* http://surf.suckless.org/[surf] (C, GTK+ with WebKit1/WebKit2) * https://surf.suckless.org/[surf] (C, GTK+ with WebKit1/WebKit2)
* http://www.uzbl.org/[uzbl] (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: * Chrome/Chromium addons:
https://github.com/1995eaton/chromium-vim[cVim], https://vimium.github.io/[Vimium],
http://vimium.github.io/[Vimium],
https://github.com/brookhong/Surfingkeys[Surfingkeys], https://github.com/brookhong/Surfingkeys[Surfingkeys],
https://key.saka.io/[Saka Key]
* Firefox addons (based on WebExtensions): * Firefox addons (based on WebExtensions):
https://addons.mozilla.org/en-GB/firefox/addon/vimium-ff/[Vimium-FF] (experimental), 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/ueokande/vim-vixen[Vim Vixen],
https://github.com/shinglyu/QuantumVim[QuantumVim], https://github.com/amedama41/vvimpulation[VVimpulation],
https://github.com/cmcaine/tridactyl[Tridactyl] (working https://github.com/cmcaine/tridactyl[Tridactyl] (working
on a https://bugzilla.mozilla.org/show_bug.cgi?id=1215061[better API] for on a https://bugzilla.mozilla.org/show_bug.cgi?id=1215061[better API] for
keyboard integration in Firefox). keyboard integration in Firefox).
@ -192,17 +203,23 @@ Inactive
* https://bitbucket.org/portix/dwb[dwb] (C, GTK+ with WebKit1, * 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] - https://bitbucket.org/portix/dwb/pull-requests/22/several-cleanups-to-increase-portability/diff[unmaintained] -
main inspiration for qutebrowser) 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) 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) * 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): * Firefox addons (not based on WebExtensions or no recent activity):
http://www.vimperator.org/[Vimperator], http://www.vimperator.org/[Vimperator],
http://5digits.org/pentadactyl/[Pentadactyl], http://bug.5digits.org/pentadactyl/index[Pentadactyl],
https://github.com/akhodakivskiy/VimFx[VimFx], https://github.com/akhodakivskiy/VimFx[VimFx],
https://key.saka.io[Saka Key],
https://github.com/shinglyu/QuantumVim[QuantumVim],
* Chrome/Chromium addons: * Chrome/Chromium addons:
https://chrome.google.com/webstore/detail/vichrome/gghkfhpblkcmlkmpcpgaajbbiikbhpdi?hl=en[ViChrome], https://chrome.google.com/webstore/detail/vichrome/gghkfhpblkcmlkmpcpgaajbbiikbhpdi?hl=en[ViChrome],
https://github.com/jinzhu/vrome[Vrome] https://github.com/jinzhu/vrome[Vrome]
https://key.saka.io[Saka Key],
https://github.com/1995eaton/chromium-vim[cVim],
License 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 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 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 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].

View File

@ -15,12 +15,219 @@ breaking changes (such as renamed commands) can happen in minor releases.
// `Fixed` for any bug fixes. // `Fixed` for any bug fixes.
// `Security` to invite users to upgrade in case of vulnerabilities. // `Security` to invite users to upgrade in case of vulnerabilities.
v1.4.0 (unreleased) v1.6.0 (unreleased)
------------------- -------------------
Added 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 - New `--debug-flag log-requests` to log requests to the debug log for
debugging. debugging.
- New `--first` flag for `:hint` (bound to `gi` for inputs) which automatically - New `--first` flag for `:hint` (bound to `gi` for inputs) which automatically
@ -40,11 +247,14 @@ Added
* Support for requesting persistent storage via * Support for requesting persistent storage via
`navigator.webkitPersistentStorage.requestQuota` with a new `navigator.webkitPersistentStorage.requestQuota` with a new
`content.persistent_storage` setting (requires Qt 5.11). `content.persistent_storage` setting (requires Qt 5.11).
This setting also supports URL patterns.
* Support for registering custom protocol handlers via * Support for registering custom protocol handlers via
`navigator.registerProtocolHandler` with a new `navigator.registerProtocolHandler` with a new
`content.register_protocol_handler` setting (requires Qt 5.11). `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` * Support for WebRTC screen sharing with a new `content.desktop_capture`
setting (requires Qt 5.10). setting (requires Qt 5.10).
This setting also supports URL patterns.
* New `content.autoplay` setting to enable/disable automatic video playback * New `content.autoplay` setting to enable/disable automatic video playback
(requires Qt 5.10). (requires Qt 5.10).
* New `content.webrtc_public_interfaces_only` setting to only expose public * New `content.webrtc_public_interfaces_only` setting to only expose public
@ -55,8 +265,17 @@ Added
Changed 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 - 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 - New short flags for commandline arguments: `-B` and `-T` for `--basedir` and
`--temp-basedir`; `-d` and `-D` for `--debug` and `--debug-flag`. `--temp-basedir`; `-d` and `-D` for `--debug` and `--debug-flag`.
- Deleting history items via `:history-clear` or `:completion-item-del` now - 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. - Improved error messages when a setting needs a newer Qt version.
- QtWebEngine: Various improvements to make the cursor more visible in caret - QtWebEngine: Various improvements to make the cursor more visible in caret
browsing. 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 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. - `:tab-detach` which has been deprecated in v1.1.0 has been removed.
- The `content.developer_extras` setting got removed. On QtWebKit, developer - The `content.developer_extras` setting got removed. On QtWebKit, developer
extras are now automatically enabled when opening the inspector. 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 Fixed
~~~~~ ~~~~~
- Crash in a workaround for a Qt 5.11 bug in rare circumstances. - 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 v1.3.2
------ ------
@ -1479,7 +1737,7 @@ Changed
`tabs.bg/fg.selected.odd/even`. `tabs.bg/fg.selected.odd/even`.
- `:spawn --userscript` and `:hint` with the `userscript` target now look up - `:spawn --userscript` and `:hint` with the `userscript` target now look up
relative paths in `~/.local/share/qutebrowser/userscripts` or 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`. `--userscript`.
- New design for error pages - New design for error pages
- Link filtering for hints now checks if the text is contained anywhere in - Link filtering for hints now checks if the text is contained anywhere in

View File

@ -5,6 +5,14 @@ The Compiler <mail@qutebrowser.org>
:data-uri: :data-uri:
:toc: :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 `&lt;3` footnote:[Of course, that says `<3` in HTML.] contributors! I `&lt;3` footnote:[Of course, that says `<3` in HTML.] contributors!
This document contains guidelines for contributing to qutebrowser, as well as This document contains guidelines for contributing to qutebrowser, as well as
@ -88,7 +96,7 @@ git format-patch origin/master <1>
Running qutebrowser 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 `.venv/bin/qutebrowser --debug --temp-basedir` to test your changes with debug
logging enabled and without affecting existing running instances. logging enabled and without affecting existing running instances.
@ -689,8 +697,6 @@ New PyQt release
~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~
* See above. * 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. * Update `tox.ini`/`.travis.yml`/`.appveyor.yml` to test new versions.
qutebrowser release qutebrowser release
@ -712,7 +718,7 @@ qutebrowser release
as closed. as closed.
* Linux: Run `git checkout v1.$x.$y && ./.venv/bin/python3 scripts/dev/build_release.py --upload v1.$x.$y`. * 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). * macOS: Run `git checkout v1.X.Y && python3 scripts/dev/build_release.py --upload v1.X.Y` (replace X/Y by hand).
* On server: * On server:
- Run `python3 scripts/dev/download_release.py v1.X.Y` (replace X/Y by hand). - Run `python3 scripts/dev/download_release.py v1.X.Y` (replace X/Y by hand).

View File

@ -5,10 +5,10 @@ The Compiler <mail@qutebrowser.org>
[qanda] [qanda]
What is qutebrowser based on?:: What is qutebrowser based on?::
qutebrowser uses http://www.python.org/[Python], http://qt.io/[Qt] and qutebrowser uses https://www.python.org/[Python], https://www.qt.io/[Qt] and
http://www.riverbankcomputing.com/software/pyqt/intro[PyQt]. 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 and http://www.vimperator.org/vimperator[Vimperator]. Many actions and
key bindings are similar to dwb. 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 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 happy with, so I started to write my own. Also, I needed a project to get
into writing GUI applications with Python and 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 Read the next few questions to find out why I was unhappy with existing
software. 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)?:: 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 http://webkitgtk.org/[WebKitGTK+] Most of them are based on the https://webkitgtk.org/[WebKitGTK+]
http://webkitgtk.org/reference/webkitgtk/stable/index.html[WebKit1] API, https://webkitgtk.org/reference/webkitgtk/stable/index.html[WebKit1] API,
which causes a lot of crashes. As the GTK API using WebKit1 is 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], https://lists.webkit.org/pipermail/webkit-gtk/2014-March/001821.html[deprecated],
these bugs are never going to be fixed. these bugs are never going to be fixed.
+ +
When qutebrowser was created, the newer 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 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 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 still only a few projects which have some kind of WebKit2 support (see the
https://github.com/qutebrowser/qutebrowser#similar-projects[list of https://github.com/qutebrowser/qutebrowser#similar-projects[list of
alternatives]). alternatives]).
+ +
qutebrowser uses http://qt.io/[Qt] and qutebrowser uses https://www.qt.io/[Qt] and
http://wiki.qt.io/QtWebEngine[QtWebEngine] by default (and supports https://wiki.qt.io/QtWebEngine[QtWebEngine] by default (and supports
http://wiki.qt.io/QtWebKit[QtWebKit] optionally). QtWebEngine is based on 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 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 much more man-power behind it than WebKitGTK+ has, and thus supports more modern
web features - it's also arguably more secure. 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 Firefox likes to break compatibility with addons on each upgrade, gets
slower and more bloated with every upgrade, and has some 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 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. 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 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 writers, which results in Vimium not really having all the features you'd
expect from a proper minimal, vim-like browser. expect from a proper minimal, vim-like browser.
Why Python?:: Why Python?::
I enjoy writing Python since 2011, which made it one of the possible I enjoy writing Python since 2011, which made it one of the possible
choices. I wanted to use http://qt.io/[Qt] because of choices. I wanted to use https://www.qt.io/[Qt] because of
http://wiki.qt.io/QtWebKit[QtWebKit] so I didn't have https://wiki.qt.io/QtWebKit[QtWebKit] so I didn't have
http://wiki.qt.io/Category:LanguageBindings[many other choices]. I don't 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. like C++ and can't write it very well, so that wasn't an alternative.
But isn't Python too slow for a browser?:: 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 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 while running. Also, most of the heavy lifting of qutebrowser is done by Qt
and WebKit in C++, with the and WebKit in C++, with the
@ -74,7 +74,7 @@ Is qutebrowser secure?::
Most security issues are in the backend (which handles networking, Most security issues are in the backend (which handles networking,
rendering, JavaScript, etc.) and not qutebrowser itself. 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 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 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 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] https://chromium.googlesource.com/chromium/src/+/master/docs/design/sandbox.md[sandboxing]
features are also enabled as a second line of defense. 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 backend, but hasn't seen new releases
https://github.com/annulen/webkit/releases[in a while]. It also doesn't have any 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, Security issues in qutebrowser's code happen very rarely (as per July 2018,
there has been one security issue caused by qutebrowser in over four years) and there have been three security issues caused by qutebrowser in over 4.5 years).
are fixed timely. To report security bugs, please contact me directly at Those were handled appropriately
mail@qutebrowser.org, GPG ID (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]. https://www.the-compiler.org/pubkey.asc[0x916eb0c8fd55a072].
Is there an adblocker?:: Is there an adblocker?::
There is a host-based adblocker which takes /etc/hosts-like lists. A "real" There is a host-based adblocker which takes /etc/hosts-like lists. A "real"
adblocker has a 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 impact] on browsing speed and
https://blog.mozilla.org/nnethercote/2014/05/14/adblock-pluss-effect-on-firefoxs-memory-usage/[RAM 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 usage], so implementing support for AdBlockPlus-like lists is currently not
a priority. 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?:: How do I play Youtube videos with mpv?::
You can easily add a key binding to play youtube videos inside a real video You can easily add a key binding to play youtube videos inside a real video
player - optionally even with hinting for links: player - optionally even with hinting for links:
@ -132,24 +147,25 @@ It also works nicely with rapid hints:
---- ----
How do I use qutebrowser with mutt?:: How do I use qutebrowser with mutt?::
Due to a Qt limitation, local files without `.html` extensions are For security reasons, local files without `.html` extensions aren't
"downloaded" instead of displayed, see rendered as HTML, see
https://github.com/qutebrowser/qutebrowser/issues/566[#566]. You can work https://bugs.chromium.org/p/chromium/issues/detail?id=777737[this Chromium issue]
around this by using this in your `mailcap`: 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?:: What is the difference between bookmarks and quickmarks?::
Bookmarks will always use the title of the website as their name, but with quickmarks Bookmarks will always use the title of the website as their name, but with quickmarks
you can set your own title. you can set your own title.
+ +
For example, if you bookmark multiple food recipe websites and use `:open`, For example, if you bookmark multiple food recipe websites and use `:open`,
you have to type the title or address of the website. you have to type the title or address of the website.
+ +
When using quickmark, you can give them all names, like When using quickmark, you can give them all names, like
`foodrecipes1`, `foodrecipes2` and so on. When you type `foodrecipes1`, `foodrecipes2` and so on. When you type
`:open foodrecipes`, you will see a list of all the food recipe sites, `:open foodrecipes`, you will see a list of all the food recipe sites,
without having to remember the exact website title or address. without having to remember the exact website title or address.
@ -243,7 +259,7 @@ Note that there are some missing features which you may run into:
See https://wiki.greasespot.net/Metadata_Block[the wiki] for more info. See https://wiki.greasespot.net/Metadata_Block[the wiki] for more info.
. Any greasemonkey API function to do with adding UI elements is not currently . Any greasemonkey API function to do with adding UI elements is not currently
supported. That means context menu extentensions and background pages. supported. That means context menu extentensions and background pages.
== Troubleshooting == Troubleshooting
Unable to view flash content.:: Unable to view flash content.::
@ -251,7 +267,7 @@ Unable to view flash content.::
to use the flash plugin. Using the command `:set content.plugins true` to use the flash plugin. Using the command `:set content.plugins true`
in qutebrowser will enable plugins. Packages for flash should in qutebrowser will enable plugins. Packages for flash should
be provided for your platform or it can be obtained from 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.:: Experiencing freezing on sites like duckduckgo and youtube.::
This issue could be caused by stale plugin files installed by `mozplugger` This issue could be caused by stale plugin files installed by `mozplugger`
@ -264,12 +280,12 @@ When using QtWebEngine, qutebrowser reports "Render Process Crashed" and the con
As stated in https://gcc.gnu.org/gcc-6/changes.html[GCC's Website] GCC 6 has introduced some optimizations that could break non-conforming codebases, like QtWebEngine. + As stated in https://gcc.gnu.org/gcc-6/changes.html[GCC's Website] GCC 6 has introduced some optimizations that could break non-conforming codebases, like QtWebEngine. +
As a workaround, you can disable the nullpointer check optimization by adding the -fno-delete-null-pointer-checks flag while compiling. + As a workaround, you can disable the nullpointer check optimization by adding the -fno-delete-null-pointer-checks flag while compiling. +
On gentoo, you just need to add it into your make.conf, like this: + On gentoo, you just need to add it into your make.conf, like this: +
CFLAGS="... -fno-delete-null-pointer-checks" CFLAGS="... -fno-delete-null-pointer-checks"
CXXFLAGS="... -fno-delete-null-pointer-checks" CXXFLAGS="... -fno-delete-null-pointer-checks"
+ +
And then re-emerging qtwebengine with: + And then re-emerging qtwebengine with: +
emerge -1 qtwebengine emerge -1 qtwebengine
Unable to view DRM content (Netflix, Spotify, etc.).:: Unable to view DRM content (Netflix, Spotify, etc.).::
@ -295,10 +311,19 @@ Lastly, set your `qt.args` to point to that directory and restart qutebrowser:
:restart :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.:: My issue is not listed.::
If you experience any segfaults or crashes, you can report the issue in If you experience any segfaults or crashes, you can report the issue in
https://github.com/qutebrowser/qutebrowser/issues[the issue tracker] or https://github.com/qutebrowser/qutebrowser/issues[the issue tracker] or
using the `:report` command. using the `:report` command.
If you are reporting a segfault, make sure you read the 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. information.

View File

@ -38,7 +38,11 @@ It is possible to run or bind multiple commands by separating them with `;;`.
|<<close,close>>|Close the current window. |<<close,close>>|Close the current window.
|<<config-clear,config-clear>>|Set all settings back to their default. |<<config-clear,config-clear>>|Set all settings back to their default.
|<<config-cycle,config-cycle>>|Cycle an option between multiple values. |<<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-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-source,config-source>>|Read a config.py file.
|<<config-unset,config-unset>>|Unset an option. |<<config-unset,config-unset>>|Unset an option.
|<<config-write-py,config-write-py>>|Write the current configuration to a config.py file. |<<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. * +*-t*+, +*--temp*+: Set value temporarily until qutebrowser is closed.
* +*-p*+, +*--print*+: Print the value after setting. * +*-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]]
=== config-edit === config-edit
Syntax: +:config-edit [*--no-source*]+ Syntax: +:config-edit [*--no-source*]+
@ -301,6 +334,32 @@ Open the config.py file in the editor.
==== optional arguments ==== optional arguments
* +*-n*+, +*--no-source*+: Don't re-source the config file after editing. * +*-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]]
=== config-source === config-source
Syntax: +:config-source [*--clear*] ['filename']+ 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. * +'option'+: The name of the option.
==== optional arguments ==== optional arguments
* +*-t*+, +*--temp*+: Don't touch autoconfig.yml. * +*-t*+, +*--temp*+: Set value temporarily until qutebrowser is closed.
[[config-write-py]] [[config-write-py]]
=== config-write-py === config-write-py
@ -545,6 +604,10 @@ Start hinting.
- `inputs`: Only input fields. - `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. * +'target'+: What to do with the selected element.
@ -576,7 +639,7 @@ Start hinting.
- With `userscript`: The userscript to execute. Either store - With `userscript`: The userscript to execute. Either store
the userscript in the userscript in
`~/.local/share/qutebrowser/userscripts` `~/.local/share/qutebrowser/userscripts`
(or `$XDG_DATA_DIR`), or use an absolute (or `$XDG_DATA_HOME`), or use an absolute
path. path.
- With `fill`: The command to fill the statusbar with. - With `fill`: The command to fill the statusbar with.
`{hint-url}` will get replaced by the selected `{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. - `up`: Go up a level in the current URL.
- `increment`: Increment the last number in the URL. - `increment`: Increment the last number in the URL.
Uses the Uses the
link:settings.html#url.incdec_segments[url.incdec_segments] link:settings{outsuffix}#url.incdec_segments[url.incdec_segments]
config option. config option.
- `decrement`: Decrement the last number in the URL. - `decrement`: Decrement the last number in the URL.
Uses the Uses the
link:settings.html#url.incdec_segments[url.incdec_segments] link:settings{outsuffix}#url.incdec_segments[url.incdec_segments]
config option. config option.
@ -918,6 +981,9 @@ Repeat a given command.
* +'times'+: How many times to repeat. * +'times'+: How many times to repeat.
* +'command'+: The command to run, with optional args. * +'command'+: The command to run, with optional args.
==== count
Multiplies with 'times' when given.
==== note ==== note
* This command does not split arguments after the last argument and handles quotes literally. * 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. * 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. 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 ==== positional arguments
* +'option'+: The name of the option. * +'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 * +*-u*+, +*--userscript*+: Run the command as a userscript. You can use an absolute path, or store the userscript in one of those
locations: locations:
- `~/.local/share/qutebrowser/userscripts` - `~/.local/share/qutebrowser/userscripts`
(or `$XDG_DATA_DIR`) (or `$XDG_DATA_HOME`)
- `/usr/share/qutebrowser/userscripts` - `/usr/share/qutebrowser/userscripts`
* +*-v*+, +*--verbose*+: Show notifications when the command started/exited. * +*-v*+, +*--verbose*+: Show notifications when the command started/exited.
* +*-o*+, +*--output*+: Whether the output should be shown in a new tab. * +*-o*+, +*--output*+: Whether the output should be shown in a new tab.
* +*-d*+, +*--detach*+: Whether the command should be detached from qutebrowser. * +*-d*+, +*--detach*+: Whether the command should be detached from qutebrowser.
==== count
Given to userscripts as $QUTE_COUNT.
==== note ==== note
* This command does not split arguments after the last argument and handles quotes literally. * 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]]
=== 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. 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 ==== positional arguments
* +'win-id'+: The window ID of the window to give the current tab to. * +'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 ==== count
Overrides win_id (index starts at 1 for win_id=0). 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]]
=== tab-take === tab-take
Syntax: +:tab-take 'index'+ Syntax: +:tab-take [*--keep*] 'index'+
Take a tab from another window. 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. * +'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]]
=== unbind === unbind
Syntax: +:unbind [*--mode* 'mode'] 'key'+ Syntax: +:unbind [*--mode* 'mode'] 'key'+
@ -1382,7 +1460,7 @@ Close all windows except for the current one.
[[yank]] [[yank]]
=== yank === yank
Syntax: +:yank [*--sel*] [*--keep*] ['what']+ Syntax: +:yank [*--sel*] [*--keep*] [*--quiet*] ['what']+
Yank something to the clipboard or primary selection. Yank something to the clipboard or primary selection.
@ -1401,10 +1479,11 @@ Yank something to the clipboard or primary selection.
==== optional arguments ==== optional arguments
* +*-s*+, +*--sel*+: Use the primary selection instead of the clipboard. * +*-s*+, +*--sel*+: Use the primary selection instead of the clipboard.
* +*-k*+, +*--keep*+: Stay in visual mode after yanking the selection. * +*-k*+, +*--keep*+: Stay in visual mode after yanking the selection.
* +*-q*+, +*--quiet*+: Don't show an information message.
[[zoom]] [[zoom]]
=== zoom === zoom
Syntax: +:zoom ['zoom']+ Syntax: +:zoom [*--quiet*] ['zoom']+
Set the zoom level for the current tab. 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 ==== positional arguments
* +'zoom'+: The zoom percentage to set. * +'zoom'+: The zoom percentage to set.
==== optional arguments
* +*-q*+, +*--quiet*+: Don't show a zoom level message.
==== count ==== count
The zoom percentage to set. The zoom percentage to set.
[[zoom-in]] [[zoom-in]]
=== zoom-in === zoom-in
Syntax: +:zoom-in [*--quiet*]+
Increase the zoom level for the current tab. Increase the zoom level for the current tab.
==== optional arguments
* +*-q*+, +*--quiet*+: Don't show a zoom level message.
==== count ==== count
How many steps to zoom in. How many steps to zoom in.
[[zoom-out]] [[zoom-out]]
=== zoom-out === zoom-out
Syntax: +:zoom-out [*--quiet*]+
Decrease the zoom level for the current tab. Decrease the zoom level for the current tab.
==== optional arguments
* +*-q*+, +*--quiet*+: Don't show a zoom level message.
==== count ==== count
How many steps to zoom out. 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]]
=== prompt-open-download === prompt-open-download
Syntax: +:prompt-open-download ['cmdline']+ Syntax: +:prompt-open-download [*--pdfjs*] ['cmdline']+
Immediately open a download. Immediately open a download.
@ -1669,6 +1761,9 @@ If no specific command is given, this will use the system's default application
cmdline. cmdline.
==== optional arguments
* +*-p*+, +*--pdfjs*+: Open the download via PDF.js.
==== note ==== note
* This command does not split arguments after the last argument and handles quotes literally. * This command does not split arguments after the last argument and handles quotes literally.

View File

@ -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 page by running `:set` without any arguments, where all settings are listed and
customizable. 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`. can quickly set settings interactively, for example `:set tabs.position left`.
Some settings are also customizable for a given 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 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 To bind and unbind keys, you can use the link:commands{outfilesuffix}#bind[`:bind`] and
link:commands.html#unbind[`:unbind`] commands: link:commands{outfilesuffix}#unbind[`:unbind`] commands:
- Binding the key chain `,v` to the `:spawn mpv {url}` command: - Binding the key chain `,v` to the `:spawn mpv {url}` command:
`:bind ,v spawn mpv {url}` `:bind ,v spawn mpv {url}`
@ -67,9 +67,9 @@ See the help pages linked above (or `:help :bind`, `:help :unbind`) for more
information. information.
Other useful commands for config manipulation are Other useful commands for config manipulation are
link:commands.html#config-unset[`:config-unset`] to reset a value to its default, link:commands{outfilesuffix}#config-unset[`:config-unset`] to reset a value to its default,
link:commands.html#config-clear[`:config-clear`] to reset the entire configuration, link:commands{outfilesuffix}#config-clear[`:config-clear`] to reset the entire configuration,
and link:commands.html#config-cycle[`:config-cycle`] to cycle a setting between and link:commands{outfilesuffix}#config-cycle[`:config-cycle`] to cycle a setting between
different values. different values.
[[configpy]] [[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"`, mistyped config settings. As an example, if you do `c.tabs.possition = "left"`,
you'll get an error when starting. 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: accepted values depend on the type of the option. Commonly used are:
- Strings: `c.tabs.position = "left"` - 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. valid and normalizes different expressions which map to the same key.
For details on how to specify keys and the available modes, see the 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. setting.
To bind a key: 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]. - 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] - 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 Avoiding flake8 errors
^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^
@ -452,7 +453,7 @@ or always navigate through command history with
:bind -m command <Down> command-history-next :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 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 sqlite-based completion is much faster. If the `:open` completion is too slow
on your machine, set an appropriate limit again. on your machine, set an appropriate limit again.

View File

@ -6,14 +6,14 @@ Documentation
The following help pages are currently available: The following help pages are currently available:
* link:../quickstart.html[Quick start guide] * link:../quickstart{outfilesuffix}[Quick start guide]
* link:../faq.html[Frequently asked questions] * link:../faq{outfilesuffix}[Frequently asked questions]
* link:../changelog.html[Change Log] * link:../changelog{outfilesuffix}[Change Log]
* link:commands.html[Documentation of commands] * link:commands{outfilesuffix}[Documentation of commands]
* link:configuring.html[Configuring qutebrowser] * link:configuring{outfilesuffix}[Configuring qutebrowser]
* link:settings.html[Documentation of settings] * link:settings{outfilesuffix}[Documentation of settings]
* link:../userscripts.html[How to write userscripts] * link:../userscripts{outfilesuffix}[How to write userscripts]
* link:../contributing.html[Contributing to qutebrowser] * link:../contributing{outfilesuffix}[Contributing to qutebrowser]
Getting help Getting help
------------ ------------

View File

@ -100,6 +100,7 @@
|<<completion.delay,completion.delay>>|Delay (in milliseconds) before updating completions after typing a character. |<<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.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.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.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.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. |<<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.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.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.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. |<<confirm_quit,confirm_quit>>|Require a confirmation before quitting the application.
|<<content.autoplay,content.autoplay>>|Automatically start playing `<video>` elements. |<<content.autoplay,content.autoplay>>|Automatically start playing `<video>` elements.
|<<content.cache.appcache,content.cache.appcache>>|Enable support for the HTML 5 web application cache feature. |<<content.cache.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.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.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.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.hyperlink_auditing,content.hyperlink_auditing>>|Enable hyperlink auditing (`<a ping>`).
|<<content.images,content.images>>|Load images automatically in web pages. |<<content.images,content.images>>|Load images automatically in web pages.
|<<content.javascript.alert,content.javascript.alert>>|Show javascript alerts. |<<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_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.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.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.netrc_file,content.netrc_file>>|Netrc-file for HTTP authentication.
|<<content.notifications,content.notifications>>|Allow websites to show notifications. |<<content.notifications,content.notifications>>|Allow websites to show notifications.
|<<content.pdfjs,content.pdfjs>>|Allow pdf.js to view PDF files in the browser. |<<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.ssl_strict,content.ssl_strict>>|Validate SSL handshakes.
|<<content.user_stylesheets,content.user_stylesheets>>|List of user stylesheet filenames to use. |<<content.user_stylesheets,content.user_stylesheets>>|List of user stylesheet filenames to use.
|<<content.webgl,content.webgl>>|Enable WebGL. |<<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.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. |<<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. |<<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.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.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.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. |<<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. |<<history_gap_interval,history_gap_interval>>|Maximum time (in minutes) between two history items for them to be considered being from the same browsing session.
|<<input.escape_quits_reporter,input.escape_quits_reporter>>|Allow Escape to quit the crash reporter. |<<input.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_platform,qt.force_platform>>|Force a Qt platform to use.
|<<qt.force_software_rendering,qt.force_software_rendering>>|Force software rendering for QtWebEngine. |<<qt.force_software_rendering,qt.force_software_rendering>>|Force software rendering for QtWebEngine.
|<<qt.highdpi,qt.highdpi>>|Turn on Qt HighDPI scaling. |<<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. |<<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.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. |<<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.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.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.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.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.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.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.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.padding,tabs.padding>>|Padding (in pixels) around text for tabs.
|<<tabs.pinned.shrink,tabs.pinned.shrink>>|Shrink pinned tabs down to their contents. |<<tabs.pinned.shrink,tabs.pinned.shrink>>|Shrink pinned tabs down to their contents.
|<<tabs.position,tabs.position>>|Position of the tab bar. |<<tabs.position,tabs.position>>|Position of the tab bar.
@ -287,9 +296,11 @@ Type: <<types,Dict>>
Default: Default:
- +pass:[q]+: +pass:[quit]+ - +pass:[q]+: +pass:[close]+
- +pass:[qa]+: +pass:[quit]+
- +pass:[w]+: +pass:[session-save]+ - +pass:[w]+: +pass:[session-save]+
- +pass:[wq]+: +pass:[quit --save]+ - +pass:[wq]+: +pass:[quit --save]+
- +pass:[wqa]+: +pass:[quit --save]+
[[auto_save.interval]] [[auto_save.interval]]
=== auto_save.interval === auto_save.interval
@ -562,6 +573,7 @@ Default:
* +pass:[g0]+: +pass:[tab-focus 1]+ * +pass:[g0]+: +pass:[tab-focus 1]+
* +pass:[gB]+: +pass:[set-cmd-text -s :bookmark-load -t]+ * +pass:[gB]+: +pass:[set-cmd-text -s :bookmark-load -t]+
* +pass:[gC]+: +pass:[tab-clone]+ * +pass:[gC]+: +pass:[tab-clone]+
* +pass:[gD]+: +pass:[tab-give]+
* +pass:[gO]+: +pass:[set-cmd-text :open -t -r {url:pretty}]+ * +pass:[gO]+: +pass:[set-cmd-text :open -t -r {url:pretty}]+
* +pass:[gU]+: +pass:[navigate up -t]+ * +pass:[gU]+: +pass:[navigate up -t]+
* +pass:[g^]+: +pass:[tab-focus 1]+ * +pass:[g^]+: +pass:[tab-focus 1]+
@ -593,6 +605,9 @@ Default:
* +pass:[sk]+: +pass:[set-cmd-text -s :bind]+ * +pass:[sk]+: +pass:[set-cmd-text -s :bind]+
* +pass:[sl]+: +pass:[set-cmd-text -s :set -t]+ * +pass:[sl]+: +pass:[set-cmd-text -s :set -t]+
* +pass:[ss]+: +pass:[set-cmd-text -s :set]+ * +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:[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]+ * +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:[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:[tSu]+: +pass:[config-cycle -p -u {url} content.javascript.enabled ;; reload]+
* +pass:[th]+: +pass:[back -t]+ * +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:[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]+
* +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:[}}]+: +pass:[navigate next -t]+
- +pass:[passthrough]+: - +pass:[passthrough]+:
* +pass:[&lt;Ctrl-V&gt;]+: +pass:[leave-mode]+ * +pass:[&lt;Shift-Escape&gt;]+: +pass:[leave-mode]+
- +pass:[prompt]+: - +pass:[prompt]+:
* +pass:[&lt;Alt-B&gt;]+: +pass:[rl-backward-word]+ * +pass:[&lt;Alt-B&gt;]+: +pass:[rl-backward-word]+
@ -649,6 +667,7 @@ Default:
* +pass:[&lt;Ctrl-F&gt;]+: +pass:[rl-forward-char]+ * +pass:[&lt;Ctrl-F&gt;]+: +pass:[rl-forward-char]+
* +pass:[&lt;Ctrl-H&gt;]+: +pass:[rl-backward-delete-char]+ * +pass:[&lt;Ctrl-H&gt;]+: +pass:[rl-backward-delete-char]+
* +pass:[&lt;Ctrl-K&gt;]+: +pass:[rl-kill-line]+ * +pass:[&lt;Ctrl-K&gt;]+: +pass:[rl-kill-line]+
* +pass:[&lt;Ctrl-P&gt;]+: +pass:[prompt-open-download --pdfjs]+
* +pass:[&lt;Ctrl-U&gt;]+: +pass:[rl-unix-line-discard]+ * +pass:[&lt;Ctrl-U&gt;]+: +pass:[rl-unix-line-discard]+
* +pass:[&lt;Ctrl-W&gt;]+: +pass:[rl-unix-word-rubout]+ * +pass:[&lt;Ctrl-W&gt;]+: +pass:[rl-unix-word-rubout]+
* +pass:[&lt;Ctrl-X&gt;]+: +pass:[prompt-open-download]+ * +pass:[&lt;Ctrl-X&gt;]+: +pass:[prompt-open-download]+
@ -780,7 +799,7 @@ Default: +pass:[black]+
=== colors.completion.match.fg === colors.completion.match.fg
Foreground color of the matched text in the completion. Foreground color of the matched text in the completion.
Type: <<types,QssColor>> Type: <<types,QtColor>>
Default: +pass:[#ff4444]+ Default: +pass:[#ff4444]+
@ -1383,6 +1402,26 @@ Type: <<types,Int>>
Default: +pass:[1]+ 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]]
=== completion.quick === completion.quick
Move on to the next part when there's only one possible completion left. Move on to the next part when there's only one possible completion left.
@ -1445,8 +1484,19 @@ Type: <<types,Bool>>
Default: +pass:[false]+ Default: +pass:[false]+
[[completion.web_history_max_items]] [[completion.web_history.exclude]]
=== completion.web_history_max_items === 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. Number of URLs to show in the web history.
0: no history / -1: unlimited 0: no history / -1: unlimited
@ -1474,7 +1524,9 @@ Default:
[[content.autoplay]] [[content.autoplay]]
=== content.autoplay === content.autoplay
Automatically start playing `<video>` elements. 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>> Type: <<types,Bool>>
@ -1570,6 +1622,8 @@ Default: +pass:[iso-8859-1]+
Allow websites to share screen content. Allow websites to share screen content.
On Qt < 5.10, a dialog box is always displayed, even if this is set to "true". 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>> Type: <<types,BoolAsk>>
Valid values: Valid values:
@ -1609,6 +1663,8 @@ This setting is only available with the QtWebKit backend.
=== content.geolocation === content.geolocation
Allow websites to request geolocations. Allow websites to request geolocations.
This setting supports URL patterns.
Type: <<types,BoolAsk>> Type: <<types,BoolAsk>>
Valid values: Valid values:
@ -1622,6 +1678,9 @@ Default: +pass:[ask]+
[[content.headers.accept_language]] [[content.headers.accept_language]]
=== content.headers.accept_language === content.headers.accept_language
Value to send in the `Accept-Language` header. 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>> Type: <<types,String>>
@ -1631,6 +1690,8 @@ Default: +pass:[en-US,en]+
=== content.headers.custom === content.headers.custom
Custom headers for qutebrowser HTTP requests. Custom headers for qutebrowser HTTP requests.
This setting supports URL patterns.
Type: <<types,Dict>> Type: <<types,Dict>>
Default: empty Default: empty
@ -1640,6 +1701,8 @@ Default: empty
Value to send in the `DNT` header. 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. 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>> Type: <<types,Bool>>
Default: +pass:[true]+ Default: +pass:[true]+
@ -1648,6 +1711,8 @@ Default: +pass:[true]+
=== content.headers.referer === content.headers.referer
When to send the Referer header. When to send the Referer header.
The Referer header tells websites from which website you were coming from when visiting them. 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>> Type: <<types,String>>
@ -1655,15 +1720,16 @@ Valid values:
* +always+: Always send the Referer. * +always+: Always send the Referer.
* +never+: Never send the Referer. This is not recommended, as some sites may break. * +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]+ Default: +pass:[same-domain]+
This setting is only available with the QtWebKit backend.
[[content.headers.user_agent]] [[content.headers.user_agent]]
=== content.headers.user_agent === content.headers.user_agent
User agent to send. Unset to send the default. 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>> Type: <<types,String>>
@ -1673,6 +1739,8 @@ Default: empty
=== content.host_blocking.enabled === content.host_blocking.enabled
Enable host blocking. Enable host blocking.
This setting supports URL patterns.
Type: <<types,Bool>> Type: <<types,Bool>>
Default: +pass:[true]+ 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 - A zip-file of any of the above, with either only one file, or a file
named `hosts` (with any extension). 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>> Type: <<types,List of Url>>
@ -1697,11 +1770,11 @@ Default:
[[content.host_blocking.whitelist]] [[content.host_blocking.whitelist]]
=== content.host_blocking.whitelist === content.host_blocking.whitelist
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. Local domains are always exempt from hostblocking.
Type: <<types,List of String>> Type: <<types,List of UrlPattern>>
Default: Default:
@ -1843,6 +1916,8 @@ Default: +pass:[true]+
=== content.media_capture === content.media_capture
Allow websites to record audio/video. Allow websites to record audio/video.
This setting supports URL patterns.
Type: <<types,BoolAsk>> Type: <<types,BoolAsk>>
Valid values: Valid values:
@ -1855,6 +1930,37 @@ Default: +pass:[ask]+
This setting is only available with the QtWebEngine backend. 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]]
=== content.netrc_file === content.netrc_file
Netrc-file for HTTP authentication. Netrc-file for HTTP authentication.
@ -1868,6 +1974,8 @@ Default: empty
=== content.notifications === content.notifications
Allow websites to show notifications. Allow websites to show notifications.
This setting supports URL patterns.
Type: <<types,BoolAsk>> Type: <<types,BoolAsk>>
Valid values: Valid values:
@ -1889,12 +1997,12 @@ Type: <<types,Bool>>
Default: +pass:[false]+ Default: +pass:[false]+
This setting is only available with the QtWebKit backend.
[[content.persistent_storage]] [[content.persistent_storage]]
=== content.persistent_storage === content.persistent_storage
Allow websites to request persistent storage quota via `navigator.webkitPersistentStorage.requestQuota`. Allow websites to request persistent storage quota via `navigator.webkitPersistentStorage.requestQuota`.
This setting supports URL patterns.
Type: <<types,BoolAsk>> Type: <<types,BoolAsk>>
Valid values: Valid values:
@ -1967,6 +2075,8 @@ This setting is only available with the QtWebKit backend.
=== content.register_protocol_handler === content.register_protocol_handler
Allow websites to register protocol handlers via `navigator.registerProtocolHandler`. Allow websites to register protocol handlers via `navigator.registerProtocolHandler`.
This setting supports URL patterns.
Type: <<types,BoolAsk>> Type: <<types,BoolAsk>>
Valid values: Valid values:
@ -1985,6 +2095,8 @@ On QtWebKit, this setting is unavailable.
=== content.ssl_strict === content.ssl_strict
Validate SSL handshakes. Validate SSL handshakes.
This setting supports URL patterns.
Type: <<types,BoolAsk>> Type: <<types,BoolAsk>>
Valid values: Valid values:
@ -2013,14 +2125,22 @@ Type: <<types,Bool>>
Default: +pass:[true]+ Default: +pass:[true]+
[[content.webrtc_public_interfaces_only]] [[content.webrtc_ip_handling_policy]]
=== content.webrtc_public_interfaces_only === content.webrtc_ip_handling_policy
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.
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. On QtWebEngine, this setting requires Qt 5.9.2 or newer.
@ -2037,13 +2157,13 @@ Default: +pass:[false]+
[[content.xss_auditing]] [[content.xss_auditing]]
=== content.xss_auditing === content.xss_auditing
Monitor load requests for cross-site scripting attempts. 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. This setting supports URL patterns.
Type: <<types,Bool>> Type: <<types,Bool>>
Default: +pass:[false]+ Default: +pass:[true]+
[[downloads.location.directory]] [[downloads.location.directory]]
=== downloads.location.directory === downloads.location.directory
@ -2455,6 +2575,76 @@ Type: <<types,Bool>>
Default: +pass:[true]+ 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=&quot;hidden&quot;])]+
* +pass:[button]+
* +pass:[frame]+
* +pass:[iframe]+
* +pass:[img]+
* +pass:[link]+
* +pass:[summary]+
* +pass:[[onclick]]+
* +pass:[[onmousedown]]+
* +pass:[[role=&quot;link&quot;]]+
* +pass:[[role=&quot;option&quot;]]+
* +pass:[[role=&quot;button&quot;]]+
* +pass:[[ng-click]]+
* +pass:[[ngClick]]+
* +pass:[[data-ng-click]]+
* +pass:[[x-ng-click]]+
* +pass:[[tabindex]]+
- +pass:[images]+:
* +pass:[img]+
- +pass:[inputs]+:
* +pass:[input[type=&quot;text&quot;]]+
* +pass:[input[type=&quot;date&quot;]]+
* +pass:[input[type=&quot;datetime-local&quot;]]+
* +pass:[input[type=&quot;email&quot;]]+
* +pass:[input[type=&quot;month&quot;]]+
* +pass:[input[type=&quot;number&quot;]]+
* +pass:[input[type=&quot;password&quot;]]+
* +pass:[input[type=&quot;search&quot;]]+
* +pass:[input[type=&quot;tel&quot;]]+
* +pass:[input[type=&quot;time&quot;]]+
* +pass:[input[type=&quot;url&quot;]]+
* +pass:[input[type=&quot;week&quot;]]+
* +pass:[input:not([type])]+
* +pass:[textarea]+
- +pass:[links]+:
* +pass:[a[href]]+
* +pass:[area[href]]+
* +pass:[link[href]]+
* +pass:[[role=&quot;link&quot;][href]]+
- +pass:[media]+:
* +pass:[audio]+
* +pass:[img]+
* +pass:[video]+
- +pass:[url]+:
* +pass:[[src]]+
* +pass:[[href]]+
[[hints.uppercase]] [[hints.uppercase]]
=== hints.uppercase === hints.uppercase
Make characters in hint strings uppercase. Make characters in hint strings uppercase.
@ -2699,13 +2889,59 @@ Type: <<types,Bool>>
Default: +pass:[false]+ 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]]
=== 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]]
=== scrolling.smooth === scrolling.smooth
@ -2972,6 +3208,17 @@ Valid values:
Default: +pass:[ignore]+ 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]]
=== tabs.min_width === tabs.min_width
Minimum width (in pixels) of tabs (-1 for the default minimum size behavior). 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]]
=== tabs.new_position.related === tabs.new_position.related
Position of new tabs opened from another tab. Position of new tabs opened from another tab.
See `tabs.new_position.stacking` for controlling stacking behavior.
Type: <<types,NewTabPosition>> Type: <<types,NewTabPosition>>
@ -3019,9 +3267,19 @@ Valid values:
Default: +pass:[next]+ 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]]
=== 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>> 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'' 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. |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. |QtFont|A font family, with optional style/weight/size.
* Style: `normal`/`italic`/`oblique` * Weight: `normal`, `bold`, `100`..`900` * Size: _number_ `px`/`pt` * 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. See https://sqlite.org/lang_datefunc.html for reference.
|UniqueCharString|A string which may not contain duplicate chars. |UniqueCharString|A string which may not contain duplicate chars.
|Url|A URL as a string. |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. |VerticalPosition|The position of the download bar.
|============== |==============

View File

@ -222,6 +222,38 @@ There are prebuilt RPMs available at https://software.opensuse.org/download.html
To use the QtWebEngine backend, install `libqt5-qtwebengine`. 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 On OpenBSD
---------- ----------
@ -407,6 +439,14 @@ caveats:
(`export LD_LIBRARY_PATH=/usr/lib/openssl-1.0` on Archlinux) before starting (`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 qutebrowser if you want SSL to work in certain downloads (e.g. for
`:adblock-update` or `:download`). `: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 - It comes with a QtWebEngine compiled without proprietary codec support (such
as h.264). as h.264).

View File

@ -19,7 +19,7 @@ Also note userscripts need to have the executable bit set (`chmod +x`) for
qutebrowser to run them. qutebrowser to run them.
To call a userscript, it needs to be stored in your data directory under To call a userscript, it needs to be stored in your data directory under
`userscripts` (for example: `~/.local/share/qutebrowser/userscripts/myscript`), `userscripts` (for example: `~/.local/share/qutebrowser/userscripts/myscript`),
or just use an absolute path. or just use an absolute path.
NOTE: On Windows, only userscripts with `com`, `bat`, or `exe` extensions will be launched. NOTE: On Windows, only userscripts with `com`, `bat`, or `exe` extensions will be launched.
@ -45,12 +45,13 @@ In `command` mode:
- `QUTE_URL`: The current URL. - `QUTE_URL`: The current URL.
- `QUTE_TITLE`: The title of the current page. - `QUTE_TITLE`: The title of the current page.
- `QUTE_SELECTED_TEXT`: The text currently selected on the 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: In `hints` mode:
- `QUTE_URL`: The URL selected via hints. - `QUTE_URL`: The URL selected via hints.
- `QUTE_SELECTED_TEXT`: The plain text of the element 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 Sending commands
---------------- ----------------

View File

@ -1,7 +1,9 @@
PYTHON = python3 PYTHON = python3
PREFIX = /usr/local PREFIX ?= /usr/local
DESTDIR =
ICONSIZES = 16 24 32 48 64 128 256 512 ICONSIZES = 16 24 32 48 64 128 256 512
DATAROOTDIR = $(PREFIX)/share
DATADIR ?= $(DATAROOTDIR)
MANDIR ?= $(DATAROOTDIR)/man
SETUPTOOLSOPTIONS = SETUPTOOLSOPTIONS =
ifdef DESTDIR ifdef DESTDIR
@ -16,18 +18,18 @@ doc/qutebrowser.1.html:
install: doc/qutebrowser.1.html install: doc/qutebrowser.1.html
$(PYTHON) setup.py install --prefix="$(PREFIX)" --optimize=1 $(SETUPTOOLSOPTS) $(PYTHON) setup.py install --prefix="$(PREFIX)" --optimize=1 $(SETUPTOOLSOPTS)
install -Dm644 misc/qutebrowser.appdata.xml \ install -Dm644 misc/qutebrowser.appdata.xml \
"$(DESTDIR)$(PREFIX)/share/metainfo/qutebrowser.appdata.xml" "$(DESTDIR)$(DATADIR)/metainfo/qutebrowser.appdata.xml"
install -Dm644 doc/qutebrowser.1 \ install -Dm644 doc/qutebrowser.1 \
"$(DESTDIR)$(PREFIX)/share/man/man1/qutebrowser.1" "$(DESTDIR)$(MANDIR)/man1/qutebrowser.1"
install -Dm644 misc/qutebrowser.desktop \ 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" \ $(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 \ install -Dm644 icons/qutebrowser.svg \
"$(DESTDIR)$(PREFIX)/share/icons/hicolor/scalable/apps/qutebrowser.svg" "$(DESTDIR)$(DATADIR)/icons/hicolor/scalable/apps/qutebrowser.svg"
install -Dm755 -t "$(DESTDIR)$(PREFIX)/share/qutebrowser/userscripts/" \ install -Dm755 -t "$(DESTDIR)$(DATADIR)/qutebrowser/userscripts/" \
$(wildcard misc/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 \ $(filter-out scripts/__init__.py scripts/__pycache__ scripts/dev \
scripts/testbrowser scripts/asciidoc2html.py scripts/setupcommon.py \ scripts/testbrowser scripts/asciidoc2html.py scripts/setupcommon.py \
scripts/link_pyqt.py,$(wildcard scripts/*)) scripts/link_pyqt.py,$(wildcard scripts/*))

View File

@ -13,7 +13,7 @@
height="682.66669" height="682.66669"
id="svg2" id="svg2"
sodipodi:version="0.32" 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" version="1.0"
sodipodi:docname="cheatsheet.svg" sodipodi:docname="cheatsheet.svg"
inkscape:output_extension="org.inkscape.output.svg.inkscape" inkscape:output_extension="org.inkscape.output.svg.inkscape"
@ -33,7 +33,7 @@
inkscape:pageopacity="0.0" inkscape:pageopacity="0.0"
inkscape:pageshadow="2" inkscape:pageshadow="2"
inkscape:zoom="1.7536248" inkscape:zoom="1.7536248"
inkscape:cx="430.72917" inkscape:cx="613.20834"
inkscape:cy="268.64059" inkscape:cy="268.64059"
inkscape:document-units="px" inkscape:document-units="px"
inkscape:current-layer="layer1" 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" 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 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" 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" 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 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" 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" 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 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" 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" 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" 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 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" 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

View File

@ -1,6 +1,7 @@
[Desktop Entry] [Desktop Entry]
Name=qutebrowser Name=qutebrowser
GenericName=Web Browser GenericName=Web Browser
Comment=A keyboard-driven, vim-like browser based on PyQt5
Icon=qutebrowser Icon=qutebrowser
Type=Application Type=Application
Categories=Network;WebBrowser; Categories=Network;WebBrowser;

View File

@ -40,6 +40,9 @@ Section "Install"
; Uninstall old versions ; Uninstall old versions
ExecWait 'MsiExec.exe /quiet /qn /norestart /X{633F41F9-FE9B-42D1-9CC4-718CBD01EE11}' ExecWait 'MsiExec.exe /quiet /qn /norestart /X{633F41F9-FE9B-42D1-9CC4-718CBD01EE11}'
ExecWait 'MsiExec.exe /quiet /qn /norestart /X{9331D947-AC86-4542-A755-A833429C6E69}' 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" SetOutPath "$INSTDIR"

View File

@ -19,10 +19,10 @@ def get_data_files():
('../qutebrowser/config/configdata.yml', 'config'), ('../qutebrowser/config/configdata.yml', 'config'),
] ]
# if os.path.exists(os.path.join('qutebrowser', '3rdparty', 'pdfjs')): if os.path.exists(os.path.join('qutebrowser', '3rdparty', 'pdfjs')):
# data_files.append(('../qutebrowser/3rdparty/pdfjs', '3rdparty/pdfjs')) data_files.append(('../qutebrowser/3rdparty/pdfjs', '3rdparty/pdfjs'))
# else: else:
# print("Warning: excluding pdfjs as it's not present!") print("Warning: excluding pdfjs as it's not present!")
return data_files return data_files

View File

@ -1,9 +1,9 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py # This file is automatically generated by scripts/dev/recompile_requirements.py
certifi==2018.4.16 certifi==2018.8.24
chardet==3.0.4 chardet==3.0.4
codecov==2.0.15 codecov==2.0.15
coverage==4.5.1 coverage==4.5.1
idna==2.7 idna==2.7
requests==2.18.4 requests==2.19.1
urllib3==1.22 urllib3==1.23

View File

@ -1,15 +1,15 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py # This file is automatically generated by scripts/dev/recompile_requirements.py
attrs==18.1.0 attrs==18.2.0
flake8==3.5.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-builtins==1.4.1 # rq.filter: != 1.4.0
flake8-comprehensions==1.4.1 flake8-comprehensions==1.4.1
flake8-copyright==0.2.0 flake8-copyright==0.2.0
flake8-debugger==3.1.0 flake8-debugger==3.1.0
flake8-deprecated==1.3 flake8-deprecated==1.3
flake8-docstrings==1.3.0 flake8-docstrings==1.3.0
flake8-future-import==0.4.4 flake8-future-import==0.4.5
flake8-mock==0.3 flake8-mock==0.3
flake8-per-file-ignores==0.6 flake8-per-file-ignores==0.6
flake8-polyfill==1.0.2 flake8-polyfill==1.0.2
@ -24,4 +24,4 @@ pydocstyle==2.1.1
pyflakes==2.0.0 pyflakes==2.0.0
six==1.11.0 six==1.11.0
snowballstemmer==1.2.1 snowballstemmer==1.2.1
typing==3.6.4 typing==3.6.6

View File

@ -1,8 +1,8 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py # This file is automatically generated by scripts/dev/recompile_requirements.py
appdirs==1.4.3 appdirs==1.4.3
packaging==17.1 packaging==18.0
pyparsing==2.2.0 pyparsing==2.2.2
setuptools==39.2.0 setuptools==40.4.3
six==1.11.0 six==1.11.0
wheel==0.31.1 wheel==0.32.1

View File

@ -1,7 +1,7 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py # This file is automatically generated by scripts/dev/recompile_requirements.py
altgraph==0.15 altgraph==0.16.1
future==0.16.0 future==0.16.0
macholib==1.9 macholib==1.11
pefile==2017.11.5 pefile==2018.8.8
PyInstaller==3.3.1 -e git+https://github.com/pyinstaller/pyinstaller.git@develop#egg=PyInstaller

View File

@ -1 +1,4 @@
PyInstaller -e git+https://github.com/pyinstaller/pyinstaller.git@develop#egg=PyInstaller
# remove @commit-id for scm installs
#@ replace: @.*# @develop#

View File

@ -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

View File

@ -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

View File

@ -1,18 +1,23 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py # This file is automatically generated by scripts/dev/recompile_requirements.py
astroid==1.6.5 asn1crypto==0.24.0
certifi==2018.4.16 astroid==2.0.4
certifi==2018.8.24
cffi==1.11.5
chardet==3.0.4 chardet==3.0.4
github3.py==1.1.0 cryptography==2.3.1
github3.py==1.2.0
idna==2.7 idna==2.7
isort==4.3.4 isort==4.3.4
jwcrypto==0.5.0
lazy-object-proxy==1.3.1 lazy-object-proxy==1.3.1
mccabe==0.6.1 mccabe==0.6.1
pylint==1.9.2 pycparser==2.19
pylint==2.1.1
python-dateutil==2.7.3 python-dateutil==2.7.3
./scripts/dev/pylint_checkers ./scripts/dev/pylint_checkers
requests==2.18.4 requests==2.19.1
six==1.11.0 six==1.11.0
uritemplate==3.0.0 uritemplate==3.0.0
urllib3==1.22 urllib3==1.23
wrapt==1.10.11 wrapt==1.10.11

View File

@ -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

View File

@ -1,2 +0,0 @@
PyQt5==5.10.0
#@ filter: PyQt5 != 5.10.1

View File

@ -1,4 +1,4 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py # This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt5==5.10.1 PyQt5==5.11.3
sip==4.19.8 PyQt5-sip==4.19.13

View File

@ -1,4 +1,4 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py # This file is automatically generated by scripts/dev/recompile_requirements.py
docutils==0.14 docutils==0.14
pyroma==2.3.1 pyroma==2.4

View File

@ -35,8 +35,4 @@ git+https://github.com/pallets/markupsafe.git
hg+http://bitbucket.org/birkenfeld/pygments-main hg+http://bitbucket.org/birkenfeld/pygments-main
hg+https://bitbucket.org/fdik/pypeg hg+https://bitbucket.org/fdik/pypeg
git+https://github.com/python-attrs/attrs.git git+https://github.com/python-attrs/attrs.git
git+https://github.com/yaml/pyyaml.git
# Fails to build:
# gcc: error: ext/_yaml.c: No such file or directory
# hg+https://bitbucket.org/xi/pyyaml
PyYAML==3.12

View File

@ -1,9 +1,11 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py # This file is automatically generated by scripts/dev/recompile_requirements.py
attrs==18.1.0 atomicwrites==1.2.1
beautifulsoup4==4.6.0 attrs==18.2.0
cheroot==6.3.1 backports.functools-lru-cache==1.5
click==6.7 beautifulsoup4==4.6.3
cheroot==6.5.2
click==7.0
# colorama==0.3.9 # colorama==0.3.9
coverage==4.5.1 coverage==4.5.1
EasyProcess==0.2.3 EasyProcess==0.2.3
@ -11,30 +13,30 @@ fields==5.0.0
Flask==1.0.2 Flask==1.0.2
glob2==0.6 glob2==0.6
hunter==2.0.2 hunter==2.0.2
hypothesis==3.57.0 hypothesis==3.74.3
itsdangerous==0.24 itsdangerous==0.24
# Jinja2==2.10 # Jinja2==2.10
Mako==1.0.7 Mako==1.0.7
# MarkupSafe==1.0 # MarkupSafe==1.0
more-itertools==4.2.0 more-itertools==4.3.0
parse==1.8.4 parse==1.9.0
parse-type==0.4.2 parse-type==0.4.2
pluggy==0.6.0 pluggy==0.7.1
py==1.5.3 py==1.6.0
py-cpuinfo==4.0.0 py-cpuinfo==4.0.0
pytest==3.6.1 pytest==3.6.4 # rq.filter: <3.7
pytest-bdd==2.21.0 pytest-bdd==2.21.0
pytest-benchmark==3.1.1 pytest-benchmark==3.1.1
pytest-cov==2.5.1 pytest-cov==2.6.0
pytest-faulthandler==1.5.0 pytest-faulthandler==1.5.0
pytest-instafail==0.4.0 pytest-instafail==0.4.0
pytest-mock==1.10.0 pytest-mock==1.10.0
pytest-qt==2.4.0 pytest-qt==3.2.1
pytest-repeat==0.4.1 pytest-repeat==0.7.0
pytest-rerunfailures==4.1 pytest-rerunfailures==4.2
pytest-travis-fold==1.3.0 pytest-travis-fold==1.3.0
pytest-xvfb==1.1.0 pytest-xvfb==1.1.0
PyVirtualDisplay==0.2.1 PyVirtualDisplay==0.2.1
six==1.11.0 six==1.11.0
vulture==0.27 vulture==0.29
Werkzeug==0.14.1 Werkzeug==0.14.1

View File

@ -4,7 +4,7 @@ coverage
Flask Flask
hunter hunter
hypothesis hypothesis
pytest pytest<3.7
pytest-bdd pytest-bdd
pytest-benchmark pytest-benchmark
pytest-cov pytest-cov
@ -19,3 +19,4 @@ pytest-xvfb
vulture vulture
#@ ignore: Jinja2, MarkupSafe, colorama #@ ignore: Jinja2, MarkupSafe, colorama
#@ filter: pytest <3.7

View File

@ -1,7 +1,8 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py # This file is automatically generated by scripts/dev/recompile_requirements.py
pluggy==0.6.0 pluggy==0.7.1
py==1.5.3 py==1.6.0
six==1.11.0 six==1.11.0
tox==3.0.0 toml==0.10.0
tox==3.5.1
virtualenv==16.0.0 virtualenv==16.0.0

View File

@ -1,3 +1,3 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py # This file is automatically generated by scripts/dev/recompile_requirements.py
vulture==0.27 vulture==0.29

View 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/

View File

@ -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 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 "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: 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. 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 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 = argument_parser.add_mutually_exclusive_group()
group.add_argument('--username-only', '-e', action='store_true', help='Only insert username') 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('--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) stderr = functools.partial(print, file=sys.stderr)
@ -87,7 +96,7 @@ def qute_command(command):
def find_pass_candidates(domain, password_store_path): def find_pass_candidates(domain, password_store_path):
candidates = [] 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): if directories or domain not in path.split(os.path.sep):
continue continue
@ -98,11 +107,19 @@ def find_pass_candidates(domain, password_store_path):
return candidates return candidates
def pass_(path, encoding): def _run_pass(command, encoding):
process = subprocess.run(['pass', path], stdout=subprocess.PIPE) process = subprocess.run(command, stdout=subprocess.PIPE)
return process.stdout.decode(encoding).strip() 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): def dmenu(items, invocation, encoding):
command = shlex.split(invocation) command = shlex.split(invocation)
process = subprocess.run(command, input='\n'.join(items).encode(encoding), stdout=subprocess.PIPE) process = subprocess.run(command, input='\n'.join(items).encode(encoding), stdout=subprocess.PIPE)
@ -152,7 +169,7 @@ def main(arguments):
# Match username # Match username
target = selection if arguments.username_target == 'path' else secret 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: if not match:
stderr('Failed to match username pattern on {}!'.format(arguments.username_target)) stderr('Failed to match username pattern on {}!'.format(arguments.username_target))
return ExitCodes.COULD_NOT_MATCH_USERNAME return ExitCodes.COULD_NOT_MATCH_USERNAME
@ -169,6 +186,9 @@ def main(arguments):
fake_key_raw(username) fake_key_raw(username)
elif arguments.password_only: elif arguments.password_only:
fake_key_raw(password) fake_key_raw(password)
elif arguments.otp_only:
otp = pass_otp(selection, arguments.io_encoding)
fake_key_raw(otp)
else: else:
# Enter username and password using fake-key and <Tab> (which seems to work almost universally), then switch # 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 # back into insert-mode, so the form can be directly submitted by hitting enter afterwards

View File

@ -63,4 +63,8 @@ qt_log_ignore =
^inotify_add_watch\(".*"\) failed: "No space left on device" ^inotify_add_watch\(".*"\) failed: "No space left on device"
^QSettings::value: Empty key passed ^QSettings::value: Empty key passed
^Icon theme ".*" not found ^Icon theme ".*" not found
^Error receiving trust for a CA certificate
xfail_strict = true 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

View File

@ -26,7 +26,7 @@ __copyright__ = "Copyright 2014-2018 Florian Bruhin (The Compiler)"
__license__ = "GPL" __license__ = "GPL"
__maintainer__ = __author__ __maintainer__ = __author__
__email__ = "mail@qutebrowser.org" __email__ = "mail@qutebrowser.org"
__version_info__ = (1, 3, 2) __version_info__ = (1, 5, 1)
__version__ = '.'.join(str(e) for e in __version_info__) __version__ = '.'.join(str(e) for e in __version_info__)
__description__ = "A keyboard-driven, vim-like browser based on PyQt5." __description__ = "A keyboard-driven, vim-like browser based on PyQt5."

View File

@ -59,7 +59,6 @@ except ImportError:
import qutebrowser import qutebrowser
import qutebrowser.resources import qutebrowser.resources
from qutebrowser.completion import completiondelegate
from qutebrowser.completion.models import miscmodels from qutebrowser.completion.models import miscmodels
from qutebrowser.commands import cmdutils, runners, cmdexc from qutebrowser.commands import cmdutils, runners, cmdexc
from qutebrowser.config import config, websettings, configfiles, configinit 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.mainwindow import mainwindow, prompt
from qutebrowser.misc import (readline, ipc, savemanager, sessions, from qutebrowser.misc import (readline, ipc, savemanager, sessions,
crashsignal, earlyinit, sql, cmdhistory, crashsignal, earlyinit, sql, cmdhistory,
backendproblem) backendproblem, objects)
from qutebrowser.utils import (log, version, message, utils, urlutils, objreg, from qutebrowser.utils import (log, version, message, utils, urlutils, objreg,
usertypes, standarddir, error) usertypes, standarddir, error, qtutils)
# pylint: disable=unused-import # pylint: disable=unused-import
# We import those to run the cmdutils.register decorators. # We import those to run the cmdutils.register decorators.
from qutebrowser.mainwindow.statusbar import command from qutebrowser.mainwindow.statusbar import command
@ -104,6 +103,7 @@ def run(args):
qApp = Application(args) qApp = Application(args)
qApp.setOrganizationName("qutebrowser") qApp.setOrganizationName("qutebrowser")
qApp.setApplicationName("qutebrowser") qApp.setApplicationName("qutebrowser")
qApp.setDesktopFileName("qutebrowser")
qApp.setApplicationVersion(qutebrowser.__version__) qApp.setApplicationVersion(qutebrowser.__version__)
qApp.lastWindowClosed.connect(quitter.on_last_window_closed) qApp.lastWindowClosed.connect(quitter.on_last_window_closed)
@ -129,6 +129,9 @@ def run(args):
sys.exit(usertypes.Exit.err_ipc) sys.exit(usertypes.Exit.err_ipc)
if server is None: 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) sys.exit(usertypes.Exit.ok)
else: else:
server.got_args.connect(lambda args, target_arg, cwd: 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('https', open_desktopservices_url)
QDesktopServices.setUrlHandler('qute', open_desktopservices_url) QDesktopServices.setUrlHandler('qute', open_desktopservices_url)
objreg.get('web-history').import_txt()
log.init.debug("Init done!") log.init.debug("Init done!")
crash_handler.raise_crashdlg() crash_handler.raise_crashdlg()
@ -349,10 +350,6 @@ def _open_startpage(win_id=None):
def _open_special_pages(args): def _open_special_pages(args):
"""Open special notification pages which are only shown once. """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:
args: The argparse namespace. args: The argparse namespace.
""" """
@ -364,25 +361,30 @@ def _open_special_pages(args):
tabbed_browser = objreg.get('tabbed-browser', scope='window', tabbed_browser = objreg.get('tabbed-browser', scope='window',
window='last-focused') 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: ('webkit-warning-shown',
tabbed_browser.tabopen( objects.backend == usertypes.Backend.QtWebKit,
QUrl('https://www.qutebrowser.org/quickstart.html')) 'qute://warning/webkit'),
general_sect['quickstart-done'] = '1'
# Setting migration page ('old-qt-warning-shown',
not qtutils.version_check('5.9'),
'qute://warning/old-qt'),
]
needs_migration = os.path.exists( for state, condition, url in pages:
os.path.join(standarddir.config(), 'qutebrowser.conf')) if general_sect.get(state) != '1' and condition:
migration_shown = general_sect.get('config-migration-shown') == '1' tabbed_browser.tabopen(QUrl(url), background=False)
general_sect[state] = '1'
if needs_migration and not migration_shown:
tabbed_browser.tabopen(QUrl('qute://help/configuring.html'),
background=False)
general_sect['config-migration-shown'] = '1'
def on_focus_changed(_old, new): def on_focus_changed(_old, new):
@ -445,16 +447,10 @@ def _init_modules(args, crash_handler):
log.init.debug("Initializing web history...") log.init.debug("Initializing web history...")
history.init(qApp) history.init(qApp)
except sql.SqlError as e: except sql.SqlEnvironmentError as e:
if e.environmental: error.handle_fatal_exc(e, args, 'Error initializing SQL',
error.handle_fatal_exc(e, args, 'Error initializing SQL', pre_text='Error initializing SQL')
pre_text='Error initializing SQL') sys.exit(usertypes.Exit.err_init)
sys.exit(usertypes.Exit.err_init)
else:
raise
log.init.debug("Initializing completion...")
completiondelegate.init()
log.init.debug("Initializing command history...") log.init.debug("Initializing command history...")
cmdhistory.init() cmdhistory.init()

View File

@ -24,7 +24,6 @@ import os.path
import functools import functools
import posixpath import posixpath
import zipfile import zipfile
import fnmatch
from qutebrowser.browser import downloads from qutebrowser.browser import downloads
from qutebrowser.config import config from qutebrowser.config import config
@ -32,7 +31,7 @@ from qutebrowser.utils import objreg, standarddir, log, message
from qutebrowser.commands import cmdutils from qutebrowser.commands import cmdutils
def guess_zip_filename(zf): def _guess_zip_filename(zf):
"""Guess which file to use inside a zip file. """Guess which file to use inside a zip file.
Args: Args:
@ -54,26 +53,26 @@ def get_fileobj(byte_io):
if zipfile.is_zipfile(byte_io): if zipfile.is_zipfile(byte_io):
byte_io.seek(0) # rewind what zipfile.is_zipfile did byte_io.seek(0) # rewind what zipfile.is_zipfile did
zf = zipfile.ZipFile(byte_io) zf = zipfile.ZipFile(byte_io)
filename = guess_zip_filename(zf) filename = _guess_zip_filename(zf)
byte_io = zf.open(filename, mode='r') byte_io = zf.open(filename, mode='r')
else: else:
byte_io.seek(0) # rewind what zipfile.is_zipfile did byte_io.seek(0) # rewind what zipfile.is_zipfile did
return byte_io return byte_io
def is_whitelisted_host(host): def _is_whitelisted_url(url):
"""Check if the given host is on the adblock whitelist. """Check if the given URL is on the adblock whitelist.
Args: 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: for pattern in config.val.content.host_blocking.whitelist:
if fnmatch.fnmatch(host, pattern.lower()): if pattern.matches(url):
return True return True
return False return False
class FakeDownload: class _FakeDownload:
"""A download stub to use on_download_finished with local files.""" """A download stub to use on_download_finished with local files."""
@ -111,14 +110,18 @@ class HostBlocker:
config.instance.changed.connect(self._update_files) 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.""" """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 return False
host = url.host() host = url.host()
return ((host in self._blocked_hosts or return ((host in self._blocked_hosts or
host in self._config_blocked_hosts) and host in self._config_blocked_hosts) and
not is_whitelisted_host(host)) not _is_whitelisted_url(url))
def _read_hosts_file(self, filename, target): def _read_hosts_file(self, filename, target):
"""Read hosts from the given filename. """Read hosts from the given filename.
@ -174,15 +177,12 @@ class HostBlocker:
for url in config.val.content.host_blocking.lists: for url in config.val.content.host_blocking.lists:
if url.scheme() == 'file': if url.scheme() == 'file':
filename = url.toLocalFile() filename = url.toLocalFile()
try: if os.path.isdir(filename):
fileobj = open(filename, 'rb') for entry in os.scandir(filename):
except OSError as e: if entry.is_file():
message.error("adblock: Error while reading {}: {}".format( self._import_local(entry.path)
filename, e.strerror)) else:
continue self._import_local(filename)
download = FakeDownload(fileobj)
self._in_progress.append(download)
self.on_download_finished(download)
else: else:
fobj = io.BytesIO() fobj = io.BytesIO()
fobj.name = 'adblock: ' + url.host() fobj.name = 'adblock: ' + url.host()
@ -191,7 +191,23 @@ class HostBlocker:
auto_remove=True) auto_remove=True)
self._in_progress.append(download) self._in_progress.append(download)
download.finished.connect( 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): def _parse_line(self, line):
"""Parse a line from a host file. """Parse a line from a host file.
@ -234,7 +250,9 @@ class HostBlocker:
hosts = parts[1:] hosts = parts[1:]
for host in hosts: 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) self._blocked_hosts.add(host)
return True return True
@ -269,7 +287,7 @@ class HostBlocker:
message.error("adblock: {} read errors for {}".format( message.error("adblock: {} read errors for {}".format(
error_count, byte_io.name)) error_count, byte_io.name))
def on_lists_downloaded(self): def _on_lists_downloaded(self):
"""Install block lists after files have been downloaded.""" """Install block lists after files have been downloaded."""
with open(self._local_hosts_file, 'w', encoding='utf-8') as f: with open(self._local_hosts_file, 'w', encoding='utf-8') as f:
for host in sorted(self._blocked_hosts): for host in sorted(self._blocked_hosts):
@ -288,7 +306,7 @@ class HostBlocker:
except OSError as e: except OSError as e:
log.misc.exception("Failed to delete hosts file: {}".format(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. """Check if all downloads are finished and if so, trigger reading.
Arguments: Arguments:
@ -303,6 +321,6 @@ class HostBlocker:
download.fileobj.close() download.fileobj.close()
if not self._in_progress: if not self._in_progress:
try: try:
self.on_lists_downloaded() self._on_lists_downloaded()
except OSError: except OSError:
log.misc.exception("Failed to write host block list!") log.misc.exception("Failed to write host block list!")

View File

@ -22,11 +22,11 @@
import enum import enum
import itertools import itertools
import sip
import attr import attr
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QUrl, QObject, QSizeF, Qt from PyQt5.QtCore import pyqtSignal, pyqtSlot, QUrl, QObject, QSizeF, Qt
from PyQt5.QtGui import QIcon 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
import pygments.lexers import pygments.lexers
@ -38,6 +38,7 @@ from qutebrowser.utils import (utils, objreg, usertypes, log, qtutils,
urlutils, message) urlutils, message)
from qutebrowser.misc import miscwidgets, objects from qutebrowser.misc import miscwidgets, objects
from qutebrowser.browser import mouse, hints from qutebrowser.browser import mouse, hints
from qutebrowser.qt import sip
tab_id_gen = itertools.count(0) tab_id_gen = itertools.count(0)
@ -187,8 +188,9 @@ class AbstractPrinting:
"""Attribute of AbstractTab for printing the page.""" """Attribute of AbstractTab for printing the page."""
def __init__(self): def __init__(self, tab):
self._widget = None self._widget = None
self._tab = tab
def check_pdf_support(self): def check_pdf_support(self):
raise NotImplementedError raise NotImplementedError
@ -212,6 +214,29 @@ class AbstractPrinting:
""" """
raise NotImplementedError 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): class AbstractSearch(QObject):
@ -223,10 +248,19 @@ class AbstractSearch(QObject):
this view. this view.
_flags: The flags of the last search (needs to be set by subclasses). _flags: The flags of the last search (needs to be set by subclasses).
_widget: The underlying WebView widget. _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) super().__init__(parent)
self._tab = tab
self._widget = None self._widget = None
self.text = None self.text = None
self.search_displayed = False self.search_displayed = False
@ -370,9 +404,11 @@ class AbstractCaret(QObject):
Signals: Signals:
selection_toggled: Emitted when the selection was toggled. selection_toggled: Emitted when the selection was toggled.
arg: Whether the selection is now active. arg: Whether the selection is now active.
follow_selected_done: Emitted when a follow_selection action is done.
""" """
selection_toggled = pyqtSignal(bool) selection_toggled = pyqtSignal(bool)
follow_selected_done = pyqtSignal()
def __init__(self, tab, mode_manager, parent=None): def __init__(self, tab, mode_manager, parent=None):
super().__init__(parent) super().__init__(parent)
@ -596,6 +632,9 @@ class AbstractElements:
def find_css(self, selector, callback, *, only_visible=False): def find_css(self, selector, callback, *, only_visible=False):
"""Find all HTML elements matching a given selector async. """Find all HTML elements matching a given selector async.
If there's an error, the callback is called with a webelem.Error
instance.
Args: Args:
callback: The callback to be called when the search finished. callback: The callback to be called when the search finished.
selector: The CSS selector to search for. selector: The CSS selector to search for.
@ -641,20 +680,27 @@ class AbstractAudio(QObject):
muted_changed = pyqtSignal(bool) muted_changed = pyqtSignal(bool)
recently_audible_changed = pyqtSignal(bool) recently_audible_changed = pyqtSignal(bool)
def __init__(self, parent=None): def __init__(self, tab, parent=None):
super().__init__(parent) super().__init__(parent)
self._widget = None self._widget = None
self._tab = tab
def set_muted(self, muted: bool): def set_muted(self, muted: bool, override: bool = False):
"""Set this tab as muted or not.""" """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 raise NotImplementedError
def is_muted(self): def is_muted(self):
"""Whether this tab is muted.""" """Whether this tab is muted."""
raise NotImplementedError raise NotImplementedError
def toggle_muted(self): def toggle_muted(self, *, override: bool = False):
self.set_muted(not self.is_muted()) self.set_muted(not self.is_muted(), override=override)
def is_recently_audible(self): def is_recently_audible(self):
"""Whether this tab has had audio playing recently.""" """Whether this tab has had audio playing recently."""
@ -829,12 +875,20 @@ class AbstractTab(QWidget):
navigation.navigation_type, navigation.navigation_type,
navigation.is_main_frame)) navigation.is_main_frame))
if (navigation.navigation_type == navigation.Type.link_clicked and if not navigation.url.isValid():
not navigation.url.isValid()): # Also a WORKAROUND for missing IDNA 2008 support in QUrl, see
msg = urlutils.get_errstring(navigation.url, # https://bugreports.qt.io/browse/QTBUG-60364
"Invalid link clicked")
message.error(msg) if navigation.navigation_type == navigation.Type.link_clicked:
self.data.open_target = usertypes.ClickTarget.normal 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 navigation.accepted = False
def handle_auto_insert_mode(self, ok): def handle_auto_insert_mode(self, ok):
@ -893,10 +947,6 @@ class AbstractTab(QWidget):
self._progress = perc self._progress = perc
self.load_progress.emit(perc) self.load_progress.emit(perc)
@pyqtSlot()
def _on_ssl_errors(self):
self._has_ssl_errors = True
def url(self, requested=False): def url(self, requested=False):
raise NotImplementedError raise NotImplementedError

View File

@ -25,9 +25,9 @@ import shlex
import functools import functools
import typing 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.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.commands import userscripts, cmdexc, cmdutils, runners
from qutebrowser.config import config, configdata from qutebrowser.config import config, configdata
@ -66,6 +66,11 @@ class CommandDispatcher:
def _new_tabbed_browser(self, private): def _new_tabbed_browser(self, private):
"""Get a tabbed-browser from a new window.""" """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 = mainwindow.MainWindow(private=private)
new_window.show() new_window.show()
return new_window.tabbed_browser return new_window.tabbed_browser
@ -415,27 +420,6 @@ class CommandDispatcher:
tab.printing.to_pdf(filename) tab.printing.to_pdf(filename)
log.misc.debug("Print to file: {}".format(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', @cmdutils.register(instance='command-dispatcher', name='print',
scope='window') scope='window')
@cmdutils.argument('count', count=True) @cmdutils.argument('count', count=True)
@ -453,22 +437,15 @@ class CommandDispatcher:
return return
try: try:
if pdf:
tab.printing.check_pdf_support()
else:
tab.printing.check_printer_support()
if preview: 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: except browsertab.WebTabError as e:
raise cmdexc.CommandError(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') @cmdutils.register(instance='command-dispatcher', scope='window')
def tab_clone(self, bg=False, window=False): def tab_clone(self, bg=False, window=False):
"""Duplicate the current tab. """Duplicate the current tab.
@ -513,14 +490,16 @@ class CommandDispatcher:
new_tabbed_browser.widget.set_tab_pinned(newtab, curtab.data.pinned) new_tabbed_browser.widget.set_tab_pinned(newtab, curtab.data.pinned)
return newtab 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) @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. """Take a tab from another window.
Args: Args:
index: The [win_id/]index of the tab to take. Or a substring index: The [win_id/]index of the tab to take. Or a substring
in which case the closest match will be taken. 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) 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") raise cmdexc.CommandError("Can't take a tab from the same window")
self._open(tab.url(), tab=True) 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.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('win_id', completion=miscmodels.window) @cmdutils.argument('win_id', completion=miscmodels.window)
@cmdutils.argument('count', count=True) @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. """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. If no win_id is given, the tab will get detached into a new window.
Args: Args:
win_id: The window ID of the window to give the current tab to. 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). count: Overrides win_id (index starts at 1 for win_id=0).
""" """
if count is not None: if count is not None:
@ -549,7 +530,7 @@ class CommandDispatcher:
raise cmdexc.CommandError("Can't give a tab to the same window") raise cmdexc.CommandError("Can't give a tab to the same window")
if win_id is None: 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 " raise cmdexc.CommandError("Cannot detach from a window with "
"only one tab") "only one tab")
@ -564,7 +545,9 @@ class CommandDispatcher:
window=win_id) window=win_id)
tabbed_browser.tabopen(self._current_url()) 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): def _back_forward(self, tab, bg, window, count, forward):
"""Helper function for :back/:forward.""" """Helper function for :back/:forward."""
@ -634,11 +617,11 @@ class CommandDispatcher:
- `up`: Go up a level in the current URL. - `up`: Go up a level in the current URL.
- `increment`: Increment the last number in the URL. - `increment`: Increment the last number in the URL.
Uses the Uses the
link:settings.html#url.incdec_segments[url.incdec_segments] link:settings{outsuffix}#url.incdec_segments[url.incdec_segments]
config option. config option.
- `decrement`: Decrement the last number in the URL. - `decrement`: Decrement the last number in the URL.
Uses the Uses the
link:settings.html#url.incdec_segments[url.incdec_segments] link:settings{outsuffix}#url.incdec_segments[url.incdec_segments]
config option. config option.
tab: Open in a new tab. tab: Open in a new tab.
@ -652,7 +635,8 @@ class CommandDispatcher:
cmdutils.check_exclusive((tab, bg, window), 'tbw') cmdutils.check_exclusive((tab, bg, window), 'tbw')
widget = self._current_widget() widget = self._current_widget()
url = self._current_url().adjusted(QUrl.RemoveFragment) url = self._current_url()
url = url.adjusted(QUrl.RemoveFragment | QUrl.RemoveQuery)
handlers = { handlers = {
'prev': functools.partial(navigate.prevnext, prev=True), 'prev': functools.partial(navigate.prevnext, prev=True),
@ -833,7 +817,7 @@ class CommandDispatcher:
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('what', choices=['selection', 'url', 'pretty-url', @cmdutils.argument('what', choices=['selection', 'url', 'pretty-url',
'title', 'domain']) '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. """Yank something to the clipboard or primary selection.
Args: Args:
@ -847,6 +831,7 @@ class CommandDispatcher:
sel: Use the primary selection instead of the clipboard. sel: Use the primary selection instead of the clipboard.
keep: Stay in visual mode after yanking the selection. keep: Stay in visual mode after yanking the selection.
quiet: Don't show an information message.
""" """
if what == 'title': if what == 'title':
s = self._tabbed_browser.widget.page_title(self._current_index()) s = self._tabbed_browser.widget.page_title(self._current_index())
@ -860,10 +845,10 @@ class CommandDispatcher:
what = 'URL' # For printing what = 'URL' # For printing
elif what == 'selection': elif what == 'selection':
def _selection_callback(s): def _selection_callback(s):
if not s: if not s and not quiet:
message.info("Nothing to yank") message.info("Nothing to yank")
return return
self._yank_to_target(s, sel, what, keep) self._yank_to_target(s, sel, what, keep, quiet)
caret = self._current_widget().caret caret = self._current_widget().caret
caret.selection(callback=_selection_callback) caret.selection(callback=_selection_callback)
@ -871,9 +856,9 @@ class CommandDispatcher:
else: # pragma: no cover else: # pragma: no cover
raise ValueError("Invalid value {!r} for `what'.".format(what)) 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(): if sel and utils.supports_selection():
target = "primary selection" target = "primary selection"
else: else:
@ -882,47 +867,53 @@ class CommandDispatcher:
utils.set_clipboard(s, selection=sel) utils.set_clipboard(s, selection=sel)
if what != 'selection': if what != 'selection':
message.info("Yanked {} to {}: {}".format(what, target, s)) if not quiet:
message.info("Yanked {} to {}: {}".format(what, target, s))
else: else:
message.info("{} {} yanked to {}".format( if not quiet:
len(s), "char" if len(s) == 1 else "chars", target)) message.info("{} {} yanked to {}".format(
len(s), "char" if len(s) == 1 else "chars", target))
if not keep: if not keep:
modeman.leave(self._win_id, KeyMode.caret, "yank selected", modeman.leave(self._win_id, KeyMode.caret, "yank selected",
maybe=True) maybe=True)
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('count', count=True) @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. """Increase the zoom level for the current tab.
Args: Args:
count: How many steps to zoom in. count: How many steps to zoom in.
quiet: Don't show a zoom level message.
""" """
tab = self._current_widget() tab = self._current_widget()
try: try:
perc = tab.zoom.offset(count) perc = tab.zoom.offset(count)
except ValueError as e: except ValueError as e:
raise cmdexc.CommandError(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.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('count', count=True) @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. """Decrease the zoom level for the current tab.
Args: Args:
count: How many steps to zoom out. count: How many steps to zoom out.
quiet: Don't show a zoom level message.
""" """
tab = self._current_widget() tab = self._current_widget()
try: try:
perc = tab.zoom.offset(-count) perc = tab.zoom.offset(-count)
except ValueError as e: except ValueError as e:
raise cmdexc.CommandError(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.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('count', count=True) @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. """Set the zoom level for the current tab.
The zoom can be given as argument or as [count]. If neither is The zoom can be given as argument or as [count]. If neither is
@ -932,6 +923,7 @@ class CommandDispatcher:
Args: Args:
zoom: The zoom percentage to set. zoom: The zoom percentage to set.
count: The zoom percentage to set. count: The zoom percentage to set.
quiet: Don't show a zoom level message.
""" """
if zoom is not None: if zoom is not None:
try: try:
@ -949,7 +941,8 @@ class CommandDispatcher:
tab.zoom.set_factor(float(level) / 100) tab.zoom.set_factor(float(level) / 100)
except ValueError: except ValueError:
raise cmdexc.CommandError("Can't zoom {}%!".format(level)) 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') @cmdutils.register(instance='command-dispatcher', scope='window')
def tab_only(self, prev=False, next_=False, force=False): def tab_only(self, prev=False, next_=False, force=False):
@ -1138,9 +1131,6 @@ class CommandDispatcher:
if index == 'last': if index == 'last':
self._tab_focus_last() self._tab_focus_last()
return return
elif not no_last and index == self._current_index() + 1:
self._tab_focus_last(show_error=False)
return
elif index is None: elif index is None:
self.tab_next() self.tab_next()
return return
@ -1148,6 +1138,10 @@ class CommandDispatcher:
if index < 0: if index < 0:
index = self._count() + index + 1 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(): if 1 <= index <= self._count():
self._set_current_index(index - 1) self._set_current_index(index - 1)
else: else:
@ -1201,8 +1195,9 @@ class CommandDispatcher:
@cmdutils.register(instance='command-dispatcher', scope='window', @cmdutils.register(instance='command-dispatcher', scope='window',
maxsplit=0, no_replace_variables=True) maxsplit=0, no_replace_variables=True)
@cmdutils.argument('count', count=True)
def spawn(self, cmdline, userscript=False, verbose=False, def spawn(self, cmdline, userscript=False, verbose=False,
output=False, detach=False): output=False, detach=False, count=None):
"""Spawn a command in a shell. """Spawn a command in a shell.
Args: Args:
@ -1210,12 +1205,13 @@ class CommandDispatcher:
absolute path, or store the userscript in one of those absolute path, or store the userscript in one of those
locations: locations:
- `~/.local/share/qutebrowser/userscripts` - `~/.local/share/qutebrowser/userscripts`
(or `$XDG_DATA_DIR`) (or `$XDG_DATA_HOME`)
- `/usr/share/qutebrowser/userscripts` - `/usr/share/qutebrowser/userscripts`
verbose: Show notifications when the command started/exited. verbose: Show notifications when the command started/exited.
output: Whether the output should be shown in a new tab. output: Whether the output should be shown in a new tab.
detach: Whether the command should be detached from qutebrowser. detach: Whether the command should be detached from qutebrowser.
cmdline: The commandline to execute. cmdline: The commandline to execute.
count: Given to userscripts as $QUTE_COUNT.
""" """
cmdutils.check_exclusive((userscript, detach), 'ud') cmdutils.check_exclusive((userscript, detach), 'ud')
try: try:
@ -1239,7 +1235,7 @@ class CommandDispatcher:
if userscript: if userscript:
def _selection_callback(s): def _selection_callback(s):
try: try:
runner = self._run_userscript(s, cmd, args, verbose) runner = self._run_userscript(s, cmd, args, verbose, count)
runner.finished.connect(_on_proc_finished) runner.finished.connect(_on_proc_finished)
except cmdexc.CommandError as e: except cmdexc.CommandError as e:
message.error(str(e)) message.error(str(e))
@ -1266,19 +1262,23 @@ class CommandDispatcher:
"""Open main startpage in current tab.""" """Open main startpage in current tab."""
self.openurl(config.val.url.start_pages[0]) 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. """Run a userscript given as argument.
Args: Args:
cmd: The userscript to run. cmd: The userscript to run.
args: Arguments to pass to the userscript. args: Arguments to pass to the userscript.
verbose: Show notifications when the command started/exited. verbose: Show notifications when the command started/exited.
count: Exposed to the userscript.
""" """
env = { env = {
'QUTE_MODE': 'command', 'QUTE_MODE': 'command',
'QUTE_SELECTED_TEXT': selection, 'QUTE_SELECTED_TEXT': selection,
} }
if count is not None:
env['QUTE_COUNT'] = str(count)
idx = self._current_index() idx = self._current_index()
if idx != -1: if idx != -1:
env['QUTE_TITLE'] = self._tabbed_browser.widget.page_title(idx) env['QUTE_TITLE'] = self._tabbed_browser.widget.page_title(idx)
@ -1455,6 +1455,7 @@ class CommandDispatcher:
if tab.data.inspector is None: if tab.data.inspector is None:
tab.data.inspector = inspector.create() tab.data.inspector = inspector.create()
tab.data.inspector.inspect(page) tab.data.inspector.inspect(page)
tab.data.inspector.show()
else: else:
tab.data.inspector.toggle(page) tab.data.inspector.toggle(page)
except inspector.WebInspectorError as e: except inspector.WebInspectorError as e:
@ -1673,6 +1674,8 @@ class CommandDispatcher:
""" """
try: try:
elem.set_value(text) 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: except webelem.OrphanedError:
message.error('Edited element vanished') message.error('Edited element vanished')
ed.backup() ed.backup()
@ -2106,7 +2109,10 @@ class CommandDispatcher:
raise cmdexc.CommandError(str(e)) raise cmdexc.CommandError(str(e))
widget = self._current_widget() 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') @cmdutils.register(instance='command-dispatcher', scope='window')
def fake_key(self, keystring, global_=False): def fake_key(self, keystring, global_=False):
@ -2243,6 +2249,6 @@ class CommandDispatcher:
if tab is None: if tab is None:
return return
try: try:
tab.audio.toggle_muted() tab.audio.toggle_muted(override=True)
except browsertab.WebTabError as e: except browsertab.WebTabError as e:
raise cmdexc.CommandError(e) raise cmdexc.CommandError(e)

View File

@ -29,14 +29,15 @@ import pathlib
import tempfile import tempfile
import enum import enum
import sip
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, Qt, QObject, QModelIndex, from PyQt5.QtCore import (pyqtSlot, pyqtSignal, Qt, QObject, QModelIndex,
QTimer, QAbstractListModel, QUrl) QTimer, QAbstractListModel, QUrl)
from qutebrowser.browser import pdfjs
from qutebrowser.commands import cmdexc, cmdutils from qutebrowser.commands import cmdexc, cmdutils
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.utils import (usertypes, standarddir, utils, message, log, 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) ModelRole = enum.IntEnum('ModelRole', ['item'], start=Qt.UserRole)
@ -80,9 +81,9 @@ def download_dir():
ddir = directory ddir = directory
try: try:
os.makedirs(ddir) os.makedirs(ddir, exist_ok=True)
except FileExistsError: except OSError as e:
pass message.error("Failed to create download directory: {}".format(e))
return ddir return ddir
@ -134,11 +135,12 @@ def create_full_filename(basename, filename):
Return: Return:
The full absolute path, or None if filename creation was not possible. 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. # Remove chars which can't be encoded in the filename encoding.
# See https://github.com/qutebrowser/qutebrowser/issues/427 # See https://github.com/qutebrowser/qutebrowser/issues/427
encoding = sys.getfilesystemencoding() encoding = sys.getfilesystemencoding()
filename = utils.force_encoding(filename, encoding) filename = utils.force_encoding(filename, encoding)
basename = utils.force_encoding(basename, encoding)
if os.path.isabs(filename) and (os.path.isdir(filename) or if os.path.isabs(filename) and (os.path.isdir(filename) or
filename.endswith(os.sep)): filename.endswith(os.sep)):
# We got an absolute directory from the user, so we save it under # 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. url: The URL the download originated from.
parent: The parent of the question (a QObject). parent: The parent of the question (a QObject).
""" """
encoding = sys.getfilesystemencoding() suggested_filename = utils.sanitize_filename(suggested_filename)
suggested_filename = utils.force_encoding(suggested_filename, encoding)
q = usertypes.Question(parent) q = usertypes.Question(parent)
q.title = "Save file to:" q.title = "Save file to:"
@ -224,9 +225,6 @@ class _DownloadTarget:
"""Abstract base class for different download targets.""" """Abstract base class for different download targets."""
def __init__(self):
raise NotImplementedError
def suggested_filename(self): def suggested_filename(self):
"""Get the suggested filename for this download target.""" """Get the suggested filename for this download target."""
raise NotImplementedError raise NotImplementedError
@ -243,7 +241,6 @@ class FileDownloadTarget(_DownloadTarget):
""" """
def __init__(self, filename, force_overwrite=False): def __init__(self, filename, force_overwrite=False):
# pylint: disable=super-init-not-called
self.filename = filename self.filename = filename
self.force_overwrite = force_overwrite self.force_overwrite = force_overwrite
@ -263,7 +260,6 @@ class FileObjDownloadTarget(_DownloadTarget):
""" """
def __init__(self, fileobj): def __init__(self, fileobj):
# pylint: disable=super-init-not-called
self.fileobj = fileobj self.fileobj = fileobj
def suggested_filename(self): def suggested_filename(self):
@ -290,7 +286,6 @@ class OpenFileDownloadTarget(_DownloadTarget):
""" """
def __init__(self, cmdline=None): def __init__(self, cmdline=None):
# pylint: disable=super-init-not-called
self.cmdline = cmdline self.cmdline = cmdline
def suggested_filename(self): def suggested_filename(self):
@ -300,6 +295,17 @@ class OpenFileDownloadTarget(_DownloadTarget):
return 'temporary file' 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): class DownloadItemStats(QObject):
"""Statistics (bytes done, total bytes, time, etc.) about a download. """Statistics (bytes done, total bytes, time, etc.) about a download.
@ -405,6 +411,8 @@ class AbstractDownloadItem(QObject):
arg: The error message as string. arg: The error message as string.
remove_requested: Emitted when the removal of this download was remove_requested: Emitted when the removal of this download was
requested. requested.
pdfjs_requested: Emitted when PDF.js should be opened with the given
filename.
""" """
data_changed = pyqtSignal() data_changed = pyqtSignal()
@ -412,6 +420,7 @@ class AbstractDownloadItem(QObject):
error = pyqtSignal(str) error = pyqtSignal(str)
cancelled = pyqtSignal() cancelled = pyqtSignal()
remove_requested = pyqtSignal() remove_requested = pyqtSignal()
pdfjs_requested = pyqtSignal(str)
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
@ -692,9 +701,7 @@ class AbstractDownloadItem(QObject):
global last_used_directory global last_used_directory
try: try:
os.makedirs(os.path.dirname(self._filename)) os.makedirs(os.path.dirname(self._filename), exist_ok=True)
except FileExistsError:
pass
except OSError as e: except OSError as e:
self._die(e.strerror) self._die(e.strerror)
@ -732,6 +739,19 @@ class AbstractDownloadItem(QObject):
return return
self.open_file(cmdline) 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): def set_target(self, target):
"""Set the target for a given download. """Set the target for a given download.
@ -743,7 +763,7 @@ class AbstractDownloadItem(QObject):
elif isinstance(target, FileDownloadTarget): elif isinstance(target, FileDownloadTarget):
self._set_filename( self._set_filename(
target.filename, force_overwrite=target.force_overwrite) target.filename, force_overwrite=target.force_overwrite)
elif isinstance(target, OpenFileDownloadTarget): elif isinstance(target, (OpenFileDownloadTarget, PDFJSDownloadTarget)):
try: try:
fobj = temp_download_manager.get_tmpfile(self.basename) fobj = temp_download_manager.get_tmpfile(self.basename)
except OSError as exc: except OSError as exc:
@ -751,8 +771,15 @@ class AbstractDownloadItem(QObject):
message.error(msg) message.error(msg)
self.cancel() self.cancel()
return 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) self._set_tempfile(fobj)
else: # pragma: no cover else: # pragma: no cover
raise ValueError("Unsupported download target: {}".format(target)) raise ValueError("Unsupported download target: {}".format(target))
@ -799,6 +826,13 @@ class AbstractDownloadManager(QObject):
dl.stats.update_speed() dl.stats.update_speed()
self.data_changed.emit(-1) 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): def _init_item(self, download, auto_remove, suggested_filename):
"""Initialize a newly created DownloadItem.""" """Initialize a newly created DownloadItem."""
download.cancelled.connect(download.remove) download.cancelled.connect(download.remove)
@ -815,6 +849,8 @@ class AbstractDownloadManager(QObject):
download.data_changed.connect( download.data_changed.connect(
functools.partial(self._on_data_changed, download)) functools.partial(self._on_data_changed, download))
download.error.connect(self._on_error) download.error.connect(self._on_error)
download.pdfjs_requested.connect(self._on_pdfjs_requested)
download.basename = suggested_filename download.basename = suggested_filename
idx = len(self.downloads) idx = len(self.downloads)
download.index = idx + 1 # "Human readable" index download.index = idx + 1 # "Human readable" index
@ -1197,7 +1233,7 @@ class TempDownloadManager:
"directory") "directory")
self._tmpdir = None self._tmpdir = None
def _get_tmpdir(self): def get_tmpdir(self):
"""Return the temporary directory that is used for downloads. """Return the temporary directory that is used for downloads.
The directory is created lazily on first access. The directory is created lazily on first access.
@ -1223,13 +1259,12 @@ class TempDownloadManager:
Return: Return:
A tempfile.NamedTemporaryFile that should be used to save the file. A tempfile.NamedTemporaryFile that should be used to save the file.
""" """
tmpdir = self._get_tmpdir() tmpdir = self.get_tmpdir()
encoding = sys.getfilesystemencoding() suggested_name = utils.sanitize_filename(suggested_name)
suggested_name = utils.force_encoding(suggested_name, encoding)
# Make sure that the filename is not too long # Make sure that the filename is not too long
suggested_name = utils.elide_filename(suggested_name, 50) suggested_name = utils.elide_filename(suggested_name, 50)
fobj = tempfile.NamedTemporaryFile(dir=tmpdir.name, delete=False, fobj = tempfile.NamedTemporaryFile(dir=tmpdir.name, delete=False,
suffix=suggested_name) suffix='_' + suggested_name)
self.files.append(fobj) self.files.append(fobj)
return fobj return fobj

View File

@ -21,13 +21,13 @@
import functools import functools
import sip
from PyQt5.QtCore import pyqtSlot, QSize, Qt, QTimer from PyQt5.QtCore import pyqtSlot, QSize, Qt, QTimer
from PyQt5.QtWidgets import QListView, QSizePolicy, QMenu, QStyleFactory from PyQt5.QtWidgets import QListView, QSizePolicy, QMenu, QStyleFactory
from qutebrowser.browser import downloads from qutebrowser.browser import downloads
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.utils import qtutils, utils, objreg from qutebrowser.utils import qtutils, utils, objreg
from qutebrowser.qt import sip
def update_geometry(obj): def update_geometry(obj):

View File

@ -46,7 +46,8 @@ class GreasemonkeyScript:
"""Container class for userscripts, parses metadata blocks.""" """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._code = code
self.includes = [] self.includes = []
self.matches = [] self.matches = []
@ -58,6 +59,7 @@ class GreasemonkeyScript:
self.run_at = None self.run_at = None
self.script_meta = None self.script_meta = None
self.runs_on_sub_frames = True self.runs_on_sub_frames = True
self.jsworld = "main"
for name, value in properties: for name, value in properties:
if name == 'name': if name == 'name':
self.name = value self.name = value
@ -77,12 +79,22 @@ class GreasemonkeyScript:
self.runs_on_sub_frames = False self.runs_on_sub_frames = False
elif name == 'require': elif name == 'require':
self.requires.append(value) 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' HEADER_REGEX = r'// ==UserScript==|\n+// ==/UserScript==\n'
PROPS_REGEX = r'// @(?P<prop>[^\s]+)\s*(?P<val>.*)' PROPS_REGEX = r'// @(?P<prop>[^\s]+)\s*(?P<val>.*)'
@classmethod @classmethod
def parse(cls, source): def parse(cls, source, filename=None):
"""GreasemonkeyScript factory. """GreasemonkeyScript factory.
Takes a userscript source and returns a GreasemonkeyScript. Takes a userscript source and returns a GreasemonkeyScript.
@ -94,7 +106,11 @@ class GreasemonkeyScript:
_head, props, _code = matches _head, props, _code = matches
except ValueError: except ValueError:
props = "" 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 script.script_meta = props
if not script.includes and not script.matches: if not script.includes and not script.matches:
script.includes = ['*'] script.includes = ['*']
@ -118,7 +134,7 @@ class GreasemonkeyScript:
scriptName=javascript.string_escape( scriptName=javascript.string_escape(
"/".join([self.namespace or '', self.name])), "/".join([self.namespace or '', self.name])),
scriptInfo=self._meta_json(), scriptInfo=self._meta_json(),
scriptMeta=javascript.string_escape(self.script_meta), scriptMeta=javascript.string_escape(self.script_meta or ''),
scriptSource=self._code, scriptSource=self._code,
use_proxy=use_proxy) use_proxy=use_proxy)
@ -142,7 +158,7 @@ class GreasemonkeyScript:
@attr.s @attr.s
class MatchingScripts(object): class MatchingScripts:
"""All userscripts registered to run on a particular url.""" """All userscripts registered to run on a particular url."""
@ -231,8 +247,9 @@ class GreasemonkeyManager(QObject):
if not os.path.isfile(script_filename): if not os.path.isfile(script_filename):
continue continue
script_path = os.path.join(scripts_dir, script_filename) script_path = os.path.join(scripts_dir, script_filename)
with open(script_path, encoding='utf-8') as script_file: with open(script_path, encoding='utf-8-sig') as script_file:
script = GreasemonkeyScript.parse(script_file.read()) script = GreasemonkeyScript.parse(script_file.read(),
script_filename)
if not script.name: if not script.name:
script.name = script_filename script.name = script_filename
self.add_script(script, force) self.add_script(script, force)

View File

@ -32,7 +32,7 @@ import attr
from PyQt5.QtCore import pyqtSlot, QObject, Qt, QUrl from PyQt5.QtCore import pyqtSlot, QObject, Qt, QUrl
from PyQt5.QtWidgets import QLabel from PyQt5.QtWidgets import QLabel
from qutebrowser.config import config from qutebrowser.config import config, configexc
from qutebrowser.keyinput import modeman, modeparsers from qutebrowser.keyinput import modeman, modeparsers
from qutebrowser.browser import webelem from qutebrowser.browser import webelem
from qutebrowser.commands import userscripts, cmdexc, cmdutils, runners from qutebrowser.commands import userscripts, cmdexc, cmdutils, runners
@ -601,8 +601,12 @@ class HintManager(QObject):
return return
if elems is None: if elems is None:
message.error("There was an error while getting hint elements") message.error("Unknown error while getting hint elements.")
return return
elif isinstance(elems, webelem.Error):
message.error(str(elems))
return
if not elems: if not elems:
message.error("No elements found.") message.error("No elements found.")
return return
@ -637,9 +641,8 @@ class HintManager(QObject):
star_args_optional=True, maxsplit=2) star_args_optional=True, maxsplit=2)
@cmdutils.argument('win_id', win_id=True) @cmdutils.argument('win_id', win_id=True)
def start(self, # pylint: disable=keyword-arg-before-vararg def start(self, # pylint: disable=keyword-arg-before-vararg
group=webelem.Group.all, target=Target.normal, group='all', target=Target.normal, *args, win_id, mode=None,
*args, win_id, mode=None, add_history=False, rapid=False, add_history=False, rapid=False, first=False):
first=False):
"""Start hinting. """Start hinting.
Args: Args:
@ -658,6 +661,9 @@ class HintManager(QObject):
- `images`: Only images. - `images`: Only images.
- `inputs`: Only input fields. - `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. target: What to do with the selected element.
- `normal`: Open the link. - `normal`: Open the link.
@ -693,7 +699,7 @@ class HintManager(QObject):
- With `userscript`: The userscript to execute. Either store - With `userscript`: The userscript to execute. Either store
the userscript in the userscript in
`~/.local/share/qutebrowser/userscripts` `~/.local/share/qutebrowser/userscripts`
(or `$XDG_DATA_DIR`), or use an absolute (or `$XDG_DATA_HOME`), or use an absolute
path. path.
- With `fill`: The command to fill the statusbar with. - With `fill`: The command to fill the statusbar with.
`{hint-url}` will get replaced by the selected `{hint-url}` will get replaced by the selected
@ -724,15 +730,12 @@ class HintManager(QObject):
raise cmdexc.CommandError("Rapid hinting makes no sense with " raise cmdexc.CommandError("Rapid hinting makes no sense with "
"target {}!".format(name)) "target {}!".format(name))
if mode is None:
mode = config.val.hints.mode
self._check_args(target, *args) self._check_args(target, *args)
self._context = HintContext() self._context = HintContext()
self._context.tab = tab self._context.tab = tab
self._context.target = target self._context.target = target
self._context.rapid = rapid 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.add_history = add_history
self._context.first = first self._context.first = first
try: try:
@ -741,10 +744,28 @@ class HintManager(QObject):
raise cmdexc.CommandError("No URL set for this page yet!") raise cmdexc.CommandError("No URL set for this page yet!")
self._context.args = args self._context.args = args
self._context.group = group 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, self._context.tab.elements.find_css(selector, self._start_cb,
only_visible=True) 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): def current_mode(self):
"""Return the currently active hinting mode (or None otherwise).""" """Return the currently active hinting mode (or None otherwise)."""
if self._context is None: if self._context is None:

View File

@ -23,11 +23,12 @@ import os
import time import time
import contextlib 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.commands import cmdutils, cmdexc
from qutebrowser.utils import (utils, objreg, log, usertypes, message, from qutebrowser.utils import utils, objreg, log, usertypes, message, qtutils
debug, standarddir, qtutils)
from qutebrowser.misc import objects, sql from qutebrowser.misc import objects, sql
@ -35,6 +36,77 @@ from qutebrowser.misc import objects, sql
_USER_VERSION = 2 _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): class CompletionHistory(sql.SqlTable):
"""History which only has the newest entry for each URL.""" """History which only has the newest entry for each URL."""
@ -50,26 +122,46 @@ class CompletionHistory(sql.SqlTable):
class WebHistory(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 # All web history cleared
history_cleared = pyqtSignal() history_cleared = pyqtSignal()
# one url cleared # one url cleared
url_cleared = pyqtSignal(QUrl) 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'], super().__init__("History", ['url', 'title', 'atime', 'redirect'],
constraints={'url': 'NOT NULL', constraints={'url': 'NOT NULL',
'title': 'NOT NULL', 'title': 'NOT NULL',
'atime': 'NOT NULL', 'atime': 'NOT NULL',
'redirect': 'NOT NULL'}, 'redirect': 'NOT NULL'},
parent=parent) parent=parent)
self._progress = progress
self.completion = CompletionHistory(parent=self) self.completion = CompletionHistory(parent=self)
self.metainfo = CompletionMetaInfo(parent=self)
if sql.Query('pragma user_version').run().value() < _USER_VERSION: if sql.Query('pragma user_version').run().value() < _USER_VERSION:
self.completion.delete_all() self.completion.delete_all()
if self.metainfo['force_rebuild']:
self.completion.delete_all()
self.metainfo['force_rebuild'] = False
if not self.completion: if not self.completion:
# either the table is out-of-date or the user wiped it manually # either the table is out-of-date or the user wiped it manually
self._rebuild_completion() self._rebuild_completion()
self.create_index('HistoryIndex', 'url') self.create_index('HistoryIndex', 'url')
self.create_index('HistoryAtimeIndex', 'atime') self.create_index('HistoryAtimeIndex', 'atime')
self._contains_query = self.contains_query('url') self._contains_query = self.contains_query('url')
@ -87,21 +179,29 @@ class WebHistory(sql.SqlTable):
'ORDER BY atime desc ' 'ORDER BY atime desc '
'limit :limit offset :offset') 'limit :limit offset :offset')
config.instance.changed.connect(self._on_config_changed)
def __repr__(self): def __repr__(self):
return utils.get_repr(self, length=len(self)) return utils.get_repr(self, length=len(self))
def __contains__(self, url): def __contains__(self, url):
return self._contains_query.run(val=url).value() 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 @contextlib.contextmanager
def _handle_sql_errors(self): def _handle_sql_errors(self):
try: try:
yield yield
except sql.SqlError as e: except sql.SqlEnvironmentError as e:
if e.environmental: message.error("Failed to write history: {}".format(e.text()))
message.error("Failed to write history: {}".format(e.text()))
else: def _is_excluded(self, url):
raise """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): def _rebuild_completion(self):
data = {'url': [], 'title': [], 'last_atime': []} 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 ' q = sql.Query('SELECT url, title, max(atime) AS atime FROM History '
'WHERE NOT redirect and url NOT LIKE "qute://back%" ' 'WHERE NOT redirect and url NOT LIKE "qute://back%" '
'GROUP BY url ORDER BY atime asc') 'GROUP BY url ORDER BY atime asc')
for entry in q.run(): entries = list(q.run())
data['url'].append(self._format_completion_url(QUrl(entry.url)))
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['title'].append(entry.title)
data['last_atime'].append(entry.atime) data['last_atime'].append(entry.atime)
self._progress.finish()
self.completion.insert_batch(data, replace=True) self.completion.insert_batch(data, replace=True)
sql.Query('pragma user_version = {}'.format(_USER_VERSION)).run() sql.Query('pragma user_version = {}'.format(_USER_VERSION)).run()
@ -218,114 +331,15 @@ class WebHistory(sql.SqlTable):
'title': title, 'title': title,
'atime': atime, 'atime': atime,
'redirect': redirect}) '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): if redirect or self._is_excluded(url):
"""Parse a history line like '12345 http://example.com title'.""" return
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")
# http://xn--pple-43d.com/ with self.completion.insert({
# https://bugreports.qt.io/browse/QTBUG-60364 'url': self._format_completion_url(url),
if url in ['http://.com/', 'https://.com/', 'title': title,
'http://www..com/', 'https://www..com/']: 'last_atime': atime
return None }, replace=True)
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)
def _format_url(self, url): def _format_url(self, url):
return url.toString(QUrl.RemovePassword | QUrl.FullyEncoded) return url.toString(QUrl.RemovePassword | QUrl.FullyEncoded)
@ -360,7 +374,8 @@ def init(parent=None):
Args: Args:
parent: The parent to use for WebHistory. parent: The parent to use for WebHistory.
""" """
history = WebHistory(parent=parent) progress = HistoryProgress()
history = WebHistory(progress=progress, parent=parent)
objreg.register('web-history', history) objreg.register('web-history', history)
if objects.backend == usertypes.Backend.QtWebKit: # pragma: no cover if objects.backend == usertypes.Backend.QtWebKit: # pragma: no cover

View File

@ -22,7 +22,7 @@
from PyQt5.QtCore import QObject, QEvent, Qt, QTimer from PyQt5.QtCore import QObject, QEvent, Qt, QTimer
from qutebrowser.config import config 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 from qutebrowser.keyinput import modeman
@ -40,11 +40,12 @@ class ChildEventFilter(QObject):
_widget: The widget expected to send out childEvents. _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) super().__init__(parent)
self._filter = eventfilter self._filter = eventfilter
assert widget is not None assert widget is not None
self._widget = widget self._widget = widget
self._win_id = win_id
def eventFilter(self, obj, event): def eventFilter(self, obj, event):
"""Act on ChildAdded events.""" """Act on ChildAdded events."""
@ -57,7 +58,22 @@ class ChildEventFilter(QObject):
if qtutils.version_check('5.11', compiled=False, exact=True): if qtutils.version_check('5.11', compiled=False, exact=True):
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-68076 # 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: elif event.type() == QEvent.ChildRemoved:
child = event.child() child = event.child()
log.mouse.debug("{}: removed child {}".format(obj, child)) log.mouse.debug("{}: removed child {}".format(obj, child))
@ -100,9 +116,13 @@ class MouseEventFilter(QObject):
self._ignore_wheel_event = True 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: if e.button() != Qt.NoButton:
self._tab.elements.find_at_pos(e.pos(), self._tab.elements.find_at_pos(pos, self._mousepress_insertmode_cb)
self._mousepress_insertmode_cb)
return False return False
@ -125,6 +145,10 @@ class MouseEventFilter(QObject):
return True return True
if e.modifiers() & Qt.ControlModifier: 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 divider = config.val.zoom.mouse_divider
if divider == 0: if divider == 0:
return False return False

View File

@ -117,7 +117,10 @@ def prevnext(*, browsertab, win_id, baseurl, prev=False,
""" """
def _prevnext_cb(elems): def _prevnext_cb(elems):
if elems is None: 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 return
elem = _find_prevnext(prev, elems) elem = _find_prevnext(prev, elems)
@ -147,5 +150,9 @@ def prevnext(*, browsertab, win_id, baseurl, prev=False,
else: else:
browsertab.openurl(url) browsertab.openurl(url)
browsertab.elements.find_css(webelem.SELECTORS[webelem.Group.links], try:
_prevnext_cb) link_selector = webelem.css_selector('links', baseurl)
except webelem.Error as e:
raise Error(str(e))
browsertab.elements.find_css(link_selector, _prevnext_cb)

View File

@ -22,9 +22,12 @@
import os 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): class PDFJSNotFound(Exception):
@ -41,73 +44,73 @@ class PDFJSNotFound(Exception):
super().__init__(message) super().__init__(message)
def generate_pdfjs_page(url): def generate_pdfjs_page(filename, url):
"""Return the html content of a page that displays url with pdfjs. """Return the html content of a page that displays a file with pdfjs.
Returns a string. Returns a string.
Args: 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') if not is_available():
script = _generate_pdfjs_script(url) pdfjs_dir = os.path.join(standarddir.data(), 'pdfjs')
html_page = viewer.replace('</body>', return jinja.render('no_pdfjs.html',
'</body><script>{}</script>'.format(script)) url=url.toDisplayString(),
return html_page 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. """Generate the script that shows the pdf with pdf.js.
Args: Args:
url: The url of the pdf page as QUrl. filename: The name of the file to open.
""" """
return ( url = QUrl('qute://pdfjs/file')
'document.addEventListener("DOMContentLoaded", function() {{\n' url_query = QUrlQuery()
' PDFJS.verbosity = PDFJS.VERBOSITY_LEVELS.info;\n' url_query.addQueryItem('filename', filename)
' (window.PDFView || window.PDFViewerApplication).open("{url}");\n' url.setQuery(url_query)
'}});\n'
).format(url=javascript.string_escape(url.toString(QUrl.FullyEncoded)))
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): const viewer = window.PDFView || window.PDFViewerApplication;
"""Take an html page and replace each relative URL with an absolute. viewer.open({{ url }});
});
This is specialized for pdf.js files and not a general purpose function. """).render(
url=javascript.to_js(url.toString(QUrl.FullyEncoded)),
Args: # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-70420
asset: js file or html page as string. disable_create_object_url=(
""" not qtutils.version_check('5.12') and
new_urls = [ not qtutils.version_check('5.7.1', exact=True, compiled=False) and
('viewer.css', 'qute://pdfjs/web/viewer.css'), objects.backend == usertypes.Backend.QtWebEngine))
('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/'),
]
def get_pdfjs_res_and_path(path): def get_pdfjs_res_and_path(path):
@ -124,11 +127,25 @@ def get_pdfjs_res_and_path(path):
content = None content = None
file_path = 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 # First try a system wide installation
# System installations might strip off the 'build/' or 'web/' prefixes. # System installations might strip off the 'build/' or 'web/' prefixes.
# qute expects them, so we need to adjust for it. # qute expects them, so we need to adjust for it.
names_to_try = [path, _remove_prefix(path)] 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) content, file_path = _read_from_system(system_path, names_to_try)
if content is not None: if content is not None:
break break
@ -140,14 +157,11 @@ def get_pdfjs_res_and_path(path):
content = utils.read_file(res_path, binary=True) content = utils.read_file(res_path, binary=True)
except FileNotFoundError: except FileNotFoundError:
raise PDFJSNotFound(path) from None 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: return content, file_path
# 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)
def get_pdfjs_res(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) full_path = os.path.join(system_path, name)
with open(full_path, 'rb') as f: with open(full_path, 'rb') as f:
return (f.read(), full_path) 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 continue
return (None, None) return (None, None)
@ -206,3 +223,22 @@ def is_available():
return False return False
else: else:
return True 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

View File

@ -29,7 +29,7 @@ from PyQt5.QtCore import pyqtSlot, pyqtSignal, QTimer
from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply
from qutebrowser.config import config 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 import downloads
from qutebrowser.browser.webkit import http from qutebrowser.browser.webkit import http
from qutebrowser.browser.webkit.network import networkmanager from qutebrowser.browser.webkit.network import networkmanager
@ -307,7 +307,14 @@ class DownloadItem(downloads.AbstractDownloadItem):
"""Handle QNetworkReply errors.""" """Handle QNetworkReply errors."""
if code == QNetworkReply.OperationCanceledError: if code == QNetworkReply.OperationCanceledError:
return 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() @pyqtSlot()
def _on_read_timer_timeout(self): def _on_read_timer_timeout(self):

View File

@ -24,62 +24,72 @@ Module attributes:
_HANDLERS: The handlers registered via decorators. _HANDLERS: The handlers registered via decorators.
""" """
import html
import json import json
import os import os
import time import time
import textwrap import textwrap
import mimetypes
import urllib import urllib
import collections import collections
import base64
import pkg_resources try:
import sip import secrets
from PyQt5.QtCore import QUrlQuery, QUrl except ImportError:
# New in Python 3.6
secrets = None
from PyQt5.QtCore import QUrlQuery, QUrl, qVersion
import qutebrowser import qutebrowser
from qutebrowser.browser import pdfjs, downloads
from qutebrowser.config import config, configdata, configexc, configdiff from qutebrowser.config import config, configdata, configexc, configdiff
from qutebrowser.utils import (version, utils, jinja, log, message, docutils, from qutebrowser.utils import (version, utils, jinja, log, message, docutils,
objreg, urlutils) objreg, urlutils)
from qutebrowser.misc import objects from qutebrowser.qt import sip
pyeval_output = ":pyeval was never called" pyeval_output = ":pyeval was never called"
spawn_output = ":spawn was never called" spawn_output = ":spawn was never called"
csrf_token = None
_HANDLERS = {} _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 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 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 pass
networkreply.ErrorNetworkReply.
Attributes:
errorstring: Error string to print.
error: Numerical error value.
"""
def __init__(self, errorstring, error): class UrlInvalidError(Error):
self.errorstring = errorstring
self.error = error """Raised when an invalid URL was opened."""
super().__init__(errorstring)
pass
class RequestDeniedError(Error):
"""Raised when the request is forbidden."""
pass
class Redirect(Exception): class Redirect(Exception):
@ -101,12 +111,10 @@ class add_handler: # noqa: N801,N806 pylint: disable=invalid-name
Attributes: Attributes:
_name: The 'foo' part of qute://foo _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._name = name
self._backend = backend
self._function = None self._function = None
def __call__(self, function): def __call__(self, function):
@ -116,19 +124,7 @@ class add_handler: # noqa: N801,N806 pylint: disable=invalid-name
def wrapper(self, *args, **kwargs): def wrapper(self, *args, **kwargs):
"""Call the underlying function.""" """Call the underlying function."""
if self._backend is not None and objects.backend != self._backend: return self._function(*args, **kwargs)
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
def data_for_url(url): def data_for_url(url):
@ -170,15 +166,13 @@ def data_for_url(url):
try: try:
handler = _HANDLERS[host] handler = _HANDLERS[host]
except KeyError: except KeyError:
raise NoHandlerFound(url) raise NotFoundError("No handler found for {}".format(
url.toDisplayString()))
try: try:
mimetype, data = handler(url) mimetype, data = handler(url)
except OSError as e: except OSError as e:
# FIXME:qtwebengine how to handle this? raise SchemeOSError(e)
raise QuteSchemeOSError(e)
except QuteSchemeError:
raise
assert mimetype is not None, url assert mimetype is not None, url
if mimetype == 'text/html' and isinstance(data, str): if mimetype == 'text/html' and isinstance(data, str):
@ -196,11 +190,11 @@ def qute_bookmarks(_url):
quickmarks = sorted(objreg.get('quickmark-manager').marks.items(), quickmarks = sorted(objreg.get('quickmark-manager').marks.items(),
key=lambda x: x[0]) # Sort by name key=lambda x: x[0]) # Sort by name
html = jinja.render('bookmarks.html', src = jinja.render('bookmarks.html',
title='Bookmarks', title='Bookmarks',
bookmarks=bookmarks, bookmarks=bookmarks,
quickmarks=quickmarks) quickmarks=quickmarks)
return 'text/html', html return 'text/html', src
@add_handler('tabs') @add_handler('tabs')
@ -218,10 +212,10 @@ def qute_tabs(_url):
urlstr = tab.url().toDisplayString() urlstr = tab.url().toDisplayString()
tabs[str(win_id)].append((tab.title(), urlstr)) tabs[str(win_id)].append((tab.title(), urlstr))
html = jinja.render('tabs.html', src = jinja.render('tabs.html',
title='Tabs', title='Tabs',
tab_list_by_window=tabs) tab_list_by_window=tabs)
return 'text/html', html return 'text/html', src
def history_data(start_time, offset=None): 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 end_time = start_time - 24*60*60
entries = hist.entries_between(end_time, start_time) entries = hist.entries_between(end_time, start_time)
return [{"url": e.url, "title": e.title or e.url, "time": e.atime} return [{"url": e.url,
for e in entries] "title": html.escape(e.title) or html.escape(e.url),
"time": e.atime} for e in entries]
@add_handler('history') @add_handler('history')
@ -252,14 +247,14 @@ def qute_history(url):
try: try:
offset = QUrlQuery(url).queryItemValue("offset") offset = QUrlQuery(url).queryItemValue("offset")
offset = int(offset) if offset else None offset = int(offset) if offset else None
except ValueError as e: except ValueError:
raise QuteSchemeError("Query parameter offset is invalid", e) raise UrlInvalidError("Query parameter offset is invalid")
# Use start_time in query or current time. # Use start_time in query or current time.
try: try:
start_time = QUrlQuery(url).queryItemValue("start_time") start_time = QUrlQuery(url).queryItemValue("start_time")
start_time = float(start_time) if start_time else time.time() start_time = float(start_time) if start_time else time.time()
except ValueError as e: except ValueError:
raise QuteSchemeError("Query parameter start_time is invalid", e) raise UrlInvalidError("Query parameter start_time is invalid")
return 'text/html', json.dumps(history_data(start_time, offset)) return 'text/html', json.dumps(history_data(start_time, offset))
else: else:
@ -281,31 +276,31 @@ def qute_javascript(url):
path = "javascript" + os.sep.join(path.split('/')) path = "javascript" + os.sep.join(path.split('/'))
return 'text/html', utils.read_file(path, binary=False) return 'text/html', utils.read_file(path, binary=False)
else: else:
raise QuteSchemeError("No file specified", ValueError()) raise UrlInvalidError("No file specified")
@add_handler('pyeval') @add_handler('pyeval')
def qute_pyeval(_url): def qute_pyeval(_url):
"""Handler for qute://pyeval.""" """Handler for qute://pyeval."""
html = jinja.render('pre.html', title='pyeval', content=pyeval_output) src = jinja.render('pre.html', title='pyeval', content=pyeval_output)
return 'text/html', html return 'text/html', src
@add_handler('spawn-output') @add_handler('spawn-output')
def qute_spawn_output(_url): def qute_spawn_output(_url):
"""Handler for qute://spawn-output.""" """Handler for qute://spawn-output."""
html = jinja.render('pre.html', title='spawn output', content=spawn_output) src = jinja.render('pre.html', title='spawn output', content=spawn_output)
return 'text/html', html return 'text/html', src
@add_handler('version') @add_handler('version')
@add_handler('verizon') @add_handler('verizon')
def qute_version(_url): def qute_version(_url):
"""Handler for qute://version.""" """Handler for qute://version."""
html = jinja.render('version.html', title='Version info', src = jinja.render('version.html', title='Version info',
version=version.version(), version=version.version(),
copyright=qutebrowser.__copyright__) copyright=qutebrowser.__copyright__)
return 'text/html', html return 'text/html', src
@add_handler('plainlog') @add_handler('plainlog')
@ -323,8 +318,8 @@ def qute_plainlog(url):
if not level: if not level:
level = 'vdebug' level = 'vdebug'
text = log.ram_handler.dump_log(html=False, level=level) text = log.ram_handler.dump_log(html=False, level=level)
html = jinja.render('pre.html', title='log', content=text) src = jinja.render('pre.html', title='log', content=text)
return 'text/html', html return 'text/html', src
@add_handler('log') @add_handler('log')
@ -343,8 +338,8 @@ def qute_log(url):
level = 'vdebug' level = 'vdebug'
html_log = log.ram_handler.dump_log(html=True, level=level) html_log = log.ram_handler.dump_log(html=True, level=level)
html = jinja.render('log.html', title='log', content=html_log) src = jinja.render('log.html', title='log', content=html_log)
return 'text/html', html return 'text/html', src
@add_handler('gpl') @add_handler('gpl')
@ -353,6 +348,23 @@ def qute_gpl(_url):
return 'text/html', utils.read_file('html/license.html') 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') @add_handler('help')
def qute_help(url): def qute_help(url):
"""Handler for qute://help.""" """Handler for qute://help."""
@ -370,23 +382,14 @@ def qute_help(url):
try: try:
bdata = utils.read_file(path, binary=True) bdata = utils.read_file(path, binary=True)
except OSError as e: except OSError as e:
raise QuteSchemeOSError(e) raise SchemeOSError(e)
mimetype, _encoding = mimetypes.guess_type(urlpath) mimetype = utils.guess_mimetype(urlpath)
assert mimetype is not None, url
return mimetype, bdata return mimetype, bdata
try: try:
data = utils.read_file(path) data = utils.read_file(path)
except OSError: except OSError:
# No .html around, let's see if we find the asciidoc asciidoc = _asciidoc_fallback_path(path)
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
if asciidoc is None: if asciidoc is None:
raise raise
@ -412,17 +415,6 @@ def qute_help(url):
return 'text/html', data 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): def _qute_settings_set(url):
"""Handler for qute://settings/set.""" """Handler for qute://settings/set."""
query = QUrlQuery(url) query = QUrlQuery(url)
@ -447,13 +439,29 @@ def _qute_settings_set(url):
@add_handler('settings') @add_handler('settings')
def qute_settings(url): def qute_settings(url):
"""Handler for qute://settings. View/change qute configuration.""" """Handler for qute://settings. View/change qute configuration."""
global csrf_token
if url.path() == '/set': 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) return _qute_settings_set(url)
html = jinja.render('settings.html', title='settings', # Requests to qute://settings/set should only be allowed from
configdata=configdata, # qute://settings. As an additional security precaution, we generate a CSRF
confget=config.instance.get_str) # token to use here.
return 'text/html', html 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') @add_handler('bindings')
@ -467,9 +475,9 @@ def qute_bindings(_url):
for mode in modes: for mode in modes:
bindings[mode] = config.key_instance.get_bindings_for(mode) bindings[mode] = config.key_instance.get_bindings_for(mode)
html = jinja.render('bindings.html', title='Bindings', src = jinja.render('bindings.html', title='Bindings',
bindings=bindings) bindings=bindings)
return 'text/html', html return 'text/html', src
@add_handler('back') @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. Simple page to free ram / lazy load a site, goes back on focusing the tab.
""" """
html = jinja.render( src = jinja.render(
'back.html', 'back.html',
title='Suspended: ' + urllib.parse.unquote(url.fragment())) title='Suspended: ' + urllib.parse.unquote(url.fragment()))
return 'text/html', html return 'text/html', src
@add_handler('configdiff') @add_handler('configdiff')
@ -504,3 +512,59 @@ def qute_pastebin_version(_url):
"""Handler that pastebins the version string.""" """Handler that pastebins the version string."""
version.pastebin_version() version.pastebin_version()
return 'text/plain', b'Paste called.' 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

View File

@ -34,21 +34,22 @@ class CallSuper(Exception):
"""Raised when the caller should call the superclass instead.""" """Raised when the caller should call the superclass instead."""
def custom_headers(): def custom_headers(url):
"""Get the combined custom headers.""" """Get the combined custom headers."""
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: if dnt_config is not None:
dnt = b'1' if dnt_config else b'0' dnt = b'1' if dnt_config else b'0'
headers[b'DNT'] = dnt headers[b'DNT'] = dnt
headers[b'X-Do-Not-Track'] = 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(): for header, value in conf_headers.items():
headers[header.encode('ascii')] = value.encode('ascii') 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: if accept_language is not None:
headers[b'Accept-Language'] = accept_language.encode('ascii') headers[b'Accept-Language'] = accept_language.encode('ascii')
@ -156,7 +157,7 @@ def ignore_certificate_errors(url, errors, abort_on):
Return: Return:
True if the error should be ignored, False otherwise. 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( log.webview.debug("Certificate errors {!r}, strict {}".format(
errors, ssl_strict)) 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), The Question object if a question was asked (and blocking=False),
None otherwise. None otherwise.
""" """
config_val = config.instance.get(option) config_val = config.instance.get(option, url=url)
if config_val == 'ask': if config_val == 'ask':
if url.isValid(): if url.isValid():
urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded) 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) 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. """Get the combined user-stylesheet.
If `url` is given and there's no overridden stylesheet, return 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: with open(filename, 'r', encoding='utf-8') as f:
css += f.read() 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; }' css += '\nhtml > ::-webkit-scrollbar { width: 0px; height: 0px; }'
return css return css
@ -319,10 +321,10 @@ def netrc_authentication(url, authenticator):
(user, _account, password) = authenticators (user, _account, password) = authenticators
except FileNotFoundError: except FileNotFoundError:
log.misc.debug("No .netrc file found") log.misc.debug("No .netrc file found")
except OSError: except OSError as e:
log.misc.exception("Unable to read the netrc file") log.misc.exception("Unable to read the netrc file: {}".format(e))
except netrc.NetrcParseError: except netrc.NetrcParseError as e:
log.misc.exception("Error when parsing the netrc file") log.misc.exception("Error when parsing the netrc file: {}".format(e))
if user is None: if user is None:
return False return False

View File

@ -240,8 +240,7 @@ class BookmarkManager(UrlMarkManager):
def _init_lineparser(self): def _init_lineparser(self):
bookmarks_directory = os.path.join(standarddir.config(), 'bookmarks') bookmarks_directory = os.path.join(standarddir.config(), 'bookmarks')
if not os.path.isdir(bookmarks_directory): os.makedirs(bookmarks_directory, exist_ok=True)
os.makedirs(bookmarks_directory)
bookmarks_subdir = os.path.join('bookmarks', 'urls') bookmarks_subdir = os.path.join('bookmarks', 'urls')
self._lineparser = lineparser.LineParser( self._lineparser = lineparser.LineParser(

View File

@ -17,14 +17,8 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. # 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 import collections.abc
from PyQt5.QtCore import QUrl, Qt, QEvent, QTimer 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 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): class Error(Exception):
"""Base class for WebElement errors.""" """Base class for WebElement errors."""
@ -69,6 +44,18 @@ class OrphanedError(Error):
pass 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): class AbstractWebElement(collections.abc.MutableMapping):
"""A wrapper around QtWebKit/QtWebEngine web element. """A wrapper around QtWebKit/QtWebEngine web element.
@ -139,6 +126,18 @@ class AbstractWebElement(collections.abc.MutableMapping):
"""Set the element value.""" """Set the element value."""
raise NotImplementedError 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): def insert_text(self, text):
"""Insert the given text into the element.""" """Insert the given text into the element."""
raise NotImplementedError raise NotImplementedError

View File

@ -28,6 +28,10 @@ class CertificateErrorWrapper(usertypes.AbstractCertificateErrorWrapper):
"""A wrapper over a QWebEngineCertificateError.""" """A wrapper over a QWebEngineCertificateError."""
def __init__(self, error):
super().__init__(error)
self.ignore = False
def __str__(self): def __str__(self):
return self._error.errorDescription() return self._error.errorDescription()
@ -37,5 +41,8 @@ class CertificateErrorWrapper(usertypes.AbstractCertificateErrorWrapper):
self._error.error()), self._error.error()),
string=str(self)) string=str(self))
def url(self):
return self._error.url()
def is_overridable(self): def is_overridable(self):
return self._error.isOverridable() return self._error.isOverridable()

View File

@ -19,6 +19,7 @@
"""A request interceptor taking care of adblocking and custom headers.""" """A request interceptor taking care of adblocking and custom headers."""
from PyQt5.QtCore import QUrl
from PyQt5.QtWebEngineCore import (QWebEngineUrlRequestInterceptor, from PyQt5.QtWebEngineCore import (QWebEngineUrlRequestInterceptor,
QWebEngineUrlRequestInfo) QWebEngineUrlRequestInfo)
@ -68,15 +69,29 @@ class RequestInterceptor(QWebEngineUrlRequestInterceptor):
info.firstPartyUrl().toDisplayString(), info.firstPartyUrl().toDisplayString(),
resource_type, navigation_type)) 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? # 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( log.webview.info("Request to {} blocked by host blocker.".format(
info.requestUrl().host())) url.host()))
info.block(True) info.block(True)
for header, value in shared.custom_headers(): for header, value in shared.custom_headers(url=url):
info.setHttpHeader(header, value) 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: if user_agent is not None:
info.setHttpHeader(b'User-Agent', user_agent.encode('ascii')) info.setHttpHeader(b'User-Agent', user_agent.encode('ascii'))

View File

@ -21,10 +21,12 @@
import glob import glob
import os import os
import os.path
import re import re
import shutil
from PyQt5.QtCore import QLibraryInfo 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") 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('-')) 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.""" """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') return os.path.join(datapath, 'qtwebengine_dictionaries')
@ -73,3 +78,16 @@ def local_filename(code):
""" """
all_installed = local_files(code) all_installed = local_files(code)
return os.path.splitext(all_installed[0])[0] if all_installed else None 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")

View File

@ -27,7 +27,7 @@ import functools
from PyQt5.QtCore import pyqtSlot, Qt from PyQt5.QtCore import pyqtSlot, Qt
from PyQt5.QtWebEngineWidgets import QWebEngineDownloadItem 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 from qutebrowser.utils import debug, usertypes, message, log, qtutils
@ -212,15 +212,19 @@ class DownloadManager(downloads.AbstractDownloadManager):
def handle_download(self, qt_item): def handle_download(self, qt_item):
"""Start a download coming from a QWebEngineProfile.""" """Start a download coming from a QWebEngineProfile."""
suggested_filename = _get_suggested_filename(qt_item.path()) suggested_filename = _get_suggested_filename(qt_item.path())
use_pdfjs = pdfjs.should_use_pdfjs(qt_item.mimeType(), qt_item.url())
download = DownloadItem(qt_item) download = DownloadItem(qt_item)
self._init_item(download, auto_remove=False, self._init_item(download, auto_remove=use_pdfjs,
suggested_filename=suggested_filename) suggested_filename=suggested_filename)
if self._mhtml_target is not None: if self._mhtml_target is not None:
download.set_target(self._mhtml_target) download.set_target(self._mhtml_target)
self._mhtml_target = None self._mhtml_target = None
return return
if use_pdfjs:
download.set_target(downloads.PDFJSDownloadTarget())
return
filename = downloads.immediate_download_path() filename = downloads.immediate_download_path()
if filename is not None: if filename is not None:

View File

@ -135,6 +135,10 @@ class WebEngineElement(webelem.AbstractWebElement):
def set_value(self, value): def set_value(self, value):
self._js_call('set_value', 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): def caret_position(self):
"""Get the text caret position for the current element. """Get the text caret position for the current element.
@ -203,6 +207,8 @@ class WebEngineElement(webelem.AbstractWebElement):
url = self.resolve_url(baseurl) url = self.resolve_url(baseurl)
if url is None: if url is None:
return True return True
if baseurl.scheme() == url.scheme(): # e.g. a qute:// link
return False
return url.scheme() not in urlutils.WEBENGINE_SCHEMES return url.scheme() not in urlutils.WEBENGINE_SCHEMES
def _click_editable(self, click_target): def _click_editable(self, click_target):

View File

@ -19,7 +19,7 @@
"""QtWebEngine specific qute://* handlers and glue code.""" """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, from PyQt5.QtWebEngineCore import (QWebEngineUrlSchemeHandler,
QWebEngineUrlRequestJob) QWebEngineUrlRequestJob)
@ -37,6 +37,38 @@ class QuteSchemeHandler(QWebEngineUrlSchemeHandler):
if qtutils.version_check('5.11', compiled=False): if qtutils.version_check('5.11', compiled=False):
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-63378 # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-63378
profile.installUrlSchemeHandler(b'chrome-error', self) 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): def requestStarted(self, job):
"""Handle a request for a qute: scheme. """Handle a request for a qute: scheme.
@ -49,28 +81,40 @@ class QuteSchemeHandler(QWebEngineUrlSchemeHandler):
""" """
url = job.requestUrl() 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 # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-63378
job.fail(QWebEngineUrlRequestJob.UrlInvalid) job.fail(QWebEngineUrlRequestJob.UrlInvalid)
return 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' assert url.scheme() == 'qute'
log.misc.debug("Got request for {}".format(url.toDisplayString())) log.misc.debug("Got request for {}".format(url.toDisplayString()))
try: try:
mimetype, data = qutescheme.data_for_url(url) mimetype, data = qutescheme.data_for_url(url)
except qutescheme.NoHandlerFound: except qutescheme.Error as e:
log.misc.debug("No handler found for {}".format( errors = {
url.toDisplayString())) qutescheme.NotFoundError:
job.fail(QWebEngineUrlRequestJob.UrlNotFound) QWebEngineUrlRequestJob.UrlNotFound,
except qutescheme.QuteSchemeOSError: qutescheme.UrlInvalidError:
# FIXME:qtwebengine how do we show a better error here? QWebEngineUrlRequestJob.UrlInvalid,
log.misc.exception("OSError while handling qute://* URL") qutescheme.RequestDeniedError:
job.fail(QWebEngineUrlRequestJob.UrlNotFound) QWebEngineUrlRequestJob.RequestDenied,
except qutescheme.QuteSchemeError: qutescheme.SchemeOSError:
# FIXME:qtwebengine how do we show a better error here? QWebEngineUrlRequestJob.UrlNotFound,
log.misc.exception("Error while handling qute://* URL") qutescheme.Error:
job.fail(QWebEngineUrlRequestJob.RequestFailed) QWebEngineUrlRequestJob.RequestFailed,
}
exctype = type(e)
log.misc.error("{} while handling qute://* URL".format(
exctype.__name__))
job.fail(errors[exctype])
except qutescheme.Redirect as e: except qutescheme.Redirect as e:
qtutils.ensure_valid(e.url) qtutils.ensure_valid(e.url)
job.redirect(e.url) job.redirect(e.url)

View File

@ -298,6 +298,8 @@ def init(args):
not hasattr(QWebEnginePage, 'setInspectedPage')): # only Qt < 5.11 not hasattr(QWebEnginePage, 'setInspectedPage')): # only Qt < 5.11
os.environ['QTWEBENGINE_REMOTE_DEBUGGING'] = str(utils.random_port()) os.environ['QTWEBENGINE_REMOTE_DEBUGGING'] = str(utils.random_port())
spell.init()
_init_profiles() _init_profiles()
config.instance.changed.connect(_update_settings) config.instance.changed.connect(_update_settings)

View File

@ -21,27 +21,26 @@
import math import math
import functools import functools
import sys
import re import re
import html as html_utils import html as html_utils
import sip
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, Qt, QEvent, QPoint, QPointF, 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.QtGui import QKeyEvent, QIcon
from PyQt5.QtNetwork import QAuthenticator from PyQt5.QtNetwork import QAuthenticator
from PyQt5.QtWidgets import QApplication from PyQt5.QtWidgets import QApplication
from PyQt5.QtWebEngineWidgets import QWebEnginePage, QWebEngineScript from PyQt5.QtWebEngineWidgets import QWebEnginePage, QWebEngineScript
from qutebrowser.config import configdata, config, configutils 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, from qutebrowser.browser.webengine import (webview, webengineelem, tabhistory,
interceptor, webenginequtescheme, interceptor, webenginequtescheme,
cookies, webenginedownloads, cookies, webenginedownloads,
webenginesettings) webenginesettings, certificateerror)
from qutebrowser.misc import miscwidgets from qutebrowser.misc import miscwidgets
from qutebrowser.utils import (usertypes, qtutils, log, javascript, utils, from qutebrowser.utils import (usertypes, qtutils, log, javascript, utils,
message, objreg, jinja, debug) message, objreg, jinja, debug)
from qutebrowser.qt import sip
_qute_scheme_handler = None _qute_scheme_handler = None
@ -163,8 +162,8 @@ class WebEngineSearch(browsertab.AbstractSearch):
back yet. back yet.
""" """
def __init__(self, parent=None): def __init__(self, tab, parent=None):
super().__init__(parent) super().__init__(tab, parent)
self._flags = QWebEnginePage.FindFlags(0) self._flags = QWebEnginePage.FindFlags(0)
self._pending_searches = 0 self._pending_searches = 0
@ -184,6 +183,13 @@ class WebEngineSearch(browsertab.AbstractSearch):
self._pending_searches)) self._pending_searches))
return 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" found_text = 'found' if found else "didn't find"
if flags: if flags:
flag_text = 'with flags {}'.format(debug.qflags_key( flag_text = 'with flags {}'.format(debug.qflags_key(
@ -192,8 +198,11 @@ class WebEngineSearch(browsertab.AbstractSearch):
flag_text = '' flag_text = ''
log.webview.debug(' '.join([caller, found_text, text, flag_text]) log.webview.debug(' '.join([caller, found_text, text, flag_text])
.strip()) .strip())
if callback is not None: if callback is not None:
callback(found) callback(found)
self.finished.emit(found)
self._widget.findText(text, flags, wrapped_callback) self._widget.findText(text, flags, wrapped_callback)
def search(self, text, *, ignore_case='never', reverse=False, 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') self._find(text, self._flags, result_cb, 'search')
def clear(self): def clear(self):
if self.search_displayed:
self.cleared.emit()
self.search_displayed = False self.search_displayed = False
self._widget.findText('') self._widget.findText('')
@ -234,6 +245,15 @@ class WebEngineCaret(browsertab.AbstractCaret):
"""QtWebEngine implementations related to moving the cursor/selection.""" """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) @pyqtSlot(usertypes.KeyMode)
def _on_mode_entered(self, mode): def _on_mode_entered(self, mode):
if mode != usertypes.KeyMode.caret: if mode != usertypes.KeyMode.caret:
@ -246,9 +266,9 @@ class WebEngineCaret(browsertab.AbstractCaret):
self._tab.search.clear() self._tab.search.clear()
self._tab.run_js_async( self._tab.run_js_async(
javascript.assemble('caret', javascript.assemble('caret', 'setFlags', self._flags()))
'setPlatform', sys.platform, qVersion()))
self._js_call('setInitialCursor', self._selection_cb) self._js_call('setInitialCursor', callback=self._selection_cb)
def _selection_cb(self, enabled): def _selection_cb(self, enabled):
"""Emit selection_toggled based on setInitialCursor.""" """Emit selection_toggled based on setInitialCursor."""
@ -266,32 +286,25 @@ class WebEngineCaret(browsertab.AbstractCaret):
self._js_call('disableCaret') self._js_call('disableCaret')
def move_to_next_line(self, count=1): def move_to_next_line(self, count=1):
for _ in range(count): self._js_call('moveDown', count)
self._js_call('moveDown')
def move_to_prev_line(self, count=1): def move_to_prev_line(self, count=1):
for _ in range(count): self._js_call('moveUp', count)
self._js_call('moveUp')
def move_to_next_char(self, count=1): def move_to_next_char(self, count=1):
for _ in range(count): self._js_call('moveRight', count)
self._js_call('moveRight')
def move_to_prev_char(self, count=1): def move_to_prev_char(self, count=1):
for _ in range(count): self._js_call('moveLeft', count)
self._js_call('moveLeft')
def move_to_end_of_word(self, count=1): def move_to_end_of_word(self, count=1):
for _ in range(count): self._js_call('moveToEndOfWord', count)
self._js_call('moveToEndOfWord')
def move_to_next_word(self, count=1): def move_to_next_word(self, count=1):
for _ in range(count): self._js_call('moveToNextWord', count)
self._js_call('moveToNextWord')
def move_to_prev_word(self, count=1): def move_to_prev_word(self, count=1):
for _ in range(count): self._js_call('moveToPreviousWord', count)
self._js_call('moveToPreviousWord')
def move_to_start_of_line(self): def move_to_start_of_line(self):
self._js_call('moveToStartOfLine') self._js_call('moveToStartOfLine')
@ -300,20 +313,16 @@ class WebEngineCaret(browsertab.AbstractCaret):
self._js_call('moveToEndOfLine') self._js_call('moveToEndOfLine')
def move_to_start_of_next_block(self, count=1): def move_to_start_of_next_block(self, count=1):
for _ in range(count): self._js_call('moveToStartOfNextBlock', count)
self._js_call('moveToStartOfNextBlock')
def move_to_start_of_prev_block(self, count=1): def move_to_start_of_prev_block(self, count=1):
for _ in range(count): self._js_call('moveToStartOfPrevBlock', count)
self._js_call('moveToStartOfPrevBlock')
def move_to_end_of_next_block(self, count=1): def move_to_end_of_next_block(self, count=1):
for _ in range(count): self._js_call('moveToEndOfNextBlock', count)
self._js_call('moveToEndOfNextBlock')
def move_to_end_of_prev_block(self, count=1): def move_to_end_of_prev_block(self, count=1):
for _ in range(count): self._js_call('moveToEndOfPrevBlock', count)
self._js_call('moveToEndOfPrevBlock')
def move_to_start_of_document(self): def move_to_start_of_document(self):
self._js_call('moveToStartOfDocument') self._js_call('moveToStartOfDocument')
@ -322,7 +331,7 @@ class WebEngineCaret(browsertab.AbstractCaret):
self._js_call('moveToEndOfDocument') self._js_call('moveToEndOfDocument')
def toggle_selection(self): 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): def drop_selection(self):
self._js_call('dropSelection') self._js_call('dropSelection')
@ -335,7 +344,13 @@ class WebEngineCaret(browsertab.AbstractCaret):
self._tab.run_js_async(javascript.assemble('caret', 'getSelection'), self._tab.run_js_async(javascript.assemble('caret', 'getSelection'),
callback) 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. """Callback for javascript which clicks the selected element.
Args: Args:
@ -344,6 +359,7 @@ class WebEngineCaret(browsertab.AbstractCaret):
""" """
if js_elem is None: if js_elem is None:
return return
if js_elem == "focused": if js_elem == "focused":
# we had a focused element, not a selected one. Just send <enter> # we had a focused element, not a selected one. Just send <enter>
self._follow_enter(tab) self._follow_enter(tab)
@ -360,7 +376,10 @@ class WebEngineCaret(browsertab.AbstractCaret):
if elem.is_link(): if elem.is_link():
log.webview.debug("Found link in selection, clicking. ClickTarget " log.webview.debug("Found link in selection, clicking. ClickTarget "
"{}, elem {}".format(click_type, elem)) "{}, 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): def follow_selected(self, *, tab=False):
if self._tab.search.search_displayed: if self._tab.search.search_displayed:
@ -376,11 +395,13 @@ class WebEngineCaret(browsertab.AbstractCaret):
# click an existing blue selection # click an existing blue selection
js_code = javascript.assemble('webelem', js_code = javascript.assemble('webelem',
'find_selected_focused_link') 'find_selected_focused_link')
self._tab.run_js_async(js_code, lambda jsret: self._tab.run_js_async(
self._follow_selected_cb(jsret, tab)) js_code,
lambda jsret: self._follow_selected_cb_wrapped(jsret, tab))
def _js_call(self, command, callback=None): def _js_call(self, command, *args, callback=None):
self._tab.run_js_async(javascript.assemble('caret', command), callback) code = javascript.assemble('caret', command, *args)
self._tab.run_js_async(code, callback)
class WebEngineScroller(browsertab.AbstractScroller): class WebEngineScroller(browsertab.AbstractScroller):
@ -575,9 +596,12 @@ class WebEngineElements(browsertab.AbstractElements):
if js_elems is None: if js_elems is None:
callback(None) callback(None)
return return
elif not js_elems['success']:
callback(webelem.Error(js_elems['error']))
return
elems = [] elems = []
for js_elem in js_elems: for js_elem in js_elems['result']:
elem = webengineelem.WebEngineElement(js_elem, tab=self._tab) elem = webengineelem.WebEngineElement(js_elem, tab=self._tab)
elems.append(elem) elems.append(elem)
callback(elems) callback(elems)
@ -617,8 +641,8 @@ class WebEngineElements(browsertab.AbstractElements):
self._tab.run_js_async(js_code, js_cb) self._tab.run_js_async(js_code, js_cb)
def find_at_pos(self, pos, callback): def find_at_pos(self, pos, callback):
assert pos.x() >= 0 assert pos.x() >= 0, pos
assert pos.y() >= 0 assert pos.y() >= 0, pos
pos /= self._tab.zoom.factor() pos /= self._tab.zoom.factor()
js_code = javascript.assemble('webelem', 'find_at_pos', js_code = javascript.assemble('webelem', 'find_at_pos',
pos.x(), pos.y()) pos.x(), pos.y())
@ -628,14 +652,26 @@ class WebEngineElements(browsertab.AbstractElements):
class WebEngineAudio(browsertab.AbstractAudio): 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): def _connect_signals(self):
page = self._widget.page() page = self._widget.page()
page.audioMutedChanged.connect(self.muted_changed) page.audioMutedChanged.connect(self.muted_changed)
page.recentlyAudibleChanged.connect(self.recently_audible_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 = self._widget.page()
page.setAudioMuted(muted) page.setAudioMuted(muted)
@ -647,6 +683,17 @@ class WebEngineAudio(browsertab.AbstractAudio):
page = self._widget.page() page = self._widget.page()
return page.recentlyAudible() 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): class _WebEnginePermissions(QObject):
@ -703,6 +750,18 @@ class _WebEnginePermissions(QObject):
QWebEnginePage.MediaVideoCapture: 'record video', QWebEnginePage.MediaVideoCapture: 'record video',
QWebEnginePage.MediaAudioVideoCapture: 'record audio/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: try:
options.update({ options.update({
QWebEnginePage.DesktopVideoCapture: QWebEnginePage.DesktopVideoCapture:
@ -788,12 +847,18 @@ class _WebEngineScripts(QObject):
super().__init__(parent) super().__init__(parent)
self._tab = tab self._tab = tab
self._widget = None self._widget = None
self._greasemonkey = objreg.get('greasemonkey')
def connect_signals(self): def connect_signals(self):
"""Connect signals to our private slots."""
config.instance.changed.connect(self._on_config_changed) config.instance.changed.connect(self._on_config_changed)
self._tab.url_changed.connect(self._update_stylesheet) self._tab.url_changed.connect(self._update_stylesheet)
self._tab.load_finished.connect(self._on_load_finished) 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) @pyqtSlot(str)
def _on_config_changed(self, option): def _on_config_changed(self, option):
if option in ['scrolling.bar', 'content.user_stylesheets']: if option in ['scrolling.bar', 'content.user_stylesheets']:
@ -805,14 +870,14 @@ class _WebEngineScripts(QObject):
self._update_stylesheet(url) self._update_stylesheet(url)
@pyqtSlot(QUrl) @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. """Update the custom stylesheet in existing tabs.
Arguments: Arguments:
url: The url to get the stylesheet for. url: The url to get the stylesheet for.
force: Also update the global stylesheet. 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: if css is configutils.UNSET and force:
css = shared.get_user_stylesheet(url=None) css = shared.get_user_stylesheet(url=None)
@ -869,9 +934,16 @@ class _WebEngineScripts(QObject):
self._inject_early_js('js', js_code, subframes=True) self._inject_early_js('js', js_code, subframes=True)
self._init_stylesheet() self._init_stylesheet()
greasemonkey = objreg.get('greasemonkey') # The Greasemonkey metadata block support in QtWebEngine only starts at
greasemonkey.scripts_reloaded.connect(self._inject_userscripts) # Qt 5.8. With 5.7.1, we need to inject the scripts ourselves in
self._inject_userscripts() # 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): def _init_stylesheet(self):
"""Initialize custom stylesheets. """Initialize custom stylesheets.
@ -888,40 +960,90 @@ class _WebEngineScripts(QObject):
) )
self._inject_early_js('stylesheet', js_code, subframes=True) self._inject_early_js('stylesheet', js_code, subframes=True)
def _inject_userscripts(self): @pyqtSlot(QUrl)
"""Register user JavaScript files with the global profiles.""" def _inject_greasemonkey_scripts_for_url(self, url):
# The Greasemonkey metadata block support in QtWebEngine only starts at matching_scripts = self._greasemonkey.scripts_for(url)
# Qt 5.8. With 5.7.1, we need to inject the scripts ourselves in self._inject_greasemonkey_scripts(
# response to urlChanged. matching_scripts.start, QWebEngineScript.DocumentCreation, True)
if not qtutils.version_check('5.8'): self._inject_greasemonkey_scripts(
return 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 @pyqtSlot()
# just get replaced by new gm scripts like if we were injecting them def _inject_all_greasemonkey_scripts(self):
# ourselves so we need to remove all gm scripts, while not removing scripts = self._greasemonkey.all_scripts()
# any other stuff that might have been added. Like the one for self._inject_greasemonkey_scripts(scripts)
# stylesheets.
greasemonkey = objreg.get('greasemonkey') def _remove_all_greasemonkey_scripts(self):
scripts = self._widget.page().scripts() page_scripts = self._widget.page().scripts()
for script in scripts.toList(): for script in page_scripts.toList():
if script.name().startswith("GM-"): if script.name().startswith("GM-"):
log.greasemonkey.debug('Removing script: {}' log.greasemonkey.debug('Removing script: {}'
.format(script.name())) .format(script.name()))
removed = scripts.remove(script) removed = page_scripts.remove(script)
assert removed, script.name() assert removed, script.name()
# Then add the new scripts. def _inject_greasemonkey_scripts(self, scripts=None, injection_point=None,
for script in greasemonkey.all_scripts(): remove_first=True):
# @run-at (and @include/@exclude/@match) is parsed by """Register user JavaScript files with the current tab.
# QWebEngineScript.
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 = 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.setSourceCode(script.code())
new_script.setName("GM-{}".format(script.name)) new_script.setName("GM-{}".format(script.name))
new_script.setRunsOnSubFrames(script.runs_on_sub_frames) 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: {}' log.greasemonkey.debug('adding script: {}'
.format(new_script.name())) .format(new_script.name()))
scripts.insert(new_script) page_scripts.insert(new_script)
class WebEngineTab(browsertab.AbstractTab): class WebEngineTab(browsertab.AbstractTab):
@ -941,16 +1063,16 @@ class WebEngineTab(browsertab.AbstractTab):
private=private, parent=parent) private=private, parent=parent)
widget = webview.WebEngineView(tabdata=self.data, win_id=win_id, widget = webview.WebEngineView(tabdata=self.data, win_id=win_id,
private=private) private=private)
self.history = WebEngineHistory(self) self.history = WebEngineHistory(tab=self)
self.scroller = WebEngineScroller(self, parent=self) self.scroller = WebEngineScroller(tab=self, parent=self)
self.caret = WebEngineCaret(mode_manager=mode_manager, self.caret = WebEngineCaret(mode_manager=mode_manager,
tab=self, parent=self) tab=self, parent=self)
self.zoom = WebEngineZoom(tab=self, parent=self) self.zoom = WebEngineZoom(tab=self, parent=self)
self.search = WebEngineSearch(parent=self) self.search = WebEngineSearch(tab=self, parent=self)
self.printing = WebEnginePrinting() self.printing = WebEnginePrinting(tab=self)
self.elements = WebEngineElements(tab=self) self.elements = WebEngineElements(tab=self)
self.action = WebEngineAction(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._permissions = _WebEnginePermissions(tab=self, parent=self)
self._scripts = _WebEngineScripts(tab=self, parent=self) self._scripts = _WebEngineScripts(tab=self, parent=self)
# We're assigning settings in _set_widget # We're assigning settings in _set_widget
@ -975,7 +1097,7 @@ class WebEngineTab(browsertab.AbstractTab):
fp.installEventFilter(self._mouse_event_filter) fp.installEventFilter(self._mouse_event_filter)
self._child_event_filter = mouse.ChildEventFilter( self._child_event_filter = mouse.ChildEventFilter(
eventfilter=self._mouse_event_filter, widget=self._widget, eventfilter=self._mouse_event_filter, widget=self._widget,
parent=self) win_id=self.win_id, parent=self)
self._widget.installEventFilter(self._child_event_filter) self._widget.installEventFilter(self._child_event_filter)
@pyqtSlot() @pyqtSlot()
@ -995,6 +1117,9 @@ class WebEngineTab(browsertab.AbstractTab):
url: The QUrl to open. url: The QUrl to open.
predict: If set to False, predicted_navigation is not emitted. 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._saved_zoom = self.zoom.factor()
self._openurl_prepare(url, predict=predict) self._openurl_prepare(url, predict=predict)
self._widget.load(url) self._widget.load(url)
@ -1017,6 +1142,10 @@ class WebEngineTab(browsertab.AbstractTab):
world_id = QWebEngineScript.ApplicationWorld world_id = QWebEngineScript.ApplicationWorld
elif isinstance(world, int): elif isinstance(world, int):
world_id = world 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: else:
world_id = _JS_WORLD_MAP[world] world_id = _JS_WORLD_MAP[world]
@ -1153,11 +1282,10 @@ class WebEngineTab(browsertab.AbstractTab):
@pyqtSlot() @pyqtSlot()
def _on_load_started(self): def _on_load_started(self):
"""Clear search when a new load is started if needed.""" """Clear search when a new load is started if needed."""
if (qtutils.version_check('5.9', compiled=False) and # WORKAROUND for
not qtutils.version_check('5.9.2', compiled=False)): # https://bugreports.qt.io/browse/QTBUG-61506
# WORKAROUND for # (seems to be back in later Qt versions as well)
# https://bugreports.qt.io/browse/QTBUG-61506 self.search.clear()
self.search.clear()
super()._on_load_started() super()._on_load_started()
self.data.netrc_used = False self.data.netrc_used = False
@ -1241,6 +1369,34 @@ class WebEngineTab(browsertab.AbstractTab):
# the old icon is still displayed. # the old icon is still displayed.
self.icon_changed.emit(QIcon()) 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) @pyqtSlot(QUrl)
def _on_predicted_navigation(self, url): def _on_predicted_navigation(self, url):
"""If we know we're going to visit an URL soon, change the settings. """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) super()._on_navigation_request(navigation)
if navigation.url == QUrl('qute://print'): if navigation.url == QUrl('qute://print'):
command_dispatcher = objreg.get('command-dispatcher', try:
scope='window', self.printing.show_dialog()
window=self.win_id) except browsertab.WebTabError as e:
command_dispatcher.printpage() message.error(str(e))
navigation.accepted = False navigation.accepted = False
if not navigation.accepted or not navigation.is_main_frame: if not navigation.accepted or not navigation.is_main_frame:

View File

@ -19,18 +19,17 @@
"""The main browser widget for QtWebEngine.""" """The main browser widget for QtWebEngine."""
import sip from PyQt5.QtCore import pyqtSignal, QUrl, PYQT_VERSION
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QUrl, PYQT_VERSION
from PyQt5.QtGui import QPalette from PyQt5.QtGui import QPalette
from PyQt5.QtQuickWidgets import QQuickWidget from PyQt5.QtWidgets import QWidget
from PyQt5.QtWebEngineWidgets import (QWebEngineView, QWebEnginePage, from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage
QWebEngineScript)
from qutebrowser.browser import shared 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.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.misc import miscwidgets
from qutebrowser.qt import sip
class WebEngineView(QWebEngineView): class WebEngineView(QWebEngineView):
@ -71,10 +70,10 @@ class WebEngineView(QWebEngineView):
if proxy is not None: if proxy is not None:
return proxy return proxy
# This should only find the RenderWidgetHostViewQtDelegateWidget, # We don't want e.g. a QMenu.
# but not e.g. a QMenu rwhv_class = 'QtWebEngineCore::RenderWidgetHostViewQtDelegateWidget'
children = [c for c in self.findChildren(QQuickWidget) children = [c for c in self.findChildren(QWidget)
if c.isVisible()] if c.isVisible() and c.inherits(rwhv_class)]
log.webview.debug("Found possibly lost focusProxy: {}" log.webview.debug("Found possibly lost focusProxy: {}"
.format(children)) .format(children))
@ -152,11 +151,13 @@ class WebEnginePage(QWebEnginePage):
Signals: Signals:
certificate_error: Emitted on certificate errors. 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. shutting_down: Emitted when the page is shutting down.
navigation_request: Emitted on acceptNavigationRequest. navigation_request: Emitted on acceptNavigationRequest.
""" """
certificate_error = pyqtSignal() certificate_error = pyqtSignal(certificateerror.CertificateErrorWrapper)
shutting_down = pyqtSignal() shutting_down = pyqtSignal()
navigation_request = pyqtSignal(usertypes.NavigationRequest) navigation_request = pyqtSignal(usertypes.NavigationRequest)
@ -166,7 +167,6 @@ class WebEnginePage(QWebEnginePage):
self._theme_color = theme_color self._theme_color = theme_color
self._set_bg_color() self._set_bg_color()
config.instance.changed.connect(self._set_bg_color) config.instance.changed.connect(self._set_bg_color)
self.urlChanged.connect(self._inject_userjs)
@config.change_filter('colors.webpage.bg') @config.change_filter('colors.webpage.bg')
def _set_bg_color(self): def _set_bg_color(self):
@ -181,36 +181,9 @@ class WebEnginePage(QWebEnginePage):
def certificateError(self, error): def certificateError(self, error):
"""Handle certificate errors coming from Qt.""" """Handle certificate errors coming from Qt."""
self.certificate_error.emit()
url = error.url()
error = certificateerror.CertificateErrorWrapper(error) error = certificateerror.CertificateErrorWrapper(error)
log.webview.debug("Certificate error: {}".format(error)) self.certificate_error.emit(error)
return error.ignore
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
def javaScriptConfirm(self, url, js_msg): def javaScriptConfirm(self, url, js_msg):
"""Override javaScriptConfirm to use qutebrowser prompts.""" """Override javaScriptConfirm to use qutebrowser prompts."""
@ -288,43 +261,3 @@ class WebEnginePage(QWebEnginePage):
is_main_frame=is_main_frame) is_main_frame=is_main_frame)
self.navigation_request.emit(navigation) self.navigation_request.emit(navigation)
return navigation.accepted 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)

View File

@ -312,9 +312,9 @@ class _Downloader:
for style in styles: for style in styles:
style = webkitelem.WebKitElement(style, tab=self.tab) style = webkitelem.WebKitElement(style, tab=self.tab)
# The Mozilla Developer Network says: # The Mozilla Developer Network says:
# type: This attribute defines the styling language as a MIME type # > type: This attribute defines the styling language as a MIME
# (charset should not be specified). This attribute is optional and # > type (charset should not be specified). This attribute is
# default to text/css if it's missing. # > optional and default to text/css if it's missing.
# https://developer.mozilla.org/en/docs/Web/HTML/Element/style # https://developer.mozilla.org/en/docs/Web/HTML/Element/style
if 'type' in style and style['type'] != 'text/css': if 'type' in style and style['type'] != 'text/css':
continue continue

View File

@ -111,11 +111,13 @@ def dirbrowser_html(path):
return html.encode('UTF-8', errors='xmlcharrefreplace') return html.encode('UTF-8', errors='xmlcharrefreplace')
def handler(request): def handler(request, _operation, _current_url):
"""Handler for a file:// URL. """Handler for a file:// URL.
Args: Args:
request: QNetworkRequest to answer to. request: QNetworkRequest to answer to.
_operation: The HTTP operation being done.
_current_url: The page we're on currently.
Return: Return:
A QNetworkReply for directories, None for files. A QNetworkReply for directories, None for files.

View File

@ -373,24 +373,9 @@ class NetworkManager(QNetworkAccessManager):
req, proxy_error, QNetworkReply.UnknownProxyError, req, proxy_error, QNetworkReply.UnknownProxyError,
self) self)
scheme = req.url().scheme() for header, value in shared.custom_headers(url=req.url()):
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():
req.setRawHeader(header, value) 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 are some scenarios where we can't figure out current_url:
# - There's a generic NetworkManager, e.g. for downloads # - There's a generic NetworkManager, e.g. for downloads
# - The download was in a tab which is now closed. # - The download was in a tab which is now closed.
@ -408,6 +393,14 @@ class NetworkManager(QNetworkAccessManager):
# the webpage shutdown here. # the webpage shutdown here.
current_url = QUrl() 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: if 'log-requests' in self._args.debug_flags:
operation = debug.qenum_key(QNetworkAccessManager, op) operation = debug.qenum_key(QNetworkAccessManager, op)
operation = operation.replace('Operation', '').upper() operation = operation.replace('Operation', '').upper()
@ -416,5 +409,12 @@ class NetworkManager(QNetworkAccessManager):
req.url().toDisplayString(), req.url().toDisplayString(),
current_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) self.set_referer(req, current_url)
return super().createRequest(op, req, outgoing_data) return super().createRequest(op, req, outgoing_data)

View File

@ -19,58 +19,63 @@
"""QtWebKit specific qute://* handlers and glue code.""" """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 qutescheme
from qutebrowser.browser import pdfjs, qutescheme
from qutebrowser.browser.webkit.network import networkreply 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. """Scheme handler for qute:// URLs.
Args: Args:
request: QNetworkRequest to answer to. request: QNetworkRequest to answer to.
operation: The HTTP operation being done.
current_url: The page we're on currently.
Return: Return:
A QNetworkReply. 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: try:
mimetype, data = qutescheme.data_for_url(request.url()) mimetype, data = qutescheme.data_for_url(url)
except qutescheme.NoHandlerFound: except qutescheme.Error as e:
errorstr = "No handler found for {}!".format( errors = {
request.url().toDisplayString()) qutescheme.NotFoundError:
return networkreply.ErrorNetworkReply( QNetworkReply.ContentNotFoundError,
request, errorstr, QNetworkReply.ContentNotFoundError) qutescheme.UrlInvalidError:
except qutescheme.QuteSchemeOSError as e: QNetworkReply.ContentOperationNotPermittedError,
return networkreply.ErrorNetworkReply( qutescheme.RequestDeniedError:
request, str(e), QNetworkReply.ContentNotFoundError) QNetworkReply.ContentAccessDenied,
except qutescheme.QuteSchemeError as e: qutescheme.SchemeOSError:
return networkreply.ErrorNetworkReply(request, e.errorstring, e.error) 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: except qutescheme.Redirect as e:
qtutils.ensure_valid(e.url) qtutils.ensure_valid(e.url)
return networkreply.RedirectNetworkReply(e.url) return networkreply.RedirectNetworkReply(e.url)
return networkreply.FixedDataNetworkReply(request, data, mimetype) 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

View File

@ -125,8 +125,20 @@ class WebKitElement(webelem.AbstractWebElement):
self._elem.setPlainText(value) self._elem.setPlainText(value)
else: else:
log.webelem.debug("Filling {!r} via javascript.".format(self)) log.webelem.debug("Filling {!r} via javascript.".format(self))
value = javascript.string_escape(value) value = javascript.to_js(value)
self._elem.evaluateJavaScript("this.value='{}'".format(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): def caret_position(self):
"""Get the text caret position for the current element.""" """Get the text caret position for the current element."""
@ -142,11 +154,11 @@ class WebKitElement(webelem.AbstractWebElement):
raise webelem.Error("Element is not editable!") raise webelem.Error("Element is not editable!")
log.webelem.debug("Inserting text into element {!r}".format(self)) log.webelem.debug("Inserting text into element {!r}".format(self))
self._elem.evaluateJavaScript(""" self._elem.evaluateJavaScript("""
var text = "{}"; var text = {};
var event = document.createEvent("TextEvent"); var event = document.createEvent("TextEvent");
event.initTextEvent("textInput", true, true, null, text); event.initTextEvent("textInput", true, true, null, text);
this.dispatchEvent(event); this.dispatchEvent(event);
""".format(javascript.string_escape(text))) """.format(javascript.to_js(text)))
def _parent(self): def _parent(self):
"""Get the parent element of this element.""" """Get the parent element of this element."""

View File

@ -23,7 +23,6 @@ import re
import functools import functools
import xml.etree.ElementTree import xml.etree.ElementTree
import sip
from PyQt5.QtCore import (pyqtSlot, Qt, QEvent, QUrl, QPoint, QTimer, QSizeF, from PyQt5.QtCore import (pyqtSlot, Qt, QEvent, QUrl, QPoint, QTimer, QSizeF,
QSize) QSize)
from PyQt5.QtGui import QKeyEvent, QIcon from PyQt5.QtGui import QKeyEvent, QIcon
@ -35,6 +34,7 @@ from qutebrowser.browser import browsertab, shared
from qutebrowser.browser.webkit import (webview, tabhistory, webkitelem, from qutebrowser.browser.webkit import (webview, tabhistory, webkitelem,
webkitsettings) webkitsettings)
from qutebrowser.utils import qtutils, usertypes, utils, log, debug from qutebrowser.utils import qtutils, usertypes, utils, log, debug
from qutebrowser.qt import sip
class WebKitAction(browsertab.AbstractAction): class WebKitAction(browsertab.AbstractAction):
@ -84,8 +84,8 @@ class WebKitSearch(browsertab.AbstractSearch):
"""QtWebKit implementations related to searching on the page.""" """QtWebKit implementations related to searching on the page."""
def __init__(self, parent=None): def __init__(self, tab, parent=None):
super().__init__(parent) super().__init__(tab, parent)
self._flags = QWebPage.FindFlags(0) self._flags = QWebPage.FindFlags(0)
def _call_cb(self, callback, found, text, flags, caller): def _call_cb(self, callback, found, text, flags, caller):
@ -115,7 +115,11 @@ class WebKitSearch(browsertab.AbstractSearch):
if callback is not None: if callback is not None:
QTimer.singleShot(0, functools.partial(callback, found)) QTimer.singleShot(0, functools.partial(callback, found))
self.finished.emit(found)
def clear(self): def clear(self):
if self.search_displayed:
self.cleared.emit()
self.search_displayed = False self.search_displayed = False
# We first clear the marked text, then the highlights # We first clear the marked text, then the highlights
self._widget.findText('') self._widget.findText('')
@ -348,7 +352,7 @@ class WebKitCaret(browsertab.AbstractCaret):
def selection(self, callback): def selection(self, callback):
callback(self._widget.selectedText()) callback(self._widget.selectedText())
def follow_selected(self, *, tab=False): def _follow_selected(self, *, tab=False):
if QWebSettings.globalSettings().testAttribute( if QWebSettings.globalSettings().testAttribute(
QWebSettings.JavascriptEnabled): QWebSettings.JavascriptEnabled):
if tab: if tab:
@ -389,6 +393,12 @@ class WebKitCaret(browsertab.AbstractCaret):
else: else:
self._tab.openurl(url) 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): class WebKitZoom(browsertab.AbstractZoom):
@ -631,7 +641,7 @@ class WebKitAudio(browsertab.AbstractAudio):
"""Dummy handling of audio status for QtWebKit.""" """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!') raise browsertab.WebTabError('Muting is not supported on QtWebKit!')
def is_muted(self): def is_muted(self):
@ -652,16 +662,16 @@ class WebKitTab(browsertab.AbstractTab):
private=private, tab=self) private=private, tab=self)
if private: if private:
self._make_private(widget) self._make_private(widget)
self.history = WebKitHistory(self) self.history = WebKitHistory(tab=self)
self.scroller = WebKitScroller(self, parent=self) self.scroller = WebKitScroller(tab=self, parent=self)
self.caret = WebKitCaret(mode_manager=mode_manager, self.caret = WebKitCaret(mode_manager=mode_manager,
tab=self, parent=self) tab=self, parent=self)
self.zoom = WebKitZoom(tab=self, parent=self) self.zoom = WebKitZoom(tab=self, parent=self)
self.search = WebKitSearch(parent=self) self.search = WebKitSearch(tab=self, parent=self)
self.printing = WebKitPrinting() self.printing = WebKitPrinting(tab=self)
self.elements = WebKitElements(tab=self) self.elements = WebKitElements(tab=self)
self.action = WebKitAction(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 # We're assigning settings in _set_widget
self.settings = webkitsettings.WebKitSettings(settings=None) self.settings = webkitsettings.WebKitSettings(settings=None)
self._set_widget(widget) self._set_widget(widget)
@ -808,6 +818,10 @@ class WebKitTab(browsertab.AbstractTab):
if navigation.is_main_frame: if navigation.is_main_frame:
self.settings.update_for_url(navigation.url) self.settings.update_for_url(navigation.url)
@pyqtSlot()
def _on_ssl_errors(self):
self._has_ssl_errors = True
def _connect_signals(self): def _connect_signals(self):
view = self._widget view = self._widget
page = view.page() page = view.page()

View File

@ -22,7 +22,6 @@
import html import html
import functools import functools
import sip
from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QUrl, QPoint from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QUrl, QPoint
from PyQt5.QtGui import QDesktopServices from PyQt5.QtGui import QDesktopServices
from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest
@ -31,10 +30,11 @@ from PyQt5.QtPrintSupport import QPrintDialog
from PyQt5.QtWebKitWidgets import QWebPage, QWebFrame from PyQt5.QtWebKitWidgets import QWebPage, QWebFrame
from qutebrowser.config import config 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 import http
from qutebrowser.browser.webkit.network import networkmanager from qutebrowser.browser.webkit.network import networkmanager
from qutebrowser.utils import message, usertypes, log, jinja, objreg from qutebrowser.utils import message, usertypes, log, jinja, objreg
from qutebrowser.qt import sip
class BrowserPage(QWebPage): class BrowserPage(QWebPage):
@ -206,17 +206,6 @@ class BrowserPage(QWebPage):
suggested_file) suggested_file)
return True 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): def shutdown(self):
"""Prepare the web page for being deleted.""" """Prepare the web page for being deleted."""
self._is_shutting_down = True self._is_shutting_down = True
@ -279,10 +268,10 @@ class BrowserPage(QWebPage):
else: else:
reply.finished.connect(functools.partial( reply.finished.connect(functools.partial(
self.display_content, reply, 'image/jpeg')) self.display_content, reply, 'image/jpeg'))
elif (mimetype in ['application/pdf', 'application/x-pdf'] and elif pdfjs.should_use_pdfjs(mimetype, reply.url()):
config.val.content.pdfjs): download_manager.fetch(reply,
# Use pdf.js to display the page target=downloads.PDFJSDownloadTarget(),
self._show_pdfjs(reply) auto_remove=True)
else: else:
# Unknown mimetype, so download anyways. # Unknown mimetype, so download anyways.
download_manager.fetch(reply, download_manager.fetch(reply,
@ -415,7 +404,7 @@ class BrowserPage(QWebPage):
def userAgentForUrl(self, url): def userAgentForUrl(self, url):
"""Override QWebPage::userAgentForUrl to customize the user agent.""" """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: if ua is None:
return super().userAgentForUrl(url) return super().userAgentForUrl(url)
else: else:

View File

@ -19,7 +19,7 @@
"""The main browser widgets.""" """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.QtGui import QPalette
from PyQt5.QtWidgets import QStyleFactory from PyQt5.QtWidgets import QStyleFactory
from PyQt5.QtWebKit import QWebSettings from PyQt5.QtWebKit import QWebSettings
@ -78,10 +78,6 @@ class WebView(QWebView):
self.setPage(page) 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) config.instance.changed.connect(self._set_bg_color)
def __repr__(self): def __repr__(self):
@ -130,32 +126,6 @@ class WebView(QWebView):
""" """
self.load(url) 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): def createWindow(self, wintype):
"""Called by Qt when a page wants to create a new window. """Called by Qt when a page wants to create a new window.

View File

@ -432,7 +432,7 @@ def run_async(tab, cmd, *args, win_id, env, verbose=False):
cmd_path = os.path.expanduser(cmd) cmd_path = os.path.expanduser(cmd)
# if cmd is not given as an absolute path, look it up # 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): if not os.path.isabs(cmd_path):
log.misc.debug("{} is no absolute path".format(cmd_path)) log.misc.debug("{} is no absolute path".format(cmd_path))
cmd_path = _lookup_path(cmd) cmd_path = _lookup_path(cmd)

View File

@ -28,13 +28,27 @@ import html
from PyQt5.QtWidgets import QStyle, QStyleOptionViewItem, QStyledItemDelegate from PyQt5.QtWidgets import QStyle, QStyleOptionViewItem, QStyledItemDelegate
from PyQt5.QtCore import QRectF, QSize, Qt from PyQt5.QtCore import QRectF, QSize, Qt
from PyQt5.QtGui import (QIcon, QPalette, QTextDocument, QTextOption, from PyQt5.QtGui import (QIcon, QPalette, QTextDocument, QTextOption,
QAbstractTextDocumentLayout) QAbstractTextDocumentLayout, QSyntaxHighlighter,
QTextCharFormat)
from qutebrowser.config import config 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): class CompletionItemDelegate(QStyledItemDelegate):
@ -194,21 +208,15 @@ class CompletionItemDelegate(QStyledItemDelegate):
self._doc.setDefaultTextOption(text_option) self._doc.setDefaultTextOption(text_option)
self._doc.setDocumentMargin(2) self._doc.setDocumentMargin(2)
assert _cached_stylesheet is not None
self._doc.setDefaultStyleSheet(_cached_stylesheet)
if index.parent().isValid(): if index.parent().isValid():
view = self.parent() view = self.parent()
pattern = view.pattern pattern = view.pattern
columns_to_filter = index.model().columns_to_filter(index) columns_to_filter = index.model().columns_to_filter(index)
self._doc.setPlainText(self._opt.text)
if index.column() in columns_to_filter and pattern: if index.column() in columns_to_filter and pattern:
repl = r'<span class="highlight">\g<0></span>' pat = re.escape(pattern).replace(r'\ ', r'|')
pat = html.escape(re.escape(pattern)).replace(r'\ ', r'|') _Highlighter(self._doc, pat,
txt = html.escape(self._opt.text) config.val.colors.completion.match.fg)
text = re.sub(pat, repl, txt, flags=re.IGNORECASE)
self._doc.setHtml(text)
else:
self._doc.setPlainText(self._opt.text)
else: else:
self._doc.setHtml( self._doc.setHtml(
'<span style="font: {};">{}</span>'.format( '<span style="font: {};">{}</span>'.format(
@ -283,24 +291,3 @@ class CompletionItemDelegate(QStyledItemDelegate):
self._draw_focus_rect() self._draw_focus_rect()
self._painter.restore() 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)

View File

@ -27,12 +27,7 @@ from qutebrowser.keyinput import keyutils
def option(*, info): def option(*, info):
"""A CompletionModel filled with settings and their descriptions.""" """A CompletionModel filled with settings and their descriptions."""
model = completionmodel.CompletionModel(column_widths=(20, 70, 10)) return _option(info, "Options", lambda opt: not opt.no_autoconfig)
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
def customized_option(*, info): def customized_option(*, info):
@ -47,6 +42,37 @@ def customized_option(*, info):
return model 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): def value(optname, *values, info):
"""A CompletionModel filled with setting values. """A CompletionModel filled with setting values.

View File

@ -42,7 +42,7 @@ class HistoryCategory(QSqlQueryModel):
def _atime_expr(self): def _atime_expr(self):
"""If max_items is set, return an expression to limit the query.""" """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. # HistoryCategory should not be added to the completion in that case.
assert max_items != 0 assert max_items != 0
@ -84,7 +84,7 @@ class HistoryCategory(QSqlQueryModel):
timefmt = ("strftime('{}', last_atime, 'unixepoch', 'localtime')" timefmt = ("strftime('{}', last_atime, 'unixepoch', 'localtime')"
.format(timestamp_format.replace("'", "`"))) .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 # if the number of words changed, we need to generate a new query
# otherwise, we can reuse the prepared query for performance # otherwise, we can reuse the prepared query for performance
self._query = sql.Query(' '.join([ self._query = sql.Query(' '.join([
@ -100,14 +100,14 @@ class HistoryCategory(QSqlQueryModel):
with debug.log_time('sql', 'Running completion query'): with debug.log_time('sql', 'Running completion query'):
self._query.run(**{ self._query.run(**{
str(i): w for i, w in enumerate(words)}) 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): def removeRows(self, row, _count, _parent=None):
"""Override QAbstractItemModel::removeRows to re-run sql query.""" """Override QAbstractItemModel::removeRows to re-run sql query."""
# re-run query to reload updated table # re-run query to reload updated table
with debug.log_time('sql', 'Re-running completion query post-delete'): with debug.log_time('sql', 'Re-running completion query post-delete'):
self._query.run() self._query.run()
self.setQuery(self._query) self.setQuery(self._query.query)
while self.rowCount() < row: while self.rowCount() < row:
self.fetchMore() self.fetchMore()
return True return True

View File

@ -24,7 +24,7 @@ import re
from PyQt5.QtCore import Qt, QSortFilterProxyModel, QRegExp from PyQt5.QtCore import Qt, QSortFilterProxyModel, QRegExp
from PyQt5.QtGui import QStandardItem, QStandardItemModel from PyQt5.QtGui import QStandardItem, QStandardItemModel
from qutebrowser.utils import qtutils from qutebrowser.utils import qtutils, log
class ListCategory(QSortFilterProxyModel): class ListCategory(QSortFilterProxyModel):
@ -80,6 +80,13 @@ class ListCategory(QSortFilterProxyModel):
left = self.srcmodel.data(lindex) left = self.srcmodel.data(lindex)
right = self.srcmodel.data(rindex) 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) leftstart = left.startswith(self._pattern)
rightstart = right.startswith(self._pattern) rightstart = right.startswith(self._pattern)

View File

@ -122,8 +122,8 @@ def _buffer(skip_win_id=None):
tabs.append(("{}/{}".format(win_id, idx + 1), tabs.append(("{}/{}".format(win_id, idx + 1),
tab.url().toDisplayString(), tab.url().toDisplayString(),
tabbed_browser.widget.page_title(idx))) tabbed_browser.widget.page_title(idx)))
cat = listcategory.ListCategory("{}".format(win_id), tabs, cat = listcategory.ListCategory(
delete_func=delete_buffer) str(win_id), tabs, delete_func=delete_buffer, sort=False)
model.add_category(cat) model.add_category(cat)
return model return model

View File

@ -22,6 +22,7 @@
from qutebrowser.completion.models import (completionmodel, listcategory, from qutebrowser.completion.models import (completionmodel, listcategory,
histcategory) histcategory)
from qutebrowser.utils import log, objreg from qutebrowser.utils import log, objreg
from qutebrowser.config import config
_URLCOL = 0 _URLCOL = 0
@ -50,25 +51,48 @@ def _delete_quickmark(data):
def url(*, info): 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. Used for the `open` command.
""" """
model = completionmodel.CompletionModel(column_widths=(40, 50, 10)) model = completionmodel.CompletionModel(column_widths=(40, 50, 10))
# pylint: disable=bad-config-option
quickmarks = [(url, name) for (name, url) quickmarks = [(url, name) for (name, url)
in objreg.get('quickmark-manager').marks.items()] in objreg.get('quickmark-manager').marks.items()]
bookmarks = objreg.get('bookmark-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: if searchengines and 'searchengines' in categories:
model.add_category(listcategory.ListCategory( 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, 'Quickmarks', quickmarks, delete_func=_delete_quickmark,
sort=False)) sort=False)
if bookmarks: if bookmarks and 'bookmarks' in categories:
model.add_category(listcategory.ListCategory( models['bookmarks'] = listcategory.ListCategory(
'Bookmarks', bookmarks, delete_func=_delete_bookmark, sort=False)) '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) 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 return model

View File

@ -34,6 +34,7 @@ from qutebrowser.keyinput import keyutils
val = None val = None
instance = None instance = None
key_instance = None key_instance = None
cache = None
# Keeping track of all change filters to validate them later. # Keeping track of all change filters to validate them later.
change_filters = [] change_filters = []

View 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]

View File

@ -85,8 +85,8 @@ class ConfigCommands:
*, pattern=None): *, pattern=None):
"""Set an option. """Set an option.
If the option name ends with '?', the value of the option is shown If the option name ends with '?' or no value is provided, the
instead. value of the option is shown instead.
Using :set without any arguments opens a page where settings can be Using :set without any arguments opens a page where settings can be
changed interactively. changed interactively.
@ -116,8 +116,7 @@ class ConfigCommands:
with self._handle_config_error(): with self._handle_config_error():
if value is None: if value is None:
raise cmdexc.CommandError("set: The following arguments " self._print_value(option, pattern=pattern)
"are required: value")
else: else:
self._config.set_str(option, value, pattern=pattern, self._config.set_str(option, value, pattern=pattern,
save_yaml=not temp) save_yaml=not temp)
@ -246,11 +245,114 @@ class ConfigCommands:
Args: Args:
option: The name of the option. 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(): with self._handle_config_error():
self._config.unset(option, save_yaml=not temp) 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') @cmdutils.register(instance='config-commands')
def config_clear(self, save=False): def config_clear(self, save=False):
"""Set all settings back to their default. """Set all settings back to their default.

View File

@ -56,7 +56,7 @@ class Option:
@attr.s @attr.s
class Migrations: class Migrations:
"""Nigrated options in configdata.yml. """Migrated options in configdata.yml.
Attributes: Attributes:
renamed: A dict mapping old option names to new names. renamed: A dict mapping old option names to new names.

View File

@ -3,8 +3,10 @@
aliases: aliases:
default: default:
w: session-save w: session-save
q: quit q: close
qa: quit
wq: quit --save wq: quit --save
wqa: quit --save
type: type:
name: Dict name: Dict
keytype: keytype:
@ -181,6 +183,51 @@ qt.force_platform:
This sets the `QT_QPA_PLATFORM` environment variable and is useful to force This sets the `QT_QPA_PLATFORM` environment variable and is useful to force
using the XCB plugin when running QtWebEngine on Wayland. 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: qt.highdpi:
type: Bool type: Bool
@ -220,10 +267,12 @@ content.autoplay:
backend: backend:
QtWebEngine: Qt 5.10 QtWebEngine: Qt 5.10
QtWebKit: false QtWebKit: false
supports_pattern: true
desc: >- desc: >-
Automatically start playing `<video>` elements. 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: content.cache.size:
default: null default: null
@ -321,6 +370,7 @@ content.windowed_fullscreen:
content.desktop_capture: content.desktop_capture:
type: BoolAsk type: BoolAsk
default: ask default: ask
supports_pattern: true
desc: >- desc: >-
Allow websites to share screen content. Allow websites to share screen content.
@ -350,14 +400,28 @@ content.frame_flattening:
content.geolocation: content.geolocation:
default: ask default: ask
type: BoolAsk type: BoolAsk
supports_pattern: true
desc: Allow websites to request geolocations. 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: content.headers.accept_language:
type: type:
name: String name: String
none_ok: true none_ok: true
supports_pattern: true
default: en-US,en 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: content.headers.custom:
default: {} default: {}
@ -370,6 +434,7 @@ content.headers.custom:
name: String name: String
encoding: ascii encoding: ascii
none_ok: true none_ok: true
supports_pattern: true
desc: Custom headers for qutebrowser HTTP requests. desc: Custom headers for qutebrowser HTTP requests.
content.headers.do_not_track: content.headers.do_not_track:
@ -377,6 +442,7 @@ content.headers.do_not_track:
name: Bool name: Bool
none_ok: true none_ok: true
default: true default: true
supports_pattern: true
desc: >- desc: >-
Value to send in the `DNT` header. 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 - never: "Never send the Referer. This is not recommended, as some sites
may break." may break."
- same-domain: "Only send the Referer for the same domain. This will - same-domain: "Only send the Referer for the same domain. This will
still protect your privacy, but shouldn't break any sites." still protect your privacy, but shouldn't break any sites. With
backend: QtWebKit QtWebEngine, the referer will still be sent for other domains, but
with stripped path information."
restart: true
desc: >- desc: >-
When to send the Referer header. When to send the Referer header.
The Referer header tells websites from which website you were coming from The Referer header tells websites from which website you were coming from
when visiting them. when visiting them.
No restart is needed with QtWebKit.
content.headers.user_agent: content.headers.user_agent:
default: null default: null
type: type:
@ -451,10 +521,15 @@ content.headers.user_agent:
Gecko" Gecko"
- IE 11.0 for Desktop Win7 64-bit - 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: content.host_blocking.enabled:
default: true default: true
supports_pattern: true
type: Bool type: Bool
desc: Enable host blocking. 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 - A zip-file of any of the above, with either only one file, or a file
named `hosts` (with any extension). 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: content.host_blocking.whitelist:
default: default:
- piwik.org - piwik.org
type: type:
name: List name: List
valtype: String valtype: UrlPattern
none_ok: true none_ok: true
desc: >- 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 Note this whitelists blocked hosts, not first-party URLs. As an example, if
match the requested domain. `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. Local domains are always exempt from hostblocking.
@ -594,6 +677,7 @@ content.local_storage:
content.media_capture: content.media_capture:
default: ask default: ask
type: BoolAsk type: BoolAsk
supports_pattern: true
backend: QtWebEngine backend: QtWebEngine
desc: Allow websites to record audio/video. desc: Allow websites to record audio/video.
@ -610,13 +694,13 @@ content.netrc_file:
content.notifications: content.notifications:
default: ask default: ask
type: BoolAsk type: BoolAsk
supports_pattern: true
backend: QtWebKit backend: QtWebKit
desc: Allow websites to show notifications. desc: Allow websites to show notifications.
content.pdfjs: content.pdfjs:
default: false default: false
type: Bool type: Bool
backend: QtWebKit
desc: >- desc: >-
Allow pdf.js to view PDF files in the browser. Allow pdf.js to view PDF files in the browser.
@ -626,6 +710,7 @@ content.pdfjs:
content.persistent_storage: content.persistent_storage:
default: ask default: ask
type: BoolAsk type: BoolAsk
supports_pattern: true
backend: backend:
QtWebKit: false QtWebKit: false
QtWebEngine: Qt 5.11 QtWebEngine: Qt 5.11
@ -672,6 +757,7 @@ content.proxy_dns_requests:
content.register_protocol_handler: content.register_protocol_handler:
default: ask default: ask
type: BoolAsk type: BoolAsk
supports_pattern: true
backend: backend:
QtWebKit: false QtWebKit: false
QtWebEngine: Qt 5.11 QtWebEngine: Qt 5.11
@ -681,6 +767,7 @@ content.register_protocol_handler:
content.ssl_strict: content.ssl_strict:
default: ask default: ask
type: BoolAsk type: BoolAsk
supports_pattern: true
desc: Validate SSL handshakes. desc: Validate SSL handshakes.
content.user_stylesheets: content.user_stylesheets:
@ -698,29 +785,52 @@ content.webgl:
supports_pattern: true supports_pattern: true
desc: Enable WebGL. desc: Enable WebGL.
content.webrtc_public_interfaces_only: content.webrtc_ip_handling_policy:
default: false default: all-interfaces
type: Bool 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: backend:
QtWebKit: false QtWebKit: false
QtWebEngine: Qt 5.9.2 QtWebEngine: Qt 5.9.2
restart: true
desc: >- 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 because of a Qt bug.
On Qt 5.10, this option doesn't work at all because of a Qt bug.
On Qt >= 5.11, no restart is required.
content.xss_auditing: content.xss_auditing:
type: Bool type: Bool
default: false default: true
supports_pattern: true supports_pattern: true
desc: >- desc: >-
Monitor load requests for cross-site scripting attempts. Monitor load requests for cross-site scripting attempts.
Suspicious scripts will be blocked and reported in the inspector's Suspicious scripts will be blocked and reported in the inspector's
JavaScript console. Enabling this feature might have an impact on JavaScript console.
performance.
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: ' # emacs: '
@ -788,7 +898,27 @@ completion.timestamp_format:
default: '%Y-%m-%d' default: '%Y-%m-%d'
desc: Format of timestamps (e.g. for the history completion). 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: completion.web_history_max_items:
renamed: completion.web_history.max_items
completion.web_history.max_items:
default: -1 default: -1
type: type:
name: Int name: Int
@ -854,6 +984,18 @@ downloads.location.suggestion:
- both: Show download path and filename. - both: Show download path and filename.
desc: What to display in the download filename input. 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: downloads.open_dispatcher:
type: type:
name: String name: String
@ -1021,6 +1163,71 @@ hints.scatter:
Ignored for number hints. 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: hints.uppercase:
default: false default: false
type: Bool type: Bool
@ -1167,9 +1374,15 @@ prompt.radius:
## scrolling ## scrolling
scrolling.bar: scrolling.bar:
type: Bool type:
default: false name: String
desc: Show a scrollbar. 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: scrolling.smooth:
type: Bool type: Bool
@ -1349,12 +1562,27 @@ tabs.mousewheel_switching:
tabs.new_position.related: tabs.new_position.related:
default: next default: next
type: NewTabPosition 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: tabs.new_position.unrelated:
default: last default: last
type: NewTabPosition 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: tabs.padding:
default: default:
@ -1498,6 +1726,23 @@ tabs.min_width:
This setting does not apply to pinned tabs, unless `tabs.pinned.shrink` is False. 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: tabs.width.indicator:
renamed: tabs.indicator.width renamed: tabs.indicator.width
@ -1753,7 +1998,7 @@ colors.completion.item.selected.border.bottom:
colors.completion.match.fg: colors.completion.match.fg:
default: '#ff4444' default: '#ff4444'
type: QssColor type: QtColor
desc: Foreground color of the matched text in the completion. desc: Foreground color of the matched text in the completion.
colors.completion.scrollbar.fg: colors.completion.scrollbar.fg:
@ -2440,6 +2685,7 @@ bindings.default:
.: repeat-command .: repeat-command
<Ctrl-p>: tab-pin <Ctrl-p>: tab-pin
<Alt-m>: tab-mute <Alt-m>: tab-mute
gD: tab-give
q: record-macro q: record-macro
"@": run-macro "@": run-macro
tsh: config-cycle -p -t -u *://{url:host}/* content.javascript.enabled ;; reload 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 tPH: config-cycle -p -u *://*.{url:host}/* content.plugins ;; reload
tpu: config-cycle -p -t -u {url} content.plugins ;; reload tpu: config-cycle -p -t -u {url} content.plugins ;; reload
tPu: config-cycle -p -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: insert:
<Ctrl-E>: open-editor <Ctrl-E>: open-editor
<Shift-Ins>: insert-text {primary} <Shift-Ins>: insert-text {primary}
@ -2465,7 +2717,7 @@ bindings.default:
<Ctrl-B>: hint all tab-bg <Ctrl-B>: hint all tab-bg
<Escape>: leave-mode <Escape>: leave-mode
passthrough: passthrough:
<Ctrl-V>: leave-mode <Shift-Escape>: leave-mode
command: command:
<Ctrl-P>: command-history-prev <Ctrl-P>: command-history-prev
<Ctrl-N>: command-history-next <Ctrl-N>: command-history-next
@ -2499,6 +2751,7 @@ bindings.default:
prompt: prompt:
<Return>: prompt-accept <Return>: prompt-accept
<Ctrl-X>: prompt-open-download <Ctrl-X>: prompt-open-download
<Ctrl-P>: prompt-open-download --pdfjs
<Shift-Tab>: prompt-item-focus prev <Shift-Tab>: prompt-item-focus prev
<Up>: prompt-item-focus prev <Up>: prompt-item-focus prev
<Tab>: prompt-item-focus next <Tab>: prompt-item-focus next

View File

@ -276,7 +276,24 @@ class YamlConfig(QObject):
del settings['bindings.default'] del settings['bindings.default']
self._mark_changed() 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, 'tabs.favicons.show', 'always', 'never')
self._migrate_bool(settings, 'scrolling.bar',
'when-searching', 'never')
self._migrate_bool(settings, 'qt.force_software_rendering', self._migrate_bool(settings, 'qt.force_software_rendering',
'software-opengl', 'none') 'software-opengl', 'none')

View File

@ -28,6 +28,7 @@ from qutebrowser.config import (config, configdata, configfiles, configtypes,
configexc, configcommands) configexc, configcommands)
from qutebrowser.utils import (objreg, usertypes, log, standarddir, message, from qutebrowser.utils import (objreg, usertypes, log, standarddir, message,
qtutils) qtutils)
from qutebrowser.config import configcache
from qutebrowser.misc import msgbox, objects from qutebrowser.misc import msgbox, objects
@ -44,6 +45,7 @@ def early_init(args):
config.instance = config.Config(yaml_config=yaml_config) config.instance = config.Config(yaml_config=yaml_config)
config.val = config.ConfigContainer(config.instance) config.val = config.ConfigContainer(config.instance)
config.key_instance = config.KeyConfig(config.instance) config.key_instance = config.KeyConfig(config.instance)
config.cache = configcache.ConfigCache()
yaml_config.setParent(config.instance) yaml_config.setParent(config.instance)
for cf in config.change_filters: for cf in config.change_filters:
@ -89,6 +91,8 @@ def _init_envvars():
os.environ['QT_XCB_FORCE_SOFTWARE_OPENGL'] = '1' os.environ['QT_XCB_FORCE_SOFTWARE_OPENGL'] = '1'
elif software_rendering == 'qt-quick': elif software_rendering == 'qt-quick':
os.environ['QT_QUICK_BACKEND'] = 'software' 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: if config.val.qt.force_platform is not None:
os.environ['QT_QPA_PLATFORM'] = config.val.qt.force_platform 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] argv += ['--' + arg for arg in config.val.qt.args]
if objects.backend == usertypes.Backend.QtWebEngine: if objects.backend == usertypes.Backend.QtWebEngine:
if not qtutils.version_check('5.11', compiled=False): argv += list(_qtwebengine_args())
# WORKAROUND equivalent to
# https://codereview.qt-project.org/#/c/217932/
# Needed for Qt < 5.9.5 and < 5.10.1
argv.append('--disable-shared-workers')
if config.val.qt.force_software_rendering == 'chromium':
argv.append('--disable-gpu')
if not config.val.content.canvas_reading:
argv.append('--disable-reading-from-canvas')
if not qtutils.version_check('5.11'):
# On Qt 5.11, we can control this via QWebEngineSettings
if not config.val.content.autoplay:
argv.append('--autoplay-policy=user-gesture-required')
if config.val.content.webrtc_public_interfaces_only:
argv.append('--force-webrtc-ip-handling-policy='
'default_public_interface_only')
return argv 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

View File

@ -60,8 +60,8 @@ from PyQt5.QtGui import QColor, QFont
from PyQt5.QtWidgets import QTabWidget, QTabBar from PyQt5.QtWidgets import QTabWidget, QTabBar
from qutebrowser.commands import cmdutils from qutebrowser.commands import cmdutils
from qutebrowser.config import configexc from qutebrowser.config import configexc, configutils
from qutebrowser.utils import standarddir, utils, qtutils, urlutils from qutebrowser.utils import standarddir, utils, qtutils, urlutils, urlmatch
from qutebrowser.keyinput import keyutils from qutebrowser.keyinput import keyutils
@ -123,7 +123,7 @@ class BaseType:
"""A type used for a setting value. """A type used for a setting value.
Attributes: 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: Class attributes:
valid_values: Possible values if they can be expressed as a fixed valid_values: Possible values if they can be expressed as a fixed
@ -149,6 +149,9 @@ class BaseType:
value: The value to check. value: The value to check.
pytype: A Python type to check the value against. 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 if (value is None or (pytype == list and value == []) or
(pytype == dict and value == {})): (pytype == dict and value == {})):
if not self.none_ok: if not self.none_ok:
@ -309,7 +312,9 @@ class MappingType(BaseType):
def to_py(self, value): def to_py(self, value):
self._basic_py_validation(value, str) self._basic_py_validation(value, str)
if not value: if value is configutils.UNSET:
return value
elif not value:
return None return None
self._validate_valid_values(value.lower()) self._validate_valid_values(value.lower())
return self.MAPPING[value.lower()] return self.MAPPING[value.lower()]
@ -367,7 +372,9 @@ class String(BaseType):
def to_py(self, value): def to_py(self, value):
self._basic_py_validation(value, str) self._basic_py_validation(value, str)
if not value: if value is configutils.UNSET:
return value
elif not value:
return None return None
self._validate_encoding(value) self._validate_encoding(value)
@ -399,7 +406,9 @@ class UniqueCharString(String):
def to_py(self, value): def to_py(self, value):
value = super().to_py(value) value = super().to_py(value)
if not value: if value is configutils.UNSET:
return value
elif not value:
return None return None
# Check for duplicate values # Check for duplicate values
@ -455,7 +464,9 @@ class List(BaseType):
def to_py(self, value): def to_py(self, value):
self._basic_py_validation(value, list) self._basic_py_validation(value, list)
if not value: if value is configutils.UNSET:
return value
elif not value:
return [] return []
for val in value: for val in value:
@ -534,6 +545,9 @@ class ListOrValue(BaseType):
return value return value
def to_py(self, value): def to_py(self, value):
if value is configutils.UNSET:
return value
try: try:
return [self.valtype.to_py(value)] return [self.valtype.to_py(value)]
except configexc.ValidationError: except configexc.ValidationError:
@ -577,7 +591,8 @@ class FlagList(List):
def to_py(self, value): def to_py(self, value):
vals = super().to_py(value) vals = super().to_py(value)
self._check_duplicates(vals) if vals is not configutils.UNSET:
self._check_duplicates(vals)
return vals return vals
def complete(self): def complete(self):
@ -764,7 +779,9 @@ class Perc(_Numeric):
def to_py(self, value): def to_py(self, value):
self._basic_py_validation(value, (float, int, str)) self._basic_py_validation(value, (float, int, str))
if not value: if value is configutils.UNSET:
return value
elif not value:
return None return None
if isinstance(value, str): if isinstance(value, str):
@ -903,13 +920,49 @@ class QtColor(BaseType):
* An SVG color name as specified in * An SVG color name as specified in
http://www.w3.org/TR/SVG/types.html#ColorKeywords[the W3C specification]. http://www.w3.org/TR/SVG/types.html#ColorKeywords[the W3C specification].
* transparent (no color) * 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): def to_py(self, value):
self._basic_py_validation(value, str) self._basic_py_validation(value, str)
if not value: if value is configutils.UNSET:
return value
elif not value:
return None 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) color = QColor(value)
if color.isValid(): if color.isValid():
return color return color
@ -936,7 +989,9 @@ class QssColor(BaseType):
def to_py(self, value): def to_py(self, value):
self._basic_py_validation(value, str) self._basic_py_validation(value, str)
if not value: if value is configutils.UNSET:
return value
elif not value:
return None return None
functions = ['rgb', 'rgba', 'hsv', 'hsva', 'qlineargradient', functions = ['rgb', 'rgba', 'hsv', 'hsva', 'qlineargradient',
@ -981,7 +1036,9 @@ class Font(BaseType):
def to_py(self, value): def to_py(self, value):
self._basic_py_validation(value, str) self._basic_py_validation(value, str)
if not value: if value is configutils.UNSET:
return value
elif not value:
return None return None
if not self.font_regex.fullmatch(value): # pragma: no cover if not self.font_regex.fullmatch(value): # pragma: no cover
@ -1000,7 +1057,9 @@ class FontFamily(Font):
def to_py(self, value): def to_py(self, value):
self._basic_py_validation(value, str) self._basic_py_validation(value, str)
if not value: if value is configutils.UNSET:
return value
elif not value:
return None return None
match = self.font_regex.fullmatch(value) match = self.font_regex.fullmatch(value)
@ -1024,7 +1083,9 @@ class QtFont(Font):
def to_py(self, value): def to_py(self, value):
self._basic_py_validation(value, str) self._basic_py_validation(value, str)
if not value: if value is configutils.UNSET:
return value
elif not value:
return None return None
style_map = { style_map = {
@ -1136,7 +1197,9 @@ class Regex(BaseType):
def to_py(self, value): def to_py(self, value):
"""Get a compiled regex from either a string or a regex object.""" """Get a compiled regex from either a string or a regex object."""
self._basic_py_validation(value, (str, self._regex_type)) self._basic_py_validation(value, (str, self._regex_type))
if not value: if value is configutils.UNSET:
return value
elif not value:
return None return None
elif isinstance(value, str): elif isinstance(value, str):
return self._compile_regex(value) return self._compile_regex(value)
@ -1214,7 +1277,9 @@ class Dict(BaseType):
def to_py(self, value): def to_py(self, value):
self._basic_py_validation(value, dict) self._basic_py_validation(value, dict)
if not value: if value is configutils.UNSET:
return value
elif not value:
return self._fill_fixed_keys({}) return self._fill_fixed_keys({})
self._validate_keys(value) self._validate_keys(value)
@ -1230,7 +1295,7 @@ class Dict(BaseType):
if not value: if not value:
# An empty Dict is treated just like None -> empty string # An empty Dict is treated just like None -> empty string
return '' return ''
return json.dumps(value) return json.dumps(value, sort_keys=True)
def to_doc(self, value, indent=0): def to_doc(self, value, indent=0):
if not value: if not value:
@ -1256,7 +1321,9 @@ class File(BaseType):
def to_py(self, value): def to_py(self, value):
self._basic_py_validation(value, str) self._basic_py_validation(value, str)
if not value: if value is configutils.UNSET:
return value
elif not value:
return None return None
value = os.path.expanduser(value) value = os.path.expanduser(value)
@ -1282,7 +1349,9 @@ class Directory(BaseType):
def to_py(self, value): def to_py(self, value):
self._basic_py_validation(value, str) self._basic_py_validation(value, str)
if not value: if value is configutils.UNSET:
return value
elif not value:
return None return None
value = os.path.expandvars(value) value = os.path.expandvars(value)
value = os.path.expanduser(value) value = os.path.expanduser(value)
@ -1309,7 +1378,9 @@ class FormatString(BaseType):
def to_py(self, value): def to_py(self, value):
self._basic_py_validation(value, str) self._basic_py_validation(value, str)
if not value: if value is configutils.UNSET:
return value
elif not value:
return None return None
try: try:
@ -1341,8 +1412,10 @@ class ShellCommand(List):
def to_py(self, value): def to_py(self, value):
value = super().to_py(value) value = super().to_py(value)
if not value: if value is configutils.UNSET:
return value return value
elif not value:
return []
if (self.placeholder and if (self.placeholder and
'{}' not in ' '.join(value) and '{}' not in ' '.join(value) and
@ -1365,7 +1438,9 @@ class Proxy(BaseType):
def to_py(self, value): def to_py(self, value):
self._basic_py_validation(value, str) self._basic_py_validation(value, str)
if not value: if value is configutils.UNSET:
return value
elif not value:
return None return None
try: try:
@ -1401,7 +1476,9 @@ class SearchEngineUrl(BaseType):
def to_py(self, value): def to_py(self, value):
self._basic_py_validation(value, str) self._basic_py_validation(value, str)
if not value: if value is configutils.UNSET:
return value
elif not value:
return None return None
if not ('{}' in value or '{0}' in value): if not ('{}' in value or '{0}' in value):
@ -1429,7 +1506,9 @@ class FuzzyUrl(BaseType):
def to_py(self, value): def to_py(self, value):
self._basic_py_validation(value, str) self._basic_py_validation(value, str)
if not value: if value is configutils.UNSET:
return value
elif not value:
return None return None
try: try:
@ -1463,6 +1542,9 @@ class Padding(Dict):
def to_py(self, value): def to_py(self, value):
d = super().to_py(value) d = super().to_py(value)
if d is configutils.UNSET:
return d
return PaddingValues(**d) return PaddingValues(**d)
@ -1472,7 +1554,9 @@ class Encoding(BaseType):
def to_py(self, value): def to_py(self, value):
self._basic_py_validation(value, str) self._basic_py_validation(value, str)
if not value: if value is configutils.UNSET:
return value
elif not value:
return None return None
try: try:
codecs.lookup(value) codecs.lookup(value)
@ -1529,7 +1613,9 @@ class Url(BaseType):
def to_py(self, value): def to_py(self, value):
self._basic_py_validation(value, str) self._basic_py_validation(value, str)
if not value: if value is configutils.UNSET:
return value
elif not value:
return None return None
qurl = QUrl.fromUserInput(value) qurl = QUrl.fromUserInput(value)
@ -1545,7 +1631,9 @@ class SessionName(BaseType):
def to_py(self, value): def to_py(self, value):
self._basic_py_validation(value, str) self._basic_py_validation(value, str)
if not value: if value is configutils.UNSET:
return value
elif not value:
return None return None
if value.startswith('_'): if value.startswith('_'):
raise configexc.ValidationError(value, "may not start with '_'!") raise configexc.ValidationError(value, "may not start with '_'!")
@ -1593,8 +1681,10 @@ class ConfirmQuit(FlagList):
def to_py(self, value): def to_py(self, value):
values = super().to_py(value) values = super().to_py(value)
if not values: if values is configutils.UNSET:
return values return values
elif not values:
return []
# Never can't be set with other options # Never can't be set with other options
if 'never' in values and len(values) > 1: if 'never' in values and len(values) > 1:
@ -1630,7 +1720,9 @@ class TimestampTemplate(BaseType):
def to_py(self, value): def to_py(self, value):
self._basic_py_validation(value, str) self._basic_py_validation(value, str)
if not value: if value is configutils.UNSET:
return value
elif not value:
return None return None
try: try:
@ -1654,10 +1746,33 @@ class Key(BaseType):
def to_py(self, value): def to_py(self, value):
self._basic_py_validation(value, str) self._basic_py_validation(value, str)
if not value: if value is configutils.UNSET:
return value
elif not value:
return None return None
try: try:
return keyutils.KeySequence.parse(value) return keyutils.KeySequence.parse(value)
except keyutils.KeyParseError as e: except keyutils.KeyParseError as e:
raise configexc.ValidationError(value, str(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))

View File

@ -111,7 +111,7 @@ li {
<li> <li>
You can manually download the pdf.js archive You can manually download the pdf.js archive
<a href="https://mozilla.github.io/pdf.js/getting_started/#download">here</a> <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> <br>
<span class="warning">Warning:</span> Using this method you are <span class="warning">Warning:</span> Using this method you are
responsible for yourself to keep the installation updated! If a responsible for yourself to keep the installation updated! If a

View File

@ -3,7 +3,8 @@
{% block script %} {% block script %}
var cset = function(option, value) { var cset = function(option, value) {
// FIXME:conf we might want some error handling here? // 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); url += "&value=" + encodeURIComponent(value);
var xhr = new XMLHttpRequest(); var xhr = new XMLHttpRequest();
xhr.open("GET", url); xhr.open("GET", url);
@ -33,7 +34,7 @@ input { width: 98%; }
<th>Setting</th> <th>Setting</th>
<th>Value</th> <th>Value</th>
</tr> </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> <tr>
<!-- FIXME: convert to string properly --> <!-- FIXME: convert to string properly -->
<td class="setting">{{ option.name }} (Current: {{ confget(option.name) | string |truncate(100) }}) <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