summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.bumpversion.cfg2
-rw-r--r--.github/ISSUE_TEMPLATE/1_Bug_report.md6
-rw-r--r--.gitignore1
-rw-r--r--.pylintrc2
-rw-r--r--README.asciidoc14
-rw-r--r--doc/changelog.asciidoc73
-rw-r--r--doc/contributing.asciidoc3
-rw-r--r--doc/faq.asciidoc18
-rw-r--r--doc/help/commands.asciidoc6
-rw-r--r--doc/help/settings.asciidoc113
-rw-r--r--misc/org.qutebrowser.qutebrowser.appdata.xml2
-rw-r--r--misc/requirements/requirements-check-manifest.txt3
-rw-r--r--misc/requirements/requirements-codecov.txt12
-rw-r--r--misc/requirements/requirements-dev.txt19
-rw-r--r--misc/requirements/requirements-dev.txt-raw1
-rw-r--r--misc/requirements/requirements-flake8.txt10
-rw-r--r--misc/requirements/requirements-mypy.txt4
-rw-r--r--misc/requirements/requirements-pip.txt6
-rw-r--r--misc/requirements/requirements-pylint.txt16
-rw-r--r--misc/requirements/requirements-pyqt-5.12.txt2
-rw-r--r--misc/requirements/requirements-pyqt-5.13.txt2
-rw-r--r--misc/requirements/requirements-pyqt-5.14.txt4
-rw-r--r--misc/requirements/requirements-pyqt.txt4
-rw-r--r--misc/requirements/requirements-pyroma.txt2
-rw-r--r--misc/requirements/requirements-sphinx.txt28
-rw-r--r--misc/requirements/requirements-tests.txt35
-rw-r--r--misc/requirements/requirements-tests.txt-raw1
-rw-r--r--misc/requirements/requirements-tox.txt12
-rw-r--r--misc/requirements/requirements-vulture.txt2
-rwxr-xr-xmisc/userscripts/qute-bitwarden57
-rwxr-xr-xmisc/userscripts/readability-js12
-rw-r--r--pytest.ini7
-rw-r--r--qutebrowser/__init__.py2
-rw-r--r--qutebrowser/browser/browsertab.py2
-rw-r--r--qutebrowser/browser/commands.py16
-rw-r--r--qutebrowser/browser/eventfilter.py62
-rw-r--r--qutebrowser/browser/network/proxy.py2
-rw-r--r--qutebrowser/browser/webengine/tabhistory.py51
-rw-r--r--qutebrowser/browser/webengine/webenginesettings.py49
-rw-r--r--qutebrowser/browser/webengine/webenginetab.py144
-rw-r--r--qutebrowser/browser/webkit/webkittab.py6
-rw-r--r--qutebrowser/commands/userscripts.py17
-rw-r--r--qutebrowser/config/configdata.yml80
-rw-r--r--qutebrowser/config/configfiles.py2
-rw-r--r--qutebrowser/config/configtypes.py14
-rw-r--r--qutebrowser/html/warning-webkit.html8
-rw-r--r--qutebrowser/javascript/greasemonkey_wrapper.js86
-rw-r--r--qutebrowser/javascript/whatsapp_web_quirk.user.js15
-rw-r--r--qutebrowser/keyinput/basekeyparser.py2
-rw-r--r--qutebrowser/keyinput/eventfilter.py27
-rw-r--r--qutebrowser/mainwindow/mainwindow.py9
-rw-r--r--qutebrowser/misc/miscwidgets.py2
-rw-r--r--qutebrowser/utils/log.py2
-rw-r--r--qutebrowser/utils/urlutils.py7
-rw-r--r--qutebrowser/utils/version.py14
-rw-r--r--requirements.txt6
-rwxr-xr-xscripts/asciidoc2html.py11
-rw-r--r--scripts/dev/build_pyqt_wheel.py73
-rwxr-xr-xscripts/dev/build_release.py19
-rw-r--r--scripts/dev/misc_checks.py2
-rw-r--r--scripts/dev/recompile_requirements.py30
-rw-r--r--scripts/dev/update_version.py8
-rwxr-xr-xscripts/dictcli.py9
-rw-r--r--scripts/mkvenv.py85
-rw-r--r--scripts/utils.py11
-rw-r--r--tests/end2end/features/conftest.py3
-rw-r--r--tests/end2end/features/javascript.feature1
-rw-r--r--tests/end2end/features/search.feature50
-rw-r--r--tests/end2end/features/spawn.feature8
-rw-r--r--tests/end2end/features/test_editor_bdd.py8
-rw-r--r--tests/end2end/features/test_prompts_bdd.py15
-rw-r--r--tests/end2end/features/test_qutescheme_bdd.py14
-rw-r--r--tests/end2end/features/yankpaste.feature5
-rw-r--r--tests/end2end/fixtures/quteprocess.py6
-rw-r--r--tests/end2end/fixtures/webserver.py3
-rw-r--r--tests/end2end/fixtures/webserver_sub.py9
-rw-r--r--tests/end2end/fixtures/webserver_sub_ssl.py5
-rw-r--r--tests/helpers/stubs.py5
-rw-r--r--tests/unit/config/test_configfiles.py2
-rw-r--r--tests/unit/config/test_configinit.py8
-rw-r--r--tests/unit/config/test_configtypes.py10
-rw-r--r--tests/unit/javascript/test_greasemonkey.py52
-rw-r--r--tests/unit/mainwindow/test_messageview.py2
-rw-r--r--tests/unit/misc/test_throttle.py2
-rw-r--r--tests/unit/utils/test_urlutils.py25
-rw-r--r--tests/unit/utils/test_version.py24
-rw-r--r--www/header.asciidoc4
87 files changed, 1258 insertions, 355 deletions
diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index c91a2de90..6671e388f 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -1,5 +1,5 @@
[bumpversion]
-current_version = 1.10.0
+current_version = 1.10.2
commit = True
message = Release v{new_version}
tag = True
diff --git a/.github/ISSUE_TEMPLATE/1_Bug_report.md b/.github/ISSUE_TEMPLATE/1_Bug_report.md
index 5e86b9a76..afd634f48 100644
--- a/.github/ISSUE_TEMPLATE/1_Bug_report.md
+++ b/.github/ISSUE_TEMPLATE/1_Bug_report.md
@@ -4,12 +4,12 @@ about: Report errors and problems
---
-**Version info (see `:version`)**:
+**Version info**:
+<!-- Please copy the first block from :version, not just the qutebrowser version -->
**Does the bug happen if you start with `--temp-basedir`?**:
**Description**
**How to reproduce**
-<!-- Link to the affected site, or steps to reproduce the issue
-(if possible/applicable). -->
+<!-- Link to the affected site, or steps to reproduce the issue (if possible/applicable). -->
diff --git a/.gitignore b/.gitignore
index 6074de319..2f5c25116 100644
--- a/.gitignore
+++ b/.gitignore
@@ -44,3 +44,4 @@ TODO
/doc/extapi/_build
/misc/nsis/include
/misc/nsis/plugins
+/wheels
diff --git a/.pylintrc b/.pylintrc
index c68b4ae3f..1fedefb6d 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -60,7 +60,7 @@ no-docstring-rgx=(^_|^main$)
[FORMAT]
max-line-length=79
-ignore-long-lines=(<?https?://|^# Copyright 201\d|link:)
+ignore-long-lines=(<?https?://|file://|^# Copyright 201\d|link:)
expected-line-ending-format=LF
[VARIABLES]
diff --git a/README.asciidoc b/README.asciidoc
index 96decd339..9e2a61a22 100644
--- a/README.asciidoc
+++ b/README.asciidoc
@@ -28,9 +28,6 @@ time, your help is needed! See the
https://github.com/sponsors/The-Compiler/[GitHub Sponsors page] for more
information. Depending on your sign-up date and how long you keep a certain
level, you can get qutebrowser t-shirts, stickers and more!
-
-Thanks to the GitHub Sponsors Matching Fund, all donations done via GitHub
-Sponsors (up to a $5000 total) will be doubled until October 2020.
// QUTE_WEB_HIDE_END
Screenshots
@@ -153,9 +150,6 @@ https://github.com/sponsors/The-Compiler/[GitHub Sponsors page] for more
information. Depending on your sign-up date and how long you keep a certain
level, you can get qutebrowser t-shirts, stickers and more!
-Thanks to the GitHub Sponsors Matching Fund, all donations done via GitHub
-Sponsors (up to a $5000 total) will be doubled until October 2020!
-
Alternatively, the following donation methods are available -- note that
eligibility for swag (shirts/stickers/etc.) is handled on a case-by-case basis
for those, please mailto:mail@qutebrowser.org[get in touch] for details.
@@ -168,7 +162,13 @@ for those, please mailto:mail@qutebrowser.org[get in touch] for details.
- Bank: PostFinance AG, Mingerstrasse 20, 3030 Bern, Switzerland (BIC: POFICHBEXXX)
- If you need any other information: Contact me at mail@qutebrowser.org.
* PayPal: https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=me%40the-compiler.org&item_name=qutebrowser&currency_code=CHF&source=url[CHF], https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=me%40the-compiler.org&item_name=qutebrowser&currency_code=EUR&source=url[EUR], https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=me%40the-compiler.org&item_name=qutebrowser&currency_code=USD&source=url[USD]
-* Bitcoin: link:bitcoin:1PMzbcetAHfpxoXww8Bj5XqquHtVvMjJtE[1PMzbcetAHfpxoXww8Bj5XqquHtVvMjJtE]
+* Cryptocurrencies:
+ - Bitcoin: link:bitcoin:bc1q3ptyw8hxrcfz6ucfgmglphfvhqpy8xr6k25p00[bc1q3ptyw8hxrcfz6ucfgmglphfvhqpy8xr6k25p00]
+ - Bitcoin Cash: link:bitcoincash:1BnxUbnJ5MrEPeh5nuUMx83tbiRAvqJV3N[1BnxUbnJ5MrEPeh5nuUMx83tbiRAvqJV3N]
+ - Ethereum: link:ethereum:0x10c2425856F7a8799EBCaac4943026803b1089c6[0x10c2425856F7a8799EBCaac4943026803b1089c6]
+ - Litecoin: link:litecoin:MDt3YQciuCh6QyFmr8TiWNxB94PVzbnPm2[MDt3YQciuCh6QyFmr8TiWNxB94PVzbnPm2]
+ - Others: Please mailto:mail@qutebrowser.org[get in touch], I'd happily set up anything link:https://www.ledger.com/supported-crypto-assets[supported by Ledger Live]
+
Sponsors
--------
diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc
index b10fcc441..bbb166eeb 100644
--- a/doc/changelog.asciidoc
+++ b/doc/changelog.asciidoc
@@ -18,9 +18,70 @@ breaking changes (such as renamed commands) can happen in minor releases.
v1.11.0 (unreleased)
--------------------
-No changes yet.
+Added
+~~~~~
+
+- New settings:
+ * `search.wrap` which can be set to false to prevent wrapping around the page
+ when searching. With QtWebEngine, Qt 5.14 or newer is required.
+ * `content.unknown_url_scheme_policy` which allows controlling when an
+ external application is opened for external links (never, from user
+ interaction, always).
+ * `content.fullscreen.overlay_timeout` to configure how long the fullscreen
+ overlay should be displayed. If set to `0`, no overlay is displayed.
+ * `hints.padding` to add additional padding for hints.
+ * `hints.radius` to set a border radius for hints (set to `3` by default).
+- New placeholders for `url.searchengines` values:
+ * `{unquoted}` inserts the search term without any quoting
+ * `{semiquoted}` (same as `{}`) quotes most special characters, but slashes
+ remain unquoted.
+ * `{quoted}` (same as `{}` in earlier releases) also quotes slashes.
+
+Changed
+~~~~~~~
+
+- Searching now wraps around the page by default with QtWebKit (where it didn't
+ before). Set `search.wrap` to `false` to restore the old behavior.
+- The `{}` placeholder for search engines (the `url.searchengines` setting) now
+ does not quote slashes anymore, but other characters typically encoded in
+ URLs still get encoded. This matches the behavior of search engines in
+ Chromium. To revert to the old behavior, use `{quoted}` instead.
+- The `content.windowed_fullscreen` setting got renamed to
+ `content.fullscreen.window`.
+- The `qute-bitwarden` userscript now has an optional `--totp` flag which can
+ be used to copy TOTP codes to clipboard (requires the `pyperclip` module).
+
+Fixed
+~~~~~
+
+- `unsafeWindow` is now defined for Greasemonkey scripts with QtWebKit.
+- The proxied `window` global is now shared between different
+ Greasemonkey scripts (but still separate from the page's `window`), to
+ match the original Greasemonkey implementation.
+- The `--output-messages` (`-m`) flag added in v1.9.0 now also works correctly
+ when using `:spawn --userscript`.
+- `:version` and `--version` now don't crash if there's an (invalid)
+ `/etc/os-release` file which has non-comment lines without a `=` character.
+- Scripts in `scripts/` now report errors to `stderr` correctly, instead of
+ using `stdout`.
+
+v1.10.2 (2020-04-17)
+--------------------
+
+Changed
+~~~~~~~
+
+- Windows and macOS releases now bundle Qt 5.14.2, including security fixes up
+ to Chromium 80.0.3987.132.
+
+Fixed
+~~~~~
+
+- The WhatsApp workaround now also works when using WhatsApp in languages other
+ than English.
+- The `mkvenv.py` script now also works properly on Windows.
-v1.10.1 (unreleased)
+v1.10.1 (2020-02-15)
--------------------
Fixed
@@ -28,7 +89,10 @@ Fixed
- Crash when saving data fails during shutdown (which was a regression
introduced in v1.9.0).
-- Error while reading config.py when `fonts.tabs` or `fonts.debug_console` is set to a value including `default_size`.
+- Error while reading config.py when `fonts.tabs` or `fonts.debug_console` is
+ set to a value including `default_size`.
+- When a `state` file contains invalid UTF-8 data, a proper error is now
+ displayed.
Changed
~~~~~~~
@@ -44,6 +108,9 @@ Changed
- The `mkvenv.py` now ensures the latest versions of `setuptools` and `wheel`
are installed in the virtual environment, which should speed up installation
and fix install issues.
+- The default for `colors.statusbar.command.private.bg` has been changed to a
+ slightly different gray, as a workaround for a Qt issue where the cursor was
+ invisible in that case.
v1.10.0 (2020-02-02)
--------------------
diff --git a/doc/contributing.asciidoc b/doc/contributing.asciidoc
index 8dfc94c4f..fdaf7dd37 100644
--- a/doc/contributing.asciidoc
+++ b/doc/contributing.asciidoc
@@ -707,6 +707,9 @@ qutebrowser release
* Make sure there are no unstaged changes and the tests are green.
* Make sure all issues with the related milestone are closed.
* Consider updating the completions for `content.headers.user_agent` in `configdata.yml`.
+* Minor release: Consider updating some files from master:
+ - `misc/requirements/` and `requirements.txt`
+ - `scripts/`
* Make sure Python is up-to-date on build machines.
* Mark the milestone at https://github.com/qutebrowser/qutebrowser/milestones as closed.
* Update changelog in master branch
diff --git a/doc/faq.asciidoc b/doc/faq.asciidoc
index 32861c87c..1c69056c0 100644
--- a/doc/faq.asciidoc
+++ b/doc/faq.asciidoc
@@ -367,14 +367,15 @@ up for a monthly donation to The-Compiler (qutebrowser's main developer),
allowing him to work part-time on qutebrowser. If you keep your donation level
for long enough, you can get some qutebrowser stickers!
-Why GitHub Sponsors? What is the GitHub Matching Fund?::
- Thanks to the
- https://help.github.com/en/github/supporting-the-open-source-community-with-github-sponsors/about-github-sponsors#about-the-github-sponsors-matching-fund[GitHub Sponsors Matching Fund],
- all donations are doubled by GitHub in the first year, up to a $5000 total limit.
+Why GitHub Sponsors?::
+ GitHub Sponsors is a crowdfundign platform nicely integrated with
+ qutebrowser's existing GitHub page and a better offering than alternatives such
+ as Patreon or Liberapay.
+
-Even outside of the matching fund, GitHub Sponsors is nicely integrated with
-qutebrowser's existing GitHub page and a better offering than alternatives such
-as Patreon or Liberapay.
+It also offers a
+https://help.github.com/en/github/supporting-the-open-source-community-with-github-sponsors/about-github-sponsors#about-the-github-sponsors-matching-fund[Matching Fund]
+which matches all donations until a cap of $5000, which has already been
+reached by qutebrowser.
Is it possible to contribute via a one-time donation instead?::
If you prefer a one-time donation, there are various possibilities:
@@ -382,8 +383,7 @@ Is it possible to contribute via a one-time donation instead?::
- Select a tier which covers the total amount you'd like to donate (note that
payments are prorated based on the current date). After the payment is
processed, cancel your GitHub sponsors subscription again. This has a big
- benefit: Thanks to GitHub's matching fund, your donation will be doubled (and
- nothing will be lost to fees).
+ benefit: There are no fees deducted from your amount.
+
- Sign up for a lower recurring donation instead.
+
diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc
index bbe5a6f3d..80d2ca848 100644
--- a/doc/help/commands.asciidoc
+++ b/doc/help/commands.asciidoc
@@ -112,7 +112,7 @@ possible to run or bind multiple commands by separating them with `;;`.
|<<set,set>>|Set an option.
|<<set-cmd-text,set-cmd-text>>|Preset the statusbar to some text.
|<<set-mark,set-mark>>|Set a mark at the current scroll position in the current tab.
-|<<spawn,spawn>>|Spawn a command in a shell.
+|<<spawn,spawn>>|Spawn an external command.
|<<stop,stop>>|Stop loading in the current/[count]th tab.
|<<tab-clone,tab-clone>>|Duplicate the current tab.
|<<tab-close,tab-close>>|Close the current/[count]th tab.
@@ -1256,7 +1256,9 @@ Syntax: +:spawn [*--userscript*] [*--verbose*] [*--output*] [*--output-messages*
[*--detach*]
'cmdline'+
-Spawn a command in a shell.
+Spawn an external command.
+
+Note that the command is *not* run in a shell, so things like `$VAR` or `> output` won't have the desired effect.
==== positional arguments
* +'cmdline'+: The commandline to execute.
diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc
index 92f6a3a51..5182968a6 100644
--- a/doc/help/settings.asciidoc
+++ b/doc/help/settings.asciidoc
@@ -136,6 +136,8 @@
|<<content.desktop_capture,content.desktop_capture>>|Allow websites to share screen content.
|<<content.dns_prefetch,content.dns_prefetch>>|Try to pre-fetch DNS entries to speed up browsing.
|<<content.frame_flattening,content.frame_flattening>>|Expand each subframe to its contents.
+|<<content.fullscreen.overlay_timeout,content.fullscreen.overlay_timeout>>|Set fullscreen notification overlay timeout in milliseconds.
+|<<content.fullscreen.window,content.fullscreen.window>>|Limit fullscreen to the browser window (does not expand to fill the screen).
|<<content.geolocation,content.geolocation>>|Allow websites to request geolocations.
|<<content.headers.accept_language,content.headers.accept_language>>|Value to send in the `Accept-Language` header.
|<<content.headers.custom,content.headers.custom>>|Custom headers for qutebrowser HTTP requests.
@@ -173,10 +175,10 @@
|<<content.register_protocol_handler,content.register_protocol_handler>>|Allow websites to register protocol handlers via `navigator.registerProtocolHandler`.
|<<content.site_specific_quirks,content.site_specific_quirks>>|Enable quirks (such as faked user agent headers) needed to get specific sites to work properly.
|<<content.ssl_strict,content.ssl_strict>>|Validate SSL handshakes.
+|<<content.unknown_url_scheme_policy,content.unknown_url_scheme_policy>>|How navigation requests to URLs with unknown schemes are handled.
|<<content.user_stylesheets,content.user_stylesheets>>|List of user stylesheet filenames to use.
|<<content.webgl,content.webgl>>|Enable WebGL.
|<<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.
|<<downloads.location.prompt,downloads.location.prompt>>|Prompt the user for the download location.
@@ -223,7 +225,9 @@
|<<hints.min_chars,hints.min_chars>>|Minimum number of characters used for hint strings.
|<<hints.mode,hints.mode>>|Mode to use for hints.
|<<hints.next_regexes,hints.next_regexes>>|Comma-separated list of regular expressions to use for 'next' links.
+|<<hints.padding,hints.padding>>|Padding (in pixels) for hints.
|<<hints.prev_regexes,hints.prev_regexes>>|Comma-separated list of regular expressions to use for 'prev' links.
+|<<hints.radius,hints.radius>>|Rounding radius (in pixels) for the edges of hints.
|<<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.
@@ -245,7 +249,7 @@
|<<messages.timeout,messages.timeout>>|Duration (in milliseconds) to show messages in the statusbar for.
|<<new_instance_open_target,new_instance_open_target>>|How to open links in an existing instance if a new one is launched.
|<<new_instance_open_target_window,new_instance_open_target_window>>|Which window to choose when opening links as new tabs.
-|<<prompt.filebrowser,prompt.filebrowser>>|Show a filebrowser in upload/download prompts.
+|<<prompt.filebrowser,prompt.filebrowser>>|Show a filebrowser in download prompts.
|<<prompt.radius,prompt.radius>>|Rounding radius (in pixels) for the edges of prompts.
|<<qt.args,qt.args>>|Additional arguments to pass to Qt, without leading `--`.
|<<qt.force_platform,qt.force_platform>>|Force a Qt platform to use.
@@ -258,6 +262,7 @@
|<<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.
+|<<search.wrap,search.wrap>>|Wrap around at the top and bottom of the page when advancing through text matches using `:search-next` and `:search-prev`.
|<<session.default_name,session.default_name>>|Name of the session to save by default.
|<<session.lazy_restore,session.lazy_restore>>|Load a restored tab as soon as it takes focus.
|<<spellcheck.languages,spellcheck.languages>>|Languages to use for spell checking.
@@ -1193,7 +1198,7 @@ Background color of the statusbar in private browsing + command mode.
Type: <<types,QssColor>>
-Default: +pass:[grey]+
+Default: +pass:[darkslategray]+
[[colors.statusbar.command.private.fg]]
=== colors.statusbar.command.private.fg
@@ -1809,6 +1814,23 @@ Default: +pass:[false]+
This setting is only available with the QtWebKit backend.
+[[content.fullscreen.overlay_timeout]]
+=== content.fullscreen.overlay_timeout
+Set fullscreen notification overlay timeout in milliseconds.
+If set to 0, no overlay will be displayed.
+
+Type: <<types,Int>>
+
+Default: +pass:[3000]+
+
+[[content.fullscreen.window]]
+=== content.fullscreen.window
+Limit fullscreen to the browser window (does not expand to fill the screen).
+
+Type: <<types,Bool>>
+
+Default: +pass:[false]+
+
[[content.geolocation]]
=== content.geolocation
Allow websites to request geolocations.
@@ -2283,6 +2305,26 @@ Valid values:
Default: +pass:[ask]+
+[[content.unknown_url_scheme_policy]]
+=== content.unknown_url_scheme_policy
+How navigation requests to URLs with unknown schemes are handled.
+
+This setting supports URL patterns.
+
+Type: <<types,String>>
+
+Valid values:
+
+ * +disallow+: Disallows all navigation requests to URLs with unknown schemes.
+ * +allow-from-user-interaction+: Allows navigation requests to URLs with unknown schemes that are issued from user-interaction (like a mouse-click), whereas other navigation requests (for example from JavaScript) are suppressed.
+ * +allow-all+: Allows all navigation requests to URLs with unknown schemes.
+
+Default: +pass:[allow-from-user-interaction]+
+
+On QtWebEngine, this setting requires Qt 5.11 or newer.
+
+On QtWebKit, this setting is unavailable.
+
[[content.user_stylesheets]]
=== content.user_stylesheets
List of user stylesheet filenames to use.
@@ -2322,14 +2364,6 @@ On QtWebEngine, this setting requires Qt 5.9.2 or newer.
On QtWebKit, this setting is unavailable.
-[[content.windowed_fullscreen]]
-=== content.windowed_fullscreen
-Limit fullscreen to the browser window (does not expand to fill the screen).
-
-Type: <<types,Bool>>
-
-Default: +pass:[false]+
-
[[content.xss_auditing]]
=== content.xss_auditing
Monitor load requests for cross-site scripting attempts.
@@ -2764,6 +2798,19 @@ Default:
- +pass:[\b(&gt;&gt;|»)\b]+
- +pass:[\bcontinue\b]+
+[[hints.padding]]
+=== hints.padding
+Padding (in pixels) for hints.
+
+Type: <<types,Padding>>
+
+Default:
+
+- +pass:[bottom]+: +pass:[0]+
+- +pass:[left]+: +pass:[3]+
+- +pass:[right]+: +pass:[3]+
+- +pass:[top]+: +pass:[0]+
+
[[hints.prev_regexes]]
=== hints.prev_regexes
Comma-separated list of regular expressions to use for 'prev' links.
@@ -2778,6 +2825,14 @@ Default:
- +pass:[\b[&lt;←≪]\b]+
- +pass:[\b(&lt;&lt;|«)\b]+
+[[hints.radius]]
+=== hints.radius
+Rounding radius (in pixels) for the edges of hints.
+
+Type: <<types,Int>>
+
+Default: +pass:[3]+
+
[[hints.scatter]]
=== hints.scatter
Scatter hint key chains (like Vimium) or not (like dwb).
@@ -3048,7 +3103,7 @@ Default: +pass:[last-focused]+
[[prompt.filebrowser]]
=== prompt.filebrowser
-Show a filebrowser in upload/download prompts.
+Show a filebrowser in download prompts.
Type: <<types,Bool>>
@@ -3209,6 +3264,16 @@ Type: <<types,Bool>>
Default: +pass:[true]+
+[[search.wrap]]
+=== search.wrap
+Wrap around at the top and bottom of the page when advancing through text matches using `:search-next` and `:search-prev`.
+
+Type: <<types,Bool>>
+
+Default: +pass:[true]+
+
+On QtWebEngine, this setting requires Qt 5.14 or newer.
+
[[session.default_name]]
=== session.default_name
Name of the session to save by default.
@@ -3754,8 +3819,28 @@ Default: +pass:[false]+
[[url.searchengines]]
=== url.searchengines
Search engines which can be used via the address bar.
-Maps a search engine name (such as `DEFAULT`, or `ddg`) to a URL with a `{}` placeholder. The placeholder will be replaced by the search term, use `{{` and `}}` for literal `{`/`}` signs.
-The search engine named `DEFAULT` is used when `url.auto_search` is turned on and something else than a URL was entered to be opened. Other search engines can be used by prepending the search engine name to the search term, e.g. `:open google qutebrowser`.
+
+Maps a search engine name (such as `DEFAULT`, or `ddg`) to a URL with a
+`{}` placeholder. The placeholder will be replaced by the search term, use
+`{{` and `}}` for literal `{`/`}` braces.
+
+The following further placeholds are defined to configure how special
+characters in the search terms are replaced by safe characters (called
+'quoting'):
+
+* `{}` and `{semiquoted}` quote everything except slashes; this is the most
+ sensible choice for almost all search engines (for the search term
+ `slash/and&amp` this placeholder expands to `slash/and%26amp`).
+* `{quoted}` quotes all characters (for `slash/and&amp` this placeholder
+ expands to `slash%2Fand%26amp`).
+* `{unquoted}` quotes nothing (for `slash/and&amp` this placeholder
+ expands to `slash/and&amp`).
+
+The search engine named `DEFAULT` is used when `url.auto_search` is turned
+on and something else than a URL was entered to be opened. Other search
+engines can be used by prepending the search engine name to the search
+term, e.g. `:open google qutebrowser`.
+
Type: <<types,Dict>>
diff --git a/misc/org.qutebrowser.qutebrowser.appdata.xml b/misc/org.qutebrowser.qutebrowser.appdata.xml
index 4a52401e4..d11824650 100644
--- a/misc/org.qutebrowser.qutebrowser.appdata.xml
+++ b/misc/org.qutebrowser.qutebrowser.appdata.xml
@@ -44,6 +44,8 @@
</content_rating>
<releases>
<!-- Add new releases here -->
+<release version="1.10.2" date="2020-04-17"/>
+<release version="1.10.1" date="2020-02-15"/>
<release version="1.10.0" date="2020-02-02"/>
<release version="1.9.0" date="2020-01-08"/>
<release version="1.8.3" date="2019-12-05"/>
diff --git a/misc/requirements/requirements-check-manifest.txt b/misc/requirements/requirements-check-manifest.txt
index 8a38cead1..8a7a45351 100644
--- a/misc/requirements/requirements-check-manifest.txt
+++ b/misc/requirements/requirements-check-manifest.txt
@@ -1,4 +1,5 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
-check-manifest==0.40
+check-manifest==0.41
+pep517==0.8.2
toml==0.10.0
diff --git a/misc/requirements/requirements-codecov.txt b/misc/requirements/requirements-codecov.txt
index 743297ca4..3811d792f 100644
--- a/misc/requirements/requirements-codecov.txt
+++ b/misc/requirements/requirements-codecov.txt
@@ -1,9 +1,9 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
-certifi==2019.11.28
+certifi==2020.4.5.1
chardet==3.0.4
-codecov==2.0.15
-coverage==5.0.3
-idna==2.8
-requests==2.22.0
-urllib3==1.25.8
+codecov==2.0.22
+coverage==5.1
+idna==2.9
+requests==2.23.0
+urllib3==1.25.9
diff --git a/misc/requirements/requirements-dev.txt b/misc/requirements/requirements-dev.txt
index d871f35a7..040587204 100644
--- a/misc/requirements/requirements-dev.txt
+++ b/misc/requirements/requirements-dev.txt
@@ -1,22 +1,27 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
bump2version==1.0.0
-certifi==2019.11.28
+certifi==2020.4.5.1
cffi==1.14.0
chardet==3.0.4
colorama==0.4.3
-cryptography==2.8
+cryptography==2.9
cssutils==1.0.2
github3.py==1.3.0
hunter==3.1.3
-idna==2.8
-jwcrypto==0.6.0
+idna==2.9
+jwcrypto==0.7
lxml==4.5.0
manhole==1.6.0
-pycparser==2.19
+packaging==20.3
+pycparser==2.20
Pympler==0.8
+pyparsing==2.4.7
+PyQt-builder==1.3.2
python-dateutil==2.8.1
-requests==2.22.0
+requests==2.23.0
+sip==5.2.0
six==1.14.0
+toml==0.10.0
uritemplate==3.0.1
-urllib3==1.25.8
+urllib3==1.25.9
diff --git a/misc/requirements/requirements-dev.txt-raw b/misc/requirements/requirements-dev.txt-raw
index ab58a3fef..f75a837af 100644
--- a/misc/requirements/requirements-dev.txt-raw
+++ b/misc/requirements/requirements-dev.txt-raw
@@ -5,3 +5,4 @@ github3.py
bump2version
requests
lxml
+pyqt-builder
diff --git a/misc/requirements/requirements-flake8.txt b/misc/requirements/requirements-flake8.txt
index dc9c25654..8513c7824 100644
--- a/misc/requirements/requirements-flake8.txt
+++ b/misc/requirements/requirements-flake8.txt
@@ -4,7 +4,7 @@ attrs==19.3.0
entrypoints==0.3
flake8==3.7.9
flake8-bugbear==20.1.4
-flake8-builtins==1.4.2
+flake8-builtins==1.5.2
flake8-comprehensions==3.2.2
flake8-copyright==0.2.2
flake8-debugger==3.2.1
@@ -13,13 +13,13 @@ flake8-docstrings==1.5.0
flake8-future-import==0.4.6
flake8-mock==0.3
flake8-polyfill==1.0.2
-flake8-string-format==0.2.3
-flake8-tidy-imports==4.0.0
+flake8-string-format==0.3.0
+flake8-tidy-imports==4.1.0
flake8-tuple==0.4.1
mccabe==0.6.1
-pep8-naming==0.9.1
+pep8-naming==0.10.0
pycodestyle==2.5.0
pydocstyle==5.0.2
-pyflakes==2.1.1
+pyflakes==2.2.0
six==1.14.0
snowballstemmer==2.0.0
diff --git a/misc/requirements/requirements-mypy.txt b/misc/requirements/requirements-mypy.txt
index aa08cc2b7..435e5d618 100644
--- a/misc/requirements/requirements-mypy.txt
+++ b/misc/requirements/requirements-mypy.txt
@@ -1,9 +1,9 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
-mypy==0.761
+mypy==0.770
mypy-extensions==0.4.3
# PyQt5==5.11.3
# PyQt5-sip==4.19.19
-e git+https://github.com/qutebrowser/PyQt5-stubs.git@wip#egg=PyQt5_stubs
typed-ast==1.4.1
-typing-extensions==3.7.4.1
+typing-extensions==3.7.4.2
diff --git a/misc/requirements/requirements-pip.txt b/misc/requirements/requirements-pip.txt
index cadb99793..ce2e46532 100644
--- a/misc/requirements/requirements-pip.txt
+++ b/misc/requirements/requirements-pip.txt
@@ -1,8 +1,8 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
appdirs==1.4.3
-packaging==20.1
-pyparsing==2.4.6
-setuptools==45.2.0
+packaging==20.3
+pyparsing==2.4.7
+setuptools==46.1.3
six==1.14.0
wheel==0.34.2
diff --git a/misc/requirements/requirements-pylint.txt b/misc/requirements/requirements-pylint.txt
index a3f5be04a..24b34ed66 100644
--- a/misc/requirements/requirements-pylint.txt
+++ b/misc/requirements/requirements-pylint.txt
@@ -1,23 +1,23 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
astroid==2.3.3
-certifi==2019.11.28
+certifi==2020.4.5.1
cffi==1.14.0
chardet==3.0.4
-cryptography==2.8
+cryptography==2.9
github3.py==1.3.0
-idna==2.8
+idna==2.9
isort==4.3.21
-jwcrypto==0.6.0
+jwcrypto==0.7
lazy-object-proxy==1.4.3
mccabe==0.6.1
-pycparser==2.19
+pycparser==2.20
pylint==2.4.4
python-dateutil==2.8.1
./scripts/dev/pylint_checkers
-requests==2.22.0
+requests==2.23.0
six==1.14.0
typed-ast==1.4.1 ; python_version<"3.8"
uritemplate==3.0.1
-urllib3==1.25.8
-wrapt==1.11.2
+urllib3==1.25.9
+wrapt==1.12.1
diff --git a/misc/requirements/requirements-pyqt-5.12.txt b/misc/requirements/requirements-pyqt-5.12.txt
index 51d019f6f..b1be83265 100644
--- a/misc/requirements/requirements-pyqt-5.12.txt
+++ b/misc/requirements/requirements-pyqt-5.12.txt
@@ -1,5 +1,5 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt5==5.12.3 # rq.filter: < 5.13
-PyQt5-sip==12.7.1
+PyQt5-sip==12.7.2
PyQtWebEngine==5.12.1 # rq.filter: < 5.13
diff --git a/misc/requirements/requirements-pyqt-5.13.txt b/misc/requirements/requirements-pyqt-5.13.txt
index e80cf79ee..dc2f0359a 100644
--- a/misc/requirements/requirements-pyqt-5.13.txt
+++ b/misc/requirements/requirements-pyqt-5.13.txt
@@ -1,5 +1,5 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt5==5.13.2 # rq.filter: < 5.14
-PyQt5-sip==12.7.1
+PyQt5-sip==12.7.2
PyQtWebEngine==5.13.2 # rq.filter: < 5.14
diff --git a/misc/requirements/requirements-pyqt-5.14.txt b/misc/requirements/requirements-pyqt-5.14.txt
index a0107262e..7640a8adb 100644
--- a/misc/requirements/requirements-pyqt-5.14.txt
+++ b/misc/requirements/requirements-pyqt-5.14.txt
@@ -1,5 +1,5 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
-PyQt5==5.14.1 # rq.filter: < 5.15
-PyQt5-sip==12.7.1
+PyQt5==5.14.2 # rq.filter: < 5.15
+PyQt5-sip==12.7.2
PyQtWebEngine==5.14.0 # rq.filter: < 5.15
diff --git a/misc/requirements/requirements-pyqt.txt b/misc/requirements/requirements-pyqt.txt
index b0aedce2b..90febc2e7 100644
--- a/misc/requirements/requirements-pyqt.txt
+++ b/misc/requirements/requirements-pyqt.txt
@@ -1,5 +1,5 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
-PyQt5==5.14.1
-PyQt5-sip==12.7.1
+PyQt5==5.14.2
+PyQt5-sip==12.7.2
PyQtWebEngine==5.14.0
diff --git a/misc/requirements/requirements-pyroma.txt b/misc/requirements/requirements-pyroma.txt
index e224b1431..6b131e155 100644
--- a/misc/requirements/requirements-pyroma.txt
+++ b/misc/requirements/requirements-pyroma.txt
@@ -1,5 +1,5 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
docutils==0.16
-Pygments==2.5.2
+Pygments==2.6.1
pyroma==2.6
diff --git a/misc/requirements/requirements-sphinx.txt b/misc/requirements/requirements-sphinx.txt
index b0217a3a3..4d2676ba7 100644
--- a/misc/requirements/requirements-sphinx.txt
+++ b/misc/requirements/requirements-sphinx.txt
@@ -2,25 +2,25 @@
alabaster==0.7.12
Babel==2.8.0
-certifi==2019.11.28
+certifi==2020.4.5.1
chardet==3.0.4
docutils==0.16
-idna==2.8
+idna==2.9
imagesize==1.2.0
-Jinja2==2.11.1
+Jinja2==2.11.2
MarkupSafe==1.1.1
-packaging==20.1
-Pygments==2.5.2
-pyparsing==2.4.6
+packaging==20.3
+Pygments==2.6.1
+pyparsing==2.4.7
pytz==2019.3
-requests==2.22.0
+requests==2.23.0
six==1.14.0
snowballstemmer==2.0.0
-Sphinx==2.4.0
-sphinxcontrib-applehelp==1.0.1
-sphinxcontrib-devhelp==1.0.1
-sphinxcontrib-htmlhelp==1.0.2
+Sphinx==3.0.2
+sphinxcontrib-applehelp==1.0.2
+sphinxcontrib-devhelp==1.0.2
+sphinxcontrib-htmlhelp==1.0.3
sphinxcontrib-jsmath==1.0.1
-sphinxcontrib-qthelp==1.0.2
-sphinxcontrib-serializinghtml==1.1.3
-urllib3==1.25.8
+sphinxcontrib-qthelp==1.0.3
+sphinxcontrib-serializinghtml==1.1.4
+urllib3==1.25.9
diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt
index 480d8c726..49d43d5d6 100644
--- a/misc/requirements/requirements-tests.txt
+++ b/misc/requirements/requirements-tests.txt
@@ -1,46 +1,47 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
attrs==19.3.0
-beautifulsoup4==4.8.2
+beautifulsoup4==4.9.0
cheroot==8.3.0
-Click==7.0
+click==7.1.1
# colorama==0.4.3
-coverage==5.0.3
+coverage==5.1
EasyProcess==0.2.10
-Flask==1.1.1
+Flask==1.1.2
glob2==0.7
hunter==3.1.3
-hypothesis==5.5.1
+hypothesis==5.10.1
itsdangerous==1.1.0
jaraco.functools==3.0.0 ; python_version>="3.6"
-# Jinja2==2.11.1
-Mako==1.1.1
+# Jinja2==2.11.2
+Mako==1.1.2
manhole==1.6.0
# MarkupSafe==1.1.1
more-itertools==8.2.0
-packaging==20.1
-parse==1.14.0
+packaging==20.3
+parse==1.15.0
parse-type==0.5.2
pluggy==0.13.1
py==1.8.1
py-cpuinfo==5.0.0
-pyparsing==2.4.6
-pytest==5.3.5
+Pygments==2.6.1
+pyparsing==2.4.7
+pytest==5.4.1
pytest-bdd==3.2.1
pytest-benchmark==3.2.3
pytest-cov==2.8.1
pytest-instafail==0.4.1.post0
-pytest-mock==2.0.0
+pytest-mock==3.1.0
pytest-qt==3.3.0
pytest-repeat==0.8.0
-pytest-rerunfailures==8.0
+pytest-rerunfailures==9.0
pytest-travis-fold==1.3.0
pytest-xvfb==1.2.0
PyVirtualDisplay==0.2.5
six==1.14.0
sortedcontainers==2.1.0
-soupsieve==1.9.5
-vulture==1.3
-wcwidth==0.1.8
-Werkzeug==1.0.0
+soupsieve==2.0
+vulture==1.4
+wcwidth==0.1.9
+Werkzeug==1.0.1
jaraco.functools==2.0; python_version<"3.6" # rq.filter: <= 2.0
diff --git a/misc/requirements/requirements-tests.txt-raw b/misc/requirements/requirements-tests.txt-raw
index 33880416c..1b972ba18 100644
--- a/misc/requirements/requirements-tests.txt-raw
+++ b/misc/requirements/requirements-tests.txt-raw
@@ -16,6 +16,7 @@ pytest-rerunfailures
pytest-travis-fold
pytest-xvfb
vulture
+pygments
#@ markers: jaraco.functools python_version>="3.6"
#@ add: jaraco.functools==2.0; python_version<"3.6" # rq.filter: <= 2.0
diff --git a/misc/requirements/requirements-tox.txt b/misc/requirements/requirements-tox.txt
index 31771fad6..1c8ada351 100644
--- a/misc/requirements/requirements-tox.txt
+++ b/misc/requirements/requirements-tox.txt
@@ -1,13 +1,15 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
+appdirs==1.4.3
+distlib==0.3.0
filelock==3.0.12
-packaging==20.1
+packaging==20.3
pluggy==0.13.1
py==1.8.1
-pyparsing==2.4.6
+pyparsing==2.4.7
six==1.14.0
toml==0.10.0
-tox==3.14.3
-tox-pip-version==0.0.6
+tox==3.14.6
+tox-pip-version==0.0.7
tox-venv==0.4.0
-virtualenv==20.0.1
+virtualenv==20.0.18
diff --git a/misc/requirements/requirements-vulture.txt b/misc/requirements/requirements-vulture.txt
index 501dcd973..c5c343f9e 100644
--- a/misc/requirements/requirements-vulture.txt
+++ b/misc/requirements/requirements-vulture.txt
@@ -1,3 +1,3 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
-vulture==1.3
+vulture==1.4
diff --git a/misc/userscripts/qute-bitwarden b/misc/userscripts/qute-bitwarden
index f6212d35a..646b25ba7 100755
--- a/misc/userscripts/qute-bitwarden
+++ b/misc/userscripts/qute-bitwarden
@@ -27,6 +27,9 @@ USAGE = """The domain of the site has to be in the name of the Bitwarden entry,
"websites/github.com". 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.
+If enabled, with the `--totp` flag, it will also move the TOTP code to the
+clipboard, much like the Firefox add-on.
+
You must log into Bitwarden CLI using `bw login` prior to use of this script.
The session key will be stored using keyctl for the number of seconds passed to
the --auto-lock option.
@@ -34,8 +37,9 @@ the --auto-lock option.
To use in qutebrowser, run: `spawn --userscript qute-bitwarden`
"""
-EPILOG = """Dependencies: tldextract (Python 3 module), Bitwarden CLI (1.7.4 is
-known to work but older versions may well also work)
+EPILOG = """Dependencies: tldextract (Python 3 module), pyperclip (optional
+Python module, used for TOTP codes), Bitwarden CLI (1.7.4 is known to work
+but older versions may well also work)
WARNING: The login details are viewable as plaintext in qutebrowser's debug log
(qute://log) and might be shared if you decide to submit a crash report!"""
@@ -62,6 +66,8 @@ argument_parser.add_argument('--dmenu-invocation', '-d', default='rofi -dmenu -i
help='Invocation used to execute a dmenu-provider')
argument_parser.add_argument('--no-insert-mode', '-n', dest='insert_mode', action='store_false',
help="Don't automatically enter insert mode")
+argument_parser.add_argument('--totp', '-t', action='store_true',
+ help="Copy TOTP key to clipboard")
argument_parser.add_argument('--io-encoding', '-i', default='UTF-8',
help='Encoding used to communicate with subprocesses')
argument_parser.add_argument('--merge-candidates', '-m', action='store_true',
@@ -73,6 +79,8 @@ 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('--totp-only', '-T',
+ action='store_true', help='Only insert totp code')
stderr = functools.partial(print, file=sys.stderr)
@@ -158,6 +166,26 @@ def pass_(domain, encoding, auto_lock):
return out
+def get_totp_code(selection_id, domain_name, encoding, auto_lock):
+ session_key = get_session_key(auto_lock)
+ process = subprocess.run(
+ ['bw', 'get', 'totp', '--session', session_key, selection_id],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ )
+
+ err = process.stderr.decode(encoding).strip()
+ if err:
+ # domain_name instead of selection_id to make it more user-friendly
+ msg = 'Bitwarden CLI returned for {:s} - {:s}'.format(domain_name, err)
+ stderr(msg)
+ return '[]'
+
+ out = process.stdout.decode(encoding).strip()
+
+ return out
+
+
def dmenu(items, invocation, encoding):
command = shlex.split(invocation)
process = subprocess.run(command, input='\n'.join(
@@ -227,11 +255,22 @@ def main(arguments):
username = selection['login']['username']
password = selection['login']['password']
+ totp = selection['login']['totp']
if arguments.username_only:
fake_key_raw(username)
elif arguments.password_only:
fake_key_raw(password)
+ elif arguments.totp_only:
+ # No point in moving it to the clipboard in this case
+ fake_key_raw(
+ get_totp_code(
+ selection['id'],
+ selection['name'],
+ arguments.io_encoding,
+ arguments.auto_lock
+ )
+ )
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
@@ -243,6 +282,20 @@ def main(arguments):
if arguments.insert_mode:
qute_command('enter-mode insert')
+ # If it finds a TOTP code, it copies it to the clipboard,
+ # which is the same behaviour as the Firefox add-on.
+ if not arguments.totp_only and totp and arguments.totp:
+ # The import is done here, to make pyperclip an optional dependency
+ import pyperclip
+ pyperclip.copy(
+ get_totp_code(
+ selection['id'],
+ selection['name'],
+ arguments.io_encoding,
+ arguments.auto_lock
+ )
+ )
+
return ExitCodes.SUCCESS
diff --git a/misc/userscripts/readability-js b/misc/userscripts/readability-js
index efd086ce0..d800849a4 100755
--- a/misc/userscripts/readability-js
+++ b/misc/userscripts/readability-js
@@ -46,6 +46,16 @@ const HEADER = `
line-height: 1.2;
}
</style>
+ <!-- This icon is licensed under the Mozilla Public License 2.0 (available at: https://www.mozilla.org/en-US/MPL/2.0/).
+ The original icon can be found here: https://dxr.mozilla.org/mozilla-central/source/browser/themes/shared/reader/readerMode.svg -->
+ <link rel="shortcut icon" href="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjRweCIgaGVpZ2h0PSI2NHB4IiB2ZXJzaW9uPSIxLjEiI
+ HZpZXdCb3g9IjAgMCA2NCA2NCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGcgZmlsbD0iI2ZmZiI+CjxwYXRoIGQ9Im01MiAwaC00
+ MGMtNC40MiAwLTggMy41OC04IDh2NDhjMCA0LjQyIDMuNTggOCA4IDhoNDBjNC40MiAwIDgtMy41OCA4LTh2LTQ4YzAtNC40Mi0zLjU4LTgtOC04em0wIDU
+ yYzAgMi4yMS0xLjc5IDQtNCA0aC0zMmMtMi4yMSAwLTQtMS43OS00LTR2LTQwYzAtMi4yMSAxLjc5LTQgNC00aDMyYzIuMjEgMCA0IDEuNzkgNCA0em0tMT
+ AtMzZoLTIwYy0xLjExIDAtMiAwLjg5NS0yIDJzMC44OTUgMiAyIDJoMjBjMS4xMSAwIDItMC44OTUgMi0ycy0wLjg5NS0yLTItMnptMCA4aC0yMGMtMS4xM
+ SAwLTIgMC44OTUtMiAyczAuODk1IDIgMiAyaDIwYzEuMTEgMCAyLTAuODk1IDItMnMtMC44OTUtMi0yLTJ6bTAgOGgtMjBjLTEuMTEgMC0yIDAuODk1LTIg
+ MnMwLjg5NSAyIDIgMmgyMGMxLjExIDAgMi0wLjg5NSAyLTJzLTAuODk1LTItMi0yem0tMTIgOGgtOGMtMS4xMSAwLTIgMC44OTUtMiAyczAuODk1IDIgMiA
+ yaDhjMS4xMSAwIDItMC44OTUgMi0ycy0wLjg5NS0yLTItMnoiIGZpbGw9IiNmZmYiLz4KPC9nPgo8L3N2Zz4K"/>
</head>`;
const scriptsDir = path.join(process.env.QUTE_DATA_DIR, 'userscripts');
const tmpFile = path.join(scriptsDir, '/readability.html');
@@ -66,6 +76,6 @@ JSDOM.fromFile(process.env.QUTE_HTML, domOpts).then(dom => {
return 1;
}
// Success
- qute.open(['-t', tmpFile]);
+ qute.open(['-t', '-r', tmpFile]);
})
});
diff --git a/pytest.ini b/pytest.ini
index d2088b2ab..e85f2b298 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -1,6 +1,6 @@
[pytest]
log_level = NOTSET
-addopts = --strict -rfEw --instafail --benchmark-columns=Min,Max,Median
+addopts = --strict --instafail --benchmark-columns=Min,Max,Median
testpaths = tests
markers =
gui: Tests using the GUI (e.g. spawning widgets)
@@ -68,5 +68,8 @@ qt_log_ignore =
^DirectWrite: CreateFontFaceFromHDC\(\) failed .*
^Attribute Qt::AA_ShareOpenGLContexts must be set before QCoreApplication is created\.
xfail_strict = true
-filterwarnings = error
+filterwarnings =
+ error
+ # See https://github.com/HypothesisWorks/hypothesis/issues/2370
+ ignore:.*which is reset between function calls but not between test cases generated by:hypothesis.errors.HypothesisDeprecationWarning
faulthandler_timeout = 90
diff --git a/qutebrowser/__init__.py b/qutebrowser/__init__.py
index 41d872d64..fe9d18ed9 100644
--- a/qutebrowser/__init__.py
+++ b/qutebrowser/__init__.py
@@ -26,7 +26,7 @@ __copyright__ = "Copyright 2014-2020 Florian Bruhin (The Compiler)"
__license__ = "GPL"
__maintainer__ = __author__
__email__ = "mail@qutebrowser.org"
-__version__ = "1.10.0"
+__version__ = "1.10.2"
__version_info__ = tuple(int(part) for part in __version__.split('.'))
__description__ = "A keyboard-driven, vim-like browser based on PyQt5."
diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py
index 5a0f8cc27..7e8ec478f 100644
--- a/qutebrowser/browser/browsertab.py
+++ b/qutebrowser/browser/browsertab.py
@@ -317,6 +317,7 @@ class AbstractSearch(QObject):
def search(self, text: str, *,
ignore_case: usertypes.IgnoreCase = usertypes.IgnoreCase.never,
reverse: bool = False,
+ wrap: bool = True,
result_cb: _Callback = None) -> None:
"""Find the given text on the page.
@@ -324,6 +325,7 @@ class AbstractSearch(QObject):
text: The text to search for.
ignore_case: Search case-insensitively.
reverse: Reverse search direction.
+ wrap: Allow wrapping at the top or bottom of the page.
result_cb: Called with a bool indicating whether a match was found.
"""
raise NotImplementedError
diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py
index afa78df01..8f7717ea7 100644
--- a/qutebrowser/browser/commands.py
+++ b/qutebrowser/browser/commands.py
@@ -1005,7 +1005,10 @@ class CommandDispatcher:
@cmdutils.argument('output_messages', flag='m')
def spawn(self, cmdline, userscript=False, verbose=False,
output=False, output_messages=False, detach=False, count=None):
- """Spawn a command in a shell.
+ """Spawn an external command.
+
+ Note that the command is *not* run in a shell, so things like `$VAR` or
+ `> output` won't have the desired effect.
Args:
userscript: Run the command as a userscript. You can use an
@@ -1043,7 +1046,8 @@ class CommandDispatcher:
if userscript:
def _selection_callback(s):
try:
- runner = self._run_userscript(s, cmd, args, verbose, count)
+ runner = self._run_userscript(
+ s, cmd, args, verbose, output_messages, count)
runner.finished.connect(_on_proc_finished)
except cmdutils.CommandError as e:
message.error(str(e))
@@ -1069,13 +1073,15 @@ class CommandDispatcher:
proc.start(cmd, args)
proc.finished.connect(_on_proc_finished)
- def _run_userscript(self, selection, cmd, args, verbose, count):
+ def _run_userscript(self, selection, cmd, args, verbose, output_messages,
+ 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.
+ output_messages: Show the output as messages.
count: Exposed to the userscript.
"""
env = {
@@ -1102,7 +1108,8 @@ class CommandDispatcher:
try:
runner = userscripts.run_async(
- tab, cmd, *args, win_id=self._win_id, env=env, verbose=verbose)
+ tab, cmd, *args, win_id=self._win_id, env=env, verbose=verbose,
+ output_messages=output_messages)
except userscripts.Error as e:
raise cmdutils.CommandError(e)
return runner
@@ -1500,6 +1507,7 @@ class CommandDispatcher:
options = {
'ignore_case': config.val.search.ignore_case,
'reverse': reverse,
+ 'wrap': config.val.search.wrap,
}
self._tabbed_browser.search_text = text
diff --git a/qutebrowser/browser/eventfilter.py b/qutebrowser/browser/eventfilter.py
index cb38cfbc6..c1e970c93 100644
--- a/qutebrowser/browser/eventfilter.py
+++ b/qutebrowser/browser/eventfilter.py
@@ -108,7 +108,14 @@ class TabEventFilter(QObject):
self._check_insertmode_on_release = False
def _handle_mouse_press(self, e):
- """Handle pressing of a mouse button."""
+ """Handle pressing of a mouse button.
+
+ Args:
+ e: The QMouseEvent.
+
+ Return:
+ True if the event should be filtered, False otherwise.
+ """
is_rocker_gesture = (config.val.input.rocker_gestures and
e.buttons() == Qt.LeftButton | Qt.RightButton)
@@ -129,7 +136,14 @@ class TabEventFilter(QObject):
return False
def _handle_mouse_release(self, _e):
- """Handle releasing of a mouse button."""
+ """Handle releasing of a mouse button.
+
+ Args:
+ e: The QMouseEvent.
+
+ Return:
+ True if the event should be filtered, False otherwise.
+ """
# We want to make sure we check the focus element after the WebView is
# updated completely.
QTimer.singleShot(0, self._mouserelease_insertmode)
@@ -140,27 +154,34 @@ class TabEventFilter(QObject):
Args:
e: The QWheelEvent.
+
+ Return:
+ True if the event should be filtered, False otherwise.
"""
if self._ignore_wheel_event:
# See https://github.com/qutebrowser/qutebrowser/issues/395
self._ignore_wheel_event = False
return True
-
- if e.modifiers() & Qt.ControlModifier:
+ elif 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
+ # Disable mouse zooming
+ return True
+
factor = self._tab.zoom.factor() + (e.angleDelta().y() / divider)
if factor < 0:
- return False
+ return True
+
perc = int(100 * factor)
message.info("Zoom level: {}%".format(perc), replace=True)
self._tab.zoom.set_factor(factor)
- elif e.modifiers() & Qt.ShiftModifier:
+ return True
+ elif (e.modifiers() & Qt.ShiftModifier and
+ not qtutils.version_check('5.9', compiled=False)):
if e.angleDelta().y() > 0:
self._tab.scroller.left()
else:
@@ -170,16 +191,30 @@ class TabEventFilter(QObject):
return False
def _handle_context_menu(self, _e):
- """Suppress context menus if rocker gestures are turned on."""
+ """Suppress context menus if rocker gestures are turned on.
+
+ Args:
+ e: The QContextMenuEvent.
+
+ Return:
+ True if the event should be filtered, False otherwise.
+ """
return config.val.input.rocker_gestures
def _handle_key_release(self, e):
"""Ignore repeated key release events going to the website.
WORKAROUND for https://bugreports.qt.io/browse/QTBUG-77208
+
+ Args:
+ e: The QKeyEvent.
+
+ Return:
+ True if the event should be filtered, False otherwise.
"""
return (e.isAutoRepeat() and
- qtutils.version_check('5.10') and
+ qtutils.version_check('5.10', compiled=False) and
+ not qtutils.version_check('5.14', compiled=False) and
objects.backend == usertypes.Backend.QtWebEngine)
def _mousepress_insertmode_cb(self, elem):
@@ -232,6 +267,9 @@ class TabEventFilter(QObject):
Args:
e: The QMouseEvent.
+
+ Return:
+ True if the event should be filtered, False otherwise.
"""
if e.button() in [Qt.XButton1, Qt.LeftButton]:
# Back button on mice which have it, or rocker gesture
@@ -247,7 +285,11 @@ class TabEventFilter(QObject):
message.error("At end of history.")
def eventFilter(self, obj, event):
- """Filter events going to a QWeb(Engine)View."""
+ """Filter events going to a QWeb(Engine)View.
+
+ Return:
+ True if the event should be filtered, False otherwise.
+ """
evtype = event.type()
if evtype not in self._handlers:
return False
diff --git a/qutebrowser/browser/network/proxy.py b/qutebrowser/browser/network/proxy.py
index 6bfd74d36..160660f62 100644
--- a/qutebrowser/browser/network/proxy.py
+++ b/qutebrowser/browser/network/proxy.py
@@ -19,8 +19,6 @@
"""Handling of proxies."""
-import typing
-
from PyQt5.QtCore import QUrl, pyqtSlot
from PyQt5.QtNetwork import QNetworkProxy, QNetworkProxyFactory
diff --git a/qutebrowser/browser/webengine/tabhistory.py b/qutebrowser/browser/webengine/tabhistory.py
index 2690b6dfd..f630e8873 100644
--- a/qutebrowser/browser/webengine/tabhistory.py
+++ b/qutebrowser/browser/webengine/tabhistory.py
@@ -26,6 +26,13 @@ from PyQt5.QtCore import QByteArray, QDataStream, QIODevice, QUrl
from qutebrowser.utils import qtutils
+# kHistoryStreamVersion = 3 was originally set when history serializing was
+# implemented in QtWebEngine:
+# https://codereview.qt-project.org/c/qt/qtwebengine/+/81529
+#
+# Qt 5.14 added version 4 which also serializes favicons:
+# https://codereview.qt-project.org/c/qt/qtwebengine/+/279407
+# However, we don't care about those, so let's keep it at 3.
HISTORY_STREAM_VERSION = 3
@@ -36,32 +43,63 @@ def _serialize_item(item, stream):
item: The WebHistoryItem to write.
stream: The QDataStream to write to.
"""
- ### Thanks to Otter Browser:
- ### https://github.com/OtterBrowser/otter-browser/blob/v0.9.10/src/modules/backends/web/qtwebengine/QtWebEngineWebWidget.cpp#L1210
- ### src/core/web_contents_adapter.cpp serializeNavigationHistory
+ # Thanks to Otter Browser:
+ # https://github.com/OtterBrowser/otter-browser/blob/v1.0.01/src/modules/backends/web/qtwebengine/QtWebEnginePage.cpp#L260
+ #
+ # Relevant QtWebEngine source:
+ # src/core/web_contents_adapter.cpp serializeNavigationHistory
+ #
+ # Sample data:
+ # [TabHistoryItem(active=True,
+ # original_url=QUrl('file:///home/florian/proj/qutebrowser/git/tests/end2end/data/numbers/1.txt'),
+ # title='1.txt',
+ # url=QUrl('file:///home/florian/proj/qutebrowser/git/tests/end2end/data/numbers/1.txt'),
+ # user_data={'zoom': 1.0, 'scroll-pos': QPoint()})]
+
## toQt(entry->GetVirtualURL());
+ # \x00\x00\x00Jfile:///home/florian/proj/qutebrowser/git/tests/end2end/data/numbers/1.txt
qtutils.serialize_stream(stream, item.url)
+
## toQt(entry->GetTitle());
+ # \x00\x00\x00\n\x001\x00.\x00t\x00x\x00t
stream.writeQString(item.title)
+
## QByteArray(encodedPageState.data(), encodedPageState.size());
+ # \xff\xff\xff\xff
qtutils.serialize_stream(stream, QByteArray())
+
## static_cast<qint32>(entry->GetTransitionType());
# chromium/ui/base/page_transition_types.h
+ # \x00\x00\x00\x00
stream.writeInt32(0) # PAGE_TRANSITION_LINK
+
## entry->GetHasPostData();
+ # \x00
stream.writeBool(False)
+
## toQt(entry->GetReferrer().url);
+ # \xff\xff\xff\xff
qtutils.serialize_stream(stream, QUrl())
+
## static_cast<qint32>(entry->GetReferrer().policy);
# chromium/third_party/WebKit/public/platform/WebReferrerPolicy.h
+ # \x00\x00\x00\x00
stream.writeInt32(0) # WebReferrerPolicyAlways
+
## toQt(entry->GetOriginalRequestURL());
+ # \x00\x00\x00Jfile:///home/florian/proj/qutebrowser/git/tests/end2end/data/numbers/1.txt
qtutils.serialize_stream(stream, item.original_url)
+
## entry->GetIsOverridingUserAgent();
+ # \x00
stream.writeBool(False)
+
## static_cast<qint64>(entry->GetTimestamp().ToInternalValue());
+ # \x00\x00\x00\x00^\x97$\xe7
stream.writeInt64(int(time.time()))
+
## entry->GetHttpStatusCode();
+ # \x00\x00\x00\xc8
stream.writeInt(200)
@@ -102,12 +140,13 @@ def serialize(items):
current_idx = -1
### src/core/web_contents_adapter.cpp serializeNavigationHistory
+ # sample data:
# kHistoryStreamVersion
- stream.writeInt(HISTORY_STREAM_VERSION)
+ stream.writeInt(HISTORY_STREAM_VERSION) # \x00\x00\x00\x03
# count
- stream.writeInt(len(items))
+ stream.writeInt(len(items)) # \x00\x00\x00\x01
# currentIndex
- stream.writeInt(current_idx)
+ stream.writeInt(current_idx) # \x00\x00\x00\x00
for item in items:
_serialize_item(item, stream)
diff --git a/qutebrowser/browser/webengine/webenginesettings.py b/qutebrowser/browser/webengine/webenginesettings.py
index a1c1a40f9..f02b80061 100644
--- a/qutebrowser/browser/webengine/webenginesettings.py
+++ b/qutebrowser/browser/webengine/webenginesettings.py
@@ -36,7 +36,7 @@ from qutebrowser.browser.webengine import spell, webenginequtescheme
from qutebrowser.config import config, websettings
from qutebrowser.config.websettings import AttributeInfo as Attr
from qutebrowser.utils import (utils, standarddir, qtutils, message, log,
- urlmatch)
+ urlmatch, usertypes)
# The default QWebEngineProfile
default_profile = typing.cast(QWebEngineProfile, None)
@@ -76,6 +76,10 @@ class _SettingsWrapper:
for settings in self._settings:
settings.setDefaultTextEncoding(encoding)
+ def setUnknownUrlSchemePolicy(self, policy):
+ for settings in self._settings:
+ settings.setUnknownUrlSchemePolicy(policy)
+
def testAttribute(self, attribute):
return self._settings[0].testAttribute(attribute)
@@ -88,6 +92,9 @@ class _SettingsWrapper:
def defaultTextEncoding(self):
return self._settings[0].defaultTextEncoding()
+ def unknownUrlSchemePolicy(self):
+ return self._settings[0].unknownUrlSchemePolicy()
+
class WebEngineSettings(websettings.AbstractSettings):
@@ -151,6 +158,19 @@ class WebEngineSettings(websettings.AbstractSettings):
'fonts.web.family.fantasy': QWebEngineSettings.FantasyFont,
}
+ # Only Qt >= 5.11 support UnknownUrlSchemePolicy
+ try:
+ _UNKNOWN_URL_SCHEME_POLICY = {
+ 'disallow':
+ QWebEngineSettings.DisallowUnknownUrlSchemes,
+ 'allow-from-user-interaction':
+ QWebEngineSettings.AllowUnknownUrlSchemesFromUserInteraction,
+ 'allow-all':
+ QWebEngineSettings.AllowAllUnknownUrlSchemes,
+ }
+ except AttributeError:
+ _UNKNOWN_URL_SCHEME_POLICY = None
+
# Mapping from WebEngineSettings::initDefaults in
# qtwebengine/src/core/web_engine_settings.cpp
_FONT_TO_QFONT = {
@@ -162,6 +182,33 @@ class WebEngineSettings(websettings.AbstractSettings):
QWebEngineSettings.FantasyFont: QFont.Fantasy,
}
+ def set_unknown_url_scheme_policy(
+ self, policy: typing.Union[str, usertypes.Unset]) -> bool:
+ """Set the UnknownUrlSchemePolicy to use.
+
+ Return:
+ True if there was a change, False otherwise.
+ """
+ old_value = self._settings.unknownUrlSchemePolicy()
+ if isinstance(policy, usertypes.Unset):
+ self._settings.resetUnknownUrlSchemePolicy()
+ new_value = self._settings.unknownUrlSchemePolicy()
+ else:
+ new_value = self._UNKNOWN_URL_SCHEME_POLICY[policy]
+ self._settings.setUnknownUrlSchemePolicy(new_value)
+ return old_value != new_value
+
+ def _update_setting(self, setting, value):
+ if setting == 'content.unknown_url_scheme_policy':
+ if self._UNKNOWN_URL_SCHEME_POLICY:
+ return self.set_unknown_url_scheme_policy(value)
+ return False
+ return super()._update_setting(setting, value)
+
+ def init_settings(self):
+ super().init_settings()
+ self.update_setting('content.unknown_url_scheme_policy')
+
def __init__(self, settings):
super().__init__(settings)
# Attributes which don't exist in all Qt versions.
diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py
index 970ef6d45..8b26f2136 100644
--- a/qutebrowser/browser/webengine/webenginetab.py
+++ b/qutebrowser/browser/webengine/webenginetab.py
@@ -24,7 +24,6 @@ import functools
import re
import html as html_utils
import typing
-import textwrap
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, Qt, QPoint, QPointF, QUrl,
QTimer, QObject)
@@ -157,6 +156,78 @@ class WebEnginePrinting(browsertab.AbstractPrinting):
self._widget.page().print(printer, callback)
+class _WebEngineSearchWrapHandler:
+
+ """QtWebEngine implementations related to wrapping when searching.
+
+ Attributes:
+ flag_wrap: An additional flag indicating whether the last search
+ used wrapping.
+ _active_match: The 1-based index of the currently active match
+ on the page.
+ _total_matches: The total number of search matches on the page.
+ _nowrap_available: Whether the functionality to prevent wrapping
+ is available.
+ """
+
+ def __init__(self):
+ self._active_match = 0
+ self._total_matches = 0
+ self.flag_wrap = True
+ self._nowrap_available = False
+
+ def connect_signal(self, page):
+ """Connect to the findTextFinished signal of the page.
+
+ Args:
+ page: The QtWebEnginePage to connect to this handler.
+ """
+ if qtutils.version_check("5.14"):
+ page.findTextFinished.connect(self._store_match_data)
+ self._nowrap_available = True
+
+ def _store_match_data(self, result):
+ """Store information on the last match.
+
+ The information will be checked against when wrapping is turned off.
+
+ Args:
+ result: A FindTextResult passed by the findTextFinished signal.
+ """
+ self._active_match = result.activeMatch()
+ self._total_matches = result.numberOfMatches()
+ log.webview.debug("Active search match: {}/{}"
+ .format(self._active_match, self._total_matches))
+
+ def reset_match_data(self):
+ """Reset match information.
+
+ Stale information could lead to next_result or prev_result misbehaving.
+ """
+ self._active_match = 0
+ self._total_matches = 0
+
+ def prevent_wrapping(self, *, going_up):
+ """Prevent wrapping if possible and required.
+
+ Returns True if a wrap was prevented and False if not.
+
+ Args:
+ going_up: Whether the search would scroll the page up or down.
+ """
+ if (not self._nowrap_available or
+ self.flag_wrap or self._total_matches == 0):
+ return False
+ elif going_up and self._active_match == 1:
+ message.info("Search hit TOP")
+ return True
+ elif not going_up and self._active_match == self._total_matches:
+ message.info("Search hit BOTTOM")
+ return True
+ else:
+ return False
+
+
class WebEngineSearch(browsertab.AbstractSearch):
"""QtWebEngine implementations related to searching on the page.
@@ -171,6 +242,11 @@ class WebEngineSearch(browsertab.AbstractSearch):
super().__init__(tab, parent)
self._flags = QWebEnginePage.FindFlags(0) # type: ignore
self._pending_searches = 0
+ # The API necessary to stop wrapping was added in this version
+ self._wrap_handler = _WebEngineSearchWrapHandler()
+
+ def connect_signals(self):
+ self._wrap_handler.connect_signal(self._widget.page())
def _find(self, text, flags, callback, caller):
"""Call findText on the widget."""
@@ -208,10 +284,10 @@ class WebEngineSearch(browsertab.AbstractSearch):
callback(found)
self.finished.emit(found)
- self._widget.findText(text, flags, wrapped_callback)
+ self._widget.page().findText(text, flags, wrapped_callback)
def search(self, text, *, ignore_case=usertypes.IgnoreCase.never,
- reverse=False, result_cb=None):
+ reverse=False, wrap=True, result_cb=None):
# Don't go to next entry on duplicate search
if self.text == text and self.search_displayed:
log.webview.debug("Ignoring duplicate search request"
@@ -220,6 +296,8 @@ class WebEngineSearch(browsertab.AbstractSearch):
self.text = text
self._flags = QWebEnginePage.FindFlags(0) # type: ignore
+ self._wrap_handler.reset_match_data()
+ self._wrap_handler.flag_wrap = wrap
if self._is_case_sensitive(ignore_case):
self._flags |= QWebEnginePage.FindCaseSensitively
if reverse:
@@ -231,18 +309,26 @@ class WebEngineSearch(browsertab.AbstractSearch):
if self.search_displayed:
self.cleared.emit()
self.search_displayed = False
- self._widget.findText('')
+ self._wrap_handler.reset_match_data()
+ self._widget.page().findText('')
def prev_result(self, *, result_cb=None):
# The int() here makes sure we get a copy of the flags.
flags = QWebEnginePage.FindFlags(int(self._flags)) # type: ignore
if flags & QWebEnginePage.FindBackward:
+ if self._wrap_handler.prevent_wrapping(going_up=False):
+ return
flags &= ~QWebEnginePage.FindBackward
else:
+ if self._wrap_handler.prevent_wrapping(going_up=True):
+ return
flags |= QWebEnginePage.FindBackward
self._find(self.text, flags, result_cb, 'prev_result')
def next_result(self, *, result_cb=None):
+ going_up = self._flags & QWebEnginePage.FindBackward
+ if self._wrap_handler.prevent_wrapping(going_up=going_up):
+ return
self._find(self.text, self._flags, result_cb, 'next_result')
@@ -815,9 +901,11 @@ class _WebEnginePermissions(QObject):
self._tab.data.fullscreen = on
self._tab.fullscreen_requested.emit(on)
if on:
- notification = miscwidgets.FullscreenNotification(self._widget)
- notification.show()
- notification.set_timeout(3000)
+ timeout = config.val.content.fullscreen.overlay_timeout
+ if timeout != 0:
+ notification = miscwidgets.FullscreenNotification(self._widget)
+ notification.set_timeout(timeout)
+ notification.show()
@pyqtSlot(QUrl, 'QWebEnginePage::Feature')
def _on_feature_permission_requested(self, url, feature):
@@ -1104,25 +1192,16 @@ class _WebEngineScripts(QObject):
if not config.val.content.site_specific_quirks:
return
- # WhatsApp Web, based on:
- # https://github.com/jiahaog/nativefier/issues/719#issuecomment-443809630
- script = QWebEngineScript()
- script.setName('quirk-whatsapp')
- script.setWorldId(QWebEngineScript.ApplicationWorld)
- script.setInjectionPoint(QWebEngineScript.DocumentReady)
- script.setSourceCode(textwrap.dedent(r"""
- // ==UserScript==
- // @include https://web.whatsapp.com/
- // ==/UserScript==
- if (document.body.innerText.replace(/\n/g, ' ').search(
- /whatsapp works with.*to use whatsapp.*update/i) !== -1) {
- navigator.serviceWorker.getRegistration().then(function (r) {
- r.unregister();
- document.location.reload();
- });
- }
- """))
- self._widget.page().scripts().insert(script)
+ page_scripts = self._widget.page().scripts()
+
+ for filename in ['whatsapp_web_quirk']:
+ script = QWebEngineScript()
+ script.setName(filename)
+ script.setWorldId(QWebEngineScript.ApplicationWorld)
+ script.setInjectionPoint(QWebEngineScript.DocumentReady)
+ src = utils.read_file("javascript/{}.user.js".format(filename))
+ script.setSourceCode(src)
+ page_scripts.insert(script)
class WebEngineTabPrivate(browsertab.AbstractTabPrivate):
@@ -1410,14 +1489,18 @@ class WebEngineTab(browsertab.AbstractTab):
Needs to check the page content as a WORKAROUND for
https://bugreports.qt.io/browse/QTBUG-66661
"""
+ match = re.search(r'"errorCode":"([^"]*)"', html)
+ if match is None:
+ return
+
+ error = match.group(1)
+ log.webview.error("Load error: {}".format(error))
+
missing_jst = 'jstProcess(' in html and 'jstProcess=' not in html
if js_enabled and not missing_jst:
return
- match = re.search(r'"errorCode":"([^"]*)"', html)
- if match is None:
- return
- self._show_error_page(self.url(), error=match.group(1))
+ self._show_error_page(self.url(), error=error)
@pyqtSlot(int)
def _on_load_progress(self, perc: int) -> None:
@@ -1649,5 +1732,6 @@ class WebEngineTab(browsertab.AbstractTab):
# pylint: disable=protected-access
self.audio._connect_signals()
+ self.search.connect_signals()
self._permissions.connect_signals()
self._scripts.connect_signals()
diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py
index 4aa0abcf5..4d412a38b 100644
--- a/qutebrowser/browser/webkit/webkittab.py
+++ b/qutebrowser/browser/webkit/webkittab.py
@@ -125,7 +125,7 @@ class WebKitSearch(browsertab.AbstractSearch):
self._widget.findText('', QWebPage.HighlightAllOccurrences)
def search(self, text, *, ignore_case=usertypes.IgnoreCase.never,
- reverse=False, result_cb=None):
+ reverse=False, wrap=True, result_cb=None):
# Don't go to next entry on duplicate search
if self.text == text and self.search_displayed:
log.webview.debug("Ignoring duplicate search request"
@@ -137,11 +137,13 @@ class WebKitSearch(browsertab.AbstractSearch):
self.text = text
self.search_displayed = True
- self._flags = QWebPage.FindWrapsAroundDocument
+ self._flags = QWebPage.FindFlags(0) # type: ignore
if self._is_case_sensitive(ignore_case):
self._flags |= QWebPage.FindCaseSensitively
if reverse:
self._flags |= QWebPage.FindBackward
+ if wrap:
+ self._flags |= QWebPage.FindWrapsAroundDocument
# We actually search *twice* - once to highlight everything, then again
# to get a mark so we can navigate.
found = self._widget.findText(text, self._flags)
diff --git a/qutebrowser/commands/userscripts.py b/qutebrowser/commands/userscripts.py
index 9d9ce3c52..f3a706d1a 100644
--- a/qutebrowser/commands/userscripts.py
+++ b/qutebrowser/commands/userscripts.py
@@ -148,7 +148,8 @@ class _BaseUserscriptRunner(QObject):
log.procs.debug("Both text/HTML stored, kicking off userscript!")
self._run_process(*self._args, **self._kwargs)
- def _run_process(self, cmd, *args, env=None, verbose=False):
+ def _run_process(self, cmd, *args, env=None, verbose=False,
+ output_messages=False):
"""Start the given command.
Args:
@@ -156,15 +157,16 @@ class _BaseUserscriptRunner(QObject):
*args: The arguments to hand to the command
env: A dictionary of environment variables to add.
verbose: Show notifications when the command started/exited.
+ output_messages: Show the output as messages.
"""
assert self._filepath is not None
self._env['QUTE_FIFO'] = self._filepath
if env is not None:
self._env.update(env)
- self._proc = guiprocess.GUIProcess('userscript',
- additional_env=self._env,
- verbose=verbose, parent=self)
+ self._proc = guiprocess.GUIProcess(
+ 'userscript', additional_env=self._env,
+ output_messages=output_messages, verbose=verbose, parent=self)
self._proc.finished.connect(self.on_proc_finished)
self._proc.error.connect(self.on_proc_error)
self._proc.start(cmd, args)
@@ -398,7 +400,8 @@ def _lookup_path(cmd):
raise NotFoundError(cmd, directories)
-def run_async(tab, cmd, *args, win_id, env, verbose=False):
+def run_async(tab, cmd, *args, win_id, env, verbose=False,
+ output_messages=False):
"""Run a userscript after dumping page html/source.
Raises:
@@ -413,6 +416,7 @@ def run_async(tab, cmd, *args, win_id, env, verbose=False):
win_id: The window id the userscript is executed in.
env: A dictionary of variables to add to the process environment.
verbose: Show notifications when the command started/exited.
+ output_messages: Show the output as messages.
"""
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
@@ -451,7 +455,8 @@ def run_async(tab, cmd, *args, win_id, env, verbose=False):
runner.finished.connect(commandrunner.deleteLater)
runner.finished.connect(runner.deleteLater)
- runner.prepare_run(cmd_path, *args, env=env, verbose=verbose)
+ runner.prepare_run(cmd_path, *args, env=env, verbose=verbose,
+ output_messages=output_messages)
tab.dump_async(runner.store_html)
tab.dump_async(runner.store_text, plain=True)
return runner
diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml
index cc76c63b9..d055b92ba 100644
--- a/qutebrowser/config/configdata.yml
+++ b/qutebrowser/config/configdata.yml
@@ -48,6 +48,16 @@ search.incremental:
default: True
desc: Find text on a page incrementally, renewing the search for each typed character.
+search.wrap:
+ type: Bool
+ default: True
+ backend:
+ QtWebEngine: Qt 5.14
+ QtWebKit: true
+ desc: >-
+ Wrap around at the top and bottom of the page when advancing through text matches
+ using `:search-next` and `:search-prev`.
+
new_instance_open_target:
type:
name: String
@@ -370,12 +380,46 @@ content.default_encoding:
The encoding must be a string describing an encoding such as _utf-8_,
_iso-8859-1_, etc.
+content.unknown_url_scheme_policy:
+ type:
+ name: String
+ valid_values:
+ - disallow: "Disallows all navigation requests to URLs with unknown
+ schemes."
+ - allow-from-user-interaction: "Allows navigation requests to URLs with
+ unknown schemes that are issued from user-interaction (like a
+ mouse-click), whereas other navigation requests (for example from
+ JavaScript) are suppressed."
+ - allow-all: "Allows all navigation requests to URLs with unknown
+ schemes."
+ default: allow-from-user-interaction
+ backend:
+ QtWebEngine: Qt 5.11
+ QtWebKit: false
+ supports_pattern: true
+ desc: >-
+ How navigation requests to URLs with unknown schemes are handled.
+
content.windowed_fullscreen:
+ renamed: content.fullscreen.window
+
+content.fullscreen.window:
type: Bool
default: false
desc: >-
Limit fullscreen to the browser window (does not expand to fill the screen).
+content.fullscreen.overlay_timeout:
+ type:
+ name: Int
+ minval: 0
+ maxval: maxint
+ default: 3000
+ desc: >-
+ Set fullscreen notification overlay timeout in milliseconds.
+
+ If set to 0, no overlay will be displayed.
+
content.desktop_capture:
type: BoolAsk
default: ask
@@ -1091,6 +1135,22 @@ hints.border:
type: String
desc: CSS border value for hints.
+hints.padding:
+ default:
+ top: 0
+ bottom: 0
+ left: 3
+ right: 3
+ type: Padding
+ desc: Padding (in pixels) for hints.
+
+hints.radius:
+ default: 3
+ type:
+ name: Int
+ minval: 0
+ desc: Rounding radius (in pixels) for the edges of hints.
+
hints.chars:
default: asdfghjkl
type:
@@ -1394,7 +1454,7 @@ messages.unfocused:
prompt.filebrowser:
type: Bool
default: true
- desc: Show a filebrowser in upload/download prompts.
+ desc: Show a filebrowser in download prompts.
prompt.radius:
type:
@@ -1883,12 +1943,24 @@ url.searchengines:
name: String
forbidden: ' '
valtype: SearchEngineUrl
- desc: >-
+ desc: |
Search engines which can be used via the address bar.
Maps a search engine name (such as `DEFAULT`, or `ddg`) to a URL with a
`{}` placeholder. The placeholder will be replaced by the search term, use
- `{{` and `}}` for literal `{`/`}` signs.
+ `{{` and `}}` for literal `{`/`}` braces.
+
+ The following further placeholds are defined to configure how special
+ characters in the search terms are replaced by safe characters (called
+ 'quoting'):
+
+ * `{}` and `{semiquoted}` quote everything except slashes; this is the most
+ sensible choice for almost all search engines (for the search term
+ `slash/and&amp` this placeholder expands to `slash/and%26amp`).
+ * `{quoted}` quotes all characters (for `slash/and&amp` this placeholder
+ expands to `slash%2Fand%26amp`).
+ * `{unquoted}` quotes nothing (for `slash/and&amp` this placeholder
+ expands to `slash/and&amp`).
The search engine named `DEFAULT` is used when `url.auto_search` is turned
on and something else than a URL was entered to be opened. Other search
@@ -2326,7 +2398,7 @@ colors.statusbar.command.private.fg:
desc: Foreground color of the statusbar in private browsing + command mode.
colors.statusbar.command.private.bg:
- default: grey
+ default: darkslategray
type: QssColor
desc: Background color of the statusbar in private browsing + command mode.
diff --git a/qutebrowser/config/configfiles.py b/qutebrowser/config/configfiles.py
index 5dc8a95e5..63cab9377 100644
--- a/qutebrowser/config/configfiles.py
+++ b/qutebrowser/config/configfiles.py
@@ -774,7 +774,7 @@ def init() -> None:
try:
state = StateConfig()
- except configparser.Error as e:
+ except (configparser.Error, UnicodeDecodeError) as e:
msg = "While loading state file from {}".format(standarddir.data())
desc = configexc.ConfigErrorDesc(msg, e)
raise configexc.ConfigFileErrors('state', [desc], fatal=True)
diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py
index befc43806..3d0f5c924 100644
--- a/qutebrowser/config/configtypes.py
+++ b/qutebrowser/config/configtypes.py
@@ -1760,22 +1760,22 @@ class SearchEngineUrl(BaseType):
elif not value:
return None
- if not ('{}' in value or '{0}' in value):
+ if not re.search('{(|0|semiquoted|unquoted|quoted)}', value):
raise configexc.ValidationError(value, "must contain \"{}\"")
try:
- value.format("")
+ format_keys = {
+ 'quoted': "",
+ 'unquoted': "",
+ 'semiquoted': "",
+ }
+ value.format("", **format_keys)
except (KeyError, IndexError):
raise configexc.ValidationError(
value, "may not contain {...} (use {{ and }} for literal {/})")
except ValueError as e:
raise configexc.ValidationError(value, str(e))
- url = QUrl(value.replace('{}', 'foobar'))
- if not url.isValid():
- raise configexc.ValidationError(
- value, "invalid url, {}".format(url.errorString()))
-
return value
diff --git a/qutebrowser/html/warning-webkit.html b/qutebrowser/html/warning-webkit.html
index e87597bd0..7fc22903a 100644
--- a/qutebrowser/html/warning-webkit.html
+++ b/qutebrowser/html/warning-webkit.html
@@ -70,10 +70,10 @@ installed.</p>
<p><b>QtWebEngine being unavailable on Parabola</b>: Claims of Parabola
developers about QtWebEngine being "non-free" have repeatedly been disputed,
and so far nobody came up with solid evidence about that being the case. Also,
-note that their qutebrowser package was often outdated in the past (even
-qutebrowser security fixes took months to arrive there). You might be better
-off chosing an <a href="https://qutebrowser.org/doc/install.html#tox">
-alternative install method</a>.</p>
+note that their qutebrowser package is usually very outdated (even qutebrowser
+security fixes took months to arrive there). You might be better off chosing an
+<a href="https://qutebrowser.org/doc/install.html#tox"> alternative install
+method</a>.</p>
<p><b>White flashing between loads with a custom stylesheet</b>: This doesn't
seem to happen with <span class="mono">qt.process_model = single-process</span>
diff --git a/qutebrowser/javascript/greasemonkey_wrapper.js b/qutebrowser/javascript/greasemonkey_wrapper.js
index 440a11904..7bfabc635 100644
--- a/qutebrowser/javascript/greasemonkey_wrapper.js
+++ b/qutebrowser/javascript/greasemonkey_wrapper.js
@@ -145,47 +145,47 @@
}
};
+ const unsafeWindow = window;
{% if use_proxy %}
- /*
- * Try to give userscripts an environment that they expect. Which
- * seems to be that the global window object should look the same as
- * the page's one and that if a script writes to an attribute of
- * window it should be able to access that variable in the global
- * scope.
- * Use a Proxy to stop scripts from actually changing the global
- * window (that's what unsafeWindow is for).
- * Use the "with" statement to make the proxy provide what looks
- * like global scope.
- *
- * There are other Proxy functions that we may need to override.
- * set, get and has are definitely required.
- */
- const unsafeWindow = window;
- const qute_gm_window_shadow = {}; // stores local changes to window
- const qute_gm_windowProxyHandler = {
- get: function(target, prop) {
- if (prop in qute_gm_window_shadow)
- return qute_gm_window_shadow[prop];
- if (prop in target) {
- if (typeof target[prop] === 'function' && typeof target[prop].prototype == 'undefined')
- // Getting TypeError: Illegal Execution when callers try to execute
- // eg addEventListener from here because they were returned
- // unbound
- return target[prop].bind(target);
- return target[prop];
- }
- },
- set: function(target, prop, val) {
- return qute_gm_window_shadow[prop] = val;
- },
- has: function(target, key) {
- return key in qute_gm_window_shadow || key in target;
- }
- };
- const qute_gm_window_proxy = new Proxy(
- unsafeWindow, qute_gm_windowProxyHandler);
-
- with (qute_gm_window_proxy) {
+ /*
+ * Try to give userscripts an environment that they expect. Which seems
+ * to be that the global window object should look the same as the page's
+ * one and that if a script writes to an attribute of window all other
+ * scripts should be able to access that variable in the global scope.
+ * Use a Proxy to stop scripts from actually changing the global window
+ * (that's what unsafeWindow is for). Use the "with" statement to make
+ * the proxy provide what looks like global scope.
+ *
+ * There are other Proxy functions that we may need to override. set,
+ * get and has are definitely required.
+ */
+
+ if (!window._qute_gm_window_proxy) {
+ const qute_gm_window_shadow = {}; // stores local changes to window
+ const qute_gm_windowProxyHandler = {
+ get: function (target, prop) {
+ if (prop in qute_gm_window_shadow)
+ return qute_gm_window_shadow[prop];
+ if (prop in target) {
+ if (typeof target[prop] === 'function' && typeof target[prop].prototype == 'undefined')
+ // Getting TypeError: Illegal Execution when callers try
+ // to execute eg addEventListener from here because they
+ // were returned unbound
+ return target[prop].bind(target);
+ return target[prop];
+ }
+ },
+ set: function(target, prop, val) {
+ return qute_gm_window_shadow[prop] = val;
+ },
+ has: function(target, key) {
+ return key in qute_gm_window_shadow || key in target;
+ }
+ };
+ window._qute_gm_window_proxy = new Proxy(unsafeWindow, qute_gm_windowProxyHandler);
+ }
+ const qute_gm_window_proxy = window._qute_gm_window_proxy;
+ with (qute_gm_window_proxy) {
// We can't return `this` or `qute_gm_window_proxy` from
// `qute_gm_window_proxy.get('window')` because the Proxy implementation
// does typechecking on read-only things. So we have to shadow `window`
@@ -194,10 +194,10 @@
// ====== The actual user script source ====== //
{{ scriptSource }}
// ====== End User Script ====== //
- };
+ };
{% else %}
- // ====== The actual user script source ====== //
+ // ====== The actual user script source ====== //
{{ scriptSource }}
- // ====== End User Script ====== //
+ // ====== End User Script ====== //
{% endif %}
})();
diff --git a/qutebrowser/javascript/whatsapp_web_quirk.user.js b/qutebrowser/javascript/whatsapp_web_quirk.user.js
new file mode 100644
index 000000000..b8979d15e
--- /dev/null
+++ b/qutebrowser/javascript/whatsapp_web_quirk.user.js
@@ -0,0 +1,15 @@
+// ==UserScript==
+// @include https://web.whatsapp.com/
+// ==/UserScript==
+
+// Quirk for WhatsApp Web, based on:
+// https://github.com/jiahaog/nativefier/issues/719#issuecomment-443809630
+
+"use strict";
+
+if (document.querySelector("a[href='https://support.google.com/chrome/answer/95414']")) {
+ navigator.serviceWorker.getRegistration().then((registration) => {
+ registration.unregister();
+ document.location.reload();
+ });
+}
diff --git a/qutebrowser/keyinput/basekeyparser.py b/qutebrowser/keyinput/basekeyparser.py
index 0f8ec421a..536a7e6ee 100644
--- a/qutebrowser/keyinput/basekeyparser.py
+++ b/qutebrowser/keyinput/basekeyparser.py
@@ -208,7 +208,6 @@ class BaseKeyParser(QObject):
- The found binding with Match.definitive.
"""
assert sequence
- assert not isinstance(sequence, str)
return self.bindings.matches(sequence)
def _match_without_modifiers(
@@ -340,7 +339,6 @@ class BaseKeyParser(QObject):
self.bindings = BindingTrie()
for key, cmd in config.key_instance.get_bindings_for(modename).items():
- assert not isinstance(key, str), key
assert cmd
self.bindings[key] = cmd
diff --git a/qutebrowser/keyinput/eventfilter.py b/qutebrowser/keyinput/eventfilter.py
index 7916f16fe..992d9f4ce 100644
--- a/qutebrowser/keyinput/eventfilter.py
+++ b/qutebrowser/keyinput/eventfilter.py
@@ -85,19 +85,22 @@ class EventFilter(QObject):
Return:
True if the event should be filtered, False if it's passed through.
"""
+ if not isinstance(obj, QWindow):
+ # We already handled this same event at some point earlier, so
+ # we're not interested in it anymore.
+ return False
+
+ typ = event.type()
+
+ if typ not in self._handlers:
+ return False
+
+ if not self._activated:
+ return False
+
+ handler = self._handlers[typ]
try:
- if not self._activated:
- return False
- if not isinstance(obj, QWindow):
- # We already handled this same event at some point earlier, so
- # we're not interested in it anymore.
- return False
- try:
- handler = self._handlers[event.type()]
- except KeyError:
- return False
- else:
- return handler(event)
+ return handler(event)
except:
# If there is an exception in here and we leave the eventfilter
# activated, we'll get an infinite loop and a stack overflow.
diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py
index 2cdf64e1f..03762766d 100644
--- a/qutebrowser/mainwindow/mainwindow.py
+++ b/qutebrowser/mainwindow/mainwindow.py
@@ -157,8 +157,11 @@ class MainWindow(QWidget):
color: {{ conf.colors.hints.fg }};
font: {{ conf.fonts.hints }};
border: {{ conf.hints.border }};
- padding-left: 3px;
- padding-right: 3px;
+ border-radius: {{ conf.hints.radius }}px;
+ padding-top: {{ conf.hints.padding['top'] }}px;
+ padding-left: {{ conf.hints.padding['left'] }}px;
+ padding-right: {{ conf.hints.padding['right'] }}px;
+ padding-bottom: {{ conf.hints.padding['bottom'] }}px;
}
QMenu {
@@ -577,7 +580,7 @@ class MainWindow(QWidget):
@pyqtSlot(bool)
def _on_fullscreen_requested(self, on):
- if not config.val.content.windowed_fullscreen:
+ if not config.val.content.fullscreen.window:
if on:
self.state_before_fullscreen = self.windowState()
self.setWindowState(Qt.WindowFullScreen | # type: ignore
diff --git a/qutebrowser/misc/miscwidgets.py b/qutebrowser/misc/miscwidgets.py
index 69bce56f5..39be3f15c 100644
--- a/qutebrowser/misc/miscwidgets.py
+++ b/qutebrowser/misc/miscwidgets.py
@@ -326,7 +326,7 @@ class FullscreenNotification(QLabel):
self.setText("Page is now fullscreen.")
self.resize(self.sizeHint())
- if config.val.content.windowed_fullscreen:
+ if config.val.content.fullscreen.window:
geom = self.parentWidget().geometry()
else:
geom = QApplication.desktop().screenGeometry(self)
diff --git a/qutebrowser/utils/log.py b/qutebrowser/utils/log.py
index 30a399773..922981511 100644
--- a/qutebrowser/utils/log.py
+++ b/qutebrowser/utils/log.py
@@ -105,7 +105,7 @@ def vdebug(self: logging.Logger,
"""
if self.isEnabledFor(VDEBUG_LEVEL):
# pylint: disable=protected-access
- self._log(VDEBUG_LEVEL, msg, args, **kwargs) # type: ignore
+ self._log(VDEBUG_LEVEL, msg, args, **kwargs)
# pylint: enable=protected-access
diff --git a/qutebrowser/utils/urlutils.py b/qutebrowser/utils/urlutils.py
index b0013c195..efd2caf75 100644
--- a/qutebrowser/utils/urlutils.py
+++ b/qutebrowser/utils/urlutils.py
@@ -116,8 +116,13 @@ def _get_search_url(txt: str) -> QUrl:
engine = 'DEFAULT'
if term:
template = config.val.url.searchengines[engine]
+ semiquoted_term = urllib.parse.quote(term)
quoted_term = urllib.parse.quote(term, safe='')
- url = qurl_from_user_input(template.format(quoted_term))
+ evaluated = template.format(semiquoted_term,
+ unquoted=term,
+ quoted=quoted_term,
+ semiquoted=semiquoted_term)
+ url = qurl_from_user_input(evaluated)
else:
url = qurl_from_user_input(config.val.url.searchengines[engine])
url.setPath(None) # type: ignore
diff --git a/qutebrowser/utils/version.py b/qutebrowser/utils/version.py
index 5b0a05c9f..d10c57411 100644
--- a/qutebrowser/utils/version.py
+++ b/qutebrowser/utils/version.py
@@ -90,7 +90,7 @@ def distribution() -> typing.Optional[DistributionInfo]:
with open(filename, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
- if (not line) or line.startswith('#'):
+ if (not line) or line.startswith('#') or '=' not in line:
continue
k, v = line.split("=", maxsplit=1)
info[k] = v.strip('"')
@@ -287,7 +287,7 @@ def _os_info() -> typing.Sequence[str]:
versioninfo = ''
else:
versioninfo = '.'.join(info_tpl)
- osver = ', '.join([e for e in [release, versioninfo, machine] if e])
+ osver = ', '.join(e for e in [release, versioninfo, machine] if e)
elif utils.is_posix:
osver = ' '.join(platform.uname())
else:
@@ -355,8 +355,7 @@ def _chromium_version() -> str:
Qt 5.12: Chromium 69
(LTS) 69.0.3497.113 (2018-09-27)
- 5.12.6: Security fixes up to 76.0.3809.87 (2019-07-30)
- plus fix for CVE-2019-13720 from Chrome 78
+ 5.12.8: Security fixes up to 80.0.3987.149 (2020-03-18)
Qt 5.13: Chromium 73
73.0.3683.105 (~2019-02-28)
@@ -364,7 +363,10 @@ def _chromium_version() -> str:
Qt 5.14: Chromium 77
77.0.3865.129 (~2019-10-10)
- 5.14.1: Security fixes up to 79.0.3945.117 (2020-01-07)
+ 5.14.2: Security fixes up to 80.0.3987.132 (2020-03-03)
+
+ Qt 5.15: Chromium 80
+ 80.0.3987.136 (~2020-03-09)
Also see https://www.chromium.org/developers/calendar
and https://chromereleases.googleblog.com/
@@ -439,6 +441,8 @@ def version() -> str:
if qapp:
style = qapp.style()
lines.append('Style: {}'.format(style.metaObject().className()))
+ platform_name = qapp.platformName()
+ lines.append('Platform plugin: {}'.format(platform_name))
importpath = os.path.dirname(os.path.abspath(qutebrowser.__file__))
diff --git a/requirements.txt b/requirements.txt
index 245a4d38a..59b6bb414 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,8 +3,8 @@
attrs==19.3.0
colorama==0.4.3
cssutils==1.0.2
-Jinja2==2.11.1
+Jinja2==2.11.2
MarkupSafe==1.1.1
-Pygments==2.5.2
+Pygments==2.6.1
pyPEG2==2.15.2
-PyYAML==5.3
+PyYAML==5.3.1
diff --git a/scripts/asciidoc2html.py b/scripts/asciidoc2html.py
index ceac1ff41..d43531ca3 100755
--- a/scripts/asciidoc2html.py
+++ b/scripts/asciidoc2html.py
@@ -263,8 +263,9 @@ class AsciiDoc:
subprocess.run(cmdline, check=True, env=env)
except (subprocess.CalledProcessError, OSError) as e:
self._failed = True
- utils.print_col(str(e), 'red')
- print("Keeping modified sources in {}.".format(self._homedir))
+ utils.print_error(str(e))
+ print("Keeping modified sources in {}.".format(self._homedir),
+ file=sys.stderr)
sys.exit(1)
@@ -291,9 +292,9 @@ def run(**kwargs):
try:
asciidoc.prepare()
except FileNotFoundError:
- utils.print_col("Could not find asciidoc! Please install it, or use "
- "the --asciidoc argument to point this script to the "
- "correct python/asciidoc.py location!", 'red')
+ utils.print_error("Could not find asciidoc! Please install it, or use "
+ "the --asciidoc argument to point this script to "
+ "the correct python/asciidoc.py location!")
sys.exit(1)
try:
diff --git a/scripts/dev/build_pyqt_wheel.py b/scripts/dev/build_pyqt_wheel.py
new file mode 100644
index 000000000..b4a3477c6
--- /dev/null
+++ b/scripts/dev/build_pyqt_wheel.py
@@ -0,0 +1,73 @@
+#!/usr/bin/env python3
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+
+# Copyright 2020 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
+#
+# This file is part of qutebrowser.
+#
+# qutebrowser is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# qutebrowser is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
+
+"""Build updated PyQt wheels."""
+
+import os
+import subprocess
+import argparse
+import sys
+import pathlib
+
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir,
+ os.pardir))
+from scripts import utils
+
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument('qt_location', help='Qt compiler directory')
+ parser.add_argument('--wheels-dir', help='Directory to use for wheels',
+ default='wheels')
+ args = parser.parse_args()
+
+ old_cwd = pathlib.Path.cwd()
+
+ wheels_dir = pathlib.Path(args.wheels_dir).resolve()
+ wheels_dir.mkdir(exist_ok=True)
+
+ if list(wheels_dir.glob('*')):
+ utils.print_col("Wheels directory is not empty, "
+ "unexpected behavior might occur!", 'yellow')
+
+ os.chdir(wheels_dir)
+
+ utils.print_title("Downloading wheels")
+ subprocess.run([sys.executable, '-m', 'pip', 'download',
+ '--no-deps', '--only-binary', 'PyQt5,PyQtWebEngine',
+ 'PyQt5', 'PyQtWebEngine'], check=True)
+
+ utils.print_title("Patching wheels")
+ input_files = wheels_dir.glob('*.whl')
+ for wheel in input_files:
+ utils.print_subtitle(wheel.stem.split('-')[0])
+ bin_path = pathlib.Path(sys.executable).parent
+ subprocess.run([str(bin_path / 'pyqt-bundle'),
+ '--qt-dir', args.qt_location, str(wheel)],
+ check=True)
+ wheel.unlink()
+
+ print("Done, output files:")
+ for wheel in wheels_dir.glob('*.whl'):
+ print(wheel.relative_to(old_cwd))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/scripts/dev/build_release.py b/scripts/dev/build_release.py
index 95ce66473..68befff65 100755
--- a/scripts/dev/build_release.py
+++ b/scripts/dev/build_release.py
@@ -410,13 +410,21 @@ def github_upload(artifacts, tag):
for filename, mimetype, description in artifacts:
while True:
print("Uploading {}".format(filename))
+
+ basename = os.path.basename(filename)
+ assets = [asset for asset in release.assets()
+ if asset.name == basename]
+ if assets:
+ print("Assets already exist: {}".format(assets))
+ print("Press enter to continue anyways or Ctrl-C to abort.")
+ input()
+
try:
with open(filename, 'rb') as f:
- basename = os.path.basename(filename)
release.upload_asset(mimetype, basename, f, description)
except github3.exceptions.ConnectionError as e:
- utils.print_col('Failed to upload: {}'.format(e), 'red')
- print("Press Enter to retry...")
+ utils.print_error('Failed to upload: {}'.format(e))
+ print("Press Enter to retry...", file=sys.stderr)
input()
print("Retrying!")
@@ -481,10 +489,9 @@ def main():
upload_to_pypi = True
if args.upload:
- utils.print_title("Press enter to release...")
- input()
-
version_tag = "v" + qutebrowser.__version__
+ utils.print_title("Press enter to release {}...".format(version_tag))
+ input()
github_upload(artifacts, version_tag)
if upload_to_pypi:
diff --git a/scripts/dev/misc_checks.py b/scripts/dev/misc_checks.py
index 64864df8f..24c3a1ddc 100644
--- a/scripts/dev/misc_checks.py
+++ b/scripts/dev/misc_checks.py
@@ -83,7 +83,7 @@ def check_spelling():
"""Check commonly misspelled words."""
# Words which I often misspell
words = {'[Bb]ehaviour', '[Qq]uitted', 'Ll]ikelyhood', '[Ss]ucessfully',
- '[Oo]ccur[^rs .]', '[Ss]eperator', '[Ee]xplicitely',
+ '[Oo]ccur[^rs .!]', '[Ss]eperator', '[Ee]xplicitely',
'[Aa]uxillary', '[Aa]ccidentaly', '[Aa]mbigious', '[Ll]oosly',
'[Ii]nitialis', '[Cc]onvienence', '[Ss]imiliar', '[Uu]ncommited',
'[Rr]eproducable', '[Aa]n [Uu]ser', '[Cc]onvienience',
diff --git a/scripts/dev/recompile_requirements.py b/scripts/dev/recompile_requirements.py
index 84cc155e3..ecd7dd153 100644
--- a/scripts/dev/recompile_requirements.py
+++ b/scripts/dev/recompile_requirements.py
@@ -113,6 +113,20 @@ def get_all_names():
yield basename[len('requirements-'):-len('.txt-raw')]
+def init_venv(host_python, venv_dir, requirements):
+ """Initialize a new virtualenv and install the given packages."""
+ subprocess.run([host_python, '-m', 'venv', venv_dir], check=True)
+
+ venv_python = os.path.join(venv_dir, 'bin', 'python')
+ subprocess.run([venv_python, '-m', 'pip',
+ 'install', '-U', 'pip'], check=True)
+
+ subprocess.run([venv_python, '-m', 'pip',
+ 'install', '-r', requirements], check=True)
+ subprocess.run([venv_python, '-m', 'pip', 'check'], check=True)
+ return venv_python
+
+
def main():
"""Re-compile the given (or all) requirement files."""
names = sys.argv[1:] if len(sys.argv) > 1 else sorted(get_all_names())
@@ -136,15 +150,10 @@ def main():
else:
host_python = sys.executable
- with tempfile.TemporaryDirectory() as tmpdir:
- subprocess.run([host_python, '-m', 'venv', tmpdir], check=True)
+ utils.print_subtitle("Building")
- venv_python = os.path.join(tmpdir, 'bin', 'python')
- subprocess.run([venv_python, '-m', 'pip',
- 'install', '-U', 'pip'], check=True)
-
- subprocess.run([venv_python, '-m', 'pip',
- 'install', '-r', filename], check=True)
+ with tempfile.TemporaryDirectory() as tmpdir:
+ venv_python = init_venv(host_python, tmpdir, filename)
proc = subprocess.run([venv_python, '-m', 'pip', 'freeze'],
check=True, stdout=subprocess.PIPE)
reqs = proc.stdout.decode('utf-8')
@@ -163,6 +172,11 @@ def main():
for line in comments['add']:
f.write(line + '\n')
+ # Test resulting file
+ utils.print_subtitle("Testing")
+ with tempfile.TemporaryDirectory() as tmpdir:
+ init_venv(host_python, tmpdir, outfile)
+
if __name__ == '__main__':
main()
diff --git a/scripts/dev/update_version.py b/scripts/dev/update_version.py
index 89f7e4218..f6cc2c27b 100644
--- a/scripts/dev/update_version.py
+++ b/scripts/dev/update_version.py
@@ -69,19 +69,19 @@ if __name__ == "__main__":
print("* git checkout master && git cherry-pick v{v} && "
"git push origin".format(v=version))
else:
- print("* git branch v{x} v{v} && git push origin v{x}"
+ print("* git branch v{x} v{v} && git push --set-upstream origin v{x}"
.format(v=version, x=x_version))
print("* Create new release via GitHub (required to upload release "
"artifacts)")
- print("* Linux: git pull && git checkout v{v} && "
+ print("* Linux: git fetch && git checkout v{v} && "
"./.venv/bin/python3 scripts/dev/build_release.py --upload"
.format(v=version))
- print("* Windows: git pull; git checkout v{v}; "
+ print("* Windows: git fetch; git checkout v{v}; "
"py -3 scripts\\dev\\build_release.py --asciidoc "
"C:\\Python27\\python "
"$env:userprofile\\bin\\asciidoc-8.6.10\\asciidoc.py --upload"
.format(v=version))
- print("* macOS: git pull && git checkout v{v} && "
+ print("* macOS: git fetch && git checkout v{v} && "
"python3 scripts/dev/build_release.py --upload"
.format(v=version))
diff --git a/scripts/dictcli.py b/scripts/dictcli.py
index 3676506e1..ebe4e285c 100755
--- a/scripts/dictcli.py
+++ b/scripts/dictcli.py
@@ -38,6 +38,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
from qutebrowser.browser.webengine import spell
from qutebrowser.config import configdata
from qutebrowser.utils import standarddir, utils
+from scripts import utils as scriptutils
API_URL = 'https://chromium.googlesource.com/chromium/deps/hunspell_dictionaries.git/+/master/'
@@ -219,7 +220,13 @@ def install(languages):
print('Installing {}: {}'.format(lang.code, lang.name))
install_lang(lang)
except PermissionError as e:
- sys.exit(str(e))
+ msg = ("\n{}\n\nWith Qt < 5.10, you will need to run this script "
+ "as root, as dictionaries need to be installed "
+ "system-wide. If your qutebrowser uses a newer Qt version "
+ "via a virtualenv, make sure you start this script with "
+ "the virtualenv's Python.".format(e))
+ scriptutils.print_error(msg)
+ sys.exit(1)
def update(languages):
diff --git a/scripts/mkvenv.py b/scripts/mkvenv.py
index 6c4c3c87d..04cf0e8c0 100644
--- a/scripts/mkvenv.py
+++ b/scripts/mkvenv.py
@@ -52,9 +52,12 @@ def parse_args() -> argparse.Namespace:
default='auto',
help="PyQt version to install.")
parser.add_argument('--pyqt-type',
- choices=['binary', 'source', 'link'],
+ choices=['binary', 'source', 'link', 'wheels', 'skip'],
default='binary',
help="How to install PyQt/Qt.")
+ parser.add_argument('--pyqt-wheels-dir',
+ default='wheels',
+ help="Directory to get PyQt wheels from.")
parser.add_argument('--virtualenv',
action='store_true',
help="Use virtualenv instead of venv.")
@@ -62,6 +65,12 @@ def parse_args() -> argparse.Namespace:
"asciidoc.py. If not given, it's searched in PATH.",
nargs=2, required=False,
metavar=('PYTHON', 'ASCIIDOC'))
+ parser.add_argument('--dev',
+ action='store_true',
+ help="Also install dev/test dependencies.")
+ parser.add_argument('--skip-docs',
+ action='store_true',
+ help="Skip doc generation.")
parser.add_argument('--tox-error',
action='store_true',
help=argparse.SUPPRESS)
@@ -92,7 +101,7 @@ def run_venv(venv_dir: pathlib.Path, executable, *args: str) -> None:
subprocess.run([str(venv_dir / subdir / executable)] +
[str(arg) for arg in args], check=True)
except subprocess.CalledProcessError as e:
- utils.print_col("Subprocess failed, exiting", 'red')
+ utils.print_error("Subprocess failed, exiting")
sys.exit(e.returncode)
@@ -100,7 +109,7 @@ def pip_install(venv_dir: pathlib.Path, *args: str) -> None:
"""Run a pip install command inside the virtualenv."""
arg_str = ' '.join(str(arg) for arg in args)
utils.print_col('venv$ pip install {}'.format(arg_str), 'blue')
- run_venv(venv_dir, 'python3', '-m', 'pip', 'install', *args)
+ run_venv(venv_dir, 'python', '-m', 'pip', 'install', *args)
def show_tox_error(pyqt_type: str) -> None:
@@ -115,9 +124,9 @@ def show_tox_error(pyqt_type: str) -> None:
raise AssertionError
print()
- utils.print_col('tox -e {} is deprecated. '
- 'Please use "python3 scripts/mkvenv.py{}" instead.'
- .format(env, args), 'red')
+ utils.print_error('tox -e {} is deprecated. '
+ 'Please use "python3 scripts/mkvenv.py{}" instead.'
+ .format(env, args))
print()
@@ -134,9 +143,8 @@ def delete_old_venv(venv_dir: pathlib.Path) -> None:
]
if not any(m.exists() for m in markers):
- utils.print_col('{} does not look like a virtualenv, '
- 'cowardly refusing to remove it.'.format(venv_dir),
- 'red')
+ utils.print_error('{} does not look like a virtualenv, '
+ 'cowardly refusing to remove it.'.format(venv_dir))
sys.exit(1)
utils.print_col('$ rm -r {}'.format(venv_dir), 'blue')
@@ -151,7 +159,7 @@ def create_venv(venv_dir: pathlib.Path, use_virtualenv: bool = False) -> None:
subprocess.run([sys.executable, '-m', 'virtualenv', venv_dir],
check=True)
except subprocess.CalledProcessError as e:
- utils.print_col("virtualenv failed, exiting", 'red')
+ utils.print_error("virtualenv failed, exiting")
sys.exit(e.returncode)
else:
utils.print_col('$ python3 -m venv {}'.format(venv_dir), 'blue')
@@ -169,11 +177,16 @@ def upgrade_seed_pkgs(venv_dir: pathlib.Path) -> None:
pip_install(venv_dir, '-U', 'setuptools', 'wheel')
-def pyqt_requirements_file(version: str):
- """Get the filename of the requirements file for the given PyQt version."""
- suffix = '' if version == 'auto' else '-{}'.format(version)
+def requirements_file(name: str) -> pathlib.Path:
+ """Get the filename of a requirements file."""
return (REPO_ROOT / 'misc' / 'requirements' /
- 'requirements-pyqt{}.txt'.format(suffix))
+ 'requirements-{}.txt'.format(name))
+
+
+def pyqt_requirements_file(version: str) -> pathlib.Path:
+ """Get the filename of the requirements file for the given PyQt version."""
+ name = 'pyqt' if version == 'auto' else 'pyqt-{}'.format(version)
+ return requirements_file(name)
def install_pyqt_binary(venv_dir: pathlib.Path, version: str) -> None:
@@ -199,11 +212,27 @@ def install_pyqt_link(venv_dir: pathlib.Path) -> None:
link_pyqt.link_pyqt(sys.executable, lib_path)
+def install_pyqt_wheels(venv_dir: pathlib.Path,
+ wheels_dir: pathlib.Path) -> None:
+ """Install PyQt from the wheels/ directory."""
+ utils.print_title("Installing PyQt wheels")
+ wheels = [str(wheel) for wheel in wheels_dir.glob('*.whl')]
+ pip_install(venv_dir, *wheels)
+
+
def install_requirements(venv_dir: pathlib.Path) -> None:
"""Install qutebrowser's requirement.txt."""
utils.print_title("Installing other qutebrowser dependencies")
- requirements_file = REPO_ROOT / 'requirements.txt'
- pip_install(venv_dir, '-r', str(requirements_file))
+ requirements = REPO_ROOT / 'requirements.txt'
+ pip_install(venv_dir, '-r', str(requirements))
+
+
+def install_dev_requirements(venv_dir: pathlib.Path) -> None:
+ """Install development dependencies."""
+ utils.print_title("Installing dev dependencies")
+ pip_install(venv_dir,
+ '-r', str(requirements_file('dev')),
+ '-r', requirements_file('tests'))
def install_qutebrowser(venv_dir: pathlib.Path) -> None:
@@ -224,21 +253,27 @@ def regenerate_docs(venv_dir: pathlib.Path,
utils.print_col('venv$ python3 scripts/asciidoc2html.py {}'
.format(' '.join(a2h_args)), 'blue')
- run_venv(venv_dir, 'python3', str(script_path), *a2h_args)
+ run_venv(venv_dir, 'python', str(script_path), *a2h_args)
def main() -> None:
"""Install qutebrowser in a virtualenv.."""
args = parse_args()
venv_dir = pathlib.Path(args.venv_dir)
+ wheels_dir = pathlib.Path(args.pyqt_wheels_dir)
utils.change_cwd()
if args.tox_error:
show_tox_error(args.pyqt_type)
sys.exit(1)
- elif args.pyqt_type == 'link' and args.pyqt_version != 'auto':
- utils.print_col('The --pyqt-version option is not available when '
- 'linking a system-wide install.', 'red')
+ elif (args.pyqt_version != 'auto' and
+ args.pyqt_type not in ['binary', 'source']):
+ utils.print_error('The --pyqt-version option is only available when '
+ 'installing PyQt from binary or source')
+ sys.exit(1)
+ elif args.pyqt_wheels_dir != 'wheels' and args.pyqt_type != 'wheels':
+ utils.print_error('The --pyqt-wheels-dir option is only available '
+ 'when installing PyQt from wheels')
sys.exit(1)
if not args.keep:
@@ -254,12 +289,20 @@ def main() -> None:
install_pyqt_source(venv_dir, args.pyqt_version)
elif args.pyqt_type == 'link':
install_pyqt_link(venv_dir)
+ elif args.pyqt_type == 'wheels':
+ install_pyqt_wheels(venv_dir, wheels_dir)
+ elif args.pyqt_type == 'skip':
+ pass
else:
raise AssertionError
install_requirements(venv_dir)
install_qutebrowser(venv_dir)
- regenerate_docs(venv_dir, args.asciidoc)
+ if args.dev:
+ install_dev_requirements(venv_dir)
+
+ if not args.skip_docs:
+ regenerate_docs(venv_dir, args.asciidoc)
if __name__ == '__main__':
diff --git a/scripts/utils.py b/scripts/utils.py
index 0d405c8a6..bdf3f96fc 100644
--- a/scripts/utils.py
+++ b/scripts/utils.py
@@ -21,6 +21,7 @@
import os
import os.path
+import sys
# Import side-effects are an evil thing, but here it's okay so scripts using
@@ -58,14 +59,18 @@ def _esc(code):
return '\033[{}m'.format(code)
-def print_col(text, color):
+def print_col(text, color, file=sys.stdout):
"""Print a colorized text."""
if use_color:
fg = _esc(fg_colors[color.lower()])
reset = _esc(fg_colors['reset'])
- print(''.join([fg, text, reset]))
+ print(''.join([fg, text, reset]), file=file)
else:
- print(text)
+ print(text, file=file)
+
+
+def print_error(text):
+ print_col(text, 'red', file=sys.stderr)
def print_title(text):
diff --git a/tests/end2end/features/conftest.py b/tests/end2end/features/conftest.py
index f9df23ac9..6ac5f281d 100644
--- a/tests/end2end/features/conftest.py
+++ b/tests/end2end/features/conftest.py
@@ -29,6 +29,7 @@ import logging
import collections
import textwrap
import subprocess
+import shutil
import pytest
import pytest_bdd as bdd
@@ -49,7 +50,7 @@ def _get_echo_exe_path():
return os.path.join(testutils.abs_datapath(), 'userscripts',
'echo.bat')
else:
- return 'echo'
+ return shutil.which("echo")
@pytest.hookimpl(hookwrapper=True)
diff --git a/tests/end2end/features/javascript.feature b/tests/end2end/features/javascript.feature
index 8fedf5af1..a4c3d1338 100644
--- a/tests/end2end/features/javascript.feature
+++ b/tests/end2end/features/javascript.feature
@@ -187,6 +187,7 @@ Feature: Javascript stuff
When I set content.javascript.enabled to false
And I open 500 without waiting
Then "Showing error page for* 500" should be logged
+ And "Load error: *500" should be logged
@flaky
Scenario: Using JS after window.open
diff --git a/tests/end2end/features/search.feature b/tests/end2end/features/search.feature
index 2c0aa06a5..de815a5b7 100644
--- a/tests/end2end/features/search.feature
+++ b/tests/end2end/features/search.feature
@@ -213,6 +213,56 @@ Feature: Searching on a page
# TODO: wrapping message with scrolling
# TODO: wrapping message without scrolling
+ ## wrapping prevented
+
+ @qtwebkit_skip @qt>=5.14
+ Scenario: Preventing wrapping at the top of the page with QtWebEngine
+ When I set search.ignore_case to always
+ And I set search.wrap to false
+ And I run :search --reverse foo
+ And I wait for "search found foo with flags FindBackward" in the log
+ And I run :search-next
+ And I wait for "next_result found foo with flags FindBackward" in the log
+ And I run :search-next
+ And I wait for "Search hit TOP" in the log
+ Then "foo" should be found
+
+ @qtwebkit_skip @qt>=5.14
+ Scenario: Preventing wrapping at the bottom of the page with QtWebEngine
+ When I set search.ignore_case to always
+ And I set search.wrap to false
+ And I run :search foo
+ And I wait for "search found foo" in the log
+ And I run :search-next
+ And I wait for "next_result found foo" in the log
+ And I run :search-next
+ And I wait for "Search hit BOTTOM" in the log
+ Then "Foo" should be found
+
+ @qtwebengine_skip
+ Scenario: Preventing wrapping at the top of the page with QtWebKit
+ When I set search.ignore_case to always
+ And I set search.wrap to false
+ And I run :search --reverse foo
+ And I wait for "search found foo with flags FindBackward" in the log
+ And I run :search-next
+ And I wait for "next_result found foo with flags FindBackward" in the log
+ And I run :search-next
+ And I wait for "next_result didn't find foo with flags FindBackward" in the log
+ Then the warning "Text 'foo' not found on page!" should be shown
+
+ @qtwebengine_skip
+ Scenario: Preventing wrapping at the bottom of the page with QtWebKit
+ When I set search.ignore_case to always
+ And I set search.wrap to false
+ And I run :search foo
+ And I wait for "search found foo" in the log
+ And I run :search-next
+ And I wait for "next_result found foo" in the log
+ And I run :search-next
+ And I wait for "next_result didn't find foo" in the log
+ Then the warning "Text 'foo' not found on page!" should be shown
+
## follow searched links
@skip # Too flaky
Scenario: Follow a searched link
diff --git a/tests/end2end/features/spawn.feature b/tests/end2end/features/spawn.feature
index 5fabb044d..623bf4959 100644
--- a/tests/end2end/features/spawn.feature
+++ b/tests/end2end/features/spawn.feature
@@ -38,6 +38,14 @@ Feature: :spawn
And I run :spawn (echo-exe) {url:pretty}
Then "Executing * with args ['http://localhost:(port)/data/title with spaces.html'], userscript=False" should be logged
+ Scenario: Running :spawn with -m
+ When I run :spawn -m (echo-exe) Message 1
+ Then the message "Message 1" should be shown
+
+ Scenario: Running :spawn with -u -m
+ When I run :spawn -u -m (echo-exe) Message 2
+ Then the message "Message 2" should be shown
+
@posix
Scenario: Running :spawn with userscript
When I open data/hello.txt
diff --git a/tests/end2end/features/test_editor_bdd.py b/tests/end2end/features/test_editor_bdd.py
index edc4a9927..36719324a 100644
--- a/tests/end2end/features/test_editor_bdd.py
+++ b/tests/end2end/features/test_editor_bdd.py
@@ -98,6 +98,9 @@ class EditorPidWatcher(QObject):
else:
self._watcher.addPath(str(self._pidfile))
+ def manual_check(self):
+ return self._pidfile.check()
+
@pytest.fixture
def editor_pid_watcher(tmpdir):
@@ -143,9 +146,12 @@ def set_up_editor_wait(quteproc, tmpdir, text, editor_pid_watcher):
@bdd.when("I wait until the editor has started")
def wait_editor(qtbot, editor_pid_watcher):
if not editor_pid_watcher.has_pidfile:
- with qtbot.wait_signal(editor_pid_watcher.appeared, timeout=5000):
+ with qtbot.wait_signal(editor_pid_watcher.appeared, raising=False):
pass
+ if not editor_pid_watcher.manual_check():
+ pytest.fail("Editor pidfile failed to appear!")
+
@bdd.when(bdd.parsers.parse('I kill the waiting editor'))
def kill_editor_wait(tmpdir):
diff --git a/tests/end2end/features/test_prompts_bdd.py b/tests/end2end/features/test_prompts_bdd.py
index b05424f8e..e7602c5b4 100644
--- a/tests/end2end/features/test_prompts_bdd.py
+++ b/tests/end2end/features/test_prompts_bdd.py
@@ -17,8 +17,6 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
-import time
-
import pytest_bdd as bdd
bdd.scenarios('prompts.feature')
@@ -53,10 +51,15 @@ def no_prompt_shown(quteproc):
def ssl_error_page(request, quteproc):
if request.config.webengine and qtutils.version_check('5.9'):
quteproc.wait_for(message="Certificate error: *")
- time.sleep(0.5) # Wait for error page to appear
- content = quteproc.get_content().strip()
- assert ("ERR_INSECURE_RESPONSE" in content or # Qt <= 5.10
- "ERR_CERT_AUTHORITY_INVALID" in content) # Qt 5.11
+
+ msg = quteproc.wait_for(message="Load error: *")
+ msg.expected = True
+
+ expected_messages = [
+ 'Load error: ERR_INSECURE_RESPONSE', # Qt <= 5.10
+ 'Load error: ERR_CERT_AUTHORITY_INVALID', # Qt 5.11
+ ]
+ assert msg.message in expected_messages
else:
if not request.config.webengine:
line = quteproc.wait_for(message='Error while loading *: SSL '
diff --git a/tests/end2end/features/test_qutescheme_bdd.py b/tests/end2end/features/test_qutescheme_bdd.py
index 5741dae75..587aadc41 100644
--- a/tests/end2end/features/test_qutescheme_bdd.py
+++ b/tests/end2end/features/test_qutescheme_bdd.py
@@ -38,6 +38,8 @@ def request_blocked(request, quteproc, kind):
"[http://localhost:*/data/misc/qutescheme_csrf.html:0] Not allowed to "
"load local resource: qute://settings/set?*"
)
+ unsafe_redirect_msg = "Load error: ERR_UNSAFE_REDIRECT"
+ blocked_request_msg = "Load error: ERR_BLOCKED_BY_CLIENT"
webkit_error_invalid = (
"Error while loading qute://settings/set?*: Invalid qute://settings "
@@ -51,15 +53,19 @@ def request_blocked(request, quteproc, kind):
expected_messages = {
'img': [blocking_js_msg],
'link': [blocking_js_msg],
- 'redirect': [blocking_set_msg],
+ 'redirect': [blocking_set_msg, blocked_request_msg],
'form': [blocking_js_msg],
}
+ if qtutils.version_check('5.15', compiled=False):
+ # On Qt 5.15, Chromium blocks the redirect as ERR_UNSAFE_REDIRECT
+ # instead.
+ expected_messages['redirect'] = [unsafe_redirect_msg]
elif request.config.webengine:
expected_messages = {
'img': [blocking_csrf_msg],
- 'link': [blocking_set_msg],
- 'redirect': [blocking_set_msg],
- 'form': [blocking_set_msg],
+ 'link': [blocking_set_msg, blocked_request_msg],
+ 'redirect': [blocking_set_msg, blocked_request_msg],
+ 'form': [blocking_set_msg, blocked_request_msg],
}
else: # QtWebKit
expected_messages = {
diff --git a/tests/end2end/features/yankpaste.feature b/tests/end2end/features/yankpaste.feature
index 46ef92a9c..ecabffd13 100644
--- a/tests/end2end/features/yankpaste.feature
+++ b/tests/end2end/features/yankpaste.feature
@@ -194,10 +194,10 @@ Feature: Yanking and pasting.
http://qutebrowser.org
should not open
And I run :open -t {clipboard}
- And I wait until data/hello.txt?q=this%20url%3A%0Ahttp%3A%2F%2Fqutebrowser.org%0Ashould%20not%20open is loaded
+ And I wait until data/hello.txt?q=this%20url%3A%0Ahttp%3A//qutebrowser.org%0Ashould%20not%20open is loaded
Then the following tabs should be open:
- about:blank
- - data/hello.txt?q=this%20url%3A%0Ahttp%3A%2F%2Fqutebrowser.org%0Ashould%20not%20open (active)
+ - data/hello.txt?q=this%20url%3A%0Ahttp%3A//qutebrowser.org%0Ashould%20not%20open (active)
Scenario: Pasting multiline whose first line looks like a URI
When I set url.auto_search to naive
@@ -315,6 +315,7 @@ Feature: Yanking and pasting.
And I run :insert-text This text should be undone
And I wait for the javascript message "textarea contents: This text should be undone"
And I press the key "<Ctrl+z>"
+ And I wait for the javascript message "textarea contents: "
# Paste final text
And I run :insert-text This text should stay
# Compare
diff --git a/tests/end2end/fixtures/quteprocess.py b/tests/end2end/fixtures/quteprocess.py
index 6222f3a6a..5f8263334 100644
--- a/tests/end2end/fixtures/quteprocess.py
+++ b/tests/end2end/fixtures/quteprocess.py
@@ -279,6 +279,12 @@ def is_ignored_chromium_message(line):
# https://bugreports.qt.io/browse/QTBUG-78319
'temp file failure: * : could not create temporary file: No such file '
'or directory (2)',
+
+ # Travis
+ # test_ssl_error_with_contentssl_strict__true
+ # [5306:5324:0417/151739.362362:ERROR:address_tracker_linux.cc(171)]
+ # Could not bind NETLINK socket: Address already in use (98)
+ 'Could not bind NETLINK socket: Address already in use (98)',
]
return any(testutils.pattern_match(pattern=pattern, value=message)
for pattern in ignored_messages)
diff --git a/tests/end2end/fixtures/webserver.py b/tests/end2end/fixtures/webserver.py
index 54ea441de..839355664 100644
--- a/tests/end2end/fixtures/webserver.py
+++ b/tests/end2end/fixtures/webserver.py
@@ -63,7 +63,8 @@ class Request(testprocess.Line):
def _check_status(self):
"""Check if the http status is what we expected."""
path_to_statuses = {
- '/favicon.ico': [HTTPStatus.NOT_FOUND],
+ '/favicon.ico': [HTTPStatus.OK, HTTPStatus.PARTIAL_CONTENT],
+
'/does-not-exist': [HTTPStatus.NOT_FOUND],
'/does-not-exist-2': [HTTPStatus.NOT_FOUND],
'/404': [HTTPStatus.NOT_FOUND],
diff --git a/tests/end2end/fixtures/webserver_sub.py b/tests/end2end/fixtures/webserver_sub.py
index e34d4c295..9902ab125 100644
--- a/tests/end2end/fixtures/webserver_sub.py
+++ b/tests/end2end/fixtures/webserver_sub.py
@@ -272,6 +272,15 @@ def view_user_agent():
return flask.jsonify({'user-agent': flask.request.headers['user-agent']})
+@app.route('/favicon.ico')
+def favicon():
+ basedir = os.path.join(os.path.realpath(os.path.dirname(__file__)),
+ '..', '..', '..')
+ return flask.send_from_directory(os.path.join(basedir, 'icons'),
+ 'qutebrowser.ico',
+ mimetype='image/vnd.microsoft.icon')
+
+
@app.after_request
def log_request(response):
"""Log a webserver request."""
diff --git a/tests/end2end/fixtures/webserver_sub_ssl.py b/tests/end2end/fixtures/webserver_sub_ssl.py
index 7cd6dc92c..d3869201f 100644
--- a/tests/end2end/fixtures/webserver_sub_ssl.py
+++ b/tests/end2end/fixtures/webserver_sub_ssl.py
@@ -40,6 +40,11 @@ def hello_world():
return "Hello World via SSL!"
+@app.route('/favicon.ico')
+def favicon():
+ return webserver_sub.favicon()
+
+
@app.after_request
def log_request(response):
return webserver_sub.log_request(response)
diff --git a/tests/helpers/stubs.py b/tests/helpers/stubs.py
index ff6690da5..caa7aac3f 100644
--- a/tests/helpers/stubs.py
+++ b/tests/helpers/stubs.py
@@ -115,8 +115,8 @@ class FakeQApplication:
UNSET = object()
- def __init__(self, style=None, all_widgets=None, active_window=None,
- instance=UNSET, arguments=None):
+ def __init__(self, *, style=None, all_widgets=None, active_window=None,
+ instance=UNSET, arguments=None, platform_name=None):
if instance is self.UNSET:
self.instance = mock.Mock(return_value=self)
@@ -129,6 +129,7 @@ class FakeQApplication:
self.allWidgets = lambda: all_widgets
self.activeWindow = lambda: active_window
self.arguments = lambda: arguments
+ self.platformName = lambda: platform_name
class FakeNetworkReply:
diff --git a/tests/unit/config/test_configfiles.py b/tests/unit/config/test_configfiles.py
index e79134e2d..f7512e2a6 100644
--- a/tests/unit/config/test_configfiles.py
+++ b/tests/unit/config/test_configfiles.py
@@ -1223,4 +1223,4 @@ def test_init(init_patch, config_tmpdir):
settings.sync()
assert (config_tmpdir / 'qsettings').exists()
- # Lots of other stuff is tested in test_config.py in test_init
+ # Lots of other stuff is tested in test_configinit.py
diff --git a/tests/unit/config/test_configinit.py b/tests/unit/config/test_configinit.py
index 2063f6c13..694a95437 100644
--- a/tests/unit/config/test_configinit.py
+++ b/tests/unit/config/test_configinit.py
@@ -206,9 +206,13 @@ class TestEarlyInit:
assert dump == '\n'.join(expected)
- def test_state_init_errors(self, init_patch, args, data_tmpdir):
+ @pytest.mark.parametrize('byte', [
+ b'\x00', # configparser.Error
+ b'\xda', # UnicodeDecodeError
+ ])
+ def test_state_init_errors(self, init_patch, args, data_tmpdir, byte):
state_file = data_tmpdir / 'state'
- state_file.write_binary(b'\x00')
+ state_file.write_binary(byte)
configinit.early_init(args)
assert configinit._init_errors.errors
diff --git a/tests/unit/config/test_configtypes.py b/tests/unit/config/test_configtypes.py
index f949e1ca0..fe954ca3d 100644
--- a/tests/unit/config/test_configtypes.py
+++ b/tests/unit/config/test_configtypes.py
@@ -262,8 +262,13 @@ class TestAll:
configtypes.PercOrInt, # ditto
]:
return
- if (isinstance(typ, configtypes.ListOrValue) and
- isinstance(typ.valtype, configtypes.Int)):
+ elif (isinstance(typ, functools.partial) and
+ isinstance(typ.func, configtypes.ListOrValue)):
+ # "- /" -> "/"
+ return
+ elif (isinstance(typ, configtypes.ListOrValue) and
+ isinstance(typ.valtype, configtypes.Int)):
+ # "00" -> "0"
return
assert converted == s
@@ -2003,7 +2008,6 @@ class TestSearchEngineUrl:
@pytest.mark.parametrize('val', [
'foo', # no placeholder
- ':{}', # invalid URL
'foo{bar}baz{}', # {bar} format string variable
'{1}{}', # numbered format string variable
'{{}', # invalid format syntax
diff --git a/tests/unit/javascript/test_greasemonkey.py b/tests/unit/javascript/test_greasemonkey.py
index 5d85f84cd..26da7e342 100644
--- a/tests/unit/javascript/test_greasemonkey.py
+++ b/tests/unit/javascript/test_greasemonkey.py
@@ -315,3 +315,55 @@ class TestWindowIsolation:
elem.evaluateJavaScript(setup.setup_script)
result = elem.evaluateJavaScript(setup.test_script)
assert result == setup.expected
+
+
+class TestSharedWindowProxy:
+ """Check that all scripts have access to the same window proxy."""
+
+ @pytest.fixture
+ def setup(self):
+ # pylint: disable=attribute-defined-outside-init
+ class SetupData:
+ pass
+ ret = SetupData()
+
+ # Greasemonkey script to add a property to the window proxy.
+ ret.test_script_a = greasemonkey.GreasemonkeyScript.parse(
+ textwrap.dedent("""
+ // ==UserScript==
+ // @name a
+ // ==/UserScript==
+ // Set a value from script a
+ window.$ = 'test';
+ """)
+ ).code()
+
+ # Greasemonkey script to retrieve a property from the window proxy.
+ ret.test_script_b = greasemonkey.GreasemonkeyScript.parse(
+ textwrap.dedent("""
+ // ==UserScript==
+ // @name b
+ // ==/UserScript==
+ // Check that the value is accessible from script b
+ return [window.$, $];
+ """)
+ ).code()
+
+ # What we expect the script to report back.
+ ret.expected = ["test", "test"]
+ return ret
+
+ def test_webengine(self, qtbot, webengineview, setup):
+ page = webengineview.page()
+
+ with qtbot.wait_callback() as callback:
+ page.runJavaScript(setup.test_script_a, callback)
+ with qtbot.wait_callback() as callback:
+ page.runJavaScript(setup.test_script_b, callback)
+ callback.assert_called_with(setup.expected)
+
+ def test_webkit(self, webview, setup):
+ elem = webview.page().mainFrame().documentElement()
+ elem.evaluateJavaScript(setup.test_script_a)
+ result = elem.evaluateJavaScript(setup.test_script_b)
+ assert result == setup.expected
diff --git a/tests/unit/mainwindow/test_messageview.py b/tests/unit/mainwindow/test_messageview.py
index 68d3b2c56..8691bf07f 100644
--- a/tests/unit/mainwindow/test_messageview.py
+++ b/tests/unit/mainwindow/test_messageview.py
@@ -36,7 +36,7 @@ def view(qtbot, config_stub):
usertypes.MessageLevel.warning,
usertypes.MessageLevel.error])
def test_single_message(qtbot, view, level):
- with qtbot.waitExposed(view):
+ with qtbot.waitExposed(view, timeout=5000):
view.show_message(level, 'test')
assert view._messages[0].isVisible()
diff --git a/tests/unit/misc/test_throttle.py b/tests/unit/misc/test_throttle.py
index 63babc122..0e2db3aee 100644
--- a/tests/unit/misc/test_throttle.py
+++ b/tests/unit/misc/test_throttle.py
@@ -29,7 +29,7 @@ from helpers import utils
from qutebrowser.misc import throttle
-DELAY = 300 if utils.ON_CI else 100
+DELAY = 500 if utils.ON_CI else 100
@pytest.fixture
diff --git a/tests/unit/utils/test_urlutils.py b/tests/unit/utils/test_urlutils.py
index 0c265917d..39a43479b 100644
--- a/tests/unit/utils/test_urlutils.py
+++ b/tests/unit/utils/test_urlutils.py
@@ -98,6 +98,8 @@ def init_config(config_stub):
'test': 'http://www.qutebrowser.org/?q={}',
'test-with-dash': 'http://www.example.org/?q={}',
'path-search': 'http://www.example.org/{}',
+ 'quoted-path': 'http://www.example.org/{quoted}',
+ 'unquoted': 'http://www.example.org/?{unquoted}',
'DEFAULT': 'http://www.example.com/?q={}',
}
@@ -286,8 +288,10 @@ def test_special_urls(url, special):
('blub testfoo', 'www.example.com', 'q=blub testfoo'),
('stripped ', 'www.example.com', 'q=stripped'),
('test-with-dash testfoo', 'www.example.org', 'q=testfoo'),
- ('test/with/slashes', 'www.example.com', 'q=test%2Fwith%2Fslashes'),
+ ('test/with/slashes', 'www.example.com', 'q=test/with/slashes'),
('test path-search', 'www.qutebrowser.org', 'q=path-search'),
+ ('slash/and&amp', 'www.example.com', 'q=slash/and%26amp'),
+ ('unquoted one=1&two=2', 'www.example.org', 'one=1&two=2'),
])
def test_get_search_url(config_stub, url, host, query, open_base_url):
"""Test _get_search_url().
@@ -303,6 +307,25 @@ def test_get_search_url(config_stub, url, host, query, open_base_url):
assert url.query() == query
+@pytest.mark.parametrize('open_base_url', [True, False])
+@pytest.mark.parametrize('url, host, path', [
+ ('path-search t/w/s', 'www.example.org', 't/w/s'),
+ ('quoted-path t/w/s', 'www.example.org', 't%2Fw%2Fs'),
+])
+def test_get_search_url_for_path_search(config_stub, url, host, path, open_base_url):
+ """Test _get_search_url_for_path_search().
+
+ Args:
+ url: The "URL" to enter.
+ host: The expected search machine host.
+ path: The expected path on that host that is requested.
+ """
+ config_stub.val.url.open_base_url = open_base_url
+ url = urlutils._get_search_url(url)
+ assert url.host() == host
+ assert url.path(options=QUrl.PrettyDecoded) == '/' + path
+
+
@pytest.mark.parametrize('url, host', [
('test', 'www.qutebrowser.org'),
('test-with-dash', 'www.example.org'),
diff --git a/tests/unit/utils/test_version.py b/tests/unit/utils/test_version.py
index 1b703e6b0..0a3c5e4aa 100644
--- a/tests/unit/utils/test_version.py
+++ b/tests/unit/utils/test_version.py
@@ -196,6 +196,15 @@ from qutebrowser.browser import pdfjs
version.DistributionInfo(
id='tux', parsed=version.Distribution.unknown,
version=None, pretty='Tux')),
+ # Invalid multi-line value
+ ("""
+ ID=tux
+ PRETTY_NAME="Multiline
+ Text"
+ """,
+ version.DistributionInfo(
+ id='tux', parsed=version.Distribution.unknown,
+ version=None, pretty='Multiline')),
])
def test_distribution(tmpdir, monkeypatch, os_release, expected):
os_release_file = tmpdir / 'os-release'
@@ -897,7 +906,7 @@ class VersionParams:
name = attr.ib()
git_commit = attr.ib(True)
frozen = attr.ib(False)
- style = attr.ib(True)
+ qapp = attr.ib(True)
with_webkit = attr.ib(True)
known_distribution = attr.ib(True)
ssl_support = attr.ib(True)
@@ -909,7 +918,7 @@ class VersionParams:
VersionParams('normal'),
VersionParams('no-git-commit', git_commit=False),
VersionParams('frozen', frozen=True),
- VersionParams('no-style', style=False),
+ VersionParams('no-qapp', qapp=False),
VersionParams('no-webkit', with_webkit=False),
VersionParams('unknown-dist', known_distribution=False),
VersionParams('no-ssl', ssl_support=False),
@@ -937,8 +946,9 @@ def test_version_output(params, stubs, monkeypatch, config_stub):
'platform.architecture': lambda: ('ARCHITECTURE', ''),
'_os_info': lambda: ['OS INFO 1', 'OS INFO 2'],
'_path_info': lambda: {'PATH DESC': 'PATH NAME'},
- 'QApplication': (stubs.FakeQApplication(style='STYLE')
- if params.style else
+ 'QApplication': (stubs.FakeQApplication(style='STYLE',
+ platform_name='PLATFORM')
+ if params.qapp else
stubs.FakeQApplication(instance=None)),
'QLibraryInfo.location': (lambda _loc: 'QT PATH'),
'sql.version': lambda: 'SQLITE VERSION',
@@ -948,7 +958,9 @@ def test_version_output(params, stubs, monkeypatch, config_stub):
substitutions = {
'git_commit': '\nGit commit: GIT COMMIT' if params.git_commit else '',
- 'style': '\nStyle: STYLE' if params.style else '',
+ 'style': '\nStyle: STYLE' if params.qapp else '',
+ 'platform_plugin': ('\nPlatform plugin: PLATFORM' if params.qapp
+ else ''),
'qt': 'QT VERSION',
'frozen': str(params.frozen),
'import_path': import_path,
@@ -1014,7 +1026,7 @@ def test_version_output(params, stubs, monkeypatch, config_stub):
pdf.js: PDFJS VERSION
sqlite: SQLITE VERSION
QtNetwork SSL: {ssl}
- {style}
+ {style}{platform_plugin}
Platform: PLATFORM, ARCHITECTURE{linuxdist}
Frozen: {frozen}
Imported from {import_path}
diff --git a/www/header.asciidoc b/www/header.asciidoc
index 2f8ed6a1e..66f6f2bb3 100644
--- a/www/header.asciidoc
+++ b/www/header.asciidoc
@@ -28,9 +28,5 @@ time, your help is needed! See the
information. Depending on your sign-up date and how long you keep a certain
level, you can get qutebrowser t-shirts, stickers and more!
</p>
-<p>
-Thanks to the GitHub Sponsors Matching Fund, all donations done via GitHub
-Sponsors (up to a $5000 total) will be doubled until October 2020.
-</p>
</div>
+++