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
environment:
PYTHONUNBUFFERED: 1
PYTHON: C:\Python36\python.exe
PYTHON: C:\Python36-x64\python.exe
matrix:
- TESTENV: py36-pyqt510
- TESTENV: py36-pyqt511
- TESTENV: pylint
install:
- '%PYTHON% -m pip install -U pip'
- '%PYTHON% -m pip install -r misc\requirements\requirements-tox.txt'
- 'set PATH=%PATH%;C:\Python36'
- 'set PATH=C:\Python36-x64;%PATH'
test_script:
- '%PYTHON% -m tox -e %TESTENV%'

1
.gitattributes vendored Normal file
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/completion/* @rcorre
qutebrowser/completion/** @rcorre
qutebrowser/misc/sql.py @rcorre
tests/end2end/features/completion.feature @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
issue (or open one). This makes sure there is no duplicate work done.
@ -7,6 +15,5 @@
- If you are stuck somewhere or have questions,
https://github.com/qutebrowser/qutebrowser#getting-help[please ask]!
See the full contribution documentation for details and other useful hints:
include::../doc/contributing.asciidoc[]
See the link:../doc/contributing.asciidoc[full contribution documentation] for
details and other useful hints.

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
/README.html
/qutebrowser/html/doc/
/qutebrowser/html/*.html
/.venv*
/.coverage
/htmlcov

View File

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

View File

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

View File

@ -59,7 +59,7 @@ Getting help
You can get help in the IRC channel
irc://irc.freenode.org/#qutebrowser[`#qutebrowser`] on
http://freenode.net/[Freenode]
https://freenode.net/[Freenode]
(https://webchat.freenode.net/?channels=#qutebrowser[webchat]), or by writing a
message to the
https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser[mailinglist] at
@ -96,24 +96,25 @@ Requirements
The following software and libraries are required to run qutebrowser:
* http://www.python.org/[Python] 3.5 or newer (3.6 recommended)
* http://qt.io/[Qt] 5.7.1 or newer (5.10 recommended) with the following modules:
* https://www.python.org/[Python] 3.5 or newer (3.6 recommended)
* https://www.qt.io/[Qt] 5.7.1 or newer (5.11 recommended, support for < 5.9
will be dropped soon) with the following modules:
- QtCore / qtbase
- QtQuick (part of qtbase in some distributions)
- QtSQL (part of qtbase in some distributions)
- QtOpenGL
- QtWebEngine, or
- QtWebKit - only the
link:https://github.com/annulen/webkit/wiki[updated fork] (5.212) is
supported
* http://www.riverbankcomputing.com/software/pyqt/intro[PyQt] 5.7.0 or newer
(5.10 recommended) for Python 3
- alternatively QtWebKit - support for QtWebKit will be dropped soon, and
only the link:https://github.com/annulen/webkit/wiki[updated fork] (5.212)
is supported
* https://www.riverbankcomputing.com/software/pyqt/intro[PyQt] 5.7.0 or newer
(5.11 recommended, support for < 5.9 will be dropped soon) for Python 3
* https://pypi.python.org/pypi/setuptools/[pkg_resources/setuptools]
* http://fdik.org/pyPEG/[pyPEG2]
* https://fdik.org/pyPEG/[pyPEG2]
* http://jinja.pocoo.org/[jinja2]
* http://pygments.org/[pygments]
* https://github.com/yaml/pyyaml[PyYAML]
* http://www.attrs.org/[attrs]
* https://www.attrs.org/[attrs]
The following libraries are optional:
@ -143,6 +144,18 @@ get in touch!
* PayPal: me@the-compiler.org
* Bitcoin: link:bitcoin:1PMzbcetAHfpxoXww8Bj5XqquHtVvMjJtE[1PMzbcetAHfpxoXww8Bj5XqquHtVvMjJtE]
Sponsors
--------
Thanks a lot to https://www.macstadium.com/[MacStadium] for supporting
qutebrowser with a free hosted Mac Mini via their
https://www.macstadium.com/opensource[Open Source Project].
(They don't require including this here - I've just been very happy with their
offer, and without them, no macOS releases or tests would exist)
image:.github/img/macstadium.png["powered by MacStadium",width=200,link="https://www.macstadium.com/"]
Authors
-------
@ -152,7 +165,7 @@ https://github.com/qutebrowser/qutebrowser/graphs/contributors[hundreds of contr
Additionally, the following people have contributed graphics:
* Jad/link:http://yelostudio.com[yelo] (new icon)
* Jad/link:https://yelostudio.com[yelo] (new icon)
* WOFall (original icon)
* regines (key binding cheatsheet)
@ -170,18 +183,16 @@ Active
* https://fanglingsu.github.io/vimb/[vimb] (C, GTK+ with WebKit2)
* https://luakit.github.io/luakit/[luakit] (C/Lua, GTK+ with WebKit2)
* http://surf.suckless.org/[surf] (C, GTK+ with WebKit1/WebKit2)
* http://www.uzbl.org/[uzbl] (C, GTK+ with WebKit1/WebKit2)
* https://surf.suckless.org/[surf] (C, GTK+ with WebKit1/WebKit2)
* https://github.com/next-browser/next/[next] (Lisp, Emacs-like, GTK+ with WebKit)
* https://github.com/parkouss/webmacs/[webmacs] (Python, Emacs-like with QtWebEngine)
* Chrome/Chromium addons:
https://github.com/1995eaton/chromium-vim[cVim],
http://vimium.github.io/[Vimium],
https://vimium.github.io/[Vimium],
https://github.com/brookhong/Surfingkeys[Surfingkeys],
https://key.saka.io/[Saka Key]
* Firefox addons (based on WebExtensions):
https://addons.mozilla.org/en-GB/firefox/addon/vimium-ff/[Vimium-FF] (experimental),
https://key.saka.io[Saka Key],
https://github.com/ueokande/vim-vixen[Vim Vixen],
https://github.com/shinglyu/QuantumVim[QuantumVim],
https://github.com/amedama41/vvimpulation[VVimpulation],
https://github.com/cmcaine/tridactyl[Tridactyl] (working
on a https://bugzilla.mozilla.org/show_bug.cgi?id=1215061[better API] for
keyboard integration in Firefox).
@ -192,17 +203,23 @@ Inactive
* https://bitbucket.org/portix/dwb[dwb] (C, GTK+ with WebKit1,
https://bitbucket.org/portix/dwb/pull-requests/22/several-cleanups-to-increase-portability/diff[unmaintained] -
main inspiration for qutebrowser)
* http://sourceforge.net/p/vimprobable/wiki/Home/[vimprobable] (C, GTK+ with
* https://sourceforge.net/p/vimprobable/wiki/Home/[vimprobable] (C, GTK+ with
WebKit1)
* http://pwmt.org/projects/jumanji/[jumanji] (C, GTK+ with WebKit1)
* https://wiki.archlinux.org/index.php?title=Jumanji[jumanji] (C, GTK+ with WebKit1,
original site is gone but Arch Linux has some data)
* http://conkeror.org/[conkeror] (Javascript, Emacs-like, XULRunner/Gecko)
* https://www.uzbl.org/[uzbl] (C, GTK+ with WebKit1/WebKit2)
* Firefox addons (not based on WebExtensions or no recent activity):
http://www.vimperator.org/[Vimperator],
http://5digits.org/pentadactyl/[Pentadactyl],
http://bug.5digits.org/pentadactyl/index[Pentadactyl],
https://github.com/akhodakivskiy/VimFx[VimFx],
https://key.saka.io[Saka Key],
https://github.com/shinglyu/QuantumVim[QuantumVim],
* Chrome/Chromium addons:
https://chrome.google.com/webstore/detail/vichrome/gghkfhpblkcmlkmpcpgaajbbiikbhpdi?hl=en[ViChrome],
https://github.com/jinzhu/vrome[Vrome]
https://key.saka.io[Saka Key],
https://github.com/1995eaton/chromium-vim[cVim],
License
-------
@ -229,4 +246,4 @@ display PDF files in the browser. Windows releases come with a bundled pdf.js.
pdf.js is distributed under the terms of the Apache License. You can
find a copy of the license in `qutebrowser/3rdparty/pdfjs/LICENSE` (in the
Windows release or after running `scripts/dev/update_3rdparty.py`), or online
http://www.apache.org/licenses/LICENSE-2.0.html[here].
https://www.apache.org/licenses/LICENSE-2.0.html[here].

View File

@ -15,12 +15,219 @@ breaking changes (such as renamed commands) can happen in minor releases.
// `Fixed` for any bug fixes.
// `Security` to invite users to upgrade in case of vulnerabilities.
v1.4.0 (unreleased)
v1.6.0 (unreleased)
-------------------
Added
~~~~~
- New `tabs.new_position.stacking` setting which controls whether new tabs
opened from a page should stack on each other or not.
- New `completion.open_categories` setting which allows to configure which
categories are shown in the `:open` completion, and how they are ordered.
- New config manipulation commands:
* `:config-dict-add` and `:config-list-add` to a new element to a dict/list
setting.
* `:config-dict-remove` and `:config-list-remove` to remove an element from a
dict/list setting.
- New `hints.selectors` setting which allows to configure what CSS selectors
are used for hints, and also allows adding custom hint groups.
Changed
~~~~~~~
- `:q` now closes current window instead of quitting qutebrowser completely
(`:close`), while `:qa` quits (`:quit`). The behavior of `:wq` remains
unchanged (`:quit --save`), as closing a window while saving the session
doesn't make sense.
- Completion highlighting is now done differently (using QSyntaxHighlither),
which should fix some highlighting corner-cases.
- The `QtColor` config type now also understands colors like `rgb(...)`.
- `:yank` now has a `--quiet` option which causes it to not display a message.
- The `:open` completion now also shows search engines by default.
- The `content.host_blocking.enabled` setting now supports URL patterns, so the
adblocker can be disabled on a given page.
- Elements with a `tabindex` attribute now also get hints by default.
Fixed
~~~~~
- Invalid world IDs now get rejected for `:jseval` and GreaseMonkey scripts.
- When websites suggest download filenames with invalid characters, those are
now correctly replaced.
v1.5.1
------
Fixed
~~~~~
- Flickering when opening/closing tabs (as soon as more than 10 are open) on
some pages.
- PDF.js is now bundled again with the macOS/Windows release.
- PDF.js is now searched in the correct path (if not installed system-wide)
instead of hardcoding `~/.local/share/qutebrowser`.
- Improved logging for PDF.js resources which fail to load.
- Crash when closing a tab after doing a search.
- Tabs appearing when hidden after e.g. closing tabs.
v1.5.0
------
Added
~~~~~
- Rewritten PDF.js support:
* PDF.js support and the `content.pdfjs` setting are now also available with
QtWebEngine.
* Opening a PDF file now doesn't start a second request anymore.
* Opening PDFs on https:// sites now works properly.
* New `--pdfjs` flag for `prompt-open-download`, so PDFs can be opened in
PDF.js with `<Ctrl-P>` in the download prompt.
- New settings:
* `content.mouse_lock` to handle HTML5 pointer locking.
* `completion.web_history.exclude` which hides a list of URL patterns from
the completion.
* `qt.process_model` which can be used to change Chromium's process model.
* `qt.low_end_device_mode` which turns on Chromium's low-end device mode.
This mode uses less RAM, but the expense of performance.
* `content.webrtc_ip_handling_policy`, which allows more
fine-grained/restrictive control about which IPs are exposed via WebRTC.
* `tabs.max_width` which allows to have a more "normal" look for tabs.
* `content.mute` which allows to mute pages (or all tabs) by default.
- Running qutebrowser with QtWebKit or Qt < 5.9 now shows a warning (only
once), as support for those is going to be removed in a future release.
- New t[iI][hHu] default bindings (similar to `tsh` etc.) to toggle images.
- The qute-pass userscript now has optional OTP support.
- When `:spawn --userscript` is called with a count, that count is now
passed to userscripts as `$QUTE_COUNT`.
Changed
~~~~~~~
- Windows and macOS releases now bundle Python 3.7, PyQt 5.11.3 and Qt 5.11.2.
QtWebEngine includes security fixes up to Chromium 68.0.3440.75 and
http://code.qt.io/cgit/qt/qtwebengine.git/tree/dist/changes-5.11.2/?h=v5.11.2[various other fixes].
- Various performance improvements when many tabs are opened.
- The `content.headers.referer` setting now works on QtWebEngine.
- The `:repeat` command now takes a count which is multiplied with the given
"times" argument.
- The default keybinding to leave passthrough mode was changed from `<Ctrl-V>`
to `<Shift-Escape>`, which makes pasting from the clipboard easier in
passthrough mode and is also unlikely to conflict with webpage bindings.
- The `app_id` is now set to `qutebrowser` for Wayland.
- `Command` or `Cmd` can now be used (instead of `Meta`) to map the Command key
on macOS.
- Using `:set option` now shows the value of the setting (like `:set option?`
already did).
- The `completion.web_history_max_items` setting got renamed to
`completion.web_history.max_items`.
- The Makefile shipped with qutebrowser now supports overriding variables
`DATADIR` and `MANDIR`.
- Regenerating completion history now shows a progress dialog.
- The `content.autoplay` setting now supports URL patterns on Qt >= 5.11.
- The `content.host_blocking.whitelist` setting now takes a list of URL
patterns instead of globs.
- In passthrough mode, Ctrl + Mousewheel now also gets passed through to the
page instead of zooming.
- Editing text in an external editor now simulates a JS "input" event, which
improves compatibility with websites reacting via JS to input.
- The `qute://settings` page is now properly sorted on Python 3.5.
- `:zoom`, `:zoom-in` and `:zoom-out` now have a `--quiet` switch which causes
them to not display a message.
- The `scrolling.bar` setting now takes three values instead of being a
boolean: `always`, `never`, and `when-searching` (which only displays it
while a search is active).
- '@@' now repeats the last run macro.
- The `content.host_blocking.lists` setting now accepts a `file://` URL to a
directory, and reads all files in that directory.
- The `:tab-give` and `:tab-take` command now have a new flag `--keep` which
causes them to keep the old tab around.
- `:navigate` now clears the URL query.
Fixed
~~~~~
- `qute://` pages now work properly on Qt 5.11.2
- Error when passing a substring with spaces to `:tab-take`.
- Greasemonkey scripts which start with an UTF-8 BOM are now handled correctly.
- When no documentation has been generated, the plaintext documentation now can
be shown for more files such as `qute://help/userscripts.html`.
- Crash when doing initial run on Wayland without XWayland.
- Crash when trying to load an empty session file.
- `:hint` with an invalid `--mode=` value now shows a proper error.
- Rare crash on Qt 5.11.2 when clicking on `<select>` elements.
- Rare crash related to the completion.
Removed
~~~~~~~
- Support for importing pre-v1.0.0 history files has been removed.
- The `content.webrtc_public_interfaces_only` setting has been removed and
replaced by `content.webrtc_ip_handling_policy`.
v1.4.2
------
Changed
~~~~~~~
- The `content.xss_auditing` setting is now enabled by default, to mirror
Chromium's rather than Qt's default behavior.
- Long URLs in the statusbar are now elided at the end rather than in the
middle, to make sure the hostname is completely visible whenever possible.
Fixed
~~~~~
- Crash in Qt 5.7.1 when a website uses `window.print()`.
- The workaround for Nouveau graphic drivers now works properly again.
- Crash when using `:follow-selected` with a link which is outside of the view.
- Workaround for windows not showing as urgent with some window managers
(like i3).
- Crash when opening URLs with some unicode characters (IDNA 2008). Those URLs
still won't open though, due to missing support in Qt.
- Crash when a download directory which can't be created is configured.
- Crash in the `importer.py` script when importing Chrome bookmarks from newer Chrome versions.
- The `content.webrtc_public_interfaces_only` option didn't work on Qt 5.11 previously (it now does).
Note it still does not work on Qt 5.10 (due to a Qt bug) and Qt < 5.9.2.
- Repeated escaping of entries in `qute://log` when refreshing page.
- The host blocker doesn't block 0.0.0.0 anymore.
- Crash when using :// as URL pattern.
- The `:buffer` completion now sorts tabs with indices >= 10 correctly again.
v1.4.1
------
Security
~~~~~~~~
- CVE-2018-10895: Fix CSRF issue on the qute://settings page, leading to
possible arbitrary code execution. See the related GitHub issue for details:
https://github.com/qutebrowser/qutebrowser/issues/4060
Fixed
~~~~~
- Rare crash when an error occurs in downloads.
- Newlines are now stripped from the :version pastebin URL.
- There's a new `mkvenv-pypi-old` environment in `tox.ini` which installs an
older Qt, which is needed on Ubuntu 16.04.
- Worked around a Qt issue which redirects to a `chrome-error://` page when
trying to use U2F.
- The `link_pyqt.py` script now works correctly with PyQt 5.11.
- The Windows installer now uninstalls the old version before installing the
new one, fixing issues with qutebrowser not starting after installing v1.4.0
over v1.3.3.
v1.4.0
------
Added
~~~~~
- Support for the bundled `sip` module in PyQt 5.11 and other changes in
Qt/PyQt 5.11.x.
- New `--debug-flag log-requests` to log requests to the debug log for
debugging.
- New `--first` flag for `:hint` (bound to `gi` for inputs) which automatically
@ -40,11 +247,14 @@ Added
* Support for requesting persistent storage via
`navigator.webkitPersistentStorage.requestQuota` with a new
`content.persistent_storage` setting (requires Qt 5.11).
This setting also supports URL patterns.
* Support for registering custom protocol handlers via
`navigator.registerProtocolHandler` with a new
`content.register_protocol_handler` setting (requires Qt 5.11).
This setting also supports URL patterns.
* Support for WebRTC screen sharing with a new `content.desktop_capture`
setting (requires Qt 5.10).
This setting also supports URL patterns.
* New `content.autoplay` setting to enable/disable automatic video playback
(requires Qt 5.10).
* New `content.webrtc_public_interfaces_only` setting to only expose public
@ -55,8 +265,17 @@ Added
Changed
~~~~~~~
- The following settings now support URL patterns:
* `content.headers.do_not_track`
* `content.headers.custom`
* `content.headers.accept_language`
* `content.headers.user_agent`
* `content.ssl_strict`
* `content.geolocation`
* `content.notifications`
* `content.media_capture`
- The Windows/macOS releases now bundle Qt 5.11.1 which is based on
Chromium 65.0.3325.151 with security fixes up to Chromium 67.0.3396.79.
Chromium 65.0.3325.151 with security fixes up to Chromium 67.0.3396.87.
- New short flags for commandline arguments: `-B` and `-T` for `--basedir` and
`--temp-basedir`; `-d` and `-D` for `--debug` and `--debug-flag`.
- Deleting history items via `:history-clear` or `:completion-item-del` now
@ -85,21 +304,60 @@ Changed
- Improved error messages when a setting needs a newer Qt version.
- QtWebEngine: Various improvements to make the cursor more visible in caret
browsing.
- When a prompt is opened in insert/passthrough mode, the mode is restored
after closing the prompt.
- On Qt 5.10 or newer, dictionaries are now read from the qutebrowser data
directory (e.g. `~/.local/share/qutebrowser`) instead of `/usr/share/qt`.
Existing dictionaries are copied over.
- If an error while parsing `~/.netrc` occurs, the cause of the error is now
logged.
- On Qt 5.9 or newer, certificate errors now show Chromium's detailed error
page.
- Greasemonkey scripts now support a "@qute-js-world" tag to run them in a
different JavaScript context.
Fixed
~~~~~
- Various subtle keyboard focus issues.
- The security fix in v1.3.3 caused URLs with ampersands
(`www.example.com?one=1&two=2`) to send the wrong arguments when clicked on
the `qute://history` page.
- Crash when opening a PDF page with PDF.js enabled (on QtWebKit), but no
PDF.js installed.
- Crash when closing a tab shortly after opening it.
Removed
~~~~~~~
- No prebuilt binaries for 32-bit Windows are supplied anymore. This is due to
Qt removing QtWebEngine support for those upstream. It might be possible to
distribute 32-bit binaries again with Qt 5.12 in December, but that will only
happen if it turns out enough people actually need 32-bit support.
- `:tab-detach` which has been deprecated in v1.1.0 has been removed.
- The `content.developer_extras` setting got removed. On QtWebKit, developer
extras are now automatically enabled when opening the inspector.
v1.3.3 (unreleased)
-------------------
v1.3.3
------
Security
~~~~~~~~
- An XSS vulnerability on the `qute://history` page allowed websites to inject
HTML into the page via a crafted title tag. This could allow them to steal
your browsing history. If you're currently unable to upgrade, avoid using
`:history`. A CVE request for this issue is pending, see
https://github.com/qutebrowser/qutebrowser/issues/4011[#4011] for updates.
Fixed
~~~~~
- Crash in a workaround for a Qt 5.11 bug in rare circumstances.
- Workaround for a Qt bug which preserves searches between page loads.
- In v1.3.2 a dependency on the `PyQt5.QtQuickWidgets` module was accidentally
introduced. Since that module isn't packaged everywhere, it's been removed
again.
v1.3.2
------
@ -1479,7 +1737,7 @@ Changed
`tabs.bg/fg.selected.odd/even`.
- `:spawn --userscript` and `:hint` with the `userscript` target now look up
relative paths in `~/.local/share/qutebrowser/userscripts` or
`$XDG_DATA_DIR`. Using a binary in `$PATH` won't work anymore with
`$XDG_DATA_HOME`. Using a binary in `$PATH` won't work anymore with
`--userscript`.
- New design for error pages
- Link filtering for hints now checks if the text is contained anywhere in

View File

@ -5,6 +5,14 @@ The Compiler <mail@qutebrowser.org>
:data-uri:
:toc:
IMPORTANT: *Currently, bigger changes are going on in qutebrowser, as
part of a
https://lists.schokokeks.org/pipermail/qutebrowser-announce/2018-September/000051.html[student research project]
about adding a plugin API to qutebrowser and moving a lot of code from the code
into plugins.* Due to that, bandwidth for pull request review is currently
very limited, and contributions might lead to merge conflicts due to
ongoing refactorings.
I `&lt;3` footnote:[Of course, that says `<3` in HTML.] contributors!
This document contains guidelines for contributing to qutebrowser, as well as
@ -88,7 +96,7 @@ git format-patch origin/master <1>
Running qutebrowser
-------------------
After link:install.asciidoc#tox[installing qutebrowser via tox], you can run
After link:install{outfilesuffix}#tox[installing qutebrowser via tox], you can run
`.venv/bin/qutebrowser --debug --temp-basedir` to test your changes with debug
logging enabled and without affecting existing running instances.
@ -689,8 +697,6 @@ New PyQt release
~~~~~~~~~~~~~~~~
* See above.
* Install new PyQt in Windows VM (32- and 64-bit).
* Download new installer and update PyQt installer path in `ci_install.py`.
* Update `tox.ini`/`.travis.yml`/`.appveyor.yml` to test new versions.
qutebrowser release
@ -712,7 +718,7 @@ qutebrowser release
as closed.
* Linux: Run `git checkout v1.$x.$y && ./.venv/bin/python3 scripts/dev/build_release.py --upload v1.$x.$y`.
* Windows: Run `git checkout v1.X.Y; C:\Python36-32\python scripts\dev\build_release.py --asciidoc C:\Python27\python C:\asciidoc-8.6.9\asciidoc.py --upload v1.X.Y` (replace X/Y by hand).
* Windows: Run `git checkout v1.X.Y; py -3 scripts\dev\build_release.py --asciidoc C:\Python27\python %userprofile%\bin\asciidoc-8.6.10\asciidoc.py --upload v1.X.Y` (replace X/Y by hand).
* macOS: Run `git checkout v1.X.Y && python3 scripts/dev/build_release.py --upload v1.X.Y` (replace X/Y by hand).
* On server:
- Run `python3 scripts/dev/download_release.py v1.X.Y` (replace X/Y by hand).

View File

@ -5,10 +5,10 @@ The Compiler <mail@qutebrowser.org>
[qanda]
What is qutebrowser based on?::
qutebrowser uses http://www.python.org/[Python], http://qt.io/[Qt] and
http://www.riverbankcomputing.com/software/pyqt/intro[PyQt].
qutebrowser uses https://www.python.org/[Python], https://www.qt.io/[Qt] and
https://www.riverbankcomputing.com/software/pyqt/intro[PyQt].
+
The concept of it is largely inspired by http://portix.bitbucket.org/dwb/[dwb]
The concept of it is largely inspired by https://bitbucket.org/portix/dwb/[dwb]
and http://www.vimperator.org/vimperator[Vimperator]. Many actions and
key bindings are similar to dwb.
@ -16,34 +16,34 @@ Why another browser?::
It might be hard to believe, but I didn't find any browser which I was
happy with, so I started to write my own. Also, I needed a project to get
into writing GUI applications with Python and
link:http://qt.io/[Qt]/link:http://www.riverbankcomputing.com/software/pyqt/intro[PyQt].
link:https://www.qt.io/[Qt]/link:https://www.riverbankcomputing.com/software/pyqt/intro[PyQt].
+
Read the next few questions to find out why I was unhappy with existing
software.
What's wrong with link:http://portix.bitbucket.org/dwb/[dwb]/link:http://sourceforge.net/projects/vimprobable/[vimprobable]/link:https://mason-larobina.github.io/luakit/[luakit]/link:http://pwmt.org/projects/jumanji/[jumanji]/... (projects based on WebKitGTK)?::
Most of them are based on the http://webkitgtk.org/[WebKitGTK+]
http://webkitgtk.org/reference/webkitgtk/stable/index.html[WebKit1] API,
What's wrong with link:https://bitbucket.org/portix/dwb/[dwb]/link:https://sourceforge.net/projects/vimprobable/[vimprobable]/link:https://mason-larobina.github.io/luakit/[luakit]/jumanji/... (projects based on WebKitGTK)?::
Most of them are based on the https://webkitgtk.org/[WebKitGTK+]
https://webkitgtk.org/reference/webkitgtk/stable/index.html[WebKit1] API,
which causes a lot of crashes. As the GTK API using WebKit1 is
https://lists.webkit.org/pipermail/webkit-gtk/2014-March/001821.html[deprecated],
these bugs are never going to be fixed.
+
When qutebrowser was created, the newer
http://webkitgtk.org/reference/webkit2gtk/stable/index.html[WebKit2 API] lacked
https://webkitgtk.org/reference/webkit2gtk/stable/index.html[WebKit2 API] lacked
basic features like proxy support, and almost no projects have started porting
to WebKit2. In the meantime, this situation has improved a bit, but there are
still only a few projects which have some kind of WebKit2 support (see the
https://github.com/qutebrowser/qutebrowser#similar-projects[list of
alternatives]).
+
qutebrowser uses http://qt.io/[Qt] and
http://wiki.qt.io/QtWebEngine[QtWebEngine] by default (and supports
http://wiki.qt.io/QtWebKit[QtWebKit] optionally). QtWebEngine is based on
qutebrowser uses https://www.qt.io/[Qt] and
https://wiki.qt.io/QtWebEngine[QtWebEngine] by default (and supports
https://wiki.qt.io/QtWebKit[QtWebKit] optionally). QtWebEngine is based on
Google's https://www.chromium.org/Home[Chromium]. With an up-to-date Qt, it has
much more man-power behind it than WebKitGTK+ has, and thus supports more modern
web features - it's also arguably more secure.
What's wrong with https://www.mozilla.org/en-US/firefox/new/[Firefox] and link:http://5digits.org/pentadactyl/[Pentadactyl]/link:http://www.vimperator.org/vimperator[Vimperator]?::
What's wrong with https://www.mozilla.org/en-US/firefox/new/[Firefox] and link:http://bug.5digits.org/pentadactyl/[Pentadactyl]/link:http://www.vimperator.org/vimperator[Vimperator]?::
Firefox likes to break compatibility with addons on each upgrade, gets
slower and more bloated with every upgrade, and has some
https://blog.mozilla.org/advancingcontent/2014/02/11/publisher-transformation-with-users-at-the-center/[horrible
@ -51,20 +51,20 @@ What's wrong with https://www.mozilla.org/en-US/firefox/new/[Firefox] and link:h
+
Also, developing addons for it is a nightmare.
What's wrong with http://www.chromium.org/Home[Chromium] and https://vimium.github.io/[Vimium]?::
What's wrong with https://www.chromium.org/Home[Chromium] and https://vimium.github.io/[Vimium]?::
The Chrome plugin API doesn't seem to allow much freedom for plugin
writers, which results in Vimium not really having all the features you'd
expect from a proper minimal, vim-like browser.
Why Python?::
I enjoy writing Python since 2011, which made it one of the possible
choices. I wanted to use http://qt.io/[Qt] because of
http://wiki.qt.io/QtWebKit[QtWebKit] so I didn't have
http://wiki.qt.io/Category:LanguageBindings[many other choices]. I don't
choices. I wanted to use https://www.qt.io/[Qt] because of
https://wiki.qt.io/QtWebKit[QtWebKit] so I didn't have
https://wiki.qt.io/Category:LanguageBindings[many other choices]. I don't
like C++ and can't write it very well, so that wasn't an alternative.
But isn't Python too slow for a browser?::
http://www.infoworld.com/d/application-development/van-rossum-python-not-too-slow-188715[No.]
https://www.infoworld.com/d/application-development/van-rossum-python-not-too-slow-188715[No.]
I believe efficiency while coding is a lot more important than efficiency
while running. Also, most of the heavy lifting of qutebrowser is done by Qt
and WebKit in C++, with the
@ -74,7 +74,7 @@ Is qutebrowser secure?::
Most security issues are in the backend (which handles networking,
rendering, JavaScript, etc.) and not qutebrowser itself.
+
qutebrowser uses http://wiki.qt.io/QtWebEngine[QtWebEngine] by default.
qutebrowser uses https://wiki.qt.io/QtWebEngine[QtWebEngine] by default.
QtWebEngine is based on Google's https://www.chromium.org/Home[Chromium]. While
Qt only updates to a new Chromium release on every minor Qt release (all ~6
months), every patch release backports security fixes from newer Chromium
@ -84,26 +84,41 @@ do anything. Chromium's process isolation and
https://chromium.googlesource.com/chromium/src/+/master/docs/design/sandbox.md[sandboxing]
features are also enabled as a second line of defense.
+
http://wiki.qt.io/QtWebKit[QtWebKit] is also supported as an alternative
https://wiki.qt.io/QtWebKit[QtWebKit] is also supported as an alternative
backend, but hasn't seen new releases
https://github.com/annulen/webkit/releases[in a while]. It also doesn't have any
process isolation or sandboxing.
process isolation or sandboxing. See
https://github.com/qutebrowser/qutebrowser/issues/4039[#4039] for more details.
+
Security issues in qutebrowser's code happen very rarely (as per March 2018,
there has been one security issue caused by qutebrowser in over four years) and
are fixed timely. To report security bugs, please contact me directly at
mail@qutebrowser.org, GPG ID
Security issues in qutebrowser's code happen very rarely (as per July 2018,
there have been three security issues caused by qutebrowser in over 4.5 years).
Those were handled appropriately
(http://seclists.org/oss-sec/2018/q3/29[example]) and fixed timely. To report
security bugs, please contact me directly at mail@qutebrowser.org, GPG ID
https://www.the-compiler.org/pubkey.asc[0x916eb0c8fd55a072].
Is there an adblocker?::
There is a host-based adblocker which takes /etc/hosts-like lists. A "real"
adblocker has a
http://www.reddit.com/r/programming/comments/25j41u/adblock_pluss_effect_on_firefoxs_memory_usage/chhpomw[big
https://www.reddit.com/r/programming/comments/25j41u/adblock_pluss_effect_on_firefoxs_memory_usage/chhpomw[big
impact] on browsing speed and
https://blog.mozilla.org/nnethercote/2014/05/14/adblock-pluss-effect-on-firefoxs-memory-usage/[RAM
usage], so implementing support for AdBlockPlus-like lists is currently not
a priority.
How can I get No-Script-like behavior?::
To disable JavaScript by default:
+
----
:set content.javascript.enabled false
----
+
The basic command for enabling JavaScript for the current host is `tsh`.
This will allow JavaScript execution for the current session.
Use `S` instead of `s` to make the exception permanent.
With `H` instead of `h`, subdomains are included.
With `u` instead of `h`, only the current URL is whitelisted (not the whole host).
How do I play Youtube videos with mpv?::
You can easily add a key binding to play youtube videos inside a real video
player - optionally even with hinting for links:
@ -132,13 +147,14 @@ It also works nicely with rapid hints:
----
How do I use qutebrowser with mutt?::
Due to a Qt limitation, local files without `.html` extensions are
"downloaded" instead of displayed, see
https://github.com/qutebrowser/qutebrowser/issues/566[#566]. You can work
around this by using this in your `mailcap`:
For security reasons, local files without `.html` extensions aren't
rendered as HTML, see
https://bugs.chromium.org/p/chromium/issues/detail?id=777737[this Chromium issue]
for details. You can do this in your `mailcap` file to get a proper
extension:
+
----
text/html; mv %s %s.html && qutebrowser %s.html >/dev/null 2>/dev/null; needsterminal;
text/html; qutebrowser %s; nametemplate=%s.html
----
What is the difference between bookmarks and quickmarks?::
@ -251,7 +267,7 @@ Unable to view flash content.::
to use the flash plugin. Using the command `:set content.plugins true`
in qutebrowser will enable plugins. Packages for flash should
be provided for your platform or it can be obtained from
http://get.adobe.com/flashplayer/[Adobe].
https://get.adobe.com/flashplayer/[Adobe].
Experiencing freezing on sites like duckduckgo and youtube.::
This issue could be caused by stale plugin files installed by `mozplugger`
@ -295,10 +311,19 @@ Lastly, set your `qt.args` to point to that directory and restart qutebrowser:
:restart
----
Unable to use `spawn` on MacOS.::
When running qutebrowser from the prebuilt binary (`qutebrowser.app`) it *will
not* read any files that would alter your `$PATH` (e.g. `.profile`, `.bashrc`,
etc). This is not a bug, just that `.profile` is not propogated to GUI
applications in MacOS.
+
See https://github.com/qutebrowser/qutebrowser/issues/4273[Issue #4273] for
details and potential workarounds.
My issue is not listed.::
If you experience any segfaults or crashes, you can report the issue in
https://github.com/qutebrowser/qutebrowser/issues[the issue tracker] or
using the `:report` command.
If you are reporting a segfault, make sure you read the
link:stacktrace.asciidoc[guide] on how to report them with all needed
link:stacktrace{outfilesuffix}[guide] on how to report them with all needed
information.

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.
|<<config-clear,config-clear>>|Set all settings back to their default.
|<<config-cycle,config-cycle>>|Cycle an option between multiple values.
|<<config-dict-add,config-dict-add>>|Add a key/value pair to a dictionary option.
|<<config-dict-remove,config-dict-remove>>|Remove a key from a dict.
|<<config-edit,config-edit>>|Open the config.py file in the editor.
|<<config-list-add,config-list-add>>|Append a value to a config option that is a list.
|<<config-list-remove,config-list-remove>>|Remove a value from a list.
|<<config-source,config-source>>|Read a config.py file.
|<<config-unset,config-unset>>|Unset an option.
|<<config-write-py,config-write-py>>|Write the current configuration to a config.py file.
@ -292,6 +296,35 @@ Cycle an option between multiple values.
* +*-t*+, +*--temp*+: Set value temporarily until qutebrowser is closed.
* +*-p*+, +*--print*+: Print the value after setting.
[[config-dict-add]]
=== config-dict-add
Syntax: +:config-dict-add [*--temp*] [*--replace*] 'option' 'key' 'value'+
Add a key/value pair to a dictionary option.
==== positional arguments
* +'option'+: The name of the option.
* +'key'+: The key to use.
* +'value'+: The value to place in the dictionary.
==== optional arguments
* +*-t*+, +*--temp*+: Add value temporarily until qutebrowser is closed.
* +*-r*+, +*--replace*+: Replace existing values. By default, existing values are not overwritten.
[[config-dict-remove]]
=== config-dict-remove
Syntax: +:config-dict-remove [*--temp*] 'option' 'key'+
Remove a key from a dict.
==== positional arguments
* +'option'+: The name of the option.
* +'key'+: The key to remove from the dict.
==== optional arguments
* +*-t*+, +*--temp*+: Remove value temporarily until qutebrowser is closed.
[[config-edit]]
=== config-edit
Syntax: +:config-edit [*--no-source*]+
@ -301,6 +334,32 @@ Open the config.py file in the editor.
==== optional arguments
* +*-n*+, +*--no-source*+: Don't re-source the config file after editing.
[[config-list-add]]
=== config-list-add
Syntax: +:config-list-add [*--temp*] 'option' 'value'+
Append a value to a config option that is a list.
==== positional arguments
* +'option'+: The name of the option.
* +'value'+: The value to append to the end of the list.
==== optional arguments
* +*-t*+, +*--temp*+: Add value temporarily until qutebrowser is closed.
[[config-list-remove]]
=== config-list-remove
Syntax: +:config-list-remove [*--temp*] 'option' 'value'+
Remove a value from a list.
==== positional arguments
* +'option'+: The name of the option.
* +'value'+: The value to remove from the list.
==== optional arguments
* +*-t*+, +*--temp*+: Remove value temporarily until qutebrowser is closed.
[[config-source]]
=== config-source
Syntax: +:config-source [*--clear*] ['filename']+
@ -326,7 +385,7 @@ This sets an option back to its default and removes it from autoconfig.yml.
* +'option'+: The name of the option.
==== optional arguments
* +*-t*+, +*--temp*+: Don't touch autoconfig.yml.
* +*-t*+, +*--temp*+: Set value temporarily until qutebrowser is closed.
[[config-write-py]]
=== config-write-py
@ -545,6 +604,10 @@ Start hinting.
- `inputs`: Only input fields.
Custom groups can be added via the `hints.selectors` setting
and also used here.
* +'target'+: What to do with the selected element.
@ -576,7 +639,7 @@ Start hinting.
- With `userscript`: The userscript to execute. Either store
the userscript in
`~/.local/share/qutebrowser/userscripts`
(or `$XDG_DATA_DIR`), or use an absolute
(or `$XDG_DATA_HOME`), or use an absolute
path.
- With `fill`: The command to fill the statusbar with.
`{hint-url}` will get replaced by the selected
@ -758,11 +821,11 @@ This tries to automatically click on typical _Previous Page_ or _Next Page_ link
- `up`: Go up a level in the current URL.
- `increment`: Increment the last number in the URL.
Uses the
link:settings.html#url.incdec_segments[url.incdec_segments]
link:settings{outsuffix}#url.incdec_segments[url.incdec_segments]
config option.
- `decrement`: Decrement the last number in the URL.
Uses the
link:settings.html#url.incdec_segments[url.incdec_segments]
link:settings{outsuffix}#url.incdec_segments[url.incdec_segments]
config option.
@ -918,6 +981,9 @@ Repeat a given command.
* +'times'+: How many times to repeat.
* +'command'+: The command to run, with optional args.
==== count
Multiplies with 'times' when given.
==== note
* This command does not split arguments after the last argument and handles quotes literally.
* With this command, +;;+ is interpreted literally instead of splitting off a second command.
@ -1136,7 +1202,7 @@ Syntax: +:set [*--temp*] [*--print*] [*--pattern* 'pattern'] ['option'] ['value'
Set an option.
If the option name ends with '?', the value of the option is shown instead. Using :set without any arguments opens a page where settings can be changed interactively.
If the option name ends with '?' or no value is provided, the value of the option is shown instead. Using :set without any arguments opens a page where settings can be changed interactively.
==== positional arguments
* +'option'+: The name of the option.
@ -1190,13 +1256,16 @@ Spawn a command in a shell.
* +*-u*+, +*--userscript*+: Run the command as a userscript. You can use an absolute path, or store the userscript in one of those
locations:
- `~/.local/share/qutebrowser/userscripts`
(or `$XDG_DATA_DIR`)
(or `$XDG_DATA_HOME`)
- `/usr/share/qutebrowser/userscripts`
* +*-v*+, +*--verbose*+: Show notifications when the command started/exited.
* +*-o*+, +*--output*+: Whether the output should be shown in a new tab.
* +*-d*+, +*--detach*+: Whether the command should be detached from qutebrowser.
==== count
Given to userscripts as $QUTE_COUNT.
==== note
* This command does not split arguments after the last argument and handles quotes literally.
@ -1255,7 +1324,7 @@ The tab index to focus, starting with 1.
[[tab-give]]
=== tab-give
Syntax: +:tab-give ['win-id']+
Syntax: +:tab-give [*--keep*] ['win-id']+
Give the current tab to a new or existing window if win_id given.
@ -1264,6 +1333,9 @@ If no win_id is given, the tab will get detached into a new window.
==== positional arguments
* +'win-id'+: The window ID of the window to give the current tab to.
==== optional arguments
* +*-k*+, +*--keep*+: If given, keep the old tab around.
==== count
Overrides win_id (index starts at 1 for win_id=0).
@ -1328,7 +1400,7 @@ How many tabs to switch back.
[[tab-take]]
=== tab-take
Syntax: +:tab-take 'index'+
Syntax: +:tab-take [*--keep*] 'index'+
Take a tab from another window.
@ -1336,6 +1408,12 @@ Take a tab from another window.
* +'index'+: The [win_id/]index of the tab to take. Or a substring in which case the closest match will be taken.
==== optional arguments
* +*-k*+, +*--keep*+: If given, keep the old tab around.
==== note
* This command does not split arguments after the last argument and handles quotes literally.
[[unbind]]
=== unbind
Syntax: +:unbind [*--mode* 'mode'] 'key'+
@ -1382,7 +1460,7 @@ Close all windows except for the current one.
[[yank]]
=== yank
Syntax: +:yank [*--sel*] [*--keep*] ['what']+
Syntax: +:yank [*--sel*] [*--keep*] [*--quiet*] ['what']+
Yank something to the clipboard or primary selection.
@ -1401,10 +1479,11 @@ Yank something to the clipboard or primary selection.
==== optional arguments
* +*-s*+, +*--sel*+: Use the primary selection instead of the clipboard.
* +*-k*+, +*--keep*+: Stay in visual mode after yanking the selection.
* +*-q*+, +*--quiet*+: Don't show an information message.
[[zoom]]
=== zoom
Syntax: +:zoom ['zoom']+
Syntax: +:zoom [*--quiet*] ['zoom']+
Set the zoom level for the current tab.
@ -1413,20 +1492,33 @@ The zoom can be given as argument or as [count]. If neither is given, the zoom i
==== positional arguments
* +'zoom'+: The zoom percentage to set.
==== optional arguments
* +*-q*+, +*--quiet*+: Don't show a zoom level message.
==== count
The zoom percentage to set.
[[zoom-in]]
=== zoom-in
Syntax: +:zoom-in [*--quiet*]+
Increase the zoom level for the current tab.
==== optional arguments
* +*-q*+, +*--quiet*+: Don't show a zoom level message.
==== count
How many steps to zoom in.
[[zoom-out]]
=== zoom-out
Syntax: +:zoom-out [*--quiet*]+
Decrease the zoom level for the current tab.
==== optional arguments
* +*-q*+, +*--quiet*+: Don't show a zoom level message.
==== count
How many steps to zoom out.
@ -1657,7 +1749,7 @@ Shift the focus of the prompt file completion menu to another item.
[[prompt-open-download]]
=== prompt-open-download
Syntax: +:prompt-open-download ['cmdline']+
Syntax: +:prompt-open-download [*--pdfjs*] ['cmdline']+
Immediately open a download.
@ -1669,6 +1761,9 @@ If no specific command is given, this will use the system's default application
cmdline.
==== optional arguments
* +*-p*+, +*--pdfjs*+: Open the download via PDF.js.
==== note
* This command does not split arguments after the last argument and handles quotes literally.

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
customizable.
Using the link:commands.html#set[`:set`] command and command completion, you
Using the link:commands{outfilesuffix}#set[`:set`] command and command completion, you
can quickly set settings interactively, for example `:set tabs.position left`.
Some settings are also customizable for a given
@ -53,8 +53,8 @@ https://developer.chrome.com/apps/match_patterns[URL pattern] by doing e.g.
To get more help about a setting, use e.g. `:help tabs.position`.
To bind and unbind keys, you can use the link:commands.html#bind[`:bind`] and
link:commands.html#unbind[`:unbind`] commands:
To bind and unbind keys, you can use the link:commands{outfilesuffix}#bind[`:bind`] and
link:commands{outfilesuffix}#unbind[`:unbind`] commands:
- Binding the key chain `,v` to the `:spawn mpv {url}` command:
`:bind ,v spawn mpv {url}`
@ -67,9 +67,9 @@ See the help pages linked above (or `:help :bind`, `:help :unbind`) for more
information.
Other useful commands for config manipulation are
link:commands.html#config-unset[`:config-unset`] to reset a value to its default,
link:commands.html#config-clear[`:config-clear`] to reset the entire configuration,
and link:commands.html#config-cycle[`:config-cycle`] to cycle a setting between
link:commands{outfilesuffix}#config-unset[`:config-unset`] to reset a value to its default,
link:commands{outfilesuffix}#config-clear[`:config-clear`] to reset the entire configuration,
and link:commands{outfilesuffix}#config-cycle[`:config-cycle`] to cycle a setting between
different values.
[[configpy]]
@ -111,7 +111,7 @@ Note that qutebrowser does some Python magic so it's able to warn you about
mistyped config settings. As an example, if you do `c.tabs.possition = "left"`,
you'll get an error when starting.
See the link:settings.html[settings help page] for all available settings. The
See the link:settings{outfilesuffix}[settings help page] for all available settings. The
accepted values depend on the type of the option. Commonly used are:
- Strings: `c.tabs.position = "left"`
@ -187,7 +187,7 @@ preferred to use the `config.bind` command. Doing so ensures the commands are
valid and normalizes different expressions which map to the same key.
For details on how to specify keys and the available modes, see the
link:settings.html#bindings.commands[documentation] for the `bindings.commands`
link:settings{outfilesuffix}#bindings.commands[documentation] for the `bindings.commands`
setting.
To bind a key:
@ -395,6 +395,7 @@ Pre-built colorschemes
- A collection of https://github.com/chriskempson/base16[base16] color-schemes can be found in https://github.com/theova/base16-qutebrowser[base16-qutebrowser] and used with https://github.com/AuditeMarlow/base16-manager[base16-manager].
- Two implementations of the https://github.com/arcticicestudio/nord[Nord] colorscheme for qutebrowser exist: https://github.com/Linuus/nord-qutebrowser[Linuus], https://github.com/KnownAsDon/QuteBrowser-Nord-Theme[KnownAsDon]
- https://github.com/evannagle/qutebrowser-dracula-theme[Dracula]
Avoiding flake8 errors
^^^^^^^^^^^^^^^^^^^^^^
@ -452,7 +453,7 @@ or always navigate through command history with
:bind -m command <Down> command-history-next
----
- The default for `completion.web_history_max_items` is now set to `-1`, showing
- The default for `completion.web_history.max_items` is now set to `-1`, showing
an unlimited number of items in the completion for `:open` as the new
sqlite-based completion is much faster. If the `:open` completion is too slow
on your machine, set an appropriate limit again.

View File

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

View File

@ -100,6 +100,7 @@
|<<completion.delay,completion.delay>>|Delay (in milliseconds) before updating completions after typing a character.
|<<completion.height,completion.height>>|Height (in pixels or as percentage of the window) of the completion.
|<<completion.min_chars,completion.min_chars>>|Minimum amount of characters needed to update completions.
|<<completion.open_categories,completion.open_categories>>|Which categories to show (in which order) in the :open completion.
|<<completion.quick,completion.quick>>|Move on to the next part when there's only one possible completion left.
|<<completion.scrollbar.padding,completion.scrollbar.padding>>|Padding (in pixels) of the scrollbar handle in the completion window.
|<<completion.scrollbar.width,completion.scrollbar.width>>|Width (in pixels) of the scrollbar in the completion window.
@ -107,7 +108,8 @@
|<<completion.shrink,completion.shrink>>|Shrink the completion to be smaller than the configured size if there are no scrollbars.
|<<completion.timestamp_format,completion.timestamp_format>>|Format of timestamps (e.g. for the history completion).
|<<completion.use_best_match,completion.use_best_match>>|Execute the best-matching command on a partial match.
|<<completion.web_history_max_items,completion.web_history_max_items>>|Number of URLs to show in the web history.
|<<completion.web_history.exclude,completion.web_history.exclude>>|A list of patterns which should not be shown in the history.
|<<completion.web_history.max_items,completion.web_history.max_items>>|Number of URLs to show in the web history.
|<<confirm_quit,confirm_quit>>|Require a confirmation before quitting the application.
|<<content.autoplay,content.autoplay>>|Automatically start playing `<video>` elements.
|<<content.cache.appcache,content.cache.appcache>>|Enable support for the HTML 5 web application cache feature.
@ -128,7 +130,7 @@
|<<content.headers.user_agent,content.headers.user_agent>>|User agent to send. Unset to send the default.
|<<content.host_blocking.enabled,content.host_blocking.enabled>>|Enable host blocking.
|<<content.host_blocking.lists,content.host_blocking.lists>>|List of URLs of lists which contain hosts to block.
|<<content.host_blocking.whitelist,content.host_blocking.whitelist>>|List of domains that should always be loaded, despite being ad-blocked.
|<<content.host_blocking.whitelist,content.host_blocking.whitelist>>|A list of patterns that should always be loaded, despite being ad-blocked.
|<<content.hyperlink_auditing,content.hyperlink_auditing>>|Enable hyperlink auditing (`<a ping>`).
|<<content.images,content.images>>|Load images automatically in web pages.
|<<content.javascript.alert,content.javascript.alert>>|Show javascript alerts.
@ -143,6 +145,8 @@
|<<content.local_content_can_access_remote_urls,content.local_content_can_access_remote_urls>>|Allow locally loaded documents to access remote URLs.
|<<content.local_storage,content.local_storage>>|Enable support for HTML 5 local storage and Web SQL.
|<<content.media_capture,content.media_capture>>|Allow websites to record audio/video.
|<<content.mouse_lock,content.mouse_lock>>|Allow websites to lock your mouse pointer.
|<<content.mute,content.mute>>|Automatically mute tabs.
|<<content.netrc_file,content.netrc_file>>|Netrc-file for HTTP authentication.
|<<content.notifications,content.notifications>>|Allow websites to show notifications.
|<<content.pdfjs,content.pdfjs>>|Allow pdf.js to view PDF files in the browser.
@ -156,7 +160,7 @@
|<<content.ssl_strict,content.ssl_strict>>|Validate SSL handshakes.
|<<content.user_stylesheets,content.user_stylesheets>>|List of user stylesheet filenames to use.
|<<content.webgl,content.webgl>>|Enable WebGL.
|<<content.webrtc_public_interfaces_only,content.webrtc_public_interfaces_only>>|Only expose public interfaces via WebRTC.
|<<content.webrtc_ip_handling_policy,content.webrtc_ip_handling_policy>>|Which interfaces to expose via WebRTC.
|<<content.windowed_fullscreen,content.windowed_fullscreen>>|Limit fullscreen to the browser window (does not expand to fill the screen).
|<<content.xss_auditing,content.xss_auditing>>|Monitor load requests for cross-site scripting attempts.
|<<downloads.location.directory,downloads.location.directory>>|Directory to save downloads to.
@ -203,6 +207,7 @@
|<<hints.next_regexes,hints.next_regexes>>|Comma-separated list of regular expressions to use for 'next' links.
|<<hints.prev_regexes,hints.prev_regexes>>|Comma-separated list of regular expressions to use for 'prev' links.
|<<hints.scatter,hints.scatter>>|Scatter hint key chains (like Vimium) or not (like dwb).
|<<hints.selectors,hints.selectors>>|CSS selectors used to determine which elements on a page should have hints.
|<<hints.uppercase,hints.uppercase>>|Make characters in hint strings uppercase.
|<<history_gap_interval,history_gap_interval>>|Maximum time (in minutes) between two history items for them to be considered being from the same browsing session.
|<<input.escape_quits_reporter,input.escape_quits_reporter>>|Allow Escape to quit the crash reporter.
@ -227,7 +232,9 @@
|<<qt.force_platform,qt.force_platform>>|Force a Qt platform to use.
|<<qt.force_software_rendering,qt.force_software_rendering>>|Force software rendering for QtWebEngine.
|<<qt.highdpi,qt.highdpi>>|Turn on Qt HighDPI scaling.
|<<scrolling.bar,scrolling.bar>>|Show a scrollbar.
|<<qt.low_end_device_mode,qt.low_end_device_mode>>|When to use Chromium's low-end device mode.
|<<qt.process_model,qt.process_model>>|Which Chromium process model to use.
|<<scrolling.bar,scrolling.bar>>|When to show the scrollbar.
|<<scrolling.smooth,scrolling.smooth>>|Enable smooth scrolling for web pages.
|<<search.ignore_case,search.ignore_case>>|When to find text on a page case-insensitively.
|<<search.incremental,search.incremental>>|Find text on a page incrementally, renewing the search for each typed character.
@ -246,11 +253,13 @@
|<<tabs.indicator.padding,tabs.indicator.padding>>|Padding (in pixels) for tab indicators.
|<<tabs.indicator.width,tabs.indicator.width>>|Width (in pixels) of the progress indicator (0 to disable).
|<<tabs.last_close,tabs.last_close>>|How to behave when the last tab is closed.
|<<tabs.max_width,tabs.max_width>>|Maximum width (in pixels) of tabs (-1 for no maximum).
|<<tabs.min_width,tabs.min_width>>|Minimum width (in pixels) of tabs (-1 for the default minimum size behavior).
|<<tabs.mode_on_change,tabs.mode_on_change>>|When switching tabs, what input mode is applied.
|<<tabs.mousewheel_switching,tabs.mousewheel_switching>>|Switch between tabs using the mouse wheel.
|<<tabs.new_position.related,tabs.new_position.related>>|Position of new tabs opened from another tab.
|<<tabs.new_position.unrelated,tabs.new_position.unrelated>>|Position of new tabs which aren't opened from another tab.
|<<tabs.new_position.stacking,tabs.new_position.stacking>>|Stack related tabs on top of each other when opened consecutively.
|<<tabs.new_position.unrelated,tabs.new_position.unrelated>>|Position of new tabs which are not opened from another tab.
|<<tabs.padding,tabs.padding>>|Padding (in pixels) around text for tabs.
|<<tabs.pinned.shrink,tabs.pinned.shrink>>|Shrink pinned tabs down to their contents.
|<<tabs.position,tabs.position>>|Position of the tab bar.
@ -287,9 +296,11 @@ Type: <<types,Dict>>
Default:
- +pass:[q]+: +pass:[quit]+
- +pass:[q]+: +pass:[close]+
- +pass:[qa]+: +pass:[quit]+
- +pass:[w]+: +pass:[session-save]+
- +pass:[wq]+: +pass:[quit --save]+
- +pass:[wqa]+: +pass:[quit --save]+
[[auto_save.interval]]
=== auto_save.interval
@ -562,6 +573,7 @@ Default:
* +pass:[g0]+: +pass:[tab-focus 1]+
* +pass:[gB]+: +pass:[set-cmd-text -s :bookmark-load -t]+
* +pass:[gC]+: +pass:[tab-clone]+
* +pass:[gD]+: +pass:[tab-give]+
* +pass:[gO]+: +pass:[set-cmd-text :open -t -r {url:pretty}]+
* +pass:[gU]+: +pass:[navigate up -t]+
* +pass:[g^]+: +pass:[tab-focus 1]+
@ -593,6 +605,9 @@ Default:
* +pass:[sk]+: +pass:[set-cmd-text -s :bind]+
* +pass:[sl]+: +pass:[set-cmd-text -s :set -t]+
* +pass:[ss]+: +pass:[set-cmd-text -s :set]+
* +pass:[tIH]+: +pass:[config-cycle -p -u *://*.{url:host}/* content.images ;; reload]+
* +pass:[tIh]+: +pass:[config-cycle -p -u *://{url:host}/* content.images ;; reload]+
* +pass:[tIu]+: +pass:[config-cycle -p -u {url} content.images ;; reload]+
* +pass:[tPH]+: +pass:[config-cycle -p -u *://*.{url:host}/* content.plugins ;; reload]+
* +pass:[tPh]+: +pass:[config-cycle -p -u *://{url:host}/* content.plugins ;; reload]+
* +pass:[tPu]+: +pass:[config-cycle -p -u {url} content.plugins ;; reload]+
@ -600,6 +615,9 @@ Default:
* +pass:[tSh]+: +pass:[config-cycle -p -u *://{url:host}/* content.javascript.enabled ;; reload]+
* +pass:[tSu]+: +pass:[config-cycle -p -u {url} content.javascript.enabled ;; reload]+
* +pass:[th]+: +pass:[back -t]+
* +pass:[tiH]+: +pass:[config-cycle -p -t -u *://*.{url:host}/* content.images ;; reload]+
* +pass:[tih]+: +pass:[config-cycle -p -t -u *://{url:host}/* content.images ;; reload]+
* +pass:[tiu]+: +pass:[config-cycle -p -t -u {url} content.images ;; reload]+
* +pass:[tl]+: +pass:[forward -t]+
* +pass:[tpH]+: +pass:[config-cycle -p -t -u *://*.{url:host}/* content.plugins ;; reload]+
* +pass:[tph]+: +pass:[config-cycle -p -t -u *://{url:host}/* content.plugins ;; reload]+
@ -633,7 +651,7 @@ Default:
* +pass:[}}]+: +pass:[navigate next -t]+
- +pass:[passthrough]+:
* +pass:[&lt;Ctrl-V&gt;]+: +pass:[leave-mode]+
* +pass:[&lt;Shift-Escape&gt;]+: +pass:[leave-mode]+
- +pass:[prompt]+:
* +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-H&gt;]+: +pass:[rl-backward-delete-char]+
* +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-W&gt;]+: +pass:[rl-unix-word-rubout]+
* +pass:[&lt;Ctrl-X&gt;]+: +pass:[prompt-open-download]+
@ -780,7 +799,7 @@ Default: +pass:[black]+
=== colors.completion.match.fg
Foreground color of the matched text in the completion.
Type: <<types,QssColor>>
Type: <<types,QtColor>>
Default: +pass:[#ff4444]+
@ -1383,6 +1402,26 @@ Type: <<types,Int>>
Default: +pass:[1]+
[[completion.open_categories]]
=== completion.open_categories
Which categories to show (in which order) in the :open completion.
Type: <<types,FlagList>>
Valid values:
* +searchengines+
* +quickmarks+
* +bookmarks+
* +history+
Default:
- +pass:[searchengines]+
- +pass:[quickmarks]+
- +pass:[bookmarks]+
- +pass:[history]+
[[completion.quick]]
=== completion.quick
Move on to the next part when there's only one possible completion left.
@ -1445,8 +1484,19 @@ Type: <<types,Bool>>
Default: +pass:[false]+
[[completion.web_history_max_items]]
=== completion.web_history_max_items
[[completion.web_history.exclude]]
=== completion.web_history.exclude
A list of patterns which should not be shown in the history.
This only affects the completion. Matching URLs are still saved in the history (and visible on the qute://history page), but hidden in the completion.
Changing this setting will cause the completion history to be regenerated on the next start, which will take a short while.
This setting requires a restart.
Type: <<types,List of UrlPattern>>
Default: empty
[[completion.web_history.max_items]]
=== completion.web_history.max_items
Number of URLs to show in the web history.
0: no history / -1: unlimited
@ -1474,7 +1524,9 @@ Default:
[[content.autoplay]]
=== content.autoplay
Automatically start playing `<video>` elements.
Note this option needs a restart with QtWebEngine on Qt < 5.11.
Note: On Qt < 5.11, this option needs a restart and does not support URL patterns.
This setting supports URL patterns.
Type: <<types,Bool>>
@ -1570,6 +1622,8 @@ Default: +pass:[iso-8859-1]+
Allow websites to share screen content.
On Qt < 5.10, a dialog box is always displayed, even if this is set to "true".
This setting supports URL patterns.
Type: <<types,BoolAsk>>
Valid values:
@ -1609,6 +1663,8 @@ This setting is only available with the QtWebKit backend.
=== content.geolocation
Allow websites to request geolocations.
This setting supports URL patterns.
Type: <<types,BoolAsk>>
Valid values:
@ -1622,6 +1678,9 @@ Default: +pass:[ask]+
[[content.headers.accept_language]]
=== content.headers.accept_language
Value to send in the `Accept-Language` header.
Note that the value read from JavaScript is always the global value.
This setting supports URL patterns.
Type: <<types,String>>
@ -1631,6 +1690,8 @@ Default: +pass:[en-US,en]+
=== content.headers.custom
Custom headers for qutebrowser HTTP requests.
This setting supports URL patterns.
Type: <<types,Dict>>
Default: empty
@ -1640,6 +1701,8 @@ Default: empty
Value to send in the `DNT` header.
When this is set to true, qutebrowser asks websites to not track your identity. If set to null, the DNT header is not sent at all.
This setting supports URL patterns.
Type: <<types,Bool>>
Default: +pass:[true]+
@ -1648,6 +1711,8 @@ Default: +pass:[true]+
=== content.headers.referer
When to send the Referer header.
The Referer header tells websites from which website you were coming from when visiting them.
No restart is needed with QtWebKit.
This setting requires a restart.
Type: <<types,String>>
@ -1655,15 +1720,16 @@ Valid values:
* +always+: Always send the Referer.
* +never+: Never send the Referer. This is not recommended, as some sites may break.
* +same-domain+: Only send the Referer for the same domain. This will still protect your privacy, but shouldn't break any sites.
* +same-domain+: Only send the Referer for the same domain. This will still protect your privacy, but shouldn't break any sites. With QtWebEngine, the referer will still be sent for other domains, but with stripped path information.
Default: +pass:[same-domain]+
This setting is only available with the QtWebKit backend.
[[content.headers.user_agent]]
=== content.headers.user_agent
User agent to send. Unset to send the default.
Note that the value read from JavaScript is always the global value.
This setting supports URL patterns.
Type: <<types,String>>
@ -1673,6 +1739,8 @@ Default: empty
=== content.host_blocking.enabled
Enable host blocking.
This setting supports URL patterns.
Type: <<types,Bool>>
Default: +pass:[true]+
@ -1688,6 +1756,11 @@ The file can be in one of the following formats:
- A zip-file of any of the above, with either only one file, or a file
named `hosts` (with any extension).
It's also possible to add a local file or directory via a `file://` URL. In
case of a directory, all files in the directory are read as adblock lists.
The file `~/.config/qutebrowser/blocked-hosts` is always read if it exists.
Type: <<types,List of Url>>
@ -1697,11 +1770,11 @@ Default:
[[content.host_blocking.whitelist]]
=== content.host_blocking.whitelist
List of domains that should always be loaded, despite being ad-blocked.
Domains may contain * and ? wildcards and are otherwise required to exactly match the requested domain.
A list of patterns that should always be loaded, despite being ad-blocked.
Note this whitelists blocked hosts, not first-party URLs. As an example, if `example.org` loads an ad from `ads.example.org`, the whitelisted host should be `ads.example.org`. If you want to disable the adblocker on a given page, use the `content.host_blocking.enabled` setting with a URL pattern instead.
Local domains are always exempt from hostblocking.
Type: <<types,List of String>>
Type: <<types,List of UrlPattern>>
Default:
@ -1843,6 +1916,8 @@ Default: +pass:[true]+
=== content.media_capture
Allow websites to record audio/video.
This setting supports URL patterns.
Type: <<types,BoolAsk>>
Valid values:
@ -1855,6 +1930,37 @@ Default: +pass:[ask]+
This setting is only available with the QtWebEngine backend.
[[content.mouse_lock]]
=== content.mouse_lock
Allow websites to lock your mouse pointer.
This setting supports URL patterns.
Type: <<types,BoolAsk>>
Valid values:
* +true+
* +false+
* +ask+
Default: +pass:[ask]+
On QtWebEngine, this setting requires Qt 5.8 or newer.
On QtWebKit, this setting is unavailable.
[[content.mute]]
=== content.mute
Automatically mute tabs.
Note that if the `:tab-mute` command is used, the mute status for the affected tab is now controlled manually, and this setting doesn't have any effect.
This setting supports URL patterns.
Type: <<types,Bool>>
Default: +pass:[false]+
[[content.netrc_file]]
=== content.netrc_file
Netrc-file for HTTP authentication.
@ -1868,6 +1974,8 @@ Default: empty
=== content.notifications
Allow websites to show notifications.
This setting supports URL patterns.
Type: <<types,BoolAsk>>
Valid values:
@ -1889,12 +1997,12 @@ Type: <<types,Bool>>
Default: +pass:[false]+
This setting is only available with the QtWebKit backend.
[[content.persistent_storage]]
=== content.persistent_storage
Allow websites to request persistent storage quota via `navigator.webkitPersistentStorage.requestQuota`.
This setting supports URL patterns.
Type: <<types,BoolAsk>>
Valid values:
@ -1967,6 +2075,8 @@ This setting is only available with the QtWebKit backend.
=== content.register_protocol_handler
Allow websites to register protocol handlers via `navigator.registerProtocolHandler`.
This setting supports URL patterns.
Type: <<types,BoolAsk>>
Valid values:
@ -1985,6 +2095,8 @@ On QtWebKit, this setting is unavailable.
=== content.ssl_strict
Validate SSL handshakes.
This setting supports URL patterns.
Type: <<types,BoolAsk>>
Valid values:
@ -2013,14 +2125,22 @@ Type: <<types,Bool>>
Default: +pass:[true]+
[[content.webrtc_public_interfaces_only]]
=== content.webrtc_public_interfaces_only
Only expose public interfaces via WebRTC.
On Qt 5.9, this option requires a restart. On Qt 5.10, this option doesn't work at all because of a Qt bug. On Qt >= 5.11, no restart is required.
[[content.webrtc_ip_handling_policy]]
=== content.webrtc_ip_handling_policy
Which interfaces to expose via WebRTC.
On Qt 5.10, this option doesn't work because of a Qt bug.
This setting requires a restart.
Type: <<types,Bool>>
Type: <<types,String>>
Default: +pass:[false]+
Valid values:
* +all-interfaces+: WebRTC has the right to enumerate all interfaces and bind them to discover public interfaces.
* +default-public-and-private-interfaces+: WebRTC should only use the default route used by http. This also exposes the associated default private address. Default route is the route chosen by the OS on a multi-homed endpoint.
* +default-public-interface-only+: WebRTC should only use the default route used by http. This doesn't expose any local addresses.
* +disable-non-proxied-udp+: WebRTC should only use TCP to contact peers or servers unless the proxy server supports UDP. This doesn't expose any local addresses either.
Default: +pass:[all-interfaces]+
On QtWebEngine, this setting requires Qt 5.9.2 or newer.
@ -2037,13 +2157,13 @@ Default: +pass:[false]+
[[content.xss_auditing]]
=== content.xss_auditing
Monitor load requests for cross-site scripting attempts.
Suspicious scripts will be blocked and reported in the inspector's JavaScript console. Enabling this feature might have an impact on performance.
Suspicious scripts will be blocked and reported in the inspector's JavaScript console.
This setting supports URL patterns.
Type: <<types,Bool>>
Default: +pass:[false]+
Default: +pass:[true]+
[[downloads.location.directory]]
=== downloads.location.directory
@ -2455,6 +2575,76 @@ Type: <<types,Bool>>
Default: +pass:[true]+
[[hints.selectors]]
=== hints.selectors
CSS selectors used to determine which elements on a page should have hints.
This setting supports URL patterns.
This setting can only be set in config.py.
Type: <<types,Dict>>
Default:
- +pass:[all]+:
* +pass:[a]+
* +pass:[area]+
* +pass:[textarea]+
* +pass:[select]+
* +pass:[input:not([type=&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
Make characters in hint strings uppercase.
@ -2699,13 +2889,59 @@ Type: <<types,Bool>>
Default: +pass:[false]+
[[qt.low_end_device_mode]]
=== qt.low_end_device_mode
When to use Chromium's low-end device mode.
This improves the RAM usage of renderer processes, at the expense of performance.
This setting requires a restart.
Type: <<types,String>>
Valid values:
* +always+: Always use low-end device mode.
* +auto+: Decide automatically (uses low-end mode with < 1 GB available RAM).
* +never+: Never use low-end device mode.
Default: +pass:[auto]+
This setting is only available with the QtWebEngine backend.
[[qt.process_model]]
=== qt.process_model
Which Chromium process model to use.
Alternative process models use less resources, but decrease security and robustness.
See the following pages for more details:
- https://www.chromium.org/developers/design-documents/process-models
- https://doc.qt.io/qt-5/qtwebengine-features.html#process-models
This setting requires a restart.
Type: <<types,String>>
Valid values:
* +process-per-site-instance+: Pages from separate sites are put into separate processes and separate visits to the same site are also isolated.
* +process-per-site+: Pages from separate sites are put into separate processes. Unlike Process per Site Instance, all visits to the same site will share an OS process. The benefit of this model is reduced memory consumption, because more web pages will share processes. The drawbacks include reduced security, robustness, and responsiveness.
* +single-process+: Run all tabs in a single process. This should be used for debugging purposes only, and it disables `:open --private`.
Default: +pass:[process-per-site-instance]+
This setting is only available with the QtWebEngine backend.
[[scrolling.bar]]
=== scrolling.bar
Show a scrollbar.
When to show the scrollbar.
Type: <<types,Bool>>
Type: <<types,String>>
Default: +pass:[false]+
Valid values:
* +always+: Always show the scrollbar.
* +never+: Never show the scrollbar.
* +when-searching+: Show the scrollbar when searching for text in the webpage. With the QtWebKit backend, this is equal to `never`.
Default: +pass:[when-searching]+
[[scrolling.smooth]]
=== scrolling.smooth
@ -2972,6 +3208,17 @@ Valid values:
Default: +pass:[ignore]+
[[tabs.max_width]]
=== tabs.max_width
Maximum width (in pixels) of tabs (-1 for no maximum).
This setting only applies when tabs are horizontal.
This setting does not apply to pinned tabs, unless `tabs.pinned.shrink` is False.
This setting may not apply properly if max_width is smaller than the minimum size of tab contents, or smaller than tabs.min_width.
Type: <<types,Int>>
Default: +pass:[-1]+
[[tabs.min_width]]
=== tabs.min_width
Minimum width (in pixels) of tabs (-1 for the default minimum size behavior).
@ -3007,6 +3254,7 @@ Default: +pass:[true]+
[[tabs.new_position.related]]
=== tabs.new_position.related
Position of new tabs opened from another tab.
See `tabs.new_position.stacking` for controlling stacking behavior.
Type: <<types,NewTabPosition>>
@ -3019,9 +3267,19 @@ Valid values:
Default: +pass:[next]+
[[tabs.new_position.stacking]]
=== tabs.new_position.stacking
Stack related tabs on top of each other when opened consecutively.
Only applies for `next` and `prev` values of `tabs.new_position.related` and `tabs.new_position.unrelated`.
Type: <<types,Bool>>
Default: +pass:[true]+
[[tabs.new_position.unrelated]]
=== tabs.new_position.unrelated
Position of new tabs which aren't opened from another tab.
Position of new tabs which are not opened from another tab.
See `tabs.new_position.stacking` for controlling stacking behavior.
Type: <<types,NewTabPosition>>
@ -3381,7 +3639,7 @@ When setting from a string, pass a json-like list, e.g. `["one", "two"]`.
A value can be in one of the following formats: * `#RGB`/`#RRGGBB`/`#RRRGGGBBB`/`#RRRRGGGGBBBB` * An SVG color name as specified in http://www.w3.org/TR/SVG/types.html#ColorKeywords[the W3C specification]. * transparent (no color) * `rgb(r, g, b)` / `rgba(r, g, b, a)` (values 0-255 or percentages) * `hsv(h, s, v)` / `hsva(h, s, v, a)` (values 0-255, hue 0-359) * A gradient as explained in http://doc.qt.io/qt-5/stylesheet-reference.html#list-of-property-types[the Qt documentation] under ``Gradient''
|QtColor|A color value.
A value can be in one of the following formats: * `#RGB`/`#RRGGBB`/`#RRRGGGBBB`/`#RRRRGGGGBBBB` * An SVG color name as specified in http://www.w3.org/TR/SVG/types.html#ColorKeywords[the W3C specification]. * transparent (no color)
A value can be in one of the following formats: * `#RGB`/`#RRGGBB`/`#RRRGGGBBB`/`#RRRRGGGGBBBB` * An SVG color name as specified in http://www.w3.org/TR/SVG/types.html#ColorKeywords[the W3C specification]. * transparent (no color) * `rgb(r, g, b)` / `rgba(r, g, b, a)` (values 0-255 or percentages) * `hsv(h, s, v)` / `hsva(h, s, v, a)` (values 0-255, hue 0-359)
|QtFont|A font family, with optional style/weight/size.
* Style: `normal`/`italic`/`oblique` * Weight: `normal`, `bold`, `100`..`900` * Size: _number_ `px`/`pt`
@ -3403,5 +3661,8 @@ See the setting's valid values for more information on allowed values.
See https://sqlite.org/lang_datefunc.html for reference.
|UniqueCharString|A string which may not contain duplicate chars.
|Url|A URL as a string.
|UrlPattern|A match pattern for a URL.
See https://developer.chrome.com/apps/match_patterns for the allowed syntax.
|VerticalPosition|The position of the download bar.
|==============

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`.
On Slackware
------------
qutebrowser is available in the 3rd party repository at http://slackbuilds.org[slackbuilds.org]
An easy way to install it is with sbopkg (frontend for slackbuilds.org) available at http://sbopkg.org[sbopkg.org]
sbopkg can be run with a dialog screen interface, or via command line options.
After installing the latest sbopkg package, choose your release version, and sync the repo.
----
sbopkg -V 14.2
sbopkg -r
----
The pyPEG2 and MarkupSafe dependencies both need building for python3. You can either set PYTHON3=yes in the shell or set those as options in the dialog menu for each.
Generate a queue file for qutebrowser and dependencies:
----
sqg -p qutebrowser
----
Then load the queue in the dialog queue menu or via:
----
PYTHON3=yes sbopkg -i qutebrowser
----
If you use the dialog screen you can deselect any already-installed packages that you don't need/want to rebuild before starting the build process.
On OpenBSD
----------
@ -407,6 +439,14 @@ caveats:
(`export LD_LIBRARY_PATH=/usr/lib/openssl-1.0` on Archlinux) before starting
qutebrowser if you want SSL to work in certain downloads (e.g. for
`:adblock-update` or `:download`).
* On Ubuntu (tested on 18.04), you will need to install the `libssl1.0.0`
package (`apt install libssl1.0.0`). Then, in the qutebrowser git
repository, create a directory named `libssl` (`mkdir libssl`), and link
`libcrypto.so.1.0.0` and `libssl.so.1.0.0` into it without the versioning
part in their names (`ln -s /usr/lib/x86_64-linux-gnu/libcrypto.so.1.0.0
libssl/libcrypto.so` and `ln -s /usr/lib/x86_64-linux-gnu/libssl.so.1.0.0
libssl/libssl.so`). Now you can start qutebrowser issuing `export
LD_LIBRARY_PATH=$(pwd)/libssl` beforehand.
- It comes with a QtWebEngine compiled without proprietary codec support (such
as h.264).

View File

@ -45,12 +45,13 @@ In `command` mode:
- `QUTE_URL`: The current URL.
- `QUTE_TITLE`: The title of the current page.
- `QUTE_SELECTED_TEXT`: The text currently selected on the page.
- `QUTE_COUNT`: The `count` from the spawn command running the userscript.
In `hints` mode:
- `QUTE_URL`: The URL selected via hints.
- `QUTE_SELECTED_TEXT`: The plain text of the element selected via hints.
- `QUTE_SELECTED_HTML` The HTML of the element selected via hints.
- `QUTE_SELECTED_HTML`: The HTML of the element selected via hints.
Sending commands
----------------

View File

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

View File

@ -13,7 +13,7 @@
height="682.66669"
id="svg2"
sodipodi:version="0.32"
inkscape:version="0.92.2 5c3e80d, 2017-08-06"
inkscape:version="0.92.2 2405546, 2018-03-11"
version="1.0"
sodipodi:docname="cheatsheet.svg"
inkscape:output_extension="org.inkscape.output.svg.inkscape"
@ -33,7 +33,7 @@
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1.7536248"
inkscape:cx="430.72917"
inkscape:cx="613.20834"
inkscape:cy="268.64059"
inkscape:document-units="px"
inkscape:current-layer="layer1"
@ -3085,7 +3085,9 @@
style="font-size:10.66666698px;line-height:1.25;font-family:sans-serif;fill:#000000;stroke-width:1.06666672"
id="flowPara4056"> (to index/left/right)</flowPara><flowPara
style="font-size:10.66666698px;line-height:1.25;font-family:sans-serif;fill:#000000;stroke-width:1.06666672"
id="flowPara3858">gC - clone tab  </flowPara><flowPara
id="flowPara3858">gC - clone tab</flowPara><flowPara
style="font-size:10.66666698px;line-height:1.25;font-family:sans-serif;fill:#000000;stroke-width:1.06666672"
id="flowPara6098">gD - detach tab </flowPara><flowPara
style="font-size:10.66666698px;line-height:1.25;font-family:sans-serif;fill:#000000;stroke-width:1.06666672"
id="flowPara3860">gf - view page source</flowPara><flowPara
style="font-size:10.66666698px;line-height:1.25;font-family:sans-serif;fill:#000000;stroke-width:1.06666672"
@ -3097,9 +3099,9 @@
style="font-size:10.66666698px;line-height:1.25;font-family:sans-serif;fill:#000000;stroke-width:1.06666672"
id="flowPara3921">sf - save config</flowPara><flowPara
style="font-size:10.66666698px;line-height:1.25;font-family:sans-serif;fill:#000000;stroke-width:1.06666672"
id="flowPara3925">ss - set setting</flowPara><flowPara
id="flowPara3925">ss - set setting (sl: temp)</flowPara><flowPara
style="font-size:10.66666698px;line-height:1.25;font-family:sans-serif;fill:#000000;stroke-width:1.06666672"
id="flowPara3927">sl - set temp. setting</flowPara><flowPara
id="flowPara3927" /><flowPara
style="font-size:10.66666698px;line-height:1.25;font-family:sans-serif;fill:#000000;stroke-width:1.06666672"
id="flowPara3929">sk - bind key</flowPara><flowPara
style="font-size:10.66666698px;line-height:1.25;font-family:sans-serif;fill:#000000;stroke-width:1.06666672"

Before

Width:  |  Height:  |  Size: 181 KiB

After

Width:  |  Height:  |  Size: 181 KiB

View File

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

View File

@ -40,6 +40,9 @@ Section "Install"
; Uninstall old versions
ExecWait 'MsiExec.exe /quiet /qn /norestart /X{633F41F9-FE9B-42D1-9CC4-718CBD01EE11}'
ExecWait 'MsiExec.exe /quiet /qn /norestart /X{9331D947-AC86-4542-A755-A833429C6E69}'
IfFileExists "$INSTDIR\uninst.exe" 0 +2
ExecWait "$INSTDIR\uninst.exe /S _?=$INSTDIR"
CreateDirectory "$INSTDIR"
SetOutPath "$INSTDIR"

View File

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

View File

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

View File

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

View File

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

View File

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

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
astroid==1.6.5
certifi==2018.4.16
asn1crypto==0.24.0
astroid==2.0.4
certifi==2018.8.24
cffi==1.11.5
chardet==3.0.4
github3.py==1.1.0
cryptography==2.3.1
github3.py==1.2.0
idna==2.7
isort==4.3.4
jwcrypto==0.5.0
lazy-object-proxy==1.3.1
mccabe==0.6.1
pylint==1.9.2
pycparser==2.19
pylint==2.1.1
python-dateutil==2.7.3
./scripts/dev/pylint_checkers
requests==2.18.4
requests==2.19.1
six==1.11.0
uritemplate==3.0.0
urllib3==1.22
urllib3==1.23
wrapt==1.10.11

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
PyQt5==5.10.1
sip==4.19.8
PyQt5==5.11.3
PyQt5-sip==4.19.13

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +1,3 @@
# 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
"websites/github.com". How the username and password are determined is freely configurable using the CLI arguments. The
login information is inserted by emulating key events using qutebrowser's fake-key command in this manner:
[USERNAME]<Tab>[PASSWORD], which is compatible with almost all login forms."""
[USERNAME]<Tab>[PASSWORD], which is compatible with almost all login forms.
EPILOG = """Dependencies: tldextract (Python 3 module), pass.
Suggested bindings similar to Uzbl's `formfiller` script:
config.bind('<z><l>', 'spawn --userscript qute-pass')
config.bind('<z><u><l>', 'spawn --userscript qute-pass --username-only')
config.bind('<z><p><l>', 'spawn --userscript qute-pass --password-only')
config.bind('<z><o><l>', 'spawn --userscript qute-pass --otp-only')
"""
EPILOG = """Dependencies: tldextract (Python 3 module), pass, pass-otp (optional).
For issues and feedback please use: https://github.com/cryzed/qutebrowser-userscripts.
WARNING: The login details are viewable as plaintext in qutebrowser's debug log (qute://log) and might be shared if
@ -66,6 +74,7 @@ argument_parser.add_argument('--merge-candidates', '-m', action='store_true',
group = argument_parser.add_mutually_exclusive_group()
group.add_argument('--username-only', '-e', action='store_true', help='Only insert username')
group.add_argument('--password-only', '-w', action='store_true', help='Only insert password')
group.add_argument('--otp-only', '-o', action='store_true', help='Only insert OTP code')
stderr = functools.partial(print, file=sys.stderr)
@ -87,7 +96,7 @@ def qute_command(command):
def find_pass_candidates(domain, password_store_path):
candidates = []
for path, directories, file_names in os.walk(password_store_path):
for path, directories, file_names in os.walk(password_store_path, followlinks=True):
if directories or domain not in path.split(os.path.sep):
continue
@ -98,11 +107,19 @@ def find_pass_candidates(domain, password_store_path):
return candidates
def pass_(path, encoding):
process = subprocess.run(['pass', path], stdout=subprocess.PIPE)
def _run_pass(command, encoding):
process = subprocess.run(command, stdout=subprocess.PIPE)
return process.stdout.decode(encoding).strip()
def pass_(path, encoding):
return _run_pass(['pass', path], encoding)
def pass_otp(path, encoding):
return _run_pass(['pass', 'otp', path], encoding)
def dmenu(items, invocation, encoding):
command = shlex.split(invocation)
process = subprocess.run(command, input='\n'.join(items).encode(encoding), stdout=subprocess.PIPE)
@ -152,7 +169,7 @@ def main(arguments):
# Match username
target = selection if arguments.username_target == 'path' else secret
match = re.match(arguments.username_pattern, target)
match = re.search(arguments.username_pattern, target, re.MULTILINE)
if not match:
stderr('Failed to match username pattern on {}!'.format(arguments.username_target))
return ExitCodes.COULD_NOT_MATCH_USERNAME
@ -169,6 +186,9 @@ def main(arguments):
fake_key_raw(username)
elif arguments.password_only:
fake_key_raw(password)
elif arguments.otp_only:
otp = pass_otp(selection, arguments.io_encoding)
fake_key_raw(otp)
else:
# Enter username and password using fake-key and <Tab> (which seems to work almost universally), then switch
# back into insert-mode, so the form can be directly submitted by hitting enter afterwards

View File

@ -63,4 +63,8 @@ qt_log_ignore =
^inotify_add_watch\(".*"\) failed: "No space left on device"
^QSettings::value: Empty key passed
^Icon theme ".*" not found
^Error receiving trust for a CA certificate
xfail_strict = true
filterwarnings =
# This happens in many qutebrowser dependencies...
ignore:Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated, and in 3.8 it will stop working:DeprecationWarning

View File

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

View File

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

View File

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

View File

@ -22,11 +22,11 @@
import enum
import itertools
import sip
import attr
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QUrl, QObject, QSizeF, Qt
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QWidget, QApplication
from PyQt5.QtWidgets import QWidget, QApplication, QDialog
from PyQt5.QtPrintSupport import QPrintDialog
import pygments
import pygments.lexers
@ -38,6 +38,7 @@ from qutebrowser.utils import (utils, objreg, usertypes, log, qtutils,
urlutils, message)
from qutebrowser.misc import miscwidgets, objects
from qutebrowser.browser import mouse, hints
from qutebrowser.qt import sip
tab_id_gen = itertools.count(0)
@ -187,8 +188,9 @@ class AbstractPrinting:
"""Attribute of AbstractTab for printing the page."""
def __init__(self):
def __init__(self, tab):
self._widget = None
self._tab = tab
def check_pdf_support(self):
raise NotImplementedError
@ -212,6 +214,29 @@ class AbstractPrinting:
"""
raise NotImplementedError
def show_dialog(self):
"""Print with a QPrintDialog."""
self.check_printer_support()
def print_callback(ok):
"""Called when printing finished."""
if not ok:
message.error("Printing failed!")
diag.deleteLater()
def do_print():
"""Called when the dialog was closed."""
self.to_printer(diag.printer(), print_callback)
diag = QPrintDialog(self._tab)
if utils.is_mac:
# For some reason we get a segfault when using open() on macOS
ret = diag.exec_()
if ret == QDialog.Accepted:
do_print()
else:
diag.open(do_print)
class AbstractSearch(QObject):
@ -223,10 +248,19 @@ class AbstractSearch(QObject):
this view.
_flags: The flags of the last search (needs to be set by subclasses).
_widget: The underlying WebView widget.
Signals:
finished: Emitted when a search was finished.
arg: True if the text was found, False otherwise.
cleared: Emitted when an existing search was cleared.
"""
def __init__(self, parent=None):
finished = pyqtSignal(bool)
cleared = pyqtSignal()
def __init__(self, tab, parent=None):
super().__init__(parent)
self._tab = tab
self._widget = None
self.text = None
self.search_displayed = False
@ -370,9 +404,11 @@ class AbstractCaret(QObject):
Signals:
selection_toggled: Emitted when the selection was toggled.
arg: Whether the selection is now active.
follow_selected_done: Emitted when a follow_selection action is done.
"""
selection_toggled = pyqtSignal(bool)
follow_selected_done = pyqtSignal()
def __init__(self, tab, mode_manager, parent=None):
super().__init__(parent)
@ -596,6 +632,9 @@ class AbstractElements:
def find_css(self, selector, callback, *, only_visible=False):
"""Find all HTML elements matching a given selector async.
If there's an error, the callback is called with a webelem.Error
instance.
Args:
callback: The callback to be called when the search finished.
selector: The CSS selector to search for.
@ -641,20 +680,27 @@ class AbstractAudio(QObject):
muted_changed = pyqtSignal(bool)
recently_audible_changed = pyqtSignal(bool)
def __init__(self, parent=None):
def __init__(self, tab, parent=None):
super().__init__(parent)
self._widget = None
self._tab = tab
def set_muted(self, muted: bool):
"""Set this tab as muted or not."""
def set_muted(self, muted: bool, override: bool = False):
"""Set this tab as muted or not.
Arguments:
override: If set to True, muting/unmuting was done manually and
overrides future automatic mute/unmute changes based on
the URL.
"""
raise NotImplementedError
def is_muted(self):
"""Whether this tab is muted."""
raise NotImplementedError
def toggle_muted(self):
self.set_muted(not self.is_muted())
def toggle_muted(self, *, override: bool = False):
self.set_muted(not self.is_muted(), override=override)
def is_recently_audible(self):
"""Whether this tab has had audio playing recently."""
@ -829,12 +875,20 @@ class AbstractTab(QWidget):
navigation.navigation_type,
navigation.is_main_frame))
if (navigation.navigation_type == navigation.Type.link_clicked and
not navigation.url.isValid()):
msg = urlutils.get_errstring(navigation.url,
"Invalid link clicked")
message.error(msg)
self.data.open_target = usertypes.ClickTarget.normal
if not navigation.url.isValid():
# Also a WORKAROUND for missing IDNA 2008 support in QUrl, see
# https://bugreports.qt.io/browse/QTBUG-60364
if navigation.navigation_type == navigation.Type.link_clicked:
msg = urlutils.get_errstring(navigation.url,
"Invalid link clicked")
message.error(msg)
self.data.open_target = usertypes.ClickTarget.normal
log.webview.debug("Ignoring invalid URL {} in "
"acceptNavigationRequest: {}".format(
navigation.url.toDisplayString(),
navigation.url.errorString()))
navigation.accepted = False
def handle_auto_insert_mode(self, ok):
@ -893,10 +947,6 @@ class AbstractTab(QWidget):
self._progress = perc
self.load_progress.emit(perc)
@pyqtSlot()
def _on_ssl_errors(self):
self._has_ssl_errors = True
def url(self, requested=False):
raise NotImplementedError

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -23,11 +23,12 @@ import os
import time
import contextlib
from PyQt5.QtCore import pyqtSlot, QUrl, QTimer, pyqtSignal
from PyQt5.QtCore import pyqtSlot, QUrl, pyqtSignal
from PyQt5.QtWidgets import QProgressDialog, QApplication
from qutebrowser.config import config
from qutebrowser.commands import cmdutils, cmdexc
from qutebrowser.utils import (utils, objreg, log, usertypes, message,
debug, standarddir, qtutils)
from qutebrowser.utils import utils, objreg, log, usertypes, message, qtutils
from qutebrowser.misc import objects, sql
@ -35,6 +36,77 @@ from qutebrowser.misc import objects, sql
_USER_VERSION = 2
class HistoryProgress:
"""Progress dialog for history imports/conversions.
This makes WebHistory simpler as it can call methods of this class even
when we don't want to show a progress dialog (for very small imports). This
means tick() and finish() can be called even when start() wasn't.
"""
def __init__(self):
self._progress = None
self._value = 0
def start(self, text, maximum):
"""Start showing a progress dialog."""
self._progress = QProgressDialog()
self._progress.setMinimumDuration(500)
self._progress.setLabelText(text)
self._progress.setMaximum(maximum)
self._progress.setCancelButton(None)
self._progress.show()
QApplication.processEvents()
def tick(self):
"""Increase the displayed progress value."""
self._value += 1
if self._progress is not None:
self._progress.setValue(self._value)
QApplication.processEvents()
def finish(self):
"""Finish showing the progress dialog."""
if self._progress is not None:
self._progress.hide()
class CompletionMetaInfo(sql.SqlTable):
"""Table containing meta-information for the completion."""
KEYS = {
'force_rebuild': False,
}
def __init__(self, parent=None):
super().__init__("CompletionMetaInfo", ['key', 'value'],
constraints={'key': 'PRIMARY KEY'})
for key, default in self.KEYS.items():
if key not in self:
self[key] = default
def _check_key(self, key):
if key not in self.KEYS:
raise KeyError(key)
def __contains__(self, key):
self._check_key(key)
query = self.contains_query('key')
return query.run(val=key).value()
def __getitem__(self, key):
self._check_key(key)
query = sql.Query('SELECT value FROM CompletionMetaInfo '
'WHERE key = :key')
return query.run(key=key).value()
def __setitem__(self, key, value):
self._check_key(key)
self.insert({'key': key, 'value': value}, replace=True)
class CompletionHistory(sql.SqlTable):
"""History which only has the newest entry for each URL."""
@ -50,26 +122,46 @@ class CompletionHistory(sql.SqlTable):
class WebHistory(sql.SqlTable):
"""The global history of visited pages."""
"""The global history of visited pages.
Attributes:
completion: A CompletionHistory instance.
metainfo: A CompletionMetaInfo instance.
_progress: A HistoryProgress instance.
Class attributes:
_PROGRESS_THRESHOLD: When to start showing progress dialogs.
"""
# All web history cleared
history_cleared = pyqtSignal()
# one url cleared
url_cleared = pyqtSignal(QUrl)
def __init__(self, parent=None):
_PROGRESS_THRESHOLD = 1000
def __init__(self, progress, parent=None):
super().__init__("History", ['url', 'title', 'atime', 'redirect'],
constraints={'url': 'NOT NULL',
'title': 'NOT NULL',
'atime': 'NOT NULL',
'redirect': 'NOT NULL'},
parent=parent)
self._progress = progress
self.completion = CompletionHistory(parent=self)
self.metainfo = CompletionMetaInfo(parent=self)
if sql.Query('pragma user_version').run().value() < _USER_VERSION:
self.completion.delete_all()
if self.metainfo['force_rebuild']:
self.completion.delete_all()
self.metainfo['force_rebuild'] = False
if not self.completion:
# either the table is out-of-date or the user wiped it manually
self._rebuild_completion()
self.create_index('HistoryIndex', 'url')
self.create_index('HistoryAtimeIndex', 'atime')
self._contains_query = self.contains_query('url')
@ -87,21 +179,29 @@ class WebHistory(sql.SqlTable):
'ORDER BY atime desc '
'limit :limit offset :offset')
config.instance.changed.connect(self._on_config_changed)
def __repr__(self):
return utils.get_repr(self, length=len(self))
def __contains__(self, url):
return self._contains_query.run(val=url).value()
@config.change_filter('completion.web_history.exclude')
def _on_config_changed(self):
self.metainfo['force_rebuild'] = True
@contextlib.contextmanager
def _handle_sql_errors(self):
try:
yield
except sql.SqlError as e:
if e.environmental:
message.error("Failed to write history: {}".format(e.text()))
else:
raise
except sql.SqlEnvironmentError as e:
message.error("Failed to write history: {}".format(e.text()))
def _is_excluded(self, url):
"""Check if the given URL is excluded from the completion."""
patterns = config.cache['completion.web_history.exclude']
return any(pattern.matches(url) for pattern in patterns)
def _rebuild_completion(self):
data = {'url': [], 'title': [], 'last_atime': []}
@ -109,10 +209,23 @@ class WebHistory(sql.SqlTable):
q = sql.Query('SELECT url, title, max(atime) AS atime FROM History '
'WHERE NOT redirect and url NOT LIKE "qute://back%" '
'GROUP BY url ORDER BY atime asc')
for entry in q.run():
data['url'].append(self._format_completion_url(QUrl(entry.url)))
entries = list(q.run())
if len(entries) > self._PROGRESS_THRESHOLD:
self._progress.start("Rebuilding completion...", len(entries))
for entry in entries:
self._progress.tick()
url = QUrl(entry.url)
if self._is_excluded(url):
continue
data['url'].append(self._format_completion_url(url))
data['title'].append(entry.title)
data['last_atime'].append(entry.atime)
self._progress.finish()
self.completion.insert_batch(data, replace=True)
sql.Query('pragma user_version = {}'.format(_USER_VERSION)).run()
@ -218,114 +331,15 @@ class WebHistory(sql.SqlTable):
'title': title,
'atime': atime,
'redirect': redirect})
if not redirect:
self.completion.insert({
'url': self._format_completion_url(url),
'title': title,
'last_atime': atime
}, replace=True)
def _parse_entry(self, line):
"""Parse a history line like '12345 http://example.com title'."""
if not line or line.startswith('#'):
return None
data = line.split(maxsplit=2)
if len(data) == 2:
atime, url = data
title = ""
elif len(data) == 3:
atime, url, title = data
else:
raise ValueError("2 or 3 fields expected")
if redirect or self._is_excluded(url):
return
# http://xn--pple-43d.com/ with
# https://bugreports.qt.io/browse/QTBUG-60364
if url in ['http://.com/', 'https://.com/',
'http://www..com/', 'https://www..com/']:
return None
url = QUrl(url)
if not url.isValid():
raise ValueError("Invalid URL: {}".format(url.errorString()))
# https://github.com/qutebrowser/qutebrowser/issues/2646
if url.scheme() == 'data':
return None
# https://github.com/qutebrowser/qutebrowser/issues/670
atime = atime.lstrip('\0')
if '-' in atime:
atime, flags = atime.split('-')
else:
flags = ''
if not set(flags).issubset('r'):
raise ValueError("Invalid flags {!r}".format(flags))
redirect = 'r' in flags
return (url, title, int(atime), redirect)
def import_txt(self):
"""Import a history text file into sqlite if it exists.
In older versions of qutebrowser, history was stored in a text format.
This converts that file into the new sqlite format and moves it to a
backup location.
"""
path = os.path.join(standarddir.data(), 'history')
if not os.path.isfile(path):
return
def action():
"""Actually run the import."""
with debug.log_time(log.init, 'Import old history file to sqlite'):
try:
self._read(path)
except ValueError as ex:
message.error('Failed to import history: {}'.format(ex))
else:
self._write_backup(path)
# delay to give message time to appear before locking down for import
message.info('Converting {} to sqlite...'.format(path))
QTimer.singleShot(100, action)
def _read(self, path):
"""Import a text file into the sql database."""
with open(path, 'r', encoding='utf-8') as f:
data = {'url': [], 'title': [], 'atime': [], 'redirect': []}
completion_data = {'url': [], 'title': [], 'last_atime': []}
for (i, line) in enumerate(f):
try:
parsed = self._parse_entry(line.strip())
if parsed is None:
continue
url, title, atime, redirect = parsed
data['url'].append(self._format_url(url))
data['title'].append(title)
data['atime'].append(atime)
data['redirect'].append(redirect)
if not redirect:
completion_data['url'].append(
self._format_completion_url(url))
completion_data['title'].append(title)
completion_data['last_atime'].append(atime)
except ValueError as ex:
raise ValueError('Failed to parse line #{} of {}: "{}"'
.format(i, path, ex))
self.insert_batch(data)
self.completion.insert_batch(completion_data, replace=True)
def _write_backup(self, path):
bak = path + '.bak'
message.info('History import complete. Appending {} to {}'
.format(path, bak))
with open(path, 'r', encoding='utf-8') as infile:
with open(bak, 'a', encoding='utf-8') as outfile:
for line in infile:
outfile.write('\n' + line)
os.remove(path)
self.completion.insert({
'url': self._format_completion_url(url),
'title': title,
'last_atime': atime
}, replace=True)
def _format_url(self, url):
return url.toString(QUrl.RemovePassword | QUrl.FullyEncoded)
@ -360,7 +374,8 @@ def init(parent=None):
Args:
parent: The parent to use for WebHistory.
"""
history = WebHistory(parent=parent)
progress = HistoryProgress()
history = WebHistory(progress=progress, parent=parent)
objreg.register('web-history', history)
if objects.backend == usertypes.Backend.QtWebKit: # pragma: no cover

View File

@ -22,7 +22,7 @@
from PyQt5.QtCore import QObject, QEvent, Qt, QTimer
from qutebrowser.config import config
from qutebrowser.utils import message, log, usertypes, qtutils
from qutebrowser.utils import message, log, usertypes, qtutils, objreg
from qutebrowser.keyinput import modeman
@ -40,11 +40,12 @@ class ChildEventFilter(QObject):
_widget: The widget expected to send out childEvents.
"""
def __init__(self, eventfilter, widget, parent=None):
def __init__(self, eventfilter, widget, win_id, parent=None):
super().__init__(parent)
self._filter = eventfilter
assert widget is not None
self._widget = widget
self._win_id = win_id
def eventFilter(self, obj, event):
"""Act on ChildAdded events."""
@ -57,7 +58,22 @@ class ChildEventFilter(QObject):
if qtutils.version_check('5.11', compiled=False, exact=True):
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-68076
QTimer.singleShot(0, self._widget.setFocus)
pass_modes = [usertypes.KeyMode.command,
usertypes.KeyMode.prompt,
usertypes.KeyMode.yesno]
if modeman.instance(self._win_id).mode not in pass_modes:
tabbed_browser = objreg.get('tabbed-browser',
scope='window',
window=self._win_id)
current_index = tabbed_browser.widget.currentIndex()
try:
widget_index = tabbed_browser.widget.indexOf(
self._widget.parent())
except RuntimeError:
widget_index = -1
if current_index == widget_index:
QTimer.singleShot(0, self._widget.setFocus)
elif event.type() == QEvent.ChildRemoved:
child = event.child()
log.mouse.debug("{}: removed child {}".format(obj, child))
@ -100,9 +116,13 @@ class MouseEventFilter(QObject):
self._ignore_wheel_event = True
pos = e.pos()
if pos.x() < 0 or pos.y() < 0:
log.mouse.warning("Ignoring invalid click at {}".format(pos))
return False
if e.button() != Qt.NoButton:
self._tab.elements.find_at_pos(e.pos(),
self._mousepress_insertmode_cb)
self._tab.elements.find_at_pos(pos, self._mousepress_insertmode_cb)
return False
@ -125,6 +145,10 @@ class MouseEventFilter(QObject):
return True
if e.modifiers() & Qt.ControlModifier:
mode = modeman.instance(self._tab.win_id).mode
if mode == usertypes.KeyMode.passthrough:
return False
divider = config.val.zoom.mouse_divider
if divider == 0:
return False

View File

@ -117,7 +117,10 @@ def prevnext(*, browsertab, win_id, baseurl, prev=False,
"""
def _prevnext_cb(elems):
if elems is None:
message.error("There was an error while getting hint elements")
message.error("Unknown error while getting hint elements")
return
elif isinstance(elems, webelem.Error):
message.error(str(elems))
return
elem = _find_prevnext(prev, elems)
@ -147,5 +150,9 @@ def prevnext(*, browsertab, win_id, baseurl, prev=False,
else:
browsertab.openurl(url)
browsertab.elements.find_css(webelem.SELECTORS[webelem.Group.links],
_prevnext_cb)
try:
link_selector = webelem.css_selector('links', baseurl)
except webelem.Error as e:
raise Error(str(e))
browsertab.elements.find_css(link_selector, _prevnext_cb)

View File

@ -22,9 +22,12 @@
import os
from PyQt5.QtCore import QUrl
from PyQt5.QtCore import QUrl, QUrlQuery
from qutebrowser.utils import utils, javascript
from qutebrowser.utils import (utils, javascript, jinja, qtutils, usertypes,
standarddir, log)
from qutebrowser.misc import objects
from qutebrowser.config import config
class PDFJSNotFound(Exception):
@ -41,73 +44,73 @@ class PDFJSNotFound(Exception):
super().__init__(message)
def generate_pdfjs_page(url):
"""Return the html content of a page that displays url with pdfjs.
def generate_pdfjs_page(filename, url):
"""Return the html content of a page that displays a file with pdfjs.
Returns a string.
Args:
url: The url of the pdf as QUrl.
filename: The filename of the PDF to open.
url: The URL being opened.
"""
viewer = get_pdfjs_res('web/viewer.html').decode('utf-8')
script = _generate_pdfjs_script(url)
html_page = viewer.replace('</body>',
'</body><script>{}</script>'.format(script))
return html_page
if not is_available():
pdfjs_dir = os.path.join(standarddir.data(), 'pdfjs')
return jinja.render('no_pdfjs.html',
url=url.toDisplayString(),
title="PDF.js not found",
pdfjs_dir=pdfjs_dir)
html = get_pdfjs_res('web/viewer.html').decode('utf-8')
script = _generate_pdfjs_script(filename)
html = html.replace('</body>',
'</body><script>{}</script>'.format(script))
# WORKAROUND for the fact that PDF.js tries to use the Fetch API even with
# qute:// URLs.
pdfjs_script = '<script src="../build/pdf.js"></script>'
html = html.replace(pdfjs_script,
'<script>window.Response = undefined;</script>\n' +
pdfjs_script)
return html
def _generate_pdfjs_script(url):
def _generate_pdfjs_script(filename):
"""Generate the script that shows the pdf with pdf.js.
Args:
url: The url of the pdf page as QUrl.
filename: The name of the file to open.
"""
return (
'document.addEventListener("DOMContentLoaded", function() {{\n'
' PDFJS.verbosity = PDFJS.VERBOSITY_LEVELS.info;\n'
' (window.PDFView || window.PDFViewerApplication).open("{url}");\n'
'}});\n'
).format(url=javascript.string_escape(url.toString(QUrl.FullyEncoded)))
url = QUrl('qute://pdfjs/file')
url_query = QUrlQuery()
url_query.addQueryItem('filename', filename)
url.setQuery(url_query)
return jinja.js_environment.from_string("""
document.addEventListener("DOMContentLoaded", function() {
if (typeof window.PDFJS !== 'undefined') {
// v1.x
{% if disable_create_object_url %}
window.PDFJS.disableCreateObjectURL = true;
{% endif %}
window.PDFJS.verbosity = window.PDFJS.VERBOSITY_LEVELS.info;
} else {
// v2.x
const options = window.PDFViewerApplicationOptions;
{% if disable_create_object_url %}
options.set('disableCreateObjectURL', true);
{% endif %}
options.set('verbosity', pdfjsLib.VerbosityLevel.INFOS);
}
def fix_urls(asset):
"""Take an html page and replace each relative URL with an absolute.
This is specialized for pdf.js files and not a general purpose function.
Args:
asset: js file or html page as string.
"""
new_urls = [
('viewer.css', 'qute://pdfjs/web/viewer.css'),
('compatibility.js', 'qute://pdfjs/web/compatibility.js'),
('locale/locale.properties',
'qute://pdfjs/web/locale/locale.properties'),
('l10n.js', 'qute://pdfjs/web/l10n.js'),
('../build/pdf.js', 'qute://pdfjs/build/pdf.js'),
('debugger.js', 'qute://pdfjs/web/debugger.js'),
('viewer.js', 'qute://pdfjs/web/viewer.js'),
('compressed.tracemonkey-pldi-09.pdf', ''),
('./images/', 'qute://pdfjs/web/images/'),
('../build/pdf.worker.js', 'qute://pdfjs/build/pdf.worker.js'),
('../web/cmaps/', 'qute://pdfjs/web/cmaps/'),
]
for original, new in new_urls:
asset = asset.replace(original, new)
return asset
SYSTEM_PDFJS_PATHS = [
# Debian pdf.js-common
# Arch Linux pdfjs (AUR)
'/usr/share/pdf.js/',
# Arch Linux pdf.js (AUR)
'/usr/share/javascript/pdf.js/',
# Debian libjs-pdf
'/usr/share/javascript/pdf/',
# fallback
os.path.expanduser('~/.local/share/qutebrowser/pdfjs/'),
]
const viewer = window.PDFView || window.PDFViewerApplication;
viewer.open({{ url }});
});
""").render(
url=javascript.to_js(url.toString(QUrl.FullyEncoded)),
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-70420
disable_create_object_url=(
not qtutils.version_check('5.12') and
not qtutils.version_check('5.7.1', exact=True, compiled=False) and
objects.backend == usertypes.Backend.QtWebEngine))
def get_pdfjs_res_and_path(path):
@ -124,11 +127,25 @@ def get_pdfjs_res_and_path(path):
content = None
file_path = None
system_paths = [
# Debian pdf.js-common
# Arch Linux pdfjs (AUR)
'/usr/share/pdf.js/',
# Arch Linux pdf.js (AUR)
'/usr/share/javascript/pdf.js/',
# Debian libjs-pdf
'/usr/share/javascript/pdf/',
# fallback
os.path.join(standarddir.data(), 'pdfjs'),
# hardcoded fallback for --temp-basedir
os.path.expanduser('~/.local/share/qutebrowser/pdfjs/'),
]
# First try a system wide installation
# System installations might strip off the 'build/' or 'web/' prefixes.
# qute expects them, so we need to adjust for it.
names_to_try = [path, _remove_prefix(path)]
for system_path in SYSTEM_PDFJS_PATHS:
for system_path in system_paths:
content, file_path = _read_from_system(system_path, names_to_try)
if content is not None:
break
@ -140,14 +157,11 @@ def get_pdfjs_res_and_path(path):
content = utils.read_file(res_path, binary=True)
except FileNotFoundError:
raise PDFJSNotFound(path) from None
except OSError as e:
log.misc.warning("OSError while reading PDF.js file: {}".format(e))
raise PDFJSNotFound(path) from None
try:
# Might be script/html or might be binary
text_content = content.decode('utf-8')
except UnicodeDecodeError:
return (content, file_path)
text_content = fix_urls(text_content)
return (text_content.encode('utf-8'), file_path)
return content, file_path
def get_pdfjs_res(path):
@ -193,7 +207,10 @@ def _read_from_system(system_path, names):
full_path = os.path.join(system_path, name)
with open(full_path, 'rb') as f:
return (f.read(), full_path)
except OSError:
except FileNotFoundError:
continue
except OSError as e:
log.misc.warning("OSError while reading PDF.js file: {}".format(e))
continue
return (None, None)
@ -206,3 +223,22 @@ def is_available():
return False
else:
return True
def should_use_pdfjs(mimetype, url):
"""Check whether PDF.js should be used."""
# e.g. 'blob:qute%3A///b45250b3-787e-44d1-a8d8-c2c90f81f981'
is_download_url = (url.scheme() == 'blob' and
QUrl(url.path()).scheme() == 'qute')
is_pdf = mimetype in ['application/pdf', 'application/x-pdf']
return is_pdf and not is_download_url and config.val.content.pdfjs
def get_main_url(filename):
"""Get the URL to be opened to view a local PDF."""
url = QUrl('qute://pdfjs/web/viewer.html')
query = QUrlQuery()
query.addQueryItem('filename', filename) # read from our JS
query.addQueryItem('file', '') # to avoid pdfjs opening the default PDF
url.setQuery(query)
return url

View File

@ -29,7 +29,7 @@ from PyQt5.QtCore import pyqtSlot, pyqtSignal, QTimer
from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply
from qutebrowser.config import config
from qutebrowser.utils import message, usertypes, log, urlutils, utils
from qutebrowser.utils import message, usertypes, log, urlutils, utils, debug
from qutebrowser.browser import downloads
from qutebrowser.browser.webkit import http
from qutebrowser.browser.webkit.network import networkmanager
@ -307,7 +307,14 @@ class DownloadItem(downloads.AbstractDownloadItem):
"""Handle QNetworkReply errors."""
if code == QNetworkReply.OperationCanceledError:
return
self._die(self._reply.errorString())
if self._reply is None:
error = "Unknown error: {}".format(
debug.qenum_key(QNetworkReply, code))
else:
error = self._reply.errorString()
self._die(error)
@pyqtSlot()
def _on_read_timer_timeout(self):

View File

@ -24,62 +24,72 @@ Module attributes:
_HANDLERS: The handlers registered via decorators.
"""
import html
import json
import os
import time
import textwrap
import mimetypes
import urllib
import collections
import base64
import pkg_resources
import sip
from PyQt5.QtCore import QUrlQuery, QUrl
try:
import secrets
except ImportError:
# New in Python 3.6
secrets = None
from PyQt5.QtCore import QUrlQuery, QUrl, qVersion
import qutebrowser
from qutebrowser.browser import pdfjs, downloads
from qutebrowser.config import config, configdata, configexc, configdiff
from qutebrowser.utils import (version, utils, jinja, log, message, docutils,
objreg, urlutils)
from qutebrowser.misc import objects
from qutebrowser.qt import sip
pyeval_output = ":pyeval was never called"
spawn_output = ":spawn was never called"
csrf_token = None
_HANDLERS = {}
class NoHandlerFound(Exception):
class Error(Exception):
"""Raised when no handler was found for the given URL."""
"""Exception for generic errors on a qute:// page."""
pass
class QuteSchemeOSError(Exception):
class NotFoundError(Error):
"""Called when there was an OSError inside a handler."""
"""Raised when the given URL was not found."""
pass
class QuteSchemeError(Exception):
class SchemeOSError(Error):
"""Exception to signal that a handler should return an ErrorReply.
"""Raised when there was an OSError inside a handler."""
Attributes correspond to the arguments in
networkreply.ErrorNetworkReply.
pass
Attributes:
errorstring: Error string to print.
error: Numerical error value.
"""
def __init__(self, errorstring, error):
self.errorstring = errorstring
self.error = error
super().__init__(errorstring)
class UrlInvalidError(Error):
"""Raised when an invalid URL was opened."""
pass
class RequestDeniedError(Error):
"""Raised when the request is forbidden."""
pass
class Redirect(Exception):
@ -101,12 +111,10 @@ class add_handler: # noqa: N801,N806 pylint: disable=invalid-name
Attributes:
_name: The 'foo' part of qute://foo
backend: Limit which backends the handler can run with.
"""
def __init__(self, name, backend=None):
def __init__(self, name):
self._name = name
self._backend = backend
self._function = None
def __call__(self, function):
@ -116,19 +124,7 @@ class add_handler: # noqa: N801,N806 pylint: disable=invalid-name
def wrapper(self, *args, **kwargs):
"""Call the underlying function."""
if self._backend is not None and objects.backend != self._backend:
return self.wrong_backend_handler(*args, **kwargs)
else:
return self._function(*args, **kwargs)
def wrong_backend_handler(self, url):
"""Show an error page about using the invalid backend."""
html = jinja.render('error.html',
title="Error while opening qute://url",
url=url.toDisplayString(),
error='{} is not available with this '
'backend'.format(url.toDisplayString()))
return 'text/html', html
return self._function(*args, **kwargs)
def data_for_url(url):
@ -170,15 +166,13 @@ def data_for_url(url):
try:
handler = _HANDLERS[host]
except KeyError:
raise NoHandlerFound(url)
raise NotFoundError("No handler found for {}".format(
url.toDisplayString()))
try:
mimetype, data = handler(url)
except OSError as e:
# FIXME:qtwebengine how to handle this?
raise QuteSchemeOSError(e)
except QuteSchemeError:
raise
raise SchemeOSError(e)
assert mimetype is not None, url
if mimetype == 'text/html' and isinstance(data, str):
@ -196,11 +190,11 @@ def qute_bookmarks(_url):
quickmarks = sorted(objreg.get('quickmark-manager').marks.items(),
key=lambda x: x[0]) # Sort by name
html = jinja.render('bookmarks.html',
title='Bookmarks',
bookmarks=bookmarks,
quickmarks=quickmarks)
return 'text/html', html
src = jinja.render('bookmarks.html',
title='Bookmarks',
bookmarks=bookmarks,
quickmarks=quickmarks)
return 'text/html', src
@add_handler('tabs')
@ -218,10 +212,10 @@ def qute_tabs(_url):
urlstr = tab.url().toDisplayString()
tabs[str(win_id)].append((tab.title(), urlstr))
html = jinja.render('tabs.html',
title='Tabs',
tab_list_by_window=tabs)
return 'text/html', html
src = jinja.render('tabs.html',
title='Tabs',
tab_list_by_window=tabs)
return 'text/html', src
def history_data(start_time, offset=None):
@ -241,8 +235,9 @@ def history_data(start_time, offset=None):
end_time = start_time - 24*60*60
entries = hist.entries_between(end_time, start_time)
return [{"url": e.url, "title": e.title or e.url, "time": e.atime}
for e in entries]
return [{"url": e.url,
"title": html.escape(e.title) or html.escape(e.url),
"time": e.atime} for e in entries]
@add_handler('history')
@ -252,14 +247,14 @@ def qute_history(url):
try:
offset = QUrlQuery(url).queryItemValue("offset")
offset = int(offset) if offset else None
except ValueError as e:
raise QuteSchemeError("Query parameter offset is invalid", e)
except ValueError:
raise UrlInvalidError("Query parameter offset is invalid")
# Use start_time in query or current time.
try:
start_time = QUrlQuery(url).queryItemValue("start_time")
start_time = float(start_time) if start_time else time.time()
except ValueError as e:
raise QuteSchemeError("Query parameter start_time is invalid", e)
except ValueError:
raise UrlInvalidError("Query parameter start_time is invalid")
return 'text/html', json.dumps(history_data(start_time, offset))
else:
@ -281,31 +276,31 @@ def qute_javascript(url):
path = "javascript" + os.sep.join(path.split('/'))
return 'text/html', utils.read_file(path, binary=False)
else:
raise QuteSchemeError("No file specified", ValueError())
raise UrlInvalidError("No file specified")
@add_handler('pyeval')
def qute_pyeval(_url):
"""Handler for qute://pyeval."""
html = jinja.render('pre.html', title='pyeval', content=pyeval_output)
return 'text/html', html
src = jinja.render('pre.html', title='pyeval', content=pyeval_output)
return 'text/html', src
@add_handler('spawn-output')
def qute_spawn_output(_url):
"""Handler for qute://spawn-output."""
html = jinja.render('pre.html', title='spawn output', content=spawn_output)
return 'text/html', html
src = jinja.render('pre.html', title='spawn output', content=spawn_output)
return 'text/html', src
@add_handler('version')
@add_handler('verizon')
def qute_version(_url):
"""Handler for qute://version."""
html = jinja.render('version.html', title='Version info',
version=version.version(),
copyright=qutebrowser.__copyright__)
return 'text/html', html
src = jinja.render('version.html', title='Version info',
version=version.version(),
copyright=qutebrowser.__copyright__)
return 'text/html', src
@add_handler('plainlog')
@ -323,8 +318,8 @@ def qute_plainlog(url):
if not level:
level = 'vdebug'
text = log.ram_handler.dump_log(html=False, level=level)
html = jinja.render('pre.html', title='log', content=text)
return 'text/html', html
src = jinja.render('pre.html', title='log', content=text)
return 'text/html', src
@add_handler('log')
@ -343,8 +338,8 @@ def qute_log(url):
level = 'vdebug'
html_log = log.ram_handler.dump_log(html=True, level=level)
html = jinja.render('log.html', title='log', content=html_log)
return 'text/html', html
src = jinja.render('log.html', title='log', content=html_log)
return 'text/html', src
@add_handler('gpl')
@ -353,6 +348,23 @@ def qute_gpl(_url):
return 'text/html', utils.read_file('html/license.html')
def _asciidoc_fallback_path(html_path):
"""Fall back to plaintext asciidoc if the HTML is unavailable."""
asciidoc_path = html_path.replace('.html', '.asciidoc')
asciidoc_paths = [asciidoc_path]
if asciidoc_path.startswith('html/doc/'):
asciidoc_paths += [asciidoc_path.replace('html/doc/', '../doc/help/'),
asciidoc_path.replace('html/doc/', '../doc/')]
for path in asciidoc_paths:
try:
return utils.read_file(path)
except OSError:
pass
return None
@add_handler('help')
def qute_help(url):
"""Handler for qute://help."""
@ -370,23 +382,14 @@ def qute_help(url):
try:
bdata = utils.read_file(path, binary=True)
except OSError as e:
raise QuteSchemeOSError(e)
mimetype, _encoding = mimetypes.guess_type(urlpath)
assert mimetype is not None, url
raise SchemeOSError(e)
mimetype = utils.guess_mimetype(urlpath)
return mimetype, bdata
try:
data = utils.read_file(path)
except OSError:
# No .html around, let's see if we find the asciidoc
asciidoc_path = path.replace('.html', '.asciidoc')
if asciidoc_path.startswith('html/doc/'):
asciidoc_path = asciidoc_path.replace('html/doc/', '../doc/help/')
try:
asciidoc = utils.read_file(asciidoc_path)
except OSError:
asciidoc = None
asciidoc = _asciidoc_fallback_path(path)
if asciidoc is None:
raise
@ -412,17 +415,6 @@ def qute_help(url):
return 'text/html', data
@add_handler('backend-warning')
def qute_backend_warning(_url):
"""Handler for qute://backend-warning."""
html = jinja.render('backend-warning.html',
distribution=version.distribution(),
Distribution=version.Distribution,
version=pkg_resources.parse_version,
title="Legacy backend warning")
return 'text/html', html
def _qute_settings_set(url):
"""Handler for qute://settings/set."""
query = QUrlQuery(url)
@ -447,13 +439,29 @@ def _qute_settings_set(url):
@add_handler('settings')
def qute_settings(url):
"""Handler for qute://settings. View/change qute configuration."""
global csrf_token
if url.path() == '/set':
if url.password() != csrf_token:
message.error("Invalid CSRF token for qute://settings!")
raise RequestDeniedError("Invalid CSRF token!")
return _qute_settings_set(url)
html = jinja.render('settings.html', title='settings',
configdata=configdata,
confget=config.instance.get_str)
return 'text/html', html
# Requests to qute://settings/set should only be allowed from
# qute://settings. As an additional security precaution, we generate a CSRF
# token to use here.
if secrets:
csrf_token = secrets.token_urlsafe()
else:
# On Python < 3.6, from secrets.py
token = base64.urlsafe_b64encode(os.urandom(32))
csrf_token = token.rstrip(b'=').decode('ascii')
src = jinja.render('settings.html', title='settings',
configdata=configdata,
confget=config.instance.get_str,
csrf_token=csrf_token)
return 'text/html', src
@add_handler('bindings')
@ -467,9 +475,9 @@ def qute_bindings(_url):
for mode in modes:
bindings[mode] = config.key_instance.get_bindings_for(mode)
html = jinja.render('bindings.html', title='Bindings',
bindings=bindings)
return 'text/html', html
src = jinja.render('bindings.html', title='Bindings',
bindings=bindings)
return 'text/html', src
@add_handler('back')
@ -478,10 +486,10 @@ def qute_back(url):
Simple page to free ram / lazy load a site, goes back on focusing the tab.
"""
html = jinja.render(
src = jinja.render(
'back.html',
title='Suspended: ' + urllib.parse.unquote(url.fragment()))
return 'text/html', html
return 'text/html', src
@add_handler('configdiff')
@ -504,3 +512,59 @@ def qute_pastebin_version(_url):
"""Handler that pastebins the version string."""
version.pastebin_version()
return 'text/plain', b'Paste called.'
@add_handler('pdfjs')
def qute_pdfjs(url):
"""Handler for qute://pdfjs. Return the pdf.js viewer."""
if url.path() == '/file':
filename = QUrlQuery(url).queryItemValue('filename')
if not filename:
raise UrlInvalidError("Missing filename")
if '/' in filename or os.sep in filename:
raise RequestDeniedError("Path separator in filename.")
path = os.path.join(downloads.temp_download_manager.get_tmpdir().name,
filename)
with open(path, 'rb') as f:
data = f.read()
mimetype = utils.guess_mimetype(filename, fallback=True)
return mimetype, data
if url.path() == '/web/viewer.html':
filename = QUrlQuery(url).queryItemValue("filename")
if not filename:
raise UrlInvalidError("Missing filename")
data = pdfjs.generate_pdfjs_page(filename, url)
return 'text/html', data
try:
data = pdfjs.get_pdfjs_res(url.path())
except pdfjs.PDFJSNotFound as e:
# Logging as the error might get lost otherwise since we're not showing
# the error page if a single asset is missing. This way we don't lose
# information, as the failed pdfjs requests are still in the log.
log.misc.warning(
"pdfjs resource requested but not found: {}".format(e.path))
raise NotFoundError("Can't find pdfjs resource '{}'".format(e.path))
else:
mimetype = utils.guess_mimetype(url.fileName(), fallback=True)
return mimetype, data
@add_handler('warning')
def qute_warning(url):
"""Handler for qute://warning."""
path = url.path()
if path == '/old-qt':
src = jinja.render('warning-old-qt.html',
title='Old Qt warning',
qt_version=qVersion())
elif path == '/webkit':
src = jinja.render('warning-webkit.html',
title='QtWebKit backend warning')
else:
raise NotFoundError("Invalid warning page {}".format(path))
return 'text/html', src

View File

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

View File

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

View File

@ -17,14 +17,8 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Generic web element related code.
"""Generic web element related code."""
Module attributes:
Group: Enum for different kinds of groups.
SELECTORS: CSS selectors for different groups of elements.
"""
import enum
import collections.abc
from PyQt5.QtCore import QUrl, Qt, QEvent, QTimer
@ -36,25 +30,6 @@ from qutebrowser.mainwindow import mainwindow
from qutebrowser.utils import log, usertypes, utils, qtutils, objreg
Group = enum.Enum('Group', ['all', 'links', 'images', 'url', 'inputs'])
SELECTORS = {
Group.all: ('a, area, textarea, select, input:not([type=hidden]), button, '
'frame, iframe, link, summary, [onclick], [onmousedown], '
'[role=link], [role=option], [role=button], img, '
# Angular 1 selectors
'[ng-click], [ngClick], [data-ng-click], [x-ng-click]'),
Group.links: 'a[href], area[href], link[href], [role=link][href]',
Group.images: 'img',
Group.url: '[src], [href]',
Group.inputs: ('input[type=text], input[type=email], input[type=url], '
'input[type=tel], input[type=number], '
'input[type=password], input[type=search], '
'input:not([type]), textarea'),
}
class Error(Exception):
"""Base class for WebElement errors."""
@ -69,6 +44,18 @@ class OrphanedError(Error):
pass
def css_selector(group, url):
"""Get a CSS selector for the given group/URL."""
selectors = config.instance.get('hints.selectors', url)
if group not in selectors:
selectors = config.val.hints.selectors
if group not in selectors:
raise Error("Undefined hinting group {!r}".format(group))
return ','.join(selectors[group])
class AbstractWebElement(collections.abc.MutableMapping):
"""A wrapper around QtWebKit/QtWebEngine web element.
@ -139,6 +126,18 @@ class AbstractWebElement(collections.abc.MutableMapping):
"""Set the element value."""
raise NotImplementedError
def dispatch_event(self, event, bubbles=False,
cancelable=False, composed=False):
"""Dispatch an event to the element.
Args:
bubbles: Whether this event should bubble.
cancelable: Whether this event can be cancelled.
composed: Whether the event will trigger listeners outside of a
shadow root.
"""
raise NotImplementedError
def insert_text(self, text):
"""Insert the given text into the element."""
raise NotImplementedError

View File

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

View File

@ -19,6 +19,7 @@
"""A request interceptor taking care of adblocking and custom headers."""
from PyQt5.QtCore import QUrl
from PyQt5.QtWebEngineCore import (QWebEngineUrlRequestInterceptor,
QWebEngineUrlRequestInfo)
@ -68,15 +69,29 @@ class RequestInterceptor(QWebEngineUrlRequestInterceptor):
info.firstPartyUrl().toDisplayString(),
resource_type, navigation_type))
url = info.requestUrl()
first_party = info.firstPartyUrl()
if ((url.scheme(), url.host(), url.path()) ==
('qute', 'settings', '/set')):
if (first_party != QUrl('qute://settings/') or
info.resourceType() !=
QWebEngineUrlRequestInfo.ResourceTypeXhr):
log.webview.warning("Blocking malicious request from {} to {}"
.format(first_party.toDisplayString(),
url.toDisplayString()))
info.block(True)
return
# FIXME:qtwebengine only block ads for NavigationTypeOther?
if self._host_blocker.is_blocked(info.requestUrl()):
if self._host_blocker.is_blocked(url, first_party):
log.webview.info("Request to {} blocked by host blocker.".format(
info.requestUrl().host()))
url.host()))
info.block(True)
for header, value in shared.custom_headers():
for header, value in shared.custom_headers(url=url):
info.setHttpHeader(header, value)
user_agent = config.val.content.headers.user_agent
user_agent = config.instance.get('content.headers.user_agent', url=url)
if user_agent is not None:
info.setHttpHeader(b'User-Agent', user_agent.encode('ascii'))

View File

@ -21,10 +21,12 @@
import glob
import os
import os.path
import re
import shutil
from PyQt5.QtCore import QLibraryInfo
from qutebrowser.utils import log, message
from qutebrowser.utils import log, message, standarddir, qtutils
dict_version_re = re.compile(r".+-(?P<version>[0-9]+-[0-9]+?)\.bdic")
@ -39,9 +41,12 @@ def version(filename):
return tuple(int(n) for n in match.group('version').split('-'))
def dictionary_dir():
def dictionary_dir(old=False):
"""Return the path (str) to the QtWebEngine's dictionaries directory."""
datapath = QLibraryInfo.location(QLibraryInfo.DataPath)
if qtutils.version_check('5.10', compiled=False) and not old:
datapath = standarddir.data()
else:
datapath = QLibraryInfo.location(QLibraryInfo.DataPath)
return os.path.join(datapath, 'qtwebengine_dictionaries')
@ -73,3 +78,16 @@ def local_filename(code):
"""
all_installed = local_files(code)
return os.path.splitext(all_installed[0])[0] if all_installed else None
def init():
"""Initialize the dictionary path if supported."""
if qtutils.version_check('5.10', compiled=False):
new_dir = dictionary_dir()
old_dir = dictionary_dir(old=True)
os.environ['QTWEBENGINE_DICTIONARIES_PATH'] = new_dir
try:
if os.path.exists(old_dir) and not os.path.exists(new_dir):
shutil.copytree(old_dir, new_dir)
except OSError:
log.misc.exception("Failed to copy old dictionaries")

View File

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

View File

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

View File

@ -19,7 +19,7 @@
"""QtWebEngine specific qute://* handlers and glue code."""
from PyQt5.QtCore import QBuffer, QIODevice
from PyQt5.QtCore import QBuffer, QIODevice, QUrl
from PyQt5.QtWebEngineCore import (QWebEngineUrlSchemeHandler,
QWebEngineUrlRequestJob)
@ -37,6 +37,38 @@ class QuteSchemeHandler(QWebEngineUrlSchemeHandler):
if qtutils.version_check('5.11', compiled=False):
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-63378
profile.installUrlSchemeHandler(b'chrome-error', self)
profile.installUrlSchemeHandler(b'chrome-extension', self)
def _check_initiator(self, job):
"""Check whether the initiator of the job should be allowed.
Only the browser itself or qute:// pages should access any of those
URLs. The request interceptor further locks down qute://settings/set.
Args:
job: QWebEngineUrlRequestJob
Return:
True if the initiator is allowed, False if it was blocked.
"""
try:
initiator = job.initiator()
except AttributeError:
# Added in Qt 5.11
return True
if initiator == QUrl('null') and not qtutils.version_check('5.12'):
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-70421
return True
if initiator.isValid() and initiator.scheme() != 'qute':
log.misc.warning("Blocking malicious request from {} to {}".format(
initiator.toDisplayString(),
job.requestUrl().toDisplayString()))
job.fail(QWebEngineUrlRequestJob.RequestDenied)
return False
return True
def requestStarted(self, job):
"""Handle a request for a qute: scheme.
@ -49,28 +81,40 @@ class QuteSchemeHandler(QWebEngineUrlSchemeHandler):
"""
url = job.requestUrl()
if url.scheme() == 'chrome-error':
if url.scheme() in ['chrome-error', 'chrome-extension']:
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-63378
job.fail(QWebEngineUrlRequestJob.UrlInvalid)
return
assert job.requestMethod() == b'GET'
if not self._check_initiator(job):
return
if job.requestMethod() != b'GET':
job.fail(QWebEngineUrlRequestJob.RequestDenied)
return
assert url.scheme() == 'qute'
log.misc.debug("Got request for {}".format(url.toDisplayString()))
try:
mimetype, data = qutescheme.data_for_url(url)
except qutescheme.NoHandlerFound:
log.misc.debug("No handler found for {}".format(
url.toDisplayString()))
job.fail(QWebEngineUrlRequestJob.UrlNotFound)
except qutescheme.QuteSchemeOSError:
# FIXME:qtwebengine how do we show a better error here?
log.misc.exception("OSError while handling qute://* URL")
job.fail(QWebEngineUrlRequestJob.UrlNotFound)
except qutescheme.QuteSchemeError:
# FIXME:qtwebengine how do we show a better error here?
log.misc.exception("Error while handling qute://* URL")
job.fail(QWebEngineUrlRequestJob.RequestFailed)
except qutescheme.Error as e:
errors = {
qutescheme.NotFoundError:
QWebEngineUrlRequestJob.UrlNotFound,
qutescheme.UrlInvalidError:
QWebEngineUrlRequestJob.UrlInvalid,
qutescheme.RequestDeniedError:
QWebEngineUrlRequestJob.RequestDenied,
qutescheme.SchemeOSError:
QWebEngineUrlRequestJob.UrlNotFound,
qutescheme.Error:
QWebEngineUrlRequestJob.RequestFailed,
}
exctype = type(e)
log.misc.error("{} while handling qute://* URL".format(
exctype.__name__))
job.fail(errors[exctype])
except qutescheme.Redirect as e:
qtutils.ensure_valid(e.url)
job.redirect(e.url)

View File

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

View File

@ -21,27 +21,26 @@
import math
import functools
import sys
import re
import html as html_utils
import sip
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, Qt, QEvent, QPoint, QPointF,
QUrl, QTimer, QObject, qVersion)
QUrl, QTimer, QObject)
from PyQt5.QtGui import QKeyEvent, QIcon
from PyQt5.QtNetwork import QAuthenticator
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWebEngineWidgets import QWebEnginePage, QWebEngineScript
from qutebrowser.config import configdata, config, configutils
from qutebrowser.browser import browsertab, mouse, shared
from qutebrowser.browser import browsertab, mouse, shared, webelem
from qutebrowser.browser.webengine import (webview, webengineelem, tabhistory,
interceptor, webenginequtescheme,
cookies, webenginedownloads,
webenginesettings)
webenginesettings, certificateerror)
from qutebrowser.misc import miscwidgets
from qutebrowser.utils import (usertypes, qtutils, log, javascript, utils,
message, objreg, jinja, debug)
from qutebrowser.qt import sip
_qute_scheme_handler = None
@ -163,8 +162,8 @@ class WebEngineSearch(browsertab.AbstractSearch):
back yet.
"""
def __init__(self, parent=None):
super().__init__(parent)
def __init__(self, tab, parent=None):
super().__init__(tab, parent)
self._flags = QWebEnginePage.FindFlags(0)
self._pending_searches = 0
@ -184,6 +183,13 @@ class WebEngineSearch(browsertab.AbstractSearch):
self._pending_searches))
return
if sip.isdeleted(self._widget):
# This happens when starting a search, and closing the tab
# before results arrive.
log.webview.debug("Ignoring finished search for deleted "
"widget")
return
found_text = 'found' if found else "didn't find"
if flags:
flag_text = 'with flags {}'.format(debug.qflags_key(
@ -192,8 +198,11 @@ class WebEngineSearch(browsertab.AbstractSearch):
flag_text = ''
log.webview.debug(' '.join([caller, found_text, text, flag_text])
.strip())
if callback is not None:
callback(found)
self.finished.emit(found)
self._widget.findText(text, flags, wrapped_callback)
def search(self, text, *, ignore_case='never', reverse=False,
@ -214,6 +223,8 @@ class WebEngineSearch(browsertab.AbstractSearch):
self._find(text, self._flags, result_cb, 'search')
def clear(self):
if self.search_displayed:
self.cleared.emit()
self.search_displayed = False
self._widget.findText('')
@ -234,6 +245,15 @@ class WebEngineCaret(browsertab.AbstractCaret):
"""QtWebEngine implementations related to moving the cursor/selection."""
def _flags(self):
"""Get flags to pass to JS."""
flags = set()
if qtutils.version_check('5.7.1', compiled=False):
flags.add('filter-prefix')
if utils.is_windows:
flags.add('windows')
return list(flags)
@pyqtSlot(usertypes.KeyMode)
def _on_mode_entered(self, mode):
if mode != usertypes.KeyMode.caret:
@ -246,9 +266,9 @@ class WebEngineCaret(browsertab.AbstractCaret):
self._tab.search.clear()
self._tab.run_js_async(
javascript.assemble('caret',
'setPlatform', sys.platform, qVersion()))
self._js_call('setInitialCursor', self._selection_cb)
javascript.assemble('caret', 'setFlags', self._flags()))
self._js_call('setInitialCursor', callback=self._selection_cb)
def _selection_cb(self, enabled):
"""Emit selection_toggled based on setInitialCursor."""
@ -266,32 +286,25 @@ class WebEngineCaret(browsertab.AbstractCaret):
self._js_call('disableCaret')
def move_to_next_line(self, count=1):
for _ in range(count):
self._js_call('moveDown')
self._js_call('moveDown', count)
def move_to_prev_line(self, count=1):
for _ in range(count):
self._js_call('moveUp')
self._js_call('moveUp', count)
def move_to_next_char(self, count=1):
for _ in range(count):
self._js_call('moveRight')
self._js_call('moveRight', count)
def move_to_prev_char(self, count=1):
for _ in range(count):
self._js_call('moveLeft')
self._js_call('moveLeft', count)
def move_to_end_of_word(self, count=1):
for _ in range(count):
self._js_call('moveToEndOfWord')
self._js_call('moveToEndOfWord', count)
def move_to_next_word(self, count=1):
for _ in range(count):
self._js_call('moveToNextWord')
self._js_call('moveToNextWord', count)
def move_to_prev_word(self, count=1):
for _ in range(count):
self._js_call('moveToPreviousWord')
self._js_call('moveToPreviousWord', count)
def move_to_start_of_line(self):
self._js_call('moveToStartOfLine')
@ -300,20 +313,16 @@ class WebEngineCaret(browsertab.AbstractCaret):
self._js_call('moveToEndOfLine')
def move_to_start_of_next_block(self, count=1):
for _ in range(count):
self._js_call('moveToStartOfNextBlock')
self._js_call('moveToStartOfNextBlock', count)
def move_to_start_of_prev_block(self, count=1):
for _ in range(count):
self._js_call('moveToStartOfPrevBlock')
self._js_call('moveToStartOfPrevBlock', count)
def move_to_end_of_next_block(self, count=1):
for _ in range(count):
self._js_call('moveToEndOfNextBlock')
self._js_call('moveToEndOfNextBlock', count)
def move_to_end_of_prev_block(self, count=1):
for _ in range(count):
self._js_call('moveToEndOfPrevBlock')
self._js_call('moveToEndOfPrevBlock', count)
def move_to_start_of_document(self):
self._js_call('moveToStartOfDocument')
@ -322,7 +331,7 @@ class WebEngineCaret(browsertab.AbstractCaret):
self._js_call('moveToEndOfDocument')
def toggle_selection(self):
self._js_call('toggleSelection', self.selection_toggled.emit)
self._js_call('toggleSelection', callback=self.selection_toggled.emit)
def drop_selection(self):
self._js_call('dropSelection')
@ -335,7 +344,13 @@ class WebEngineCaret(browsertab.AbstractCaret):
self._tab.run_js_async(javascript.assemble('caret', 'getSelection'),
callback)
def _follow_selected_cb(self, js_elem, tab=False):
def _follow_selected_cb_wrapped(self, js_elem, tab):
try:
self._follow_selected_cb(js_elem, tab)
finally:
self.follow_selected_done.emit()
def _follow_selected_cb(self, js_elem, tab):
"""Callback for javascript which clicks the selected element.
Args:
@ -344,6 +359,7 @@ class WebEngineCaret(browsertab.AbstractCaret):
"""
if js_elem is None:
return
if js_elem == "focused":
# we had a focused element, not a selected one. Just send <enter>
self._follow_enter(tab)
@ -360,7 +376,10 @@ class WebEngineCaret(browsertab.AbstractCaret):
if elem.is_link():
log.webview.debug("Found link in selection, clicking. ClickTarget "
"{}, elem {}".format(click_type, elem))
elem.click(click_type)
try:
elem.click(click_type)
except webelem.Error as e:
message.error(str(e))
def follow_selected(self, *, tab=False):
if self._tab.search.search_displayed:
@ -376,11 +395,13 @@ class WebEngineCaret(browsertab.AbstractCaret):
# click an existing blue selection
js_code = javascript.assemble('webelem',
'find_selected_focused_link')
self._tab.run_js_async(js_code, lambda jsret:
self._follow_selected_cb(jsret, tab))
self._tab.run_js_async(
js_code,
lambda jsret: self._follow_selected_cb_wrapped(jsret, tab))
def _js_call(self, command, callback=None):
self._tab.run_js_async(javascript.assemble('caret', command), callback)
def _js_call(self, command, *args, callback=None):
code = javascript.assemble('caret', command, *args)
self._tab.run_js_async(code, callback)
class WebEngineScroller(browsertab.AbstractScroller):
@ -575,9 +596,12 @@ class WebEngineElements(browsertab.AbstractElements):
if js_elems is None:
callback(None)
return
elif not js_elems['success']:
callback(webelem.Error(js_elems['error']))
return
elems = []
for js_elem in js_elems:
for js_elem in js_elems['result']:
elem = webengineelem.WebEngineElement(js_elem, tab=self._tab)
elems.append(elem)
callback(elems)
@ -617,8 +641,8 @@ class WebEngineElements(browsertab.AbstractElements):
self._tab.run_js_async(js_code, js_cb)
def find_at_pos(self, pos, callback):
assert pos.x() >= 0
assert pos.y() >= 0
assert pos.x() >= 0, pos
assert pos.y() >= 0, pos
pos /= self._tab.zoom.factor()
js_code = javascript.assemble('webelem', 'find_at_pos',
pos.x(), pos.y())
@ -628,14 +652,26 @@ class WebEngineElements(browsertab.AbstractElements):
class WebEngineAudio(browsertab.AbstractAudio):
"""QtWebEngine implemementations related to audio/muting."""
"""QtWebEngine implemementations related to audio/muting.
Attributes:
_overridden: Whether the user toggled muting manually.
If that's the case, we leave it alone.
"""
def __init__(self, tab, parent=None):
super().__init__(tab, parent)
self._overridden = False
def _connect_signals(self):
page = self._widget.page()
page.audioMutedChanged.connect(self.muted_changed)
page.recentlyAudibleChanged.connect(self.recently_audible_changed)
self._tab.url_changed.connect(self._on_url_changed)
config.instance.changed.connect(self._on_config_changed)
def set_muted(self, muted: bool):
def set_muted(self, muted: bool, override: bool = False):
self._overridden = override
page = self._widget.page()
page.setAudioMuted(muted)
@ -647,6 +683,17 @@ class WebEngineAudio(browsertab.AbstractAudio):
page = self._widget.page()
return page.recentlyAudible()
@pyqtSlot(QUrl)
def _on_url_changed(self, url):
if self._overridden:
return
mute = config.instance.get('content.mute', url=url)
self.set_muted(mute)
@config.change_filter('content.mute')
def _on_config_changed(self):
self._on_url_changed(self._tab.url())
class _WebEnginePermissions(QObject):
@ -703,6 +750,18 @@ class _WebEnginePermissions(QObject):
QWebEnginePage.MediaVideoCapture: 'record video',
QWebEnginePage.MediaAudioVideoCapture: 'record audio/video',
}
try:
options.update({
QWebEnginePage.MouseLock:
'content.mouse_lock',
})
messages.update({
QWebEnginePage.MouseLock:
'hide your mouse pointer',
})
except AttributeError:
# Added in Qt 5.8
pass
try:
options.update({
QWebEnginePage.DesktopVideoCapture:
@ -788,12 +847,18 @@ class _WebEngineScripts(QObject):
super().__init__(parent)
self._tab = tab
self._widget = None
self._greasemonkey = objreg.get('greasemonkey')
def connect_signals(self):
"""Connect signals to our private slots."""
config.instance.changed.connect(self._on_config_changed)
self._tab.url_changed.connect(self._update_stylesheet)
self._tab.load_finished.connect(self._on_load_finished)
self._tab.search.cleared.connect(functools.partial(
self._update_stylesheet, searching=False))
self._tab.search.finished.connect(self._on_load_finished)
@pyqtSlot(str)
def _on_config_changed(self, option):
if option in ['scrolling.bar', 'content.user_stylesheets']:
@ -805,14 +870,14 @@ class _WebEngineScripts(QObject):
self._update_stylesheet(url)
@pyqtSlot(QUrl)
def _update_stylesheet(self, url, force=False):
def _update_stylesheet(self, url, searching=False, force=False):
"""Update the custom stylesheet in existing tabs.
Arguments:
url: The url to get the stylesheet for.
force: Also update the global stylesheet.
"""
css = shared.get_user_stylesheet(url=url)
css = shared.get_user_stylesheet(searching=searching, url=url)
if css is configutils.UNSET and force:
css = shared.get_user_stylesheet(url=None)
@ -869,9 +934,16 @@ class _WebEngineScripts(QObject):
self._inject_early_js('js', js_code, subframes=True)
self._init_stylesheet()
greasemonkey = objreg.get('greasemonkey')
greasemonkey.scripts_reloaded.connect(self._inject_userscripts)
self._inject_userscripts()
# The Greasemonkey metadata block support in QtWebEngine only starts at
# Qt 5.8. With 5.7.1, we need to inject the scripts ourselves in
# response to urlChanged.
if not qtutils.version_check('5.8'):
self._tab.url_changed.connect(
self._inject_greasemonkey_scripts_for_url)
else:
self._greasemonkey.scripts_reloaded.connect(
self._inject_all_greasemonkey_scripts)
self._inject_all_greasemonkey_scripts()
def _init_stylesheet(self):
"""Initialize custom stylesheets.
@ -888,40 +960,90 @@ class _WebEngineScripts(QObject):
)
self._inject_early_js('stylesheet', js_code, subframes=True)
def _inject_userscripts(self):
"""Register user JavaScript files with the global profiles."""
# The Greasemonkey metadata block support in QtWebEngine only starts at
# Qt 5.8. With 5.7.1, we need to inject the scripts ourselves in
# response to urlChanged.
if not qtutils.version_check('5.8'):
return
@pyqtSlot(QUrl)
def _inject_greasemonkey_scripts_for_url(self, url):
matching_scripts = self._greasemonkey.scripts_for(url)
self._inject_greasemonkey_scripts(
matching_scripts.start, QWebEngineScript.DocumentCreation, True)
self._inject_greasemonkey_scripts(
matching_scripts.end, QWebEngineScript.DocumentReady, False)
self._inject_greasemonkey_scripts(
matching_scripts.idle, QWebEngineScript.Deferred, False)
# Since we are inserting scripts into profile.scripts they won't
# just get replaced by new gm scripts like if we were injecting them
# ourselves so we need to remove all gm scripts, while not removing
# any other stuff that might have been added. Like the one for
# stylesheets.
greasemonkey = objreg.get('greasemonkey')
scripts = self._widget.page().scripts()
for script in scripts.toList():
@pyqtSlot()
def _inject_all_greasemonkey_scripts(self):
scripts = self._greasemonkey.all_scripts()
self._inject_greasemonkey_scripts(scripts)
def _remove_all_greasemonkey_scripts(self):
page_scripts = self._widget.page().scripts()
for script in page_scripts.toList():
if script.name().startswith("GM-"):
log.greasemonkey.debug('Removing script: {}'
.format(script.name()))
removed = scripts.remove(script)
removed = page_scripts.remove(script)
assert removed, script.name()
# Then add the new scripts.
for script in greasemonkey.all_scripts():
# @run-at (and @include/@exclude/@match) is parsed by
# QWebEngineScript.
def _inject_greasemonkey_scripts(self, scripts=None, injection_point=None,
remove_first=True):
"""Register user JavaScript files with the current tab.
Args:
scripts: A list of GreasemonkeyScripts, or None to add all
known by the Greasemonkey subsystem.
injection_point: The QWebEngineScript::InjectionPoint stage
to inject the script into, None to use
auto-detection.
remove_first: Whether to remove all previously injected
scripts before adding these ones.
"""
if sip.isdeleted(self._widget):
return
# Since we are inserting scripts into a per-tab collection,
# rather than just injecting scripts on page load, we need to
# make sure we replace existing scripts, not just add new ones.
# While, taking care not to remove any other scripts that might
# have been added elsewhere, like the one for stylesheets.
page_scripts = self._widget.page().scripts()
if remove_first:
self._remove_all_greasemonkey_scripts()
if not scripts:
return
for script in scripts:
new_script = QWebEngineScript()
new_script.setWorldId(QWebEngineScript.MainWorld)
try:
world = int(script.jsworld)
if not 0 <= world <= qtutils.MAX_WORLD_ID:
log.greasemonkey.error(
"script {} has invalid value for '@qute-js-world'"
": {}, should be between 0 and {}"
.format(
script.name,
script.jsworld,
qtutils.MAX_WORLD_ID))
continue
except ValueError:
try:
world = _JS_WORLD_MAP[usertypes.JsWorld[
script.jsworld.lower()]]
except KeyError:
log.greasemonkey.error(
"script {} has invalid value for '@qute-js-world'"
": {}".format(script.name, script.jsworld))
continue
new_script.setWorldId(world)
new_script.setSourceCode(script.code())
new_script.setName("GM-{}".format(script.name))
new_script.setRunsOnSubFrames(script.runs_on_sub_frames)
# Override the @run-at value parsed by QWebEngineScript if desired.
if injection_point:
new_script.setInjectionPoint(injection_point)
log.greasemonkey.debug('adding script: {}'
.format(new_script.name()))
scripts.insert(new_script)
page_scripts.insert(new_script)
class WebEngineTab(browsertab.AbstractTab):
@ -941,16 +1063,16 @@ class WebEngineTab(browsertab.AbstractTab):
private=private, parent=parent)
widget = webview.WebEngineView(tabdata=self.data, win_id=win_id,
private=private)
self.history = WebEngineHistory(self)
self.scroller = WebEngineScroller(self, parent=self)
self.history = WebEngineHistory(tab=self)
self.scroller = WebEngineScroller(tab=self, parent=self)
self.caret = WebEngineCaret(mode_manager=mode_manager,
tab=self, parent=self)
self.zoom = WebEngineZoom(tab=self, parent=self)
self.search = WebEngineSearch(parent=self)
self.printing = WebEnginePrinting()
self.search = WebEngineSearch(tab=self, parent=self)
self.printing = WebEnginePrinting(tab=self)
self.elements = WebEngineElements(tab=self)
self.action = WebEngineAction(tab=self)
self.audio = WebEngineAudio(parent=self)
self.audio = WebEngineAudio(tab=self, parent=self)
self._permissions = _WebEnginePermissions(tab=self, parent=self)
self._scripts = _WebEngineScripts(tab=self, parent=self)
# We're assigning settings in _set_widget
@ -975,7 +1097,7 @@ class WebEngineTab(browsertab.AbstractTab):
fp.installEventFilter(self._mouse_event_filter)
self._child_event_filter = mouse.ChildEventFilter(
eventfilter=self._mouse_event_filter, widget=self._widget,
parent=self)
win_id=self.win_id, parent=self)
self._widget.installEventFilter(self._child_event_filter)
@pyqtSlot()
@ -995,6 +1117,9 @@ class WebEngineTab(browsertab.AbstractTab):
url: The QUrl to open.
predict: If set to False, predicted_navigation is not emitted.
"""
if sip.isdeleted(self._widget):
# https://github.com/qutebrowser/qutebrowser/issues/3896
return
self._saved_zoom = self.zoom.factor()
self._openurl_prepare(url, predict=predict)
self._widget.load(url)
@ -1017,6 +1142,10 @@ class WebEngineTab(browsertab.AbstractTab):
world_id = QWebEngineScript.ApplicationWorld
elif isinstance(world, int):
world_id = world
if not 0 <= world_id <= qtutils.MAX_WORLD_ID:
raise browsertab.WebTabError(
"World ID should be between 0 and {}"
.format(qtutils.MAX_WORLD_ID))
else:
world_id = _JS_WORLD_MAP[world]
@ -1153,11 +1282,10 @@ class WebEngineTab(browsertab.AbstractTab):
@pyqtSlot()
def _on_load_started(self):
"""Clear search when a new load is started if needed."""
if (qtutils.version_check('5.9', compiled=False) and
not qtutils.version_check('5.9.2', compiled=False)):
# WORKAROUND for
# https://bugreports.qt.io/browse/QTBUG-61506
self.search.clear()
# WORKAROUND for
# https://bugreports.qt.io/browse/QTBUG-61506
# (seems to be back in later Qt versions as well)
self.search.clear()
super()._on_load_started()
self.data.netrc_used = False
@ -1241,6 +1369,34 @@ class WebEngineTab(browsertab.AbstractTab):
# the old icon is still displayed.
self.icon_changed.emit(QIcon())
@pyqtSlot(certificateerror.CertificateErrorWrapper)
def _on_ssl_errors(self, error):
self._has_ssl_errors = True
url = error.url()
log.webview.debug("Certificate error: {}".format(error))
if error.is_overridable():
error.ignore = shared.ignore_certificate_errors(
url, [error], abort_on=[self.shutting_down, self.load_started])
else:
log.webview.error("Non-overridable certificate error: "
"{}".format(error))
log.webview.debug("ignore {}, URL {}, requested {}".format(
error.ignore, url, self.url(requested=True)))
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-56207
# We can't really know when to show an error page, as the error might
# have happened when loading some resource.
# However, self.url() is not available yet and the requested URL
# might not match the URL we get from the error - so we just apply a
# heuristic here.
if (not qtutils.version_check('5.9') and
not error.ignore and
url.matches(self.url(requested=True), QUrl.RemoveScheme)):
self._show_error_page(url, str(error))
@pyqtSlot(QUrl)
def _on_predicted_navigation(self, url):
"""If we know we're going to visit an URL soon, change the settings.
@ -1256,10 +1412,10 @@ class WebEngineTab(browsertab.AbstractTab):
super()._on_navigation_request(navigation)
if navigation.url == QUrl('qute://print'):
command_dispatcher = objreg.get('command-dispatcher',
scope='window',
window=self.win_id)
command_dispatcher.printpage()
try:
self.printing.show_dialog()
except browsertab.WebTabError as e:
message.error(str(e))
navigation.accepted = False
if not navigation.accepted or not navigation.is_main_frame:

View File

@ -19,18 +19,17 @@
"""The main browser widget for QtWebEngine."""
import sip
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QUrl, PYQT_VERSION
from PyQt5.QtCore import pyqtSignal, QUrl, PYQT_VERSION
from PyQt5.QtGui import QPalette
from PyQt5.QtQuickWidgets import QQuickWidget
from PyQt5.QtWebEngineWidgets import (QWebEngineView, QWebEnginePage,
QWebEngineScript)
from PyQt5.QtWidgets import QWidget
from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage
from qutebrowser.browser import shared
from qutebrowser.browser.webengine import certificateerror, webenginesettings
from qutebrowser.browser.webengine import webenginesettings, certificateerror
from qutebrowser.config import config
from qutebrowser.utils import log, debug, usertypes, jinja, objreg, qtutils
from qutebrowser.utils import log, debug, usertypes, objreg, qtutils
from qutebrowser.misc import miscwidgets
from qutebrowser.qt import sip
class WebEngineView(QWebEngineView):
@ -71,10 +70,10 @@ class WebEngineView(QWebEngineView):
if proxy is not None:
return proxy
# This should only find the RenderWidgetHostViewQtDelegateWidget,
# but not e.g. a QMenu
children = [c for c in self.findChildren(QQuickWidget)
if c.isVisible()]
# We don't want e.g. a QMenu.
rwhv_class = 'QtWebEngineCore::RenderWidgetHostViewQtDelegateWidget'
children = [c for c in self.findChildren(QWidget)
if c.isVisible() and c.inherits(rwhv_class)]
log.webview.debug("Found possibly lost focusProxy: {}"
.format(children))
@ -152,11 +151,13 @@ class WebEnginePage(QWebEnginePage):
Signals:
certificate_error: Emitted on certificate errors.
Needs to be directly connected to a slot setting the
'ignore' attribute.
shutting_down: Emitted when the page is shutting down.
navigation_request: Emitted on acceptNavigationRequest.
"""
certificate_error = pyqtSignal()
certificate_error = pyqtSignal(certificateerror.CertificateErrorWrapper)
shutting_down = pyqtSignal()
navigation_request = pyqtSignal(usertypes.NavigationRequest)
@ -166,7 +167,6 @@ class WebEnginePage(QWebEnginePage):
self._theme_color = theme_color
self._set_bg_color()
config.instance.changed.connect(self._set_bg_color)
self.urlChanged.connect(self._inject_userjs)
@config.change_filter('colors.webpage.bg')
def _set_bg_color(self):
@ -181,36 +181,9 @@ class WebEnginePage(QWebEnginePage):
def certificateError(self, error):
"""Handle certificate errors coming from Qt."""
self.certificate_error.emit()
url = error.url()
error = certificateerror.CertificateErrorWrapper(error)
log.webview.debug("Certificate error: {}".format(error))
url_string = url.toDisplayString()
error_page = jinja.render(
'error.html', title="Error loading page: {}".format(url_string),
url=url_string, error=str(error))
if error.is_overridable():
ignore = shared.ignore_certificate_errors(
url, [error], abort_on=[self.loadStarted, self.shutting_down])
else:
log.webview.error("Non-overridable certificate error: "
"{}".format(error))
ignore = False
# We can't really know when to show an error page, as the error might
# have happened when loading some resource.
# However, self.url() is not available yet and self.requestedUrl()
# might not match the URL we get from the error - so we just apply a
# heuristic here.
# See https://bugreports.qt.io/browse/QTBUG-56207
log.webview.debug("ignore {}, URL {}, requested {}".format(
ignore, url, self.requestedUrl()))
if not ignore and url.matches(self.requestedUrl(), QUrl.RemoveScheme):
self.setHtml(error_page)
return ignore
self.certificate_error.emit(error)
return error.ignore
def javaScriptConfirm(self, url, js_msg):
"""Override javaScriptConfirm to use qutebrowser prompts."""
@ -288,43 +261,3 @@ class WebEnginePage(QWebEnginePage):
is_main_frame=is_main_frame)
self.navigation_request.emit(navigation)
return navigation.accepted
@pyqtSlot('QUrl')
def _inject_userjs(self, url):
"""Inject userscripts registered for `url` into the current page."""
if qtutils.version_check('5.8'):
# Handled in webenginetab with the builtin Greasemonkey
# support.
return
# Using QWebEnginePage.scripts() to hold the user scripts means
# we don't have to worry ourselves about where to inject the
# page but also means scripts hang around for the tab lifecycle.
# So clear them here.
scripts = self.scripts()
for script in scripts.toList():
if script.name().startswith("GM-"):
log.greasemonkey.debug("Removing script: {}"
.format(script.name()))
removed = scripts.remove(script)
assert removed, script.name()
def _add_script(script, injection_point):
new_script = QWebEngineScript()
new_script.setInjectionPoint(injection_point)
new_script.setWorldId(QWebEngineScript.MainWorld)
new_script.setSourceCode(script.code())
new_script.setName("GM-{}".format(script.name))
new_script.setRunsOnSubFrames(script.runs_on_sub_frames)
log.greasemonkey.debug("Adding script: {}"
.format(new_script.name()))
scripts.insert(new_script)
greasemonkey = objreg.get('greasemonkey')
matching_scripts = greasemonkey.scripts_for(url)
for script in matching_scripts.start:
_add_script(script, QWebEngineScript.DocumentCreation)
for script in matching_scripts.end:
_add_script(script, QWebEngineScript.DocumentReady)
for script in matching_scripts.idle:
_add_script(script, QWebEngineScript.Deferred)

View File

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

View File

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

View File

@ -373,24 +373,9 @@ class NetworkManager(QNetworkAccessManager):
req, proxy_error, QNetworkReply.UnknownProxyError,
self)
scheme = req.url().scheme()
if scheme in self._scheme_handlers:
result = self._scheme_handlers[scheme](req)
if result is not None:
result.setParent(self)
return result
for header, value in shared.custom_headers():
for header, value in shared.custom_headers(url=req.url()):
req.setRawHeader(header, value)
host_blocker = objreg.get('host-blocker')
if host_blocker.is_blocked(req.url()):
log.webview.info("Request to {} blocked by host blocker.".format(
req.url().host()))
return networkreply.ErrorNetworkReply(
req, HOSTBLOCK_ERROR_STRING, QNetworkReply.ContentAccessDenied,
self)
# There are some scenarios where we can't figure out current_url:
# - There's a generic NetworkManager, e.g. for downloads
# - The download was in a tab which is now closed.
@ -408,6 +393,14 @@ class NetworkManager(QNetworkAccessManager):
# the webpage shutdown here.
current_url = QUrl()
host_blocker = objreg.get('host-blocker')
if host_blocker.is_blocked(req.url(), current_url):
log.webview.info("Request to {} blocked by host blocker.".format(
req.url().host()))
return networkreply.ErrorNetworkReply(
req, HOSTBLOCK_ERROR_STRING, QNetworkReply.ContentAccessDenied,
self)
if 'log-requests' in self._args.debug_flags:
operation = debug.qenum_key(QNetworkAccessManager, op)
operation = operation.replace('Operation', '').upper()
@ -416,5 +409,12 @@ class NetworkManager(QNetworkAccessManager):
req.url().toDisplayString(),
current_url.toDisplayString()))
scheme = req.url().scheme()
if scheme in self._scheme_handlers:
result = self._scheme_handlers[scheme](req, op, current_url)
if result is not None:
result.setParent(self)
return result
self.set_referer(req, current_url)
return super().createRequest(op, req, outgoing_data)

View File

@ -19,58 +19,63 @@
"""QtWebKit specific qute://* handlers and glue code."""
import mimetypes
from PyQt5.QtCore import QUrl
from PyQt5.QtNetwork import QNetworkReply, QNetworkAccessManager
from PyQt5.QtNetwork import QNetworkReply
from qutebrowser.browser import pdfjs, qutescheme
from qutebrowser.browser import qutescheme
from qutebrowser.browser.webkit.network import networkreply
from qutebrowser.utils import log, usertypes, qtutils
from qutebrowser.utils import log, qtutils
def handler(request):
def handler(request, operation, current_url):
"""Scheme handler for qute:// URLs.
Args:
request: QNetworkRequest to answer to.
operation: The HTTP operation being done.
current_url: The page we're on currently.
Return:
A QNetworkReply.
"""
if operation != QNetworkAccessManager.GetOperation:
return networkreply.ErrorNetworkReply(
request, "Unsupported request type",
QNetworkReply.ContentOperationNotPermittedError)
url = request.url()
if ((url.scheme(), url.host(), url.path()) ==
('qute', 'settings', '/set')):
if current_url != QUrl('qute://settings/'):
log.webview.warning("Blocking malicious request from {} to {}"
.format(current_url.toDisplayString(),
url.toDisplayString()))
return networkreply.ErrorNetworkReply(
request, "Invalid qute://settings request",
QNetworkReply.ContentAccessDenied)
try:
mimetype, data = qutescheme.data_for_url(request.url())
except qutescheme.NoHandlerFound:
errorstr = "No handler found for {}!".format(
request.url().toDisplayString())
return networkreply.ErrorNetworkReply(
request, errorstr, QNetworkReply.ContentNotFoundError)
except qutescheme.QuteSchemeOSError as e:
return networkreply.ErrorNetworkReply(
request, str(e), QNetworkReply.ContentNotFoundError)
except qutescheme.QuteSchemeError as e:
return networkreply.ErrorNetworkReply(request, e.errorstring, e.error)
mimetype, data = qutescheme.data_for_url(url)
except qutescheme.Error as e:
errors = {
qutescheme.NotFoundError:
QNetworkReply.ContentNotFoundError,
qutescheme.UrlInvalidError:
QNetworkReply.ContentOperationNotPermittedError,
qutescheme.RequestDeniedError:
QNetworkReply.ContentAccessDenied,
qutescheme.SchemeOSError:
QNetworkReply.ContentNotFoundError,
qutescheme.Error:
QNetworkReply.InternalServerError,
}
exctype = type(e)
log.misc.error("{} while handling qute://* URL".format(
exctype.__name__))
return networkreply.ErrorNetworkReply(request, str(e), errors[exctype])
except qutescheme.Redirect as e:
qtutils.ensure_valid(e.url)
return networkreply.RedirectNetworkReply(e.url)
return networkreply.FixedDataNetworkReply(request, data, mimetype)
@qutescheme.add_handler('pdfjs', backend=usertypes.Backend.QtWebKit)
def qute_pdfjs(url):
"""Handler for qute://pdfjs. Return the pdf.js viewer."""
try:
data = pdfjs.get_pdfjs_res(url.path())
except pdfjs.PDFJSNotFound as e:
# Logging as the error might get lost otherwise since we're not showing
# the error page if a single asset is missing. This way we don't lose
# information, as the failed pdfjs requests are still in the log.
log.misc.warning(
"pdfjs resource requested but not found: {}".format(e.path))
raise qutescheme.QuteSchemeError("Can't find pdfjs resource "
"'{}'".format(e.path),
QNetworkReply.ContentNotFoundError)
else:
mimetype, _encoding = mimetypes.guess_type(url.fileName())
assert mimetype is not None, url
return mimetype, data

View File

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

View File

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

View File

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

View File

@ -19,7 +19,7 @@
"""The main browser widgets."""
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QUrl
from PyQt5.QtCore import pyqtSignal, Qt, QUrl
from PyQt5.QtGui import QPalette
from PyQt5.QtWidgets import QStyleFactory
from PyQt5.QtWebKit import QWebSettings
@ -78,10 +78,6 @@ class WebView(QWebView):
self.setPage(page)
mode_manager = objreg.get('mode-manager', scope='window',
window=win_id)
mode_manager.entered.connect(self.on_mode_entered)
mode_manager.left.connect(self.on_mode_left)
config.instance.changed.connect(self._set_bg_color)
def __repr__(self):
@ -130,32 +126,6 @@ class WebView(QWebView):
"""
self.load(url)
@pyqtSlot(usertypes.KeyMode)
def on_mode_entered(self, mode):
"""Ignore attempts to focus the widget if in any status-input mode.
FIXME:qtwebengine
For QtWebEngine, doing the same has no effect, so we do it in here.
"""
if mode in [usertypes.KeyMode.command, usertypes.KeyMode.prompt,
usertypes.KeyMode.yesno]:
log.webview.debug("Ignoring focus because mode {} was "
"entered.".format(mode))
self.setFocusPolicy(Qt.NoFocus)
@pyqtSlot(usertypes.KeyMode)
def on_mode_left(self, mode):
"""Restore focus policy if status-input modes were left.
FIXME:qtwebengine
For QtWebEngine, doing the same has no effect, so we do it in here.
"""
if mode in [usertypes.KeyMode.command, usertypes.KeyMode.prompt,
usertypes.KeyMode.yesno]:
log.webview.debug("Restoring focus policy because mode {} was "
"left.".format(mode))
self.setFocusPolicy(Qt.WheelFocus)
def createWindow(self, wintype):
"""Called by Qt when a page wants to create a new window.

View File

@ -432,7 +432,7 @@ def run_async(tab, cmd, *args, win_id, env, verbose=False):
cmd_path = os.path.expanduser(cmd)
# if cmd is not given as an absolute path, look it up
# ~/.local/share/qutebrowser/userscripts (or $XDG_DATA_DIR)
# ~/.local/share/qutebrowser/userscripts (or $XDG_DATA_HOME)
if not os.path.isabs(cmd_path):
log.misc.debug("{} is no absolute path".format(cmd_path))
cmd_path = _lookup_path(cmd)

View File

@ -28,13 +28,27 @@ import html
from PyQt5.QtWidgets import QStyle, QStyleOptionViewItem, QStyledItemDelegate
from PyQt5.QtCore import QRectF, QSize, Qt
from PyQt5.QtGui import (QIcon, QPalette, QTextDocument, QTextOption,
QAbstractTextDocumentLayout)
QAbstractTextDocumentLayout, QSyntaxHighlighter,
QTextCharFormat)
from qutebrowser.config import config
from qutebrowser.utils import qtutils, jinja
from qutebrowser.utils import qtutils
_cached_stylesheet = None
class _Highlighter(QSyntaxHighlighter):
def __init__(self, doc, pattern, color):
super().__init__(doc)
self._format = QTextCharFormat()
self._format.setForeground(color)
self._pattern = pattern
def highlightBlock(self, text):
"""Override highlightBlock for custom highlighting."""
for match in re.finditer(self._pattern, text, re.IGNORECASE):
start, end = match.span()
length = end - start
self.setFormat(start, length, self._format)
class CompletionItemDelegate(QStyledItemDelegate):
@ -194,21 +208,15 @@ class CompletionItemDelegate(QStyledItemDelegate):
self._doc.setDefaultTextOption(text_option)
self._doc.setDocumentMargin(2)
assert _cached_stylesheet is not None
self._doc.setDefaultStyleSheet(_cached_stylesheet)
if index.parent().isValid():
view = self.parent()
pattern = view.pattern
columns_to_filter = index.model().columns_to_filter(index)
self._doc.setPlainText(self._opt.text)
if index.column() in columns_to_filter and pattern:
repl = r'<span class="highlight">\g<0></span>'
pat = html.escape(re.escape(pattern)).replace(r'\ ', r'|')
txt = html.escape(self._opt.text)
text = re.sub(pat, repl, txt, flags=re.IGNORECASE)
self._doc.setHtml(text)
else:
self._doc.setPlainText(self._opt.text)
pat = re.escape(pattern).replace(r'\ ', r'|')
_Highlighter(self._doc, pat,
config.val.colors.completion.match.fg)
else:
self._doc.setHtml(
'<span style="font: {};">{}</span>'.format(
@ -283,24 +291,3 @@ class CompletionItemDelegate(QStyledItemDelegate):
self._draw_focus_rect()
self._painter.restore()
@config.change_filter('colors.completion.match.fg', function=True)
def _update_stylesheet():
"""Update the cached stylesheet."""
stylesheet = """
.highlight {
color: {{ conf.colors.completion.match.fg }};
}
"""
with jinja.environment.no_autoescape():
template = jinja.environment.from_string(stylesheet)
global _cached_stylesheet
_cached_stylesheet = template.render(conf=config.val)
def init():
"""Initialize the cached stylesheet."""
_update_stylesheet()
config.instance.changed.connect(_update_stylesheet)

View File

@ -27,12 +27,7 @@ from qutebrowser.keyinput import keyutils
def option(*, info):
"""A CompletionModel filled with settings and their descriptions."""
model = completionmodel.CompletionModel(column_widths=(20, 70, 10))
options = ((opt.name, opt.description, info.config.get_str(opt.name))
for opt in configdata.DATA.values()
if not opt.no_autoconfig)
model.add_category(listcategory.ListCategory("Options", options))
return model
return _option(info, "Options", lambda opt: not opt.no_autoconfig)
def customized_option(*, info):
@ -47,6 +42,37 @@ def customized_option(*, info):
return model
def list_option(*, info):
"""A CompletionModel filled with settings whose values are lists."""
predicate = lambda opt: (isinstance(info.config.get_obj(opt.name),
list) and not opt.no_autoconfig)
return _option(info, "List options", predicate)
def dict_option(*, info):
"""A CompletionModel filled with settings whose values are dicts."""
predicate = lambda opt: (isinstance(info.config.get_obj(opt.name),
dict) and not opt.no_autoconfig)
return _option(info, "Dict options", predicate)
def _option(info, title, predicate):
"""A CompletionModel that is generated for several option sets.
Args:
info: The config info that can be passed through.
title: The title of the options.
predicate: The function for filtering out the options. Takes a single
argument.
"""
model = completionmodel.CompletionModel(column_widths=(20, 70, 10))
options = ((opt.name, opt.description, info.config.get_str(opt.name))
for opt in configdata.DATA.values()
if predicate(opt))
model.add_category(listcategory.ListCategory(title, options))
return model
def value(optname, *values, info):
"""A CompletionModel filled with setting values.

View File

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

View File

@ -24,7 +24,7 @@ import re
from PyQt5.QtCore import Qt, QSortFilterProxyModel, QRegExp
from PyQt5.QtGui import QStandardItem, QStandardItemModel
from qutebrowser.utils import qtutils
from qutebrowser.utils import qtutils, log
class ListCategory(QSortFilterProxyModel):
@ -80,6 +80,13 @@ class ListCategory(QSortFilterProxyModel):
left = self.srcmodel.data(lindex)
right = self.srcmodel.data(rindex)
if left is None or right is None: # pragma: no cover
log.completion.warning("Got unexpected None value, "
"left={!r} right={!r} "
"lindex={!r} rindex={!r}"
.format(left, right, lindex, rindex))
return False
leftstart = left.startswith(self._pattern)
rightstart = right.startswith(self._pattern)

View File

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

View File

@ -22,6 +22,7 @@
from qutebrowser.completion.models import (completionmodel, listcategory,
histcategory)
from qutebrowser.utils import log, objreg
from qutebrowser.config import config
_URLCOL = 0
@ -50,25 +51,48 @@ def _delete_quickmark(data):
def url(*, info):
"""A model which combines bookmarks, quickmarks and web history URLs.
"""A model which combines various URLs.
This combines:
- bookmarks
- quickmarks
- search engines
- web history URLs
Used for the `open` command.
"""
model = completionmodel.CompletionModel(column_widths=(40, 50, 10))
# pylint: disable=bad-config-option
quickmarks = [(url, name) for (name, url)
in objreg.get('quickmark-manager').marks.items()]
bookmarks = objreg.get('bookmark-manager').marks.items()
searchengines = [(k, v) for k, v
in sorted(config.val.url.searchengines.items())
if k != 'DEFAULT']
# pylint: enable=bad-config-option
categories = config.val.completion.open_categories
models = {}
if quickmarks:
model.add_category(listcategory.ListCategory(
if searchengines and 'searchengines' in categories:
models['searchengines'] = listcategory.ListCategory(
'Search engines', searchengines, sort=False)
if quickmarks and 'quickmarks' in categories:
models['quickmarks'] = listcategory.ListCategory(
'Quickmarks', quickmarks, delete_func=_delete_quickmark,
sort=False))
if bookmarks:
model.add_category(listcategory.ListCategory(
'Bookmarks', bookmarks, delete_func=_delete_bookmark, sort=False))
sort=False)
if bookmarks and 'bookmarks' in categories:
models['bookmarks'] = listcategory.ListCategory(
'Bookmarks', bookmarks, delete_func=_delete_bookmark, sort=False)
if info.config.get('completion.web_history_max_items') != 0:
history_disabled = info.config.get('completion.web_history.max_items') == 0
if not history_disabled and 'history' in categories:
hist_cat = histcategory.HistoryCategory(delete_func=_delete_history)
model.add_category(hist_cat)
models['history'] = hist_cat
for category in categories:
if category in models:
model.add_category(models[category])
return model

View File

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

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):
"""Set an option.
If the option name ends with '?', the value of the option is shown
instead.
If the option name ends with '?' or no value is provided, the
value of the option is shown instead.
Using :set without any arguments opens a page where settings can be
changed interactively.
@ -116,8 +116,7 @@ class ConfigCommands:
with self._handle_config_error():
if value is None:
raise cmdexc.CommandError("set: The following arguments "
"are required: value")
self._print_value(option, pattern=pattern)
else:
self._config.set_str(option, value, pattern=pattern,
save_yaml=not temp)
@ -246,11 +245,114 @@ class ConfigCommands:
Args:
option: The name of the option.
temp: Don't touch autoconfig.yml.
temp: Set value temporarily until qutebrowser is closed.
"""
with self._handle_config_error():
self._config.unset(option, save_yaml=not temp)
@cmdutils.register(instance='config-commands')
@cmdutils.argument('option', completion=configmodel.list_option)
def config_list_add(self, option, value, temp=False):
"""Append a value to a config option that is a list.
Args:
option: The name of the option.
value: The value to append to the end of the list.
temp: Add value temporarily until qutebrowser is closed.
"""
opt = self._config.get_opt(option)
valid_list_types = (configtypes.List, configtypes.ListOrValue)
if not isinstance(opt.typ, valid_list_types):
raise cmdexc.CommandError(":config-list-add can only be used for "
"lists")
with self._handle_config_error():
option_value = self._config.get_mutable_obj(option)
option_value.append(value)
self._config.update_mutables(save_yaml=not temp)
@cmdutils.register(instance='config-commands')
@cmdutils.argument('option', completion=configmodel.dict_option)
def config_dict_add(self, option, key, value, temp=False, replace=False):
"""Add a key/value pair to a dictionary option.
Args:
option: The name of the option.
key: The key to use.
value: The value to place in the dictionary.
temp: Add value temporarily until qutebrowser is closed.
replace: Replace existing values. By default, existing values are
not overwritten.
"""
opt = self._config.get_opt(option)
if not isinstance(opt.typ, configtypes.Dict):
raise cmdexc.CommandError(":config-dict-add can only be used for "
"dicts")
with self._handle_config_error():
option_value = self._config.get_mutable_obj(option)
if key in option_value and not replace:
raise cmdexc.CommandError("{} already exists in {} - use "
"--replace to overwrite!"
.format(key, option))
option_value[key] = value
self._config.update_mutables(save_yaml=not temp)
@cmdutils.register(instance='config-commands')
@cmdutils.argument('option', completion=configmodel.list_option)
def config_list_remove(self, option, value, temp=False):
"""Remove a value from a list.
Args:
option: The name of the option.
value: The value to remove from the list.
temp: Remove value temporarily until qutebrowser is closed.
"""
opt = self._config.get_opt(option)
valid_list_types = (configtypes.List, configtypes.ListOrValue)
if not isinstance(opt.typ, valid_list_types):
raise cmdexc.CommandError(":config-list-remove can only be used "
"for lists")
with self._handle_config_error():
option_value = self._config.get_mutable_obj(option)
if value not in option_value:
raise cmdexc.CommandError("{} is not in {}!".format(value,
option))
option_value.remove(value)
self._config.update_mutables(save_yaml=not temp)
@cmdutils.register(instance='config-commands')
@cmdutils.argument('option', completion=configmodel.dict_option)
def config_dict_remove(self, option, key, temp=False):
"""Remove a key from a dict.
Args:
option: The name of the option.
key: The key to remove from the dict.
temp: Remove value temporarily until qutebrowser is closed.
"""
opt = self._config.get_opt(option)
if not isinstance(opt.typ, configtypes.Dict):
raise cmdexc.CommandError(":config-dict-remove can only be used "
"for dicts")
with self._handle_config_error():
option_value = self._config.get_mutable_obj(option)
if key not in option_value:
raise cmdexc.CommandError("{} is not in {}!".format(key,
option))
del option_value[key]
self._config.update_mutables(save_yaml=not temp)
@cmdutils.register(instance='config-commands')
def config_clear(self, save=False):
"""Set all settings back to their default.

View File

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

View File

@ -3,8 +3,10 @@
aliases:
default:
w: session-save
q: quit
q: close
qa: quit
wq: quit --save
wqa: quit --save
type:
name: Dict
keytype:
@ -181,6 +183,51 @@ qt.force_platform:
This sets the `QT_QPA_PLATFORM` environment variable and is useful to force
using the XCB plugin when running QtWebEngine on Wayland.
qt.process_model:
type:
name: String
valid_values:
- process-per-site-instance: Pages from separate sites are put into
separate processes and separate visits to the same site are also
isolated.
- process-per-site: Pages from separate sites are put into separate
processes. Unlike Process per Site Instance, all visits to the same
site will share an OS process. The benefit of this model is reduced
memory consumption, because more web pages will share processes.
The drawbacks include reduced security, robustness, and
responsiveness.
- single-process: Run all tabs in a single process. This should be used
for debugging purposes only, and it disables `:open --private`.
default: process-per-site-instance
backend: QtWebEngine
restart: true
desc: >-
Which Chromium process model to use.
Alternative process models use less resources, but decrease security and
robustness.
See the following pages for more details:
- https://www.chromium.org/developers/design-documents/process-models
- https://doc.qt.io/qt-5/qtwebengine-features.html#process-models
qt.low_end_device_mode:
type:
name: String
valid_values:
- always: Always use low-end device mode.
- auto: Decide automatically (uses low-end mode with < 1 GB available
RAM).
- never: Never use low-end device mode.
default: auto
backend: QtWebEngine
restart: true
desc: >-
When to use Chromium's low-end device mode.
This improves the RAM usage of renderer processes, at the expense of
performance.
qt.highdpi:
type: Bool
@ -220,10 +267,12 @@ content.autoplay:
backend:
QtWebEngine: Qt 5.10
QtWebKit: false
supports_pattern: true
desc: >-
Automatically start playing `<video>` elements.
Note this option needs a restart with QtWebEngine on Qt < 5.11.
Note: On Qt < 5.11, this option needs a restart and does not support URL
patterns.
content.cache.size:
default: null
@ -321,6 +370,7 @@ content.windowed_fullscreen:
content.desktop_capture:
type: BoolAsk
default: ask
supports_pattern: true
desc: >-
Allow websites to share screen content.
@ -350,14 +400,28 @@ content.frame_flattening:
content.geolocation:
default: ask
type: BoolAsk
supports_pattern: true
desc: Allow websites to request geolocations.
content.mouse_lock:
default: ask
type: BoolAsk
supports_pattern: true
backend:
QtWebKit: false
QtWebEngine: Qt 5.8
desc: Allow websites to lock your mouse pointer.
content.headers.accept_language:
type:
name: String
none_ok: true
supports_pattern: true
default: en-US,en
desc: Value to send in the `Accept-Language` header.
desc: >-
Value to send in the `Accept-Language` header.
Note that the value read from JavaScript is always the global value.
content.headers.custom:
default: {}
@ -370,6 +434,7 @@ content.headers.custom:
name: String
encoding: ascii
none_ok: true
supports_pattern: true
desc: Custom headers for qutebrowser HTTP requests.
content.headers.do_not_track:
@ -377,6 +442,7 @@ content.headers.do_not_track:
name: Bool
none_ok: true
default: true
supports_pattern: true
desc: >-
Value to send in the `DNT` header.
@ -392,14 +458,18 @@ content.headers.referer:
- never: "Never send the Referer. This is not recommended, as some sites
may break."
- same-domain: "Only send the Referer for the same domain. This will
still protect your privacy, but shouldn't break any sites."
backend: QtWebKit
still protect your privacy, but shouldn't break any sites. With
QtWebEngine, the referer will still be sent for other domains, but
with stripped path information."
restart: true
desc: >-
When to send the Referer header.
The Referer header tells websites from which website you were coming from
when visiting them.
No restart is needed with QtWebKit.
content.headers.user_agent:
default: null
type:
@ -451,10 +521,15 @@ content.headers.user_agent:
Gecko"
- IE 11.0 for Desktop Win7 64-bit
desc: User agent to send. Unset to send the default.
supports_pattern: true
desc: >-
User agent to send. Unset to send the default.
Note that the value read from JavaScript is always the global value.
content.host_blocking.enabled:
default: true
supports_pattern: true
type: Bool
desc: Enable host blocking.
@ -475,18 +550,26 @@ content.host_blocking.lists:
- A zip-file of any of the above, with either only one file, or a file
named `hosts` (with any extension).
It's also possible to add a local file or directory via a `file://` URL. In
case of a directory, all files in the directory are read as adblock lists.
The file `~/.config/qutebrowser/blocked-hosts` is always read if it exists.
content.host_blocking.whitelist:
default:
- piwik.org
type:
name: List
valtype: String
valtype: UrlPattern
none_ok: true
desc: >-
List of domains that should always be loaded, despite being ad-blocked.
A list of patterns that should always be loaded, despite being ad-blocked.
Domains may contain * and ? wildcards and are otherwise required to exactly
match the requested domain.
Note this whitelists blocked hosts, not first-party URLs. As an example, if
`example.org` loads an ad from `ads.example.org`, the whitelisted host
should be `ads.example.org`. If you want to disable the adblocker on a
given page, use the `content.host_blocking.enabled` setting with a URL
pattern instead.
Local domains are always exempt from hostblocking.
@ -594,6 +677,7 @@ content.local_storage:
content.media_capture:
default: ask
type: BoolAsk
supports_pattern: true
backend: QtWebEngine
desc: Allow websites to record audio/video.
@ -610,13 +694,13 @@ content.netrc_file:
content.notifications:
default: ask
type: BoolAsk
supports_pattern: true
backend: QtWebKit
desc: Allow websites to show notifications.
content.pdfjs:
default: false
type: Bool
backend: QtWebKit
desc: >-
Allow pdf.js to view PDF files in the browser.
@ -626,6 +710,7 @@ content.pdfjs:
content.persistent_storage:
default: ask
type: BoolAsk
supports_pattern: true
backend:
QtWebKit: false
QtWebEngine: Qt 5.11
@ -672,6 +757,7 @@ content.proxy_dns_requests:
content.register_protocol_handler:
default: ask
type: BoolAsk
supports_pattern: true
backend:
QtWebKit: false
QtWebEngine: Qt 5.11
@ -681,6 +767,7 @@ content.register_protocol_handler:
content.ssl_strict:
default: ask
type: BoolAsk
supports_pattern: true
desc: Validate SSL handshakes.
content.user_stylesheets:
@ -698,29 +785,52 @@ content.webgl:
supports_pattern: true
desc: Enable WebGL.
content.webrtc_public_interfaces_only:
default: false
type: Bool
content.webrtc_ip_handling_policy:
default: all-interfaces
type:
name: String
valid_values:
- all-interfaces: WebRTC has the right to enumerate all interfaces and
bind them to discover public interfaces.
- default-public-and-private-interfaces: WebRTC should only use the
default route used by http. This also exposes the associated
default private address. Default route is the route chosen by the
OS on a multi-homed endpoint.
- default-public-interface-only: WebRTC should only use the default route
used by http. This doesn't expose any local addresses.
- disable-non-proxied-udp: WebRTC should only use TCP to contact peers or
servers unless the proxy server supports UDP. This doesn't expose
any local addresses either.
default: all-interfaces
backend:
QtWebKit: false
QtWebEngine: Qt 5.9.2
restart: true
desc: >-
Only expose public interfaces via WebRTC.
Which interfaces to expose via WebRTC.
On Qt 5.9, this option requires a restart.
On Qt 5.10, this option doesn't work at all because of a Qt bug.
On Qt >= 5.11, no restart is required.
On Qt 5.10, this option doesn't work because of a Qt bug.
content.xss_auditing:
type: Bool
default: false
default: true
supports_pattern: true
desc: >-
Monitor load requests for cross-site scripting attempts.
Suspicious scripts will be blocked and reported in the inspector's
JavaScript console. Enabling this feature might have an impact on
performance.
JavaScript console.
content.mute:
default: false
type: Bool
supports_pattern: true
desc: >-
Automatically mute tabs.
Note that if the `:tab-mute` command is used, the mute status for the
affected tab is now controlled manually, and this setting doesn't have any
effect.
# emacs: '
@ -788,7 +898,27 @@ completion.timestamp_format:
default: '%Y-%m-%d'
desc: Format of timestamps (e.g. for the history completion).
completion.web_history.exclude:
type:
name: List
valtype: UrlPattern
none_ok: true
default: []
restart: true
desc: >-
A list of patterns which should not be shown in the history.
This only affects the completion. Matching URLs are still saved in the
history (and visible on the qute://history page), but hidden in the
completion.
Changing this setting will cause the completion history to be regenerated
on the next start, which will take a short while.
completion.web_history_max_items:
renamed: completion.web_history.max_items
completion.web_history.max_items:
default: -1
type:
name: Int
@ -854,6 +984,18 @@ downloads.location.suggestion:
- both: Show download path and filename.
desc: What to display in the download filename input.
completion.open_categories:
type:
name: FlagList
valid_values: [searchengines, quickmarks, bookmarks, history]
none_ok: true
default:
- searchengines
- quickmarks
- bookmarks
- history
desc: Which categories to show (in which order) in the :open completion.
downloads.open_dispatcher:
type:
name: String
@ -1021,6 +1163,71 @@ hints.scatter:
Ignored for number hints.
hints.selectors:
no_autoconfig: true
default:
all:
- 'a'
- 'area'
- 'textarea'
- 'select'
- 'input:not([type="hidden"])'
- 'button'
- 'frame'
- 'iframe'
- 'img'
- 'link'
- 'summary'
- '[onclick]'
- '[onmousedown]'
- '[role="link"]'
- '[role="option"]'
- '[role="button"]'
- '[ng-click]'
- '[ngClick]'
- '[data-ng-click]'
- '[x-ng-click]'
- '[tabindex]'
links:
- 'a[href]'
- 'area[href]'
- 'link[href]'
- '[role="link"][href]'
images:
- 'img'
media:
- 'audio'
- 'img'
- 'video'
url:
- '[src]'
- '[href]'
inputs:
- 'input[type="text"]'
- 'input[type="date"]'
- 'input[type="datetime-local"]'
- 'input[type="email"]'
- 'input[type="month"]'
- 'input[type="number"]'
- 'input[type="password"]'
- 'input[type="search"]'
- 'input[type="tel"]'
- 'input[type="time"]'
- 'input[type="url"]'
- 'input[type="week"]'
- 'input:not([type])'
- 'textarea'
type:
name: Dict
keytype: String
valtype:
name: List
none_ok: true
valtype: String
supports_pattern: true
desc: CSS selectors used to determine which elements on a page should have
hints.
hints.uppercase:
default: false
type: Bool
@ -1167,9 +1374,15 @@ prompt.radius:
## scrolling
scrolling.bar:
type: Bool
default: false
desc: Show a scrollbar.
type:
name: String
valid_values:
- always: Always show the scrollbar.
- never: Never show the scrollbar.
- when-searching: Show the scrollbar when searching for text in the
webpage. With the QtWebKit backend, this is equal to `never`.
default: when-searching
desc: When to show the scrollbar.
scrolling.smooth:
type: Bool
@ -1349,12 +1562,27 @@ tabs.mousewheel_switching:
tabs.new_position.related:
default: next
type: NewTabPosition
desc: Position of new tabs opened from another tab.
desc: >-
Position of new tabs opened from another tab.
See `tabs.new_position.stacking` for controlling stacking behavior.
tabs.new_position.unrelated:
default: last
type: NewTabPosition
desc: "Position of new tabs which aren't opened from another tab."
desc: >-
Position of new tabs which are not opened from another tab.
See `tabs.new_position.stacking` for controlling stacking behavior.
tabs.new_position.stacking:
default: true
type: Bool
desc: >-
Stack related tabs on top of each other when opened consecutively.
Only applies for `next` and `prev` values of `tabs.new_position.related`
and `tabs.new_position.unrelated`.
tabs.padding:
default:
@ -1498,6 +1726,23 @@ tabs.min_width:
This setting does not apply to pinned tabs, unless `tabs.pinned.shrink` is False.
tabs.max_width:
default: -1
type:
name: Int
minval: -1
maxval: maxint
desc: >-
Maximum width (in pixels) of tabs (-1 for no maximum).
This setting only applies when tabs are horizontal.
This setting does not apply to pinned tabs, unless `tabs.pinned.shrink` is
False.
This setting may not apply properly if max_width is smaller than the
minimum size of tab contents, or smaller than tabs.min_width.
tabs.width.indicator:
renamed: tabs.indicator.width
@ -1753,7 +1998,7 @@ colors.completion.item.selected.border.bottom:
colors.completion.match.fg:
default: '#ff4444'
type: QssColor
type: QtColor
desc: Foreground color of the matched text in the completion.
colors.completion.scrollbar.fg:
@ -2440,6 +2685,7 @@ bindings.default:
.: repeat-command
<Ctrl-p>: tab-pin
<Alt-m>: tab-mute
gD: tab-give
q: record-macro
"@": run-macro
tsh: config-cycle -p -t -u *://{url:host}/* content.javascript.enabled ;; reload
@ -2454,6 +2700,12 @@ bindings.default:
tPH: config-cycle -p -u *://*.{url:host}/* content.plugins ;; reload
tpu: config-cycle -p -t -u {url} content.plugins ;; reload
tPu: config-cycle -p -u {url} content.plugins ;; reload
tih: config-cycle -p -t -u *://{url:host}/* content.images ;; reload
tIh: config-cycle -p -u *://{url:host}/* content.images ;; reload
tiH: config-cycle -p -t -u *://*.{url:host}/* content.images ;; reload
tIH: config-cycle -p -u *://*.{url:host}/* content.images ;; reload
tiu: config-cycle -p -t -u {url} content.images ;; reload
tIu: config-cycle -p -u {url} content.images ;; reload
insert:
<Ctrl-E>: open-editor
<Shift-Ins>: insert-text {primary}
@ -2465,7 +2717,7 @@ bindings.default:
<Ctrl-B>: hint all tab-bg
<Escape>: leave-mode
passthrough:
<Ctrl-V>: leave-mode
<Shift-Escape>: leave-mode
command:
<Ctrl-P>: command-history-prev
<Ctrl-N>: command-history-next
@ -2499,6 +2751,7 @@ bindings.default:
prompt:
<Return>: prompt-accept
<Ctrl-X>: prompt-open-download
<Ctrl-P>: prompt-open-download --pdfjs
<Shift-Tab>: prompt-item-focus prev
<Up>: prompt-item-focus prev
<Tab>: prompt-item-focus next

View File

@ -276,7 +276,24 @@ class YamlConfig(QObject):
del settings['bindings.default']
self._mark_changed()
# content.webrtc_public_interfaces_only got merged into
# content.webrtc_ip_handling_policy.
old = 'content.webrtc_public_interfaces_only'
new = 'content.webrtc_ip_handling_policy'
if old in settings:
settings[new] = {}
for scope, val in settings[old].items():
if val:
settings[new][scope] = 'default-public-interface-only'
else:
settings[new][scope] = 'all-interfaces'
del settings[old]
self._mark_changed()
self._migrate_bool(settings, 'tabs.favicons.show', 'always', 'never')
self._migrate_bool(settings, 'scrolling.bar',
'when-searching', 'never')
self._migrate_bool(settings, 'qt.force_software_rendering',
'software-opengl', 'none')

View File

@ -28,6 +28,7 @@ from qutebrowser.config import (config, configdata, configfiles, configtypes,
configexc, configcommands)
from qutebrowser.utils import (objreg, usertypes, log, standarddir, message,
qtutils)
from qutebrowser.config import configcache
from qutebrowser.misc import msgbox, objects
@ -44,6 +45,7 @@ def early_init(args):
config.instance = config.Config(yaml_config=yaml_config)
config.val = config.ConfigContainer(config.instance)
config.key_instance = config.KeyConfig(config.instance)
config.cache = configcache.ConfigCache()
yaml_config.setParent(config.instance)
for cf in config.change_filters:
@ -89,6 +91,8 @@ def _init_envvars():
os.environ['QT_XCB_FORCE_SOFTWARE_OPENGL'] = '1'
elif software_rendering == 'qt-quick':
os.environ['QT_QUICK_BACKEND'] = 'software'
elif software_rendering == 'chromium':
os.environ['QT_WEBENGINE_DISABLE_NOUVEAU_WORKAROUND'] = '1'
if config.val.qt.force_platform is not None:
os.environ['QT_QPA_PLATFORM'] = config.val.qt.force_platform
@ -167,24 +171,67 @@ def qt_args(namespace):
argv += ['--' + arg for arg in config.val.qt.args]
if objects.backend == usertypes.Backend.QtWebEngine:
if not qtutils.version_check('5.11', compiled=False):
# WORKAROUND equivalent to
# https://codereview.qt-project.org/#/c/217932/
# Needed for Qt < 5.9.5 and < 5.10.1
argv.append('--disable-shared-workers')
if config.val.qt.force_software_rendering == 'chromium':
argv.append('--disable-gpu')
if not config.val.content.canvas_reading:
argv.append('--disable-reading-from-canvas')
if not qtutils.version_check('5.11'):
# On Qt 5.11, we can control this via QWebEngineSettings
if not config.val.content.autoplay:
argv.append('--autoplay-policy=user-gesture-required')
if config.val.content.webrtc_public_interfaces_only:
argv.append('--force-webrtc-ip-handling-policy='
'default_public_interface_only')
argv += list(_qtwebengine_args())
return argv
def _qtwebengine_args():
"""Get the QtWebEngine arguments to use based on the config."""
if not qtutils.version_check('5.11', compiled=False):
# WORKAROUND equivalent to
# https://codereview.qt-project.org/#/c/217932/
# Needed for Qt < 5.9.5 and < 5.10.1
yield '--disable-shared-workers'
settings = {
'qt.force_software_rendering': {
'software-opengl': None,
'qt-quick': None,
'chromium': '--disable-gpu',
'none': None,
},
'content.canvas_reading': {
True: None,
False: '--disable-reading-from-canvas',
},
'content.webrtc_ip_handling_policy': {
'all-interfaces': None,
'default-public-and-private-interfaces':
'--force-webrtc-ip-handling-policy='
'default_public_and_private_interfaces',
'default-public-interface-only':
'--force-webrtc-ip-handling-policy='
'default_public_interface_only',
'disable-non-proxied-udp':
'--force-webrtc-ip-handling-policy='
'disable_non_proxied_udp',
},
'qt.process_model': {
'process-per-site-instance': None,
'process-per-site': '--process-per-site',
'single-process': '--single-process',
},
'qt.low_end_device_mode': {
'auto': None,
'always': '--enable-low-end-device-mode',
'never': '--disable-low-end-device-mode',
},
'content.headers.referer': {
'always': None,
'never': '--no-referrers',
'same-domain': '--reduced-referrer-granularity',
}
}
if not qtutils.version_check('5.11'):
# On Qt 5.11, we can control this via QWebEngineSettings
settings['content.autoplay'] = {
True: None,
False: '--autoplay-policy=user-gesture-required',
}
for setting, args in sorted(settings.items()):
arg = args[config.instance.get(setting)]
if arg is not None:
yield arg

View File

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

View File

@ -111,7 +111,7 @@ li {
<li>
You can manually download the pdf.js archive
<a href="https://mozilla.github.io/pdf.js/getting_started/#download">here</a>
and extract it to <code>~/.local/share/qutebrowser/pdfjs</code>
and extract it to <code>{{ pdfjs_dir }}</code>
<br>
<span class="warning">Warning:</span> Using this method you are
responsible for yourself to keep the installation updated! If a

View File

@ -3,7 +3,8 @@
{% block script %}
var cset = function(option, value) {
// FIXME:conf we might want some error handling here?
var url = "qute://settings/set?option=" + encodeURIComponent(option);
var url = "qute://user:{{csrf_token}}@settings/set"
url += "?option=" + encodeURIComponent(option);
url += "&value=" + encodeURIComponent(value);
var xhr = new XMLHttpRequest();
xhr.open("GET", url);
@ -33,7 +34,7 @@ input { width: 98%; }
<th>Setting</th>
<th>Value</th>
</tr>
{% for option in configdata.DATA.values() if not option.no_autoconfig %}
{% for option in configdata.DATA.values()|sort(attribute='name') if not option.no_autoconfig %}
<tr>
<!-- FIXME: convert to string properly -->
<td class="setting">{{ option.name }} (Current: {{ confget(option.name) | string |truncate(100) }})

Some files were not shown because too many files have changed in this diff Show More