Merge branch 'master' of https://github.com/qutebrowser/qutebrowser into perdomainstylesheets

This commit is contained in:
Jay Kamat 2019-02-22 22:23:57 -08:00
commit 7c175543d0
No known key found for this signature in database
GPG Key ID: 5D2E399600F4F7B5
259 changed files with 60038 additions and 3626 deletions

View File

@ -14,6 +14,7 @@ exclude_lines =
raise NotImplementedError
raise utils\.Unreachable
if __name__ == ["']__main__["']:
if MYPY:
[xml]
output=coverage.xml

14
.flake8
View File

@ -33,6 +33,7 @@ exclude = .*,__pycache__,resources.py
# (false-positives)
# D413: Missing blank line after last section (not in pep257?)
# A003: Builtin name for class attribute (needed for overridden methods)
# W504: line break after binary operator
ignore =
B001,B008,B305,
E128,E226,E265,E501,E402,E266,E722,E731,
@ -40,15 +41,16 @@ ignore =
N802,
P101,P102,P103,
D102,D103,D106,D107,D104,D105,D209,D211,D401,D402,D403,D413,
A003
A003,
W504
min-version = 3.4.0
max-complexity = 12
per-file-ignores =
/tests/**/*.py : D100,D101,D401
/tests/unit/browser/test_history.py : N806
/tests/helpers/fixtures.py : N806
/tests/unit/browser/webkit/http/test_content_disposition.py : D400
/scripts/dev/ci/appveyor_install.py : FI53
qutebrowser/api/hook.py : N801
tests/* : D100,D101
tests/unit/browser/test_history.py : D100,D101,N806
tests/helpers/fixtures.py : D100,D101,N806
tests/unit/browser/webkit/http/test_content_disposition.py : D100,D101,D400
copyright-check = True
copyright-regexp = # Copyright [\d-]+ .*
copyright-min-file-size = 110

BIN
.github/img/hsr.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

1
.gitignore vendored
View File

@ -41,3 +41,4 @@ TODO
/scripts/testbrowser/cpp/webengine/.qmake.stash
/scripts/dev/pylint_checkers/qute_pylint.egg-info
/misc/file_version_info.txt
/doc/extapi/_build

View File

@ -1,78 +1,88 @@
sudo: false
dist: trusty
dist: xenial
language: python
group: edge
python: 3.6
os: linux
matrix:
fast_finish: true
include:
- os: linux
env: DOCKER=archlinux
### Archlinux QtWebKit
- env: DOCKER=archlinux
services: docker
- os: linux
env: DOCKER=archlinux-webengine QUTE_BDD_WEBENGINE=true
### Archlinux QtWebEngine
- env: DOCKER=archlinux-webengine QUTE_BDD_WEBENGINE=true
services: docker
- os: linux
env: TESTENV=py36-pyqt571
- os: linux
python: 3.5
### PyQt 5.7.1 (Python 3.5)
- python: 3.5
env: TESTENV=py35-pyqt571
- os: linux
env: TESTENV=py36-pyqt59
- os: linux
env: TESTENV=py36-pyqt510
### PyQt 5.7.1
- env: TESTENV=py36-pyqt571
### PyQt 5.9
- env: TESTENV=py36-pyqt59
### PyQt 5.10
- env: TESTENV=py36-pyqt510
addons:
apt:
packages:
- xfonts-base
- os: linux
env: TESTENV=py36-pyqt511-cov
# https://github.com/travis-ci/travis-ci/issues/9069
- os: linux
python: 3.7
sudo: required
dist: xenial
### PyQt 5.11 (with coverage)
- env: TESTENV=py36-pyqt511-cov
### PyQt 5.11 (Python 3.7)
- python: 3.7
env: TESTENV=py37-pyqt511
### PyQt 5.12
- env: TESTENV=py36-pyqt512
addons:
apt:
packages:
- libxkbcommon-x11-0
### macOS sierra
- os: osx
env: TESTENV=py37 OSX=sierra
env: TESTENV=py37-pyqt511 OSX=sierra
osx_image: xcode9.2
language: generic
### macOS yosemite
# https://github.com/qutebrowser/qutebrowser/issues/2013
# - os: osx
# env: TESTENV=py35 OSX=yosemite
# osx_image: xcode6.4
- os: linux
env: TESTENV=pylint PYTHON=python3.6
- os: linux
env: TESTENV=flake8
- os: linux
env: TESTENV=docs
### pylint/flake8/mypy
- env: TESTENV=pylint
- env: TESTENV=flake8
- env: TESTENV=mypy
### docs
- env: TESTENV=docs
addons:
apt:
packages:
- asciidoc
- os: linux
env: TESTENV=vulture
- os: linux
env: TESTENV=misc
- os: linux
env: TESTENV=pyroma
- os: linux
env: TESTENV=check-manifest
- os: linux
env: TESTENV=eslint
### vulture/misc/pyroma/check-manifest
- env: TESTENV=vulture
- env: TESTENV=misc
- env: TESTENV=pyroma
- env: TESTENV=check-manifest
### eslint
- env: TESTENV=eslint
language: node_js
python: null
node_js: "lts/*"
- os: linux
language: generic
### shellcheck
- language: generic
env: TESTENV=shellcheck
services: docker
fast_finish: true
allow_failures:
# https://github.com/qutebrowser/qutebrowser/issues/4055
- os: linux
env: TESTENV=py36-pyqt510
cache:
directories:
@ -91,16 +101,3 @@ after_success:
after_failure:
- bash scripts/dev/ci/travis_backtrace.sh
notifications:
webhooks:
- https://buildtimetrend.herokuapp.com/travis
irc:
channels:
- "chat.freenode.net#qutebrowser-dev"
on_success: always
on_failure: always
skip_join: true
template:
- "%{repository}#%{build_number} (%{branch} - %{commit} : %{author}): %{message}"
- "%{compare_url} - %{build_url}"

View File

@ -13,7 +13,7 @@ include qutebrowser/utils/testfile
include qutebrowser/git-commit-id
include LICENSE doc/* README.asciidoc
include misc/qutebrowser.desktop
include misc/qutebrowser.appdata.xml
include misc/org.qutebrowser.qutebrowser.appdata.xml
include misc/Makefile
include requirements.txt
include tox.ini
@ -32,6 +32,7 @@ include doc/changelog.asciidoc
prune tests
prune qutebrowser/3rdparty
exclude pytest.ini
exclude mypy.ini
exclude qutebrowser/javascript/.eslintrc.yaml
exclude qutebrowser/javascript/.eslintignore
exclude doc/help
@ -39,5 +40,6 @@ exclude .*
exclude misc/qutebrowser.spec
exclude misc/qutebrowser.nsi
exclude misc/qutebrowser.rcc
prune doc/extapi
global-exclude __pycache__ *.pyc *.pyo

View File

@ -154,7 +154,11 @@ https://www.macstadium.com/opensource[Open Source Project].
(They don't require including this here - I've just been very happy with their
offer, and without them, no macOS releases or tests would exist)
Thanks to the https://www.hsr.ch/[HSR Hochschule für Technik Rapperswil], which
made it possible to work on qutebrowser extensions as a student research project.
image:.github/img/macstadium.png["powered by MacStadium",width=200,link="https://www.macstadium.com/"]
image:.github/img/hsr.png["HSR Hochschule für Technik Rapperswil",link="https://www.hsr.ch/"]
Authors
-------

View File

@ -25,6 +25,7 @@ Added
opened from a page should stack on each other or not.
- New `completion.open_categories` setting which allows to configure which
categories are shown in the `:open` completion, and how they are ordered.
- New `tabs.pinned.frozen` setting to allow/deny navigating in pinned tabs.
- New config manipulation commands:
* `:config-dict-add` and `:config-list-add` to a new element to a dict/list
setting.
@ -32,6 +33,11 @@ Added
dict/list setting.
- New `hints.selectors` setting which allows to configure what CSS selectors
are used for hints, and also allows adding custom hint groups.
- New `:yank markdown` feature which yanks the current URL and title in
markdown format.
- Basic support for client certificates with Qt 5.12. Selecting the certificate
to show when there are multiple matching certificates isn't implemented yet.
- Support for DNS prefetching (`content.dns_prefetch`) with QtWebEngine on 5.12.
Changed
~~~~~~~
@ -40,7 +46,7 @@ Changed
(`:close`), while `:qa` quits (`:quit`). The behavior of `:wq` remains
unchanged (`:quit --save`), as closing a window while saving the session
doesn't make sense.
- Completion highlighting is now done differently (using QSyntaxHighlither),
- Completion highlighting is now done differently (using `QSyntaxHighlighter`),
which should fix some highlighting corner-cases.
- The `QtColor` config type now also understands colors like `rgb(...)`.
- `:yank` now has a `--quiet` option which causes it to not display a message.
@ -48,6 +54,17 @@ Changed
- The `content.host_blocking.enabled` setting now supports URL patterns, so the
adblocker can be disabled on a given page.
- Elements with a `tabindex` attribute now also get hints by default.
- Various small performance improvements for hints and the completion.
- The Wayland check for QtWebEngine is now disabled on Qt >= 5.11.2, as those
versions should work without any issues.
- The JavaScript `console` object is now available in PAC files.
- The metainfo file `qutebrowser.appdata.xml` is now renamed to
`org.qutebrowser.qutebrowser.appdata.xml`.
- The `qute-pass` userscript now understands domains in gpg filenames
in addition to directory names.
- macOS: The IPC socket path used to communicate with existing instances
changed due to changes in Qt 5.12. Please make sure to quit qutebrowser
before upgrading.
Fixed
~~~~~
@ -55,6 +72,34 @@ Fixed
- Invalid world IDs now get rejected for `:jseval` and GreaseMonkey scripts.
- When websites suggest download filenames with invalid characters, those are
now correctly replaced.
- Invalid hint length calculation in certain rare cases.
- Dragging tabs in the tab bar (which was broken in v1.5.0)
- Using Shift-Home in command mode now works properly.
- Workaround for a Qt bug which prevented
`content.cookies.accept = no-3rdparty` from working properly on some pages
like GMail. However, the default for `content.cookies.accept` is still `all`
to be in line with what other browsers do.
- `:navigate` not incrementing in anchors or queries.
- Crash when trying to use a proxy requiring authentication with QtWebKit.
- Slashes in search terms are now percent-escaped.
- When `scrolling.bar = True` was set in versions before v1.5.0, this now
correctly gets migrated to `always` instead of `when-searching`.
- Completion highlighting now works again on Qt 5.11.3 and 5.12.1.
- The outdated header `X-Do-Not-Track` is no longer sent.
- A javascript error on page load when using Qt 5.12 was fixed.
- `window.print()` works with Qt 5.12 now.
- PAC proxies were never correctly supported with QtWebEngine, but are now
explicitly disallowed.
v1.5.2
------
Changed
~~~~~~~
- The `content.cookies.accept` setting is now set to `all` instead of
`no-3rdparty` by default, as `no-3rdparty` breaks various pages such as
GMail.
v1.5.1
------
@ -344,11 +389,11 @@ v1.3.3
Security
~~~~~~~~
- An XSS vulnerability on the `qute://history` page allowed websites to inject
HTML into the page via a crafted title tag. This could allow them to steal
your browsing history. If you're currently unable to upgrade, avoid using
`:history`. A CVE request for this issue is pending, see
https://github.com/qutebrowser/qutebrowser/issues/4011[#4011] for updates.
- CVE-2018-1000559: An XSS vulnerability on the `qute://history` page allowed
websites to inject HTML into the page via a crafted title tag. This could
allow them to steal your browsing history. If you're currently unable to
upgrade, avoid using `:history`. See the related GitHub issue for details:
https://github.com/qutebrowser/qutebrowser/issues/4011.
Fixed
~~~~~
@ -1223,7 +1268,7 @@ Added
- New `:debug-log-filter` command to change console log filtering on-the-fly.
- New `:debug-log-level` command to change the console loglevel on-the-fly.
- New `general -> yank-ignored-url-parameters` option to configure which URL
parameters (like `utm_source` etc.) to strip off when yanking an URL.
parameters (like `utm_source` etc.) to strip off when yanking a URL.
- Support for the
https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API[HTML5 page visibility API]
- New `readability` userscript which shows a readable version of a page (using
@ -1334,7 +1379,7 @@ Changed
- `:hint` has a new `--add-history` argument to add the URL to the history for
yank/spawn targets.
- `:set` now cycles through values if more than one argument is given.
- `:open` now opens `default-page` without an URL even without `-t`/`-b`/`-w` given.
- `:open` now opens `default-page` without a URL even without `-t`/`-b`/`-w` given.
Deprecated
~~~~~~~~~~

View File

@ -407,7 +407,7 @@ Creating a new command is straightforward:
[source,python]
----
import qutebrowser.commands.cmdutils
from qutebrowser.api import cmdutils
...
@ -429,7 +429,7 @@ selects which object registry (global, per-tab, etc.) to use. See the
There are also other arguments to customize the way the command is
registered; see the class documentation for `register` in
`qutebrowser.commands.cmdutils` for details.
`qutebrowser.api.cmdutils` for details.
The types of the function arguments are inferred based on their default values,
e.g., an argument `foo=True` will be converted to a flag `-f`/`--foo` in
@ -480,8 +480,10 @@ For `typing.Union` types, the given `choices` are only checked if other types
The following arguments are supported for `@cmdutils.argument`:
- `flag`: Customize the short flag (`-x`) the argument will get.
- `win_id=True`: Mark the argument as special window ID argument.
- `count=True`: Mark the argument as special count argument.
- `value`: Tell qutebrowser to fill the argument with special values:
- `value=cmdutils.Value.count`: The `count` given by the user to the command.
- `value=cmdutils.Value.win_id`: The window ID of the current window.
- `value=cmdutils.Value.cur_tab`: The tab object which is currently focused.
- `completion`: A completion function (see `qutebrowser.completions.models.*`)
to use when completing arguments for the given command.
- `choices`: The allowed string choices for the argument.
@ -708,6 +710,7 @@ qutebrowser release
* Update changelog (remove *(unreleased)*).
* Adjust `__version_info__` in `qutebrowser/__init__.py`.
* Consider updating the completions for `content.headers.user_agent` in `configdata.yml`.
* Commit.
* Create annotated git tag (`git tag -s "v1.$x.$y" -m "Release v1.$x.$y"`).

View File

View File

48
doc/extapi/api.rst Normal file
View File

@ -0,0 +1,48 @@
API modules
===========
cmdutils module
---------------
.. automodule:: qutebrowser.api.cmdutils
:members:
:imported-members:
apitypes module
---------------
.. automodule:: qutebrowser.api.apitypes
:members:
:imported-members:
config module
-------------
.. automodule:: qutebrowser.api.config
:members:
downloads module
----------------
.. automodule:: qutebrowser.api.downloads
:members:
hook module
-----------
.. automodule:: qutebrowser.api.hook
:members:
interceptor module
------------------
.. automodule:: qutebrowser.api.interceptor
:members:
:imported-members:
message module
--------------
.. automodule:: qutebrowser.api.message
:members:
:imported-members:

179
doc/extapi/conf.py Normal file
View File

@ -0,0 +1,179 @@
# -*- coding: utf-8 -*-
#
# Configuration file for the Sphinx documentation builder.
#
# This file does only contain a selection of the most common options. For a
# full list see the documentation:
# http://www.sphinx-doc.org/en/master/config
# -- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))
# -- Project information -----------------------------------------------------
project = 'qutebrowser extensions'
copyright = '2018, Florian Bruhin'
author = 'Florian Bruhin'
# The short X.Y version
version = ''
# The full version, including alpha/beta/rc tags
release = ''
# -- General configuration ---------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#
# needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.napoleon',
]
autodoc_member_order = 'bysource'
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'
# The master toctree document.
master_doc = 'index'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = None
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'alabaster'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#
# html_theme_options = {}
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# Custom sidebar templates, must be a dictionary that maps document names
# to template names.
#
# The default sidebars (for documents that don't match any pattern) are
# defined by theme itself. Builtin themes are using these templates by
# default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
# 'searchbox.html']``.
#
# html_sidebars = {}
# -- Options for HTMLHelp output ---------------------------------------------
# Output file base name for HTML help builder.
htmlhelp_basename = 'qutebrowserextensionsdoc'
# -- Options for LaTeX output ------------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'qutebrowserextensions.tex', 'qutebrowser extensions Documentation',
'Florian Bruhin', 'manual'),
]
# -- Options for manual page output ------------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'qutebrowserextensions', 'qutebrowser extensions Documentation',
[author], 1)
]
# -- Options for Texinfo output ----------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'qutebrowserextensions', 'qutebrowser extensions Documentation',
author, 'qutebrowserextensions', 'One line description of project.',
'Miscellaneous'),
]
# -- Options for Epub output -------------------------------------------------
# Bibliographic Dublin Core info.
epub_title = project
# The unique identifier of the text. This can be a ISBN number
# or the project homepage.
#
# epub_identifier = ''
# A unique identification for the text.
#
# epub_uid = ''
# A list of files that should not be packed into the epub file.
epub_exclude_files = ['search.html']
# -- Extension configuration -------------------------------------------------

22
doc/extapi/index.rst Normal file
View File

@ -0,0 +1,22 @@
.. qutebrowser extensions documentation master file, created by
sphinx-quickstart on Tue Dec 11 18:59:44 2018.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Welcome to qutebrowser extensions's documentation!
==================================================
.. toctree::
:maxdepth: 2
:caption: Contents:
api
tab
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

44
doc/extapi/tab.rst Normal file
View File

@ -0,0 +1,44 @@
Tab API
=======
.. autoclass:: qutebrowser.browser.browsertab.AbstractTab()
:members:
.. autoclass:: qutebrowser.browser.browsertab.AbstractAction()
:members:
.. autoclass:: qutebrowser.browser.browsertab.AbstractPrinting()
:members:
.. autoclass:: qutebrowser.browser.browsertab.AbstractSearch()
:members:
.. autoclass:: qutebrowser.browser.browsertab.AbstractZoom()
:members:
.. autoclass:: qutebrowser.browser.browsertab.AbstractCaret()
:members:
.. autoclass:: qutebrowser.browser.browsertab.AbstractScroller()
:members:
.. autoclass:: qutebrowser.browser.browsertab.AbstractHistory()
:members:
.. autoclass:: qutebrowser.browser.browsertab.AbstractElements()
:members:
.. autoclass:: qutebrowser.browser.browsertab.AbstractAudio()
:members:
Web element API
===============
.. autoclass:: qutebrowser.browser.webelem.AbstractWebElement
:members:
.. autoclass:: qutebrowser.browser.webelem.Error
:members:
.. autoclass:: qutebrowser.browser.webelem.OrphanedError
:members:

View File

@ -211,15 +211,16 @@ Why does J move to the next (right) tab, and K to the previous (left) one?::
What's the difference between insert and passthrough mode?::
They are quite similar, but insert mode has some bindings (like `Ctrl-e` to
open an editor) while passthrough mode only has escape bound. It might also
be useful to rebind escape to something else in passthrough mode only, to be
able to send an escape keypress to the website.
open an editor) while passthrough mode only has shift+escape bound. This is
because shift+escape is unlikely to be a useful binding to be passed to a
webpage. However, any other keys may be assigned to leaving passthrough mode
instead of shift+escape should this be desired.
Why takes it longer to open an URL in qutebrowser than in chromium?::
When opening an URL in an existing instance the normal qutebrowser
Why does it take longer to open a URL in qutebrowser than in chromium?::
When opening a URL in an existing instance, the normal qutebrowser
Python script is started and a few PyQt libraries need to be
loaded until it is detected that there is an instance running
where the URL is then passed to. This takes some time.
to which the URL is then passed. This takes some time.
One workaround is to use this
https://github.com/qutebrowser/qutebrowser/blob/master/scripts/open_url_in_instance.sh[script]
and place it in your $PATH with the name "qutebrowser". This
@ -260,6 +261,12 @@ Note that there are some missing features which you may run into:
. Any greasemonkey API function to do with adding UI elements is not currently
supported. That means context menu extentensions and background pages.
How do I change the `WM_CLASS` used by qutebrowser windows?::
Qt only supports setting `WM_CLASS` globally, which you can do by starting
with `--qt-arg name foo`. Note that all windows are part of the same
qutebrowser instance (unless you use `--temp-basedir` or `--basedir`), so
they all will share the same `WM_CLASS`.
== Troubleshooting
Unable to view flash content.::

View File

@ -1472,6 +1472,7 @@ Yank something to the clipboard or primary selection.
- `title`: The current page's title.
- `domain`: The current scheme, domain, and port number.
- `selection`: The selection under the cursor.
- `markdown`: Yank title and URL in markdown format.
@ -1483,14 +1484,14 @@ Yank something to the clipboard or primary selection.
[[zoom]]
=== zoom
Syntax: +:zoom [*--quiet*] ['zoom']+
Syntax: +:zoom [*--quiet*] ['level']+
Set the zoom level for the current tab.
The zoom can be given as argument or as [count]. If neither is given, the zoom is set to the default zoom. If both are given, use [count].
==== positional arguments
* +'zoom'+: The zoom percentage to set.
* +'level'+: The zoom percentage to set.
==== optional arguments
* +*-q*+, +*--quiet*+: Don't show a zoom level message.

View File

@ -19,10 +19,10 @@ hand, you can simply use those - see
<<autoconfig,"Configuring qutebrowser via the user interface">> for details.
For more advanced configuration, you can write a `config.py` file - see
<<configpy,"Configuring qutebrowser via config.py">>. As soon as a `config.py`
<<configpy,"Configuring qutebrowser via config.py">>. When a `config.py`
exists, the `autoconfig.yml` file **is not read anymore** by default. You need
to <<configpy-autoconfig,load it by hand>> if you want settings done via
`:set`/`:bind` to still persist.
to <<configpy-autoconfig,load it from `config.py`>> if you want settings changed via
`:set`/`:bind` to persist between restarts.
[[autoconfig]]
Configuring qutebrowser via the user interface
@ -229,18 +229,18 @@ Loading `autoconfig.yml`
~~~~~~~~~~~~~~~~~~~~~~~~
All customization done via the UI (`:set`, `:bind` and `:unbind`) is
stored in the `autoconfig.yml` file, which is not loaded automatically as soon
as a `config.py` exists. If you want those settings to be loaded, you'll need to
explicitly load the `autoconfig.yml` file in your `config.py` by doing:
stored in the `autoconfig.yml` file. When a `config.py` file exists, `autoconfig.yml`
is not loaded automatically. To load `autoconfig.yml` automatically, add the
following snippet to `config.py`:
.config.py:
[source,python]
----
config.load_autoconfig()
----
If you do so at the top of your file, your `config.py` settings will take
precedence as they overwrite the settings done in `autoconfig.yml`.
You can configure which file overrides the other by the location of the above code snippet.
Place the snippet at the top to allow `config.py` to override `autoconfig.yml`.
Place the snippet at the bottom for the opposite effect.
Importing other modules
~~~~~~~~~~~~~~~~~~~~~~~
@ -396,6 +396,7 @@ Pre-built colorschemes
- A collection of https://github.com/chriskempson/base16[base16] color-schemes can be found in https://github.com/theova/base16-qutebrowser[base16-qutebrowser] and used with https://github.com/AuditeMarlow/base16-manager[base16-manager].
- Two implementations of the https://github.com/arcticicestudio/nord[Nord] colorscheme for qutebrowser exist: https://github.com/Linuus/nord-qutebrowser[Linuus], https://github.com/KnownAsDon/QuteBrowser-Nord-Theme[KnownAsDon]
- https://github.com/evannagle/qutebrowser-dracula-theme[Dracula]
- https://github.com/jjzmajic/qutewal[Pywal theme]
Avoiding flake8 errors
^^^^^^^^^^^^^^^^^^^^^^
@ -423,6 +424,19 @@ config = config # type: ConfigAPI # noqa: F821 pylint: disable=E0602,C0103
c = c # type: ConfigContainer # noqa: F821 pylint: disable=E0602,C0103
----
emacs-like config
^^^^^^^^^^^^^^^^^
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]
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
`all`.
[[migrating]]
Migrating older configurations
------------------------------

View File

@ -261,6 +261,7 @@
|<<tabs.new_position.stacking,tabs.new_position.stacking>>|Stack related tabs on top of each other when opened consecutively.
|<<tabs.new_position.unrelated,tabs.new_position.unrelated>>|Position of new tabs which are not opened from another tab.
|<<tabs.padding,tabs.padding>>|Padding (in pixels) around text for tabs.
|<<tabs.pinned.frozen,tabs.pinned.frozen>>|Force pinned tabs to stay at fixed URL.
|<<tabs.pinned.shrink,tabs.pinned.shrink>>|Shrink pinned tabs down to their contents.
|<<tabs.position,tabs.position>>|Position of the tab bar.
|<<tabs.select_on_remove,tabs.select_on_remove>>|Which tab to select when the focused tab is removed.
@ -640,10 +641,12 @@ Default:
* +pass:[xO]+: +pass:[set-cmd-text :open -b -r {url:pretty}]+
* +pass:[xo]+: +pass:[set-cmd-text -s :open -b]+
* +pass:[yD]+: +pass:[yank domain -s]+
* +pass:[yM]+: +pass:[yank markdown -s]+
* +pass:[yP]+: +pass:[yank pretty-url -s]+
* +pass:[yT]+: +pass:[yank title -s]+
* +pass:[yY]+: +pass:[yank -s]+
* +pass:[yd]+: +pass:[yank domain]+
* +pass:[ym]+: +pass:[yank markdown]+
* +pass:[yp]+: +pass:[yank pretty-url]+
* +pass:[yt]+: +pass:[yank title]+
* +pass:[yy]+: +pass:[yank]+
@ -1591,11 +1594,11 @@ Type: <<types,String>>
Valid values:
* +all+: Accept all cookies.
* +no-3rdparty+: Accept cookies from the same origin only.
* +no-3rdparty+: Accept cookies from the same origin only. This is known to break some sites, such as GMail.
* +no-unknown-3rdparty+: Accept cookies from the same origin only, unless a cookie is already set for the domain. On QtWebEngine, this is the same as no-3rdparty.
* +never+: Don't accept cookies at all.
Default: +pass:[no-3rdparty]+
Default: +pass:[all]+
On QtWebEngine, this setting requires Qt 5.11 or newer.
@ -1644,7 +1647,7 @@ Type: <<types,Bool>>
Default: +pass:[true]+
This setting is only available with the QtWebKit backend.
On QtWebEngine, this setting requires Qt 5.12 or newer.
[[content.frame_flattening]]
=== content.frame_flattening
@ -2958,7 +2961,7 @@ Default: +pass:[false]+
=== search.ignore_case
When to find text on a page case-insensitively.
Type: <<types,String>>
Type: <<types,IgnoreCase>>
Valid values:
@ -3305,6 +3308,14 @@ Default:
- +pass:[right]+: +pass:[5]+
- +pass:[top]+: +pass:[0]+
[[tabs.pinned.frozen]]
=== tabs.pinned.frozen
Force pinned tabs to stay at fixed URL.
Type: <<types,Bool>>
Default: +pass:[true]+
[[tabs.pinned.shrink]]
=== tabs.pinned.shrink
Shrink pinned tabs down to their contents.
@ -3622,6 +3633,7 @@ Lists with duplicate flags are invalid. Each item is checked against the valid v
|FontFamily|A Qt font family.
|FormatString|A string with placeholders.
|FuzzyUrl|A URL which gets interpreted as search if needed.
|IgnoreCase|Whether to search case insensitively.
|Int|Base class for an integer setting.
|Key|A name of a key.
|List|A list of values.

View File

@ -102,18 +102,12 @@ $ python3 scripts/asciidoc2html.py
On Fedora
---------
NOTE: Fedora's packages used to be outdated for a long time, but are
now (November 2017) maintained and up-to-date again.
qutebrowser is available in the official repositories:
-----
# dnf install qutebrowser
-----
However, note that Fedora 25/26 won't be updated to qutebrowser v1.0, so you
might want to <<tox,install qutebrowser via tox>> instead there.
Additional hints
~~~~~~~~~~~~~~~~

View File

@ -17,8 +17,8 @@ doc/qutebrowser.1.html:
install: doc/qutebrowser.1.html
$(PYTHON) setup.py install --prefix="$(PREFIX)" --optimize=1 $(SETUPTOOLSOPTS)
install -Dm644 misc/qutebrowser.appdata.xml \
"$(DESTDIR)$(DATADIR)/metainfo/qutebrowser.appdata.xml"
install -Dm644 misc/org.qutebrowser.qutebrowser.appdata.xml \
"$(DESTDIR)$(DATADIR)/metainfo/org.qutebrowser.qutebrowser.appdata.xml"
install -Dm644 doc/qutebrowser.1 \
"$(DESTDIR)$(MANDIR)/man1/qutebrowser.1"
install -Dm644 misc/qutebrowser.desktop \

View File

@ -1,7 +1,45 @@
[Desktop Entry]
Name=qutebrowser
GenericName=Web Browser
GenericName[ar]=
GenericName[bg]=Уеб браузър
GenericName[ca]=Navegador web
GenericName[cs]=WWW prohlížeč
GenericName[da]=Browser
GenericName[de]=Web-Browser
GenericName[el]=Περιηγητής ιστού
GenericName[en_GB]=Web Browser
GenericName[es]=Navegador web
GenericName[et]=Veebibrauser
GenericName[fi]=WWW-selain
GenericName[fr]=Navigateur Web
GenericName[gu]=
GenericName[he]=דפדפן אינטרנט
GenericName[hi]=
GenericName[hu]=Webböngésző
GenericName[it]=Browser Web
GenericName[ja]=
GenericName[kn]=
GenericName[ko]=
GenericName[lt]=Žiniatinklio naršyklė
GenericName[lv]=Tīmekļa pārlūks
GenericName[ml]= <200d>
GenericName[mr]=
GenericName[nb]=Nettleser
GenericName[nl]=Webbrowser
GenericName[pl]=Przeglądarka WWW
GenericName[pt]=Navegador Web
GenericName[pt_BR]=Navegador da Internet
GenericName[ro]=Navigator de Internet
GenericName[ru]=Веб-браузер
GenericName[sl]=Spletni brskalnik
GenericName[sv]=Webbläsare
GenericName[ta]= ி
GenericName[th]=
GenericName[tr]=Web Tarayıcı
GenericName[uk]=Навігатор Тенет
Comment=A keyboard-driven, vim-like browser based on PyQt5
Comment[it]= Un browser web vim-like utilizzabile da tastiera basato su PyQt5
Icon=qutebrowser
Type=Application
Categories=Network;WebBrowser;
@ -10,3 +48,128 @@ Terminal=false
StartupNotify=false
MimeType=text/html;text/xml;application/xhtml+xml;application/xml;application/rdf+xml;image/gif;image/jpeg;image/png;x-scheme-handler/http;x-scheme-handler/https;x-scheme-handler/qute;
Keywords=Browser
[Desktop Action new-window]
Name=New Window
Name[am]=
Name[ar]=
Name[bg]=Нов прозорец
Name[bn]=
Name[ca]=Finestra nova
Name[cs]=Nové okno
Name[da]=Nyt vindue
Name[de]=Neues Fenster
Name[el]=Νέο Παράθυρο
Name[en_GB]=New Window
Name[es]=Nueva ventana
Name[et]=Uus aken
Name[fa]=پ ی
Name[fi]=Uusi ikkuna
Name[fil]=New Window
Name[fr]=Nouvelle fenêtre
Name[gu]= િ
Name[hi]= ि
Name[hr]=Novi prozor
Name[hu]=Új ablak
Name[id]=Jendela Baru
Name[it]=Nuova finestra
Name[iw]=חלון חדש
Name[ja]=
Name[kn]= ಿ
Name[ko]=
Name[lt]=Naujas langas
Name[lv]=Jauns logs
Name[ml]=ി ി<200d>
Name[mr]= ि
Name[nl]=Nieuw venster
Name[no]=Nytt vindu
Name[pl]=Nowe okno
Name[pt]=Nova janela
Name[pt_BR]=Nova janela
Name[ro]=Fereastră nouă
Name[ru]=Новое окно
Name[sk]=Nové okno
Name[sl]=Novo okno
Name[sr]=Нови прозор
Name[sv]=Nytt fönster
Name[sw]=Dirisha Jipya
Name[ta]=ி
Name[te]= ి
Name[th]=
Name[tr]=Yeni Pencere
Name[uk]=Нове вікно
Name[vi]=Ca s Mi
Exec=qutebrowser
[Desktop Action preferences]
Name=Preferences
Name[an]=Preferencias
Name[ar]=
Name[as]=
Name[be]=Настройкі
Name[bg]=Настройки
Name[bn_IN]=
Name[bs]=Postavke
Name[ca]=Preferències
Name[ca@valencia]=Preferències
Name[cs]=Předvolby
Name[da]=Indstillinger
Name[de]=Einstellungen
Name[el]=Προτιμήσεις
Name[en_GB]=Preferences
Name[eo]=Agordoj
Name[es]=Preferencias
Name[et]=Eelistused
Name[eu]=Hobespenak
Name[fa]=ی
Name[fi]=Asetukset
Name[fr]=Préférences
Name[fur]=Preferencis
Name[ga]=Sainroghanna
Name[gd]=Roghainnean
Name[gl]=Preferencias
Name[gu]=
Name[he]=העדפות
Name[hi]=
Name[hr]=Osobitosti
Name[hu]=Beállítások
Name[id]=Preferensi
Name[is]=Kjörstillingar
Name[it]=Preferenze
Name[ja]=
Name[kk]=Баптаулар
Name[km]=
Name[kn]=
Name[ko]=
Name[lt]=Nuostatos
Name[lv]=Iestatījumi
Name[ml]=<200d><200d>
Name[mr]=
Name[nb]=Brukervalg
Name[ne]=ि
Name[nl]=Voorkeuren
Name[oc]=Preferéncias
Name[or]=
Name[pa]=
Name[pl]=Preferencje
Name[pt]=Preferências
Name[pt_BR]=Preferências
Name[ro]=Preferințe
Name[ru]=Параметры
Name[sk]=Nastavenia
Name[sl]=Možnosti
Name[sr]=Поставке
Name[sr@latin]=Postavke
Name[sv]=Inställningar
Name[ta]=ி
Name[te]=
Name[tg]=Хусусиятҳо
Name[th]=
Name[tr]=Tercihler
Name[ug]=
Name[uk]=Параметри
Name[vi]=Tùy thích
Name[zh_CN]=
Name[zh_HK]=
Name[zh_TW]=
Exec=qutebrowser "qute://settings"

View File

@ -6,6 +6,8 @@ import os
sys.path.insert(0, os.getcwd())
from scripts import setupcommon
from qutebrowser.extensions import loader
block_cipher = None
@ -27,6 +29,13 @@ def get_data_files():
return data_files
def get_hidden_imports():
imports = ['PyQt5.QtOpenGL', 'PyQt5._QOpenGLFunctions_2_0']
for info in loader.walk_components():
imports.append(info.name)
return imports
setupcommon.write_git_file()
@ -42,7 +51,7 @@ a = Analysis(['../qutebrowser/__main__.py'],
pathex=['misc'],
binaries=None,
datas=get_data_files(),
hiddenimports=['PyQt5.QtOpenGL', 'PyQt5._QOpenGLFunctions_2_0'],
hiddenimports=get_hidden_imports(),
hookspath=[],
runtime_hooks=[],
excludes=['tkinter'],

View File

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

View File

@ -1,27 +1,25 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
attrs==18.2.0
flake8==3.5.0
entrypoints==0.3
flake8==3.7.5
flake8-bugbear==18.8.0
flake8-builtins==1.4.1 # rq.filter: != 1.4.0
flake8-comprehensions==1.4.1
flake8-copyright==0.2.0
flake8-builtins==1.4.1
flake8-comprehensions==2.0.0
flake8-copyright==0.2.2
flake8-debugger==3.1.0
flake8-deprecated==1.3
flake8-docstrings==1.3.0
flake8-future-import==0.4.5
flake8-mock==0.3
flake8-per-file-ignores==0.6
flake8-polyfill==1.0.2
flake8-string-format==0.2.3
flake8-tidy-imports==1.1.0
flake8-tidy-imports==2.0.0
flake8-tuple==0.2.13
mccabe==0.6.1
pathmatch==0.2.1
pep8-naming==0.7.0
pycodestyle==2.3.1 # rq.filter: < 2.4.0
pydocstyle==2.1.1
pyflakes==2.0.0
six==1.11.0
pep8-naming==0.8.2
pycodestyle==2.5.0
pydocstyle==3.0.0
pyflakes==2.1.0
six==1.12.0
snowballstemmer==1.2.1
typing==3.6.6

View File

@ -1,6 +1,6 @@
flake8
flake8-bugbear
flake8-builtins!=1.4.0
flake8-builtins
flake8-comprehensions
flake8-copyright
flake8-debugger
@ -8,16 +8,9 @@ flake8-deprecated
flake8-docstrings
flake8-future-import
flake8-mock
flake8-per-file-ignores
flake8-string-format
flake8-tidy-imports
flake8-tuple
pep8-naming
pydocstyle
pyflakes
# https://github.com/PyCQA/pycodestyle/issues/741
#@ filter: pycodestyle < 2.4.0
# https://github.com/gforcada/flake8-builtins/issues/36
#@ filter: flake8-builtins != 1.4.0

View File

@ -0,0 +1,8 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
mypy==0.670
mypy-extensions==0.4.1
# PyQt5==5.11.3
# PyQt5-sip==4.19.14
-e git+https://github.com/qutebrowser/PyQt5-stubs.git@wip#egg=PyQt5_stubs
typed-ast==1.3.1

View File

@ -0,0 +1,6 @@
mypy
-e git+https://github.com/qutebrowser/PyQt5-stubs.git@wip#egg=PyQt5-stubs
# remove @commit-id for scm installs
#@ replace: @.*# @wip#
#@ ignore: PyQt5, PyQt5-sip

View File

@ -0,0 +1,6 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
colorama==0.4.1
cssutils==1.0.2
hunter==2.2.1
Pympler==0.6

View File

@ -0,0 +1,3 @@
hunter
cssutils
pympler

View File

@ -1,8 +1,8 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
appdirs==1.4.3
packaging==18.0
pyparsing==2.2.2
setuptools==40.4.3
six==1.11.0
wheel==0.32.1
packaging==19.0
pyparsing==2.3.1
setuptools==40.8.0
six==1.12.0
wheel==0.32.3

View File

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

View File

@ -1,23 +1,23 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
asn1crypto==0.24.0
astroid==2.0.4
certifi==2018.8.24
astroid==2.1.0
certifi==2018.11.29
cffi==1.11.5
chardet==3.0.4
cryptography==2.3.1
github3.py==1.2.0
idna==2.7
cryptography==2.5
github3.py==1.3.0
idna==2.8
isort==4.3.4
jwcrypto==0.5.0
jwcrypto==0.6.0
lazy-object-proxy==1.3.1
mccabe==0.6.1
pycparser==2.19
pylint==2.1.1
python-dateutil==2.7.3
pylint==2.2.2
python-dateutil==2.8.0
./scripts/dev/pylint_checkers
requests==2.19.1
six==1.11.0
requests==2.21.0
six==1.12.0
uritemplate==3.0.0
urllib3==1.23
wrapt==1.10.11
urllib3==1.24.1
wrapt==1.11.1

View File

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

View File

@ -1 +1,2 @@
PyQt5
PyQt5
PyQtWebEngine

View File

@ -0,0 +1,21 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
alabaster==0.7.12
Babel==2.6.0
certifi==2018.11.29
chardet==3.0.4
docutils==0.14
idna==2.8
imagesize==1.1.0
Jinja2==2.10
MarkupSafe==1.1.0
packaging==19.0
Pygments==2.3.1
pyparsing==2.3.1
pytz==2018.9
requests==2.21.0
six==1.12.0
snowballstemmer==1.2.1
Sphinx==1.8.4
sphinxcontrib-websupport==1.1.0
urllib3==1.24.1

View File

@ -0,0 +1 @@
sphinx

View File

@ -1,42 +1,42 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
atomicwrites==1.2.1
atomicwrites==1.3.0
attrs==18.2.0
backports.functools-lru-cache==1.5
beautifulsoup4==4.6.3
cheroot==6.5.2
click==7.0
# colorama==0.3.9
coverage==4.5.1
EasyProcess==0.2.3
fields==5.0.0
beautifulsoup4==4.7.1
cheroot==6.5.4
Click==7.0
# colorama==0.4.1
coverage==4.5.2
EasyProcess==0.2.5
Flask==1.0.2
glob2==0.6
hunter==2.0.2
hypothesis==3.74.3
itsdangerous==0.24
hunter==2.2.1
hypothesis==4.5.6
itsdangerous==1.1.0
# Jinja2==2.10
Mako==1.0.7
# MarkupSafe==1.0
more-itertools==4.3.0
parse==1.9.0
# MarkupSafe==1.1.0
more-itertools==5.0.0
parse==1.11.1
parse-type==0.4.2
pluggy==0.7.1
py==1.6.0
pluggy==0.8.1
py==1.7.0
py-cpuinfo==4.0.0
pytest==3.6.4 # rq.filter: <3.7
pytest-bdd==2.21.0
pytest-benchmark==3.1.1
pytest-cov==2.6.0
pytest==4.2.0
pytest-bdd==3.0.1
pytest-benchmark==3.2.2
pytest-cov==2.6.1
pytest-faulthandler==1.5.0
pytest-instafail==0.4.0
pytest-mock==1.10.0
pytest-qt==3.2.1
pytest-mock==1.10.1
pytest-qt==3.2.2
pytest-repeat==0.7.0
pytest-rerunfailures==4.2
pytest-rerunfailures==6.0
pytest-travis-fold==1.3.0
pytest-xvfb==1.1.0
pytest-xvfb==1.2.0
PyVirtualDisplay==0.2.1
six==1.11.0
vulture==0.29
six==1.12.0
soupsieve==1.7.3
vulture==1.0
Werkzeug==0.14.1

View File

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

View File

@ -1,8 +1,9 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
pluggy==0.7.1
py==1.6.0
six==1.11.0
filelock==3.0.10
pluggy==0.8.1
py==1.7.0
six==1.12.0
toml==0.10.0
tox==3.5.1
virtualenv==16.0.0
tox==3.7.0
virtualenv==16.4.0

View File

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

View File

@ -53,9 +53,10 @@ The following userscripts can be found on their own repositories.
- [qtb.us](https://github.com/Chinggis6/qtb.us): small pack of userscripts.
- [pinboard.zsh](https://github.com/dmix/pinboard.zsh): Add URL to your
[Pinboard][] bookmark manager.
- [qute-capture](https://github.com/alcah/qute-capture): Capture links with
Emacs's org-mode to a read-later file.
[Zotero]: https://www.zotero.org/
[Pocket]: https://getpocket.com/
[Instapaper]: https://www.instapaper.com/
[Pinboard]: https://pinboard.in/

View File

@ -1,4 +1,4 @@
#!/bin/sh
#!/bin/bash
set -euo pipefail
#
# Behavior:

View File

@ -64,6 +64,7 @@ die() {
javascript_escape() {
# print the first argument in an escaped way, such that it can safely
# be used within javascripts double quotes
# shellcheck disable=SC2001
sed "s,[\\\\'\"],\\\\&,g" <<< "$1"
}
@ -111,6 +112,7 @@ simplify_url() {
# are found:
no_entries_found() {
while [ 0 -eq "${#files[@]}" ] && [ -n "$simple_url" ]; do
# shellcheck disable=SC2001
shorter_simple_url=$(sed 's,^[^.]*\.,,' <<< "$simple_url")
if [ "$shorter_simple_url" = "$simple_url" ] ; then
# if no dot, then even remove the top level domain
@ -269,7 +271,7 @@ pass_backend() {
break
fi
fi
done < <($GPG "${GPG_OPTS[@]}" -d "$path" )
done < <($GPG "${GPG_OPTS[@]}" -d "$path" | awk 1 )
}
}
# =======================================================

View File

@ -97,13 +97,19 @@ def qute_command(command):
def find_pass_candidates(domain, password_store_path):
candidates = []
for path, directories, file_names in os.walk(password_store_path, followlinks=True):
if directories or domain not in path.split(os.path.sep):
secrets = fnmatch.filter(file_names, '*.gpg')
if not secrets:
continue
# Strip password store path prefix to get the relative pass path
pass_path = path[len(password_store_path) + 1:]
secrets = fnmatch.filter(file_names, '*.gpg')
candidates.extend(os.path.join(pass_path, os.path.splitext(secret)[0]) for secret in secrets)
split_path = pass_path.split(os.path.sep)
for secret in secrets:
secret_base = os.path.splitext(secret)[0]
if domain not in (split_path + [secret_base]):
continue
candidates.append(os.path.join(pass_path, secret_base))
return candidates

View File

@ -37,7 +37,7 @@ get_selection() {
# https://github.com/halfwit/dotfiles/blob/master/.config/dmenu/font
[[ -s $confdir/dmenu/font ]] && read -r font < "$confdir"/dmenu/font
[[ $font ]] && opts+=(-fn "$font")
[[ -n $font ]] && opts+=(-fn "$font")
# shellcheck source=/dev/null
[[ -s $optsfile ]] && source "$optsfile"
@ -46,7 +46,7 @@ url=$(get_selection)
url=${url/*http/http}
# If no selection is made, exit (escape pressed, e.g.)
[[ ! $url ]] && exit 0
[[ -z $url ]] && exit 0
case $1 in
open) printf '%s' "open $url" >> "$QUTE_FIFO" || qutebrowser "$url" ;;

87
mypy.ini Normal file
View File

@ -0,0 +1,87 @@
[mypy]
# We also need to support 3.5, but if we'd chose that here, we'd need to deal
# with conditional imports (like secrets.py).
python_version = 3.6
# --strict
warn_redundant_casts = True
warn_unused_ignores = True
disallow_subclassing_any = True
disallow_untyped_decorators = True
## https://github.com/python/mypy/issues/5957
# warn_unused_configs = True
# disallow_untyped_calls = True
# disallow_untyped_defs = True
## https://github.com/python/mypy/issues/5954
# disallow_incomplete_defs = True
# check_untyped_defs = True
# no_implicit_optional = True
# warn_return_any = True
[mypy-colorama]
# https://github.com/tartley/colorama/issues/206
ignore_missing_imports = True
[mypy-hunter]
# https://github.com/ionelmc/python-hunter/issues/43
ignore_missing_imports = True
[mypy-pygments.*]
# https://bitbucket.org/birkenfeld/pygments-main/issues/1485/type-hints
ignore_missing_imports = True
[mypy-cssutils]
# Pretty much inactive currently
ignore_missing_imports = True
[mypy-pypeg2]
# Pretty much inactive currently
ignore_missing_imports = True
[mypy-bdb]
# stdlib, missing in typeshed
ignore_missing_imports = True
[mypy-qutebrowser.browser.webkit.rfc6266]
# subclasses dynamic PyPEG2 classes
disallow_subclassing_any = False
[mypy-qutebrowser.browser.browsertab]
disallow_untyped_defs = True
disallow_incomplete_defs = True
[mypy-qutebrowser.misc.objects]
disallow_untyped_defs = True
disallow_incomplete_defs = True
[mypy-qutebrowser.commands.cmdutils]
disallow_untyped_defs = True
disallow_incomplete_defs = True
[mypy-qutebrowser.config.*]
disallow_untyped_defs = True
disallow_incomplete_defs = True
[mypy-qutebrowser.api.*]
disallow_untyped_defs = True
disallow_incomplete_defs = True
[mypy-qutebrowser.components.*]
disallow_untyped_defs = True
disallow_incomplete_defs = True
[mypy-qutebrowser.extensions.*]
disallow_untyped_defs = True
disallow_incomplete_defs = True
[mypy-qutebrowser.browser.webelem]
disallow_untyped_defs = True
disallow_incomplete_defs = True
[mypy-qutebrowser.browser.webkit.webkitelem]
disallow_untyped_defs = True
disallow_incomplete_defs = True
[mypy-qutebrowser.browser.webengine.webengineelem]
disallow_untyped_defs = True
disallow_incomplete_defs = True

View File

@ -1,6 +1,6 @@
[pytest]
log_level = NOTSET
addopts = --strict -rfEw --faulthandler-timeout=90 --instafail --pythonwarnings error --benchmark-columns=Min,Max,Median
addopts = --strict -rfEw --faulthandler-timeout=90 --instafail --benchmark-columns=Min,Max,Median
testpaths = tests
markers =
gui: Tests using the GUI (e.g. spawning widgets)
@ -64,7 +64,10 @@ qt_log_ignore =
^QSettings::value: Empty key passed
^Icon theme ".*" not found
^Error receiving trust for a CA certificate
^QBackingStore::endPaint\(\) called with active painter on backingstore paint device
^QPaintDevice: Cannot destroy paint device that is being painted
xfail_strict = true
filterwarnings =
# This happens in many qutebrowser dependencies...
ignore:Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated, and in 3.8 it will stop working:DeprecationWarning
error
# WORKAROUND for https://github.com/pytest-dev/pytest-bdd/pull/288
ignore:the `pytest\.config` global is deprecated\. Please use `request.config` or `pytest_configure` \(if you're a pytest plugin\) instead\.

View File

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

View File

@ -0,0 +1,26 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2018 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/>.
"""API for extensions.
This API currently isn't exposed to third-party extensions yet, but will be in
the future. Thus, care must be taken when adding new APIs here.
Code in qutebrowser.components only uses this API.
"""

View File

@ -0,0 +1,27 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2018 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/>.
"""A single tab."""
# pylint: disable=unused-import
from qutebrowser.browser.browsertab import WebTabError, AbstractTab as Tab
from qutebrowser.browser.webelem import (Error as WebElemError,
AbstractWebElement as WebElement)
from qutebrowser.utils.usertypes import ClickTarget, JsWorld
from qutebrowser.extensions.loader import InitContext

219
qutebrowser/api/cmdutils.py Normal file
View File

@ -0,0 +1,219 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-2018 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/>.
"""qutebrowser has the concept of functions, exposed to the user as commands.
Creating a new command is straightforward::
from qutebrowser.api import cmdutils
@cmdutils.register(...)
def foo():
...
The commands arguments are automatically deduced by inspecting your function.
The types of the function arguments are inferred based on their default values,
e.g., an argument `foo=True` will be converted to a flag `-f`/`--foo` in
qutebrowser's commandline.
The type can be overridden using Python's function annotations::
@cmdutils.register(...)
def foo(bar: int, baz=True):
...
Possible values:
- A callable (``int``, ``float``, etc.): Gets called to validate/convert the
value.
- A python enum type: All members of the enum are possible values.
- A ``typing.Union`` of multiple types above: Any of these types are valid
values, e.g., ``typing.Union[str, int]``.
"""
import inspect
import typing
from qutebrowser.utils import qtutils
from qutebrowser.commands import command, cmdexc
# pylint: disable=unused-import
from qutebrowser.utils.usertypes import KeyMode, CommandValue as Value
class CommandError(cmdexc.Error):
"""Raised when a command encounters an error while running.
If your command handler encounters an error and cannot continue, raise this
exception with an appropriate error message::
raise cmdexc.CommandError("Message")
The message will then be shown in the qutebrowser status bar.
.. note::
You should only raise this exception while a command handler is run.
Raising it at another point causes qutebrowser to crash due to an
unhandled exception.
"""
def check_overflow(arg: int, ctype: str) -> None:
"""Check if the given argument is in bounds for the given type.
Args:
arg: The argument to check.
ctype: The C++/Qt type to check as a string ('int'/'int64').
"""
try:
qtutils.check_overflow(arg, ctype)
except OverflowError:
raise CommandError("Numeric argument is too large for internal {} "
"representation.".format(ctype))
def check_exclusive(flags: typing.Iterable[bool],
names: typing.Iterable[str]) -> None:
"""Check if only one flag is set with exclusive flags.
Raise a CommandError if not.
Args:
flags: The flag values to check.
names: A list of names (corresponding to the flags argument).
"""
if sum(1 for e in flags if e) > 1:
argstr = '/'.join('-' + e for e in names)
raise CommandError("Only one of {} can be given!".format(argstr))
class register: # noqa: N801,N806 pylint: disable=invalid-name
"""Decorator to register a new command handler."""
def __init__(self, *,
instance: str = None,
name: str = None,
**kwargs: typing.Any) -> None:
"""Save decorator arguments.
Gets called on parse-time with the decorator arguments.
Args:
See class attributes.
"""
# The object from the object registry to be used as "self".
self._instance = instance
# The name (as string) or names (as list) of the command.
self._name = name
# The arguments to pass to Command.
self._kwargs = kwargs
def __call__(self, func: typing.Callable) -> typing.Callable:
"""Register the command before running the function.
Gets called when a function should be decorated.
Doesn't actually decorate anything, but creates a Command object and
registers it in the global commands dict.
Args:
func: The function to be decorated.
Return:
The original function (unmodified).
"""
if self._name is None:
name = func.__name__.lower().replace('_', '-')
else:
assert isinstance(self._name, str), self._name
name = self._name
cmd = command.Command(name=name, instance=self._instance,
handler=func, **self._kwargs)
cmd.register()
return func
class argument: # noqa: N801,N806 pylint: disable=invalid-name
"""Decorator to customize an argument.
You can customize how an argument is handled using the
``@cmdutils.argument`` decorator *after* ``@cmdutils.register``. This can,
for example, be used to customize the flag an argument should get::
@cmdutils.register(...)
@cmdutils.argument('bar', flag='c')
def foo(bar):
...
For a ``str`` argument, you can restrict the allowed strings using
``choices``::
@cmdutils.register(...)
@cmdutils.argument('bar', choices=['val1', 'val2'])
def foo(bar: str):
...
For ``typing.Union`` types, the given ``choices`` are only checked if other
types (like ``int``) don't match.
The following arguments are supported for ``@cmdutils.argument``:
- ``flag``: Customize the short flag (``-x``) the argument will get.
- ``value``: Tell qutebrowser to fill the argument with special values:
* ``value=cmdutils.Value.count``: The ``count`` given by the user to the
command.
* ``value=cmdutils.Value.win_id``: The window ID of the current window.
* ``value=cmdutils.Value.cur_tab``: The tab object which is currently
focused.
- ``completion``: A completion function to use when completing arguments
for the given command.
- ``choices``: The allowed string choices for the argument.
The name of an argument will always be the parameter name, with any
trailing underscores stripped and underscores replaced by dashes.
"""
def __init__(self, argname: str, **kwargs: typing.Any) -> None:
self._argname = argname # The name of the argument to handle.
self._kwargs = kwargs # Valid ArgInfo members.
def __call__(self, func: typing.Callable) -> typing.Callable:
funcname = func.__name__
if self._argname not in inspect.signature(func).parameters:
raise ValueError("{} has no argument {}!".format(funcname,
self._argname))
if not hasattr(func, 'qute_args'):
func.qute_args = {} # type: ignore
elif func.qute_args is None: # type: ignore
raise ValueError("@cmdutils.argument got called above (after) "
"@cmdutils.register for {}!".format(funcname))
arginfo = command.ArgInfo(**self._kwargs)
func.qute_args[self._argname] = arginfo # type: ignore
return func

43
qutebrowser/api/config.py Normal file
View File

@ -0,0 +1,43 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2018 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/>.
"""Access to the qutebrowser configuration."""
import typing
from PyQt5.QtCore import QUrl
from qutebrowser.config import config
#: Simplified access to config values using attribute acccess.
#: For example, to access the ``content.javascript.enabled`` setting,
#: you can do::
#:
#: if config.val.content.javascript.enabled:
#: ...
#:
#: This also supports setting configuration values::
#:
#: config.val.content.javascript.enabled = False
val = typing.cast('config.ConfigContainer', None)
def get(name: str, url: QUrl = None) -> typing.Any:
"""Get a value from the config based on a string name."""
return config.instance.get(name, url)

View File

@ -0,0 +1,75 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2018 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/>.
"""APIs related to downloading files."""
import io
from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, QUrl
from qutebrowser.browser import downloads, qtnetworkdownloads
from qutebrowser.utils import objreg
class TempDownload(QObject):
"""A download of some data into a file object."""
finished = pyqtSignal()
def __init__(self, item: qtnetworkdownloads.DownloadItem) -> None:
super().__init__()
self._item = item
self._item.finished.connect(self._on_download_finished)
self.successful = False
self.fileobj = item.fileobj
@pyqtSlot()
def _on_download_finished(self) -> None:
self.successful = self._item.successful
self.finished.emit()
def download_temp(url: QUrl) -> TempDownload:
"""Download the given URL into a file object.
The download is not saved to disk.
Returns a ``TempDownload`` object, which triggers a ``finished`` signal
when the download has finished::
dl = downloads.download_temp(QUrl("https://www.example.com/"))
dl.finished.connect(functools.partial(on_download_finished, dl))
After the download has finished, its ``successful`` attribute can be
checked to make sure it finished successfully. If so, its contents can be
read by accessing the ``fileobj`` attribute::
def on_download_finished(download: downloads.TempDownload) -> None:
if download.successful:
print(download.fileobj.read())
download.fileobj.close()
"""
fobj = io.BytesIO()
fobj.name = 'temporary: ' + url.host()
target = downloads.FileObjDownloadTarget(fobj)
download_manager = objreg.get('qtnetwork-download-manager')
return download_manager.get(url, target=target, auto_remove=True)

92
qutebrowser/api/hook.py Normal file
View File

@ -0,0 +1,92 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2018 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/>.
# pylint: disable=invalid-name
"""Hooks for extensions."""
import importlib
import typing
from qutebrowser.extensions import loader
def _add_module_info(func: typing.Callable) -> loader.ModuleInfo:
"""Add module info to the given function."""
module = importlib.import_module(func.__module__)
return loader.add_module_info(module)
class init:
"""Decorator to mark a function to run when initializing.
The decorated function gets called with a
:class:`qutebrowser.api.apitypes.InitContext` as argument.
Example::
@hook.init()
def init(_context):
message.info("Extension initialized.")
"""
def __call__(self, func: typing.Callable) -> typing.Callable:
info = _add_module_info(func)
if info.init_hook is not None:
raise ValueError("init hook is already registered!")
info.init_hook = func
return func
class config_changed:
"""Decorator to get notified about changed configs.
By default, the decorated function is called when any change in the config
occurs::
@hook.config_changed()
def on_config_changed():
...
When an option name is passed, it's only called when the given option was
changed::
@hook.config_changed('content.javascript.enabled')
def on_config_changed():
...
Alternatively, a part of an option name can be specified. In the following
snippet, ``on_config_changed`` gets called when either
``bindings.commands`` or ``bindings.key_mappings`` have changed::
@hook.config_changed('bindings')
def on_config_changed():
...
"""
def __init__(self, option_filter: str = None) -> None:
self._filter = option_filter
def __call__(self, func: typing.Callable) -> typing.Callable:
info = _add_module_info(func)
info.config_changed_hooks.append((self._filter, func))
return func

View File

@ -0,0 +1,43 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2018 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/>.
"""APIs related to intercepting/blocking requests."""
from qutebrowser.extensions import interceptors
# pylint: disable=unused-import
from qutebrowser.extensions.interceptors import Request
#: Type annotation for an interceptor function.
InterceptorType = interceptors.InterceptorType
def register(interceptor: InterceptorType) -> None:
"""Register a request interceptor.
Whenever a request happens, the interceptor gets called with a
:class:`Request` object.
Example::
def intercept(request: interceptor.Request) -> None:
if request.request_url.host() == 'badhost.example.com':
request.block()
"""
interceptors.register(interceptor)

View File

@ -0,0 +1,23 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2018 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/>.
"""Utilities to display messages above the status bar."""
# pylint: disable=unused-import
from qutebrowser.utils.message import error, warning, info

View File

@ -60,13 +60,15 @@ except ImportError:
import qutebrowser
import qutebrowser.resources
from qutebrowser.completion.models import miscmodels
from qutebrowser.commands import cmdutils, runners, cmdexc
from qutebrowser.commands import runners
from qutebrowser.api import cmdutils
from qutebrowser.config import config, websettings, configfiles, configinit
from qutebrowser.browser import (urlmarks, adblock, history, browsertab,
from qutebrowser.browser import (urlmarks, history, browsertab,
qtnetworkdownloads, downloads, greasemonkey)
from qutebrowser.browser.network import proxy
from qutebrowser.browser.webkit import cookies, cache
from qutebrowser.browser.webkit.network import networkmanager
from qutebrowser.extensions import loader
from qutebrowser.keyinput import macros
from qutebrowser.mainwindow import mainwindow, prompt
from qutebrowser.misc import (readline, ipc, savemanager, sessions,
@ -81,7 +83,7 @@ from qutebrowser.misc import utilcmds
# pylint: enable=unused-import
qApp = None
q_app = None
def run(args):
@ -99,25 +101,25 @@ def run(args):
log.init.debug("Initializing config...")
configinit.early_init(args)
global qApp
qApp = Application(args)
qApp.setOrganizationName("qutebrowser")
qApp.setApplicationName("qutebrowser")
qApp.setDesktopFileName("qutebrowser")
qApp.setApplicationVersion(qutebrowser.__version__)
qApp.lastWindowClosed.connect(quitter.on_last_window_closed)
global q_app
q_app = Application(args)
q_app.setOrganizationName("qutebrowser")
q_app.setApplicationName("qutebrowser")
q_app.setDesktopFileName("qutebrowser")
q_app.setApplicationVersion(qutebrowser.__version__)
q_app.lastWindowClosed.connect(quitter.on_last_window_closed)
if args.version:
print(version.version())
sys.exit(usertypes.Exit.ok)
crash_handler = crashsignal.CrashHandler(
app=qApp, quitter=quitter, args=args, parent=qApp)
app=q_app, quitter=quitter, args=args, parent=q_app)
crash_handler.activate()
objreg.register('crash-handler', crash_handler)
signal_handler = crashsignal.SignalHandler(app=qApp, quitter=quitter,
parent=qApp)
signal_handler = crashsignal.SignalHandler(app=q_app, quitter=quitter,
parent=q_app)
signal_handler.activate()
objreg.register('signal-handler', signal_handler)
@ -149,7 +151,7 @@ def qt_mainloop():
WARNING: misc/crashdialog.py checks the stacktrace for this function
name, so if this is changed, it should be changed there as well!
"""
return qApp.exec_()
return q_app.exec_()
def init(args, crash_handler):
@ -160,9 +162,11 @@ def init(args, crash_handler):
crash_handler: The CrashHandler instance.
"""
log.init.debug("Starting init...")
qApp.setQuitOnLastWindowClosed(False)
q_app.setQuitOnLastWindowClosed(False)
_init_icon()
loader.init()
loader.load_components()
try:
_init_modules(args, crash_handler)
except (OSError, UnicodeDecodeError, browsertab.WebTabError) as e:
@ -171,12 +175,12 @@ def init(args, crash_handler):
sys.exit(usertypes.Exit.err_init)
log.init.debug("Initializing eventfilter...")
event_filter = EventFilter(qApp)
qApp.installEventFilter(event_filter)
event_filter = EventFilter(q_app)
q_app.installEventFilter(event_filter)
objreg.register('event-filter', event_filter)
log.init.debug("Connecting signals...")
qApp.focusChanged.connect(on_focus_changed)
q_app.focusChanged.connect(on_focus_changed)
_process_args(args)
@ -193,7 +197,7 @@ def _init_icon():
icon = QIcon()
fallback_icon = QIcon()
for size in [16, 24, 32, 48, 64, 96, 128, 256, 512]:
filename = ':/icons/qutebrowser-{}x{}.png'.format(size, size)
filename = ':/icons/qutebrowser-{size}x{size}.png'.format(size=size)
pixmap = QPixmap(filename)
if pixmap.isNull():
log.init.warning("Failed to load {}".format(filename))
@ -203,7 +207,7 @@ def _init_icon():
if icon.isNull():
log.init.warning("Failed to load icon")
else:
qApp.setWindowIcon(icon)
q_app.setWindowIcon(icon)
def _process_args(args):
@ -216,7 +220,7 @@ def _process_args(args):
window = mainwindow.MainWindow(private=None)
if not args.nowindow:
window.show()
qApp.setActiveWindow(window)
q_app.setActiveWindow(window)
process_pos_args(args.command)
_open_startpage()
@ -303,10 +307,10 @@ def process_pos_args(args, via_ipc=False, cwd=None, target_arg=None):
def open_url(url, target=None, no_raise=False, via_ipc=True):
"""Open an URL in new window/tab.
"""Open a URL in new window/tab.
Args:
url: An URL to open.
url: A URL to open.
target: same as new_instance_open_target (used as a default).
no_raise: suppress target window raising.
via_ipc: Whether the arguments were transmitted over IPC.
@ -421,7 +425,7 @@ def _init_modules(args, crash_handler):
crash_handler: The CrashHandler instance.
"""
log.init.debug("Initializing save manager...")
save_manager = savemanager.SaveManager(qApp)
save_manager = savemanager.SaveManager(q_app)
objreg.register('save-manager', save_manager)
configinit.late_init(save_manager)
@ -446,7 +450,7 @@ def _init_modules(args, crash_handler):
sql.init(os.path.join(standarddir.data(), 'history.sqlite'))
log.init.debug("Initializing web history...")
history.init(qApp)
history.init(q_app)
except sql.SqlEnvironmentError as e:
error.handle_fatal_exc(e, args, 'Error initializing SQL',
pre_text='Error initializing SQL')
@ -460,36 +464,31 @@ def _init_modules(args, crash_handler):
crash_handler.handle_segfault()
log.init.debug("Initializing sessions...")
sessions.init(qApp)
sessions.init(q_app)
log.init.debug("Initializing websettings...")
websettings.init(args)
log.init.debug("Initializing adblock...")
host_blocker = adblock.HostBlocker()
host_blocker.read_hosts()
objreg.register('host-blocker', host_blocker)
log.init.debug("Initializing quickmarks...")
quickmark_manager = urlmarks.QuickmarkManager(qApp)
quickmark_manager = urlmarks.QuickmarkManager(q_app)
objreg.register('quickmark-manager', quickmark_manager)
log.init.debug("Initializing bookmarks...")
bookmark_manager = urlmarks.BookmarkManager(qApp)
bookmark_manager = urlmarks.BookmarkManager(q_app)
objreg.register('bookmark-manager', bookmark_manager)
log.init.debug("Initializing cookies...")
cookie_jar = cookies.CookieJar(qApp)
ram_cookie_jar = cookies.RAMCookieJar(qApp)
cookie_jar = cookies.CookieJar(q_app)
ram_cookie_jar = cookies.RAMCookieJar(q_app)
objreg.register('cookie-jar', cookie_jar)
objreg.register('ram-cookie-jar', ram_cookie_jar)
log.init.debug("Initializing cache...")
diskcache = cache.DiskCache(standarddir.cache(), parent=qApp)
diskcache = cache.DiskCache(standarddir.cache(), parent=q_app)
objreg.register('cache', diskcache)
log.init.debug("Initializing downloads...")
download_manager = qtnetworkdownloads.DownloadManager(parent=qApp)
download_manager = qtnetworkdownloads.DownloadManager(parent=q_app)
objreg.register('qtnetwork-download-manager', download_manager)
log.init.debug("Initializing Greasemonkey...")
@ -619,10 +618,11 @@ class Quitter:
ok = self.restart(session='_restart')
except sessions.SessionError as e:
log.destroy.exception("Failed to save session!")
raise cmdexc.CommandError("Failed to save session: {}!".format(e))
raise cmdutils.CommandError("Failed to save session: {}!"
.format(e))
except SyntaxError as e:
log.destroy.exception("Got SyntaxError")
raise cmdexc.CommandError("SyntaxError in {}:{}: {}".format(
raise cmdutils.CommandError("SyntaxError in {}:{}: {}".format(
e.filename, e.lineno, e))
if ok:
self.shutdown(restart=True)
@ -684,7 +684,7 @@ class Quitter:
session: The name of the session to save.
"""
if session is not None and not save:
raise cmdexc.CommandError("Session name given without --save!")
raise cmdutils.CommandError("Session name given without --save!")
if save:
if session is None:
session = sessions.default
@ -735,7 +735,7 @@ class Quitter:
def _shutdown(self, status, restart): # noqa
"""Second stage of shutdown."""
log.destroy.debug("Stage 2 of shutting down...")
if qApp is None:
if q_app is None:
# No QApplication exists yet, so quit hard.
sys.exit(status)
# Remove eventfilter
@ -743,7 +743,7 @@ class Quitter:
log.destroy.debug("Removing eventfilter...")
event_filter = objreg.get('event-filter', None)
if event_filter is not None:
qApp.removeEventFilter(event_filter)
q_app.removeEventFilter(event_filter)
except AttributeError:
pass
# Close all windows
@ -792,7 +792,7 @@ class Quitter:
session_manager.delete_autosave()
# We use a singleshot timer to exit here to minimize the likelihood of
# segfaults.
QTimer.singleShot(0, functools.partial(qApp.exit, status))
QTimer.singleShot(0, functools.partial(q_app.exit, status))
class Application(QApplication):
@ -893,7 +893,7 @@ class EventFilter(QObject):
Return:
True if the event should be filtered, False if it's passed through.
"""
if qApp.activeWindow() not in objreg.window_registry.values():
if q_app.activeWindow() not in objreg.window_registry.values():
# Some other window (print dialog, etc.) is focused so we pass the
# event through.
return False

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -33,14 +33,18 @@ from PyQt5.QtCore import (pyqtSlot, pyqtSignal, Qt, QObject, QModelIndex,
QTimer, QAbstractListModel, QUrl)
from qutebrowser.browser import pdfjs
from qutebrowser.commands import cmdexc, cmdutils
from qutebrowser.api import cmdutils
from qutebrowser.config import config
from qutebrowser.utils import (usertypes, standarddir, utils, message, log,
qtutils, objreg)
from qutebrowser.qt import sip
ModelRole = enum.IntEnum('ModelRole', ['item'], start=Qt.UserRole)
class ModelRole(enum.IntEnum):
"""Custom download model roles."""
item = Qt.UserRole
# Remember the last used directory
@ -60,8 +64,6 @@ class UnsupportedAttribute:
supported with QtWebengine.
"""
pass
class UnsupportedOperationError(Exception):
@ -1007,11 +1009,11 @@ class DownloadModel(QAbstractListModel):
count: The index of the download
"""
if not count:
raise cmdexc.CommandError("There's no download!")
raise cmdexc.CommandError("There's no download {}!".format(count))
raise cmdutils.CommandError("There's no download!")
raise cmdutils.CommandError("There's no download {}!".format(count))
@cmdutils.register(instance='download-model', scope='window')
@cmdutils.argument('count', count=True)
@cmdutils.argument('count', value=cmdutils.Value.count)
def download_cancel(self, all_=False, count=0):
"""Cancel the last/[count]th download.
@ -1032,12 +1034,12 @@ class DownloadModel(QAbstractListModel):
if download.done:
if not count:
count = len(self)
raise cmdexc.CommandError("Download {} is already done!"
.format(count))
raise cmdutils.CommandError("Download {} is already done!"
.format(count))
download.cancel()
@cmdutils.register(instance='download-model', scope='window')
@cmdutils.argument('count', count=True)
@cmdutils.argument('count', value=cmdutils.Value.count)
def download_delete(self, count=0):
"""Delete the last/[count]th download from disk.
@ -1051,14 +1053,15 @@ class DownloadModel(QAbstractListModel):
if not download.successful:
if not count:
count = len(self)
raise cmdexc.CommandError("Download {} is not done!".format(count))
raise cmdutils.CommandError("Download {} is not done!"
.format(count))
download.delete()
download.remove()
log.downloads.debug("deleted download {}".format(download))
@cmdutils.register(instance='download-model', scope='window', maxsplit=0)
@cmdutils.argument('count', count=True)
def download_open(self, cmdline: str = None, count=0):
@cmdutils.argument('count', value=cmdutils.Value.count)
def download_open(self, cmdline: str = None, count: int = 0) -> None:
"""Open the last/[count]th download.
If no specific command is given, this will use the system's default
@ -1078,11 +1081,12 @@ class DownloadModel(QAbstractListModel):
if not download.successful:
if not count:
count = len(self)
raise cmdexc.CommandError("Download {} is not done!".format(count))
raise cmdutils.CommandError("Download {} is not done!"
.format(count))
download.open_file(cmdline)
@cmdutils.register(instance='download-model', scope='window')
@cmdutils.argument('count', count=True)
@cmdutils.argument('count', value=cmdutils.Value.count)
def download_retry(self, count=0):
"""Retry the first failed/[count]th download.
@ -1095,12 +1099,12 @@ class DownloadModel(QAbstractListModel):
except IndexError:
self._raise_no_download(count)
if download.successful or not download.done:
raise cmdexc.CommandError("Download {} did not fail!".format(
count))
raise cmdutils.CommandError("Download {} did not fail!"
.format(count))
else:
to_retry = [d for d in self if d.done and not d.successful]
if not to_retry:
raise cmdexc.CommandError("No failed downloads!")
raise cmdutils.CommandError("No failed downloads!")
else:
download = to_retry[0]
download.try_retry()
@ -1117,7 +1121,7 @@ class DownloadModel(QAbstractListModel):
download.remove()
@cmdutils.register(instance='download-model', scope='window')
@cmdutils.argument('count', count=True)
@cmdutils.argument('count', value=cmdutils.Value.count)
def download_remove(self, all_=False, count=0):
"""Remove the last/[count]th download from the list.
@ -1135,8 +1139,8 @@ class DownloadModel(QAbstractListModel):
if not download.done:
if not count:
count = len(self)
raise cmdexc.CommandError("Download {} is not done!"
.format(count))
raise cmdutils.CommandError("Download {} is not done!"
.format(count))
download.remove()
def running_downloads(self):

View File

@ -75,7 +75,8 @@ class DownloadView(QListView):
def __init__(self, win_id, parent=None):
super().__init__(parent)
self.setStyle(QStyleFactory.create('Fusion'))
if not utils.is_mac:
self.setStyle(QStyleFactory.create('Fusion'))
config.set_register_stylesheet(self)
self.setResizeMode(QListView.Adjust)
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)

View File

@ -32,7 +32,7 @@ from PyQt5.QtCore import pyqtSignal, QObject, QUrl
from qutebrowser.utils import (log, standarddir, jinja, objreg, utils,
javascript, urlmatch, version, usertypes)
from qutebrowser.commands import cmdutils
from qutebrowser.api import cmdutils
from qutebrowser.browser import downloads
from qutebrowser.misc import objects

View File

@ -21,7 +21,6 @@
import collections
import functools
import math
import os
import re
import html
@ -35,7 +34,8 @@ from PyQt5.QtWidgets import QLabel
from qutebrowser.config import config, configexc
from qutebrowser.keyinput import modeman, modeparsers
from qutebrowser.browser import webelem
from qutebrowser.commands import userscripts, cmdexc, cmdutils, runners
from qutebrowser.commands import userscripts, runners
from qutebrowser.api import cmdutils
from qutebrowser.utils import usertypes, log, qtutils, message, objreg, utils
@ -102,7 +102,7 @@ class HintLabel(QLabel):
matched: The part of the text which was typed.
unmatched: The part of the text which was not typed yet.
"""
if (config.val.hints.uppercase and
if (config.cache['hints.uppercase'] and
self._context.hint_mode in ['letter', 'word']):
matched = html.escape(matched.upper())
unmatched = html.escape(unmatched.upper())
@ -110,9 +110,12 @@ class HintLabel(QLabel):
matched = html.escape(matched)
unmatched = html.escape(unmatched)
match_color = html.escape(config.val.colors.hints.match.fg)
self.setText('<font color="{}">{}</font>{}'.format(
match_color, matched, unmatched))
match_color = html.escape(config.cache['colors.hints.match.fg'])
if matched:
self.setText('<font color="{}">{}</font>{}'.format(
match_color, matched, unmatched))
else:
self.setText(unmatched)
self.adjustSize()
@pyqtSlot()
@ -123,7 +126,7 @@ class HintLabel(QLabel):
log.hints.debug("Frame for {!r} vanished!".format(self))
self.hide()
return
no_js = config.val.hints.find_implementation != 'javascript'
no_js = config.cache['hints.find_implementation'] != 'javascript'
rect = self.elem.rect_on_view(no_js=no_js)
self.move(rect.x(), rect.y())
@ -215,9 +218,7 @@ class HintActions:
if context.target in [Target.normal, Target.current]:
# Set the pre-jump mark ', so we can jump back here after following
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=self._win_id)
tabbed_browser.set_mark("'")
context.tab.scroller.before_jump_requested.emit()
try:
if context.target == Target.hover:
@ -302,8 +303,8 @@ class HintActions:
raise HintingError("No suitable link found for this element.")
prompt = False if context.rapid else None
qnam = context.tab.networkaccessmanager()
user_agent = context.tab.user_agent()
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')
@ -454,21 +455,15 @@ class HintManager(QObject):
# Determine how many digits the link hints will require in the worst
# case. Usually we do not need all of these digits for every link
# single hint, so we can show shorter hints for a few of the links.
needed = max(min_chars, math.ceil(math.log(len(elems), len(chars))))
needed = max(min_chars, utils.ceil_log(len(elems), len(chars)))
# Short hints are the number of hints we can possibly show which are
# (needed - 1) digits in length.
if needed > min_chars:
if needed > min_chars and needed > 1:
total_space = len(chars) ** needed
# Calculate short_count naively, by finding the avaiable space and
# dividing by the number of spots we would loose by adding a
# short element
short_count = math.floor((total_space - len(elems)) /
len(chars))
# Check if we double counted above to warrant another short_count
# https://github.com/qutebrowser/qutebrowser/issues/3242
if total_space - (short_count * len(chars) +
(len(elems) - short_count)) >= len(chars) - 1:
short_count += 1
# For each 1 short link being added, len(chars) long links are
# removed, therefore the space removed is len(chars) - 1.
short_count = (total_space - len(elems)) // (len(chars) - 1)
else:
short_count = 0
@ -495,7 +490,7 @@ class HintManager(QObject):
elems: The elements to generate labels for.
"""
strings = []
needed = max(min_chars, math.ceil(math.log(len(elems), len(chars))))
needed = max(min_chars, utils.ceil_log(len(elems), len(chars)))
for i in range(len(elems)):
strings.append(self._number_to_hint_str(i, chars, needed))
return strings
@ -567,12 +562,12 @@ class HintManager(QObject):
if target in [Target.userscript, Target.spawn, Target.run,
Target.fill]:
if not args:
raise cmdexc.CommandError(
raise cmdutils.CommandError(
"'args' is required with target userscript/spawn/run/"
"fill.")
else:
if args:
raise cmdexc.CommandError(
raise cmdutils.CommandError(
"'args' is only allowed with target userscript/spawn.")
def _filter_matches(self, filterstr, elemstr):
@ -600,13 +595,6 @@ class HintManager(QObject):
log.hints.debug("In _start_cb without context!")
return
if elems is None:
message.error("Unknown error while getting hint elements.")
return
elif isinstance(elems, webelem.Error):
message.error(str(elems))
return
if not elems:
message.error("No elements found.")
return
@ -639,9 +627,8 @@ class HintManager(QObject):
@cmdutils.register(instance='hintmanager', scope='tab', name='hint',
star_args_optional=True, maxsplit=2)
@cmdutils.argument('win_id', win_id=True)
def start(self, # pylint: disable=keyword-arg-before-vararg
group='all', target=Target.normal, *args, win_id, mode=None,
group='all', target=Target.normal, *args, mode=None,
add_history=False, rapid=False, first=False):
"""Start hinting.
@ -710,12 +697,12 @@ class HintManager(QObject):
window=self._win_id)
tab = tabbed_browser.widget.currentWidget()
if tab is None:
raise cmdexc.CommandError("No WebView available yet!")
raise cmdutils.CommandError("No WebView available yet!")
mode_manager = objreg.get('mode-manager', scope='window',
window=self._win_id)
if mode_manager.mode == usertypes.KeyMode.hint:
modeman.leave(win_id, usertypes.KeyMode.hint, 're-hinting')
modeman.leave(self._win_id, usertypes.KeyMode.hint, 're-hinting')
if rapid:
if target in [Target.tab_bg, Target.window, Target.run,
@ -727,8 +714,8 @@ class HintManager(QObject):
pass
else:
name = target.name.replace('_', '-')
raise cmdexc.CommandError("Rapid hinting makes no sense with "
"target {}!".format(name))
raise cmdutils.CommandError("Rapid hinting makes no sense "
"with target {}!".format(name))
self._check_args(target, *args)
self._context = HintContext()
@ -741,18 +728,21 @@ class HintManager(QObject):
try:
self._context.baseurl = tabbed_browser.current_url()
except qtutils.QtValueError:
raise cmdexc.CommandError("No URL set for this page yet!")
self._context.args = args
raise cmdutils.CommandError("No URL set for this page yet!")
self._context.args = list(args)
self._context.group = group
try:
selector = webelem.css_selector(self._context.group,
self._context.baseurl)
except webelem.Error as e:
raise cmdexc.CommandError(str(e))
raise cmdutils.CommandError(str(e))
self._context.tab.elements.find_css(selector, self._start_cb,
only_visible=True)
self._context.tab.elements.find_css(
selector,
callback=self._start_cb,
error_cb=lambda err: message.error(str(err)),
only_visible=True)
def _get_hint_mode(self, mode):
"""Get the hinting mode to use based on a mode argument."""
@ -763,7 +753,7 @@ class HintManager(QObject):
try:
opt.typ.to_py(mode)
except configexc.ValidationError as e:
raise cmdexc.CommandError("Invalid mode: {}".format(e))
raise cmdutils.CommandError("Invalid mode: {}".format(e))
return mode
def current_mode(self):
@ -838,9 +828,10 @@ class HintManager(QObject):
Args:
filterstr: The string to filter with, or None to use the filter
from previous call (saved in `self._filterstr`). If
`filterstr` is an empty string or if both `filterstr`
and `self._filterstr` are None, all hints are shown.
from previous call (saved in `self._context.filterstr`).
If `filterstr` is an empty string or if both `filterstr`
and `self._context.filterstr` are None, all hints are
shown.
"""
if filterstr is None:
filterstr = self._context.filterstr
@ -964,13 +955,13 @@ class HintManager(QObject):
"""
if keystring is None:
if self._context.to_follow is None:
raise cmdexc.CommandError("No hint to follow")
raise cmdutils.CommandError("No hint to follow")
elif select:
raise cmdexc.CommandError("Can't use --select without hint.")
raise cmdutils.CommandError("Can't use --select without hint.")
else:
keystring = self._context.to_follow
elif keystring not in self._context.labels:
raise cmdexc.CommandError("No hint {}!".format(keystring))
raise cmdutils.CommandError("No hint {}!".format(keystring))
if select:
self.handle_partial_key(keystring)

View File

@ -27,7 +27,7 @@ from PyQt5.QtCore import pyqtSlot, QUrl, pyqtSignal
from PyQt5.QtWidgets import QProgressDialog, QApplication
from qutebrowser.config import config
from qutebrowser.commands import cmdutils, cmdexc
from qutebrowser.api import cmdutils
from qutebrowser.utils import utils, objreg, log, usertypes, message, qtutils
from qutebrowser.misc import objects, sql
@ -365,7 +365,8 @@ class WebHistory(sql.SqlTable):
f.write('\n'.join(lines))
message.info("Dumped history to {}".format(dest))
except OSError as e:
raise cmdexc.CommandError('Could not write history: {}'.format(e))
raise cmdutils.CommandError('Could not write history: {}'
.format(e))
def init(parent=None):

View File

@ -49,8 +49,6 @@ class WebInspectorError(Exception):
"""Raised when the inspector could not be initialized."""
pass
class AbstractWebInspector(QWidget):

View File

@ -240,7 +240,7 @@ class MouseEventFilter(QObject):
evtype = event.type()
if evtype not in self._handlers:
return False
if obj is not self._tab.event_target():
if obj is not self._tab.private_api.event_target():
log.mouse.debug("Ignoring {} to {}".format(
event.__class__.__name__, obj))
return False

View File

@ -116,13 +116,6 @@ def prevnext(*, browsertab, win_id, baseurl, prev=False,
window: True to open in a new window, False for the current one.
"""
def _prevnext_cb(elems):
if elems is None:
message.error("Unknown error while getting hint elements")
return
elif isinstance(elems, webelem.Error):
message.error(str(elems))
return
elem = _find_prevnext(prev, elems)
word = 'prev' if prev else 'forward'
@ -140,7 +133,7 @@ def prevnext(*, browsertab, win_id, baseurl, prev=False,
if window:
new_window = mainwindow.MainWindow(
private=cur_tabbed_browser.private)
private=cur_tabbed_browser.is_private)
new_window.show()
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=new_window.win_id)
@ -148,11 +141,12 @@ def prevnext(*, browsertab, win_id, baseurl, prev=False,
elif tab:
cur_tabbed_browser.tabopen(url, background=background)
else:
browsertab.openurl(url)
browsertab.load_url(url)
try:
link_selector = webelem.css_selector('links', baseurl)
except webelem.Error as e:
raise Error(str(e))
browsertab.elements.find_css(link_selector, _prevnext_cb)
browsertab.elements.find_css(link_selector, callback=_prevnext_cb,
error_cb=lambda err: message.error(str(err)))

View File

@ -35,15 +35,11 @@ class ParseProxyError(Exception):
"""Error while parsing PAC result string."""
pass
class EvalProxyError(Exception):
"""Error while evaluating PAC script."""
pass
def _js_slot(*args):
"""Wrap a methods as a JavaScript function.
@ -184,6 +180,8 @@ class PACResolver:
"""
self._engine = QJSEngine()
self._engine.installExtensions(QJSEngine.ConsoleExtension)
self._ctx = _PACContext(self._engine)
self._engine.globalObject().setProperty(
"PAC", self._engine.newQObject(self._ctx))
@ -210,6 +208,8 @@ class PACResolver:
Return:
A list of QNetworkProxy objects in order of preference.
"""
qtutils.ensure_valid(query.url())
if from_file:
string_flags = QUrl.PrettyDecoded
else:

View File

@ -20,10 +20,12 @@
"""Handling of proxies."""
from PyQt5.QtCore import QUrl
from PyQt5.QtNetwork import QNetworkProxy, QNetworkProxyFactory
from qutebrowser.config import config, configtypes
from qutebrowser.utils import objreg
from qutebrowser.utils import objreg, message, usertypes, urlutils
from qutebrowser.misc import objects
from qutebrowser.browser.network import pac
@ -33,6 +35,18 @@ def init():
objreg.register('proxy-factory', proxy_factory)
QNetworkProxyFactory.setApplicationProxyFactory(proxy_factory)
config.instance.changed.connect(_warn_for_pac)
_warn_for_pac()
@config.change_filter('content.proxy', function=True)
def _warn_for_pac():
"""Show a warning if PAC is used with QtWebEngine."""
proxy = config.val.content.proxy
if (isinstance(proxy, pac.PACFetcher) and
objects.backend == usertypes.Backend.QtWebEngine):
message.error("PAC support isn't implemented for QtWebEngine yet!")
def shutdown():
QNetworkProxyFactory.setApplicationProxyFactory(None)
@ -70,7 +84,11 @@ class ProxyFactory(QNetworkProxyFactory):
# ref. http://doc.qt.io/qt-5/qnetworkproxyfactory.html#systemProxyForQuery
proxies = QNetworkProxyFactory.systemProxyForQuery(query)
elif isinstance(proxy, pac.PACFetcher):
proxies = proxy.resolve(query)
if objects.backend == usertypes.Backend.QtWebEngine:
# Looks like query.url() is always invalid on QtWebEngine...
proxies = [urlutils.proxy_from_url(QUrl('direct://'))]
else:
proxies = proxy.resolve(query)
else:
proxies = [proxy]
for p in proxies:

View File

@ -37,7 +37,7 @@ try:
import secrets
except ImportError:
# New in Python 3.6
secrets = None
secrets = None # type: ignore
from PyQt5.QtCore import QUrlQuery, QUrl, qVersion
@ -61,36 +61,26 @@ class Error(Exception):
"""Exception for generic errors on a qute:// page."""
pass
class NotFoundError(Error):
"""Raised when the given URL was not found."""
pass
class SchemeOSError(Error):
"""Raised when there was an OSError inside a handler."""
pass
class UrlInvalidError(Error):
"""Raised when an invalid URL was opened."""
pass
class RequestDeniedError(Error):
"""Raised when the request is forbidden."""
pass
class Redirect(Exception):
@ -350,19 +340,11 @@ def qute_gpl(_url):
def _asciidoc_fallback_path(html_path):
"""Fall back to plaintext asciidoc if the HTML is unavailable."""
asciidoc_path = html_path.replace('.html', '.asciidoc')
asciidoc_paths = [asciidoc_path]
if asciidoc_path.startswith('html/doc/'):
asciidoc_paths += [asciidoc_path.replace('html/doc/', '../doc/help/'),
asciidoc_path.replace('html/doc/', '../doc/')]
for path in asciidoc_paths:
try:
return utils.read_file(path)
except OSError:
pass
return None
path = html_path.replace('.html', '.asciidoc')
try:
return utils.read_file(path)
except OSError:
return None
@add_handler('help')

View File

@ -42,7 +42,6 @@ def custom_headers(url):
if dnt_config is not None:
dnt = b'1' if dnt_config else b'0'
headers[b'DNT'] = dnt
headers[b'X-Do-Not-Track'] = dnt
conf_headers = config.instance.get('content.headers.custom', url=url)
for header, value in conf_headers.items():
@ -262,7 +261,7 @@ def get_tab(win_id, target):
elif target == usertypes.ClickTarget.window:
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
window = mainwindow.MainWindow(private=tabbed_browser.private)
window = mainwindow.MainWindow(private=tabbed_browser.is_private)
window.show()
win_id = window.win_id
bg_tab = False

View File

@ -35,7 +35,7 @@ from PyQt5.QtCore import pyqtSignal, QUrl, QObject
from qutebrowser.utils import (message, usertypes, qtutils, urlutils,
standarddir, objreg, log)
from qutebrowser.commands import cmdutils
from qutebrowser.api import cmdutils
from qutebrowser.misc import lineparser
@ -43,29 +43,21 @@ class Error(Exception):
"""Base class for all errors in this module."""
pass
class InvalidUrlError(Error):
"""Exception emitted when a URL is invalid."""
pass
class DoesNotExistError(Error):
"""Exception emitted when a given URL does not exist."""
pass
class AlreadyExistsError(Error):
"""Exception emitted when a given URL does already exist."""
pass
class UrlMarkManager(QObject):
@ -174,7 +166,7 @@ class QuickmarkManager(UrlMarkManager):
url: The url to add as quickmark.
name: The name for the new quickmark.
"""
# We don't raise cmdexc.CommandError here as this can be called async
# We don't raise cmdutils.CommandError here as this can be called async
# via prompt_save.
if not name:
message.error("Can't set mark with empty name!")

View File

@ -19,32 +19,36 @@
"""Generic web element related code."""
import typing
import collections.abc
from PyQt5.QtCore import QUrl, Qt, QEvent, QTimer
from PyQt5.QtCore import QUrl, Qt, QEvent, QTimer, QRect, QPoint
from PyQt5.QtGui import QMouseEvent
from qutebrowser.config import config
from qutebrowser.keyinput import modeman
from qutebrowser.mainwindow import mainwindow
from qutebrowser.utils import log, usertypes, utils, qtutils, objreg
MYPY = False
if MYPY:
# pylint: disable=unused-import,useless-suppression
from qutebrowser.browser import browsertab
JsValueType = typing.Union[int, float, str, None]
class Error(Exception):
"""Base class for WebElement errors."""
pass
class OrphanedError(Error):
"""Raised when a webelement's parent has vanished."""
pass
def css_selector(group, url):
def css_selector(group: str, url: QUrl) -> str:
"""Get a CSS selector for the given group/URL."""
selectors = config.instance.get('hints.selectors', url)
if group not in selectors:
@ -58,76 +62,74 @@ def css_selector(group, url):
class AbstractWebElement(collections.abc.MutableMapping):
"""A wrapper around QtWebKit/QtWebEngine web element.
"""A wrapper around QtWebKit/QtWebEngine web element."""
Attributes:
tab: The tab associated with this element.
"""
def __init__(self, tab):
def __init__(self, tab: 'browsertab.AbstractTab') -> None:
self._tab = tab
def __eq__(self, other):
def __eq__(self, other: object) -> bool:
raise NotImplementedError
def __str__(self):
def __str__(self) -> str:
raise NotImplementedError
def __getitem__(self, key):
def __getitem__(self, key: str) -> str:
raise NotImplementedError
def __setitem__(self, key, val):
def __setitem__(self, key: str, val: str) -> None:
raise NotImplementedError
def __delitem__(self, key):
def __delitem__(self, key: str) -> None:
raise NotImplementedError
def __iter__(self):
def __iter__(self) -> typing.Iterator[str]:
raise NotImplementedError
def __len__(self):
def __len__(self) -> int:
raise NotImplementedError
def __repr__(self):
def __repr__(self) -> str:
try:
html = utils.compact_text(self.outer_xml(), 500)
except Error:
html = None
return utils.get_repr(self, html=html)
def has_frame(self):
def has_frame(self) -> bool:
"""Check if this element has a valid frame attached."""
raise NotImplementedError
def geometry(self):
def geometry(self) -> QRect:
"""Get the geometry for this element."""
raise NotImplementedError
def classes(self):
def classes(self) -> typing.List[str]:
"""Get a list of classes assigned to this element."""
raise NotImplementedError
def tag_name(self):
def tag_name(self) -> str:
"""Get the tag name of this element.
The returned name will always be lower-case.
"""
raise NotImplementedError
def outer_xml(self):
def outer_xml(self) -> str:
"""Get the full HTML representation of this element."""
raise NotImplementedError
def value(self):
def value(self) -> JsValueType:
"""Get the value attribute for this element, or None."""
raise NotImplementedError
def set_value(self, value):
def set_value(self, value: JsValueType) -> None:
"""Set the element value."""
raise NotImplementedError
def dispatch_event(self, event, bubbles=False,
cancelable=False, composed=False):
def dispatch_event(self, event: str,
bubbles: bool = False,
cancelable: bool = False,
composed: bool = False) -> None:
"""Dispatch an event to the element.
Args:
@ -138,35 +140,25 @@ class AbstractWebElement(collections.abc.MutableMapping):
"""
raise NotImplementedError
def insert_text(self, text):
def insert_text(self, text: str) -> None:
"""Insert the given text into the element."""
raise NotImplementedError
def rect_on_view(self, *, elem_geometry=None, no_js=False):
def rect_on_view(self, *, elem_geometry: QRect = None,
no_js: bool = False) -> QRect:
"""Get the geometry of the element relative to the webview.
Uses the getClientRects() JavaScript method to obtain the collection of
rectangles containing the element and returns the first rectangle which
is large enough (larger than 1px times 1px). If all rectangles returned
by getClientRects() are too small, falls back to elem.rect_on_view().
Skipping of small rectangles is due to <a> elements containing other
elements with "display:block" style, see
https://github.com/qutebrowser/qutebrowser/issues/1298
Args:
elem_geometry: The geometry of the element, or None.
Calling QWebElement::geometry is rather expensive so
we want to avoid doing it twice.
no_js: Fall back to the Python implementation
no_js: Fall back to the Python implementation.
"""
raise NotImplementedError
def is_writable(self):
def is_writable(self) -> bool:
"""Check whether an element is writable."""
return not ('disabled' in self or 'readonly' in self)
def is_content_editable(self):
def is_content_editable(self) -> bool:
"""Check if an element has a contenteditable attribute.
Args:
@ -181,7 +173,7 @@ class AbstractWebElement(collections.abc.MutableMapping):
except KeyError:
return False
def _is_editable_object(self):
def _is_editable_object(self) -> bool:
"""Check if an object-element is editable."""
if 'type' not in self:
log.webelem.debug("<object> without type clicked...")
@ -197,7 +189,7 @@ class AbstractWebElement(collections.abc.MutableMapping):
# Image/Audio/...
return False
def _is_editable_input(self):
def _is_editable_input(self) -> bool:
"""Check if an input-element is editable.
Return:
@ -214,7 +206,7 @@ class AbstractWebElement(collections.abc.MutableMapping):
else:
return False
def _is_editable_classes(self):
def _is_editable_classes(self) -> bool:
"""Check if an element is editable based on its classes.
Return:
@ -233,7 +225,7 @@ class AbstractWebElement(collections.abc.MutableMapping):
return True
return False
def is_editable(self, strict=False):
def is_editable(self, strict: bool = False) -> bool:
"""Check whether we should switch to insert mode for this element.
Args:
@ -264,17 +256,17 @@ class AbstractWebElement(collections.abc.MutableMapping):
return self._is_editable_classes() and not strict
return False
def is_text_input(self):
def is_text_input(self) -> bool:
"""Check if this element is some kind of text box."""
roles = ('combobox', 'textbox')
tag = self.tag_name()
return self.get('role', None) in roles or tag in ['input', 'textarea']
def remove_blank_target(self):
def remove_blank_target(self) -> None:
"""Remove target from link."""
raise NotImplementedError
def resolve_url(self, baseurl):
def resolve_url(self, baseurl: QUrl) -> typing.Optional[QUrl]:
"""Resolve the URL in the element's src/href attribute.
Args:
@ -301,16 +293,16 @@ class AbstractWebElement(collections.abc.MutableMapping):
qtutils.ensure_valid(url)
return url
def is_link(self):
def is_link(self) -> bool:
"""Return True if this AbstractWebElement is a link."""
href_tags = ['a', 'area', 'link']
return self.tag_name() in href_tags and 'href' in self
def _requires_user_interaction(self):
def _requires_user_interaction(self) -> bool:
"""Return True if clicking this element needs user interaction."""
raise NotImplementedError
def _mouse_pos(self):
def _mouse_pos(self) -> QPoint:
"""Get the position to click/hover."""
# Click the center of the largest square fitting into the top/left
# corner of the rectangle, this will help if part of the <a> element
@ -326,35 +318,38 @@ class AbstractWebElement(collections.abc.MutableMapping):
raise Error("Element position is out of view!")
return pos
def _move_text_cursor(self):
def _move_text_cursor(self) -> None:
"""Move cursor to end after clicking."""
raise NotImplementedError
def _click_fake_event(self, click_target):
def _click_fake_event(self, click_target: usertypes.ClickTarget) -> None:
"""Send a fake click event to the element."""
pos = self._mouse_pos()
log.webelem.debug("Sending fake click to {!r} at position {} with "
"target {}".format(self, pos, click_target))
modifiers = {
target_modifiers = {
usertypes.ClickTarget.normal: Qt.NoModifier,
usertypes.ClickTarget.window: Qt.AltModifier | Qt.ShiftModifier,
usertypes.ClickTarget.tab: Qt.ControlModifier,
usertypes.ClickTarget.tab_bg: Qt.ControlModifier,
}
if config.val.tabs.background:
modifiers[usertypes.ClickTarget.tab] |= Qt.ShiftModifier
target_modifiers[usertypes.ClickTarget.tab] |= Qt.ShiftModifier
else:
modifiers[usertypes.ClickTarget.tab_bg] |= Qt.ShiftModifier
target_modifiers[usertypes.ClickTarget.tab_bg] |= Qt.ShiftModifier
modifiers = typing.cast(Qt.KeyboardModifiers,
target_modifiers[click_target])
events = [
QMouseEvent(QEvent.MouseMove, pos, Qt.NoButton, Qt.NoButton,
Qt.NoModifier),
QMouseEvent(QEvent.MouseButtonPress, pos, Qt.LeftButton,
Qt.LeftButton, modifiers[click_target]),
Qt.LeftButton, modifiers),
QMouseEvent(QEvent.MouseButtonRelease, pos, Qt.LeftButton,
Qt.NoButton, modifiers[click_target]),
Qt.NoButton, modifiers),
]
for evt in events:
@ -362,15 +357,15 @@ class AbstractWebElement(collections.abc.MutableMapping):
QTimer.singleShot(0, self._move_text_cursor)
def _click_editable(self, click_target):
def _click_editable(self, click_target: usertypes.ClickTarget) -> None:
"""Fake a click on an editable input field."""
raise NotImplementedError
def _click_js(self, click_target):
def _click_js(self, click_target: usertypes.ClickTarget) -> None:
"""Fake a click by using the JS .click() method."""
raise NotImplementedError
def _click_href(self, click_target):
def _click_href(self, click_target: usertypes.ClickTarget) -> None:
"""Fake a click on an element with a href by opening the link."""
baseurl = self._tab.url()
url = self.resolve_url(baseurl)
@ -386,13 +381,14 @@ class AbstractWebElement(collections.abc.MutableMapping):
background = click_target == usertypes.ClickTarget.tab_bg
tabbed_browser.tabopen(url, background=background)
elif click_target == usertypes.ClickTarget.window:
window = mainwindow.MainWindow(private=tabbed_browser.private)
window = mainwindow.MainWindow(private=tabbed_browser.is_private)
window.show()
window.tabbed_browser.tabopen(url)
else:
raise ValueError("Unknown ClickTarget {}".format(click_target))
def click(self, click_target, *, force_event=False):
def click(self, click_target: usertypes.ClickTarget, *,
force_event: bool = False) -> None:
"""Simulate a click on the element.
Args:
@ -429,7 +425,7 @@ class AbstractWebElement(collections.abc.MutableMapping):
else:
raise ValueError("Unknown ClickTarget {}".format(click_target))
def hover(self):
def hover(self) -> None:
"""Simulate a mouse hover over the element."""
pos = self._mouse_pos()
event = QMouseEvent(QEvent.MouseMove, pos, Qt.NoButton, Qt.NoButton,

View File

@ -20,7 +20,7 @@
"""Filter for QtWebEngine cookies."""
from qutebrowser.config import config
from qutebrowser.utils import utils
from qutebrowser.utils import utils, qtutils
def _accept_cookie(request):
@ -29,7 +29,13 @@ def _accept_cookie(request):
if accept == 'all':
return True
elif accept in ['no-3rdparty', 'no-unknown-3rdparty']:
return not request.thirdParty
if qtutils.version_check('5.11.3', compiled=False):
third_party = request.thirdParty
else:
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-71393
third_party = (request.thirdParty and
not request.firstPartyUrl.isEmpty())
return not third_party
elif accept == 'never':
return False
else:

View File

@ -26,15 +26,15 @@ from PyQt5.QtWebEngineCore import (QWebEngineUrlRequestInterceptor,
from qutebrowser.config import config
from qutebrowser.browser import shared
from qutebrowser.utils import utils, log, debug
from qutebrowser.extensions import interceptors
class RequestInterceptor(QWebEngineUrlRequestInterceptor):
"""Handle ad blocking and custom headers."""
def __init__(self, host_blocker, args, parent=None):
def __init__(self, args, parent=None):
super().__init__(parent)
self._host_blocker = host_blocker
self._args = args
def install(self, profile):
@ -84,9 +84,10 @@ class RequestInterceptor(QWebEngineUrlRequestInterceptor):
return
# FIXME:qtwebengine only block ads for NavigationTypeOther?
if self._host_blocker.is_blocked(url, first_party):
log.webview.info("Request to {} blocked by host blocker.".format(
url.host()))
request = interceptors.Request(first_party_url=first_party,
request_url=url)
interceptors.run(request)
if request.is_blocked:
info.block(True)
for header, value in shared.custom_headers(url=url):

View File

@ -117,8 +117,7 @@ class DownloadItem(downloads.AbstractDownloadItem):
def _get_open_filename(self):
return self._filename
def _set_fileobj(self, fileobj, *,
autoclose=True): # pylint: disable=unused-argument
def _set_fileobj(self, fileobj, *, autoclose=True):
raise downloads.UnsupportedOperationError
def _set_tempfile(self, fileobj):
@ -181,7 +180,21 @@ def _get_suggested_filename(path):
See https://bugreports.qt.io/browse/QTBUG-56978
"""
filename = os.path.basename(path)
filename = re.sub(r'\([0-9]+\)(?=\.|$)', '', filename)
suffix_re = re.compile(r"""
\ ? # Optional space between filename and suffix
(
# Numerical suffix
\([0-9]+\)
|
# ISO-8601 suffix
# https://cs.chromium.org/chromium/src/base/time/time_to_iso8601.cc
\ -\ \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z
)
(?=\.|$) # Begin of extension, or filename without extension
""", re.VERBOSE)
filename = suffix_re.sub('', filename)
if not qtutils.version_check('5.9', compiled=False):
# https://bugreports.qt.io/browse/QTBUG-58155
filename = urllib.parse.unquote(filename)

View File

@ -22,20 +22,27 @@
"""QtWebEngine specific part of the web element API."""
import typing
from PyQt5.QtCore import QRect, Qt, QPoint, QEventLoop
from PyQt5.QtGui import QMouseEvent
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWebEngineWidgets import QWebEngineSettings
from qutebrowser.utils import log, javascript, urlutils
from qutebrowser.utils import log, javascript, urlutils, usertypes
from qutebrowser.browser import webelem
MYPY = False
if MYPY:
# pylint: disable=unused-import,useless-suppression
from qutebrowser.browser.webengine import webenginetab
class WebEngineElement(webelem.AbstractWebElement):
"""A web element for QtWebEngine, using JS under the hood."""
def __init__(self, js_dict, tab):
def __init__(self, js_dict: typing.Dict[str, typing.Any],
tab: 'webenginetab.WebEngineTab') -> None:
super().__init__(tab)
# Do some sanity checks on the data we get from JS
js_dict_types = {
@ -48,7 +55,7 @@ class WebEngineElement(webelem.AbstractWebElement):
'rects': list,
'attributes': dict,
'caret_position': (int, type(None)),
}
} # type: typing.Dict[str, typing.Union[type, typing.Tuple[type,...]]]
assert set(js_dict.keys()).issubset(js_dict_types.keys())
for name, typ in js_dict_types.items():
if name in js_dict and not isinstance(js_dict[name], typ):
@ -73,50 +80,51 @@ class WebEngineElement(webelem.AbstractWebElement):
self._id = js_dict['id']
self._js_dict = js_dict
def __str__(self):
def __str__(self) -> str:
return self._js_dict.get('text', '')
def __eq__(self, other):
def __eq__(self, other: object) -> bool:
if not isinstance(other, WebEngineElement):
return NotImplemented
return self._id == other._id # pylint: disable=protected-access
def __getitem__(self, key):
def __getitem__(self, key: str) -> str:
attrs = self._js_dict['attributes']
return attrs[key]
def __setitem__(self, key, val):
def __setitem__(self, key: str, val: str) -> None:
self._js_dict['attributes'][key] = val
self._js_call('set_attribute', key, val)
def __delitem__(self, key):
def __delitem__(self, key: str) -> None:
log.stub()
def __iter__(self):
def __iter__(self) -> typing.Iterator[str]:
return iter(self._js_dict['attributes'])
def __len__(self):
def __len__(self) -> int:
return len(self._js_dict['attributes'])
def _js_call(self, name, *args, callback=None):
def _js_call(self, name: str, *args: webelem.JsValueType,
callback: typing.Callable[[typing.Any], None] = None) -> None:
"""Wrapper to run stuff from webelem.js."""
if self._tab.is_deleted():
raise webelem.OrphanedError("Tab containing element vanished")
js_code = javascript.assemble('webelem', name, self._id, *args)
self._tab.run_js_async(js_code, callback=callback)
def has_frame(self):
def has_frame(self) -> bool:
return True
def geometry(self):
def geometry(self) -> QRect:
log.stub()
return QRect()
def classes(self):
def classes(self) -> typing.List[str]:
"""Get a list of classes assigned to this element."""
return self._js_dict['class_name'].split()
def tag_name(self):
def tag_name(self) -> str:
"""Get the tag name of this element.
The returned name will always be lower-case.
@ -125,34 +133,37 @@ class WebEngineElement(webelem.AbstractWebElement):
assert isinstance(tag, str), tag
return tag.lower()
def outer_xml(self):
def outer_xml(self) -> str:
"""Get the full HTML representation of this element."""
return self._js_dict['outer_xml']
def value(self):
def value(self) -> webelem.JsValueType:
return self._js_dict.get('value', None)
def set_value(self, value):
def set_value(self, value: webelem.JsValueType) -> None:
self._js_call('set_value', value)
def dispatch_event(self, event, bubbles=False,
cancelable=False, composed=False):
def dispatch_event(self, event: str,
bubbles: bool = False,
cancelable: bool = False,
composed: bool = False) -> None:
self._js_call('dispatch_event', event, bubbles, cancelable, composed)
def caret_position(self):
def caret_position(self) -> typing.Optional[int]:
"""Get the text caret position for the current element.
If the element is not a text element, None is returned.
"""
return self._js_dict.get('caret_position', None)
def insert_text(self, text):
def insert_text(self, text: str) -> None:
if not self.is_editable(strict=True):
raise webelem.Error("Element is not editable!")
log.webelem.debug("Inserting text into element {!r}".format(self))
self._js_call('insert_text', text)
def rect_on_view(self, *, elem_geometry=None, no_js=False):
def rect_on_view(self, *, elem_geometry: QRect = None,
no_js: bool = False) -> QRect:
"""Get the geometry of the element relative to the webview.
Skipping of small rectangles is due to <a> elements containing other
@ -193,16 +204,16 @@ class WebEngineElement(webelem.AbstractWebElement):
self, rects))
return QRect()
def remove_blank_target(self):
def remove_blank_target(self) -> None:
if self._js_dict['attributes'].get('target') == '_blank':
self._js_dict['attributes']['target'] = '_top'
self._js_call('remove_blank_target')
def _move_text_cursor(self):
def _move_text_cursor(self) -> None:
if self.is_text_input() and self.is_editable():
self._js_call('move_cursor_to_end')
def _requires_user_interaction(self):
def _requires_user_interaction(self) -> bool:
baseurl = self._tab.url()
url = self.resolve_url(baseurl)
if url is None:
@ -211,7 +222,7 @@ class WebEngineElement(webelem.AbstractWebElement):
return False
return url.scheme() not in urlutils.WEBENGINE_SCHEMES
def _click_editable(self, click_target):
def _click_editable(self, click_target: usertypes.ClickTarget) -> None:
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-58515
ev = QMouseEvent(QMouseEvent.MouseButtonPress, QPoint(0, 0),
QPoint(0, 0), QPoint(0, 0), Qt.NoButton, Qt.NoButton,
@ -221,10 +232,11 @@ class WebEngineElement(webelem.AbstractWebElement):
self._js_call('focus')
self._move_text_cursor()
def _click_js(self, _click_target):
def _click_js(self, _click_target: usertypes.ClickTarget) -> None:
# FIXME:qtwebengine Have a proper API for this
# pylint: disable=protected-access
view = self._tab._widget
assert view is not None
# pylint: enable=protected-access
attribute = QWebEngineSettings.JavascriptCanOpenWindows
could_open_windows = view.settings().testAttribute(attribute)
@ -238,8 +250,9 @@ class WebEngineElement(webelem.AbstractWebElement):
qapp.processEvents(QEventLoop.ExcludeSocketNotifiers |
QEventLoop.ExcludeUserInputEvents)
def reset_setting(_arg):
def reset_setting(_arg: typing.Any) -> None:
"""Set the JavascriptCanOpenWindows setting to its old value."""
assert view is not None
try:
view.settings().setAttribute(attribute, could_open_windows)
except RuntimeError:

View File

@ -22,6 +22,11 @@
from PyQt5.QtCore import QBuffer, QIODevice, QUrl
from PyQt5.QtWebEngineCore import (QWebEngineUrlSchemeHandler,
QWebEngineUrlRequestJob)
try:
from PyQt5.QtWebEngineCore import QWebEngineUrlScheme # type: ignore
except ImportError:
# Added in Qt 5.12
QWebEngineUrlScheme = None
from qutebrowser.browser import qutescheme
from qutebrowser.utils import log, qtutils
@ -33,8 +38,12 @@ class QuteSchemeHandler(QWebEngineUrlSchemeHandler):
def install(self, profile):
"""Install the handler for qute:// URLs on the given profile."""
if QWebEngineUrlScheme is not None:
assert QWebEngineUrlScheme.schemeByName(b'qute') is not None
profile.installUrlSchemeHandler(b'qute', self)
if qtutils.version_check('5.11', compiled=False):
if (qtutils.version_check('5.11', compiled=False) and
not qtutils.version_check('5.12', compiled=False)):
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-63378
profile.installUrlSchemeHandler(b'chrome-error', self)
profile.installUrlSchemeHandler(b'chrome-extension', self)
@ -53,18 +62,33 @@ class QuteSchemeHandler(QWebEngineUrlSchemeHandler):
"""
try:
initiator = job.initiator()
request_url = job.requestUrl()
except AttributeError:
# Added in Qt 5.11
return True
if initiator == QUrl('null') and not qtutils.version_check('5.12'):
# https://codereview.qt-project.org/#/c/234849/
is_opaque = initiator == QUrl('null')
target = request_url.scheme(), request_url.host()
if is_opaque and not qtutils.version_check('5.12'):
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-70421
# When we don't register the qute:// scheme, all requests are
# flagged as opaque.
return True
if (target == ('qute', 'testdata') and
is_opaque and
qtutils.version_check('5.12')):
# Allow requests to qute://testdata, as this is needed in Qt 5.12
# for all tests to work properly. No qute://testdata handler is
# installed outside of tests.
return True
if initiator.isValid() and initiator.scheme() != 'qute':
log.misc.warning("Blocking malicious request from {} to {}".format(
initiator.toDisplayString(),
job.requestUrl().toDisplayString()))
request_url.toDisplayString()))
job.fail(QWebEngineUrlRequestJob.RequestDenied)
return False
@ -130,3 +154,16 @@ class QuteSchemeHandler(QWebEngineUrlSchemeHandler):
buf.seek(0)
buf.close()
job.reply(mimetype.encode('ascii'), buf)
def init():
"""Register the qute:// scheme.
Note this needs to be called early, before constructing any QtWebEngine
classes.
"""
if QWebEngineUrlScheme is not None:
scheme = QWebEngineUrlScheme(b'qute')
scheme.setFlags(QWebEngineUrlScheme.LocalScheme |
QWebEngineUrlScheme.LocalAccessAllowed)
QWebEngineUrlScheme.registerScheme(scheme)

View File

@ -25,12 +25,13 @@ Module attributes:
"""
import os
import operator
from PyQt5.QtGui import QFont
from PyQt5.QtWebEngineWidgets import (QWebEngineSettings, QWebEngineProfile,
QWebEnginePage)
from qutebrowser.browser.webengine import spell
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
@ -163,9 +164,14 @@ class WebEngineSettings(websettings.AbstractSettings):
# Qt 5.8
'content.print_element_backgrounds':
('PrintElementBackgrounds', None),
# Qt 5.11
'content.autoplay':
('PlaybackRequiresUserGesture', lambda val: not val),
('PlaybackRequiresUserGesture', operator.not_),
# Qt 5.12
'content.dns_prefetch':
('DnsPrefetchEnabled', None),
}
for name, (attribute, converter) in new_attributes.items():
try:
@ -298,6 +304,7 @@ def init(args):
not hasattr(QWebEnginePage, 'setInspectedPage')): # only Qt < 5.11
os.environ['QTWEBENGINE_REMOTE_DEBUGGING'] = str(utils.random_port())
webenginequtescheme.init()
spell.init()
_init_profiles()

View File

@ -24,9 +24,9 @@ import functools
import re
import html as html_utils
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, Qt, QEvent, QPoint, QPointF,
QUrl, QTimer, QObject)
from PyQt5.QtGui import QKeyEvent, QIcon
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, Qt, QPoint, QPointF, QUrl,
QTimer, QObject)
from PyQt5.QtGui import QIcon
from PyQt5.QtNetwork import QAuthenticator
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWebEngineWidgets import QWebEnginePage, QWebEngineScript
@ -60,10 +60,8 @@ def init():
_qute_scheme_handler.install(webenginesettings.private_profile)
log.init.debug("Initializing request interceptor...")
host_blocker = objreg.get('host-blocker')
args = objreg.get('args')
req_interceptor = interceptor.RequestInterceptor(
host_blocker, args=args, parent=app)
req_interceptor = interceptor.RequestInterceptor(args=args, parent=app)
req_interceptor.install(webenginesettings.default_profile)
req_interceptor.install(webenginesettings.private_profile)
@ -132,7 +130,7 @@ class WebEnginePrinting(browsertab.AbstractPrinting):
"""QtWebEngine implementations related to printing."""
def check_pdf_support(self):
return True
pass
def check_printer_support(self):
if not hasattr(self._widget.page(), 'print'):
@ -205,8 +203,8 @@ class WebEngineSearch(browsertab.AbstractSearch):
self._widget.findText(text, flags, wrapped_callback)
def search(self, text, *, ignore_case='never', reverse=False,
result_cb=None):
def search(self, text, *, ignore_case=usertypes.IgnoreCase.never,
reverse=False, result_cb=None):
# Don't go to next entry on duplicate search
if self.text == text and self.search_displayed:
log.webview.debug("Ignoring duplicate search request"
@ -423,7 +421,7 @@ class WebEngineScroller(browsertab.AbstractScroller):
def _repeated_key_press(self, key, count=1, modifier=Qt.NoModifier):
"""Send count fake key presses to this scroller's WebEngineTab."""
for _ in range(min(count, 1000)):
self._tab.key_press(key, modifier)
self._tab.fake_key_press(key, modifier)
@pyqtSlot(QPointF)
def _update_pos(self, pos):
@ -478,7 +476,7 @@ class WebEngineScroller(browsertab.AbstractScroller):
def to_anchor(self, name):
url = self._tab.url()
url.setFragment(name)
self._tab.openurl(url)
self._tab.load_url(url)
def delta(self, x=0, y=0):
self._tab.run_js_async(javascript.assemble('window', 'scrollBy', x, y))
@ -500,10 +498,10 @@ class WebEngineScroller(browsertab.AbstractScroller):
self._repeated_key_press(Qt.Key_Right, count)
def top(self):
self._tab.key_press(Qt.Key_Home)
self._tab.fake_key_press(Qt.Key_Home)
def bottom(self):
self._tab.key_press(Qt.Key_End)
self._tab.fake_key_press(Qt.Key_End)
def page_up(self, count=1):
self._repeated_key_press(Qt.Key_PageUp, count)
@ -518,25 +516,9 @@ class WebEngineScroller(browsertab.AbstractScroller):
return self._at_bottom
class WebEngineHistory(browsertab.AbstractHistory):
class WebEngineHistoryPrivate(browsertab.AbstractHistoryPrivate):
"""QtWebEngine implementations related to page history."""
def current_idx(self):
return self._history.currentItemIndex()
def can_go_back(self):
return self._history.canGoBack()
def can_go_forward(self):
return self._history.canGoForward()
def _item_at(self, i):
return self._history.itemAt(i)
def _go_to_item(self, item):
self._tab.predicted_navigation.emit(item.url())
self._history.goToItem(item)
"""History-related methods which are not part of the extension API."""
def serialize(self):
if not qtutils.version_check('5.9', compiled=False):
@ -551,11 +533,11 @@ class WebEngineHistory(browsertab.AbstractHistory):
return qtutils.serialize(self._history)
def deserialize(self, data):
return qtutils.deserialize(data, self._history)
qtutils.deserialize(data, self._history)
def load_items(self, items):
if items:
self._tab.predicted_navigation.emit(items[-1].url)
self._tab.before_load_started.emit(items[-1].url)
stream, _data, cur_data = tabhistory.serialize(items)
qtutils.deserialize_stream(stream, self._history)
@ -573,6 +555,37 @@ class WebEngineHistory(browsertab.AbstractHistory):
self._tab.load_finished.connect(_on_load_finished)
class WebEngineHistory(browsertab.AbstractHistory):
"""QtWebEngine implementations related to page history."""
def __init__(self, tab):
super().__init__(tab)
self.private_api = WebEngineHistoryPrivate(tab)
def __len__(self):
return len(self._history)
def __iter__(self):
return iter(self._history.items())
def current_idx(self):
return self._history.currentItemIndex()
def can_go_back(self):
return self._history.canGoBack()
def can_go_forward(self):
return self._history.canGoForward()
def _item_at(self, i):
return self._history.itemAt(i)
def _go_to_item(self, item):
self._tab.before_load_started.emit(item.url())
self._history.goToItem(item)
class WebEngineZoom(browsertab.AbstractZoom):
"""QtWebEngine implementations related to zooming."""
@ -585,19 +598,20 @@ class WebEngineElements(browsertab.AbstractElements):
"""QtWebEngine implemementations related to elements on the page."""
def _js_cb_multiple(self, callback, js_elems):
def _js_cb_multiple(self, callback, error_cb, js_elems):
"""Handle found elements coming from JS and call the real callback.
Args:
callback: The callback to call with the found elements.
Called with None if there was an error.
error_cb: The callback to call in case of an error.
js_elems: The elements serialized from javascript.
"""
if js_elems is None:
callback(None)
error_cb(webelem.Error("Unknown error while getting "
"elements"))
return
elif not js_elems['success']:
callback(webelem.Error(js_elems['error']))
error_cb(webelem.Error(js_elems['error']))
return
elems = []
@ -624,10 +638,11 @@ class WebEngineElements(browsertab.AbstractElements):
elem = webengineelem.WebEngineElement(js_elem, tab=self._tab)
callback(elem)
def find_css(self, selector, callback, *, only_visible=False):
def find_css(self, selector, callback, error_cb, *,
only_visible=False):
js_code = javascript.assemble('webelem', 'find_css', selector,
only_visible)
js_cb = functools.partial(self._js_cb_multiple, callback)
js_cb = functools.partial(self._js_cb_multiple, callback, error_cb)
self._tab.run_js_async(js_code, js_cb)
def find_id(self, elem_id, callback):
@ -670,8 +685,9 @@ class WebEngineAudio(browsertab.AbstractAudio):
self._tab.url_changed.connect(self._on_url_changed)
config.instance.changed.connect(self._on_config_changed)
def set_muted(self, muted: bool, override: bool = False):
def set_muted(self, muted: bool, override: bool = False) -> None:
self._overridden = override
assert self._widget is not None
page = self._widget.page()
page.setAudioMuted(muted)
@ -699,8 +715,6 @@ class _WebEnginePermissions(QObject):
"""Handling of various permission-related signals."""
_abort_questions = pyqtSignal()
def __init__(self, tab, parent=None):
super().__init__(parent)
self._tab = tab
@ -720,9 +734,6 @@ class _WebEnginePermissions(QObject):
page.registerProtocolHandlerRequested.connect(
self._on_register_protocol_handler_requested)
self._tab.shutting_down.connect(self._abort_questions)
self._tab.load_started.connect(self._abort_questions)
@pyqtSlot('QWebEngineFullScreenRequest')
def _on_fullscreen_requested(self, request):
request.accept()
@ -800,7 +811,7 @@ class _WebEnginePermissions(QObject):
question = shared.feature_permission(
url=url, option=options[feature], msg=messages[feature],
yes_action=yes_action, no_action=no_action,
abort_on=[self._abort_questions])
abort_on=[self._tab.abort_questions])
if question is not None:
page.featurePermissionRequestCanceled.connect(
@ -828,7 +839,7 @@ class _WebEnginePermissions(QObject):
option='content.persistent_storage',
msg='use {} of persistent storage'.format(size),
yes_action=request.accept, no_action=request.reject,
abort_on=[self._abort_questions],
abort_on=[self._tab.abort_questions],
blocking=True)
def _on_register_protocol_handler_requested(self, request):
@ -837,7 +848,7 @@ class _WebEnginePermissions(QObject):
option='content.register_protocol_handler',
msg='open all {} links'.format(request.scheme()),
yes_action=request.accept, no_action=request.reject,
abort_on=[self._abort_questions],
abort_on=[self._tab.abort_questions],
blocking=True)
@ -928,10 +939,14 @@ class _WebEngineScripts(QObject):
utils.read_file('javascript/webelem.js'),
utils.read_file('javascript/caret.js'),
)
self._inject_early_js('js',
utils.read_file('javascript/print.js'),
subframes=True,
world=QWebEngineScript.MainWorld)
if not qtutils.version_check('5.12'):
# WORKAROUND for Qt versions < 5.12 not exposing window.print().
# Qt 5.12 has a printRequested() signal so we don't need this hack
# anymore.
self._inject_early_js('js',
utils.read_file('javascript/print.js'),
subframes=True,
world=QWebEngineScript.MainWorld)
# FIXME:qtwebengine what about subframes=True?
self._inject_early_js('js', js_code, subframes=True)
self._init_stylesheet()
@ -1048,6 +1063,28 @@ class _WebEngineScripts(QObject):
page_scripts.insert(new_script)
class WebEngineTabPrivate(browsertab.AbstractTabPrivate):
"""QtWebEngine-related methods which aren't part of the public API."""
def networkaccessmanager(self):
return None
def user_agent(self):
return None
def clear_ssl_errors(self):
raise browsertab.UnsupportedOperationError
def event_target(self):
return self._widget.render_widget()
def shutdown(self):
self._tab.shutting_down.emit()
self._tab.action.exit_fullscreen()
self._widget.shutdown()
class WebEngineTab(browsertab.AbstractTab):
"""A QtWebEngine tab in the browser.
@ -1055,14 +1092,16 @@ class WebEngineTab(browsertab.AbstractTab):
Signals:
_load_finished_fake:
Used in place of unreliable loadFinished
abort_questions: Emitted when a new load started or we're shutting
down.
"""
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-65223
_load_finished_fake = pyqtSignal(bool)
abort_questions = pyqtSignal()
def __init__(self, *, win_id, mode_manager, private, parent=None):
super().__init__(win_id=win_id, mode_manager=mode_manager,
private=private, parent=parent)
super().__init__(win_id=win_id, private=private, parent=parent)
widget = webview.WebEngineView(tabdata=self.data, win_id=win_id,
private=private)
self.history = WebEngineHistory(tab=self)
@ -1075,6 +1114,8 @@ class WebEngineTab(browsertab.AbstractTab):
self.elements = WebEngineElements(tab=self)
self.action = WebEngineAction(tab=self)
self.audio = WebEngineAudio(tab=self, parent=self)
self.private_api = WebEngineTabPrivate(mode_manager=mode_manager,
tab=self)
self._permissions = _WebEnginePermissions(tab=self, parent=self)
self._scripts = _WebEngineScripts(tab=self, parent=self)
# We're assigning settings in _set_widget
@ -1112,21 +1153,23 @@ class WebEngineTab(browsertab.AbstractTab):
self.zoom.set_factor(self._saved_zoom)
self._saved_zoom = None
def openurl(self, url, *, predict=True):
"""Open the given URL in this tab.
def load_url(self, url, *, emit_before_load_started=True):
"""Load the given URL in this tab.
Arguments:
url: The QUrl to open.
predict: If set to False, predicted_navigation is not emitted.
url: The QUrl to load.
emit_before_load_started: If set to False, before_load_started is
not emitted.
"""
if sip.isdeleted(self._widget):
# https://github.com/qutebrowser/qutebrowser/issues/3896
return
self._saved_zoom = self.zoom.factor()
self._openurl_prepare(url, predict=predict)
self._load_url_prepare(
url, emit_before_load_started=emit_before_load_started)
self._widget.load(url)
def url(self, requested=False):
def url(self, *, requested=False):
page = self._widget.page()
if requested:
return page.requestedUrl()
@ -1156,11 +1199,6 @@ class WebEngineTab(browsertab.AbstractTab):
else:
self._widget.page().runJavaScript(code, world_id, callback)
def shutdown(self):
self.shutting_down.emit()
self.action.exit_fullscreen()
self._widget.shutdown()
def reload(self, *, force=False):
if force:
action = QWebEnginePage.ReloadAndBypassCache
@ -1185,22 +1223,6 @@ class WebEngineTab(browsertab.AbstractTab):
# percent encoded content is 2 megabytes minus 30 bytes.
self._widget.setHtml(html, base_url)
def networkaccessmanager(self):
return None
def user_agent(self):
return None
def clear_ssl_errors(self):
raise browsertab.UnsupportedOperationError
def key_press(self, key, modifier=Qt.NoModifier):
press_evt = QKeyEvent(QEvent.KeyPress, key, modifier, 0, 0, 0)
release_evt = QKeyEvent(QEvent.KeyRelease, key, modifier,
0, 0, 0)
self.send_event(press_evt)
self.send_event(release_evt)
def _show_error_page(self, url, error):
"""Show an error page in the tab."""
log.misc.debug("Showing error page for {}".format(error))
@ -1237,7 +1259,7 @@ class WebEngineTab(browsertab.AbstractTab):
log.misc.debug("Ignoring invalid URL being added to history")
return
self.add_history_item.emit(url, requested_url, title)
self.history_item_triggered.emit(url, requested_url, title)
@pyqtSlot(QUrl, 'QAuthenticator*', 'QString')
def _on_proxy_authentication_required(self, url, authenticator,
@ -1249,33 +1271,34 @@ class WebEngineTab(browsertab.AbstractTab):
answer = message.ask(
title="Proxy authentication required", text=msg,
mode=usertypes.PromptMode.user_pwd,
abort_on=[self.shutting_down, self.load_started], url=urlstr)
abort_on=[self.abort_questions], url=urlstr)
if answer is not None:
authenticator.setUser(answer.user)
authenticator.setPassword(answer.password)
else:
try:
# pylint: disable=no-member, useless-suppression
sip.assign(authenticator, QAuthenticator())
# pylint: enable=no-member, useless-suppression
except AttributeError:
self._show_error_page(url, "Proxy authentication required")
@pyqtSlot(QUrl, 'QAuthenticator*')
def _on_authentication_required(self, url, authenticator):
log.network.debug("Authentication requested for {}, netrc_used {}"
.format(url.toDisplayString(), self.data.netrc_used))
netrc_success = False
if not self.data.netrc_used:
self.data.netrc_used = True
netrc_success = shared.netrc_authentication(url, authenticator)
if not netrc_success:
abort_on = [self.shutting_down, self.load_started]
answer = shared.authentication_required(url, authenticator,
abort_on)
log.network.debug("Asking for credentials")
answer = shared.authentication_required(
url, authenticator, abort_on=[self.abort_questions])
if not netrc_success and answer is None:
log.network.debug("Aborting auth")
try:
# pylint: disable=no-member, useless-suppression
sip.assign(authenticator, QAuthenticator())
# pylint: enable=no-member, useless-suppression
except AttributeError:
# WORKAROUND for
# https://www.riverbankcomputing.com/pipermail/pyqt/2016-December/038400.html
@ -1359,9 +1382,9 @@ class WebEngineTab(browsertab.AbstractTab):
log.config.debug(
"Loading {} again because of config change".format(
self._reload_url.toDisplayString()))
QTimer.singleShot(100, functools.partial(self.openurl,
self._reload_url,
predict=False))
QTimer.singleShot(100, functools.partial(
self.load_url, self._reload_url,
emit_before_load_started=False))
self._reload_url = None
if not qtutils.version_check('5.10', compiled=False):
@ -1380,7 +1403,7 @@ class WebEngineTab(browsertab.AbstractTab):
if error.is_overridable():
error.ignore = shared.ignore_certificate_errors(
url, [error], abort_on=[self.shutting_down, self.load_started])
url, [error], abort_on=[self.abort_questions])
else:
log.webview.error("Non-overridable certificate error: "
"{}".format(error))
@ -1400,24 +1423,29 @@ class WebEngineTab(browsertab.AbstractTab):
self._show_error_page(url, str(error))
@pyqtSlot(QUrl)
def _on_predicted_navigation(self, url):
"""If we know we're going to visit an URL soon, change the settings.
def _on_before_load_started(self, url):
"""If we know we're going to visit a URL soon, change the settings.
This is a WORKAROUND for https://bugreports.qt.io/browse/QTBUG-66656
"""
super()._on_predicted_navigation(url)
super()._on_before_load_started(url)
if not qtutils.version_check('5.11.1', compiled=False):
self.settings.update_for_url(url)
@pyqtSlot()
def _on_print_requested(self):
"""Slot for window.print() in JS."""
try:
self.printing.show_dialog()
except browsertab.WebTabError as e:
message.error(str(e))
@pyqtSlot(usertypes.NavigationRequest)
def _on_navigation_request(self, navigation):
super()._on_navigation_request(navigation)
if navigation.url == QUrl('qute://print'):
try:
self.printing.show_dialog()
except browsertab.WebTabError as e:
message.error(str(e))
self._on_print_requested()
navigation.accepted = False
if not navigation.accepted or not navigation.is_main_frame:
@ -1449,6 +1477,37 @@ class WebEngineTab(browsertab.AbstractTab):
if reload_needed:
self._reload_url = navigation.url
def _on_select_client_certificate(self, selection):
"""Handle client certificates.
Currently, we simply pick the first available certificate and show an
additional note if there are multiple matches.
"""
certificate = selection.certificates()[0]
text = ('<b>Subject:</b> {subj}<br/>'
'<b>Issuer:</b> {issuer}<br/>'
'<b>Serial:</b> {serial}'.format(
subj=html_utils.escape(certificate.subjectDisplayName()),
issuer=html_utils.escape(certificate.issuerDisplayName()),
serial=bytes(certificate.serialNumber()).decode('ascii')))
if len(selection.certificates()) > 1:
text += ('<br/><br/><b>Note:</b> Multiple matching certificates '
'were found, but certificate selection is not '
'implemented yet!')
urlstr = selection.host().host()
present = message.ask(
title='Present client certificate to {}?'.format(urlstr),
text=text,
mode=usertypes.PromptMode.yesno,
abort_on=[self.abort_questions],
url=urlstr)
if present:
selection.select(certificate)
else:
selection.selectNone()
def _connect_signals(self):
view = self._widget
page = view.page()
@ -1464,6 +1523,11 @@ class WebEngineTab(browsertab.AbstractTab):
page.contentsSizeChanged.connect(self.contents_size_changed)
page.navigation_request.connect(self._on_navigation_request)
if qtutils.version_check('5.12'):
page.printRequested.connect(self._on_print_requested)
page.selectClientCertificate.connect(
self._on_select_client_certificate)
view.titleChanged.connect(self.title_changed)
view.urlChanged.connect(self._on_url_changed)
view.renderProcessTerminated.connect(
@ -1483,12 +1547,11 @@ class WebEngineTab(browsertab.AbstractTab):
page.loadFinished.connect(self._restore_zoom)
page.loadFinished.connect(self._on_load_finished)
self.predicted_navigation.connect(self._on_predicted_navigation)
self.before_load_started.connect(self._on_before_load_started)
self.shutting_down.connect(self.abort_questions)
self.load_started.connect(self.abort_questions)
# pylint: disable=protected-access
self.audio._connect_signals()
self._permissions.connect_signals()
self._scripts.connect_signals()
def event_target(self):
return self._widget.render_widget()

View File

@ -64,6 +64,8 @@ class WebEngineView(QWebEngineView):
Normally, this would always be the focusProxy().
However, it sometimes isn't, so we use this as a WORKAROUND for
https://bugreports.qt.io/browse/QTBUG-68727
This got introduced in Qt 5.11.0 and fixed in 5.12.0.
"""
if 'lost-focusproxy' not in objreg.get('args').debug_flags:
proxy = self.focusProxy()
@ -240,7 +242,7 @@ class WebEnginePage(QWebEnginePage):
def acceptNavigationRequest(self,
url: QUrl,
typ: QWebEnginePage.NavigationType,
is_main_frame: bool):
is_main_frame: bool) -> bool:
"""Override acceptNavigationRequest to forward it to the tab API."""
type_map = {
QWebEnginePage.NavigationTypeLinkClicked:

View File

@ -39,6 +39,7 @@ from PyQt5.QtCore import QUrl
from qutebrowser.browser import downloads
from qutebrowser.browser.webkit import webkitelem
from qutebrowser.utils import log, objreg, message, usertypes, utils, urlutils
from qutebrowser.extensions import interceptors
@attr.s
@ -354,8 +355,9 @@ class _Downloader:
# qute, see the comments/discussion on
# https://github.com/qutebrowser/qutebrowser/pull/962#discussion_r40256987
# and https://github.com/qutebrowser/qutebrowser/issues/1053
host_blocker = objreg.get('host-blocker')
if host_blocker.is_blocked(url):
request = interceptors.Request(first_party_url=None, request_url=url)
interceptors.run(request)
if request.is_blocked:
log.downloads.debug("Skipping {}, host-blocked".format(url))
# We still need an empty file in the output, QWebView can be pretty
# picky about displaying a file correctly when not all assets are
@ -516,7 +518,6 @@ class _NoCloseBytesIO(io.BytesIO):
def close(self):
"""Do nothing."""
pass
def actual_close(self):
"""Close the stream."""

View File

@ -21,6 +21,7 @@
import collections
import html
import typing # pylint: disable=unused-import
import attr
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, QCoreApplication, QUrl,
@ -28,16 +29,23 @@ from PyQt5.QtCore import (pyqtSlot, pyqtSignal, QCoreApplication, QUrl,
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkReply, QSslSocket
from qutebrowser.config import config
MYPY = False
if MYPY:
# pylint can't interpret type comments with Python 3.7
# pylint: disable=unused-import,useless-suppression
from qutebrowser.mainwindow import prompt
from qutebrowser.utils import (message, log, usertypes, utils, objreg,
urlutils, debug)
from qutebrowser.browser import shared
from qutebrowser.extensions import interceptors
from qutebrowser.browser.webkit import certificateerror
from qutebrowser.browser.webkit.network import (webkitqutescheme, networkreply,
filescheme)
HOSTBLOCK_ERROR_STRING = '%HOSTBLOCK%'
_proxy_auth_cache = {}
_proxy_auth_cache = {} # type: typing.Dict[ProxyId, prompt.AuthInfo]
@attr.s(frozen=True)
@ -275,14 +283,19 @@ class NetworkManager(QNetworkAccessManager):
@pyqtSlot('QNetworkReply*', 'QAuthenticator*')
def on_authentication_required(self, reply, authenticator):
"""Called when a website needs authentication."""
url = reply.url()
log.network.debug("Authentication requested for {}, netrc_used {}"
.format(url.toDisplayString(), self.netrc_used))
netrc_success = False
if not self.netrc_used:
self.netrc_used = True
netrc_success = shared.netrc_authentication(reply.url(),
authenticator)
netrc_success = shared.netrc_authentication(url, authenticator)
if not netrc_success:
log.network.debug("Asking for credentials")
abort_on = self._get_abort_signals(reply)
shared.authentication_required(reply.url(), authenticator,
shared.authentication_required(url, authenticator,
abort_on=abort_on)
@pyqtSlot('QNetworkProxy', 'QAuthenticator*')
@ -290,9 +303,9 @@ class NetworkManager(QNetworkAccessManager):
"""Called when a proxy needs authentication."""
proxy_id = ProxyId(proxy.type(), proxy.hostName(), proxy.port())
if proxy_id in _proxy_auth_cache:
user, password = _proxy_auth_cache[proxy_id]
authenticator.setUser(user)
authenticator.setPassword(password)
authinfo = _proxy_auth_cache[proxy_id]
authenticator.setUser(authinfo.user)
authenticator.setPassword(authinfo.password)
else:
msg = '<b>{}</b> says:<br/>{}'.format(
html.escape(proxy.hostName()),
@ -393,10 +406,10 @@ class NetworkManager(QNetworkAccessManager):
# the webpage shutdown here.
current_url = QUrl()
host_blocker = objreg.get('host-blocker')
if host_blocker.is_blocked(req.url(), current_url):
log.webview.info("Request to {} blocked by host blocker.".format(
req.url().host()))
request = interceptors.Request(first_party_url=current_url,
request_url=req.url())
interceptors.run(request)
if request.is_blocked:
return networkreply.ErrorNetworkReply(
req, HOSTBLOCK_ERROR_STRING, QNetworkReply.ContentAccessDenied,
self)

View File

@ -34,8 +34,7 @@ class FixedDataNetworkReply(QNetworkReply):
"""QNetworkReply subclass for fixed data."""
def __init__(self, request, fileData, mimeType, # noqa: N803
parent=None):
def __init__(self, request, fileData, mimeType, parent=None): # noqa: N803
"""Constructor.
Args:
@ -67,7 +66,6 @@ class FixedDataNetworkReply(QNetworkReply):
@pyqtSlot()
def abort(self):
"""Abort the operation."""
pass
def bytesAvailable(self):
"""Determine the bytes available for being read.
@ -123,7 +121,6 @@ class ErrorNetworkReply(QNetworkReply):
def abort(self):
"""Do nothing since it's a fake reply."""
pass
def bytesAvailable(self):
"""We always have 0 bytes available."""
@ -151,7 +148,6 @@ class RedirectNetworkReply(QNetworkReply):
def abort(self):
"""Called when there's e.g. a redirection limit."""
pass
def readData(self, _maxlen):
return bytes()

View File

@ -19,26 +19,31 @@
"""QtWebKit specific part of the web element API."""
import typing
from PyQt5.QtCore import QRect
from PyQt5.QtWebKit import QWebElement, QWebSettings
from PyQt5.QtWebKitWidgets import QWebFrame
from qutebrowser.config import config
from qutebrowser.utils import log, utils, javascript
from qutebrowser.utils import log, utils, javascript, usertypes
from qutebrowser.browser import webelem
MYPY = False
if MYPY:
# pylint: disable=unused-import,useless-suppression
from qutebrowser.browser.webkit import webkittab
class IsNullError(webelem.Error):
"""Gets raised by WebKitElement if an element is null."""
pass
class WebKitElement(webelem.AbstractWebElement):
"""A wrapper around a QWebElement."""
def __init__(self, elem, tab):
def __init__(self, elem: QWebElement, tab: 'webkittab.WebKitTab') -> None:
super().__init__(tab)
if isinstance(elem, self.__class__):
raise TypeError("Trying to wrap a wrapper!")
@ -46,90 +51,94 @@ class WebKitElement(webelem.AbstractWebElement):
raise IsNullError('{} is a null element!'.format(elem))
self._elem = elem
def __str__(self):
def __str__(self) -> str:
self._check_vanished()
return self._elem.toPlainText()
def __eq__(self, other):
def __eq__(self, other: object) -> bool:
if not isinstance(other, WebKitElement):
return NotImplemented
return self._elem == other._elem # pylint: disable=protected-access
def __getitem__(self, key):
def __getitem__(self, key: str) -> str:
self._check_vanished()
if key not in self:
raise KeyError(key)
return self._elem.attribute(key)
def __setitem__(self, key, val):
def __setitem__(self, key: str, val: str) -> None:
self._check_vanished()
self._elem.setAttribute(key, val)
def __delitem__(self, key):
def __delitem__(self, key: str) -> None:
self._check_vanished()
if key not in self:
raise KeyError(key)
self._elem.removeAttribute(key)
def __contains__(self, key):
def __contains__(self, key: object) -> bool:
assert isinstance(key, str)
self._check_vanished()
return self._elem.hasAttribute(key)
def __iter__(self):
def __iter__(self) -> typing.Iterator[str]:
self._check_vanished()
yield from self._elem.attributeNames()
def __len__(self):
def __len__(self) -> int:
self._check_vanished()
return len(self._elem.attributeNames())
def _check_vanished(self):
def _check_vanished(self) -> None:
"""Raise an exception if the element vanished (is null)."""
if self._elem.isNull():
raise IsNullError('Element {} vanished!'.format(self._elem))
def has_frame(self):
def has_frame(self) -> bool:
self._check_vanished()
return self._elem.webFrame() is not None
def geometry(self):
def geometry(self) -> QRect:
self._check_vanished()
return self._elem.geometry()
def classes(self):
def classes(self) -> typing.List[str]:
self._check_vanished()
return self._elem.classes()
def tag_name(self):
def tag_name(self) -> str:
"""Get the tag name for the current element."""
self._check_vanished()
return self._elem.tagName().lower()
def outer_xml(self):
def outer_xml(self) -> str:
"""Get the full HTML representation of this element."""
self._check_vanished()
return self._elem.toOuterXml()
def value(self):
def value(self) -> webelem.JsValueType:
self._check_vanished()
val = self._elem.evaluateJavaScript('this.value')
assert isinstance(val, (int, float, str, type(None))), val
return val
def set_value(self, value):
def set_value(self, value: webelem.JsValueType) -> None:
self._check_vanished()
if self._tab.is_deleted():
raise webelem.OrphanedError("Tab containing element vanished")
if self.is_content_editable():
log.webelem.debug("Filling {!r} via set_text.".format(self))
assert isinstance(value, str)
self._elem.setPlainText(value)
else:
log.webelem.debug("Filling {!r} via javascript.".format(self))
value = javascript.to_js(value)
self._elem.evaluateJavaScript("this.value={}".format(value))
def dispatch_event(self, event, bubbles=False,
cancelable=False, composed=False):
def dispatch_event(self, event: str,
bubbles: bool = False,
cancelable: bool = False,
composed: bool = False) -> None:
self._check_vanished()
log.webelem.debug("Firing event on {!r} via javascript.".format(self))
self._elem.evaluateJavaScript(
@ -140,7 +149,7 @@ class WebKitElement(webelem.AbstractWebElement):
javascript.to_js(cancelable),
javascript.to_js(composed)))
def caret_position(self):
def caret_position(self) -> int:
"""Get the text caret position for the current element."""
self._check_vanished()
pos = self._elem.evaluateJavaScript('this.selectionStart')
@ -148,7 +157,7 @@ class WebKitElement(webelem.AbstractWebElement):
return 0
return int(pos)
def insert_text(self, text):
def insert_text(self, text: str) -> None:
self._check_vanished()
if not self.is_editable(strict=True):
raise webelem.Error("Element is not editable!")
@ -160,7 +169,7 @@ class WebKitElement(webelem.AbstractWebElement):
this.dispatchEvent(event);
""".format(javascript.to_js(text)))
def _parent(self):
def _parent(self) -> typing.Optional['WebKitElement']:
"""Get the parent element of this element."""
self._check_vanished()
elem = self._elem.parent()
@ -168,7 +177,7 @@ class WebKitElement(webelem.AbstractWebElement):
return None
return WebKitElement(elem, tab=self._tab)
def _rect_on_view_js(self):
def _rect_on_view_js(self) -> typing.Optional[QRect]:
"""Javascript implementation for rect_on_view."""
# FIXME:qtwebengine maybe we can reuse this?
rects = self._elem.evaluateJavaScript("this.getClientRects()")
@ -180,8 +189,8 @@ class WebKitElement(webelem.AbstractWebElement):
return None
text = utils.compact_text(self._elem.toOuterXml(), 500)
log.webelem.vdebug("Client rectangles of element '{}': {}".format(
text, rects))
log.webelem.vdebug( # type: ignore
"Client rectangles of element '{}': {}".format(text, rects))
for i in range(int(rects.get("length", 0))):
rect = rects[str(i)]
@ -206,7 +215,8 @@ class WebKitElement(webelem.AbstractWebElement):
return None
def _rect_on_view_python(self, elem_geometry):
def _rect_on_view_python(self,
elem_geometry: typing.Optional[QRect]) -> QRect:
"""Python implementation for rect_on_view."""
if elem_geometry is None:
geometry = self._elem.geometry()
@ -220,7 +230,8 @@ class WebKitElement(webelem.AbstractWebElement):
frame = frame.parentFrame()
return rect
def rect_on_view(self, *, elem_geometry=None, no_js=False):
def rect_on_view(self, *, elem_geometry: QRect = None,
no_js: bool = False) -> QRect:
"""Get the geometry of the element relative to the webview.
Uses the getClientRects() JavaScript method to obtain the collection of
@ -250,7 +261,7 @@ class WebKitElement(webelem.AbstractWebElement):
# No suitable rects found via JS, try via the QWebElement API
return self._rect_on_view_python(elem_geometry)
def _is_visible(self, mainframe):
def _is_visible(self, mainframe: QWebFrame) -> bool:
"""Check if the given element is visible in the given frame.
This is not public API because it can't be implemented easily here with
@ -302,8 +313,8 @@ class WebKitElement(webelem.AbstractWebElement):
visible_in_frame = visible_on_screen
return all([visible_on_screen, visible_in_frame])
def remove_blank_target(self):
elem = self
def remove_blank_target(self) -> None:
elem = self # type: typing.Optional[WebKitElement]
for _ in range(5):
if elem is None:
break
@ -313,14 +324,14 @@ class WebKitElement(webelem.AbstractWebElement):
break
elem = elem._parent() # pylint: disable=protected-access
def _move_text_cursor(self):
def _move_text_cursor(self) -> None:
if self.is_text_input() and self.is_editable():
self._tab.caret.move_to_end_of_document()
def _requires_user_interaction(self):
def _requires_user_interaction(self) -> bool:
return False
def _click_editable(self, click_target):
def _click_editable(self, click_target: usertypes.ClickTarget) -> None:
ok = self._elem.evaluateJavaScript('this.focus(); true;')
if ok:
self._move_text_cursor()
@ -328,7 +339,7 @@ class WebKitElement(webelem.AbstractWebElement):
log.webelem.debug("Failed to focus via JS, falling back to event")
self._click_fake_event(click_target)
def _click_js(self, click_target):
def _click_js(self, click_target: usertypes.ClickTarget) -> None:
settings = QWebSettings.globalSettings()
attribute = QWebSettings.JavascriptCanOpenWindows
could_open_windows = settings.testAttribute(attribute)
@ -339,12 +350,12 @@ class WebKitElement(webelem.AbstractWebElement):
log.webelem.debug("Failed to click via JS, falling back to event")
self._click_fake_event(click_target)
def _click_fake_event(self, click_target):
def _click_fake_event(self, click_target: usertypes.ClickTarget) -> None:
self._tab.data.override_target = click_target
super()._click_fake_event(click_target)
def get_child_frames(startframe):
def get_child_frames(startframe: QWebFrame) -> typing.List[QWebFrame]:
"""Get all children recursively of a given QWebFrame.
Loosely based on http://blog.nextgenetics.net/?e=64
@ -358,7 +369,7 @@ def get_child_frames(startframe):
results = []
frames = [startframe]
while frames:
new_frames = []
new_frames = [] # type: typing.List[QWebFrame]
for frame in frames:
results.append(frame)
new_frames += frame.childFrames()

View File

@ -41,7 +41,6 @@ class WebHistoryInterface(QWebHistoryInterface):
def addHistoryEntry(self, url_string):
"""Required for a QWebHistoryInterface impl, obsoleted by add_url."""
pass
@functools.lru_cache(maxsize=32768)
def historyContains(self, url_string):

View File

@ -23,9 +23,8 @@ import re
import functools
import xml.etree.ElementTree
from PyQt5.QtCore import (pyqtSlot, Qt, QEvent, QUrl, QPoint, QTimer, QSizeF,
QSize)
from PyQt5.QtGui import QKeyEvent, QIcon
from PyQt5.QtCore import pyqtSlot, Qt, QUrl, QPoint, QTimer, QSizeF, QSize
from PyQt5.QtGui import QIcon
from PyQt5.QtWebKitWidgets import QWebPage, QWebFrame
from PyQt5.QtWebKit import QWebSettings
from PyQt5.QtPrintSupport import QPrinter
@ -125,8 +124,8 @@ class WebKitSearch(browsertab.AbstractSearch):
self._widget.findText('')
self._widget.findText('', QWebPage.HighlightAllOccurrences)
def search(self, text, *, ignore_case='never', reverse=False,
result_cb=None):
def search(self, text, *, ignore_case=usertypes.IgnoreCase.never,
reverse=False, result_cb=None):
# Don't go to next entry on duplicate search
if self.text == text and self.search_displayed:
log.webview.debug("Ignoring duplicate search request"
@ -391,7 +390,7 @@ class WebKitCaret(browsertab.AbstractCaret):
if tab:
self._tab.new_tab_requested.emit(url)
else:
self._tab.openurl(url)
self._tab.load_url(url)
def follow_selected(self, *, tab=False):
try:
@ -474,7 +473,7 @@ class WebKitScroller(browsertab.AbstractScroller):
if (getter is not None and
frame.scrollBarValue(direction) == getter(direction)):
return
self._tab.key_press(key)
self._tab.fake_key_press(key)
def up(self, count=1):
self._key_press(Qt.Key_Up, count, 'scrollBarMinimum', Qt.Vertical)
@ -509,35 +508,19 @@ class WebKitScroller(browsertab.AbstractScroller):
return self.pos_px().y() >= frame.scrollBarMaximum(Qt.Vertical)
class WebKitHistory(browsertab.AbstractHistory):
class WebKitHistoryPrivate(browsertab.AbstractHistoryPrivate):
"""QtWebKit implementations related to page history."""
def current_idx(self):
return self._history.currentItemIndex()
def can_go_back(self):
return self._history.canGoBack()
def can_go_forward(self):
return self._history.canGoForward()
def _item_at(self, i):
return self._history.itemAt(i)
def _go_to_item(self, item):
self._tab.predicted_navigation.emit(item.url())
self._history.goToItem(item)
"""History-related methods which are not part of the extension API."""
def serialize(self):
return qtutils.serialize(self._history)
def deserialize(self, data):
return qtutils.deserialize(data, self._history)
qtutils.deserialize(data, self._history)
def load_items(self, items):
if items:
self._tab.predicted_navigation.emit(items[-1].url)
self._tab.before_load_started.emit(items[-1].url)
stream, _data, user_data = tabhistory.serialize(items)
qtutils.deserialize_stream(stream, self._history)
@ -553,11 +536,43 @@ class WebKitHistory(browsertab.AbstractHistory):
self._tab.scroller.to_point, cur_data['scroll-pos']))
class WebKitHistory(browsertab.AbstractHistory):
"""QtWebKit implementations related to page history."""
def __init__(self, tab):
super().__init__(tab)
self.private_api = WebKitHistoryPrivate(tab)
def __len__(self):
return len(self._history)
def __iter__(self):
return iter(self._history.items())
def current_idx(self):
return self._history.currentItemIndex()
def can_go_back(self):
return self._history.canGoBack()
def can_go_forward(self):
return self._history.canGoForward()
def _item_at(self, i):
return self._history.itemAt(i)
def _go_to_item(self, item):
self._tab.before_load_started.emit(item.url())
self._history.goToItem(item)
class WebKitElements(browsertab.AbstractElements):
"""QtWebKit implemementations related to elements on the page."""
def find_css(self, selector, callback, *, only_visible=False):
def find_css(self, selector, callback, error_cb, *, only_visible=False):
utils.unused(error_cb)
mainframe = self._widget.page().mainFrame()
if mainframe is None:
raise browsertab.WebTabError("No frame focused!")
@ -586,7 +601,7 @@ class WebKitElements(browsertab.AbstractElements):
# Escape non-alphanumeric characters in the selector
# https://www.w3.org/TR/CSS2/syndata.html#value-def-identifier
elem_id = re.sub(r'[^a-zA-Z0-9_-]', r'\\\g<0>', elem_id)
self.find_css('#' + elem_id, find_id_cb)
self.find_css('#' + elem_id, find_id_cb, error_cb=lambda exc: None)
def find_focused(self, callback):
frame = self._widget.page().currentFrame()
@ -641,7 +656,7 @@ class WebKitAudio(browsertab.AbstractAudio):
"""Dummy handling of audio status for QtWebKit."""
def set_muted(self, muted: bool, override: bool = False):
def set_muted(self, muted: bool, override: bool = False) -> None:
raise browsertab.WebTabError('Muting is not supported on QtWebKit!')
def is_muted(self):
@ -651,13 +666,33 @@ class WebKitAudio(browsertab.AbstractAudio):
return False
class WebKitTabPrivate(browsertab.AbstractTabPrivate):
"""QtWebKit-related methods which aren't part of the public API."""
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()
def event_target(self):
return self._widget
def shutdown(self):
self._widget.shutdown()
class WebKitTab(browsertab.AbstractTab):
"""A QtWebKit tab in the browser."""
def __init__(self, *, win_id, mode_manager, private, parent=None):
super().__init__(win_id=win_id, mode_manager=mode_manager,
private=private, parent=parent)
super().__init__(win_id=win_id, private=private, parent=parent)
widget = webview.WebView(win_id=win_id, tab_id=self.tab_id,
private=private, tab=self)
if private:
@ -672,6 +707,8 @@ class WebKitTab(browsertab.AbstractTab):
self.elements = WebKitElements(tab=self)
self.action = WebKitAction(tab=self)
self.audio = WebKitAudio(tab=self, parent=self)
self.private_api = WebKitTabPrivate(mode_manager=mode_manager,
tab=self)
# We're assigning settings in _set_widget
self.settings = webkitsettings.WebKitSettings(settings=None)
self._set_widget(widget)
@ -685,11 +722,12 @@ class WebKitTab(browsertab.AbstractTab):
settings = widget.settings()
settings.setAttribute(QWebSettings.PrivateBrowsingEnabled, True)
def openurl(self, url, *, predict=True):
self._openurl_prepare(url, predict=predict)
self._widget.openurl(url)
def load_url(self, url, *, emit_before_load_started=True):
self._load_url_prepare(
url, emit_before_load_started=emit_before_load_started)
self._widget.load(url)
def url(self, requested=False):
def url(self, *, requested=False):
frame = self._widget.page().mainFrame()
if requested:
return frame.requestedUrl()
@ -714,9 +752,6 @@ class WebKitTab(browsertab.AbstractTab):
def icon(self):
return self._widget.icon()
def shutdown(self):
self._widget.shutdown()
def reload(self, *, force=False):
if force:
action = QWebPage.ReloadAndBypassCache
@ -730,36 +765,20 @@ class WebKitTab(browsertab.AbstractTab):
def title(self):
return self._widget.title()
def clear_ssl_errors(self):
self.networkaccessmanager().clear_all_ssl_errors()
def key_press(self, key, modifier=Qt.NoModifier):
press_evt = QKeyEvent(QEvent.KeyPress, key, modifier, 0, 0, 0)
release_evt = QKeyEvent(QEvent.KeyRelease, key, modifier,
0, 0, 0)
self.send_event(press_evt)
self.send_event(release_evt)
@pyqtSlot()
def _on_history_trigger(self):
url = self.url()
requested_url = self.url(requested=True)
self.add_history_item.emit(url, requested_url, self.title())
self.history_item_triggered.emit(url, requested_url, self.title())
def set_html(self, html, base_url=QUrl()):
self._widget.setHtml(html, base_url)
def networkaccessmanager(self):
return self._widget.page().networkAccessManager()
def user_agent(self):
page = self._widget.page()
return page.userAgentForUrl(self.url())
@pyqtSlot()
def _on_load_started(self):
super()._on_load_started()
self.networkaccessmanager().netrc_used = False
nam = self._widget.page().networkAccessManager()
nam.netrc_used = False
# Make sure the icon is cleared when navigating to a page without one.
self.icon_changed.emit(QIcon())
@ -811,7 +830,7 @@ class WebKitTab(browsertab.AbstractTab):
if (navigation.navigation_type == navigation.Type.link_clicked and
target != usertypes.ClickTarget.normal):
tab = shared.get_tab(self.win_id, target)
tab.openurl(navigation.url)
tab.load_url(navigation.url)
self.data.open_target = usertypes.ClickTarget.normal
navigation.accepted = False
@ -841,6 +860,3 @@ class WebKitTab(browsertab.AbstractTab):
frame.contentsSizeChanged.connect(self._on_contents_size_changed)
frame.initialLayoutCompleted.connect(self._on_history_trigger)
page.navigation_request.connect(self._on_navigation_request)
def event_target(self):
return self._widget

View File

@ -469,7 +469,7 @@ class BrowserPage(QWebPage):
def acceptNavigationRequest(self,
frame: QWebFrame,
request: QNetworkRequest,
typ: QWebPage.NavigationType):
typ: QWebPage.NavigationType) -> bool:
"""Override acceptNavigationRequest to handle clicked links.
Setting linkDelegationPolicy to DelegateAllLinks and using a slot bound

View File

@ -118,14 +118,6 @@ class WebView(QWebView):
self.stop()
self.page().shutdown()
def openurl(self, url):
"""Open a URL in the browser.
Args:
url: The URL to load as QUrl
"""
self.load(url)
def createWindow(self, wintype):
"""Called by Qt when a page wants to create a new window.

View File

@ -28,26 +28,15 @@ class Error(Exception):
"""Base class for all cmdexc errors."""
class CommandError(Error):
"""Raised when a command encounters an error while running."""
pass
class NoSuchCommandError(Error):
"""Raised when a command wasn't found."""
pass
class ArgumentTypeError(Error):
"""Raised when an argument had an invalid type."""
pass
class PrerequisitesError(Error):
@ -56,5 +45,3 @@ class PrerequisitesError(Error):
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.
"""
pass

View File

@ -1,147 +0,0 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-2018 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/>.
"""Contains various command utils and a global command dict.
Module attributes:
cmd_dict: A mapping from command-strings to command objects.
"""
import inspect
from qutebrowser.utils import qtutils, log
from qutebrowser.commands import command, cmdexc
cmd_dict = {}
def check_overflow(arg, ctype):
"""Check if the given argument is in bounds for the given type.
Args:
arg: The argument to check
ctype: The C/Qt type to check as a string.
"""
try:
qtutils.check_overflow(arg, ctype)
except OverflowError:
raise cmdexc.CommandError(
"Numeric argument is too large for internal {} "
"representation.".format(ctype))
def check_exclusive(flags, names):
"""Check if only one flag is set with exclusive flags.
Raise a CommandError if not.
Args:
flags: An iterable of booleans to check.
names: An iterable of flag names for the error message.
"""
if sum(1 for e in flags if e) > 1:
argstr = '/'.join('-' + e for e in names)
raise cmdexc.CommandError("Only one of {} can be given!".format(
argstr))
class register: # noqa: N801,N806 pylint: disable=invalid-name
"""Decorator to register a new command handler.
This could also be a function, but as a class (with a "wrong" name) it's
much cleaner to implement.
Attributes:
_instance: The object from the object registry to be used as "self".
_name: The name (as string) or names (as list) of the command.
_kwargs: The arguments to pass to Command.
"""
def __init__(self, *, instance=None, name=None, **kwargs):
"""Save decorator arguments.
Gets called on parse-time with the decorator arguments.
Args:
See class attributes.
"""
self._instance = instance
self._name = name
self._kwargs = kwargs
def __call__(self, func):
"""Register the command before running the function.
Gets called when a function should be decorated.
Doesn't actually decorate anything, but creates a Command object and
registers it in the cmd_dict.
Args:
func: The function to be decorated.
Return:
The original function (unmodified).
"""
if self._name is None:
name = func.__name__.lower().replace('_', '-')
else:
assert isinstance(self._name, str), self._name
name = self._name
log.commands.vdebug("Registering command {} (from {}:{})".format(
name, func.__module__, func.__qualname__))
if name in cmd_dict:
raise ValueError("{} is already registered!".format(name))
cmd = command.Command(name=name, instance=self._instance,
handler=func, **self._kwargs)
cmd_dict[name] = cmd
return func
class argument: # noqa: N801,N806 pylint: disable=invalid-name
"""Decorator to customize an argument for @cmdutils.register.
This could also be a function, but as a class (with a "wrong" name) it's
much cleaner to implement.
Attributes:
_argname: The name of the argument to handle.
_kwargs: Keyword arguments, valid ArgInfo members
"""
def __init__(self, argname, **kwargs):
self._argname = argname
self._kwargs = kwargs
def __call__(self, func):
funcname = func.__name__
if self._argname not in inspect.signature(func).parameters:
raise ValueError("{} has no argument {}!".format(funcname,
self._argname))
if not hasattr(func, 'qute_args'):
func.qute_args = {}
elif func.qute_args is None:
raise ValueError("@cmdutils.argument got called above (after) "
"@cmdutils.register for {}!".format(funcname))
func.qute_args[self._argname] = command.ArgInfo(**self._kwargs)
return func

View File

@ -26,8 +26,9 @@ import typing
import attr
from qutebrowser.api import cmdutils
from qutebrowser.commands import cmdexc, argparser
from qutebrowser.utils import log, message, docutils, objreg, usertypes
from qutebrowser.utils import log, message, docutils, objreg, usertypes, utils
from qutebrowser.utils import debug as debug_utils
from qutebrowser.misc import objects
@ -37,18 +38,13 @@ class ArgInfo:
"""Information about an argument."""
win_id = attr.ib(False)
count = attr.ib(False)
value = attr.ib(None)
hide = attr.ib(False)
metavar = attr.ib(None)
flag = attr.ib(None)
completion = attr.ib(None)
choices = attr.ib(None)
def __attrs_post_init__(self):
if self.win_id and self.count:
raise TypeError("Argument marked as both count/win_id!")
class Command:
@ -75,6 +71,10 @@ class Command:
_scope: The scope to get _instance for in the object registry.
"""
# CommandValue values which need a count
COUNT_COMMAND_VALUES = [usertypes.CommandValue.count,
usertypes.CommandValue.count_tab]
def __init__(self, *, handler, name, instance=None, maxsplit=None,
modes=None, not_modes=None, debug=False, deprecated=False,
no_cmd_split=False, star_args_optional=False, scope='global',
@ -116,7 +116,6 @@ class Command:
self.parser.add_argument('-h', '--help', action=argparser.HelpAction,
default=argparser.SUPPRESS, nargs=0,
help=argparser.SUPPRESS)
self._check_func()
self.opt_args = collections.OrderedDict()
self.namespace = None
self._count = None
@ -130,6 +129,7 @@ class Command:
self._qute_args = getattr(self.handler, 'qute_args', {})
self.handler.qute_args = None
self._check_func()
self._inspect_func()
def _check_prerequisites(self, win_id):
@ -154,16 +154,21 @@ class Command:
def _check_func(self):
"""Make sure the function parameters don't violate any rules."""
signature = inspect.signature(self.handler)
if 'self' in signature.parameters and self._instance is None:
raise TypeError("{} is a class method, but instance was not "
"given!".format(self.name[0]))
if 'self' in signature.parameters:
if self._instance is None:
raise TypeError("{} is a class method, but instance was not "
"given!".format(self.name))
arg_info = self.get_arg_info(signature.parameters['self'])
if arg_info.value is not None:
raise TypeError("{}: Can't fill 'self' with value!"
.format(self.name))
elif 'self' not in signature.parameters and self._instance is not None:
raise TypeError("{} is not a class method, but instance was "
"given!".format(self.name[0]))
"given!".format(self.name))
elif any(param.kind == inspect.Parameter.VAR_KEYWORD
for param in signature.parameters.values()):
raise TypeError("{}: functions with varkw arguments are not "
"supported!".format(self.name[0]))
"supported!".format(self.name))
def get_arg_info(self, param):
"""Get an ArgInfo tuple for the given inspect.Parameter."""
@ -186,13 +191,18 @@ class Command:
True if the parameter is special, False otherwise.
"""
arg_info = self.get_arg_info(param)
if arg_info.count:
if arg_info.value is None:
return False
elif arg_info.value == usertypes.CommandValue.count:
if param.default is inspect.Parameter.empty:
raise TypeError("{}: handler has count parameter "
"without default!".format(self.name))
return True
elif arg_info.win_id:
elif isinstance(arg_info.value, usertypes.CommandValue):
return True
else:
raise TypeError("{}: Invalid value={!r} for argument '{}'!"
.format(self.name, arg_info.value, param.name))
return False
def _inspect_func(self):
@ -292,6 +302,8 @@ class Command:
name = argparser.arg_name(param.name)
arg_info = self.get_arg_info(param)
assert not arg_info.value, name
if arg_info.flag is not None:
shortname = arg_info.flag
else:
@ -321,75 +333,66 @@ class Command:
param: The inspect.Parameter to look at.
"""
arginfo = self.get_arg_info(param)
if param.annotation is not inspect.Parameter.empty:
if arginfo.value:
# Filled values are passed 1:1
return None
elif param.kind in [inspect.Parameter.VAR_POSITIONAL,
inspect.Parameter.VAR_KEYWORD]:
# For *args/**kwargs we only support strings
assert param.annotation in [inspect.Parameter.empty, str], param
return None
elif param.annotation is not inspect.Parameter.empty:
return param.annotation
elif param.default not in [None, inspect.Parameter.empty]:
return type(param.default)
elif arginfo.count or arginfo.win_id or param.kind in [
inspect.Parameter.VAR_POSITIONAL,
inspect.Parameter.VAR_KEYWORD]:
return None
else:
return str
def _get_self_arg(self, win_id, param, args):
"""Get the self argument for a function call.
Arguments:
win_id: The window id this command should be executed in.
param: The count parameter.
args: The positional argument list. Gets modified directly.
"""
assert param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD
if self._scope == 'global':
def _get_objreg(self, *, win_id, name, scope):
"""Get an object from the objreg."""
if scope == 'global':
tab_id = None
win_id = None
elif self._scope == 'tab':
elif scope == 'tab':
tab_id = 'current'
elif self._scope == 'window':
elif scope == 'window':
tab_id = None
else:
raise ValueError("Invalid scope {}!".format(self._scope))
obj = objreg.get(self._instance, scope=self._scope, window=win_id,
tab=tab_id)
args.append(obj)
raise ValueError("Invalid scope {}!".format(scope))
return objreg.get(name, scope=scope, window=win_id, tab=tab_id)
def _get_count_arg(self, param, args, kwargs):
"""Add the count argument to a function call.
def _add_special_arg(self, *, value, param, args, kwargs):
"""Add a special argument value to a function call.
Arguments:
param: The count parameter.
value: The value to add.
param: The parameter being filled.
args: The positional argument list. Gets modified directly.
kwargs: The keyword argument dict. Gets modified directly.
"""
if param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD:
if self._count is not None:
args.append(self._count)
else:
args.append(param.default)
args.append(value)
elif param.kind == inspect.Parameter.KEYWORD_ONLY:
if self._count is not None:
kwargs[param.name] = self._count
kwargs[param.name] = value
else:
raise TypeError("{}: invalid parameter type {} for argument "
"{!r}!".format(self.name, param.kind, param.name))
def _get_win_id_arg(self, win_id, param, args, kwargs):
"""Add the win_id argument to a function call.
def _add_count_tab(self, *, win_id, param, args, kwargs):
"""Add the count_tab widget argument."""
tabbed_browser = self._get_objreg(
win_id=win_id, name='tabbed-browser', scope='window')
Arguments:
win_id: The window ID to add.
param: The count parameter.
args: The positional argument list. Gets modified directly.
kwargs: The keyword argument dict. Gets modified directly.
"""
if param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD:
args.append(win_id)
elif param.kind == inspect.Parameter.KEYWORD_ONLY:
kwargs[param.name] = win_id
if self._count is None:
tab = tabbed_browser.widget.currentWidget()
elif 1 <= self._count <= tabbed_browser.widget.count():
cmdutils.check_overflow(self._count + 1, 'int')
tab = tabbed_browser.widget.widget(self._count - 1)
else:
raise TypeError("{}: invalid parameter type {} for argument "
"{!r}!".format(self.name, param.kind, param.name))
tab = None
self._add_special_arg(value=tab, param=param, args=args,
kwargs=kwargs)
def _get_param_value(self, param):
"""Get the converted value for an inspect.Parameter."""
@ -428,6 +431,55 @@ class Command:
return value
def _handle_special_call_arg(self, *, pos, param, win_id, args, kwargs):
"""Check whether the argument is special, and if so, fill it in.
Args:
pos: The position of the argument.
param: The argparse.Parameter.
win_id: The window ID the command is run in.
args/kwargs: The args/kwargs to fill.
Return:
True if it was a special arg, False otherwise.
"""
arg_info = self.get_arg_info(param)
if pos == 0 and self._instance is not None:
assert param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD
self_value = self._get_objreg(win_id=win_id, name=self._instance,
scope=self._scope)
self._add_special_arg(value=self_value, param=param,
args=args, kwargs=kwargs)
return True
elif arg_info.value == usertypes.CommandValue.count:
if self._count is None:
assert param.default is not inspect.Parameter.empty
value = param.default
else:
value = self._count
self._add_special_arg(value=value, param=param,
args=args, kwargs=kwargs)
return True
elif arg_info.value == usertypes.CommandValue.win_id:
self._add_special_arg(value=win_id, param=param,
args=args, kwargs=kwargs)
return True
elif arg_info.value == usertypes.CommandValue.cur_tab:
tab = self._get_objreg(win_id=win_id, name='tab', scope='tab')
self._add_special_arg(value=tab, param=param,
args=args, kwargs=kwargs)
return True
elif arg_info.value == usertypes.CommandValue.count_tab:
self._add_count_tab(win_id=win_id, param=param, args=args,
kwargs=kwargs)
return True
elif arg_info.value is None:
pass
else:
raise utils.Unreachable(arg_info)
return False
def _get_call_args(self, win_id):
"""Get arguments for a function call.
@ -442,20 +494,11 @@ class Command:
signature = inspect.signature(self.handler)
for i, param in enumerate(signature.parameters.values()):
arg_info = self.get_arg_info(param)
if i == 0 and self._instance is not None:
# Special case for 'self'.
self._get_self_arg(win_id, param, args)
continue
elif arg_info.count:
# Special case for count parameter.
self._get_count_arg(param, args, kwargs)
continue
# elif arg_info.win_id:
elif arg_info.win_id:
# Special case for win_id parameter.
self._get_win_id_arg(win_id, param, args, kwargs)
if self._handle_special_call_arg(pos=i, param=param,
win_id=win_id, args=args,
kwargs=kwargs):
continue
value = self._get_param_value(param)
if param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD:
args.append(value)
@ -520,4 +563,14 @@ class Command:
def takes_count(self):
"""Return true iff this command can take a count argument."""
return any(arg.count for arg in self._qute_args)
return any(info.value in self.COUNT_COMMAND_VALUES
for info in self._qute_args.values())
def register(self):
"""Register this command in objects.commands."""
log.commands.vdebug(
"Registering command {} (from {}:{})".format(
self.name, self.handler.__module__, self.handler.__qualname__))
if self.name in objects.commands:
raise ValueError("{} is already registered!".format(self.name))
objects.commands[self.name] = self

View File

@ -25,10 +25,11 @@ import re
import attr
from PyQt5.QtCore import pyqtSlot, QUrl, QObject
from qutebrowser.api import cmdutils
from qutebrowser.config import config
from qutebrowser.commands import cmdexc, cmdutils
from qutebrowser.commands import cmdexc
from qutebrowser.utils import message, objreg, qtutils, usertypes, utils
from qutebrowser.misc import split
from qutebrowser.misc import split, objects
last_command = {}
@ -53,11 +54,14 @@ def _current_url(tabbed_browser):
if e.reason:
msg += " ({})".format(e.reason)
msg += "!"
raise cmdexc.CommandError(msg)
raise cmdutils.CommandError(msg)
def replace_variables(win_id, arglist):
"""Utility function to replace variables like {url} in a list of args."""
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
variables = {
'url': lambda: _current_url(tabbed_browser).toString(
QUrl.FullyEncoded | QUrl.RemovePassword),
@ -67,13 +71,13 @@ def replace_variables(win_id, arglist):
'clipboard': utils.get_clipboard,
'primary': lambda: utils.get_clipboard(selection=True),
}
for key in list(variables):
modified_key = '{' + key + '}'
variables[modified_key] = lambda x=modified_key: x
values = {}
args = []
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
def repl_cb(matchobj):
"""Return replacement for given match."""
@ -90,7 +94,7 @@ def replace_variables(win_id, arglist):
# "{url}" from clipboard is not expanded)
args.append(repl_pattern.sub(repl_cb, arg))
except utils.ClipboardError as e:
raise cmdexc.CommandError(e)
raise cmdutils.CommandError(e)
return args
@ -190,7 +194,7 @@ class CommandParser:
cmdstr = self._completion_match(cmdstr)
try:
cmd = cmdutils.cmd_dict[cmdstr]
cmd = objects.commands[cmdstr]
except KeyError:
if not fallback:
raise cmdexc.NoSuchCommandError(
@ -217,7 +221,7 @@ class CommandParser:
Return:
cmdstr modified to the matching completion or unmodified
"""
matches = [cmd for cmd in sorted(cmdutils.cmd_dict, key=len)
matches = [cmd for cmd in sorted(objects.commands, key=len)
if cmdstr in cmd]
if len(matches) == 1:
cmdstr = matches[0]

View File

@ -23,7 +23,8 @@ import attr
from PyQt5.QtCore import pyqtSlot, QObject, QTimer
from qutebrowser.config import config
from qutebrowser.commands import cmdutils, runners
from qutebrowser.commands import runners
from qutebrowser.misc import objects
from qutebrowser.utils import log, utils, debug
from qutebrowser.completion.models import miscmodels
@ -92,7 +93,7 @@ class Completer(QObject):
log.completion.debug('Starting command completion')
return miscmodels.command
try:
cmd = cmdutils.cmd_dict[before_cursor[0]]
cmd = objects.commands[before_cursor[0]]
except KeyError:
log.completion.debug("No completion for unknown command: {}"
.format(before_cursor[0]))
@ -170,7 +171,7 @@ class Completer(QObject):
before, center, after = self._partition()
log.completion.debug("Changing {} to '{}'".format(center, text))
try:
maxsplit = cmdutils.cmd_dict[before[0]].maxsplit
maxsplit = objects.commands[before[0]].maxsplit
except (KeyError, IndexError):
maxsplit = None
if maxsplit is None:

View File

@ -166,11 +166,11 @@ class CompletionItemDelegate(QStyledItemDelegate):
self._painter.save()
if self._opt.state & QStyle.State_Selected:
color = config.val.colors.completion.item.selected.fg
color = config.cache['colors.completion.item.selected.fg']
elif not self._opt.state & QStyle.State_Enabled:
color = config.val.colors.completion.category.fg
color = config.cache['colors.completion.category.fg']
else:
colors = config.val.colors.completion.fg
colors = config.cache['colors.completion.fg']
# if multiple colors are set, use different colors per column
color = colors[col % len(colors)]
self._painter.setPen(color)
@ -212,11 +212,11 @@ class CompletionItemDelegate(QStyledItemDelegate):
view = self.parent()
pattern = view.pattern
columns_to_filter = index.model().columns_to_filter(index)
self._doc.setPlainText(self._opt.text)
if index.column() in columns_to_filter and pattern:
pat = re.escape(pattern).replace(r'\ ', r'|')
_Highlighter(self._doc, pat,
config.val.colors.completion.match.fg)
self._doc.setPlainText(self._opt.text)
else:
self._doc.setHtml(
'<span style="font: {};">{}</span>'.format(

View File

@ -29,7 +29,7 @@ from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QItemSelectionModel, QSize
from qutebrowser.config import config
from qutebrowser.completion import completiondelegate
from qutebrowser.utils import utils, usertypes, debug, log, objreg
from qutebrowser.commands import cmdexc, cmdutils
from qutebrowser.api import cmdutils
class CompletionView(QTreeView):
@ -251,8 +251,8 @@ class CompletionView(QTreeView):
status.command_history_prev()
return
else:
raise cmdexc.CommandError("Can't combine --history with "
"{}!".format(which))
raise cmdutils.CommandError("Can't combine --history with "
"{}!".format(which))
if not self._active:
return
@ -394,7 +394,7 @@ class CompletionView(QTreeView):
"""Delete the current completion item."""
index = self.currentIndex()
if not index.isValid():
raise cmdexc.CommandError("No item selected!")
raise cmdutils.CommandError("No item selected!")
self.model().delete_cur_item(index)
@cmdutils.register(instance='completion',
@ -411,6 +411,6 @@ class CompletionView(QTreeView):
if not text:
index = self.currentIndex()
if not index.isValid():
raise cmdexc.CommandError("No item selected!")
raise cmdutils.CommandError("No item selected!")
text = self.model().data(index)
utils.set_clipboard(text, selection=sel)

View File

@ -22,7 +22,7 @@
from PyQt5.QtCore import Qt, QModelIndex, QAbstractItemModel
from qutebrowser.utils import log, qtutils
from qutebrowser.commands import cmdexc
from qutebrowser.api import cmdutils
class CompletionModel(QAbstractItemModel):
@ -224,7 +224,7 @@ class CompletionModel(QAbstractItemModel):
cat = self._cat_from_idx(parent)
assert cat, "CompletionView sent invalid index for deletion"
if not cat.delete_func:
raise cmdexc.CommandError("Cannot delete this item.")
raise cmdutils.CommandError("Cannot delete this item.")
data = [cat.data(cat.index(index.row(), i))
for i in range(cat.columnCount())]

View File

@ -74,9 +74,10 @@ class HistoryCategory(QSqlQueryModel):
# build a where clause to match all of the words in any order
# given the search term "a b", the WHERE clause would be:
# ((url || title) LIKE '%a%') AND ((url || title) LIKE '%b%')
# ((url || ' ' || title) LIKE '%a%') AND
# ((url || ' ' || title) LIKE '%b%')
where_clause = ' AND '.join(
"(url || title) LIKE :{} escape '\\'".format(i)
"(url || ' ' || title) LIKE :{} escape '\\'".format(i)
for i in range(len(words)))
# replace ' in timestamp-format to avoid breaking the query

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