summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/changelog.asciidoc40
-rw-r--r--doc/contributing.asciidoc2
-rw-r--r--doc/faq.asciidoc13
-rw-r--r--doc/help/configuring.asciidoc1
-rw-r--r--doc/help/settings.asciidoc90
-rw-r--r--misc/requirements/requirements-pyqt-5.14.txt5
-rw-r--r--misc/requirements/requirements-pyqt-5.14.txt-raw4
-rw-r--r--misc/requirements/requirements-pyqt.txt4
-rwxr-xr-xmisc/userscripts/qute-lastpass19
-rw-r--r--qutebrowser/browser/browsertab.py8
-rw-r--r--qutebrowser/browser/commands.py4
-rw-r--r--qutebrowser/browser/downloadview.py4
-rw-r--r--qutebrowser/browser/hints.py4
-rw-r--r--qutebrowser/browser/qtnetworkdownloads.py11
-rw-r--r--qutebrowser/browser/webelem.py3
-rw-r--r--qutebrowser/browser/webengine/interceptor.py112
-rw-r--r--qutebrowser/browser/webengine/webengineinspector.py17
-rw-r--r--qutebrowser/browser/webengine/webenginesettings.py70
-rw-r--r--qutebrowser/browser/webkit/webkitsettings.py14
-rw-r--r--qutebrowser/browser/webkit/webkittab.py4
-rw-r--r--qutebrowser/browser/webkit/webpage.py8
-rw-r--r--qutebrowser/browser/webkit/webview.py4
-rw-r--r--qutebrowser/commands/cmdexc.py6
-rw-r--r--qutebrowser/commands/userscripts.py6
-rw-r--r--qutebrowser/completion/completionwidget.py4
-rw-r--r--qutebrowser/config/config.py120
-rw-r--r--qutebrowser/config/configdata.yml89
-rw-r--r--qutebrowser/config/configexc.py3
-rw-r--r--qutebrowser/config/configfiles.py216
-rw-r--r--qutebrowser/config/configinit.py6
-rw-r--r--qutebrowser/config/configtypes.py125
-rw-r--r--qutebrowser/config/configutils.py83
-rw-r--r--qutebrowser/config/stylesheet.py116
-rw-r--r--qutebrowser/config/websettings.py95
-rw-r--r--qutebrowser/extensions/interceptors.py3
-rw-r--r--qutebrowser/mainwindow/mainwindow.py21
-rw-r--r--qutebrowser/mainwindow/messageview.py13
-rw-r--r--qutebrowser/mainwindow/prompt.py4
-rw-r--r--qutebrowser/mainwindow/statusbar/bar.py14
-rw-r--r--qutebrowser/mainwindow/statusbar/progress.py4
-rw-r--r--qutebrowser/mainwindow/statusbar/url.py4
-rw-r--r--qutebrowser/mainwindow/tabbedbrowser.py8
-rw-r--r--qutebrowser/mainwindow/tabwidget.py16
-rw-r--r--qutebrowser/misc/keyhintwidget.py4
-rw-r--r--qutebrowser/utils/javascript.py3
-rw-r--r--qutebrowser/utils/urlutils.py24
-rw-r--r--qutebrowser/utils/usertypes.py13
-rw-r--r--qutebrowser/utils/version.py27
-rw-r--r--scripts/dev/check_coverage.py2
-rw-r--r--tests/end2end/features/misc.feature7
-rw-r--r--tests/helpers/fixtures.py14
-rw-r--r--tests/unit/browser/webengine/test_webengineinterceptor.py30
-rw-r--r--tests/unit/browser/webengine/test_webenginesettings.py13
-rw-r--r--tests/unit/browser/webkit/test_webkitsettings.py31
-rw-r--r--tests/unit/config/test_config.py72
-rw-r--r--tests/unit/config/test_configcommands.py16
-rw-r--r--tests/unit/config/test_configfiles.py11
-rw-r--r--tests/unit/config/test_configinit.py1
-rw-r--r--tests/unit/config/test_configtypes.py22
-rw-r--r--tests/unit/config/test_configutils.py73
-rw-r--r--tests/unit/config/test_stylesheet.py72
-rw-r--r--tests/unit/config/test_websettings.py104
-rw-r--r--tests/unit/javascript/stylesheet/test_stylesheet_js.py (renamed from tests/unit/javascript/stylesheet/test_stylesheet.py)0
-rw-r--r--tests/unit/utils/test_urlutils.py25
-rw-r--r--tests/unit/utils/test_version.py53
-rw-r--r--tests/unit/utils/usertypes/test_misc.py9
-rw-r--r--tox.ini5
67 files changed, 1343 insertions, 685 deletions
diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc
index 34d3f71b6..cc44bffd7 100644
--- a/doc/changelog.asciidoc
+++ b/doc/changelog.asciidoc
@@ -21,16 +21,28 @@ v1.9.0 (unreleased)
Added
~~~~~
+- Initial support for Qt 5.14.
- New `tabs.tooltips` setting which can be used to disable hover tooltips for
tabs.
- New settings to configure the appearance of context menus:
- `fonts.contextmenu`
- - `colors.contextmenu.bg`
- - `colors.contextmenu.fg`
+ - `colors.contextmenu.menu.bg`
+ - `colors.contextmenu.menu.fg`
+ - `colors.contextmenu.active.bg`
+ - `colors.contextmenu.active.fg`
+- New `content.site_specific_quirks` setting which enables workarounds for
+ websites with broken user agent parsing (enabled by default, see the "Fixed"
+ section for fixed websites).
+- New `qt.force_platformtheme` setting to force Qt to use a given platform
+ theme.
Changed
~~~~~~~
+- The `content.headers.user_agent` setting now is a format string with the
+ default value resembling the behavior of it being set to null before.
+ This slightly changes the sent user agent for QtWebKit: Instead of mentioning
+ qutebrowser and its version it now mentions the Qt version.
- The `qute-pass` userscript now has a new `--extra-url-suffixes` (`-s`)
argument which passes extra URL suffixes to the tldextract library.
- A stack is now used for `:tab-focus last` rather than just saving one tab.
@@ -56,6 +68,14 @@ Changed
a timeout) on PyQt 5.13.1 and newer.
- The `:spawn` command has a new `-m` / `--output-messages` argument which
shows qutebrowser messages based on a command's standard output/error.
+- Improved insert mode detection for some CodeMirror usages (e.g. in
+ JupyterLab and Jupyter Notebook).
+- If JavaScript is disabled globally, `file://*` now doesn't automatically have
+ it enabled anymore. Run `:set -u file://* content.javascript.enabled true` to
+ restore the previous behavior.
+- Settings with URL patterns can now be used to affect the behavior of the
+ QtWebEngine inspector. Note that the underlying URL is `chrome-devtools://*`
+ from Qt 5.11 to Qt 5.13, but `devtools://*` with Qt 5.14.
- Performance improvements for the following areas:
* Adding settings with URL patterns
* Matching of settings using URL patterns
@@ -63,6 +83,9 @@ Changed
Fixed
~~~~~
+- Downloads (e.g. via `:download`) now see the same user agent header as
+ webpages, which fixes cases where overly restrictive servers/WAFs closed the
+ connection before.
- dictcli.py now works correctly on Windows again.
- The logic for `:restart` has been revisited which should fix issues with
relative basedirs.
@@ -70,6 +93,13 @@ Fixed
QtWebKit.
- Workaround for a Qt bug where a page never finishes loading with a
non-overridable TLS error (e.g. due to HSTS).
+- The `qute://configdiff` page now doesn't show built-in settings (e.g.
+ javascript being enabled for `qute://` and `chrome://` pages) anymore.
+- The `qute-lastpass` userscript now stops prompting for passwords when
+ cancelling the password input.
+- The tab hover text now shows ampersands (&) correctly.
+- With QtWebEngine and Qt >= 5.11, the inspector now shows its icons correctly
+ even if loading of images is disabled via the `content.images` setting.
- Various improvements for URL/searchengine detection:
- Strings with a dot but with characters not allowed in a URL (e.g. an
underscore) are now not treated as URL anymore.
@@ -81,6 +111,12 @@ Fixed
- `url.open_base_url = True` together with `url.auto_search = 'never'` is now
handled correctly.
- Fixed crash when a search engine URL turns out to be invalid.
+- Site specific quirks which work around some broken websites:
+ - WhatsApp Web
+ - Google Accounts
+ - Slack (with older QtWebEngine versions)
+ - Dell.com support pages (with Qt 5.7)
+ - Google Docs (fixes broken IME/compose key)
v1.8.3 (2019-12-05)
-------------------
diff --git a/doc/contributing.asciidoc b/doc/contributing.asciidoc
index 95c53cea5..ba984f328 100644
--- a/doc/contributing.asciidoc
+++ b/doc/contributing.asciidoc
@@ -105,7 +105,7 @@ Useful utilities
Checkers
~~~~~~~~
-qutebrowser uses http://tox.readthedocs.org/en/latest/[tox] to run its
+qutebrowser uses https://tox.readthedocs.io/en/latest/[tox] to run its
unittests and several linters/checkers.
Currently, the following tox environments are available:
diff --git a/doc/faq.asciidoc b/doc/faq.asciidoc
index 9a67fcb56..b4a7c8602 100644
--- a/doc/faq.asciidoc
+++ b/doc/faq.asciidoc
@@ -96,15 +96,10 @@ Those were handled appropriately
security bugs, please contact me directly at mail@qutebrowser.org, GPG ID
https://www.the-compiler.org/pubkey.asc[0x916eb0c8fd55a072].
-Is there an adblocker?::
- There is a host-based adblocker which takes /etc/hosts-like lists. A "real"
- adblocker has a
- https://www.reddit.com/r/programming/comments/25j41u/adblock_pluss_effect_on_firefoxs_memory_usage/chhpomw[big
- impact] on browsing speed and
- https://blog.mozilla.org/nnethercote/2014/05/14/adblock-pluss-effect-on-firefoxs-memory-usage/[RAM
- usage], so implementing support for AdBlockPlus-like lists is currently not
- a priority.
-
+Is there an ad blocker?::
+ There is a simple host-based ad blocker that takes `/etc/hosts`-like lists.
++
+More advanced ad blockers can have a big impact on browsing speed and https://blog.mozilla.org/nnethercote/2014/05/14/adblock-pluss-effect-on-firefoxs-memory-usage/[RAM usage], so implementing support for AdBlock Plus-like lists is not a priority.
How can I get No-Script-like behavior?::
To disable JavaScript by default:
+
diff --git a/doc/help/configuring.asciidoc b/doc/help/configuring.asciidoc
index b95511245..9c920a55e 100644
--- a/doc/help/configuring.asciidoc
+++ b/doc/help/configuring.asciidoc
@@ -432,6 +432,7 @@ Various emacs/conkeror-like keybinding configs exist:
- https://gitlab.com/jgkamat/qutemacs/blob/master/qutemacs.py[jgkamat]
- https://gitlab.com/Kaligule/qutebrowser-emacs-config/blob/master/config.py[Kaligule]
- http://me0w.net/pit/1540882719[nm0i]
+- https://www.reddit.com/r/qutebrowser/comments/eh10i7/config_share_qute_with_emacs_keybindings/[jasonsun0310]
It's also mostly possible to get rid of modal keybindings by setting
`input.insert_mode.auto_enter` to `false`, and `input.forward_unbound_keys` to
diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc
index 355c30153..7cab1dbe9 100644
--- a/doc/help/settings.asciidoc
+++ b/doc/help/settings.asciidoc
@@ -32,8 +32,10 @@
|<<colors.completion.odd.bg,colors.completion.odd.bg>>|Background color of the completion widget for odd rows.
|<<colors.completion.scrollbar.bg,colors.completion.scrollbar.bg>>|Color of the scrollbar in the completion view.
|<<colors.completion.scrollbar.fg,colors.completion.scrollbar.fg>>|Color of the scrollbar handle in the completion view.
-|<<colors.contextmenu.bg,colors.contextmenu.bg>>|Background color of the context menu.
-|<<colors.contextmenu.fg,colors.contextmenu.fg>>|Foreground color of the context menu.
+|<<colors.contextmenu.menu.bg,colors.contextmenu.menu.bg>>|Background color of the context menu.
+|<<colors.contextmenu.menu.fg,colors.contextmenu.menu.fg>>|Foreground color of the context menu.
+|<<colors.contextmenu.selected.bg,colors.contextmenu.selected.bg>>|Background color of the context menu's selected item.
+|<<colors.contextmenu.selected.fg,colors.contextmenu.selected.fg>>|Foreground color of the context menu's selected item.
|<<colors.downloads.bar.bg,colors.downloads.bar.bg>>|Background color for the download bar.
|<<colors.downloads.error.bg,colors.downloads.error.bg>>|Background color for downloads with errors.
|<<colors.downloads.error.fg,colors.downloads.error.fg>>|Foreground color for downloads with errors.
@@ -138,7 +140,7 @@
|<<content.headers.custom,content.headers.custom>>|Custom headers for qutebrowser HTTP requests.
|<<content.headers.do_not_track,content.headers.do_not_track>>|Value to send in the `DNT` header.
|<<content.headers.referer,content.headers.referer>>|When to send the Referer header.
-|<<content.headers.user_agent,content.headers.user_agent>>|User agent to send. Unset to send the default.
+|<<content.headers.user_agent,content.headers.user_agent>>|User agent to send.
|<<content.host_blocking.enabled,content.host_blocking.enabled>>|Enable host blocking.
|<<content.host_blocking.lists,content.host_blocking.lists>>|List of URLs of lists which contain hosts to block.
|<<content.host_blocking.whitelist,content.host_blocking.whitelist>>|A list of patterns that should always be loaded, despite being ad-blocked.
@@ -168,6 +170,7 @@
|<<content.proxy,content.proxy>>|Proxy to use.
|<<content.proxy_dns_requests,content.proxy_dns_requests>>|Send DNS requests over the configured proxy.
|<<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.user_stylesheets,content.user_stylesheets>>|List of user stylesheet filenames to use.
|<<content.webgl,content.webgl>>|Enable WebGL.
@@ -181,7 +184,7 @@
|<<downloads.open_dispatcher,downloads.open_dispatcher>>|Default program used to open downloads.
|<<downloads.position,downloads.position>>|Where to show the downloaded files.
|<<downloads.remove_finished,downloads.remove_finished>>|Duration (in milliseconds) to wait before removing finished downloads.
-|<<editor.command,editor.command>>|Editor (and arguments) to use for the `open-editor` command. The following placeholders are defined:
+|<<editor.command,editor.command>>|Editor (and arguments) to use for the `open-editor` command.
|<<editor.encoding,editor.encoding>>|Encoding to use for the editor.
|<<fonts.completion.category,fonts.completion.category>>|Font used in the completion categories.
|<<fonts.completion.entry,fonts.completion.entry>>|Font used in the completion widget.
@@ -244,6 +247,7 @@
|<<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.
+|<<qt.force_platformtheme,qt.force_platformtheme>>|Force a Qt platformtheme to use.
|<<qt.force_software_rendering,qt.force_software_rendering>>|Force software rendering for QtWebEngine.
|<<qt.highdpi,qt.highdpi>>|Turn on Qt HighDPI scaling.
|<<qt.low_end_device_mode,qt.low_end_device_mode>>|When to use Chromium's low-end device mode.
@@ -858,8 +862,8 @@ Type: <<types,QssColor>>
Default: +pass:[white]+
-[[colors.contextmenu.bg]]
-=== colors.contextmenu.bg
+[[colors.contextmenu.menu.bg]]
+=== colors.contextmenu.menu.bg
Background color of the context menu.
If set to null, the Qt default is used.
@@ -867,8 +871,8 @@ Type: <<types,QssColor>>
Default: empty
-[[colors.contextmenu.fg]]
-=== colors.contextmenu.fg
+[[colors.contextmenu.menu.fg]]
+=== colors.contextmenu.menu.fg
Foreground color of the context menu.
If set to null, the Qt default is used.
@@ -876,6 +880,24 @@ Type: <<types,QssColor>>
Default: empty
+[[colors.contextmenu.selected.bg]]
+=== colors.contextmenu.selected.bg
+Background color of the context menu's selected item.
+If set to null, the Qt default is used.
+
+Type: <<types,QssColor>>
+
+Default: empty
+
+[[colors.contextmenu.selected.fg]]
+=== colors.contextmenu.selected.fg
+Foreground color of the context menu's selected item.
+If set to null, the Qt default is used.
+
+Type: <<types,QssColor>>
+
+Default: empty
+
[[colors.downloads.bar.bg]]
=== colors.downloads.bar.bg
Background color for the download bar.
@@ -1840,14 +1862,30 @@ Default: +pass:[same-domain]+
[[content.headers.user_agent]]
=== content.headers.user_agent
-User agent to send. Unset to send the default.
+User agent to send.
+
+The following placeholders are defined:
+
+* `{os_info}`: Something like "X11; Linux x86_64".
+* `{webkit_version}`: The underlying WebKit version (set to a fixed value
+ with QtWebEngine).
+* `{qt_key}`: "Qt" for QtWebKit, "QtWebEngine" for QtWebEngine.
+* `{qt_version}`: The underlying Qt version.
+* `{upstream_browser_key}`: "Version" for QtWebKit, "Chrome" for QtWebEngine.
+* `{upstream_browser_version}`: The corresponding Safari/Chrome version.
+* `{qutebrowser_version}`: The currently running qutebrowser version.
+
+The default value is equal to the unchanged user agent of
+QtWebKit/QtWebEngine.
+
Note that the value read from JavaScript is always the global value.
+
This setting supports URL patterns.
-Type: <<types,String>>
+Type: <<types,FormatString>>
-Default: empty
+Default: +pass:[Mozilla/5.0 ({os_info}) AppleWebKit/{webkit_version} (KHTML, like Gecko) {qt_key}/{qt_version} {upstream_browser_key}/{upstream_browser_version} Safari/{webkit_version}]+
[[content.host_blocking.enabled]]
=== content.host_blocking.enabled
@@ -2206,6 +2244,15 @@ On QtWebEngine, this setting requires Qt 5.11 or newer.
On QtWebKit, this setting is unavailable.
+[[content.site_specific_quirks]]
+=== content.site_specific_quirks
+Enable quirks (such as faked user agent headers) needed to get specific sites to work properly.
+This setting requires a restart.
+
+Type: <<types,Bool>>
+
+Default: +pass:[true]+
+
[[content.ssl_strict]]
=== content.ssl_strict
Validate SSL handshakes.
@@ -2355,8 +2402,15 @@ Default: +pass:[-1]+
[[editor.command]]
=== editor.command
-Editor (and arguments) to use for the `open-editor` command. The following placeholders are defined:
-* `{file}`: Filename of the file to be edited. * `{line}`: Line in which the caret is found in the text. * `{column}`: Column in which the caret is found in the text. * `{line0}`: Same as `{line}`, but starting from index 0. * `{column0}`: Same as `{column}`, but starting from index 0.
+Editor (and arguments) to use for the `open-editor` command.
+The following placeholders are defined:
+
+* `{file}`: Filename of the file to be edited.
+* `{line}`: Line in which the caret is found in the text.
+* `{column}`: Column in which the caret is found in the text.
+* `{line0}`: Same as `{line}`, but starting from index 0.
+* `{column0}`: Same as `{column}`, but starting from index 0.
+
Type: <<types,ShellCommand>>
@@ -3003,6 +3057,16 @@ Type: <<types,String>>
Default: empty
+[[qt.force_platformtheme]]
+=== qt.force_platformtheme
+Force a Qt platformtheme to use.
+This sets the `QT_QPA_PLATFORMTHEME` environment variable which controls dialogs like the filepicker. By default, Qt determines the platform theme based on the desktop environment.
+This setting requires a restart.
+
+Type: <<types,String>>
+
+Default: empty
+
[[qt.force_software_rendering]]
=== qt.force_software_rendering
Force software rendering for QtWebEngine.
diff --git a/misc/requirements/requirements-pyqt-5.14.txt b/misc/requirements/requirements-pyqt-5.14.txt
new file mode 100644
index 000000000..5c713f7b4
--- /dev/null
+++ b/misc/requirements/requirements-pyqt-5.14.txt
@@ -0,0 +1,5 @@
+# This file is automatically generated by scripts/dev/recompile_requirements.py
+
+PyQt5==5.14.0 # rq.filter: < 5.15
+PyQt5-sip==12.7.0
+PyQtWebEngine==5.14.0 # rq.filter: < 5.15
diff --git a/misc/requirements/requirements-pyqt-5.14.txt-raw b/misc/requirements/requirements-pyqt-5.14.txt-raw
new file mode 100644
index 000000000..9dadfc846
--- /dev/null
+++ b/misc/requirements/requirements-pyqt-5.14.txt-raw
@@ -0,0 +1,4 @@
+#@ filter: PyQt5 < 5.15
+#@ filter: PyQtWebEngine < 5.15
+PyQt5 >= 5.14, < 5.15
+PyQtWebEngine >= 5.14, < 5.15
diff --git a/misc/requirements/requirements-pyqt.txt b/misc/requirements/requirements-pyqt.txt
index b616d29e1..1c1a8aabd 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.13.2
+PyQt5==5.14.0
PyQt5-sip==12.7.0
-PyQtWebEngine==5.13.2
+PyQtWebEngine==5.14.0
diff --git a/misc/userscripts/qute-lastpass b/misc/userscripts/qute-lastpass
index ea88cf86f..6845a4cda 100755
--- a/misc/userscripts/qute-lastpass
+++ b/misc/userscripts/qute-lastpass
@@ -19,7 +19,7 @@
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""
-Insert login information using lastpass CLI and a dmenu-compatible application (e.g. dmenu, rofi -dmenu, ...).
+Insert login information using lastpass CLI and a dmenu-compatible application (e.g. dmenu, rofi -dmenu, ...).
A short demonstration can be seen here: https://i.imgur.com/zA61NrF.gifv.
"""
@@ -85,15 +85,12 @@ def pass_(domain, encoding):
args = ['lpass', 'show', '-x', '-j', '-G', '.*{:s}.*'.format(domain)]
process = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ candidates = json.loads(process.stdout.decode(encoding).strip() or '[]')
err = process.stderr.decode(encoding).strip()
- if err:
- msg = "LastPass CLI returned for {:s} - {:s}".format(domain, err)
- stderr(msg)
- return '[]'
+ if 'could not find specified account' in err.lower():
+ return candidates, ''
- out = process.stdout.decode(encoding).strip()
-
- return out
+ return candidates, err
def dmenu(items, invocation, encoding):
command = shlex.split(invocation)
@@ -121,7 +118,11 @@ def main(arguments):
# the URL represents
candidates = []
for target in filter(None, [extract_result.fqdn, extract_result.registered_domain, extract_result.subdomain + extract_result.domain, extract_result.domain, extract_result.ipv4]):
- target_candidates = json.loads(pass_(target, arguments.io_encoding))
+ target_candidates, err = pass_(target, arguments.io_encoding)
+ if err:
+ stderr("LastPass CLI returned for {:s} - {:s}".format(target, err))
+ return ExitCodes.FAILURE
+
if not target_candidates:
continue
diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py
index 155421952..d429639f3 100644
--- a/qutebrowser/browser/browsertab.py
+++ b/qutebrowser/browser/browsertab.py
@@ -821,14 +821,6 @@ class AbstractTabPrivate:
"""
raise NotImplementedError
- def user_agent(self) -> typing.Optional[str]:
- """Get the user agent for this tab.
-
- This is only implemented for QtWebKit.
- For QtWebEngine, always returns None.
- """
- raise NotImplementedError
-
def shutdown(self) -> None:
raise NotImplementedError
diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py
index f4cabaccd..ab7e60aed 100644
--- a/qutebrowser/browser/commands.py
+++ b/qutebrowser/browser/commands.py
@@ -1273,7 +1273,6 @@ class CommandDispatcher:
target = downloads.FileDownloadTarget(dest)
tab = self._current_widget()
- user_agent = tab.private_api.user_agent()
if url:
if mhtml_:
@@ -1281,7 +1280,7 @@ class CommandDispatcher:
"page as mhtml.")
url = urlutils.qurl_from_user_input(url)
urlutils.raise_cmdexc_if_invalid(url)
- download_manager.get(url, user_agent=user_agent, target=target)
+ download_manager.get(url, target=target)
elif mhtml_:
tab = self._current_widget()
if tab.backend == usertypes.Backend.QtWebEngine:
@@ -1302,7 +1301,6 @@ class CommandDispatcher:
download_manager.get(
self._current_url(),
- user_agent=user_agent,
qnam=qnam,
target=target,
suggested_fn=suggested_fn
diff --git a/qutebrowser/browser/downloadview.py b/qutebrowser/browser/downloadview.py
index 91a583d08..b0944046b 100644
--- a/qutebrowser/browser/downloadview.py
+++ b/qutebrowser/browser/downloadview.py
@@ -26,7 +26,7 @@ from PyQt5.QtCore import pyqtSlot, QSize, Qt, QTimer
from PyQt5.QtWidgets import QListView, QSizePolicy, QMenu, QStyleFactory
from qutebrowser.browser import downloads
-from qutebrowser.config import config
+from qutebrowser.config import stylesheet
from qutebrowser.utils import qtutils, utils
from qutebrowser.qt import sip
@@ -85,7 +85,7 @@ class DownloadView(QListView):
super().__init__(parent)
if not utils.is_mac:
self.setStyle(QStyleFactory.create('Fusion'))
- config.set_register_stylesheet(self)
+ stylesheet.set_register(self)
self.setResizeMode(QListView.Adjust)
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed)
diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py
index 257f0bce9..1c61e976d 100644
--- a/qutebrowser/browser/hints.py
+++ b/qutebrowser/browser/hints.py
@@ -286,12 +286,10 @@ class HintActions:
prompt = False if context.rapid else None
qnam = context.tab.private_api.networkaccessmanager()
- user_agent = context.tab.private_api.user_agent()
# FIXME:qtwebengine do this with QtWebEngine downloads?
download_manager = objreg.get('qtnetwork-download-manager')
- download_manager.get(url, qnam=qnam, user_agent=user_agent,
- prompt_download_directory=prompt)
+ download_manager.get(url, qnam=qnam, prompt_download_directory=prompt)
def call_userscript(self, elem: webelem.AbstractWebElement,
context: HintContext) -> None:
diff --git a/qutebrowser/browser/qtnetworkdownloads.py b/qutebrowser/browser/qtnetworkdownloads.py
index bc76d3daa..779574419 100644
--- a/qutebrowser/browser/qtnetworkdownloads.py
+++ b/qutebrowser/browser/qtnetworkdownloads.py
@@ -29,7 +29,7 @@ import attr
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QTimer, QUrl
from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply
-from qutebrowser.config import config
+from qutebrowser.config import config, websettings
from qutebrowser.utils import message, usertypes, log, urlutils, utils, debug
from qutebrowser.browser import downloads
from qutebrowser.browser.webkit import http
@@ -414,12 +414,11 @@ class DownloadManager(downloads.AbstractDownloadManager):
private=config.val.content.private_browsing, parent=self)
@pyqtSlot('QUrl')
- def get(self, url, *, user_agent=None, **kwargs):
+ def get(self, url, **kwargs):
"""Start a download with a link URL.
Args:
url: The URL to get, as QUrl
- user_agent: The UA to set for the request, or None.
**kwargs: passed to get_request().
Return:
@@ -428,9 +427,11 @@ class DownloadManager(downloads.AbstractDownloadManager):
if not url.isValid():
urlutils.invalid_url_error(url, "start download")
return None
+
req = QNetworkRequest(url)
- if user_agent is not None:
- req.setHeader(QNetworkRequest.UserAgentHeader, user_agent)
+ user_agent = websettings.user_agent(url)
+ req.setHeader(QNetworkRequest.UserAgentHeader, user_agent)
+
return self.get_request(req, **kwargs)
def get_mhtml(self, tab, target):
diff --git a/qutebrowser/browser/webelem.py b/qutebrowser/browser/webelem.py
index 59abd8113..10abb6f2d 100644
--- a/qutebrowser/browser/webelem.py
+++ b/qutebrowser/browser/webelem.py
@@ -218,6 +218,7 @@ class AbstractWebElement(collections.abc.MutableMapping):
'kix-', # Google Docs editor
'ace_'], # http://ace.c9.io/
'pre': ['CodeMirror'],
+ 'span': ['cm-'], # Jupyter Notebook
}
relevant_classes = classes[self.tag_name()]
for klass in self.classes():
@@ -252,7 +253,7 @@ class AbstractWebElement(collections.abc.MutableMapping):
return config.val.input.insert_mode.plugins and not strict
elif tag == 'object':
return self._is_editable_object() and not strict
- elif tag in ['div', 'pre']:
+ elif tag in ['div', 'pre', 'span']:
return self._is_editable_classes() and not strict
return False
diff --git a/qutebrowser/browser/webengine/interceptor.py b/qutebrowser/browser/webengine/interceptor.py
index 55badb813..02f75fa0c 100644
--- a/qutebrowser/browser/webengine/interceptor.py
+++ b/qutebrowser/browser/webengine/interceptor.py
@@ -25,7 +25,7 @@ from PyQt5.QtCore import QUrl, QByteArray
from PyQt5.QtWebEngineCore import (QWebEngineUrlRequestInterceptor,
QWebEngineUrlRequestInfo)
-from qutebrowser.config import config
+from qutebrowser.config import websettings
from qutebrowser.browser import shared
from qutebrowser.utils import utils, log, debug, qtutils
from qutebrowser.extensions import interceptors
@@ -63,49 +63,65 @@ class WebEngineRequest(interceptors.Request):
class RequestInterceptor(QWebEngineUrlRequestInterceptor):
"""Handle ad blocking and custom headers."""
- # This dict should be from QWebEngine Resource Types to qutebrowser
- # extension ResourceTypes. If a ResourceType is added to Qt, this table
- # should be updated too.
- RESOURCE_TYPES = {
- QWebEngineUrlRequestInfo.ResourceTypeMainFrame:
- interceptors.ResourceType.main_frame,
- QWebEngineUrlRequestInfo.ResourceTypeSubFrame:
- interceptors.ResourceType.sub_frame,
- QWebEngineUrlRequestInfo.ResourceTypeStylesheet:
- interceptors.ResourceType.stylesheet,
- QWebEngineUrlRequestInfo.ResourceTypeScript:
- interceptors.ResourceType.script,
- QWebEngineUrlRequestInfo.ResourceTypeImage:
- interceptors.ResourceType.image,
- QWebEngineUrlRequestInfo.ResourceTypeFontResource:
- interceptors.ResourceType.font_resource,
- QWebEngineUrlRequestInfo.ResourceTypeSubResource:
- interceptors.ResourceType.sub_resource,
- QWebEngineUrlRequestInfo.ResourceTypeObject:
- interceptors.ResourceType.object,
- QWebEngineUrlRequestInfo.ResourceTypeMedia:
- interceptors.ResourceType.media,
- QWebEngineUrlRequestInfo.ResourceTypeWorker:
- interceptors.ResourceType.worker,
- QWebEngineUrlRequestInfo.ResourceTypeSharedWorker:
- interceptors.ResourceType.shared_worker,
- QWebEngineUrlRequestInfo.ResourceTypePrefetch:
- interceptors.ResourceType.prefetch,
- QWebEngineUrlRequestInfo.ResourceTypeFavicon:
- interceptors.ResourceType.favicon,
- QWebEngineUrlRequestInfo.ResourceTypeXhr:
- interceptors.ResourceType.xhr,
- QWebEngineUrlRequestInfo.ResourceTypePing:
- interceptors.ResourceType.ping,
- QWebEngineUrlRequestInfo.ResourceTypeServiceWorker:
- interceptors.ResourceType.service_worker,
- QWebEngineUrlRequestInfo.ResourceTypeCspReport:
- interceptors.ResourceType.csp_report,
- QWebEngineUrlRequestInfo.ResourceTypePluginResource:
- interceptors.ResourceType.plugin_resource,
- QWebEngineUrlRequestInfo.ResourceTypeUnknown:
- interceptors.ResourceType.unknown,
- }
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ # This dict should be from QWebEngine Resource Types to qutebrowser
+ # extension ResourceTypes. If a ResourceType is added to Qt, this table
+ # should be updated too.
+ self._resource_types = {
+ QWebEngineUrlRequestInfo.ResourceTypeMainFrame:
+ interceptors.ResourceType.main_frame,
+ QWebEngineUrlRequestInfo.ResourceTypeSubFrame:
+ interceptors.ResourceType.sub_frame,
+ QWebEngineUrlRequestInfo.ResourceTypeStylesheet:
+ interceptors.ResourceType.stylesheet,
+ QWebEngineUrlRequestInfo.ResourceTypeScript:
+ interceptors.ResourceType.script,
+ QWebEngineUrlRequestInfo.ResourceTypeImage:
+ interceptors.ResourceType.image,
+ QWebEngineUrlRequestInfo.ResourceTypeFontResource:
+ interceptors.ResourceType.font_resource,
+ QWebEngineUrlRequestInfo.ResourceTypeSubResource:
+ interceptors.ResourceType.sub_resource,
+ QWebEngineUrlRequestInfo.ResourceTypeObject:
+ interceptors.ResourceType.object,
+ QWebEngineUrlRequestInfo.ResourceTypeMedia:
+ interceptors.ResourceType.media,
+ QWebEngineUrlRequestInfo.ResourceTypeWorker:
+ interceptors.ResourceType.worker,
+ QWebEngineUrlRequestInfo.ResourceTypeSharedWorker:
+ interceptors.ResourceType.shared_worker,
+ QWebEngineUrlRequestInfo.ResourceTypePrefetch:
+ interceptors.ResourceType.prefetch,
+ QWebEngineUrlRequestInfo.ResourceTypeFavicon:
+ interceptors.ResourceType.favicon,
+ QWebEngineUrlRequestInfo.ResourceTypeXhr:
+ interceptors.ResourceType.xhr,
+ QWebEngineUrlRequestInfo.ResourceTypePing:
+ interceptors.ResourceType.ping,
+ QWebEngineUrlRequestInfo.ResourceTypeServiceWorker:
+ interceptors.ResourceType.service_worker,
+ QWebEngineUrlRequestInfo.ResourceTypeCspReport:
+ interceptors.ResourceType.csp_report,
+ QWebEngineUrlRequestInfo.ResourceTypePluginResource:
+ interceptors.ResourceType.plugin_resource,
+ QWebEngineUrlRequestInfo.ResourceTypeUnknown:
+ interceptors.ResourceType.unknown,
+ }
+
+ try:
+ preload_main_frame = (QWebEngineUrlRequestInfo.
+ ResourceTypeNavigationPreloadMainFrame)
+ preload_sub_frame = (QWebEngineUrlRequestInfo.
+ ResourceTypeNavigationPreloadSubFrame)
+ except AttributeError:
+ # Added in Qt 5.14
+ pass
+ else:
+ self._resource_types[preload_main_frame] = (
+ interceptors.ResourceType.preload_main_frame)
+ self._resource_types[preload_sub_frame] = (
+ interceptors.ResourceType.preload_sub_frame)
def install(self, profile):
"""Install the interceptor on the given QWebEngineProfile."""
@@ -155,8 +171,7 @@ class RequestInterceptor(QWebEngineUrlRequestInterceptor):
# Per QWebEngineUrlRequestInfo::ResourceType documentation, if we fail
# our lookup, we should fall back to ResourceTypeUnknown
try:
- resource_type = RequestInterceptor.RESOURCE_TYPES[
- info.resourceType()]
+ resource_type = self._resource_types[info.resourceType()]
except KeyError:
log.webview.warning(
"Resource type {} not found in RequestInterceptor dict."
@@ -189,6 +204,5 @@ class RequestInterceptor(QWebEngineUrlRequestInterceptor):
for header, value in shared.custom_headers(url=url):
info.setHttpHeader(header, value)
- user_agent = config.instance.get('content.headers.user_agent', url=url)
- if user_agent is not None:
- info.setHttpHeader(b'User-Agent', user_agent.encode('ascii'))
+ user_agent = websettings.user_agent(url)
+ info.setHttpHeader(b'User-Agent', user_agent.encode('ascii'))
diff --git a/qutebrowser/browser/webengine/webengineinspector.py b/qutebrowser/browser/webengine/webengineinspector.py
index 4bf72502c..28919643b 100644
--- a/qutebrowser/browser/webengine/webengineinspector.py
+++ b/qutebrowser/browser/webengine/webengineinspector.py
@@ -22,9 +22,10 @@
import os
from PyQt5.QtCore import QUrl
-from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEngineSettings
+from PyQt5.QtWebEngineWidgets import QWebEngineView
from qutebrowser.browser import inspector
+from qutebrowser.browser.webengine import webenginesettings
class WebEngineInspector(inspector.AbstractWebInspector):
@@ -35,8 +36,7 @@ class WebEngineInspector(inspector.AbstractWebInspector):
super().__init__(parent)
self.port = None
view = QWebEngineView()
- settings = view.settings()
- settings.setAttribute(QWebEngineSettings.JavascriptEnabled, True)
+ self._settings = webenginesettings.WebEngineSettings(view.settings())
self._set_widget(view)
def _inspect_old(self, page):
@@ -47,16 +47,21 @@ class WebEngineInspector(inspector.AbstractWebInspector):
raise inspector.WebInspectorError(
"QtWebEngine inspector is not enabled. See "
"'qutebrowser --help' for details.")
- url = QUrl('http://localhost:{}/'.format(port))
+
+ # We're lying about the URL here a bit, but this way, URL patterns for
+ # Qt 5.11/5.12/5.13 also work in this case.
+ self._settings.update_for_url(QUrl('chrome-devtools://devtools'))
if page is None:
self._widget.load(QUrl('about:blank'))
else:
- self._widget.load(url)
+ self._widget.load(QUrl('http://localhost:{}/'.format(port)))
def _inspect_new(self, page):
"""Set up the inspector for Qt >= 5.11."""
- self._widget.page().setInspectedPage(page)
+ inspector_page = self._widget.page()
+ inspector_page.setInspectedPage(page)
+ self._settings.update_for_url(inspector_page.requestedUrl())
def inspect(self, page):
try:
diff --git a/qutebrowser/browser/webengine/webenginesettings.py b/qutebrowser/browser/webengine/webenginesettings.py
index 3cf4a8adc..174564c39 100644
--- a/qutebrowser/browser/webengine/webenginesettings.py
+++ b/qutebrowser/browser/webengine/webenginesettings.py
@@ -35,7 +35,8 @@ from PyQt5.QtWebEngineWidgets import (QWebEngineSettings, QWebEngineProfile,
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
+from qutebrowser.utils import (utils, standarddir, qtutils, message, log,
+ urlmatch)
# The default QWebEngineProfile
default_profile = typing.cast(QWebEngineProfile, None)
@@ -44,7 +45,7 @@ private_profile = None # type: typing.Optional[QWebEngineProfile]
# The global WebEngineSettings object
global_settings = typing.cast('WebEngineSettings', None)
-default_user_agent = None
+parsed_user_agent = None
class _SettingsWrapper:
@@ -228,7 +229,9 @@ class ProfileSetter:
per-domain values), but this one still gets used for things like
window.navigator.userAgent/.languages in JS.
"""
- self._profile.setHttpUserAgent(config.val.content.headers.user_agent)
+ user_agent = websettings.user_agent()
+ self._profile.setHttpUserAgent(user_agent)
+
accept_language = config.val.content.headers.accept_language
if accept_language is not None:
self._profile.setHttpAcceptLanguage(accept_language)
@@ -296,12 +299,22 @@ def _update_settings(option):
private_profile.setter.set_dictionary_language(warn=False)
+def _init_user_agent_str(ua):
+ global parsed_user_agent
+ parsed_user_agent = websettings.UserAgent.parse(ua)
+
+
+def init_user_agent():
+ _init_user_agent_str(QWebEngineProfile.defaultProfile().httpUserAgent())
+
+
def _init_profiles():
"""Init the two used QWebEngineProfiles."""
- global default_profile, private_profile, default_user_agent
+ global default_profile, private_profile
default_profile = QWebEngineProfile.defaultProfile()
- default_user_agent = default_profile.httpUserAgent()
+ init_user_agent()
+
default_profile.setter = ProfileSetter(default_profile)
default_profile.setCachePath(
os.path.join(standarddir.cache(), 'webengine'))
@@ -317,6 +330,50 @@ def _init_profiles():
private_profile.setter.init_profile()
+def _init_site_specific_quirks():
+ if not config.val.content.site_specific_quirks:
+ return
+
+ # default_ua = ("Mozilla/5.0 ({os_info}) "
+ # "AppleWebKit/{webkit_version} (KHTML, like Gecko) "
+ # "{qt_key}/{qt_version} "
+ # "{upstream_browser_key}/{upstream_browser_version} "
+ # "Safari/{webkit_version}")
+ no_qtwe_ua = ("Mozilla/5.0 ({os_info}) "
+ "AppleWebKit/{webkit_version} (KHTML, like Gecko) "
+ "{upstream_browser_key}/{upstream_browser_version} "
+ "Safari/{webkit_version}")
+ firefox_ua = "Mozilla/5.0 ({os_info}; rv:71.0) Gecko/20100101 Firefox/71.0"
+ new_chrome_ua = ("Mozilla/5.0 ({os_info}) "
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
+ "Chrome/99 "
+ "Safari/537.36")
+
+ user_agents = {
+ 'https://web.whatsapp.com/': no_qtwe_ua,
+ 'https://accounts.google.com/*': firefox_ua,
+ 'https://*.slack.com/*': new_chrome_ua,
+ 'https://docs.google.com/*': firefox_ua,
+ }
+
+ if not qtutils.version_check('5.9'):
+ user_agents['https://www.dell.com/support/*'] = new_chrome_ua
+
+ for pattern, ua in user_agents.items():
+ config.instance.set_obj('content.headers.user_agent', ua,
+ pattern=urlmatch.UrlPattern(pattern),
+ hide_userconfig=True)
+
+
+def _init_devtools_settings():
+ """Make sure the devtools always get images/JS permissions."""
+ for setting in ['content.javascript.enabled', 'content.images']:
+ for pattern in ['chrome-devtools://*', 'devtools://*']:
+ config.instance.set_obj(setting, True,
+ pattern=urlmatch.UrlPattern(pattern),
+ hide_userconfig=True)
+
+
def init(args):
"""Initialize the global QWebSettings."""
if (args.enable_webengine_inspector and
@@ -333,6 +390,9 @@ def init(args):
global_settings = WebEngineSettings(_SettingsWrapper())
global_settings.init_settings()
+ _init_site_specific_quirks()
+ _init_devtools_settings()
+
def shutdown():
pass
diff --git a/qutebrowser/browser/webkit/webkitsettings.py b/qutebrowser/browser/webkit/webkitsettings.py
index 3c5eeb545..74b8a49a8 100644
--- a/qutebrowser/browser/webkit/webkitsettings.py
+++ b/qutebrowser/browser/webkit/webkitsettings.py
@@ -27,8 +27,10 @@ Module attributes:
import typing
import os.path
+from PyQt5.QtCore import QUrl
from PyQt5.QtGui import QFont
from PyQt5.QtWebKit import QWebSettings
+from PyQt5.QtWebKitWidgets import QWebPage
from qutebrowser.config import config, websettings
from qutebrowser.config.websettings import AttributeInfo as Attr
@@ -39,6 +41,8 @@ from qutebrowser.browser import shared
# The global WebKitSettings object
global_settings = typing.cast('WebKitSettings', None)
+parsed_user_agent = None
+
class WebKitSettings(websettings.AbstractSettings):
@@ -78,6 +82,8 @@ class WebKitSettings(websettings.AbstractSettings):
Attr(QWebSettings.PrintElementBackgrounds),
'content.xss_auditing':
Attr(QWebSettings.XSSAuditingEnabled),
+ 'content.site_specific_quirks':
+ Attr(QWebSettings.SiteSpecificQuirksEnabled),
'input.spatial_navigation':
Attr(QWebSettings.SpatialNavigationEnabled),
@@ -160,6 +166,12 @@ def _update_settings(option):
_set_cache_maximum_pages(settings)
+def _init_user_agent():
+ global parsed_user_agent
+ ua = QWebPage().userAgentForUrl(QUrl())
+ parsed_user_agent = websettings.UserAgent.parse(ua)
+
+
def init(_args):
"""Initialize the global QWebSettings."""
cache_path = standarddir.cache()
@@ -178,6 +190,8 @@ def init(_args):
_set_cookie_accept_policy(settings)
_set_cache_maximum_pages(settings)
+ _init_user_agent()
+
config.instance.changed.connect(_update_settings)
global global_settings
diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py
index 5feb9aee8..1c239ffe3 100644
--- a/qutebrowser/browser/webkit/webkittab.py
+++ b/qutebrowser/browser/webkit/webkittab.py
@@ -682,10 +682,6 @@ class WebKitTabPrivate(browsertab.AbstractTabPrivate):
def networkaccessmanager(self):
return self._widget.page().networkAccessManager()
- def user_agent(self):
- page = self._widget.page()
- return page.userAgentForUrl(self._tab.url())
-
def clear_ssl_errors(self):
self.networkaccessmanager().clear_all_ssl_errors()
diff --git a/qutebrowser/browser/webkit/webpage.py b/qutebrowser/browser/webkit/webpage.py
index 7069c1699..743e550f0 100644
--- a/qutebrowser/browser/webkit/webpage.py
+++ b/qutebrowser/browser/webkit/webpage.py
@@ -29,7 +29,7 @@ from PyQt5.QtWidgets import QFileDialog
from PyQt5.QtPrintSupport import QPrintDialog
from PyQt5.QtWebKitWidgets import QWebPage, QWebFrame
-from qutebrowser.config import config
+from qutebrowser.config import websettings
from qutebrowser.browser import pdfjs, shared, downloads, greasemonkey
from qutebrowser.browser.webkit import http
from qutebrowser.browser.webkit.network import networkmanager
@@ -411,11 +411,7 @@ class BrowserPage(QWebPage):
def userAgentForUrl(self, url):
"""Override QWebPage::userAgentForUrl to customize the user agent."""
- ua = config.instance.get('content.headers.user_agent', url=url)
- if ua is None:
- return super().userAgentForUrl(url)
- else:
- return ua
+ return websettings.user_agent(url)
def supportsExtension(self, ext):
"""Override QWebPage::supportsExtension to provide error pages.
diff --git a/qutebrowser/browser/webkit/webview.py b/qutebrowser/browser/webkit/webview.py
index 1cd9d0a2a..88daf06aa 100644
--- a/qutebrowser/browser/webkit/webview.py
+++ b/qutebrowser/browser/webkit/webview.py
@@ -24,7 +24,7 @@ from PyQt5.QtWidgets import QStyleFactory
from PyQt5.QtWebKit import QWebSettings
from PyQt5.QtWebKitWidgets import QWebView, QWebPage
-from qutebrowser.config import config
+from qutebrowser.config import config, stylesheet
from qutebrowser.keyinput import modeman
from qutebrowser.utils import log, usertypes, utils, objreg, debug
from qutebrowser.browser.webkit import webpage
@@ -84,7 +84,7 @@ class WebView(QWebView):
self.setPage(page)
- config.set_register_stylesheet(self)
+ stylesheet.set_register(self)
def __repr__(self):
urlstr = self.url().toDisplayString(QUrl.EncodeUnicode) # type: ignore
diff --git a/qutebrowser/commands/cmdexc.py b/qutebrowser/commands/cmdexc.py
index 5eb465a24..8398950a9 100644
--- a/qutebrowser/commands/cmdexc.py
+++ b/qutebrowser/commands/cmdexc.py
@@ -30,12 +30,12 @@ class Error(Exception):
class NoSuchCommandError(Error):
- """Raised when a command wasn't found."""
+ """Raised when a command isn't found."""
class ArgumentTypeError(Error):
- """Raised when an argument had an invalid type."""
+ """Raised when an argument is an invalid type."""
class PrerequisitesError(Error):
@@ -43,5 +43,5 @@ class PrerequisitesError(Error):
"""Raised when a cmd can't be used because some prerequisites aren't met.
This is raised for example when we're in the wrong mode while executing the
- command, or we need javascript enabled but don't have done so.
+ command, or we need javascript enabled but haven't done so.
"""
diff --git a/qutebrowser/commands/userscripts.py b/qutebrowser/commands/userscripts.py
index 439af695c..8ff19c7d8 100644
--- a/qutebrowser/commands/userscripts.py
+++ b/qutebrowser/commands/userscripts.py
@@ -28,7 +28,7 @@ from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QSocketNotifier
from qutebrowser.utils import message, log, objreg, standarddir, utils
from qutebrowser.commands import runners
-from qutebrowser.config import config
+from qutebrowser.config import websettings
from qutebrowser.misc import guiprocess
from qutebrowser.browser import downloads
@@ -429,10 +429,8 @@ def run_async(tab, cmd, *args, win_id, env, verbose=False):
lambda cmd:
log.commands.debug("Got userscript command: {}".format(cmd)))
runner.got_cmd.connect(commandrunner.run_safely)
- user_agent = config.val.content.headers.user_agent
- if user_agent is not None:
- env['QUTE_USER_AGENT'] = user_agent
+ env['QUTE_USER_AGENT'] = websettings.user_agent()
env['QUTE_CONFIG_DIR'] = standarddir.config()
env['QUTE_DATA_DIR'] = standarddir.data()
env['QUTE_DOWNLOAD_DIR'] = downloads.download_dir()
diff --git a/qutebrowser/completion/completionwidget.py b/qutebrowser/completion/completionwidget.py
index 4d90a4305..5b1b080e4 100644
--- a/qutebrowser/completion/completionwidget.py
+++ b/qutebrowser/completion/completionwidget.py
@@ -28,7 +28,7 @@ import typing
from PyQt5.QtWidgets import QTreeView, QSizePolicy, QStyleFactory, QWidget
from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QItemSelectionModel, QSize
-from qutebrowser.config import config
+from qutebrowser.config import config, stylesheet
from qutebrowser.completion import completiondelegate
from qutebrowser.utils import utils, usertypes, debug, log
from qutebrowser.api import cmdutils
@@ -125,7 +125,7 @@ class CompletionView(QTreeView):
self._delegate = completiondelegate.CompletionItemDelegate(self)
self.setItemDelegate(self._delegate)
self.setStyle(QStyleFactory.create('Fusion'))
- config.set_register_stylesheet(self)
+ stylesheet.set_register(self)
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self.setHeaderHidden(True)
self.setAlternatingRowColors(True)
diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py
index fdb55ce4b..41ef6eb3d 100644
--- a/qutebrowser/config/config.py
+++ b/qutebrowser/config/config.py
@@ -23,13 +23,13 @@ import copy
import contextlib
import functools
import typing
-from typing import Any, Optional, FrozenSet
+from typing import Any, Optional
-from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QUrl
+from PyQt5.QtCore import pyqtSignal, QObject, QUrl
from qutebrowser.config import configdata, configexc, configutils
-from qutebrowser.utils import utils, log, jinja, urlmatch
-from qutebrowser.misc import objects, debugcachestats
+from qutebrowser.utils import utils, log, urlmatch
+from qutebrowser.misc import objects
from qutebrowser.keyinput import keyutils
if typing.TYPE_CHECKING:
@@ -278,7 +278,6 @@ class Config(QObject):
yaml_config: 'configfiles.YamlConfig',
parent: QObject = None) -> None:
super().__init__(parent)
- self.changed.connect(_render_stylesheet.cache_clear)
self._mutables = {} # type: MutableMapping[str, Tuple[Any, Any]]
self._yaml = yaml_config
self._init_values()
@@ -307,7 +306,8 @@ class Config(QObject):
def _set_value(self,
opt: 'configdata.Option',
value: Any,
- pattern: urlmatch.UrlPattern = None) -> None:
+ pattern: urlmatch.UrlPattern = None,
+ hide_userconfig: bool = False) -> None:
"""Set the given option to the given value."""
if not isinstance(objects.backend, objects.NoBackend):
if objects.backend not in opt.backends:
@@ -316,7 +316,8 @@ class Config(QObject):
opt.typ.to_py(value) # for validation
- self._values[opt.name].add(opt.typ.from_obj(value), pattern)
+ self._values[opt.name].add(opt.typ.from_obj(value),
+ pattern, hide_userconfig=hide_userconfig)
self.changed.emit(opt.name)
log.config.debug("Config option changed: {} = {}".format(
@@ -395,7 +396,7 @@ class Config(QObject):
"""Get the given setting as object (for YAML/config.py).
This gets the overridden value for a given pattern, or
- configutils.UNSET if no such override exists.
+ usertypes.UNSET if no such override exists.
"""
self.ensure_has_opt(name)
value = self._values[name].get_for_pattern(pattern, fallback=False)
@@ -432,7 +433,7 @@ class Config(QObject):
"""Get the given setting as string.
If a pattern is given, get the setting for the given pattern or
- configutils.UNSET.
+ usertypes.UNSET.
"""
opt = self.get_opt(name)
values = self._values[name]
@@ -442,14 +443,19 @@ class Config(QObject):
def set_obj(self, name: str,
value: Any, *,
pattern: urlmatch.UrlPattern = None,
- save_yaml: bool = False) -> None:
+ save_yaml: bool = False,
+ hide_userconfig: bool = False) -> None:
"""Set the given setting from a YAML/config.py object.
If save_yaml=True is given, store the new value to YAML.
+
+ If hide_userconfig=True is given, hide the value from
+ dump_userconfig().
"""
opt = self.get_opt(name)
self._check_yaml(opt, save_yaml)
- self._set_value(opt, value, pattern=pattern)
+ self._set_value(opt, value, pattern=pattern,
+ hide_userconfig=hide_userconfig)
if save_yaml:
self._yaml.set_obj(name, value, pattern=pattern)
@@ -519,15 +525,14 @@ class Config(QObject):
Return:
The changed config part as string.
"""
- blocks = []
+ lines = [] # type: typing.List[str]
for values in sorted(self, key=lambda v: v.opt.name):
- if values:
- blocks.append(str(values))
+ lines += values.dump()
- if not blocks:
+ if not lines:
return '<Default configuration>'
- return '\n'.join(blocks)
+ return '\n'.join(lines)
class ConfigContainer:
@@ -611,86 +616,3 @@ class ConfigContainer:
return '{}.{}'.format(self._prefix, attr)
else:
return attr
-
-
-def set_register_stylesheet(obj: QObject, *,
- stylesheet: str = None,
- update: bool = True) -> None:
- """Set the stylesheet for an object.
-
- Also, register an update when the config is changed.
-
- Args:
- obj: The object to set the stylesheet for and register.
- Must have a STYLESHEET attribute if stylesheet is not given.
- stylesheet: The stylesheet to use.
- update: Whether to update the stylesheet on config changes.
- """
- observer = StyleSheetObserver(obj, stylesheet, update)
- observer.register()
-
-
-@debugcachestats.register()
-@functools.lru_cache()
-def _render_stylesheet(stylesheet: str) -> str:
- """Render the given stylesheet jinja template."""
- with jinja.environment.no_autoescape():
- template = jinja.environment.from_string(stylesheet)
- return template.render(conf=val)
-
-
-class StyleSheetObserver(QObject):
-
- """Set the stylesheet on the given object and update it on changes.
-
- Attributes:
- _obj: The object to observe.
- _stylesheet: The stylesheet template to use.
- _options: The config options that the stylesheet uses. When it's not
- necessary to listen for config changes, this attribute may be
- None.
- """
-
- def __init__(self, obj: QObject,
- stylesheet: Optional[str], update: bool) -> None:
- super().__init__()
- self._obj = obj
- self._update = update
-
- # We only need to hang around if we are asked to update.
- if update:
- self.setParent(self._obj)
- if stylesheet is None:
- self._stylesheet = obj.STYLESHEET # type: str
- else:
- self._stylesheet = stylesheet
-
- if update:
- self._options = jinja.template_config_variables(
- self._stylesheet) # type: Optional[FrozenSet[str]]
- else:
- self._options = None
-
- def _get_stylesheet(self) -> str:
- """Format a stylesheet based on a template.
-
- Return:
- The formatted template as string.
- """
- return _render_stylesheet(self._stylesheet)
-
- @pyqtSlot(str)
- def _maybe_update_stylesheet(self, option: str) -> None:
- """Update the stylesheet for obj if the option changed affects it."""
- assert self._options is not None
- if option in self._options:
- self._obj.setStyleSheet(self._get_stylesheet())
-
- def register(self) -> None:
- """Do a first update and listen for more."""
- qss = self._get_stylesheet()
- log.config.vdebug( # type: ignore
- "stylesheet for {}: {}".format(self._obj.__class__.__name__, qss))
- self._obj.setStyleSheet(qss)
- if self._update:
- instance.changed.connect(self._maybe_update_stylesheet)
diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml
index 84c157b28..1814c3a5a 100644
--- a/qutebrowser/config/configdata.yml
+++ b/qutebrowser/config/configdata.yml
@@ -178,6 +178,19 @@ qt.force_platform:
This sets the `QT_QPA_PLATFORM` environment variable and is useful to force
using the XCB plugin when running QtWebEngine on Wayland.
+qt.force_platformtheme:
+ type:
+ name: String
+ none_ok: true
+ default: null
+ restart: true
+ desc: >-
+ Force a Qt platformtheme to use.
+
+ This sets the `QT_QPA_PLATFORMTHEME` environment variable which controls
+ dialogs like the filepicker. By default, Qt determines the platform theme
+ based on the desktop environment.
+
qt.process_model:
type:
name: String
@@ -395,6 +408,15 @@ content.frame_flattening:
This will flatten all the frames to become one scrollable page.
+content.site_specific_quirks:
+ default: true
+ restart: true
+ type: Bool
+ desc: 'Enable quirks (such as faked user agent headers) needed to get
+ specific sites to work properly.'
+
+# emacs: '
+
content.geolocation:
default: ask
type: BoolAsk
@@ -469,10 +491,20 @@ content.headers.referer:
No restart is needed with QtWebKit.
content.headers.user_agent:
- default: null
+ default: 'Mozilla/5.0 ({os_info})
+ AppleWebKit/{webkit_version} (KHTML, like Gecko)
+ {qt_key}/{qt_version} {upstream_browser_key}/{upstream_browser_version}
+ Safari/{webkit_version}'
type:
- name: String
- none_ok: true
+ name: FormatString
+ fields:
+ - os_info
+ - webkit_version
+ - qt_key
+ - qt_version
+ - upstream_browser_key
+ - upstream_browser_version
+ - qutebrowser_version
completions:
# To update the following list of user agents, run the script
# 'ua_fetch.py'
@@ -484,12 +516,23 @@ content.headers.user_agent:
- - "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like
Gecko) Chrome/76.0.3809.132 Safari/537.36"
- Chrome 76 Linux
- - - ""
- - Use default QtWebKit/QtWebEngine User-Agent
-
supports_pattern: true
- desc: >-
- User agent to send. Unset to send the default.
+ desc: |
+ User agent to send.
+
+ The following placeholders are defined:
+
+ * `{os_info}`: Something like "X11; Linux x86_64".
+ * `{webkit_version}`: The underlying WebKit version (set to a fixed value
+ with QtWebEngine).
+ * `{qt_key}`: "Qt" for QtWebKit, "QtWebEngine" for QtWebEngine.
+ * `{qt_version}`: The underlying Qt version.
+ * `{upstream_browser_key}`: "Version" for QtWebKit, "Chrome" for QtWebEngine.
+ * `{upstream_browser_version}`: The corresponding Safari/Chrome version.
+ * `{qutebrowser_version}`: The currently running qutebrowser version.
+
+ The default value is equal to the unchanged user agent of
+ QtWebKit/QtWebEngine.
Note that the value read from JavaScript is always the global value.
@@ -1008,7 +1051,7 @@ editor.command:
name: ShellCommand
placeholder: true
default: ["gvim", "-f", "{file}", "-c", "normal {line}G{column0}l"]
- desc: >-
+ desc: |
Editor (and arguments) to use for the `open-editor` command.
The following placeholders are defined:
@@ -2041,6 +2084,12 @@ colors.completion.scrollbar.bg:
desc: Color of the scrollbar in the completion view.
colors.contextmenu.bg:
+ renamed: colors.contextmenu.menu.bg
+
+colors.contextmenu.fg:
+ renamed: colors.contextmenu.menu.fg
+
+colors.contextmenu.menu.bg:
type:
name: QssColor
none_ok: true
@@ -2050,7 +2099,7 @@ colors.contextmenu.bg:
If set to null, the Qt default is used.
-colors.contextmenu.fg:
+colors.contextmenu.menu.fg:
type:
name: QssColor
none_ok: true
@@ -2060,6 +2109,26 @@ colors.contextmenu.fg:
If set to null, the Qt default is used.
+colors.contextmenu.selected.bg:
+ type:
+ name: QssColor
+ none_ok: true
+ default: null
+ desc: >-
+ Background color of the context menu's selected item.
+
+ If set to null, the Qt default is used.
+
+colors.contextmenu.selected.fg:
+ type:
+ name: QssColor
+ none_ok: true
+ default: null
+ desc: >-
+ Foreground color of the context menu's selected item.
+
+ If set to null, the Qt default is used.
+
colors.downloads.bar.bg:
default: black
type: QssColor
diff --git a/qutebrowser/config/configexc.py b/qutebrowser/config/configexc.py
index d83ca403b..53b6689f8 100644
--- a/qutebrowser/config/configexc.py
+++ b/qutebrowser/config/configexc.py
@@ -22,7 +22,7 @@
import typing
import attr
-from qutebrowser.utils import jinja, usertypes, log
+from qutebrowser.utils import usertypes, log
class Error(Exception):
@@ -155,6 +155,7 @@ class ConfigFileErrors(Error):
def to_html(self) -> str:
"""Get the error texts as a HTML snippet."""
+ from qutebrowser.utils import jinja # circular import
template = jinja.environment.from_string("""
Errors occurred while reading {{ basename }}:
diff --git a/qutebrowser/config/configfiles.py b/qutebrowser/config/configfiles.py
index ebfec1354..2e3e2f632 100644
--- a/qutebrowser/config/configfiles.py
+++ b/qutebrowser/config/configfiles.py
@@ -31,7 +31,7 @@ import typing
import re
import yaml
-from PyQt5.QtCore import pyqtSignal, QObject, QSettings, qVersion
+from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QSettings, qVersion
import qutebrowser
from qutebrowser.config import configexc, config, configdata, configutils
@@ -46,6 +46,9 @@ if typing.TYPE_CHECKING:
state = typing.cast('StateConfig', None)
+_SettingsType = typing.Dict[str, typing.Dict[str, typing.Any]]
+
+
class StateConfig(configparser.ConfigParser):
"""The "state" file saving various application state."""
@@ -103,8 +106,6 @@ class YamlConfig(QObject):
VERSION = 2
changed = pyqtSignal()
- _SettingsType = typing.Dict[str, typing.Dict[str, typing.Any]]
-
def __init__(self, parent: QObject = None) -> None:
super().__init__(parent)
self._filename = os.path.join(standarddir.config(auto=True),
@@ -128,6 +129,7 @@ class YamlConfig(QObject):
"""Iterate over configutils.Values items."""
yield from self._values.values()
+ @pyqtSlot()
def _mark_changed(self) -> None:
"""Mark the YAML config as changed."""
self._dirty = True
@@ -138,7 +140,7 @@ class YamlConfig(QObject):
if not self._dirty:
return
- settings = {} # type: YamlConfig._SettingsType
+ settings = {} # type: _SettingsType
for name, values in sorted(self._values.items()):
if not values:
continue
@@ -213,7 +215,10 @@ class YamlConfig(QObject):
settings = self._load_settings_object(yaml_data)
self._dirty = False
- settings = self._handle_migrations(settings)
+ migrations = YamlMigrations(settings, parent=self)
+ migrations.changed.connect(self._mark_changed)
+ migrations.migrate()
+
self._validate(settings)
self._build_values(settings)
@@ -262,89 +267,6 @@ class YamlConfig(QObject):
if errors:
raise configexc.ConfigFileErrors('autoconfig.yml', errors)
- def _migrate_bool(self, settings: _SettingsType, name: str,
- true_value: str, false_value: str) -> None:
- """Migrate a boolean in the settings."""
- if name in settings:
- for scope, val in settings[name].items():
- if isinstance(val, bool):
- settings[name][scope] = true_value if val else false_value
- self._mark_changed()
-
- def _migrate_string_value(self, settings: _SettingsType, name: str,
- source: str, target: str) -> None:
- if name in settings:
- for scope, val in settings[name].items():
- if isinstance(val, str):
- new_val = re.sub(source, target, val)
- if new_val != val:
- settings[name][scope] = new_val
- self._mark_changed()
-
- def _handle_migrations(self, settings: _SettingsType) -> '_SettingsType':
- """Migrate older configs to the newest format."""
- # Simple renamed/deleted options
- for name in list(settings):
- if name in configdata.MIGRATIONS.renamed:
- new_name = configdata.MIGRATIONS.renamed[name]
- log.config.debug("Renaming {} to {}".format(name, new_name))
- settings[new_name] = settings[name]
- del settings[name]
- self._mark_changed()
- elif name in configdata.MIGRATIONS.deleted:
- log.config.debug("Removing {}".format(name))
- del settings[name]
- self._mark_changed()
-
- # tabs.persist_mode_on_change got merged into tabs.mode_on_change
- old = 'tabs.persist_mode_on_change'
- new = 'tabs.mode_on_change'
- if old in settings:
- settings[new] = {}
- for scope, val in settings[old].items():
- if val:
- settings[new][scope] = 'persist'
- else:
- settings[new][scope] = 'normal'
-
- del settings[old]
- self._mark_changed()
-
- # bindings.default can't be set in autoconfig.yml anymore, so ignore
- # old values.
- if 'bindings.default' in settings:
- del settings['bindings.default']
- self._mark_changed()
-
- # content.webrtc_public_interfaces_only got merged into
- # content.webrtc_ip_handling_policy.
- old = 'content.webrtc_public_interfaces_only'
- new = 'content.webrtc_ip_handling_policy'
- if old in settings:
- settings[new] = {}
- for scope, val in settings[old].items():
- if val:
- settings[new][scope] = 'default-public-interface-only'
- else:
- settings[new][scope] = 'all-interfaces'
-
- del settings[old]
- self._mark_changed()
-
- self._migrate_bool(settings, 'tabs.favicons.show', 'always', 'never')
- self._migrate_bool(settings, 'scrolling.bar',
- 'always', 'when-searching')
- self._migrate_bool(settings, 'qt.force_software_rendering',
- 'software-opengl', 'none')
-
- for s in ['tabs.title.format',
- 'tabs.title.format_pinned',
- 'window.title_format']:
- self._migrate_string_value(
- settings, s, r'(?<!{)\{title\}(?!})', r'{current_title}')
-
- return settings
-
def _validate(self, settings: _SettingsType) -> None:
"""Make sure all settings exist."""
unknown = []
@@ -377,6 +299,124 @@ class YamlConfig(QObject):
self._mark_changed()
+class YamlMigrations(QObject):
+
+ """Automated migrations for autoconfig.yml."""
+
+ changed = pyqtSignal()
+
+ def __init__(self, settings: _SettingsType,
+ parent: QObject = None) -> None:
+ super().__init__(parent)
+ self._settings = settings
+
+ def migrate(self) -> None:
+ """Migrate older configs to the newest format."""
+ self._migrate_configdata()
+ self._migrate_bindings_default()
+
+ self._migrate_bool('tabs.favicons.show', 'always', 'never')
+ self._migrate_bool('scrolling.bar', 'always', 'when-searching')
+ self._migrate_bool('qt.force_software_rendering',
+ 'software-opengl', 'none')
+ self._migrate_renamed_bool(
+ old_name='content.webrtc_public_interfaces_only',
+ new_name='content.webrtc_ip_handling_policy',
+ true_value='default-public-interface-only',
+ false_value='all-interfaces')
+ self._migrate_renamed_bool(
+ old_name='tabs.persist_mode_on_change',
+ new_name='tabs.mode_on_change',
+ true_value='persist',
+ false_value='normal')
+
+ for setting in ['tabs.title.format',
+ 'tabs.title.format_pinned',
+ 'window.title_format']:
+ self._migrate_string_value(setting,
+ r'(?<!{)\{title\}(?!})',
+ r'{current_title}')
+
+ # content.headers.user_agent can't be empty to get the default anymore.
+ setting = 'content.headers.user_agent'
+ self._migrate_none(setting, configdata.DATA[setting].default)
+
+ def _migrate_configdata(self) -> None:
+ """Migrate simple renamed/deleted options."""
+ for name in list(self._settings):
+ if name in configdata.MIGRATIONS.renamed:
+ new_name = configdata.MIGRATIONS.renamed[name]
+ log.config.debug("Renaming {} to {}".format(name, new_name))
+ self._settings[new_name] = self._settings[name]
+ del self._settings[name]
+ self.changed.emit()
+ elif name in configdata.MIGRATIONS.deleted:
+ log.config.debug("Removing {}".format(name))
+ del self._settings[name]
+ self.changed.emit()
+
+ def _migrate_bindings_default(self) -> None:
+ """bindings.default can't be set in autoconfig.yml anymore.
+
+ => Ignore old values.
+ """
+ if 'bindings.default' not in self._settings:
+ return
+
+ del self._settings['bindings.default']
+ self.changed.emit()
+
+ def _migrate_bool(self, name: str,
+ true_value: str,
+ false_value: str) -> None:
+ if name not in self._settings:
+ return
+
+ for scope, val in self._settings[name].items():
+ if isinstance(val, bool):
+ new_value = true_value if val else false_value
+ self._settings[name][scope] = new_value
+ self.changed.emit()
+
+ def _migrate_renamed_bool(self, old_name: str,
+ new_name: str,
+ true_value: str,
+ false_value: str) -> None:
+ if old_name not in self._settings:
+ return
+
+ self._settings[new_name] = {}
+
+ for scope, val in self._settings[old_name].items():
+ new_value = true_value if val else false_value
+ self._settings[new_name][scope] = new_value
+
+ del self._settings[old_name]
+ self.changed.emit()
+
+ def _migrate_none(self, name: str, value: str) -> None:
+ if name not in self._settings:
+ return
+
+ for scope, val in self._settings[name].items():
+ if val is None:
+ self._settings[name][scope] = value
+ self.changed.emit()
+
+ def _migrate_string_value(self, name: str,
+ source: str,
+ target: str) -> None:
+ if name not in self._settings:
+ return
+
+ for scope, val in self._settings[name].items():
+ if isinstance(val, str):
+ new_val = re.sub(source, target, val)
+ if new_val != val:
+ self._settings[name][scope] = new_val
+ self.changed.emit()
+
+
class ConfigAPI:
"""Object which gets passed to config.py as "config" object.
diff --git a/qutebrowser/config/configinit.py b/qutebrowser/config/configinit.py
index 21f7fa054..2924efeae 100644
--- a/qutebrowser/config/configinit.py
+++ b/qutebrowser/config/configinit.py
@@ -28,7 +28,7 @@ from PyQt5.QtWidgets import QMessageBox
from qutebrowser.api import config as configapi
from qutebrowser.config import (config, configdata, configfiles, configtypes,
- configexc, configcommands)
+ configexc, configcommands, stylesheet)
from qutebrowser.utils import (objreg, usertypes, log, standarddir, message,
qtutils)
from qutebrowser.config import configcache
@@ -88,6 +88,8 @@ def early_init(args: argparse.Namespace) -> None:
configtypes.Font.monospace_fonts = config.val.fonts.monospace
config.instance.changed.connect(_update_monospace_fonts)
+ stylesheet.init()
+
_init_envvars()
@@ -104,6 +106,8 @@ def _init_envvars() -> None:
if config.val.qt.force_platform is not None:
os.environ['QT_QPA_PLATFORM'] = config.val.qt.force_platform
+ if config.val.qt.force_platformtheme is not None:
+ os.environ['QT_QPA_PLATFORMTHEME'] = config.val.qt.force_platformtheme
if config.val.window.hide_decoration:
os.environ['QT_WAYLAND_DISABLE_WINDOWDECORATION'] = '1'
diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py
index d48c02250..475879de0 100644
--- a/qutebrowser/config/configtypes.py
+++ b/qutebrowser/config/configtypes.py
@@ -61,7 +61,7 @@ from PyQt5.QtWidgets import QTabWidget, QTabBar
from PyQt5.QtNetwork import QNetworkProxy
from qutebrowser.misc import objects, debugcachestats
-from qutebrowser.config import configexc, configutils
+from qutebrowser.config import configexc
from qutebrowser.utils import (standarddir, utils, qtutils, urlutils, urlmatch,
usertypes)
from qutebrowser.keyinput import keyutils
@@ -80,8 +80,8 @@ BOOLEAN_STATES = {'1': True, 'yes': True, 'true': True, 'on': True,
_Completions = typing.Optional[typing.Iterable[typing.Tuple[str, str]]]
-_StrUnset = typing.Union[str, configutils.Unset]
-_StrUnsetNone = typing.Union[None, str, configutils.Unset]
+_StrUnset = typing.Union[str, usertypes.Unset]
+_StrUnsetNone = typing.Union[None, str, usertypes.Unset]
class ValidValues:
@@ -168,7 +168,7 @@ class BaseType:
value: The value to check.
pytype: A Python type to check the value against.
"""
- if isinstance(value, configutils.Unset):
+ if isinstance(value, usertypes.Unset):
return
if (value is None or (pytype == list and value == []) or
@@ -342,7 +342,7 @@ class MappingType(BaseType):
def to_py(self, value: typing.Any) -> typing.Any:
self._basic_py_validation(value, str)
- if isinstance(value, configutils.Unset):
+ if isinstance(value, usertypes.Unset):
return value
elif not value:
return None
@@ -408,7 +408,7 @@ class String(BaseType):
def to_py(self, value: _StrUnset) -> _StrUnsetNone:
self._basic_py_validation(value, str)
- if isinstance(value, configutils.Unset):
+ if isinstance(value, usertypes.Unset):
return value
elif not value:
return None
@@ -450,7 +450,7 @@ class UniqueCharString(String):
def to_py(self, value: _StrUnset) -> _StrUnsetNone:
py_value = super().to_py(value)
- if isinstance(py_value, configutils.Unset):
+ if isinstance(py_value, usertypes.Unset):
return py_value
elif not py_value:
return None
@@ -510,10 +510,10 @@ class List(BaseType):
def to_py(
self,
- value: typing.Union[typing.List, configutils.Unset]
- ) -> typing.Union[typing.List, configutils.Unset]:
+ value: typing.Union[typing.List, usertypes.Unset]
+ ) -> typing.Union[typing.List, usertypes.Unset]:
self._basic_py_validation(value, list)
- if isinstance(value, configutils.Unset):
+ if isinstance(value, usertypes.Unset):
return value
elif not value:
return []
@@ -601,7 +601,7 @@ class ListOrValue(BaseType):
return value
def to_py(self, value: typing.Any) -> typing.Any:
- if isinstance(value, configutils.Unset):
+ if isinstance(value, usertypes.Unset):
return value
try:
@@ -652,10 +652,10 @@ class FlagList(List):
def to_py(
self,
- value: typing.Union[configutils.Unset, typing.List],
- ) -> typing.Union[configutils.Unset, typing.List]:
+ value: typing.Union[usertypes.Unset, typing.List],
+ ) -> typing.Union[usertypes.Unset, typing.List]:
vals = super().to_py(value)
- if not isinstance(vals, configutils.Unset):
+ if not isinstance(vals, usertypes.Unset):
self._check_duplicates(vals)
return vals
@@ -866,10 +866,10 @@ class Perc(_Numeric):
def to_py(
self,
- value: typing.Union[None, float, int, str, configutils.Unset]
- ) -> typing.Union[None, float, int, configutils.Unset]:
+ value: typing.Union[None, float, int, str, usertypes.Unset]
+ ) -> typing.Union[None, float, int, usertypes.Unset]:
self._basic_py_validation(value, (float, int, str))
- if isinstance(value, configutils.Unset):
+ if isinstance(value, usertypes.Unset):
return value
elif not value:
return None
@@ -1062,10 +1062,10 @@ class QtColor(BaseType):
except ValueError:
raise configexc.ValidationError(val, "must be a valid color value")
- def to_py(self, value: _StrUnset) -> typing.Union[configutils.Unset,
+ def to_py(self, value: _StrUnset) -> typing.Union[usertypes.Unset,
None, QColor]:
self._basic_py_validation(value, str)
- if isinstance(value, configutils.Unset):
+ if isinstance(value, usertypes.Unset):
return value
elif not value:
return None
@@ -1123,7 +1123,7 @@ class QssColor(BaseType):
def to_py(self, value: _StrUnset) -> _StrUnsetNone:
self._basic_py_validation(value, str)
- if isinstance(value, configutils.Unset):
+ if isinstance(value, usertypes.Unset):
return value
elif not value:
return None
@@ -1170,7 +1170,7 @@ class Font(BaseType):
def to_py(self, value: _StrUnset) -> _StrUnsetNone:
self._basic_py_validation(value, str)
- if isinstance(value, configutils.Unset):
+ if isinstance(value, usertypes.Unset):
return value
elif not value:
return None
@@ -1191,7 +1191,7 @@ class FontFamily(Font):
def to_py(self, value: _StrUnset) -> _StrUnsetNone:
self._basic_py_validation(value, str)
- if isinstance(value, configutils.Unset):
+ if isinstance(value, usertypes.Unset):
return value
elif not value:
return None
@@ -1215,10 +1215,10 @@ class QtFont(Font):
__doc__ = Font.__doc__ # for src2asciidoc.py
- def to_py(self, value: _StrUnset) -> typing.Union[configutils.Unset,
+ def to_py(self, value: _StrUnset) -> typing.Union[usertypes.Unset,
None, QFont]:
self._basic_py_validation(value, str)
- if isinstance(value, configutils.Unset):
+ if isinstance(value, usertypes.Unset):
return value
elif not value:
return None
@@ -1333,11 +1333,11 @@ class Regex(BaseType):
def to_py(
self,
- value: typing.Union[str, typing.Pattern[str], configutils.Unset]
- ) -> typing.Union[configutils.Unset, None, typing.Pattern[str]]:
+ value: typing.Union[str, typing.Pattern[str], usertypes.Unset]
+ ) -> typing.Union[usertypes.Unset, None, typing.Pattern[str]]:
"""Get a compiled regex from either a string or a regex object."""
self._basic_py_validation(value, (str, self._regex_type))
- if isinstance(value, configutils.Unset):
+ if isinstance(value, usertypes.Unset):
return value
elif not value:
return None
@@ -1425,10 +1425,10 @@ class Dict(BaseType):
def to_py(
self,
- value: typing.Union[typing.Dict, configutils.Unset, None]
- ) -> typing.Union[typing.Dict, configutils.Unset]:
+ value: typing.Union[typing.Dict, usertypes.Unset, None]
+ ) -> typing.Union[typing.Dict, usertypes.Unset]:
self._basic_py_validation(value, dict)
- if isinstance(value, configutils.Unset):
+ if isinstance(value, usertypes.Unset):
return value
elif not value:
return self._fill_fixed_keys({})
@@ -1477,7 +1477,7 @@ class File(BaseType):
def to_py(self, value: _StrUnset) -> _StrUnsetNone:
self._basic_py_validation(value, str)
- if isinstance(value, configutils.Unset):
+ if isinstance(value, usertypes.Unset):
return value
elif not value:
return None
@@ -1509,7 +1509,7 @@ class Directory(BaseType):
def to_py(self, value: _StrUnset) -> _StrUnsetNone:
self._basic_py_validation(value, str)
- if isinstance(value, configutils.Unset):
+ if isinstance(value, usertypes.Unset):
return value
elif not value:
return None
@@ -1530,16 +1530,23 @@ class Directory(BaseType):
class FormatString(BaseType):
- """A string with placeholders."""
+ """A string with placeholders.
+
+ Attributes:
+ fields: Which replacements are allowed in the format string.
+ completions: completions to be used, or None
+ """
def __init__(self, fields: typing.Iterable[str],
- none_ok: bool = False) -> None:
+ none_ok: bool = False,
+ completions: _Completions = None) -> None:
super().__init__(none_ok)
self.fields = fields
+ self._completions = completions
def to_py(self, value: _StrUnset) -> _StrUnsetNone:
self._basic_py_validation(value, str)
- if isinstance(value, configutils.Unset):
+ if isinstance(value, usertypes.Unset):
return value
elif not value:
return None
@@ -1554,6 +1561,12 @@ class FormatString(BaseType):
return value
+ def complete(self) -> _Completions:
+ if self._completions is not None:
+ return self._completions
+ else:
+ return super().complete()
+
def __repr__(self) -> str:
return utils.get_repr(self, none_ok=self.none_ok, fields=self.fields)
@@ -1577,10 +1590,10 @@ class ShellCommand(List):
def to_py(
self,
- value: typing.Union[typing.List, configutils.Unset],
- ) -> typing.Union[typing.List, configutils.Unset]:
+ value: typing.Union[typing.List, usertypes.Unset],
+ ) -> typing.Union[typing.List, usertypes.Unset]:
py_value = super().to_py(value)
- if isinstance(py_value, configutils.Unset):
+ if isinstance(py_value, usertypes.Unset):
return py_value
elif not py_value:
return []
@@ -1611,9 +1624,9 @@ class Proxy(BaseType):
def to_py(
self,
value: _StrUnset
- ) -> typing.Union[configutils.Unset, None, QNetworkProxy, _SystemProxy]:
+ ) -> typing.Union[usertypes.Unset, None, QNetworkProxy, _SystemProxy]:
self._basic_py_validation(value, str)
- if isinstance(value, configutils.Unset):
+ if isinstance(value, usertypes.Unset):
return value
elif not value:
return None
@@ -1653,7 +1666,7 @@ class SearchEngineUrl(BaseType):
def to_py(self, value: _StrUnset) -> _StrUnsetNone:
self._basic_py_validation(value, str)
- if isinstance(value, configutils.Unset):
+ if isinstance(value, usertypes.Unset):
return value
elif not value:
return None
@@ -1683,7 +1696,7 @@ class FuzzyUrl(BaseType):
def to_py(self, value: _StrUnset) -> _StrUnsetNone:
self._basic_py_validation(value, str)
- if isinstance(value, configutils.Unset):
+ if isinstance(value, usertypes.Unset):
return value
elif not value:
return None
@@ -1719,10 +1732,10 @@ class Padding(Dict):
def to_py( # type: ignore
self,
- value: typing.Union[configutils.Unset, typing.Dict, None],
- ) -> typing.Union[configutils.Unset, PaddingValues]:
+ value: typing.Union[usertypes.Unset, typing.Dict, None],
+ ) -> typing.Union[usertypes.Unset, PaddingValues]:
d = super().to_py(value)
- if isinstance(d, configutils.Unset):
+ if isinstance(d, usertypes.Unset):
return d
return PaddingValues(**d)
@@ -1734,7 +1747,7 @@ class Encoding(BaseType):
def to_py(self, value: _StrUnset) -> _StrUnsetNone:
self._basic_py_validation(value, str)
- if isinstance(value, configutils.Unset):
+ if isinstance(value, usertypes.Unset):
return value
elif not value:
return None
@@ -1794,9 +1807,9 @@ class Url(BaseType):
def to_py(
self,
value: _StrUnset
- ) -> typing.Union[configutils.Unset, None, QUrl]:
+ ) -> typing.Union[usertypes.Unset, None, QUrl]:
self._basic_py_validation(value, str)
- if isinstance(value, configutils.Unset):
+ if isinstance(value, usertypes.Unset):
return value
elif not value:
return None
@@ -1814,7 +1827,7 @@ class SessionName(BaseType):
def to_py(self, value: _StrUnset) -> _StrUnsetNone:
self._basic_py_validation(value, str)
- if isinstance(value, configutils.Unset):
+ if isinstance(value, usertypes.Unset):
return value
elif not value:
return None
@@ -1864,10 +1877,10 @@ class ConfirmQuit(FlagList):
def to_py(
self,
- value: typing.Union[configutils.Unset, typing.List],
- ) -> typing.Union[typing.List, configutils.Unset]:
+ value: typing.Union[usertypes.Unset, typing.List],
+ ) -> typing.Union[typing.List, usertypes.Unset]:
values = super().to_py(value)
- if isinstance(values, configutils.Unset):
+ if isinstance(values, usertypes.Unset):
return values
elif not values:
return []
@@ -1908,9 +1921,9 @@ class Key(BaseType):
def to_py(
self,
value: _StrUnset
- ) -> typing.Union[configutils.Unset, None, keyutils.KeySequence]:
+ ) -> typing.Union[usertypes.Unset, None, keyutils.KeySequence]:
self._basic_py_validation(value, str)
- if isinstance(value, configutils.Unset):
+ if isinstance(value, usertypes.Unset):
return value
elif not value:
return None
@@ -1932,9 +1945,9 @@ class UrlPattern(BaseType):
def to_py(
self,
value: _StrUnset
- ) -> typing.Union[configutils.Unset, None, urlmatch.UrlPattern]:
+ ) -> typing.Union[usertypes.Unset, None, urlmatch.UrlPattern]:
self._basic_py_validation(value, str)
- if isinstance(value, configutils.Unset):
+ if isinstance(value, usertypes.Unset):
return value
elif not value:
return None
diff --git a/qutebrowser/config/configutils.py b/qutebrowser/config/configutils.py
index 8997aef36..1a7f612cb 100644
--- a/qutebrowser/config/configutils.py
+++ b/qutebrowser/config/configutils.py
@@ -28,24 +28,20 @@ import operator
from PyQt5.QtCore import QUrl
-from qutebrowser.utils import utils, urlmatch, urlutils
+from qutebrowser.utils import utils, urlmatch, usertypes
from qutebrowser.config import configexc
if typing.TYPE_CHECKING:
from qutebrowser.config import configdata
-class Unset:
+def _widened_hostnames(hostname: str) -> typing.Iterable[str]:
+ """A generator for widening string hostnames.
- """Sentinel object."""
-
- __slots__ = ()
-
- def __repr__(self) -> str:
- return '<UNSET>'
-
-
-UNSET = Unset()
+ Ex: a.c.foo -> [a.c.foo, c.foo, foo]"""
+ while hostname:
+ yield hostname
+ hostname = hostname.partition(".")[-1]
class ScopedValue:
@@ -55,18 +51,22 @@ class ScopedValue:
Attributes:
value: The value itself.
pattern: The UrlPattern for the value, or None for global values.
+ hide_userconfig: Hide this customization from config.dump_userconfig().
"""
id_gen = itertools.count(0)
def __init__(self, value: typing.Any,
- pattern: typing.Optional[urlmatch.UrlPattern]) -> None:
+ pattern: typing.Optional[urlmatch.UrlPattern],
+ hide_userconfig: bool = False) -> None:
self.value = value
self.pattern = pattern
+ self.hide_userconfig = hide_userconfig
self.pattern_id = next(ScopedValue.id_gen)
def __repr__(self) -> str:
return utils.get_repr(self, value=self.value, pattern=self.pattern,
+ hide_userconfig=self.hide_userconfig,
pattern_id=self.pattern_id)
@@ -102,8 +102,8 @@ class Values:
self._domain_map = collections.defaultdict(set) \
# type: typing.Dict[typing.Optional[str], typing.Set[ScopedValue]]
- for v in values:
- self.add(value=v.value, pattern=v.pattern)
+ for scoped in values:
+ self._add_scoped(scoped)
def __repr__(self) -> str:
return utils.get_repr(self, opt=self.opt,
@@ -112,18 +112,31 @@ class Values:
def __str__(self) -> str:
"""Get the values as human-readable string."""
- if not self:
- return '{}: <unchanged>'.format(self.opt.name)
+ lines = self.dump(include_hidden=True)
+ if lines:
+ return '\n'.join(lines)
+ return '{}: <unchanged>'.format(self.opt.name)
+
+ def dump(self, include_hidden: bool = False) -> typing.Sequence[str]:
+ """Dump all customizations for this value.
+ Arguments:
+ include_hidden: Also show values with hide_userconfig=True.
+ """
lines = []
+
for scoped in self._vmap.values():
+ if scoped.hide_userconfig and not include_hidden:
+ continue
+
str_value = self.opt.typ.to_str(scoped.value)
if scoped.pattern is None:
lines.append('{} = {}'.format(self.opt.name, str_value))
else:
lines.append('{}: {} = {}'.format(
scoped.pattern, self.opt.name, str_value))
- return '\n'.join(lines)
+
+ return lines
def __iter__(self) -> typing.Iterator['ScopedValue']:
"""Yield ScopedValue elements.
@@ -144,14 +157,24 @@ class Values:
raise configexc.NoPatternError(self.opt.name)
def add(self, value: typing.Any,
- pattern: urlmatch.UrlPattern = None) -> None:
- """Add a value with the given pattern to the list of values."""
- self._check_pattern_support(pattern)
- self.remove(pattern)
- scoped = ScopedValue(value, pattern)
- self._vmap[pattern] = scoped
+ pattern: urlmatch.UrlPattern = None, *,
+ hide_userconfig: bool = False) -> None:
+ """Add a value with the given pattern to the list of values.
- host = pattern.host if pattern else None
+ If hide_userconfig is given, the value is hidden from
+ config.dump_userconfig() and thus qute://configdiff.
+ """
+ scoped = ScopedValue(value, pattern, hide_userconfig=hide_userconfig)
+ self._add_scoped(scoped)
+
+ def _add_scoped(self, scoped: ScopedValue) -> None:
+ """Add an existing ScopedValue object."""
+ self._check_pattern_support(scoped.pattern)
+ self.remove(scoped.pattern)
+
+ self._vmap[scoped.pattern] = scoped
+
+ host = scoped.pattern.host if scoped.pattern else None
self._domain_map[host].add(scoped)
def remove(self, pattern: urlmatch.UrlPattern = None) -> bool:
@@ -186,7 +209,7 @@ class Values:
if fallback:
return self.opt.default
else:
- return UNSET
+ return usertypes.UNSET
def get_for_url(self, url: QUrl = None, *,
fallback: bool = True) -> typing.Any:
@@ -195,14 +218,14 @@ class Values:
This first tries to find a value matching the URL (if given).
If there's no match:
With fallback=True, the global/default setting is returned.
- With fallback=False, UNSET is returned.
+ With fallback=False, usertypes.UNSET is returned.
"""
self._check_pattern_support(url)
if url is None:
return self._get_fallback(fallback)
candidates = [] # type: typing.List[ScopedValue]
- widened_hosts = urlutils.widened_hostnames(url.host())
+ widened_hosts = _widened_hostnames(url.host())
# We must check the 'None' key as well, in case any patterns that
# did not have a domain match.
for host in itertools.chain(widened_hosts, [None]):
@@ -216,7 +239,7 @@ class Values:
return scoped.value
if not fallback:
- return UNSET
+ return usertypes.UNSET
return self._get_fallback(fallback)
@@ -229,7 +252,7 @@ class Values:
If there's no match:
With fallback=True, the global/default setting is returned.
- With fallback=False, UNSET is returned.
+ With fallback=False, usertypes.UNSET is returned.
"""
self._check_pattern_support(pattern)
if pattern is not None:
@@ -237,6 +260,6 @@ class Values:
return self._vmap[pattern].value
if not fallback:
- return UNSET
+ return usertypes.UNSET
return self._get_fallback(fallback)
diff --git a/qutebrowser/config/stylesheet.py b/qutebrowser/config/stylesheet.py
new file mode 100644
index 000000000..27432d01b
--- /dev/null
+++ b/qutebrowser/config/stylesheet.py
@@ -0,0 +1,116 @@
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+
+# Copyright 2019 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/>.
+
+"""Handling of Qt qss stylesheets."""
+
+import functools
+from typing import Optional, FrozenSet
+
+from PyQt5.QtCore import pyqtSlot, QObject
+
+from qutebrowser.config import config
+from qutebrowser.misc import debugcachestats
+from qutebrowser.utils import jinja, log
+
+
+def set_register(obj: QObject,
+ stylesheet: str = None, *,
+ update: bool = True) -> None:
+ """Set the stylesheet for an object.
+
+ Also, register an update when the config is changed.
+
+ Args:
+ obj: The object to set the stylesheet for and register.
+ Must have a STYLESHEET attribute if stylesheet is not given.
+ stylesheet: The stylesheet to use.
+ update: Whether to update the stylesheet on config changes.
+ """
+ observer = _StyleSheetObserver(obj, stylesheet, update)
+ observer.register()
+
+
+@debugcachestats.register()
+@functools.lru_cache()
+def _render_stylesheet(stylesheet: str) -> str:
+ """Render the given stylesheet jinja template."""
+ with jinja.environment.no_autoescape():
+ template = jinja.environment.from_string(stylesheet)
+ return template.render(conf=config.val)
+
+
+def init() -> None:
+ config.instance.changed.connect(_render_stylesheet.cache_clear)
+
+
+class _StyleSheetObserver(QObject):
+
+ """Set the stylesheet on the given object and update it on changes.
+
+ Attributes:
+ _obj: The object to observe.
+ _stylesheet: The stylesheet template to use.
+ _options: The config options that the stylesheet uses. When it's not
+ necessary to listen for config changes, this attribute may be
+ None.
+ """
+
+ def __init__(self, obj: QObject,
+ stylesheet: Optional[str], update: bool) -> None:
+ super().__init__()
+ self._obj = obj
+ self._update = update
+
+ # We only need to hang around if we are asked to update.
+ if update:
+ self.setParent(self._obj)
+ if stylesheet is None:
+ self._stylesheet = obj.STYLESHEET # type: str
+ else:
+ self._stylesheet = stylesheet
+
+ if update:
+ self._options = jinja.template_config_variables(
+ self._stylesheet) # type: Optional[FrozenSet[str]]
+ else:
+ self._options = None
+
+ def _get_stylesheet(self) -> str:
+ """Format a stylesheet based on a template.
+
+ Return:
+ The formatted template as string.
+ """
+ return _render_stylesheet(self._stylesheet)
+
+ @pyqtSlot(str)
+ def _maybe_update_stylesheet(self, option: str) -> None:
+ """Update the stylesheet for obj if the option changed affects it."""
+ assert self._options is not None
+ if option in self._options:
+ self._obj.setStyleSheet(self._get_stylesheet())
+
+ def register(self) -> None:
+ """Do a first update and listen for more."""
+ qss = self._get_stylesheet()
+ log.config.vdebug( # type: ignore
+ "stylesheet for {}: {}".format(self._obj.__class__.__name__, qss))
+ self._obj.setStyleSheet(qss)
+ if self._update:
+ config.instance.changed.connect(self._maybe_update_stylesheet)
diff --git a/qutebrowser/config/websettings.py b/qutebrowser/config/websettings.py
index db8f77387..2f78b561e 100644
--- a/qutebrowser/config/websettings.py
+++ b/qutebrowser/config/websettings.py
@@ -19,19 +19,65 @@
"""Bridge from QWeb(Engine)Settings to our own settings."""
+import re
import typing
import argparse
+import functools
-from PyQt5.QtCore import QUrl, pyqtSlot
+import attr
+from PyQt5.QtCore import QUrl, pyqtSlot, qVersion
from PyQt5.QtGui import QFont
-from qutebrowser.config import config, configutils
+import qutebrowser
+from qutebrowser.config import config
from qutebrowser.utils import log, usertypes, urlmatch, qtutils
-from qutebrowser.misc import objects
+from qutebrowser.misc import objects, debugcachestats
UNSET = object()
+@attr.s
+class UserAgent:
+
+ """A parsed user agent."""
+
+ os_info = attr.ib() # type: str
+ webkit_version = attr.ib() # type: str
+ upstream_browser_key = attr.ib() # type: str
+ upstream_browser_version = attr.ib() # type: str
+ qt_key = attr.ib() # type: str
+
+ @classmethod
+ def parse(cls, ua: str) -> 'UserAgent':
+ """Parse a user agent string into its components."""
+ comment_matches = re.finditer(r'\(([^)]*)\)', ua)
+ os_info = list(comment_matches)[0].group(1)
+
+ version_matches = re.finditer(r'(\S+)/(\S+)', ua)
+ versions = {}
+ for match in version_matches:
+ versions[match.group(1)] = match.group(2)
+
+ webkit_version = versions['AppleWebKit']
+
+ if 'Chrome' in versions:
+ upstream_browser_key = 'Chrome'
+ qt_key = 'QtWebEngine'
+ elif 'Version' in versions:
+ upstream_browser_key = 'Version'
+ qt_key = 'Qt'
+ else:
+ raise ValueError("Invalid upstream browser key: {}".format(ua))
+
+ upstream_browser_version = versions[upstream_browser_key]
+
+ return cls(os_info=os_info,
+ webkit_version=webkit_version,
+ upstream_browser_key=upstream_browser_key,
+ upstream_browser_version=upstream_browser_version,
+ qt_key=qt_key)
+
+
class AttributeInfo:
"""Info about a settings attribute."""
@@ -60,7 +106,7 @@ class AbstractSettings:
def set_attribute(self, name: str, value: typing.Any) -> bool:
"""Set the given QWebSettings/QWebEngineSettings attribute.
- If the value is configutils.UNSET, the value is reset instead.
+ If the value is usertypes.UNSET, the value is reset instead.
Return:
True if there was a change, False otherwise.
@@ -69,7 +115,7 @@ class AbstractSettings:
info = self._ATTRIBUTES[name]
for attribute in info.attributes:
- if value is configutils.UNSET:
+ if value is usertypes.UNSET:
self._settings.resetAttribute(attribute)
new_value = self.test_attribute(name)
else:
@@ -93,7 +139,7 @@ class AbstractSettings:
Return:
True if there was a change, False otherwise.
"""
- assert value is not configutils.UNSET # type: ignore
+ assert value is not usertypes.UNSET # type: ignore
family = self._FONT_SIZES[name]
old_value = self._settings.fontSize(family)
self._settings.setFontSize(family, value)
@@ -108,7 +154,7 @@ class AbstractSettings:
Return:
True if there was a change, False otherwise.
"""
- assert value is not configutils.UNSET # type: ignore
+ assert value is not usertypes.UNSET # type: ignore
family = self._FONT_FAMILIES[name]
if value is None:
font = QFont()
@@ -126,7 +172,7 @@ class AbstractSettings:
Return:
True if there was a change, False otherwise.
"""
- assert encoding is not configutils.UNSET # type: ignore
+ assert encoding is not usertypes.UNSET # type: ignore
old_value = self._settings.defaultTextEncoding()
self._settings.setDefaultTextEncoding(encoding)
return old_value != encoding
@@ -183,6 +229,34 @@ class AbstractSettings:
self.update_setting(setting)
+@debugcachestats.register(name='user agent cache')
+@functools.lru_cache()
+def _format_user_agent(template: str, backend: usertypes.Backend) -> str:
+ if backend == usertypes.Backend.QtWebEngine:
+ from qutebrowser.browser.webengine import webenginesettings
+ parsed = webenginesettings.parsed_user_agent
+ else:
+ from qutebrowser.browser.webkit import webkitsettings
+ parsed = webkitsettings.parsed_user_agent
+
+ assert parsed is not None
+
+ return template.format(
+ os_info=parsed.os_info,
+ webkit_version=parsed.webkit_version,
+ qt_key=parsed.qt_key,
+ qt_version=qVersion(),
+ upstream_browser_key=parsed.upstream_browser_key,
+ upstream_browser_version=parsed.upstream_browser_version,
+ qutebrowser_version=qutebrowser.__version__,
+ )
+
+
+def user_agent(url: QUrl = None) -> str:
+ template = config.instance.get('content.headers.user_agent', url=url)
+ return _format_user_agent(template=template, backend=objects.backend)
+
+
def init(args: argparse.Namespace) -> None:
"""Initialize all QWeb(Engine)Settings."""
if objects.backend == usertypes.Backend.QtWebEngine:
@@ -193,9 +267,10 @@ def init(args: argparse.Namespace) -> None:
webkitsettings.init(args)
# Make sure special URLs always get JS support
- for pattern in ['file://*', 'chrome://*/*', 'qute://*/*']:
+ for pattern in ['chrome://*/*', 'qute://*/*']:
config.instance.set_obj('content.javascript.enabled', True,
- pattern=urlmatch.UrlPattern(pattern))
+ pattern=urlmatch.UrlPattern(pattern),
+ hide_userconfig=True)
@pyqtSlot()
diff --git a/qutebrowser/extensions/interceptors.py b/qutebrowser/extensions/interceptors.py
index a72fe6cfa..58bd3f2bc 100644
--- a/qutebrowser/extensions/interceptors.py
+++ b/qutebrowser/extensions/interceptors.py
@@ -52,6 +52,9 @@ class ResourceType(enum.Enum):
service_worker = 15
csp_report = 16
plugin_resource = 17
+ # 18 is "preload", deprecated in Chromium
+ preload_main_frame = 19
+ preload_sub_frame = 20
unknown = 255
diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py
index ac5df12e6..c2dea4dc7 100644
--- a/qutebrowser/mainwindow/mainwindow.py
+++ b/qutebrowser/mainwindow/mainwindow.py
@@ -31,7 +31,7 @@ from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication, QSizePolicy
from qutebrowser.commands import runners
from qutebrowser.api import cmdutils
-from qutebrowser.config import config, configfiles
+from qutebrowser.config import config, configfiles, stylesheet
from qutebrowser.utils import (message, log, usertypes, qtutils, objreg, utils,
jinja, debug)
from qutebrowser.mainwindow import messageview, prompt
@@ -165,11 +165,20 @@ class MainWindow(QWidget):
{% if conf.fonts.contextmenu %}
font: {{ conf.fonts.contextmenu }};
{% endif %}
- {% if conf.colors.contextmenu.bg %}
- background-color: {{ conf.colors.contextmenu.bg }};
+ {% if conf.colors.contextmenu.menu.bg %}
+ background-color: {{ conf.colors.contextmenu.menu.bg }};
{% endif %}
- {% if conf.colors.contextmenu.fg %}
- color: {{ conf.colors.contextmenu.fg }};
+ {% if conf.colors.contextmenu.menu.fg %}
+ color: {{ conf.colors.contextmenu.menu.fg }};
+ {% endif %}
+ }
+
+ QMenu::item:selected {
+ {% if conf.colors.contextmenu.selected.bg %}
+ background-color: {{ conf.colors.contextmenu.selected.bg }};
+ {% endif %}
+ {% if conf.colors.contextmenu.selected.fg %}
+ color: {{ conf.colors.contextmenu.selected.fg }};
{% endif %}
}
"""
@@ -269,7 +278,7 @@ class MainWindow(QWidget):
self._set_decoration(config.val.window.hide_decoration)
self.state_before_fullscreen = self.windowState()
- config.set_register_stylesheet(self)
+ stylesheet.set_register(self)
def _init_geometry(self, geometry):
"""Initialize the window geometry or load it from disk."""
diff --git a/qutebrowser/mainwindow/messageview.py b/qutebrowser/mainwindow/messageview.py
index ea14265aa..3660d3529 100644
--- a/qutebrowser/mainwindow/messageview.py
+++ b/qutebrowser/mainwindow/messageview.py
@@ -24,7 +24,7 @@ import typing
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QTimer, Qt, QSize
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QSizePolicy
-from qutebrowser.config import config
+from qutebrowser.config import config, stylesheet
from qutebrowser.utils import usertypes
@@ -36,19 +36,19 @@ class Message(QLabel):
super().__init__(text, parent)
self.replace = replace
self.setAttribute(Qt.WA_StyledBackground, True)
- stylesheet = """
+ qss = """
padding-top: 2px;
padding-bottom: 2px;
"""
if level == usertypes.MessageLevel.error:
- stylesheet += """
+ qss += """
background-color: {{ conf.colors.messages.error.bg }};
color: {{ conf.colors.messages.error.fg }};
font: {{ conf.fonts.messages.error }};
border-bottom: 1px solid {{ conf.colors.messages.error.border }};
"""
elif level == usertypes.MessageLevel.warning:
- stylesheet += """
+ qss += """
background-color: {{ conf.colors.messages.warning.bg }};
color: {{ conf.colors.messages.warning.fg }};
font: {{ conf.fonts.messages.warning }};
@@ -56,7 +56,7 @@ class Message(QLabel):
1px solid {{ conf.colors.messages.warning.border }};
"""
elif level == usertypes.MessageLevel.info:
- stylesheet += """
+ qss += """
background-color: {{ conf.colors.messages.info.bg }};
color: {{ conf.colors.messages.info.fg }};
font: {{ conf.fonts.messages.info }};
@@ -66,8 +66,7 @@ class Message(QLabel):
raise ValueError("Invalid level {!r}".format(level))
# We don't bother with set_register_stylesheet here as it's short-lived
# anyways.
- config.set_register_stylesheet(self, stylesheet=stylesheet,
- update=False)
+ stylesheet.set_register(self, qss, update=False)
class MessageView(QWidget):
diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py
index 6f7d1b9f7..ba6ff2f1b 100644
--- a/qutebrowser/mainwindow/prompt.py
+++ b/qutebrowser/mainwindow/prompt.py
@@ -33,7 +33,7 @@ from PyQt5.QtWidgets import (QWidget, QGridLayout, QVBoxLayout, QLineEdit,
QSpacerItem)
from qutebrowser.browser import downloads
-from qutebrowser.config import config, configtypes, configexc
+from qutebrowser.config import config, configtypes, configexc, stylesheet
from qutebrowser.utils import usertypes, log, utils, qtutils, objreg, message
from qutebrowser.keyinput import modeman
from qutebrowser.api import cmdutils
@@ -292,7 +292,7 @@ class PromptContainer(QWidget):
self.setObjectName('PromptContainer')
self.setAttribute(Qt.WA_StyledBackground, True)
- config.set_register_stylesheet(self)
+ stylesheet.set_register(self)
message.global_bridge.prompt_done.connect(self._on_prompt_done)
prompt_queue.show_prompts.connect(self._on_show_prompts)
diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py
index 463e0c151..dd50024b3 100644
--- a/qutebrowser/mainwindow/statusbar/bar.py
+++ b/qutebrowser/mainwindow/statusbar/bar.py
@@ -25,7 +25,7 @@ from PyQt5.QtCore import pyqtSignal, pyqtSlot, pyqtProperty, Qt, QSize, QTimer
from PyQt5.QtWidgets import QWidget, QHBoxLayout, QStackedLayout, QSizePolicy
from qutebrowser.browser import browsertab
-from qutebrowser.config import config
+from qutebrowser.config import config, stylesheet
from qutebrowser.keyinput import modeman
from qutebrowser.utils import usertypes, log, objreg, utils
from qutebrowser.mainwindow.statusbar import (backforward, command, progress,
@@ -97,7 +97,7 @@ def _generate_stylesheet():
('passthrough', 'statusbar.passthrough'),
('private-command', 'statusbar.command.private'),
]
- stylesheet = """
+ qss = """
QWidget#StatusBar,
QWidget#StatusBar QLabel,
QWidget#StatusBar QLineEdit {
@@ -110,7 +110,7 @@ def _generate_stylesheet():
}
"""
for flag, option in flags:
- stylesheet += """
+ qss += """
QWidget#StatusBar[color_flags~="%s"],
QWidget#StatusBar[color_flags~="%s"] QLabel,
QWidget#StatusBar[color_flags~="%s"] QLineEdit {
@@ -122,7 +122,7 @@ def _generate_stylesheet():
}
""" % (flag, flag, flag, # noqa: S001
option + '.fg', flag, option + '.bg')
- return stylesheet
+ return qss
class StatusBar(QWidget):
@@ -158,7 +158,7 @@ class StatusBar(QWidget):
super().__init__(parent)
self.setObjectName(self.__class__.__name__)
self.setAttribute(Qt.WA_StyledBackground)
- config.set_register_stylesheet(self)
+ stylesheet.set_register(self)
self.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Fixed)
@@ -298,7 +298,7 @@ class StatusBar(QWidget):
# Turning on is handled in on_current_caret_selection_toggled
log.statusbar.debug("Setting caret mode off")
self._color_flags.caret = ColorFlags.CaretMode.off
- config.set_register_stylesheet(self, update=False)
+ stylesheet.set_register(self, update=False)
def _set_mode_text(self, mode):
"""Set the mode text."""
@@ -382,7 +382,7 @@ class StatusBar(QWidget):
else:
self._set_mode_text("caret")
self._color_flags.caret = ColorFlags.CaretMode.on
- config.set_register_stylesheet(self, update=False)
+ stylesheet.set_register(self, update=False)
def resizeEvent(self, e):
"""Extend resizeEvent of QWidget to emit a resized signal afterwards.
diff --git a/qutebrowser/mainwindow/statusbar/progress.py b/qutebrowser/mainwindow/statusbar/progress.py
index 34c1954e8..389ddf12e 100644
--- a/qutebrowser/mainwindow/statusbar/progress.py
+++ b/qutebrowser/mainwindow/statusbar/progress.py
@@ -22,7 +22,7 @@
from PyQt5.QtCore import pyqtSlot, QSize
from PyQt5.QtWidgets import QProgressBar, QSizePolicy
-from qutebrowser.config import config
+from qutebrowser.config import stylesheet
from qutebrowser.utils import utils, usertypes
@@ -45,7 +45,7 @@ class Progress(QProgressBar):
def __init__(self, parent=None):
super().__init__(parent)
- config.set_register_stylesheet(self)
+ stylesheet.set_register(self)
self.enabled = False
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
self.setTextVisible(False)
diff --git a/qutebrowser/mainwindow/statusbar/url.py b/qutebrowser/mainwindow/statusbar/url.py
index d6aacb184..7a84b1e4e 100644
--- a/qutebrowser/mainwindow/statusbar/url.py
+++ b/qutebrowser/mainwindow/statusbar/url.py
@@ -24,7 +24,7 @@ import enum
from PyQt5.QtCore import pyqtSlot, pyqtProperty, QUrl
from qutebrowser.mainwindow.statusbar import textbase
-from qutebrowser.config import config
+from qutebrowser.config import stylesheet
from qutebrowser.utils import usertypes, urlutils
@@ -76,7 +76,7 @@ class UrlText(textbase.TextBase):
super().__init__(parent)
self._urltype = None
self.setObjectName(self.__class__.__name__)
- config.set_register_stylesheet(self)
+ stylesheet.set_register(self)
self._hover_url = None
self._normal_url = None
self._normal_url_type = UrlType.normal
diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py
index f125a976d..50c9a1d5d 100644
--- a/qutebrowser/mainwindow/tabbedbrowser.py
+++ b/qutebrowser/mainwindow/tabbedbrowser.py
@@ -553,8 +553,7 @@ class TabbedBrowser(QWidget):
self, url: QUrl = None,
background: bool = None,
related: bool = True,
- idx: int = None, *,
- ignore_tabs_are_windows: bool = False
+ idx: int = None,
) -> browsertab.AbstractTab:
"""Open a new tab with a given URL.
@@ -573,8 +572,6 @@ class TabbedBrowser(QWidget):
- Explicitly opened tabs are at the very right
(related=False)
idx: The index where the new tab should be opened.
- ignore_tabs_are_windows: If given, never open a new window, even
- with tabs.tabs_are_windows set.
Return:
The opened WebView instance.
@@ -587,8 +584,7 @@ class TabbedBrowser(QWidget):
prev_focus = QApplication.focusWidget()
- if (config.val.tabs.tabs_are_windows and self.widget.count() > 0 and
- not ignore_tabs_are_windows):
+ if config.val.tabs.tabs_are_windows and self.widget.count() > 0:
window = mainwindow.MainWindow(private=self.is_private)
window.show()
tabbed_browser = objreg.get('tabbed-browser', scope='window',
diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py
index ba6ffb79f..25ecc69b8 100644
--- a/qutebrowser/mainwindow/tabwidget.py
+++ b/qutebrowser/mainwindow/tabwidget.py
@@ -32,7 +32,7 @@ from PyQt5.QtWidgets import (QTabWidget, QTabBar, QSizePolicy, QCommonStyle,
from PyQt5.QtGui import QIcon, QPalette, QColor
from qutebrowser.utils import qtutils, objreg, utils, usertypes, log
-from qutebrowser.config import config
+from qutebrowser.config import config, stylesheet
from qutebrowser.misc import objects, debugcachestats
from qutebrowser.browser import browsertab
@@ -116,7 +116,13 @@ class TabWidget(QTabWidget):
def set_page_title(self, idx, title):
"""Set the tab title user data."""
- self.tabBar().set_tab_data(idx, 'page-title', title)
+ tabbar = self.tabBar()
+
+ if config.cache['tabs.tooltips']:
+ # always show only plain title in tooltips
+ tabbar.setTabToolTip(idx, title)
+
+ tabbar.set_tab_data(idx, 'page-title', title)
self.update_tab_title(idx)
def page_title(self, idx):
@@ -153,10 +159,6 @@ class TabWidget(QTabWidget):
if tabbar.tabText(idx) != title:
tabbar.setTabText(idx, title)
- if config.cache['tabs.tooltips']:
- # always show only plain title in tooltips
- tabbar.setTabToolTip(idx, fields['current_title'])
-
def get_tab_fields(self, idx):
"""Get the tab field data."""
tab = self.widget(idx)
@@ -400,7 +402,7 @@ class TabBar(QTabBar):
self._on_show_switching_delay_changed()
self.setAutoFillBackground(True)
self.drag_in_progress = False
- config.set_register_stylesheet(self)
+ stylesheet.set_register(self)
QTimer.singleShot(0, self.maybe_hide)
def __repr__(self):
diff --git a/qutebrowser/misc/keyhintwidget.py b/qutebrowser/misc/keyhintwidget.py
index 9cc00c6a8..68f9685d3 100644
--- a/qutebrowser/misc/keyhintwidget.py
+++ b/qutebrowser/misc/keyhintwidget.py
@@ -31,7 +31,7 @@ import re
from PyQt5.QtWidgets import QLabel, QSizePolicy
from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt
-from qutebrowser.config import config
+from qutebrowser.config import config, stylesheet
from qutebrowser.utils import utils, usertypes
from qutebrowser.misc import objects
from qutebrowser.keyinput import keyutils
@@ -72,7 +72,7 @@ class KeyHintView(QLabel):
self._show_timer = usertypes.Timer(self, 'keyhint_show')
self._show_timer.timeout.connect(self.show)
self._show_timer.setSingleShot(True)
- config.set_register_stylesheet(self)
+ stylesheet.set_register(self)
def __repr__(self):
return utils.get_repr(self, win_id=self._win_id)
diff --git a/qutebrowser/utils/javascript.py b/qutebrowser/utils/javascript.py
index f456d93c9..2c02975db 100644
--- a/qutebrowser/utils/javascript.py
+++ b/qutebrowser/utils/javascript.py
@@ -21,8 +21,6 @@
import typing
-from qutebrowser.utils import jinja
-
_InnerJsArgType = typing.Union[None, str, bool, int, float]
_JsArgType = typing.Union[_InnerJsArgType, typing.Sequence[_InnerJsArgType]]
@@ -83,5 +81,6 @@ def assemble(module: str, function: str, *args: _JsArgType) -> str:
def wrap_global(name: str, *sources: str) -> str:
"""Wrap a script using window._qutebrowser."""
+ from qutebrowser.utils import jinja # circular import
template = jinja.js_environment.get_template('global_wrapper.js')
return template.render(code='\n'.join(sources), name=name)
diff --git a/qutebrowser/utils/urlutils.py b/qutebrowser/utils/urlutils.py
index 51f0fd6cc..7e7d42e90 100644
--- a/qutebrowser/utils/urlutils.py
+++ b/qutebrowser/utils/urlutils.py
@@ -138,18 +138,15 @@ def _is_url_naive(urlstr: str) -> bool:
"""
url = qurl_from_user_input(urlstr)
assert url.isValid()
+ host = url.host()
- if not utils.raises(ValueError, ipaddress.ip_address, urlstr):
- # Valid IPv4/IPv6 address
+ # Valid IPv4/IPv6 address. Qt converts things like "23.42" or "1337" or
+ # "0xDEAD" to IP addresses, which we don't like, so we check if the host
+ # from Qt is part of the input.
+ if (not utils.raises(ValueError, ipaddress.ip_address, host) and
+ host in urlstr):
return True
- # Qt treats things like "23.42" or "1337" or "0xDEAD" as valid URLs
- # which we don't want to. Note we already filtered *real* valid IPs
- # above.
- if not QHostAddress(urlstr).isNull():
- return False
-
- host = url.host()
tld = r'\.([^.0-9_-]+|xn--[a-z0-9-]+)$'
forbidden = r'[\u0000-\u002c\u002f\u003a-\u0060\u007b-\u00b6]'
return bool(re.search(tld, host) and not re.search(forbidden, host))
@@ -617,12 +614,3 @@ def proxy_from_url(url: QUrl) -> QNetworkProxy:
if url.password():
proxy.setPassword(url.password())
return proxy
-
-
-def widened_hostnames(hostname: str) -> typing.Iterable[str]:
- """A generator for widening string hostnames.
-
- Ex: a.c.foo -> [a.c.foo, c.foo, foo]"""
- while hostname:
- yield hostname
- hostname = hostname.partition(".")[-1]
diff --git a/qutebrowser/utils/usertypes.py b/qutebrowser/utils/usertypes.py
index 5214459bb..65ab2d491 100644
--- a/qutebrowser/utils/usertypes.py
+++ b/qutebrowser/utils/usertypes.py
@@ -25,7 +25,7 @@ import typing
import attr
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QTimer
-from PyQt5.QtCore import QUrl # pylint: disable=unused-import
+from PyQt5.QtCore import QUrl
from qutebrowser.utils import log, qtutils, utils
@@ -33,14 +33,17 @@ from qutebrowser.utils import log, qtutils, utils
_T = typing.TypeVar('_T')
-class UnsetObject:
+class Unset:
"""Class for an unset object."""
__slots__ = ()
+ def __repr__(self) -> str:
+ return '<UNSET>'
+
-UNSET = UnsetObject()
+UNSET = Unset()
class NeighborList(typing.Sequence[_T]):
@@ -60,7 +63,7 @@ class NeighborList(typing.Sequence[_T]):
Modes = enum.Enum('Modes', ['edge', 'exception'])
def __init__(self, items: typing.Sequence[_T] = None,
- default: typing.Union[_T, UnsetObject] = UNSET,
+ default: typing.Union[_T, Unset] = UNSET,
mode: Modes = Modes.exception) -> None:
"""Constructor.
@@ -79,7 +82,7 @@ class NeighborList(typing.Sequence[_T]):
self._items = list(items)
self._default = default
- if not isinstance(default, UnsetObject):
+ if not isinstance(default, Unset):
idx = self._items.index(default)
self._idx = idx # type: typing.Optional[int]
else:
diff --git a/qutebrowser/utils/version.py b/qutebrowser/utils/version.py
index f3588a5d5..50a00c20b 100644
--- a/qutebrowser/utils/version.py
+++ b/qutebrowser/utils/version.py
@@ -45,11 +45,6 @@ try:
except ImportError: # pragma: no cover
qWebKitVersion = None # type: ignore # noqa: N816
-try:
- from PyQt5.QtWebEngineWidgets import QWebEngineProfile
-except ImportError: # pragma: no cover
- QWebEngineProfile = None # type: ignore
-
import qutebrowser
from qutebrowser.utils import log, utils, standarddir, usertypes, message
from qutebrowser.misc import objects, earlyinit, sql, httpclient, pastebin
@@ -369,18 +364,14 @@ def _chromium_version() -> str:
Also see https://www.chromium.org/developers/calendar
and https://chromereleases.googleblog.com/
"""
- if webenginesettings is None or QWebEngineProfile is None: # type: ignore
- # This should never happen
+ if webenginesettings is None:
return 'unavailable' # type: ignore
- ua = webenginesettings.default_user_agent
- if ua is None:
- profile = QWebEngineProfile.defaultProfile()
- ua = profile.httpUserAgent()
- match = re.search(r' Chrome/([^ ]*) ', ua)
- if not match:
- log.misc.error("Could not get Chromium version from: {}".format(ua))
- return 'unknown'
- return match.group(1)
+
+ if webenginesettings.parsed_user_agent is None:
+ webenginesettings.init_user_agent()
+ assert webenginesettings.parsed_user_agent is not None
+
+ return webenginesettings.parsed_user_agent.upstream_browser_version
def _backend() -> str:
@@ -419,13 +410,13 @@ def version() -> str:
if gitver is not None:
lines.append("Git commit: {}".format(gitver))
- lines.append("Backend: {}".format(_backend()))
+ lines.append('Backend: {}'.format(_backend()))
+ lines.append('Qt: {}'.format(earlyinit.qt_version()))
lines += [
'',
'{}: {}'.format(platform.python_implementation(),
platform.python_version()),
- 'Qt: {}'.format(earlyinit.qt_version()),
'PyQt: {}'.format(PYQT_VERSION_STR),
'',
]
diff --git a/scripts/dev/check_coverage.py b/scripts/dev/check_coverage.py
index fbf6cb954..7798415a5 100644
--- a/scripts/dev/check_coverage.py
+++ b/scripts/dev/check_coverage.py
@@ -147,6 +147,8 @@ PERFECT_FILES = [
('tests/unit/config/test_config.py',
'config/config.py'),
+ ('tests/unit/config/test_stylesheet.py',
+ 'config/stylesheet.py'),
('tests/unit/config/test_configdata.py',
'config/configdata.py'),
('tests/unit/config/test_configexc.py',
diff --git a/tests/end2end/features/misc.feature b/tests/end2end/features/misc.feature
index e6b7f9fa4..7a26c9dda 100644
--- a/tests/end2end/features/misc.feature
+++ b/tests/end2end/features/misc.feature
@@ -362,13 +362,6 @@ Feature: Various utility commands.
Then the header User-Agent should be set to toaster
And the javascript message "toaster" should be logged
- Scenario: Setting the default user-agent header
- When I set content.headers.user_agent to <empty>
- And I open headers
- And I run :jseval console.log(window.navigator.userAgent)
- Then the header User-Agent should be set to Mozilla/5.0 *
- And the javascript message "Mozilla/5.0 *" should be logged
-
## https://github.com/qutebrowser/qutebrowser/issues/1523
Scenario: Completing a single option argument
diff --git a/tests/helpers/fixtures.py b/tests/helpers/fixtures.py
index c76d4f061..045be5e94 100644
--- a/tests/helpers/fixtures.py
+++ b/tests/helpers/fixtures.py
@@ -43,7 +43,7 @@ from PyQt5.QtNetwork import QNetworkCookieJar
import helpers.stubs as stubsmod
from qutebrowser.config import (config, configdata, configtypes, configexc,
- configfiles, configcache)
+ configfiles, configcache, stylesheet)
from qutebrowser.api import config as configapi
from qutebrowser.utils import objreg, standarddir, utils, usertypes
from qutebrowser.browser import greasemonkey, history, qutescheme
@@ -204,12 +204,13 @@ def web_tab_setup(qtbot, tab_registry, session_manager_stub,
@pytest.fixture
def webkit_tab(web_tab_setup, qtbot, cookiejar_and_cache, mode_manager,
- widget_container):
+ widget_container, webpage):
webkittab = pytest.importorskip('qutebrowser.browser.webkit.webkittab')
tab = webkittab.WebKitTab(win_id=0, mode_manager=mode_manager,
private=False)
widget_container.set_widget(tab)
+
return tab
@@ -318,6 +319,9 @@ def config_stub(stubs, monkeypatch, configdata_init, yaml_config_stub):
pass
conf.val = container # For easier use in tests
+
+ stylesheet.init()
+
return conf
@@ -416,6 +420,7 @@ def webengineview(qtbot, monkeypatch, web_tab_setup):
def webpage(qnam):
"""Get a new QWebPage object."""
QtWebKitWidgets = pytest.importorskip('PyQt5.QtWebKitWidgets')
+
class WebPageStub(QtWebKitWidgets.QWebPage):
"""QWebPage with default error pages disabled."""
@@ -425,8 +430,13 @@ def webpage(qnam):
return False
page = WebPageStub()
+
page.networkAccessManager().deleteLater()
page.setNetworkAccessManager(qnam)
+
+ from qutebrowser.browser.webkit import webkitsettings
+ webkitsettings._init_user_agent()
+
return page
diff --git a/tests/unit/browser/webengine/test_webengineinterceptor.py b/tests/unit/browser/webengine/test_webengineinterceptor.py
index 9ea8ddcf5..e4df5f460 100644
--- a/tests/unit/browser/webengine/test_webengineinterceptor.py
+++ b/tests/unit/browser/webengine/test_webengineinterceptor.py
@@ -27,13 +27,23 @@ pytest.importorskip('PyQt5.QtWebEngineWidgets')
from PyQt5.QtWebEngineCore import QWebEngineUrlRequestInfo
from qutebrowser.browser.webengine import interceptor
-
-
-class TestWebengineInterceptor:
-
- def test_requestinfo_dict_valid(self):
- """Test that the RESOURCE_TYPES dict is not missing any values."""
- qb_keys = interceptor.RequestInterceptor.RESOURCE_TYPES.keys()
- qt_keys = {i for i in vars(QWebEngineUrlRequestInfo).values()
- if isinstance(i, QWebEngineUrlRequestInfo.ResourceType)}
- assert qt_keys == qb_keys
+from qutebrowser.extensions import interceptors
+from qutebrowser.utils import qtutils
+
+
+def test_no_missing_resource_types():
+ request_interceptor = interceptor.RequestInterceptor()
+ qb_keys = request_interceptor._resource_types.keys()
+ qt_keys = {i for i in vars(QWebEngineUrlRequestInfo).values()
+ if isinstance(i, QWebEngineUrlRequestInfo.ResourceType)}
+ assert qt_keys == qb_keys
+
+
+def test_resource_type_values():
+ request_interceptor = interceptor.RequestInterceptor()
+ for qt_value, qb_item in request_interceptor._resource_types.items():
+ if (qtutils.version_check('5.7.1', exact=True, compiled=False) and
+ qb_item == interceptors.ResourceType.unknown):
+ # Qt 5.7 has ResourceTypeUnknown = 18 instead of 255
+ continue
+ assert qt_value == qb_item.value
diff --git a/tests/unit/browser/webengine/test_webenginesettings.py b/tests/unit/browser/webengine/test_webenginesettings.py
index 0e369d655..d2a6b96ba 100644
--- a/tests/unit/browser/webengine/test_webenginesettings.py
+++ b/tests/unit/browser/webengine/test_webenginesettings.py
@@ -33,6 +33,7 @@ from qutebrowser.misc import objects
def init(qapp, config_stub, cache_tmpdir, data_tmpdir, monkeypatch):
monkeypatch.setattr(webenginesettings.webenginequtescheme, 'init',
lambda: None)
+ monkeypatch.setattr(objects, 'backend', usertypes.Backend.QtWebEngine)
init_args = types.SimpleNamespace(enable_webengine_inspector=False)
webenginesettings.init(init_args)
config_stub.changed.disconnect(webenginesettings._update_settings)
@@ -49,7 +50,6 @@ def test_big_cache_size(config_stub):
@pytest.mark.skipif(
not qtutils.version_check('5.8'), reason="Needs Qt 5.8 or newer")
def test_non_existing_dict(config_stub, monkeypatch, message_mock, caplog):
- monkeypatch.setattr(objects, 'backend', usertypes.Backend.QtWebEngine)
monkeypatch.setattr(webenginesettings.spell, 'local_filename',
lambda _code: None)
config_stub.val.spellcheck.languages = ['af-ZA']
@@ -66,7 +66,6 @@ def test_non_existing_dict(config_stub, monkeypatch, message_mock, caplog):
@pytest.mark.skipif(
not qtutils.version_check('5.8'), reason="Needs Qt 5.8 or newer")
def test_existing_dict(config_stub, monkeypatch):
- monkeypatch.setattr(objects, 'backend', usertypes.Backend.QtWebEngine)
monkeypatch.setattr(webenginesettings.spell, 'local_filename',
lambda _code: 'en-US-8-0')
config_stub.val.spellcheck.languages = ['en-US']
@@ -80,7 +79,6 @@ def test_existing_dict(config_stub, monkeypatch):
@pytest.mark.skipif(
not qtutils.version_check('5.8'), reason="Needs Qt 5.8 or newer")
def test_spell_check_disabled(config_stub, monkeypatch):
- monkeypatch.setattr(objects, 'backend', usertypes.Backend.QtWebEngine)
config_stub.val.spellcheck.languages = []
webenginesettings._update_settings('spellcheck.languages')
for profile in [webenginesettings.default_profile,
@@ -89,4 +87,11 @@ def test_spell_check_disabled(config_stub, monkeypatch):
def test_default_user_agent_saved():
- assert webenginesettings.default_user_agent is not None
+ assert webenginesettings.parsed_user_agent is not None
+
+
+def test_parsed_user_agent(qapp):
+ webenginesettings.init_user_agent()
+ parsed = webenginesettings.parsed_user_agent
+ assert parsed.upstream_browser_key == 'Chrome'
+ assert parsed.qt_key == 'QtWebEngine'
diff --git a/tests/unit/browser/webkit/test_webkitsettings.py b/tests/unit/browser/webkit/test_webkitsettings.py
new file mode 100644
index 000000000..bb7fbecb8
--- /dev/null
+++ b/tests/unit/browser/webkit/test_webkitsettings.py
@@ -0,0 +1,31 @@
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+
+# Copyright 2019 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/>.
+
+import pytest
+pytest.importorskip('PyQt5.QtWebKitWidgets')
+
+from qutebrowser.browser.webkit import webkitsettings
+
+
+def test_parsed_user_agent(qapp):
+ webkitsettings._init_user_agent()
+
+ parsed = webkitsettings.parsed_user_agent
+ assert parsed.upstream_browser_key == 'Version'
+ assert parsed.qt_key == 'Qt'
diff --git a/tests/unit/config/test_config.py b/tests/unit/config/test_config.py
index 9eefedf15..d81cb91dd 100644
--- a/tests/unit/config/test_config.py
+++ b/tests/unit/config/test_config.py
@@ -23,10 +23,10 @@ import unittest.mock
import functools
import pytest
-from PyQt5.QtCore import QObject, QUrl
+from PyQt5.QtCore import QUrl
from PyQt5.QtGui import QColor
-from qutebrowser.config import config, configdata, configexc, configutils
+from qutebrowser.config import config, configdata, configexc
from qutebrowser.utils import usertypes, urlmatch
from qutebrowser.misc import objects
from qutebrowser.keyinput import keyutils
@@ -410,7 +410,7 @@ class TestConfig:
assert conf.get(name) == 'always'
if save_yaml:
- assert yaml_value(name) is configutils.UNSET
+ assert yaml_value(name) is usertypes.UNSET
else:
assert yaml_value(name) == 'never'
@@ -439,8 +439,8 @@ class TestConfig:
assert options == {name1, name2}
if save_yaml:
- assert yaml_value(name1) is configutils.UNSET
- assert yaml_value(name2) is configutils.UNSET
+ assert yaml_value(name1) is usertypes.UNSET
+ assert yaml_value(name2) is usertypes.UNSET
else:
assert yaml_value(name1) == 'never'
assert yaml_value(name2) is True
@@ -482,7 +482,7 @@ class TestConfig:
@pytest.mark.parametrize('fallback, expected', [
(True, True),
- (False, configutils.UNSET)
+ (False, usertypes.UNSET)
])
def test_get_for_url_fallback(self, conf, fallback, expected):
"""Test conf.get() with a URL and fallback."""
@@ -617,7 +617,7 @@ class TestConfig:
pattern = urlmatch.UrlPattern('*://example.com')
name = 'content.javascript.enabled'
value = conf.get_obj_for_pattern(name, pattern=pattern)
- assert value is configutils.UNSET
+ assert value is usertypes.UNSET
def test_get_str(self, conf):
assert conf.get_str('content.plugins') == 'false'
@@ -637,7 +637,7 @@ class TestConfig:
if save_yaml:
assert yaml_value(option) is True
else:
- assert yaml_value(option) is configutils.UNSET
+ assert yaml_value(option) is usertypes.UNSET
@pytest.mark.parametrize('method', ['set_obj', 'set_str'])
def test_set_invalid(self, conf, qtbot, method):
@@ -669,7 +669,7 @@ class TestConfig:
meth(option, value, save_yaml=True)
assert not conf._values[option]
- assert yaml_value(option) is configutils.UNSET
+ assert yaml_value(option) is usertypes.UNSET
@pytest.mark.parametrize('method, value', [
('set_obj', {}),
@@ -766,57 +766,3 @@ class TestContainer:
with pytest.raises(TypeError,
match="Can't use pattern without configapi!"):
config.ConfigContainer(config_stub, pattern=pattern)
-
-
-class StyleObj(QObject):
-
- def __init__(self, stylesheet=None, parent=None):
- super().__init__(parent)
- if stylesheet is not None:
- self.STYLESHEET = stylesheet # noqa: N801,N806 pylint: disable=invalid-name
- self.rendered_stylesheet = None
-
- def setStyleSheet(self, stylesheet):
- self.rendered_stylesheet = stylesheet
-
-
-def test_get_stylesheet(config_stub):
- config_stub.val.colors.hints.fg = 'magenta'
- observer = config.StyleSheetObserver(
- StyleObj(), stylesheet="{{ conf.colors.hints.fg }}", update=False)
- assert observer._get_stylesheet() == 'magenta'
-
-
-@pytest.mark.parametrize('delete', [True, False])
-@pytest.mark.parametrize('stylesheet_param', [True, False])
-@pytest.mark.parametrize('update', [True, False])
-def test_set_register_stylesheet(delete, stylesheet_param, update, qtbot,
- config_stub, caplog):
- config_stub.val.colors.hints.fg = 'magenta'
- stylesheet = "{{ conf.colors.hints.fg }}"
-
- with caplog.at_level(9): # VDEBUG
- if stylesheet_param:
- obj = StyleObj()
- config.set_register_stylesheet(obj, stylesheet=stylesheet,
- update=update)
- else:
- obj = StyleObj(stylesheet)
- config.set_register_stylesheet(obj, update=update)
-
- assert caplog.messages[-1] == 'stylesheet for StyleObj: magenta'
-
- assert obj.rendered_stylesheet == 'magenta'
-
- if delete:
- with qtbot.waitSignal(obj.destroyed):
- obj.deleteLater()
-
- config_stub.val.colors.hints.fg = 'yellow'
-
- if delete or not update:
- expected = 'magenta'
- else:
- expected = 'yellow'
-
- assert obj.rendered_stylesheet == expected
diff --git a/tests/unit/config/test_configcommands.py b/tests/unit/config/test_configcommands.py
index 382d41ad8..97dcd7c42 100644
--- a/tests/unit/config/test_configcommands.py
+++ b/tests/unit/config/test_configcommands.py
@@ -25,7 +25,7 @@ import unittest.mock
import pytest
from PyQt5.QtCore import QUrl
-from qutebrowser.config import configcommands, configutils
+from qutebrowser.config import configcommands
from qutebrowser.api import cmdutils
from qutebrowser.utils import usertypes, urlmatch
from qutebrowser.keyinput import keyutils
@@ -92,7 +92,7 @@ class TestSet:
commands.set(0, option, inp, temp=temp)
assert config_stub.get(option) == new_value
- assert yaml_value(option) == (configutils.UNSET if temp else new_value)
+ assert yaml_value(option) == (usertypes.UNSET if temp else new_value)
def test_set_with_pattern(self, monkeypatch, commands, config_stub):
monkeypatch.setattr(objects, 'backend', usertypes.Backend.QtWebKit)
@@ -295,7 +295,7 @@ class TestAdd:
assert str(config_stub.get(name)[-1]) == value
if temp:
- assert yaml_value(name) == configutils.UNSET
+ assert yaml_value(name) == usertypes.UNSET
else:
assert yaml_value(name)[-1] == value
@@ -328,7 +328,7 @@ class TestAdd:
assert str(config_stub.get(name)[key]) == value
if temp:
- assert yaml_value(name) == configutils.UNSET
+ assert yaml_value(name) == usertypes.UNSET
else:
assert yaml_value(name)[key] == value
@@ -379,7 +379,7 @@ class TestRemove:
assert value not in config_stub.get(name)
if temp:
- assert yaml_value(name) == configutils.UNSET
+ assert yaml_value(name) == usertypes.UNSET
else:
assert value not in yaml_value(name)
@@ -410,7 +410,7 @@ class TestRemove:
assert key not in config_stub.get(name)
if temp:
- assert yaml_value(name) == configutils.UNSET
+ assert yaml_value(name) == usertypes.UNSET
else:
assert key not in yaml_value(name)
@@ -446,7 +446,7 @@ class TestUnsetAndClear:
commands.config_unset(name, temp=temp)
assert config_stub.get(name) == 'always'
- assert yaml_value(name) == ('never' if temp else configutils.UNSET)
+ assert yaml_value(name) == ('never' if temp else usertypes.UNSET)
def test_unset_unknown_option(self, commands):
with pytest.raises(cmdutils.CommandError, match="No option 'tabs'"):
@@ -460,7 +460,7 @@ class TestUnsetAndClear:
commands.config_clear(save=save)
assert config_stub.get(name) == 'always'
- assert yaml_value(name) == (configutils.UNSET if save else 'never')
+ assert yaml_value(name) == (usertypes.UNSET if save else 'never')
class TestSource:
diff --git a/tests/unit/config/test_configfiles.py b/tests/unit/config/test_configfiles.py
index 833e1e4fd..36bf88868 100644
--- a/tests/unit/config/test_configfiles.py
+++ b/tests/unit/config/test_configfiles.py
@@ -354,6 +354,17 @@ class TestYaml:
def test_title_format_migrations(self, migration_test, setting, old, new):
migration_test(setting, old, new)
+ @pytest.mark.parametrize('old, new', [
+ (None, ('Mozilla/5.0 ({os_info}) '
+ 'AppleWebKit/{webkit_version} (KHTML, like Gecko) '
+ '{qt_key}/{qt_version} '
+ '{upstream_browser_key}/{upstream_browser_version} '
+ 'Safari/{webkit_version}')),
+ ('toaster', 'toaster'),
+ ])
+ def test_user_agent_migration(self, migration_test, old, new):
+ migration_test('content.headers.user_agent', old, new)
+
def test_renamed_key_unknown_target(self, monkeypatch, yaml,
autoconfig):
"""A key marked as renamed with invalid name should raise an error."""
diff --git a/tests/unit/config/test_configinit.py b/tests/unit/config/test_configinit.py
index 671653b94..9d2843f63 100644
--- a/tests/unit/config/test_configinit.py
+++ b/tests/unit/config/test_configinit.py
@@ -308,6 +308,7 @@ class TestEarlyInit:
('qt.force_software_rendering', 'chromium',
'QT_WEBENGINE_DISABLE_NOUVEAU_WORKAROUND', '1'),
('qt.force_platform', 'toaster', 'QT_QPA_PLATFORM', 'toaster'),
+ ('qt.force_platformtheme', 'lxde', 'QT_QPA_PLATFORMTHEME', 'lxde'),
('window.hide_decoration', True,
'QT_WAYLAND_DISABLE_WINDOWDECORATION', '1')
])
diff --git a/tests/unit/config/test_configtypes.py b/tests/unit/config/test_configtypes.py
index e7b483dd8..e766fca8a 100644
--- a/tests/unit/config/test_configtypes.py
+++ b/tests/unit/config/test_configtypes.py
@@ -34,8 +34,8 @@ from PyQt5.QtGui import QColor, QFont
from PyQt5.QtNetwork import QNetworkProxy
from qutebrowser.misc import objects
-from qutebrowser.config import configtypes, configexc, configutils
-from qutebrowser.utils import debug, utils, qtutils, urlmatch
+from qutebrowser.config import configtypes, configexc
+from qutebrowser.utils import debug, utils, qtutils, urlmatch, usertypes
from qutebrowser.browser.network import pac
from qutebrowser.keyinput import keyutils
from helpers import utils as testutils
@@ -277,7 +277,7 @@ class TestAll:
@pytest.mark.parametrize('none_ok', [True, False])
def test_unset(self, klass, none_ok):
typ = klass(none_ok=none_ok)
- assert typ.to_py(configutils.UNSET) is configutils.UNSET
+ assert typ.to_py(usertypes.UNSET) is usertypes.UNSET
def test_to_str_none(self, klass):
assert klass().to_str(None) == ''
@@ -1838,8 +1838,12 @@ class TestDirectory:
class TestFormatString:
@pytest.fixture
- def typ(self):
- return configtypes.FormatString(fields=('foo', 'bar'))
+ def klass(self):
+ return configtypes.FormatString
+
+ @pytest.fixture
+ def typ(self, klass):
+ return klass(fields=('foo', 'bar'))
@pytest.mark.parametrize('val', [
'foo bar baz',
@@ -1857,6 +1861,14 @@ class TestFormatString:
with pytest.raises(configexc.ValidationError):
typ.to_py(val)
+ @pytest.mark.parametrize('value', [
+ None,
+ ['one', 'two'],
+ [('1', 'one'), ('2', 'two')],
+ ])
+ def test_complete(self, klass, value):
+ assert klass(fields=('foo'), completions=value).complete() == value
+
class TestShellCommand:
diff --git a/tests/unit/config/test_configutils.py b/tests/unit/config/test_configutils.py
index e87e4ab93..2c99ec34b 100644
--- a/tests/unit/config/test_configutils.py
+++ b/tests/unit/config/test_configutils.py
@@ -23,19 +23,10 @@ import pytest
from PyQt5.QtCore import QUrl
from qutebrowser.config import configutils, configdata, configtypes
-from qutebrowser.utils import urlmatch
+from qutebrowser.utils import urlmatch, usertypes
from tests.helpers import utils
-def test_unset_object_identity():
- assert configutils.Unset() is not configutils.Unset()
- assert configutils.UNSET is configutils.UNSET
-
-
-def test_unset_object_repr():
- assert repr(configutils.UNSET) == '<UNSET>'
-
-
@pytest.fixture
def opt():
return configdata.Option(name='example.option', typ=configtypes.String(),
@@ -62,6 +53,14 @@ def values(opt, pattern):
@pytest.fixture
+def mixed_values(opt, pattern):
+ scoped_values = [configutils.ScopedValue('global value', None),
+ configutils.ScopedValue('example value', pattern,
+ hide_userconfig=True)]
+ return configutils.Values(opt, scoped_values)
+
+
+@pytest.fixture
def empty_values(opt):
return configutils.Values(opt)
@@ -78,6 +77,23 @@ def test_str_empty(empty_values):
assert str(empty_values) == 'example.option: <unchanged>'
+def test_str_mixed(mixed_values):
+ expected = [
+ 'example.option = global value',
+ '*://www.example.com/: example.option = example value',
+ ]
+ assert str(mixed_values) == '\n'.join(expected)
+
+
+@pytest.mark.parametrize('include_hidden, expected', [
+ (True, ['example.option = global value',
+ '*://www.example.com/: example.option = example value']),
+ (False, ['example.option = global value']),
+])
+def test_dump(mixed_values, include_hidden, expected):
+ assert mixed_values.dump(include_hidden=include_hidden) == expected
+
+
def test_bool(values, empty_values):
assert values
assert not empty_values
@@ -121,7 +137,7 @@ def test_clear(values):
assert values
values.clear()
assert not values
- assert values.get_for_url(fallback=False) is configutils.UNSET
+ assert values.get_for_url(fallback=False) is usertypes.UNSET
def test_get_matching(values):
@@ -130,12 +146,12 @@ def test_get_matching(values):
def test_get_unset(empty_values):
- assert empty_values.get_for_url(fallback=False) is configutils.UNSET
+ assert empty_values.get_for_url(fallback=False) is usertypes.UNSET
def test_get_no_global(empty_values, other_pattern, pattern):
empty_values.add('example.org value', pattern)
- assert empty_values.get_for_url(fallback=False) is configutils.UNSET
+ assert empty_values.get_for_url(fallback=False) is usertypes.UNSET
def test_get_unset_fallback(empty_values):
@@ -144,7 +160,7 @@ def test_get_unset_fallback(empty_values):
def test_get_non_matching(values):
url = QUrl('https://www.example.ch/')
- assert values.get_for_url(url, fallback=False) is configutils.UNSET
+ assert values.get_for_url(url, fallback=False) is usertypes.UNSET
def test_get_non_matching_fallback(values):
@@ -180,13 +196,13 @@ def test_get_pattern_none(values, pattern):
def test_get_unset_pattern(empty_values, pattern):
value = empty_values.get_for_pattern(pattern, fallback=False)
- assert value is configutils.UNSET
+ assert value is usertypes.UNSET
def test_get_no_global_pattern(empty_values, pattern, other_pattern):
empty_values.add('example.org value', other_pattern)
value = empty_values.get_for_pattern(pattern, fallback=False)
- assert value is configutils.UNSET
+ assert value is usertypes.UNSET
def test_get_unset_fallback_pattern(empty_values, pattern):
@@ -195,7 +211,7 @@ def test_get_unset_fallback_pattern(empty_values, pattern):
def test_get_non_matching_pattern(values, other_pattern):
value = values.get_for_pattern(other_pattern, fallback=False)
- assert value is configutils.UNSET
+ assert value is usertypes.UNSET
def test_get_non_matching_fallback_pattern(values, other_pattern):
@@ -238,3 +254,26 @@ def test_domain_lookup_sparse_benchmark(url, values, benchmark):
values.add(False, urlmatch.UrlPattern(line))
benchmark(lambda: values.get_for_url(url))
+
+
+class TestWiden:
+
+ @pytest.mark.parametrize('hostname, expected', [
+ ('a.b.c', ['a.b.c', 'b.c', 'c']),
+ ('foobarbaz', ['foobarbaz']),
+ ('', []),
+ ('.c', ['.c', 'c']),
+ ('c.', ['c.']),
+ ('.c.', ['.c.', 'c.']),
+ (None, []),
+ ])
+ def test_widen_hostnames(self, hostname, expected):
+ assert list(configutils._widened_hostnames(hostname)) == expected
+
+ @pytest.mark.parametrize('hostname', [
+ 'test.qutebrowser.org',
+ 'a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.z.y.z',
+ 'qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq.c',
+ ])
+ def test_bench_widen_hostnames(self, hostname, benchmark):
+ benchmark(lambda: list(configutils._widened_hostnames(hostname)))
diff --git a/tests/unit/config/test_stylesheet.py b/tests/unit/config/test_stylesheet.py
new file mode 100644
index 000000000..67bdd04b4
--- /dev/null
+++ b/tests/unit/config/test_stylesheet.py
@@ -0,0 +1,72 @@
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+# Copyright 2019 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/>.
+
+import pytest
+
+from PyQt5.QtCore import QObject
+
+from qutebrowser.config import stylesheet
+
+
+class StyleObj(QObject):
+
+ def __init__(self, stylesheet=None, parent=None):
+ super().__init__(parent)
+ if stylesheet is not None:
+ self.STYLESHEET = stylesheet # noqa: N801,N806 pylint: disable=invalid-name
+ self.rendered_stylesheet = None
+
+ def setStyleSheet(self, stylesheet):
+ self.rendered_stylesheet = stylesheet
+
+
+def test_get_stylesheet(config_stub):
+ config_stub.val.colors.hints.fg = 'magenta'
+ observer = stylesheet._StyleSheetObserver(
+ StyleObj(), stylesheet="{{ conf.colors.hints.fg }}", update=False)
+ assert observer._get_stylesheet() == 'magenta'
+
+
+@pytest.mark.parametrize('delete', [True, False])
+@pytest.mark.parametrize('stylesheet_param', [True, False])
+@pytest.mark.parametrize('update', [True, False])
+def test_set_register_stylesheet(delete, stylesheet_param, update, qtbot,
+ config_stub, caplog):
+ config_stub.val.colors.hints.fg = 'magenta'
+ qss = "{{ conf.colors.hints.fg }}"
+
+ with caplog.at_level(9): # VDEBUG
+ if stylesheet_param:
+ obj = StyleObj()
+ stylesheet.set_register(obj, qss, update=update)
+ else:
+ obj = StyleObj(qss)
+ stylesheet.set_register(obj, update=update)
+
+ assert caplog.messages[-1] == 'stylesheet for StyleObj: magenta'
+
+ assert obj.rendered_stylesheet == 'magenta'
+
+ if delete:
+ with qtbot.waitSignal(obj.destroyed):
+ obj.deleteLater()
+
+ config_stub.val.colors.hints.fg = 'yellow'
+
+ expected = 'magenta' if delete or not update else 'yellow'
+ assert obj.rendered_stylesheet == expected
diff --git a/tests/unit/config/test_websettings.py b/tests/unit/config/test_websettings.py
new file mode 100644
index 000000000..b00f24af9
--- /dev/null
+++ b/tests/unit/config/test_websettings.py
@@ -0,0 +1,104 @@
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+
+# Copyright 2019 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/>.
+
+import pytest
+
+from qutebrowser.config import websettings
+from qutebrowser.misc import objects
+from qutebrowser.utils import usertypes
+
+
+@pytest.mark.parametrize([
+ 'user_agent', 'os_info', 'webkit_version',
+ 'upstream_browser_key', 'upstream_browser_version', 'qt_key'
+], [
+ (
+ # QtWebEngine, Linux
+ # (no differences other than Chrome version with older Qt Versions)
+ ("Mozilla/5.0 (X11; Linux x86_64) "
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
+ "QtWebEngine/5.14.0 Chrome/77.0.3865.98 Safari/537.36"),
+ "X11; Linux x86_64",
+ "537.36",
+ "Chrome", "77.0.3865.98",
+ "QtWebEngine",
+ ), (
+ # QtWebKit, Linux
+ ("Mozilla/5.0 (X11; Linux x86_64) "
+ "AppleWebKit/602.1 (KHTML, like Gecko) "
+ "qutebrowser/1.8.3 "
+ "Version/10.0 Safari/602.1"),
+ "X11; Linux x86_64",
+ "602.1",
+ "Version", "10.0",
+ "Qt",
+ ), (
+ # QtWebEngine, macOS
+ ("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) "
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
+ "QtWebEngine/5.13.2 Chrome/73.0.3683.105 Safari/537.36"),
+ "Macintosh; Intel Mac OS X 10_12_6",
+ "537.36",
+ "Chrome", "73.0.3683.105",
+ "QtWebEngine",
+ ), (
+ # QtWebEngine, Windows
+ ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
+ "QtWebEngine/5.12.5 Chrome/69.0.3497.128 Safari/537.36"),
+ "Windows NT 10.0; Win64; x64",
+ "537.36",
+ "Chrome", "69.0.3497.128",
+ "QtWebEngine",
+ )
+])
+def test_parse_user_agent(user_agent, os_info, webkit_version,
+ upstream_browser_key, upstream_browser_version,
+ qt_key):
+ parsed = websettings.UserAgent.parse(user_agent)
+ assert parsed.os_info == os_info
+ assert parsed.webkit_version == webkit_version
+ assert parsed.upstream_browser_key == upstream_browser_key
+ assert parsed.upstream_browser_version == upstream_browser_version
+ assert parsed.qt_key == qt_key
+
+
+def test_user_agent(monkeypatch, config_stub, qapp):
+ webenginesettings = pytest.importorskip(
+ "qutebrowser.browser.webengine.webenginesettings")
+ monkeypatch.setattr(objects, 'backend', usertypes.Backend.QtWebEngine)
+ webenginesettings.init_user_agent()
+
+ config_stub.val.content.headers.user_agent = 'test {qt_key}'
+ assert websettings.user_agent() == 'test QtWebEngine'
+
+ config_stub.val.content.headers.user_agent = 'test2 {qt_key}'
+ assert websettings.user_agent() == 'test2 QtWebEngine'
+
+
+def test_config_init(request, monkeypatch, config_stub):
+ if request.config.webengine:
+ from qutebrowser.browser.webengine import webenginesettings
+ monkeypatch.setattr(webenginesettings, 'init', lambda _args: None)
+ else:
+ from qutebrowser.browser.webkit import webkitsettings
+ monkeypatch.setattr(webkitsettings, 'init', lambda _args: None)
+
+ websettings.init(args=None)
+ assert config_stub.dump_userconfig() == '<Default configuration>'
diff --git a/tests/unit/javascript/stylesheet/test_stylesheet.py b/tests/unit/javascript/stylesheet/test_stylesheet_js.py
index 768ffaeb9..768ffaeb9 100644
--- a/tests/unit/javascript/stylesheet/test_stylesheet.py
+++ b/tests/unit/javascript/stylesheet/test_stylesheet_js.py
diff --git a/tests/unit/utils/test_urlutils.py b/tests/unit/utils/test_urlutils.py
index bf1ce47c8..49cee6382 100644
--- a/tests/unit/utils/test_urlutils.py
+++ b/tests/unit/utils/test_urlutils.py
@@ -346,7 +346,9 @@ def test_get_search_url_invalid(url):
(True, True, False, '127.0.0.1'),
(True, True, False, '::1'),
(True, True, True, '2001:41d0:2:6c11::1'),
+ (True, True, True, '[2001:41d0:2:6c11::1]:8000'),
(True, True, True, '94.23.233.17'),
+ (True, True, True, '94.23.233.17:8000'),
# Special URLs
(True, True, False, 'file:///tmp/foo'),
(True, True, False, 'about:blank'),
@@ -706,26 +708,3 @@ class TestProxyFromUrl:
def test_invalid(self, url, exception):
with pytest.raises(exception):
urlutils.proxy_from_url(QUrl(url))
-
-
-class TestWiden:
-
- @pytest.mark.parametrize('hostname, expected', [
- ('a.b.c', ['a.b.c', 'b.c', 'c']),
- ('foobarbaz', ['foobarbaz']),
- ('', []),
- ('.c', ['.c', 'c']),
- ('c.', ['c.']),
- ('.c.', ['.c.', 'c.']),
- (None, []),
- ])
- def test_widen_hostnames(self, hostname, expected):
- assert list(urlutils.widened_hostnames(hostname)) == expected
-
- @pytest.mark.parametrize('hostname', [
- 'test.qutebrowser.org',
- 'a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.z.y.z',
- 'qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq.c',
- ])
- def test_bench_widen_hostnames(self, hostname, benchmark):
- benchmark(lambda: list(urlutils.widened_hostnames(hostname)))
diff --git a/tests/unit/utils/test_version.py b/tests/unit/utils/test_version.py
index b630a6159..d1f692540 100644
--- a/tests/unit/utils/test_version.py
+++ b/tests/unit/utils/test_version.py
@@ -851,36 +851,36 @@ class FakeQSslSocket:
return self._version
-@pytest.mark.parametrize('ua, expected', [
- (None, 'unavailable'), # No QWebEngineProfile
- ('Mozilla/5.0', 'unknown'),
- ('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) '
- 'QtWebEngine/5.8.0 Chrome/53.0.2785.148 Safari/537.36', '53.0.2785.148'),
-])
-def test_chromium_version(monkeypatch, caplog, ua, expected):
+_QTWE_USER_AGENT = ("Mozilla/5.0 (X11; Linux x86_64) "
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
+ "QtWebEngine/5.14.0 Chrome/{} Safari/537.36")
+
+
+def test_chromium_version(monkeypatch, caplog):
pytest.importorskip('PyQt5.QtWebEngineWidgets')
- if ua is None:
- monkeypatch.setattr(version, 'webenginesettings', None)
- else:
- monkeypatch.setattr(version.webenginesettings,
- 'default_user_agent', ua)
- with caplog.at_level(logging.ERROR):
- assert version._chromium_version() == expected
+ ver = '77.0.3865.98'
+ version.webenginesettings._init_user_agent_str(
+ _QTWE_USER_AGENT.format(ver))
+
+ assert version._chromium_version() == ver
+
+
+def test_chromium_version_no_webengine(monkeypatch):
+ monkeypatch.setattr(version, 'webenginesettings', None)
+ assert version._chromium_version() == 'unavailable'
def test_chromium_version_prefers_saved_user_agent(monkeypatch):
pytest.importorskip('PyQt5.QtWebEngineWidgets')
- monkeypatch.setattr(
- version.webenginesettings, 'default_user_agent',
- 'QtWebEngine/5.8.0 Chrome/53.0.2785.148 Safari/537.36'
- )
+ version.webenginesettings._init_user_agent_str(_QTWE_USER_AGENT)
class FakeProfile:
def defaultProfile(self):
raise AssertionError("Should not be called")
- monkeypatch.setattr(version, 'QWebEngineProfile', FakeProfile())
+ monkeypatch.setattr(version.webenginesettings, 'QWebEngineProfile',
+ FakeProfile())
version._chromium_version()
@@ -918,12 +918,9 @@ class VersionParams:
], ids=lambda param: param.name)
def test_version_output(params, stubs, monkeypatch, config_stub):
"""Test version.version()."""
- class FakeWebEngineSettings:
- default_user_agent = ('Toaster/4.0.4 Chrome/CHROMIUMVERSION '
- 'Teapot/4.1.8')
-
config.instance.config_py_loaded = params.config_py_loaded
import_path = os.path.abspath('/IMPORTPATH')
+
patches = {
'qutebrowser.__file__': os.path.join(import_path, '__init__.py'),
'qutebrowser.__version__': 'VERSION',
@@ -960,6 +957,12 @@ def test_version_output(params, stubs, monkeypatch, config_stub):
'autoconfig_loaded': "yes" if params.autoconfig_loaded else "no",
}
+ ua = _QTWE_USER_AGENT.format('CHROMIUMVERSION')
+ if version.webenginesettings is None:
+ patches['_chromium_version'] = lambda: 'CHROMIUMVERSION'
+ else:
+ version.webenginesettings._init_user_agent_str(ua)
+
if params.config_py_loaded:
substitutions["config_py_loaded"] = "{} has been loaded".format(
standarddir.config_py())
@@ -975,8 +978,6 @@ def test_version_output(params, stubs, monkeypatch, config_stub):
monkeypatch.delattr(version, 'qtutils.qWebKitVersion', raising=False)
patches['objects.backend'] = usertypes.Backend.QtWebEngine
substitutions['backend'] = 'QtWebEngine (Chromium CHROMIUMVERSION)'
- patches['webenginesettings'] = FakeWebEngineSettings
- patches['QWebEngineProfile'] = True
if params.known_distribution:
patches['distribution'] = lambda: version.DistributionInfo(
@@ -1003,9 +1004,9 @@ def test_version_output(params, stubs, monkeypatch, config_stub):
template = textwrap.dedent("""
qutebrowser vVERSION{git_commit}
Backend: {backend}
+ Qt: {qt}
PYTHON IMPLEMENTATION: PYTHON VERSION
- Qt: {qt}
PyQt: PYQT VERSION
MODULE VERSION 1
diff --git a/tests/unit/utils/usertypes/test_misc.py b/tests/unit/utils/usertypes/test_misc.py
index 1700b7f51..68eabc213 100644
--- a/tests/unit/utils/usertypes/test_misc.py
+++ b/tests/unit/utils/usertypes/test_misc.py
@@ -25,3 +25,12 @@ def test_abstract_certificate_error_wrapper():
err = object()
wrapper = usertypes.AbstractCertificateErrorWrapper(err)
assert wrapper._error is err
+
+
+def test_unset_object_identity():
+ assert usertypes.Unset() is not usertypes.Unset()
+ assert usertypes.UNSET is usertypes.UNSET
+
+
+def test_unset_object_repr():
+ assert repr(usertypes.UNSET) == '<UNSET>'
diff --git a/tox.ini b/tox.ini
index f47952e36..55bcef78a 100644
--- a/tox.ini
+++ b/tox.ini
@@ -13,8 +13,8 @@ skipsdist = true
setenv =
QT_QPA_PLATFORM_PLUGIN_PATH={envdir}/Lib/site-packages/PyQt5/plugins/platforms
PYTEST_QT_API=pyqt5
- pyqt{,57,59,510,511,512,513}: LINK_PYQT_SKIP=true
- pyqt{,57,59,510,511,512,513}: QUTE_BDD_WEBENGINE=true
+ pyqt{,57,59,510,511,512,513,514}: LINK_PYQT_SKIP=true
+ pyqt{,57,59,510,511,512,513,514}: QUTE_BDD_WEBENGINE=true
cov: PYTEST_ADDOPTS=--cov --cov-report xml --cov-report=html --cov-report=
passenv = PYTHON DISPLAY XAUTHORITY HOME USERNAME USER CI TRAVIS XDG_* QUTE_* DOCKER QT_QUICK_BACKEND
basepython =
@@ -32,6 +32,7 @@ deps =
pyqt511: -r{toxinidir}/misc/requirements/requirements-pyqt-5.11.txt
pyqt512: -r{toxinidir}/misc/requirements/requirements-pyqt-5.12.txt
pyqt513: -r{toxinidir}/misc/requirements/requirements-pyqt-5.13.txt
+ pyqt514: -r{toxinidir}/misc/requirements/requirements-pyqt-5.14.txt
commands =
{envpython} scripts/link_pyqt.py --tox {envdir}
{envpython} -bb -m pytest {posargs:tests}