Merge branch 'master' into fix_4349
This commit is contained in:
commit
403b4643da
@ -14,6 +14,7 @@ exclude_lines =
|
||||
raise NotImplementedError
|
||||
raise utils\.Unreachable
|
||||
if __name__ == ["']__main__["']:
|
||||
if MYPY:
|
||||
|
||||
[xml]
|
||||
output=coverage.xml
|
||||
|
5
.flake8
5
.flake8
@ -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,10 +41,12 @@ 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 =
|
||||
/qutebrowser/api/hook.py : N801
|
||||
/tests/**/*.py : D100,D101,D401
|
||||
/tests/unit/browser/test_history.py : N806
|
||||
/tests/helpers/fixtures.py : N806
|
||||
|
BIN
.github/img/hsr.png
vendored
Normal file
BIN
.github/img/hsr.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
1
.gitignore
vendored
1
.gitignore
vendored
@ -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
|
||||
|
63
.travis.yml
63
.travis.yml
@ -1,37 +1,26 @@
|
||||
sudo: false
|
||||
dist: trusty
|
||||
dist: xenial
|
||||
language: python
|
||||
group: edge
|
||||
python: 3.6
|
||||
os: linux
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- os: linux
|
||||
env: DOCKER=archlinux
|
||||
- env: DOCKER=archlinux
|
||||
services: docker
|
||||
- os: linux
|
||||
env: DOCKER=archlinux-webengine QUTE_BDD_WEBENGINE=true
|
||||
- env: DOCKER=archlinux-webengine QUTE_BDD_WEBENGINE=true
|
||||
services: docker
|
||||
- os: linux
|
||||
env: TESTENV=py36-pyqt571
|
||||
- os: linux
|
||||
python: 3.5
|
||||
- env: TESTENV=py36-pyqt571
|
||||
- python: 3.5
|
||||
env: TESTENV=py35-pyqt571
|
||||
- os: linux
|
||||
env: TESTENV=py36-pyqt59
|
||||
- os: linux
|
||||
env: TESTENV=py36-pyqt510
|
||||
- env: TESTENV=py36-pyqt59
|
||||
- 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
|
||||
- env: TESTENV=py36-pyqt511-cov
|
||||
- python: 3.7
|
||||
env: TESTENV=py37-pyqt511
|
||||
- os: osx
|
||||
env: TESTENV=py37 OSX=sierra
|
||||
@ -41,38 +30,26 @@ matrix:
|
||||
# - 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
|
||||
- env: TESTENV=pylint
|
||||
- env: TESTENV=flake8
|
||||
- env: TESTENV=mypy
|
||||
- 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
|
||||
- env: TESTENV=vulture
|
||||
- env: TESTENV=misc
|
||||
- env: TESTENV=pyroma
|
||||
- env: TESTENV=check-manifest
|
||||
- env: TESTENV=eslint
|
||||
language: node_js
|
||||
python: null
|
||||
node_js: "lts/*"
|
||||
- os: linux
|
||||
language: generic
|
||||
- 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:
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
-------
|
||||
|
@ -32,6 +32,8 @@ 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.
|
||||
|
||||
Changed
|
||||
~~~~~~~
|
||||
@ -49,6 +51,8 @@ Changed
|
||||
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.
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
@ -57,6 +61,25 @@ Fixed
|
||||
- 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 or anchors.
|
||||
- Crash when trying to use a proxy requiring authentication with QtWebKit.
|
||||
- Slashes in search terms are now percent-escaped.
|
||||
|
||||
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
|
||||
------
|
||||
@ -1225,7 +1248,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
|
||||
@ -1336,7 +1359,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
|
||||
~~~~~~~~~~
|
||||
|
@ -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.
|
||||
|
0
doc/extapi/_static/.gitkeep
Normal file
0
doc/extapi/_static/.gitkeep
Normal file
0
doc/extapi/_templates/.gitkeep
Normal file
0
doc/extapi/_templates/.gitkeep
Normal file
48
doc/extapi/api.rst
Normal file
48
doc/extapi/api.rst
Normal 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
179
doc/extapi/conf.py
Normal 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
22
doc/extapi/index.rst
Normal 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
44
doc/extapi/tab.rst
Normal 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:
|
@ -215,11 +215,11 @@ What's the difference between insert and passthrough mode?::
|
||||
be useful to rebind escape to something else in passthrough mode only, to be
|
||||
able to send an escape keypress to the website.
|
||||
|
||||
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 +260,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.::
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
@ -423,6 +423,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
|
||||
------------------------------
|
||||
|
@ -640,10 +640,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 +1593,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.
|
||||
|
||||
@ -2958,7 +2960,7 @@ Default: +pass:[false]+
|
||||
=== search.ignore_case
|
||||
When to find text on a page case-insensitively.
|
||||
|
||||
Type: <<types,String>>
|
||||
Type: <<types,IgnoreCase>>
|
||||
|
||||
Valid values:
|
||||
|
||||
@ -3622,6 +3624,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.
|
||||
|
@ -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]=Cửa sổ Mới
|
||||
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"
|
||||
|
@ -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'],
|
||||
|
@ -1,9 +1,9 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
certifi==2018.10.15
|
||||
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
|
||||
|
@ -1,9 +1,9 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
attrs==18.2.0
|
||||
flake8==3.5.0
|
||||
flake8==3.6.0
|
||||
flake8-bugbear==18.8.0
|
||||
flake8-builtins==1.4.1 # rq.filter: != 1.4.0
|
||||
flake8-builtins==1.4.1
|
||||
flake8-comprehensions==1.4.1
|
||||
flake8-copyright==0.2.2
|
||||
flake8-debugger==3.1.0
|
||||
@ -19,9 +19,9 @@ 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
|
||||
pycodestyle==2.4.0
|
||||
pydocstyle==3.0.0
|
||||
pyflakes==2.0.0
|
||||
six==1.11.0
|
||||
six==1.12.0
|
||||
snowballstemmer==1.2.1
|
||||
typing==3.6.6
|
||||
|
@ -1,6 +1,6 @@
|
||||
flake8
|
||||
flake8-bugbear
|
||||
flake8-builtins!=1.4.0
|
||||
flake8-builtins
|
||||
flake8-comprehensions
|
||||
flake8-copyright
|
||||
flake8-debugger
|
||||
@ -15,9 +15,3 @@ 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
|
||||
|
8
misc/requirements/requirements-mypy.txt
Normal file
8
misc/requirements/requirements-mypy.txt
Normal file
@ -0,0 +1,8 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
mypy==0.650
|
||||
mypy-extensions==0.4.1
|
||||
PyQt5==5.11.3
|
||||
PyQt5-sip==4.19.13
|
||||
-e git+https://github.com/qutebrowser/PyQt5-stubs.git@wip#egg=PyQt5_stubs
|
||||
typed-ast==1.1.0
|
5
misc/requirements/requirements-mypy.txt-raw
Normal file
5
misc/requirements/requirements-mypy.txt-raw
Normal file
@ -0,0 +1,5 @@
|
||||
mypy
|
||||
-e git+https://github.com/qutebrowser/PyQt5-stubs.git@wip#egg=PyQt5-stubs
|
||||
|
||||
# remove @commit-id for scm installs
|
||||
#@ replace: @.*# @wip#
|
7
misc/requirements/requirements-optional.txt
Normal file
7
misc/requirements/requirements-optional.txt
Normal file
@ -0,0 +1,7 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
colorama==0.4.1
|
||||
cssutils==1.0.2
|
||||
hunter==2.1.0
|
||||
Pympler==0.6
|
||||
six==1.12.0
|
3
misc/requirements/requirements-optional.txt-raw
Normal file
3
misc/requirements/requirements-optional.txt-raw
Normal file
@ -0,0 +1,3 @@
|
||||
hunter
|
||||
cssutils
|
||||
pympler
|
@ -2,7 +2,7 @@
|
||||
|
||||
appdirs==1.4.3
|
||||
packaging==18.0
|
||||
pyparsing==2.2.2
|
||||
setuptools==40.4.3
|
||||
six==1.11.0
|
||||
wheel==0.32.1
|
||||
pyparsing==2.3.0
|
||||
setuptools==40.6.2
|
||||
six==1.12.0
|
||||
wheel==0.32.3
|
||||
|
@ -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
|
||||
|
@ -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.10.15
|
||||
astroid==2.1.0
|
||||
certifi==2018.11.29
|
||||
cffi==1.11.5
|
||||
chardet==3.0.4
|
||||
cryptography==2.3.1
|
||||
cryptography==2.4.2
|
||||
github3.py==1.2.0
|
||||
idna==2.7
|
||||
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.7.5
|
||||
./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
|
||||
urllib3==1.24.1
|
||||
wrapt==1.10.11
|
||||
|
21
misc/requirements/requirements-sphinx.txt
Normal file
21
misc/requirements/requirements-sphinx.txt
Normal 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==18.0
|
||||
Pygments==2.3.0
|
||||
pyparsing==2.3.0
|
||||
pytz==2018.7
|
||||
requests==2.21.0
|
||||
six==1.12.0
|
||||
snowballstemmer==1.2.1
|
||||
Sphinx==1.8.2
|
||||
sphinxcontrib-websupport==1.1.0
|
||||
urllib3==1.24.1
|
1
misc/requirements/requirements-sphinx.txt-raw
Normal file
1
misc/requirements/requirements-sphinx.txt-raw
Normal file
@ -0,0 +1 @@
|
||||
sphinx
|
@ -5,27 +5,26 @@ 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
|
||||
Click==7.0
|
||||
# colorama==0.4.1
|
||||
coverage==4.5.2
|
||||
EasyProcess==0.2.3
|
||||
fields==5.0.0
|
||||
Flask==1.0.2
|
||||
glob2==0.6
|
||||
hunter==2.0.2
|
||||
hypothesis==3.76.0
|
||||
itsdangerous==0.24
|
||||
hunter==2.1.0
|
||||
hypothesis==3.82.5
|
||||
itsdangerous==1.1.0
|
||||
# Jinja2==2.10
|
||||
Mako==1.0.7
|
||||
# MarkupSafe==1.0
|
||||
# MarkupSafe==1.1.0
|
||||
more-itertools==4.3.0
|
||||
parse==1.9.0
|
||||
parse-type==0.4.2
|
||||
pluggy==0.7.1
|
||||
pluggy==0.8.0
|
||||
py==1.7.0
|
||||
py-cpuinfo==4.0.0
|
||||
pytest==3.6.4 # rq.filter: <3.7
|
||||
pytest-bdd==2.21.0
|
||||
pytest==4.0.1
|
||||
pytest-bdd==3.0.0
|
||||
pytest-benchmark==3.1.1
|
||||
pytest-cov==2.6.0
|
||||
pytest-faulthandler==1.5.0
|
||||
@ -33,10 +32,10 @@ pytest-instafail==0.4.0
|
||||
pytest-mock==1.10.0
|
||||
pytest-qt==3.2.1
|
||||
pytest-repeat==0.7.0
|
||||
pytest-rerunfailures==4.2
|
||||
pytest-rerunfailures==5.0
|
||||
pytest-travis-fold==1.3.0
|
||||
pytest-xvfb==1.1.0
|
||||
PyVirtualDisplay==0.2.1
|
||||
six==1.11.0
|
||||
vulture==0.29
|
||||
six==1.12.0
|
||||
vulture==1.0
|
||||
Werkzeug==0.14.1
|
||||
|
@ -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
|
||||
|
@ -1,8 +1,9 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
pluggy==0.7.1
|
||||
filelock==3.0.10
|
||||
pluggy==0.8.0
|
||||
py==1.7.0
|
||||
six==1.11.0
|
||||
six==1.12.0
|
||||
toml==0.10.0
|
||||
tox==3.5.2
|
||||
virtualenv==16.0.0
|
||||
tox==3.5.3
|
||||
virtualenv==16.1.0
|
||||
|
@ -1,3 +1,3 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
vulture==0.29
|
||||
vulture==1.0
|
||||
|
@ -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/
|
||||
|
||||
|
@ -269,7 +269,7 @@ pass_backend() {
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done < <($GPG "${GPG_OPTS[@]}" -d "$path" )
|
||||
done < <($GPG "${GPG_OPTS[@]}" -d "$path" | awk 1 )
|
||||
}
|
||||
}
|
||||
# =======================================================
|
||||
|
87
mypy.ini
Normal file
87
mypy.ini
Normal 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
|
@ -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)
|
||||
@ -66,5 +66,8 @@ qt_log_ignore =
|
||||
^Error receiving trust for a CA certificate
|
||||
xfail_strict = true
|
||||
filterwarnings =
|
||||
error
|
||||
# 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
|
||||
# WORKAROUND for https://github.com/ionelmc/pytest-benchmark/issues/124
|
||||
ignore:Node\.warn\(code, message\) form has been deprecated, use Node\.warn\(warning_instance\) instead:pytest.PytestDeprecationWarning
|
||||
|
@ -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."
|
||||
|
||||
|
26
qutebrowser/api/__init__.py
Normal file
26
qutebrowser/api/__init__.py
Normal 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.
|
||||
"""
|
27
qutebrowser/api/apitypes.py
Normal file
27
qutebrowser/api/apitypes.py
Normal 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
219
qutebrowser/api/cmdutils.py
Normal 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
43
qutebrowser/api/config.py
Normal 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)
|
75
qutebrowser/api/downloads.py
Normal file
75
qutebrowser/api/downloads.py
Normal 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
92
qutebrowser/api/hook.py
Normal 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
|
43
qutebrowser/api/interceptor.py
Normal file
43
qutebrowser/api/interceptor.py
Normal 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)
|
23
qutebrowser/api/message.py
Normal file
23
qutebrowser/api/message.py
Normal 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
|
@ -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,
|
||||
@ -163,6 +165,8 @@ def init(args, crash_handler):
|
||||
qApp.setQuitOnLastWindowClosed(False)
|
||||
_init_icon()
|
||||
|
||||
loader.init()
|
||||
loader.load_components()
|
||||
try:
|
||||
_init_modules(args, crash_handler)
|
||||
except (OSError, UnicodeDecodeError, browsertab.WebTabError) as e:
|
||||
@ -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))
|
||||
@ -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.
|
||||
@ -465,11 +469,6 @@ def _init_modules(args, crash_handler):
|
||||
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)
|
||||
objreg.register('quickmark-manager', quickmark_manager)
|
||||
@ -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
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -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):
|
||||
|
@ -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
|
||||
|
||||
|
@ -34,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
|
||||
|
||||
|
||||
@ -101,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())
|
||||
@ -109,7 +110,7 @@ class HintLabel(QLabel):
|
||||
matched = html.escape(matched)
|
||||
unmatched = html.escape(unmatched)
|
||||
|
||||
match_color = html.escape(config.val.colors.hints.match.fg)
|
||||
match_color = html.escape(config.cache['colors.hints.match.fg'])
|
||||
if matched:
|
||||
self.setText('<font color="{}">{}</font>{}'.format(
|
||||
match_color, matched, unmatched))
|
||||
@ -125,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())
|
||||
|
||||
@ -217,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:
|
||||
@ -304,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')
|
||||
@ -563,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):
|
||||
@ -596,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
|
||||
@ -705,7 +697,7 @@ 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)
|
||||
@ -722,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()
|
||||
@ -736,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."""
|
||||
@ -758,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):
|
||||
@ -960,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)
|
||||
|
@ -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):
|
||||
|
@ -49,8 +49,6 @@ class WebInspectorError(Exception):
|
||||
|
||||
"""Raised when the inspector could not be initialized."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class AbstractWebInspector(QWidget):
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)))
|
||||
|
@ -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.
|
||||
|
@ -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):
|
||||
|
||||
|
@ -262,7 +262,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
|
||||
|
@ -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!")
|
||||
|
@ -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,
|
||||
|
@ -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:
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
||||
@ -1031,6 +1047,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.
|
||||
@ -1044,8 +1082,7 @@ class WebEngineTab(browsertab.AbstractTab):
|
||||
_load_finished_fake = pyqtSignal(bool)
|
||||
|
||||
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)
|
||||
@ -1058,6 +1095,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
|
||||
@ -1095,21 +1134,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()
|
||||
@ -1139,11 +1180,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
|
||||
@ -1168,22 +1204,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))
|
||||
@ -1220,7 +1240,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,
|
||||
@ -1246,15 +1266,21 @@ class WebEngineTab(browsertab.AbstractTab):
|
||||
|
||||
@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:
|
||||
log.network.debug("Asking for credentials")
|
||||
abort_on = [self.shutting_down, self.load_started]
|
||||
answer = shared.authentication_required(url, authenticator,
|
||||
abort_on)
|
||||
if not netrc_success and answer is None:
|
||||
log.network.debug("Aborting auth")
|
||||
try:
|
||||
# pylint: disable=no-member, useless-suppression
|
||||
sip.assign(authenticator, QAuthenticator())
|
||||
@ -1342,9 +1368,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):
|
||||
@ -1383,12 +1409,12 @@ 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)
|
||||
|
||||
@ -1466,12 +1492,9 @@ 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)
|
||||
|
||||
# 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()
|
||||
|
@ -240,7 +240,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:
|
||||
|
@ -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."""
|
||||
|
@ -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)
|
||||
|
@ -67,7 +67,6 @@ class FixedDataNetworkReply(QNetworkReply):
|
||||
@pyqtSlot()
|
||||
def abort(self):
|
||||
"""Abort the operation."""
|
||||
pass
|
||||
|
||||
def bytesAvailable(self):
|
||||
"""Determine the bytes available for being read.
|
||||
@ -123,7 +122,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 +149,6 @@ class RedirectNetworkReply(QNetworkReply):
|
||||
|
||||
def abort(self):
|
||||
"""Called when there's e.g. a redirection limit."""
|
||||
pass
|
||||
|
||||
def readData(self, _maxlen):
|
||||
return bytes()
|
||||
|
@ -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()
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
@ -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
|
||||
|
@ -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]
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -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())]
|
||||
|
@ -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
|
||||
|
@ -20,7 +20,7 @@
|
||||
"""Utility functions for completion models."""
|
||||
|
||||
from qutebrowser.utils import objreg, usertypes
|
||||
from qutebrowser.commands import cmdutils
|
||||
from qutebrowser.misc import objects
|
||||
|
||||
|
||||
def get_cmd_completions(info, include_hidden, include_aliases, prefix=''):
|
||||
@ -34,10 +34,10 @@ def get_cmd_completions(info, include_hidden, include_aliases, prefix=''):
|
||||
|
||||
Return: A list of tuples of form (name, description, bindings).
|
||||
"""
|
||||
assert cmdutils.cmd_dict
|
||||
assert objects.commands
|
||||
cmdlist = []
|
||||
cmd_to_keys = info.keyconf.get_reverse_bindings_for('normal')
|
||||
for obj in set(cmdutils.cmd_dict.values()):
|
||||
for obj in set(objects.commands.values()):
|
||||
hide_debug = obj.debug and not objreg.get('args').debug
|
||||
hide_mode = (usertypes.KeyMode.normal not in obj.modes and
|
||||
not include_hidden)
|
||||
|
20
qutebrowser/components/__init__.py
Normal file
20
qutebrowser/components/__init__.py
Normal file
@ -0,0 +1,20 @@
|
||||
# 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/>.
|
||||
|
||||
"""qutebrowser "extensions" which only use the qutebrowser.api API."""
|
@ -24,19 +24,22 @@ import os.path
|
||||
import functools
|
||||
import posixpath
|
||||
import zipfile
|
||||
import logging
|
||||
import typing
|
||||
import pathlib
|
||||
|
||||
from qutebrowser.browser import downloads
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import objreg, standarddir, log, message
|
||||
from qutebrowser.commands import cmdutils
|
||||
from PyQt5.QtCore import QUrl
|
||||
|
||||
from qutebrowser.api import (cmdutils, hook, config, message, downloads,
|
||||
interceptor, apitypes)
|
||||
|
||||
|
||||
def _guess_zip_filename(zf):
|
||||
"""Guess which file to use inside a zip file.
|
||||
logger = logging.getLogger('misc')
|
||||
_host_blocker = typing.cast('HostBlocker', None)
|
||||
|
||||
Args:
|
||||
zf: A ZipFile instance.
|
||||
"""
|
||||
|
||||
def _guess_zip_filename(zf: zipfile.ZipFile) -> str:
|
||||
"""Guess which file to use inside a zip file."""
|
||||
files = zf.namelist()
|
||||
if len(files) == 1:
|
||||
return files[0]
|
||||
@ -47,7 +50,7 @@ def _guess_zip_filename(zf):
|
||||
raise FileNotFoundError("No hosts file found in zip")
|
||||
|
||||
|
||||
def get_fileobj(byte_io):
|
||||
def get_fileobj(byte_io: typing.IO[bytes]) -> typing.IO[bytes]:
|
||||
"""Get a usable file object to read the hosts file from."""
|
||||
byte_io.seek(0) # rewind downloaded file
|
||||
if zipfile.is_zipfile(byte_io):
|
||||
@ -60,24 +63,20 @@ def get_fileobj(byte_io):
|
||||
return byte_io
|
||||
|
||||
|
||||
def _is_whitelisted_url(url):
|
||||
"""Check if the given URL is on the adblock whitelist.
|
||||
|
||||
Args:
|
||||
url: The URL to check as QUrl.
|
||||
"""
|
||||
def _is_whitelisted_url(url: QUrl) -> bool:
|
||||
"""Check if the given URL is on the adblock whitelist."""
|
||||
for pattern in config.val.content.host_blocking.whitelist:
|
||||
if pattern.matches(url):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class _FakeDownload:
|
||||
class _FakeDownload(downloads.TempDownload):
|
||||
|
||||
"""A download stub to use on_download_finished with local files."""
|
||||
|
||||
def __init__(self, fileobj):
|
||||
self.basename = os.path.basename(fileobj.name)
|
||||
def __init__(self, # pylint: disable=super-init-not-called
|
||||
fileobj: typing.IO[bytes]) -> None:
|
||||
self.fileobj = fileobj
|
||||
self.successful = True
|
||||
|
||||
@ -93,37 +92,46 @@ class HostBlocker:
|
||||
_done_count: How many files have been read successfully.
|
||||
_local_hosts_file: The path to the blocked-hosts file.
|
||||
_config_hosts_file: The path to a blocked-hosts in ~/.config
|
||||
_has_basedir: Whether a custom --basedir is set.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._blocked_hosts = set()
|
||||
self._config_blocked_hosts = set()
|
||||
self._in_progress = []
|
||||
def __init__(self, *, data_dir: pathlib.Path, config_dir: pathlib.Path,
|
||||
has_basedir: bool = False) -> None:
|
||||
self._has_basedir = has_basedir
|
||||
self._blocked_hosts = set() # type: typing.Set[str]
|
||||
self._config_blocked_hosts = set() # type: typing.Set[str]
|
||||
self._in_progress = [] # type: typing.List[downloads.TempDownload]
|
||||
self._done_count = 0
|
||||
|
||||
data_dir = standarddir.data()
|
||||
self._local_hosts_file = os.path.join(data_dir, 'blocked-hosts')
|
||||
self._update_files()
|
||||
self._local_hosts_file = str(data_dir / 'blocked-hosts')
|
||||
self.update_files()
|
||||
|
||||
config_dir = standarddir.config()
|
||||
self._config_hosts_file = os.path.join(config_dir, 'blocked-hosts')
|
||||
self._config_hosts_file = str(config_dir / 'blocked-hosts')
|
||||
|
||||
config.instance.changed.connect(self._update_files)
|
||||
|
||||
def is_blocked(self, url, first_party_url=None):
|
||||
"""Check if the given URL (as QUrl) is blocked."""
|
||||
def _is_blocked(self, request_url: QUrl,
|
||||
first_party_url: QUrl = None) -> bool:
|
||||
"""Check whether the given request is blocked."""
|
||||
if first_party_url is not None and not first_party_url.isValid():
|
||||
first_party_url = None
|
||||
if not config.instance.get('content.host_blocking.enabled',
|
||||
url=first_party_url):
|
||||
|
||||
if not config.get('content.host_blocking.enabled',
|
||||
url=first_party_url):
|
||||
return False
|
||||
|
||||
host = url.host()
|
||||
host = request_url.host()
|
||||
return ((host in self._blocked_hosts or
|
||||
host in self._config_blocked_hosts) and
|
||||
not _is_whitelisted_url(url))
|
||||
not _is_whitelisted_url(request_url))
|
||||
|
||||
def _read_hosts_file(self, filename, target):
|
||||
def filter_request(self, info: interceptor.Request) -> None:
|
||||
"""Block the given request if necessary."""
|
||||
if self._is_blocked(request_url=info.request_url,
|
||||
first_party_url=info.first_party_url):
|
||||
logger.info("Request to {} blocked by host blocker."
|
||||
.format(info.request_url.host()))
|
||||
info.block()
|
||||
|
||||
def _read_hosts_file(self, filename: str, target: typing.Set[str]) -> bool:
|
||||
"""Read hosts from the given filename.
|
||||
|
||||
Args:
|
||||
@ -141,11 +149,11 @@ class HostBlocker:
|
||||
for line in f:
|
||||
target.add(line.strip())
|
||||
except (OSError, UnicodeDecodeError):
|
||||
log.misc.exception("Failed to read host blocklist!")
|
||||
logger.exception("Failed to read host blocklist!")
|
||||
|
||||
return True
|
||||
|
||||
def read_hosts(self):
|
||||
def read_hosts(self) -> None:
|
||||
"""Read hosts from the existing blocked-hosts file."""
|
||||
self._blocked_hosts = set()
|
||||
|
||||
@ -156,24 +164,17 @@ class HostBlocker:
|
||||
self._blocked_hosts)
|
||||
|
||||
if not found:
|
||||
args = objreg.get('args')
|
||||
if (config.val.content.host_blocking.lists and
|
||||
args.basedir is None and
|
||||
not self._has_basedir and
|
||||
config.val.content.host_blocking.enabled):
|
||||
message.info("Run :adblock-update to get adblock lists.")
|
||||
|
||||
@cmdutils.register(instance='host-blocker')
|
||||
def adblock_update(self):
|
||||
"""Update the adblock block lists.
|
||||
|
||||
This updates `~/.local/share/qutebrowser/blocked-hosts` with downloaded
|
||||
host lists and re-reads `~/.config/qutebrowser/blocked-hosts`.
|
||||
"""
|
||||
def adblock_update(self) -> None:
|
||||
"""Update the adblock block lists."""
|
||||
self._read_hosts_file(self._config_hosts_file,
|
||||
self._config_blocked_hosts)
|
||||
self._blocked_hosts = set()
|
||||
self._done_count = 0
|
||||
download_manager = objreg.get('qtnetwork-download-manager')
|
||||
for url in config.val.content.host_blocking.lists:
|
||||
if url.scheme() == 'file':
|
||||
filename = url.toLocalFile()
|
||||
@ -184,16 +185,12 @@ class HostBlocker:
|
||||
else:
|
||||
self._import_local(filename)
|
||||
else:
|
||||
fobj = io.BytesIO()
|
||||
fobj.name = 'adblock: ' + url.host()
|
||||
target = downloads.FileObjDownloadTarget(fobj)
|
||||
download = download_manager.get(url, target=target,
|
||||
auto_remove=True)
|
||||
download = downloads.download_temp(url)
|
||||
self._in_progress.append(download)
|
||||
download.finished.connect(
|
||||
functools.partial(self._on_download_finished, download))
|
||||
|
||||
def _import_local(self, filename):
|
||||
def _import_local(self, filename: str) -> None:
|
||||
"""Adds the contents of a file to the blocklist.
|
||||
|
||||
Args:
|
||||
@ -209,24 +206,24 @@ class HostBlocker:
|
||||
self._in_progress.append(download)
|
||||
self._on_download_finished(download)
|
||||
|
||||
def _parse_line(self, line):
|
||||
def _parse_line(self, raw_line: bytes) -> bool:
|
||||
"""Parse a line from a host file.
|
||||
|
||||
Args:
|
||||
line: The bytes object to parse.
|
||||
raw_line: The bytes object to parse.
|
||||
|
||||
Returns:
|
||||
True if parsing succeeded, False otherwise.
|
||||
"""
|
||||
if line.startswith(b'#'):
|
||||
if raw_line.startswith(b'#'):
|
||||
# Ignoring comments early so we don't have to care about
|
||||
# encoding errors in them.
|
||||
return True
|
||||
|
||||
try:
|
||||
line = line.decode('utf-8')
|
||||
line = raw_line.decode('utf-8')
|
||||
except UnicodeDecodeError:
|
||||
log.misc.error("Failed to decode: {!r}".format(line))
|
||||
logger.error("Failed to decode: {!r}".format(raw_line))
|
||||
return False
|
||||
|
||||
# Remove comments
|
||||
@ -257,14 +254,11 @@ class HostBlocker:
|
||||
|
||||
return True
|
||||
|
||||
def _merge_file(self, byte_io):
|
||||
def _merge_file(self, byte_io: io.BytesIO) -> None:
|
||||
"""Read and merge host files.
|
||||
|
||||
Args:
|
||||
byte_io: The BytesIO object of the completed download.
|
||||
|
||||
Return:
|
||||
A set of the merged hosts.
|
||||
"""
|
||||
error_count = 0
|
||||
line_count = 0
|
||||
@ -282,12 +276,12 @@ class HostBlocker:
|
||||
if not ok:
|
||||
error_count += 1
|
||||
|
||||
log.misc.debug("{}: read {} lines".format(byte_io.name, line_count))
|
||||
logger.debug("{}: read {} lines".format(byte_io.name, line_count))
|
||||
if error_count > 0:
|
||||
message.error("adblock: {} read errors for {}".format(
|
||||
error_count, byte_io.name))
|
||||
|
||||
def _on_lists_downloaded(self):
|
||||
def _on_lists_downloaded(self) -> None:
|
||||
"""Install block lists after files have been downloaded."""
|
||||
with open(self._local_hosts_file, 'w', encoding='utf-8') as f:
|
||||
for host in sorted(self._blocked_hosts):
|
||||
@ -295,8 +289,7 @@ class HostBlocker:
|
||||
message.info("adblock: Read {} hosts from {} sources.".format(
|
||||
len(self._blocked_hosts), self._done_count))
|
||||
|
||||
@config.change_filter('content.host_blocking.lists')
|
||||
def _update_files(self):
|
||||
def update_files(self) -> None:
|
||||
"""Update files when the config changed."""
|
||||
if not config.val.content.host_blocking.lists:
|
||||
try:
|
||||
@ -304,13 +297,13 @@ class HostBlocker:
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
except OSError as e:
|
||||
log.misc.exception("Failed to delete hosts file: {}".format(e))
|
||||
logger.exception("Failed to delete hosts file: {}".format(e))
|
||||
|
||||
def _on_download_finished(self, download):
|
||||
def _on_download_finished(self, download: downloads.TempDownload) -> None:
|
||||
"""Check if all downloads are finished and if so, trigger reading.
|
||||
|
||||
Arguments:
|
||||
download: The finished DownloadItem.
|
||||
download: The finished download.
|
||||
"""
|
||||
self._in_progress.remove(download)
|
||||
if download.successful:
|
||||
@ -323,4 +316,32 @@ class HostBlocker:
|
||||
try:
|
||||
self._on_lists_downloaded()
|
||||
except OSError:
|
||||
log.misc.exception("Failed to write host block list!")
|
||||
logger.exception("Failed to write host block list!")
|
||||
|
||||
|
||||
@cmdutils.register()
|
||||
def adblock_update() -> None:
|
||||
"""Update the adblock block lists.
|
||||
|
||||
This updates `~/.local/share/qutebrowser/blocked-hosts` with downloaded
|
||||
host lists and re-reads `~/.config/qutebrowser/blocked-hosts`.
|
||||
"""
|
||||
# FIXME: As soon as we can register instances again, we should move this
|
||||
# back to the class.
|
||||
_host_blocker.adblock_update()
|
||||
|
||||
|
||||
@hook.config_changed('content.host_blocking.lists')
|
||||
def on_config_changed() -> None:
|
||||
_host_blocker.update_files()
|
||||
|
||||
|
||||
@hook.init()
|
||||
def init(context: apitypes.InitContext) -> None:
|
||||
"""Initialize the host blocker."""
|
||||
global _host_blocker
|
||||
_host_blocker = HostBlocker(data_dir=context.data_dir,
|
||||
config_dir=context.config_dir,
|
||||
has_basedir=context.args.basedir is not None)
|
||||
_host_blocker.read_hosts()
|
||||
interceptor.register(_host_blocker.filter_request)
|
211
qutebrowser/components/caretcommands.py
Normal file
211
qutebrowser/components/caretcommands.py
Normal file
@ -0,0 +1,211 @@
|
||||
# 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/>.
|
||||
|
||||
"""Commands related to caret browsing."""
|
||||
|
||||
|
||||
from qutebrowser.api import cmdutils, apitypes
|
||||
|
||||
|
||||
@cmdutils.register(modes=[cmdutils.KeyMode.caret])
|
||||
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
|
||||
@cmdutils.argument('count', value=cmdutils.Value.count)
|
||||
def move_to_next_line(tab: apitypes.Tab, count: int = 1) -> None:
|
||||
"""Move the cursor or selection to the next line.
|
||||
|
||||
Args:
|
||||
count: How many lines to move.
|
||||
"""
|
||||
tab.caret.move_to_next_line(count)
|
||||
|
||||
|
||||
@cmdutils.register(modes=[cmdutils.KeyMode.caret])
|
||||
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
|
||||
@cmdutils.argument('count', value=cmdutils.Value.count)
|
||||
def move_to_prev_line(tab: apitypes.Tab, count: int = 1) -> None:
|
||||
"""Move the cursor or selection to the prev line.
|
||||
|
||||
Args:
|
||||
count: How many lines to move.
|
||||
"""
|
||||
tab.caret.move_to_prev_line(count)
|
||||
|
||||
|
||||
@cmdutils.register(modes=[cmdutils.KeyMode.caret])
|
||||
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
|
||||
@cmdutils.argument('count', value=cmdutils.Value.count)
|
||||
def move_to_next_char(tab: apitypes.Tab, count: int = 1) -> None:
|
||||
"""Move the cursor or selection to the next char.
|
||||
|
||||
Args:
|
||||
count: How many lines to move.
|
||||
"""
|
||||
tab.caret.move_to_next_char(count)
|
||||
|
||||
|
||||
@cmdutils.register(modes=[cmdutils.KeyMode.caret])
|
||||
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
|
||||
@cmdutils.argument('count', value=cmdutils.Value.count)
|
||||
def move_to_prev_char(tab: apitypes.Tab, count: int = 1) -> None:
|
||||
"""Move the cursor or selection to the previous char.
|
||||
|
||||
Args:
|
||||
count: How many chars to move.
|
||||
"""
|
||||
tab.caret.move_to_prev_char(count)
|
||||
|
||||
|
||||
@cmdutils.register(modes=[cmdutils.KeyMode.caret])
|
||||
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
|
||||
@cmdutils.argument('count', value=cmdutils.Value.count)
|
||||
def move_to_end_of_word(tab: apitypes.Tab, count: int = 1) -> None:
|
||||
"""Move the cursor or selection to the end of the word.
|
||||
|
||||
Args:
|
||||
count: How many words to move.
|
||||
"""
|
||||
tab.caret.move_to_end_of_word(count)
|
||||
|
||||
|
||||
@cmdutils.register(modes=[cmdutils.KeyMode.caret])
|
||||
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
|
||||
@cmdutils.argument('count', value=cmdutils.Value.count)
|
||||
def move_to_next_word(tab: apitypes.Tab, count: int = 1) -> None:
|
||||
"""Move the cursor or selection to the next word.
|
||||
|
||||
Args:
|
||||
count: How many words to move.
|
||||
"""
|
||||
tab.caret.move_to_next_word(count)
|
||||
|
||||
|
||||
@cmdutils.register(modes=[cmdutils.KeyMode.caret])
|
||||
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
|
||||
@cmdutils.argument('count', value=cmdutils.Value.count)
|
||||
def move_to_prev_word(tab: apitypes.Tab, count: int = 1) -> None:
|
||||
"""Move the cursor or selection to the previous word.
|
||||
|
||||
Args:
|
||||
count: How many words to move.
|
||||
"""
|
||||
tab.caret.move_to_prev_word(count)
|
||||
|
||||
|
||||
@cmdutils.register(modes=[cmdutils.KeyMode.caret])
|
||||
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
|
||||
def move_to_start_of_line(tab: apitypes.Tab) -> None:
|
||||
"""Move the cursor or selection to the start of the line."""
|
||||
tab.caret.move_to_start_of_line()
|
||||
|
||||
|
||||
@cmdutils.register(modes=[cmdutils.KeyMode.caret])
|
||||
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
|
||||
def move_to_end_of_line(tab: apitypes.Tab) -> None:
|
||||
"""Move the cursor or selection to the end of line."""
|
||||
tab.caret.move_to_end_of_line()
|
||||
|
||||
|
||||
@cmdutils.register(modes=[cmdutils.KeyMode.caret])
|
||||
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
|
||||
@cmdutils.argument('count', value=cmdutils.Value.count)
|
||||
def move_to_start_of_next_block(tab: apitypes.Tab, count: int = 1) -> None:
|
||||
"""Move the cursor or selection to the start of next block.
|
||||
|
||||
Args:
|
||||
count: How many blocks to move.
|
||||
"""
|
||||
tab.caret.move_to_start_of_next_block(count)
|
||||
|
||||
|
||||
@cmdutils.register(modes=[cmdutils.KeyMode.caret])
|
||||
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
|
||||
@cmdutils.argument('count', value=cmdutils.Value.count)
|
||||
def move_to_start_of_prev_block(tab: apitypes.Tab, count: int = 1) -> None:
|
||||
"""Move the cursor or selection to the start of previous block.
|
||||
|
||||
Args:
|
||||
count: How many blocks to move.
|
||||
"""
|
||||
tab.caret.move_to_start_of_prev_block(count)
|
||||
|
||||
|
||||
@cmdutils.register(modes=[cmdutils.KeyMode.caret])
|
||||
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
|
||||
@cmdutils.argument('count', value=cmdutils.Value.count)
|
||||
def move_to_end_of_next_block(tab: apitypes.Tab, count: int = 1) -> None:
|
||||
"""Move the cursor or selection to the end of next block.
|
||||
|
||||
Args:
|
||||
count: How many blocks to move.
|
||||
"""
|
||||
tab.caret.move_to_end_of_next_block(count)
|
||||
|
||||
|
||||
@cmdutils.register(modes=[cmdutils.KeyMode.caret])
|
||||
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
|
||||
@cmdutils.argument('count', value=cmdutils.Value.count)
|
||||
def move_to_end_of_prev_block(tab: apitypes.Tab, count: int = 1) -> None:
|
||||
"""Move the cursor or selection to the end of previous block.
|
||||
|
||||
Args:
|
||||
count: How many blocks to move.
|
||||
"""
|
||||
tab.caret.move_to_end_of_prev_block(count)
|
||||
|
||||
|
||||
@cmdutils.register(modes=[cmdutils.KeyMode.caret])
|
||||
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
|
||||
def move_to_start_of_document(tab: apitypes.Tab) -> None:
|
||||
"""Move the cursor or selection to the start of the document."""
|
||||
tab.caret.move_to_start_of_document()
|
||||
|
||||
|
||||
@cmdutils.register(modes=[cmdutils.KeyMode.caret])
|
||||
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
|
||||
def move_to_end_of_document(tab: apitypes.Tab) -> None:
|
||||
"""Move the cursor or selection to the end of the document."""
|
||||
tab.caret.move_to_end_of_document()
|
||||
|
||||
|
||||
@cmdutils.register(modes=[cmdutils.KeyMode.caret])
|
||||
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
|
||||
def toggle_selection(tab: apitypes.Tab) -> None:
|
||||
"""Toggle caret selection mode."""
|
||||
tab.caret.toggle_selection()
|
||||
|
||||
|
||||
@cmdutils.register(modes=[cmdutils.KeyMode.caret])
|
||||
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
|
||||
def drop_selection(tab: apitypes.Tab) -> None:
|
||||
"""Drop selection and keep selection mode enabled."""
|
||||
tab.caret.drop_selection()
|
||||
|
||||
|
||||
@cmdutils.register()
|
||||
@cmdutils.argument('tab_obj', value=cmdutils.Value.cur_tab)
|
||||
def follow_selected(tab_obj: apitypes.Tab, *, tab: bool = False) -> None:
|
||||
"""Follow the selected text.
|
||||
|
||||
Args:
|
||||
tab: Load the selected link in a new tab.
|
||||
"""
|
||||
try:
|
||||
tab_obj.caret.follow_selected(tab=tab)
|
||||
except apitypes.WebTabError as e:
|
||||
raise cmdutils.CommandError(str(e))
|
312
qutebrowser/components/misccommands.py
Normal file
312
qutebrowser/components/misccommands.py
Normal file
@ -0,0 +1,312 @@
|
||||
# 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/>.
|
||||
|
||||
"""Various commands."""
|
||||
|
||||
import os
|
||||
import signal
|
||||
import functools
|
||||
import logging
|
||||
|
||||
try:
|
||||
import hunter
|
||||
except ImportError:
|
||||
hunter = None
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtPrintSupport import QPrintPreviewDialog
|
||||
|
||||
from qutebrowser.api import cmdutils, apitypes, message, config
|
||||
|
||||
|
||||
@cmdutils.register(name='reload')
|
||||
@cmdutils.argument('tab', value=cmdutils.Value.count_tab)
|
||||
def reloadpage(tab: apitypes.Tab, force: bool = False) -> None:
|
||||
"""Reload the current/[count]th tab.
|
||||
|
||||
Args:
|
||||
count: The tab index to reload, or None.
|
||||
force: Bypass the page cache.
|
||||
"""
|
||||
if tab is not None:
|
||||
tab.reload(force=force)
|
||||
|
||||
|
||||
@cmdutils.register()
|
||||
@cmdutils.argument('tab', value=cmdutils.Value.count_tab)
|
||||
def stop(tab: apitypes.Tab) -> None:
|
||||
"""Stop loading in the current/[count]th tab.
|
||||
|
||||
Args:
|
||||
count: The tab index to stop, or None.
|
||||
"""
|
||||
if tab is not None:
|
||||
tab.stop()
|
||||
|
||||
|
||||
def _print_preview(tab: apitypes.Tab) -> None:
|
||||
"""Show a print preview."""
|
||||
def print_callback(ok: bool) -> None:
|
||||
if not ok:
|
||||
message.error("Printing failed!")
|
||||
|
||||
tab.printing.check_preview_support()
|
||||
diag = QPrintPreviewDialog(tab)
|
||||
diag.setAttribute(Qt.WA_DeleteOnClose)
|
||||
diag.setWindowFlags(diag.windowFlags() | Qt.WindowMaximizeButtonHint |
|
||||
Qt.WindowMinimizeButtonHint)
|
||||
diag.paintRequested.connect(functools.partial(
|
||||
tab.printing.to_printer, callback=print_callback))
|
||||
diag.exec_()
|
||||
|
||||
|
||||
def _print_pdf(tab: apitypes.Tab, filename: str) -> None:
|
||||
"""Print to the given PDF file."""
|
||||
tab.printing.check_pdf_support()
|
||||
filename = os.path.expanduser(filename)
|
||||
directory = os.path.dirname(filename)
|
||||
if directory and not os.path.exists(directory):
|
||||
os.mkdir(directory)
|
||||
tab.printing.to_pdf(filename)
|
||||
logging.getLogger('misc').debug("Print to file: {}".format(filename))
|
||||
|
||||
|
||||
@cmdutils.register(name='print')
|
||||
@cmdutils.argument('tab', value=cmdutils.Value.count_tab)
|
||||
@cmdutils.argument('pdf', flag='f', metavar='file')
|
||||
def printpage(tab: apitypes.Tab,
|
||||
preview: bool = False, *,
|
||||
pdf: str = None) -> None:
|
||||
"""Print the current/[count]th tab.
|
||||
|
||||
Args:
|
||||
preview: Show preview instead of printing.
|
||||
count: The tab index to print, or None.
|
||||
pdf: The file path to write the PDF to.
|
||||
"""
|
||||
if tab is None:
|
||||
return
|
||||
|
||||
try:
|
||||
if preview:
|
||||
_print_preview(tab)
|
||||
elif pdf:
|
||||
_print_pdf(tab, pdf)
|
||||
else:
|
||||
tab.printing.show_dialog()
|
||||
except apitypes.WebTabError as e:
|
||||
raise cmdutils.CommandError(e)
|
||||
|
||||
|
||||
@cmdutils.register()
|
||||
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
|
||||
def home(tab: apitypes.Tab) -> None:
|
||||
"""Open main startpage in current tab."""
|
||||
if tab.data.pinned:
|
||||
message.info("Tab is pinned!")
|
||||
else:
|
||||
tab.load_url(config.val.url.start_pages[0])
|
||||
|
||||
|
||||
@cmdutils.register(debug=True)
|
||||
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
|
||||
def debug_dump_page(tab: apitypes.Tab, dest: str, plain: bool = False) -> None:
|
||||
"""Dump the current page's content to a file.
|
||||
|
||||
Args:
|
||||
dest: Where to write the file to.
|
||||
plain: Write plain text instead of HTML.
|
||||
"""
|
||||
dest = os.path.expanduser(dest)
|
||||
|
||||
def callback(data: str) -> None:
|
||||
"""Write the data to disk."""
|
||||
try:
|
||||
with open(dest, 'w', encoding='utf-8') as f:
|
||||
f.write(data)
|
||||
except OSError as e:
|
||||
message.error('Could not write page: {}'.format(e))
|
||||
else:
|
||||
message.info("Dumped page to {}.".format(dest))
|
||||
|
||||
tab.dump_async(callback, plain=plain)
|
||||
|
||||
|
||||
@cmdutils.register(maxsplit=0)
|
||||
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
|
||||
def insert_text(tab: apitypes.Tab, text: str) -> None:
|
||||
"""Insert text at cursor position.
|
||||
|
||||
Args:
|
||||
text: The text to insert.
|
||||
"""
|
||||
def _insert_text_cb(elem: apitypes.WebElement) -> None:
|
||||
if elem is None:
|
||||
message.error("No element focused!")
|
||||
return
|
||||
try:
|
||||
elem.insert_text(text)
|
||||
except apitypes.WebElemError as e:
|
||||
message.error(str(e))
|
||||
return
|
||||
|
||||
tab.elements.find_focused(_insert_text_cb)
|
||||
|
||||
|
||||
@cmdutils.register()
|
||||
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
|
||||
@cmdutils.argument('filter_', choices=['id'])
|
||||
def click_element(tab: apitypes.Tab, filter_: str, value: str, *,
|
||||
target: apitypes.ClickTarget =
|
||||
apitypes.ClickTarget.normal,
|
||||
force_event: bool = False) -> None:
|
||||
"""Click the element matching the given filter.
|
||||
|
||||
The given filter needs to result in exactly one element, otherwise, an
|
||||
error is shown.
|
||||
|
||||
Args:
|
||||
filter_: How to filter the elements.
|
||||
id: Get an element based on its ID.
|
||||
value: The value to filter for.
|
||||
target: How to open the clicked element (normal/tab/tab-bg/window).
|
||||
force_event: Force generating a fake click event.
|
||||
"""
|
||||
def single_cb(elem: apitypes.WebElement) -> None:
|
||||
"""Click a single element."""
|
||||
if elem is None:
|
||||
message.error("No element found with id {}!".format(value))
|
||||
return
|
||||
try:
|
||||
elem.click(target, force_event=force_event)
|
||||
except apitypes.WebElemError as e:
|
||||
message.error(str(e))
|
||||
return
|
||||
|
||||
handlers = {
|
||||
'id': (tab.elements.find_id, single_cb),
|
||||
}
|
||||
handler, callback = handlers[filter_]
|
||||
handler(value, callback)
|
||||
|
||||
|
||||
@cmdutils.register(debug=True)
|
||||
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
|
||||
@cmdutils.argument('count', value=cmdutils.Value.count)
|
||||
def debug_webaction(tab: apitypes.Tab, action: str, count: int = 1) -> None:
|
||||
"""Execute a webaction.
|
||||
|
||||
Available actions:
|
||||
http://doc.qt.io/archives/qt-5.5/qwebpage.html#WebAction-enum (WebKit)
|
||||
http://doc.qt.io/qt-5/qwebenginepage.html#WebAction-enum (WebEngine)
|
||||
|
||||
Args:
|
||||
action: The action to execute, e.g. MoveToNextChar.
|
||||
count: How many times to repeat the action.
|
||||
"""
|
||||
for _ in range(count):
|
||||
try:
|
||||
tab.action.run_string(action)
|
||||
except apitypes.WebTabError as e:
|
||||
raise cmdutils.CommandError(str(e))
|
||||
|
||||
|
||||
@cmdutils.register()
|
||||
@cmdutils.argument('tab', value=cmdutils.Value.count_tab)
|
||||
def tab_mute(tab: apitypes.Tab) -> None:
|
||||
"""Mute/Unmute the current/[count]th tab.
|
||||
|
||||
Args:
|
||||
count: The tab index to mute or unmute, or None
|
||||
"""
|
||||
if tab is None:
|
||||
return
|
||||
try:
|
||||
tab.audio.set_muted(not tab.audio.is_muted(), override=True)
|
||||
except apitypes.WebTabError as e:
|
||||
raise cmdutils.CommandError(e)
|
||||
|
||||
|
||||
@cmdutils.register()
|
||||
def nop() -> None:
|
||||
"""Do nothing."""
|
||||
|
||||
|
||||
@cmdutils.register()
|
||||
def message_error(text: str) -> None:
|
||||
"""Show an error message in the statusbar.
|
||||
|
||||
Args:
|
||||
text: The text to show.
|
||||
"""
|
||||
message.error(text)
|
||||
|
||||
|
||||
@cmdutils.register()
|
||||
@cmdutils.argument('count', value=cmdutils.Value.count)
|
||||
def message_info(text: str, count: int = 1) -> None:
|
||||
"""Show an info message in the statusbar.
|
||||
|
||||
Args:
|
||||
text: The text to show.
|
||||
count: How many times to show the message
|
||||
"""
|
||||
for _ in range(count):
|
||||
message.info(text)
|
||||
|
||||
|
||||
@cmdutils.register()
|
||||
def message_warning(text: str) -> None:
|
||||
"""Show a warning message in the statusbar.
|
||||
|
||||
Args:
|
||||
text: The text to show.
|
||||
"""
|
||||
message.warning(text)
|
||||
|
||||
|
||||
@cmdutils.register(debug=True)
|
||||
@cmdutils.argument('typ', choices=['exception', 'segfault'])
|
||||
def debug_crash(typ: str = 'exception') -> None:
|
||||
"""Crash for debugging purposes.
|
||||
|
||||
Args:
|
||||
typ: either 'exception' or 'segfault'.
|
||||
"""
|
||||
if typ == 'segfault':
|
||||
os.kill(os.getpid(), signal.SIGSEGV)
|
||||
raise Exception("Segfault failed (wat.)")
|
||||
else:
|
||||
raise Exception("Forced crash")
|
||||
|
||||
|
||||
@cmdutils.register(debug=True, maxsplit=0, no_cmd_split=True)
|
||||
def debug_trace(expr: str = "") -> None:
|
||||
"""Trace executed code via hunter.
|
||||
|
||||
Args:
|
||||
expr: What to trace, passed to hunter.
|
||||
"""
|
||||
if hunter is None:
|
||||
raise cmdutils.CommandError("You need to install 'hunter' to use this "
|
||||
"command!")
|
||||
try:
|
||||
eval('hunter.trace({})'.format(expr))
|
||||
except Exception as e:
|
||||
raise cmdutils.CommandError("{}: {}".format(e.__class__.__name__, e))
|
122
qutebrowser/components/scrollcommands.py
Normal file
122
qutebrowser/components/scrollcommands.py
Normal file
@ -0,0 +1,122 @@
|
||||
# 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/>.
|
||||
|
||||
"""Scrolling-related commands."""
|
||||
|
||||
from qutebrowser.api import cmdutils, apitypes
|
||||
|
||||
|
||||
@cmdutils.register()
|
||||
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
|
||||
@cmdutils.argument('count', value=cmdutils.Value.count)
|
||||
def scroll_px(tab: apitypes.Tab, dx: int, dy: int, count: int = 1) -> None:
|
||||
"""Scroll the current tab by 'count * dx/dy' pixels.
|
||||
|
||||
Args:
|
||||
dx: How much to scroll in x-direction.
|
||||
dy: How much to scroll in y-direction.
|
||||
count: multiplier
|
||||
"""
|
||||
dx *= count
|
||||
dy *= count
|
||||
cmdutils.check_overflow(dx, 'int')
|
||||
cmdutils.check_overflow(dy, 'int')
|
||||
tab.scroller.delta(dx, dy)
|
||||
|
||||
|
||||
@cmdutils.register()
|
||||
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
|
||||
@cmdutils.argument('count', value=cmdutils.Value.count)
|
||||
def scroll(tab: apitypes.Tab, direction: str, count: int = 1) -> None:
|
||||
"""Scroll the current tab in the given direction.
|
||||
|
||||
Note you can use `:run-with-count` to have a keybinding with a bigger
|
||||
scroll increment.
|
||||
|
||||
Args:
|
||||
direction: In which direction to scroll
|
||||
(up/down/left/right/top/bottom).
|
||||
count: multiplier
|
||||
"""
|
||||
funcs = {
|
||||
'up': tab.scroller.up,
|
||||
'down': tab.scroller.down,
|
||||
'left': tab.scroller.left,
|
||||
'right': tab.scroller.right,
|
||||
'top': tab.scroller.top,
|
||||
'bottom': tab.scroller.bottom,
|
||||
'page-up': tab.scroller.page_up,
|
||||
'page-down': tab.scroller.page_down,
|
||||
}
|
||||
try:
|
||||
func = funcs[direction]
|
||||
except KeyError:
|
||||
expected_values = ', '.join(sorted(funcs))
|
||||
raise cmdutils.CommandError("Invalid value {!r} for direction - "
|
||||
"expected one of: {}".format(
|
||||
direction, expected_values))
|
||||
|
||||
if direction in ['top', 'bottom']:
|
||||
func()
|
||||
else:
|
||||
func(count=count)
|
||||
|
||||
|
||||
@cmdutils.register()
|
||||
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
|
||||
@cmdutils.argument('count', value=cmdutils.Value.count)
|
||||
@cmdutils.argument('horizontal', flag='x')
|
||||
def scroll_to_perc(tab: apitypes.Tab, count: int = None,
|
||||
perc: float = None, horizontal: bool = False) -> None:
|
||||
"""Scroll to a specific percentage of the page.
|
||||
|
||||
The percentage can be given either as argument or as count.
|
||||
If no percentage is given, the page is scrolled to the end.
|
||||
|
||||
Args:
|
||||
perc: Percentage to scroll.
|
||||
horizontal: Scroll horizontally instead of vertically.
|
||||
count: Percentage to scroll.
|
||||
"""
|
||||
if perc is None and count is None:
|
||||
perc = 100
|
||||
elif count is not None:
|
||||
perc = count
|
||||
|
||||
if horizontal:
|
||||
x = perc
|
||||
y = None
|
||||
else:
|
||||
x = None
|
||||
y = perc
|
||||
|
||||
tab.scroller.before_jump_requested.emit()
|
||||
tab.scroller.to_perc(x, y)
|
||||
|
||||
|
||||
@cmdutils.register()
|
||||
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
|
||||
def scroll_to_anchor(tab: apitypes.Tab, name: str) -> None:
|
||||
"""Scroll to the given anchor in the document.
|
||||
|
||||
Args:
|
||||
name: The anchor to scroll to.
|
||||
"""
|
||||
tab.scroller.before_jump_requested.emit()
|
||||
tab.scroller.to_anchor(name)
|
95
qutebrowser/components/zoomcommands.py
Normal file
95
qutebrowser/components/zoomcommands.py
Normal file
@ -0,0 +1,95 @@
|
||||
# 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/>.
|
||||
|
||||
"""Zooming-related commands."""
|
||||
|
||||
from qutebrowser.api import cmdutils, apitypes, message, config
|
||||
|
||||
|
||||
@cmdutils.register()
|
||||
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
|
||||
@cmdutils.argument('count', value=cmdutils.Value.count)
|
||||
def zoom_in(tab: apitypes.Tab, count: int = 1, quiet: bool = False) -> None:
|
||||
"""Increase the zoom level for the current tab.
|
||||
|
||||
Args:
|
||||
count: How many steps to zoom in.
|
||||
quiet: Don't show a zoom level message.
|
||||
"""
|
||||
try:
|
||||
perc = tab.zoom.apply_offset(count)
|
||||
except ValueError as e:
|
||||
raise cmdutils.CommandError(e)
|
||||
if not quiet:
|
||||
message.info("Zoom level: {}%".format(int(perc)), replace=True)
|
||||
|
||||
|
||||
@cmdutils.register()
|
||||
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
|
||||
@cmdutils.argument('count', value=cmdutils.Value.count)
|
||||
def zoom_out(tab: apitypes.Tab, count: int = 1, quiet: bool = False) -> None:
|
||||
"""Decrease the zoom level for the current tab.
|
||||
|
||||
Args:
|
||||
count: How many steps to zoom out.
|
||||
quiet: Don't show a zoom level message.
|
||||
"""
|
||||
try:
|
||||
perc = tab.zoom.apply_offset(-count)
|
||||
except ValueError as e:
|
||||
raise cmdutils.CommandError(e)
|
||||
if not quiet:
|
||||
message.info("Zoom level: {}%".format(int(perc)), replace=True)
|
||||
|
||||
|
||||
@cmdutils.register()
|
||||
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
|
||||
@cmdutils.argument('count', value=cmdutils.Value.count)
|
||||
def zoom(tab: apitypes.Tab,
|
||||
level: str = None,
|
||||
count: int = None,
|
||||
quiet: bool = False) -> None:
|
||||
"""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].
|
||||
|
||||
Args:
|
||||
level: The zoom percentage to set.
|
||||
count: The zoom percentage to set.
|
||||
quiet: Don't show a zoom level message.
|
||||
"""
|
||||
if count is not None:
|
||||
int_level = count
|
||||
elif level is not None:
|
||||
try:
|
||||
int_level = int(level.rstrip('%'))
|
||||
except ValueError:
|
||||
raise cmdutils.CommandError("zoom: Invalid int value {}"
|
||||
.format(level))
|
||||
else:
|
||||
int_level = int(config.val.zoom.default)
|
||||
|
||||
try:
|
||||
tab.zoom.set_factor(int_level / 100)
|
||||
except ValueError:
|
||||
raise cmdutils.CommandError("Can't zoom {}%!".format(int_level))
|
||||
if not quiet:
|
||||
message.info("Zoom level: {}%".format(int_level), replace=True)
|
@ -22,19 +22,28 @@
|
||||
import copy
|
||||
import contextlib
|
||||
import functools
|
||||
import typing
|
||||
from typing import Any
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QUrl
|
||||
|
||||
from qutebrowser.config import configdata, configexc, configutils
|
||||
from qutebrowser.utils import utils, log, jinja
|
||||
from qutebrowser.utils import utils, log, jinja, urlmatch
|
||||
from qutebrowser.misc import objects
|
||||
from qutebrowser.keyinput import keyutils
|
||||
|
||||
MYPY = False
|
||||
if MYPY:
|
||||
# pylint: disable=unused-import,useless-suppression
|
||||
from typing import Tuple, MutableMapping
|
||||
from qutebrowser.config import configcache, configfiles
|
||||
from qutebrowser.misc import savemanager
|
||||
|
||||
# An easy way to access the config from other code via config.val.foo
|
||||
val = None
|
||||
instance = None
|
||||
key_instance = None
|
||||
cache = None
|
||||
val = typing.cast('ConfigContainer', None)
|
||||
instance = typing.cast('Config', None)
|
||||
key_instance = typing.cast('KeyConfig', None)
|
||||
cache = typing.cast('configcache.ConfigCache', None)
|
||||
|
||||
# Keeping track of all change filters to validate them later.
|
||||
change_filters = []
|
||||
@ -55,7 +64,7 @@ class change_filter: # noqa: N801,N806 pylint: disable=invalid-name
|
||||
_function: Whether a function rather than a method is decorated.
|
||||
"""
|
||||
|
||||
def __init__(self, option, function=False):
|
||||
def __init__(self, option: str, function: bool = False) -> None:
|
||||
"""Save decorator arguments.
|
||||
|
||||
Gets called on parse-time with the decorator arguments.
|
||||
@ -68,7 +77,7 @@ class change_filter: # noqa: N801,N806 pylint: disable=invalid-name
|
||||
self._function = function
|
||||
change_filters.append(self)
|
||||
|
||||
def validate(self):
|
||||
def validate(self) -> None:
|
||||
"""Make sure the configured option or prefix exists.
|
||||
|
||||
We can't do this in __init__ as configdata isn't ready yet.
|
||||
@ -77,7 +86,7 @@ class change_filter: # noqa: N801,N806 pylint: disable=invalid-name
|
||||
not configdata.is_valid_prefix(self._option)):
|
||||
raise configexc.NoOptionError(self._option)
|
||||
|
||||
def _check_match(self, option):
|
||||
def check_match(self, option: typing.Optional[str]) -> bool:
|
||||
"""Check if the given option matches the filter."""
|
||||
if option is None:
|
||||
# Called directly, not from a config change event.
|
||||
@ -90,7 +99,7 @@ class change_filter: # noqa: N801,N806 pylint: disable=invalid-name
|
||||
else:
|
||||
return False
|
||||
|
||||
def __call__(self, func):
|
||||
def __call__(self, func: typing.Callable) -> typing.Callable:
|
||||
"""Filter calls to the decorated function.
|
||||
|
||||
Gets called when a function should be decorated.
|
||||
@ -108,20 +117,21 @@ class change_filter: # noqa: N801,N806 pylint: disable=invalid-name
|
||||
"""
|
||||
if self._function:
|
||||
@functools.wraps(func)
|
||||
def wrapper(option=None):
|
||||
def func_wrapper(option: str = None) -> typing.Any:
|
||||
"""Call the underlying function."""
|
||||
if self._check_match(option):
|
||||
if self.check_match(option):
|
||||
return func()
|
||||
return None
|
||||
return func_wrapper
|
||||
else:
|
||||
@functools.wraps(func)
|
||||
def wrapper(wrapper_self, option=None):
|
||||
def meth_wrapper(wrapper_self: typing.Any,
|
||||
option: str = None) -> typing.Any:
|
||||
"""Call the underlying function."""
|
||||
if self._check_match(option):
|
||||
if self.check_match(option):
|
||||
return func(wrapper_self)
|
||||
return None
|
||||
|
||||
return wrapper
|
||||
return meth_wrapper
|
||||
|
||||
|
||||
class KeyConfig:
|
||||
@ -134,17 +144,22 @@ class KeyConfig:
|
||||
_config: The Config object to be used.
|
||||
"""
|
||||
|
||||
def __init__(self, config):
|
||||
_ReverseBindings = typing.Dict[str, typing.MutableSequence[str]]
|
||||
|
||||
def __init__(self, config: 'Config') -> None:
|
||||
self._config = config
|
||||
|
||||
def _validate(self, key, mode):
|
||||
def _validate(self, key: keyutils.KeySequence, mode: str) -> None:
|
||||
"""Validate the given key and mode."""
|
||||
# Catch old usage of this code
|
||||
assert isinstance(key, keyutils.KeySequence), key
|
||||
if mode not in configdata.DATA['bindings.default'].default:
|
||||
raise configexc.KeybindingError("Invalid mode {}!".format(mode))
|
||||
|
||||
def get_bindings_for(self, mode):
|
||||
def get_bindings_for(
|
||||
self,
|
||||
mode: str
|
||||
) -> typing.Dict[keyutils.KeySequence, str]:
|
||||
"""Get the combined bindings for the given mode."""
|
||||
bindings = dict(val.bindings.default[mode])
|
||||
for key, binding in val.bindings.commands[mode].items():
|
||||
@ -154,9 +169,9 @@ class KeyConfig:
|
||||
bindings[key] = binding
|
||||
return bindings
|
||||
|
||||
def get_reverse_bindings_for(self, mode):
|
||||
def get_reverse_bindings_for(self, mode: str) -> '_ReverseBindings':
|
||||
"""Get a dict of commands to a list of bindings for the mode."""
|
||||
cmd_to_keys = {}
|
||||
cmd_to_keys = {} # type: KeyConfig._ReverseBindings
|
||||
bindings = self.get_bindings_for(mode)
|
||||
for seq, full_cmd in sorted(bindings.items()):
|
||||
for cmd in full_cmd.split(';;'):
|
||||
@ -169,7 +184,10 @@ class KeyConfig:
|
||||
cmd_to_keys[cmd].insert(0, str(seq))
|
||||
return cmd_to_keys
|
||||
|
||||
def get_command(self, key, mode, default=False):
|
||||
def get_command(self,
|
||||
key: keyutils.KeySequence,
|
||||
mode: str,
|
||||
default: bool = False) -> str:
|
||||
"""Get the command for a given key (or None)."""
|
||||
self._validate(key, mode)
|
||||
if default:
|
||||
@ -178,7 +196,11 @@ class KeyConfig:
|
||||
bindings = self.get_bindings_for(mode)
|
||||
return bindings.get(key, None)
|
||||
|
||||
def bind(self, key, command, *, mode, save_yaml=False):
|
||||
def bind(self,
|
||||
key: keyutils.KeySequence,
|
||||
command: str, *,
|
||||
mode: str,
|
||||
save_yaml: bool = False) -> None:
|
||||
"""Add a new binding from key to command."""
|
||||
if command is not None and not command.strip():
|
||||
raise configexc.KeybindingError(
|
||||
@ -186,8 +208,8 @@ class KeyConfig:
|
||||
'mode'.format(key, mode))
|
||||
|
||||
self._validate(key, mode)
|
||||
log.keyboard.vdebug("Adding binding {} -> {} in mode {}.".format(
|
||||
key, command, mode))
|
||||
log.keyboard.vdebug( # type: ignore
|
||||
"Adding binding {} -> {} in mode {}.".format(key, command, mode))
|
||||
|
||||
bindings = self._config.get_mutable_obj('bindings.commands')
|
||||
if mode not in bindings:
|
||||
@ -195,7 +217,10 @@ class KeyConfig:
|
||||
bindings[mode][str(key)] = command
|
||||
self._config.update_mutables(save_yaml=save_yaml)
|
||||
|
||||
def bind_default(self, key, *, mode='normal', save_yaml=False):
|
||||
def bind_default(self,
|
||||
key: keyutils.KeySequence, *,
|
||||
mode: str = 'normal',
|
||||
save_yaml: bool = False) -> None:
|
||||
"""Restore a default keybinding."""
|
||||
self._validate(key, mode)
|
||||
|
||||
@ -207,7 +232,10 @@ class KeyConfig:
|
||||
"Can't find binding '{}' in {} mode".format(key, mode))
|
||||
self._config.update_mutables(save_yaml=save_yaml)
|
||||
|
||||
def unbind(self, key, *, mode='normal', save_yaml=False):
|
||||
def unbind(self,
|
||||
key: keyutils.KeySequence, *,
|
||||
mode: str = 'normal',
|
||||
save_yaml: bool = False) -> None:
|
||||
"""Unbind the given key in the given mode."""
|
||||
self._validate(key, mode)
|
||||
|
||||
@ -248,24 +276,27 @@ class Config(QObject):
|
||||
MUTABLE_TYPES = (dict, list)
|
||||
changed = pyqtSignal(str)
|
||||
|
||||
def __init__(self, yaml_config, parent=None):
|
||||
def __init__(self,
|
||||
yaml_config: 'configfiles.YamlConfig',
|
||||
parent: QObject = None) -> None:
|
||||
super().__init__(parent)
|
||||
self.changed.connect(_render_stylesheet.cache_clear)
|
||||
self._mutables = {}
|
||||
self._mutables = {} # type: MutableMapping[str, Tuple[Any, Any]]
|
||||
self._yaml = yaml_config
|
||||
self._init_values()
|
||||
|
||||
def _init_values(self):
|
||||
def _init_values(self) -> None:
|
||||
"""Populate the self._values dict."""
|
||||
self._values = {}
|
||||
self._values = {} # type: typing.Mapping
|
||||
for name, opt in configdata.DATA.items():
|
||||
self._values[name] = configutils.Values(opt)
|
||||
|
||||
def __iter__(self):
|
||||
def __iter__(self) -> typing.Iterator[configutils.Values]:
|
||||
"""Iterate over configutils.Values items."""
|
||||
yield from self._values.values()
|
||||
|
||||
def init_save_manager(self, save_manager):
|
||||
def init_save_manager(self,
|
||||
save_manager: 'savemanager.SaveManager') -> None:
|
||||
"""Make sure the config gets saved properly.
|
||||
|
||||
We do this outside of __init__ because the config gets created before
|
||||
@ -273,7 +304,10 @@ class Config(QObject):
|
||||
"""
|
||||
self._yaml.init_save_manager(save_manager)
|
||||
|
||||
def _set_value(self, opt, value, pattern=None):
|
||||
def _set_value(self,
|
||||
opt: 'configdata.Option',
|
||||
value: Any,
|
||||
pattern: urlmatch.UrlPattern = None) -> None:
|
||||
"""Set the given option to the given value."""
|
||||
if not isinstance(objects.backend, objects.NoBackend):
|
||||
if objects.backend not in opt.backends:
|
||||
@ -288,12 +322,12 @@ class Config(QObject):
|
||||
log.config.debug("Config option changed: {} = {}".format(
|
||||
opt.name, value))
|
||||
|
||||
def _check_yaml(self, opt, save_yaml):
|
||||
def _check_yaml(self, opt: 'configdata.Option', save_yaml: bool) -> None:
|
||||
"""Make sure the given option may be set in autoconfig.yml."""
|
||||
if save_yaml and opt.no_autoconfig:
|
||||
raise configexc.NoAutoconfigError(opt.name)
|
||||
|
||||
def read_yaml(self):
|
||||
def read_yaml(self) -> None:
|
||||
"""Read the YAML settings from self._yaml."""
|
||||
self._yaml.load()
|
||||
for values in self._yaml:
|
||||
@ -301,7 +335,7 @@ class Config(QObject):
|
||||
self._set_value(values.opt, scoped.value,
|
||||
pattern=scoped.pattern)
|
||||
|
||||
def get_opt(self, name):
|
||||
def get_opt(self, name: str) -> 'configdata.Option':
|
||||
"""Get a configdata.Option object for the given setting."""
|
||||
try:
|
||||
return configdata.DATA[name]
|
||||
@ -312,7 +346,10 @@ class Config(QObject):
|
||||
name, deleted=deleted, renamed=renamed)
|
||||
raise exception from None
|
||||
|
||||
def get(self, name, url=None, *, fallback=True):
|
||||
def get(self,
|
||||
name: str,
|
||||
url: QUrl = None, *,
|
||||
fallback: bool = True) -> Any:
|
||||
"""Get the given setting converted for Python code.
|
||||
|
||||
Args:
|
||||
@ -322,7 +359,7 @@ class Config(QObject):
|
||||
obj = self.get_obj(name, url=url, fallback=fallback)
|
||||
return opt.typ.to_py(obj)
|
||||
|
||||
def _maybe_copy(self, value):
|
||||
def _maybe_copy(self, value: Any) -> Any:
|
||||
"""Copy the value if it could potentially be mutated."""
|
||||
if isinstance(value, self.MUTABLE_TYPES):
|
||||
# For mutable objects, create a copy so we don't accidentally
|
||||
@ -333,7 +370,10 @@ class Config(QObject):
|
||||
assert value.__hash__ is not None, value
|
||||
return value
|
||||
|
||||
def get_obj(self, name, *, url=None, fallback=True):
|
||||
def get_obj(self,
|
||||
name: str, *,
|
||||
url: QUrl = None,
|
||||
fallback: bool = True) -> Any:
|
||||
"""Get the given setting as object (for YAML/config.py).
|
||||
|
||||
Note that the returned values are not watched for mutation.
|
||||
@ -343,7 +383,10 @@ class Config(QObject):
|
||||
value = self._values[name].get_for_url(url, fallback=fallback)
|
||||
return self._maybe_copy(value)
|
||||
|
||||
def get_obj_for_pattern(self, name, *, pattern):
|
||||
def get_obj_for_pattern(
|
||||
self, name: str, *,
|
||||
pattern: typing.Optional[urlmatch.UrlPattern]
|
||||
) -> Any:
|
||||
"""Get the given setting as object (for YAML/config.py).
|
||||
|
||||
This gets the overridden value for a given pattern, or
|
||||
@ -353,11 +396,12 @@ class Config(QObject):
|
||||
value = self._values[name].get_for_pattern(pattern, fallback=False)
|
||||
return self._maybe_copy(value)
|
||||
|
||||
def get_mutable_obj(self, name, *, pattern=None):
|
||||
def get_mutable_obj(self, name: str, *,
|
||||
pattern: urlmatch.UrlPattern = None) -> Any:
|
||||
"""Get an object which can be mutated, e.g. in a config.py.
|
||||
|
||||
If a pattern is given, return the value for that pattern.
|
||||
Note that it's impossible to get a mutable object for an URL as we
|
||||
Note that it's impossible to get a mutable object for a URL as we
|
||||
wouldn't know what pattern to apply.
|
||||
"""
|
||||
self.get_opt(name) # To make sure it exists
|
||||
@ -378,7 +422,8 @@ class Config(QObject):
|
||||
|
||||
return copy_value
|
||||
|
||||
def get_str(self, name, *, pattern=None):
|
||||
def get_str(self, name: str, *,
|
||||
pattern: urlmatch.UrlPattern = None) -> str:
|
||||
"""Get the given setting as string.
|
||||
|
||||
If a pattern is given, get the setting for the given pattern or
|
||||
@ -389,7 +434,10 @@ class Config(QObject):
|
||||
value = values.get_for_pattern(pattern)
|
||||
return opt.typ.to_str(value)
|
||||
|
||||
def set_obj(self, name, value, *, pattern=None, save_yaml=False):
|
||||
def set_obj(self, name: str,
|
||||
value: Any, *,
|
||||
pattern: urlmatch.UrlPattern = None,
|
||||
save_yaml: bool = False) -> None:
|
||||
"""Set the given setting from a YAML/config.py object.
|
||||
|
||||
If save_yaml=True is given, store the new value to YAML.
|
||||
@ -400,7 +448,10 @@ class Config(QObject):
|
||||
if save_yaml:
|
||||
self._yaml.set_obj(name, value, pattern=pattern)
|
||||
|
||||
def set_str(self, name, value, *, pattern=None, save_yaml=False):
|
||||
def set_str(self, name: str,
|
||||
value: str, *,
|
||||
pattern: urlmatch.UrlPattern = None,
|
||||
save_yaml: bool = False) -> None:
|
||||
"""Set the given setting from a string.
|
||||
|
||||
If save_yaml=True is given, store the new value to YAML.
|
||||
@ -415,7 +466,9 @@ class Config(QObject):
|
||||
if save_yaml:
|
||||
self._yaml.set_obj(name, converted, pattern=pattern)
|
||||
|
||||
def unset(self, name, *, save_yaml=False, pattern=None):
|
||||
def unset(self, name: str, *,
|
||||
save_yaml: bool = False,
|
||||
pattern: urlmatch.UrlPattern = None) -> None:
|
||||
"""Set the given setting back to its default."""
|
||||
opt = self.get_opt(name)
|
||||
self._check_yaml(opt, save_yaml)
|
||||
@ -426,7 +479,7 @@ class Config(QObject):
|
||||
if save_yaml:
|
||||
self._yaml.unset(name, pattern=pattern)
|
||||
|
||||
def clear(self, *, save_yaml=False):
|
||||
def clear(self, *, save_yaml: bool = False) -> None:
|
||||
"""Clear all settings in the config.
|
||||
|
||||
If save_yaml=True is given, also remove all customization from the YAML
|
||||
@ -440,7 +493,7 @@ class Config(QObject):
|
||||
if save_yaml:
|
||||
self._yaml.clear()
|
||||
|
||||
def update_mutables(self, *, save_yaml=False):
|
||||
def update_mutables(self, *, save_yaml: bool = False) -> None:
|
||||
"""Update mutable settings if they changed.
|
||||
|
||||
Every time someone calls get_obj() on a mutable object, we save a
|
||||
@ -455,7 +508,7 @@ class Config(QObject):
|
||||
self.set_obj(name, new_value, save_yaml=save_yaml)
|
||||
self._mutables = {}
|
||||
|
||||
def dump_userconfig(self):
|
||||
def dump_userconfig(self) -> str:
|
||||
"""Get the part of the config which was changed by the user.
|
||||
|
||||
Return:
|
||||
@ -484,7 +537,10 @@ class ConfigContainer:
|
||||
_pattern: The URL pattern to be used.
|
||||
"""
|
||||
|
||||
def __init__(self, config, configapi=None, prefix='', pattern=None):
|
||||
def __init__(self, config: Config,
|
||||
configapi: 'configfiles.ConfigAPI' = None,
|
||||
prefix: str = '',
|
||||
pattern: urlmatch.UrlPattern = None) -> None:
|
||||
self._config = config
|
||||
self._prefix = prefix
|
||||
self._configapi = configapi
|
||||
@ -492,13 +548,13 @@ class ConfigContainer:
|
||||
if configapi is None and pattern is not None:
|
||||
raise TypeError("Can't use pattern without configapi!")
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
return utils.get_repr(self, constructor=True, config=self._config,
|
||||
configapi=self._configapi, prefix=self._prefix,
|
||||
pattern=self._pattern)
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _handle_error(self, action, name):
|
||||
def _handle_error(self, action: str, name: str) -> typing.Iterator[None]:
|
||||
try:
|
||||
yield
|
||||
except configexc.Error as e:
|
||||
@ -507,7 +563,7 @@ class ConfigContainer:
|
||||
text = "While {} '{}'".format(action, name)
|
||||
self._configapi.errors.append(configexc.ConfigErrorDesc(text, e))
|
||||
|
||||
def __getattr__(self, attr):
|
||||
def __getattr__(self, attr: str) -> Any:
|
||||
"""Get an option or a new ConfigContainer with the added prefix.
|
||||
|
||||
If we get an option which exists, we return the value for it.
|
||||
@ -534,7 +590,7 @@ class ConfigContainer:
|
||||
return self._config.get_mutable_obj(
|
||||
name, pattern=self._pattern)
|
||||
|
||||
def __setattr__(self, attr, value):
|
||||
def __setattr__(self, attr: str, value: Any) -> None:
|
||||
"""Set the given option in the config."""
|
||||
if attr.startswith('_'):
|
||||
super().__setattr__(attr, value)
|
||||
@ -544,7 +600,7 @@ class ConfigContainer:
|
||||
with self._handle_error('setting', name):
|
||||
self._config.set_obj(name, value, pattern=self._pattern)
|
||||
|
||||
def _join(self, attr):
|
||||
def _join(self, attr: str) -> str:
|
||||
"""Get the prefix joined with the given attribute."""
|
||||
if self._prefix:
|
||||
return '{}.{}'.format(self._prefix, attr)
|
||||
@ -552,8 +608,10 @@ class ConfigContainer:
|
||||
return attr
|
||||
|
||||
|
||||
def set_register_stylesheet(obj, *, stylesheet=None, update=True):
|
||||
"""Set the stylesheet for an object based on it's STYLESHEET attribute.
|
||||
def set_register_stylesheet(obj: QObject, *,
|
||||
stylesheet: str = None,
|
||||
update: bool = True) -> None:
|
||||
"""Set the stylesheet for an object.
|
||||
|
||||
Also, register an update when the config is changed.
|
||||
|
||||
@ -568,7 +626,7 @@ def set_register_stylesheet(obj, *, stylesheet=None, update=True):
|
||||
|
||||
|
||||
@functools.lru_cache()
|
||||
def _render_stylesheet(stylesheet):
|
||||
def _render_stylesheet(stylesheet: str) -> str:
|
||||
"""Render the given stylesheet jinja template."""
|
||||
with jinja.environment.no_autoescape():
|
||||
template = jinja.environment.from_string(stylesheet)
|
||||
@ -584,7 +642,9 @@ class StyleSheetObserver(QObject):
|
||||
_stylesheet: The stylesheet template to use.
|
||||
"""
|
||||
|
||||
def __init__(self, obj, stylesheet, update):
|
||||
def __init__(self, obj: QObject,
|
||||
stylesheet: typing.Optional[str],
|
||||
update: bool) -> None:
|
||||
super().__init__()
|
||||
self._obj = obj
|
||||
self._update = update
|
||||
@ -593,11 +653,11 @@ class StyleSheetObserver(QObject):
|
||||
if self._update:
|
||||
self.setParent(self._obj)
|
||||
if stylesheet is None:
|
||||
self._stylesheet = obj.STYLESHEET
|
||||
self._stylesheet = obj.STYLESHEET # type: str
|
||||
else:
|
||||
self._stylesheet = stylesheet
|
||||
|
||||
def _get_stylesheet(self):
|
||||
def _get_stylesheet(self) -> str:
|
||||
"""Format a stylesheet based on a template.
|
||||
|
||||
Return:
|
||||
@ -606,19 +666,15 @@ class StyleSheetObserver(QObject):
|
||||
return _render_stylesheet(self._stylesheet)
|
||||
|
||||
@pyqtSlot()
|
||||
def _update_stylesheet(self):
|
||||
def _update_stylesheet(self) -> None:
|
||||
"""Update the stylesheet for obj."""
|
||||
self._obj.setStyleSheet(self._get_stylesheet())
|
||||
|
||||
def register(self):
|
||||
"""Do a first update and listen for more.
|
||||
|
||||
Args:
|
||||
update: if False, don't listen for future updates.
|
||||
"""
|
||||
def register(self) -> None:
|
||||
"""Do a first update and listen for more."""
|
||||
qss = self._get_stylesheet()
|
||||
log.config.vdebug("stylesheet for {}: {}".format(
|
||||
self._obj.__class__.__name__, qss))
|
||||
log.config.vdebug( # type: ignore
|
||||
"stylesheet for {}: {}".format(self._obj.__class__.__name__, qss))
|
||||
self._obj.setStyleSheet(qss)
|
||||
if self._update:
|
||||
instance.changed.connect(self._update_stylesheet)
|
||||
|
@ -20,6 +20,8 @@
|
||||
|
||||
"""Implementation of a basic config cache."""
|
||||
|
||||
import typing
|
||||
|
||||
from qutebrowser.config import config
|
||||
|
||||
|
||||
@ -36,14 +38,14 @@ class ConfigCache:
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._cache = {}
|
||||
self._cache = {} # type: typing.Dict[str, typing.Any]
|
||||
config.instance.changed.connect(self._on_config_changed)
|
||||
|
||||
def _on_config_changed(self, attr: str) -> None:
|
||||
if attr in self._cache:
|
||||
self._cache[attr] = config.instance.get(attr)
|
||||
|
||||
def __getitem__(self, attr: str):
|
||||
def __getitem__(self, attr: str) -> typing.Any:
|
||||
if attr not in self._cache:
|
||||
assert not config.instance.get_opt(attr).supports_pattern
|
||||
self._cache[attr] = config.instance.get(attr)
|
||||
|
@ -19,36 +19,47 @@
|
||||
|
||||
"""Commands related to the configuration."""
|
||||
|
||||
import typing
|
||||
import os.path
|
||||
import contextlib
|
||||
|
||||
from PyQt5.QtCore import QUrl
|
||||
|
||||
from qutebrowser.commands import cmdexc, cmdutils
|
||||
from qutebrowser.api import cmdutils
|
||||
from qutebrowser.completion.models import configmodel
|
||||
from qutebrowser.utils import objreg, message, standarddir, urlmatch
|
||||
from qutebrowser.config import configtypes, configexc, configfiles, configdata
|
||||
from qutebrowser.misc import editor
|
||||
from qutebrowser.keyinput import keyutils
|
||||
|
||||
MYPY = False
|
||||
if MYPY:
|
||||
# pylint: disable=unused-import,useless-suppression
|
||||
from qutebrowser.config.config import Config, KeyConfig
|
||||
|
||||
|
||||
class ConfigCommands:
|
||||
|
||||
"""qutebrowser commands related to the configuration."""
|
||||
|
||||
def __init__(self, config, keyconfig):
|
||||
def __init__(self,
|
||||
config: 'Config',
|
||||
keyconfig: 'KeyConfig') -> None:
|
||||
self._config = config
|
||||
self._keyconfig = keyconfig
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _handle_config_error(self):
|
||||
def _handle_config_error(self) -> typing.Iterator[None]:
|
||||
"""Catch errors in set_command and raise CommandError."""
|
||||
try:
|
||||
yield
|
||||
except configexc.Error as e:
|
||||
raise cmdexc.CommandError(str(e))
|
||||
raise cmdutils.CommandError(str(e))
|
||||
|
||||
def _parse_pattern(self, pattern):
|
||||
def _parse_pattern(
|
||||
self,
|
||||
pattern: typing.Optional[str]
|
||||
) -> typing.Optional[urlmatch.UrlPattern]:
|
||||
"""Parse a pattern string argument to a pattern."""
|
||||
if pattern is None:
|
||||
return None
|
||||
@ -56,17 +67,18 @@ class ConfigCommands:
|
||||
try:
|
||||
return urlmatch.UrlPattern(pattern)
|
||||
except urlmatch.ParseError as e:
|
||||
raise cmdexc.CommandError("Error while parsing {}: {}"
|
||||
.format(pattern, str(e)))
|
||||
raise cmdutils.CommandError("Error while parsing {}: {}"
|
||||
.format(pattern, str(e)))
|
||||
|
||||
def _parse_key(self, key):
|
||||
def _parse_key(self, key: str) -> keyutils.KeySequence:
|
||||
"""Parse a key argument."""
|
||||
try:
|
||||
return keyutils.KeySequence.parse(key)
|
||||
except keyutils.KeyParseError as e:
|
||||
raise cmdexc.CommandError(str(e))
|
||||
raise cmdutils.CommandError(str(e))
|
||||
|
||||
def _print_value(self, option, pattern):
|
||||
def _print_value(self, option: str,
|
||||
pattern: typing.Optional[urlmatch.UrlPattern]) -> None:
|
||||
"""Print the value of the given option."""
|
||||
with self._handle_config_error():
|
||||
value = self._config.get_str(option, pattern=pattern)
|
||||
@ -79,10 +91,11 @@ class ConfigCommands:
|
||||
@cmdutils.register(instance='config-commands')
|
||||
@cmdutils.argument('option', completion=configmodel.option)
|
||||
@cmdutils.argument('value', completion=configmodel.value)
|
||||
@cmdutils.argument('win_id', win_id=True)
|
||||
@cmdutils.argument('win_id', value=cmdutils.Value.win_id)
|
||||
@cmdutils.argument('pattern', flag='u')
|
||||
def set(self, win_id, option=None, value=None, temp=False, print_=False,
|
||||
*, pattern=None):
|
||||
def set(self, win_id: int, option: str = None, value: str = None,
|
||||
temp: bool = False, print_: bool = False,
|
||||
*, pattern: str = None) -> None:
|
||||
"""Set an option.
|
||||
|
||||
If the option name ends with '?' or no value is provided, the
|
||||
@ -101,35 +114,35 @@ class ConfigCommands:
|
||||
if option is None:
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=win_id)
|
||||
tabbed_browser.openurl(QUrl('qute://settings'), newtab=False)
|
||||
tabbed_browser.load_url(QUrl('qute://settings'), newtab=False)
|
||||
return
|
||||
|
||||
if option.endswith('!'):
|
||||
raise cmdexc.CommandError("Toggling values was moved to the "
|
||||
":config-cycle command")
|
||||
raise cmdutils.CommandError("Toggling values was moved to the "
|
||||
":config-cycle command")
|
||||
|
||||
pattern = self._parse_pattern(pattern)
|
||||
parsed_pattern = self._parse_pattern(pattern)
|
||||
|
||||
if option.endswith('?') and option != '?':
|
||||
self._print_value(option[:-1], pattern=pattern)
|
||||
self._print_value(option[:-1], pattern=parsed_pattern)
|
||||
return
|
||||
|
||||
with self._handle_config_error():
|
||||
if value is None:
|
||||
self._print_value(option, pattern=pattern)
|
||||
self._print_value(option, pattern=parsed_pattern)
|
||||
else:
|
||||
self._config.set_str(option, value, pattern=pattern,
|
||||
self._config.set_str(option, value, pattern=parsed_pattern,
|
||||
save_yaml=not temp)
|
||||
|
||||
if print_:
|
||||
self._print_value(option, pattern=pattern)
|
||||
self._print_value(option, pattern=parsed_pattern)
|
||||
|
||||
@cmdutils.register(instance='config-commands', maxsplit=1,
|
||||
no_cmd_split=True, no_replace_variables=True)
|
||||
@cmdutils.argument('command', completion=configmodel.bind)
|
||||
@cmdutils.argument('win_id', win_id=True)
|
||||
def bind(self, win_id, key=None, command=None, *, mode='normal',
|
||||
default=False):
|
||||
@cmdutils.argument('win_id', value=cmdutils.Value.win_id)
|
||||
def bind(self, win_id: str, key: str = None, command: str = None, *,
|
||||
mode: str = 'normal', default: bool = False) -> None:
|
||||
"""Bind a key to a command.
|
||||
|
||||
If no command is given, show the current binding for the given key.
|
||||
@ -147,7 +160,7 @@ class ConfigCommands:
|
||||
if key is None:
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=win_id)
|
||||
tabbed_browser.openurl(QUrl('qute://bindings'), newtab=True)
|
||||
tabbed_browser.load_url(QUrl('qute://bindings'), newtab=True)
|
||||
return
|
||||
|
||||
seq = self._parse_key(key)
|
||||
@ -174,7 +187,7 @@ class ConfigCommands:
|
||||
self._keyconfig.bind(seq, command, mode=mode, save_yaml=True)
|
||||
|
||||
@cmdutils.register(instance='config-commands')
|
||||
def unbind(self, key, *, mode='normal'):
|
||||
def unbind(self, key: str, *, mode: str = 'normal') -> None:
|
||||
"""Unbind a keychain.
|
||||
|
||||
Args:
|
||||
@ -191,8 +204,9 @@ class ConfigCommands:
|
||||
@cmdutils.argument('option', completion=configmodel.option)
|
||||
@cmdutils.argument('values', completion=configmodel.value)
|
||||
@cmdutils.argument('pattern', flag='u')
|
||||
def config_cycle(self, option, *values, pattern=None, temp=False,
|
||||
print_=False):
|
||||
def config_cycle(self, option: str, *values: str,
|
||||
pattern: str = None,
|
||||
temp: bool = False, print_: bool = False) -> None:
|
||||
"""Cycle an option between multiple values.
|
||||
|
||||
Args:
|
||||
@ -202,42 +216,42 @@ class ConfigCommands:
|
||||
temp: Set value temporarily until qutebrowser is closed.
|
||||
print_: Print the value after setting.
|
||||
"""
|
||||
pattern = self._parse_pattern(pattern)
|
||||
parsed_pattern = self._parse_pattern(pattern)
|
||||
|
||||
with self._handle_config_error():
|
||||
opt = self._config.get_opt(option)
|
||||
old_value = self._config.get_obj_for_pattern(option,
|
||||
pattern=pattern)
|
||||
old_value = self._config.get_obj_for_pattern(
|
||||
option, pattern=parsed_pattern)
|
||||
|
||||
if not values and isinstance(opt.typ, configtypes.Bool):
|
||||
values = ['true', 'false']
|
||||
values = ('true', 'false')
|
||||
|
||||
if len(values) < 2:
|
||||
raise cmdexc.CommandError("Need at least two values for "
|
||||
"non-boolean settings.")
|
||||
raise cmdutils.CommandError("Need at least two values for "
|
||||
"non-boolean settings.")
|
||||
|
||||
# Use the next valid value from values, or the first if the current
|
||||
# value does not appear in the list
|
||||
with self._handle_config_error():
|
||||
values = [opt.typ.from_str(val) for val in values]
|
||||
cycle_values = [opt.typ.from_str(val) for val in values]
|
||||
|
||||
try:
|
||||
idx = values.index(old_value)
|
||||
idx = (idx + 1) % len(values)
|
||||
value = values[idx]
|
||||
idx = cycle_values.index(old_value)
|
||||
idx = (idx + 1) % len(cycle_values)
|
||||
value = cycle_values[idx]
|
||||
except ValueError:
|
||||
value = values[0]
|
||||
value = cycle_values[0]
|
||||
|
||||
with self._handle_config_error():
|
||||
self._config.set_obj(option, value, pattern=pattern,
|
||||
self._config.set_obj(option, value, pattern=parsed_pattern,
|
||||
save_yaml=not temp)
|
||||
|
||||
if print_:
|
||||
self._print_value(option, pattern=pattern)
|
||||
self._print_value(option, pattern=parsed_pattern)
|
||||
|
||||
@cmdutils.register(instance='config-commands')
|
||||
@cmdutils.argument('option', completion=configmodel.customized_option)
|
||||
def config_unset(self, option, temp=False):
|
||||
def config_unset(self, option: str, temp: bool = False) -> None:
|
||||
"""Unset an option.
|
||||
|
||||
This sets an option back to its default and removes it from
|
||||
@ -252,7 +266,8 @@ class ConfigCommands:
|
||||
|
||||
@cmdutils.register(instance='config-commands')
|
||||
@cmdutils.argument('option', completion=configmodel.list_option)
|
||||
def config_list_add(self, option, value, temp=False):
|
||||
def config_list_add(self, option: str, value: str,
|
||||
temp: bool = False) -> None:
|
||||
"""Append a value to a config option that is a list.
|
||||
|
||||
Args:
|
||||
@ -263,8 +278,8 @@ class ConfigCommands:
|
||||
opt = self._config.get_opt(option)
|
||||
valid_list_types = (configtypes.List, configtypes.ListOrValue)
|
||||
if not isinstance(opt.typ, valid_list_types):
|
||||
raise cmdexc.CommandError(":config-list-add can only be used for "
|
||||
"lists")
|
||||
raise cmdutils.CommandError(":config-list-add can only be used "
|
||||
"for lists")
|
||||
|
||||
with self._handle_config_error():
|
||||
option_value = self._config.get_mutable_obj(option)
|
||||
@ -273,7 +288,8 @@ class ConfigCommands:
|
||||
|
||||
@cmdutils.register(instance='config-commands')
|
||||
@cmdutils.argument('option', completion=configmodel.dict_option)
|
||||
def config_dict_add(self, option, key, value, temp=False, replace=False):
|
||||
def config_dict_add(self, option: str, key: str, value: str,
|
||||
temp: bool = False, replace: bool = False) -> None:
|
||||
"""Add a key/value pair to a dictionary option.
|
||||
|
||||
Args:
|
||||
@ -286,23 +302,24 @@ class ConfigCommands:
|
||||
"""
|
||||
opt = self._config.get_opt(option)
|
||||
if not isinstance(opt.typ, configtypes.Dict):
|
||||
raise cmdexc.CommandError(":config-dict-add can only be used for "
|
||||
"dicts")
|
||||
raise cmdutils.CommandError(":config-dict-add can only be used "
|
||||
"for dicts")
|
||||
|
||||
with self._handle_config_error():
|
||||
option_value = self._config.get_mutable_obj(option)
|
||||
|
||||
if key in option_value and not replace:
|
||||
raise cmdexc.CommandError("{} already exists in {} - use "
|
||||
"--replace to overwrite!"
|
||||
.format(key, option))
|
||||
raise cmdutils.CommandError("{} already exists in {} - use "
|
||||
"--replace to overwrite!"
|
||||
.format(key, option))
|
||||
|
||||
option_value[key] = value
|
||||
self._config.update_mutables(save_yaml=not temp)
|
||||
|
||||
@cmdutils.register(instance='config-commands')
|
||||
@cmdutils.argument('option', completion=configmodel.list_option)
|
||||
def config_list_remove(self, option, value, temp=False):
|
||||
def config_list_remove(self, option: str, value: str,
|
||||
temp: bool = False) -> None:
|
||||
"""Remove a value from a list.
|
||||
|
||||
Args:
|
||||
@ -313,15 +330,15 @@ class ConfigCommands:
|
||||
opt = self._config.get_opt(option)
|
||||
valid_list_types = (configtypes.List, configtypes.ListOrValue)
|
||||
if not isinstance(opt.typ, valid_list_types):
|
||||
raise cmdexc.CommandError(":config-list-remove can only be used "
|
||||
"for lists")
|
||||
raise cmdutils.CommandError(":config-list-remove can only be used "
|
||||
"for lists")
|
||||
|
||||
with self._handle_config_error():
|
||||
option_value = self._config.get_mutable_obj(option)
|
||||
|
||||
if value not in option_value:
|
||||
raise cmdexc.CommandError("{} is not in {}!".format(value,
|
||||
option))
|
||||
raise cmdutils.CommandError("{} is not in {}!".format(
|
||||
value, option))
|
||||
|
||||
option_value.remove(value)
|
||||
|
||||
@ -329,7 +346,8 @@ class ConfigCommands:
|
||||
|
||||
@cmdutils.register(instance='config-commands')
|
||||
@cmdutils.argument('option', completion=configmodel.dict_option)
|
||||
def config_dict_remove(self, option, key, temp=False):
|
||||
def config_dict_remove(self, option: str, key: str,
|
||||
temp: bool = False) -> None:
|
||||
"""Remove a key from a dict.
|
||||
|
||||
Args:
|
||||
@ -339,22 +357,22 @@ class ConfigCommands:
|
||||
"""
|
||||
opt = self._config.get_opt(option)
|
||||
if not isinstance(opt.typ, configtypes.Dict):
|
||||
raise cmdexc.CommandError(":config-dict-remove can only be used "
|
||||
"for dicts")
|
||||
raise cmdutils.CommandError(":config-dict-remove can only be used "
|
||||
"for dicts")
|
||||
|
||||
with self._handle_config_error():
|
||||
option_value = self._config.get_mutable_obj(option)
|
||||
|
||||
if key not in option_value:
|
||||
raise cmdexc.CommandError("{} is not in {}!".format(key,
|
||||
option))
|
||||
raise cmdutils.CommandError("{} is not in {}!".format(
|
||||
key, option))
|
||||
|
||||
del option_value[key]
|
||||
|
||||
self._config.update_mutables(save_yaml=not temp)
|
||||
|
||||
@cmdutils.register(instance='config-commands')
|
||||
def config_clear(self, save=False):
|
||||
def config_clear(self, save: bool = False) -> None:
|
||||
"""Set all settings back to their default.
|
||||
|
||||
Args:
|
||||
@ -364,7 +382,7 @@ class ConfigCommands:
|
||||
self._config.clear(save_yaml=save)
|
||||
|
||||
@cmdutils.register(instance='config-commands')
|
||||
def config_source(self, filename=None, clear=False):
|
||||
def config_source(self, filename: str = None, clear: bool = False) -> None:
|
||||
"""Read a config.py file.
|
||||
|
||||
Args:
|
||||
@ -383,19 +401,19 @@ class ConfigCommands:
|
||||
try:
|
||||
configfiles.read_config_py(filename)
|
||||
except configexc.ConfigFileErrors as e:
|
||||
raise cmdexc.CommandError(e)
|
||||
raise cmdutils.CommandError(e)
|
||||
|
||||
@cmdutils.register(instance='config-commands')
|
||||
def config_edit(self, no_source=False):
|
||||
def config_edit(self, no_source: bool = False) -> None:
|
||||
"""Open the config.py file in the editor.
|
||||
|
||||
Args:
|
||||
no_source: Don't re-source the config file after editing.
|
||||
"""
|
||||
def on_file_updated():
|
||||
def on_file_updated() -> None:
|
||||
"""Source the new config when editing finished.
|
||||
|
||||
This can't use cmdexc.CommandError as it's run async.
|
||||
This can't use cmdutils.CommandError as it's run async.
|
||||
"""
|
||||
try:
|
||||
configfiles.read_config_py(filename)
|
||||
@ -410,7 +428,8 @@ class ConfigCommands:
|
||||
ed.edit_file(filename)
|
||||
|
||||
@cmdutils.register(instance='config-commands')
|
||||
def config_write_py(self, filename=None, force=False, defaults=False):
|
||||
def config_write_py(self, filename: str = None,
|
||||
force: bool = False, defaults: bool = False) -> None:
|
||||
"""Write the current configuration to a config.py file.
|
||||
|
||||
Args:
|
||||
@ -426,16 +445,16 @@ class ConfigCommands:
|
||||
filename = os.path.expanduser(filename)
|
||||
|
||||
if os.path.exists(filename) and not force:
|
||||
raise cmdexc.CommandError("{} already exists - use --force to "
|
||||
"overwrite!".format(filename))
|
||||
raise cmdutils.CommandError("{} already exists - use --force to "
|
||||
"overwrite!".format(filename))
|
||||
|
||||
options = [] # type: typing.List
|
||||
if defaults:
|
||||
options = [(None, opt, opt.default)
|
||||
for _name, opt in sorted(configdata.DATA.items())]
|
||||
bindings = dict(configdata.DATA['bindings.default'].default)
|
||||
commented = True
|
||||
else:
|
||||
options = []
|
||||
for values in self._config:
|
||||
for scoped in values:
|
||||
options.append((scoped.pattern, values.opt, scoped.value))
|
||||
@ -447,4 +466,4 @@ class ConfigCommands:
|
||||
try:
|
||||
writer.write(filename)
|
||||
except OSError as e:
|
||||
raise cmdexc.CommandError(str(e))
|
||||
raise cmdutils.CommandError(str(e))
|
||||
|
@ -24,14 +24,18 @@ Module attributes:
|
||||
DATA: A dict of Option objects after init() has been called.
|
||||
"""
|
||||
|
||||
import typing
|
||||
from typing import Optional # pylint: disable=unused-import,useless-suppression
|
||||
import functools
|
||||
|
||||
import attr
|
||||
from qutebrowser.config import configtypes
|
||||
from qutebrowser.utils import usertypes, qtutils, utils
|
||||
|
||||
DATA = None
|
||||
MIGRATIONS = None
|
||||
DATA = typing.cast(typing.Mapping[str, 'Option'], None)
|
||||
MIGRATIONS = typing.cast('Migrations', None)
|
||||
|
||||
_BackendDict = typing.Mapping[str, typing.Union[str, bool]]
|
||||
|
||||
|
||||
@attr.s
|
||||
@ -42,15 +46,15 @@ class Option:
|
||||
Note that this is just an option which exists, with no value associated.
|
||||
"""
|
||||
|
||||
name = attr.ib()
|
||||
typ = attr.ib()
|
||||
default = attr.ib()
|
||||
backends = attr.ib()
|
||||
raw_backends = attr.ib()
|
||||
description = attr.ib()
|
||||
supports_pattern = attr.ib(default=False)
|
||||
restart = attr.ib(default=False)
|
||||
no_autoconfig = attr.ib(default=False)
|
||||
name = attr.ib() # type: str
|
||||
typ = attr.ib() # type: configtypes.BaseType
|
||||
default = attr.ib() # type: typing.Any
|
||||
backends = attr.ib() # type: typing.Iterable[usertypes.Backend]
|
||||
raw_backends = attr.ib() # type: Optional[typing.Mapping[str, bool]]
|
||||
description = attr.ib() # type: str
|
||||
supports_pattern = attr.ib(default=False) # type: bool
|
||||
restart = attr.ib(default=False) # type: bool
|
||||
no_autoconfig = attr.ib(default=False) # type: bool
|
||||
|
||||
|
||||
@attr.s
|
||||
@ -63,11 +67,13 @@ class Migrations:
|
||||
deleted: A list of option names which have been removed.
|
||||
"""
|
||||
|
||||
renamed = attr.ib(default=attr.Factory(dict))
|
||||
deleted = attr.ib(default=attr.Factory(list))
|
||||
renamed = attr.ib(
|
||||
default=attr.Factory(dict)) # type: typing.Dict[str, str]
|
||||
deleted = attr.ib(
|
||||
default=attr.Factory(list)) # type: typing.List[str]
|
||||
|
||||
|
||||
def _raise_invalid_node(name, what, node):
|
||||
def _raise_invalid_node(name: str, what: str, node: typing.Any) -> None:
|
||||
"""Raise an exception for an invalid configdata YAML node.
|
||||
|
||||
Args:
|
||||
@ -79,13 +85,16 @@ def _raise_invalid_node(name, what, node):
|
||||
name, what, node))
|
||||
|
||||
|
||||
def _parse_yaml_type(name, node):
|
||||
def _parse_yaml_type(
|
||||
name: str,
|
||||
node: typing.Union[str, typing.Mapping[str, typing.Any]],
|
||||
) -> configtypes.BaseType:
|
||||
if isinstance(node, str):
|
||||
# e.g:
|
||||
# type: Bool
|
||||
# -> create the type object without any arguments
|
||||
type_name = node
|
||||
kwargs = {}
|
||||
kwargs = {} # type: typing.MutableMapping[str, typing.Any]
|
||||
elif isinstance(node, dict):
|
||||
# e.g:
|
||||
# type:
|
||||
@ -123,7 +132,10 @@ def _parse_yaml_type(name, node):
|
||||
type_name, node, e))
|
||||
|
||||
|
||||
def _parse_yaml_backends_dict(name, node):
|
||||
def _parse_yaml_backends_dict(
|
||||
name: str,
|
||||
node: _BackendDict,
|
||||
) -> typing.Sequence[usertypes.Backend]:
|
||||
"""Parse a dict definition for backends.
|
||||
|
||||
Example:
|
||||
@ -160,7 +172,10 @@ def _parse_yaml_backends_dict(name, node):
|
||||
return backends
|
||||
|
||||
|
||||
def _parse_yaml_backends(name, node):
|
||||
def _parse_yaml_backends(
|
||||
name: str,
|
||||
node: typing.Union[None, str, _BackendDict],
|
||||
) -> typing.Sequence[usertypes.Backend]:
|
||||
"""Parse a backend node in the yaml.
|
||||
|
||||
It can have one of those four forms:
|
||||
@ -187,7 +202,9 @@ def _parse_yaml_backends(name, node):
|
||||
raise utils.Unreachable
|
||||
|
||||
|
||||
def _read_yaml(yaml_data):
|
||||
def _read_yaml(
|
||||
yaml_data: str,
|
||||
) -> typing.Tuple[typing.Mapping[str, Option], Migrations]:
|
||||
"""Read config data from a YAML file.
|
||||
|
||||
Args:
|
||||
@ -249,12 +266,12 @@ def _read_yaml(yaml_data):
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=256)
|
||||
def is_valid_prefix(prefix):
|
||||
def is_valid_prefix(prefix: str) -> bool:
|
||||
"""Check whether the given prefix is a valid prefix for some option."""
|
||||
return any(key.startswith(prefix + '.') for key in DATA)
|
||||
|
||||
|
||||
def init():
|
||||
def init() -> None:
|
||||
"""Initialize configdata from the YAML file."""
|
||||
global DATA, MIGRATIONS
|
||||
DATA, MIGRATIONS = _read_yaml(utils.read_file('config/configdata.yml'))
|
||||
|
@ -39,12 +39,7 @@ ignore_case:
|
||||
renamed: search.ignore_case
|
||||
|
||||
search.ignore_case:
|
||||
type:
|
||||
name: String
|
||||
valid_values:
|
||||
- always: Search case-insensitively.
|
||||
- never: Search case-sensitively.
|
||||
- smart: Search case-sensitively if there are capital characters.
|
||||
type: IgnoreCase
|
||||
default: smart
|
||||
desc: When to find text on a page case-insensitively.
|
||||
|
||||
@ -329,7 +324,7 @@ content.cache.appcache:
|
||||
ask the application cache for the contents, before hitting the network.
|
||||
|
||||
content.cookies.accept:
|
||||
default: no-3rdparty
|
||||
default: all
|
||||
backend:
|
||||
QtWebKit: true
|
||||
QtWebEngine: Qt 5.11
|
||||
@ -337,7 +332,8 @@ content.cookies.accept:
|
||||
name: 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."
|
||||
@ -1816,7 +1812,9 @@ url.searchengines:
|
||||
type:
|
||||
name: Dict
|
||||
required_keys: ['DEFAULT']
|
||||
keytype: String
|
||||
keytype:
|
||||
name: String
|
||||
forbidden: ' '
|
||||
valtype: SearchEngineUrl
|
||||
desc: >-
|
||||
Search engines which can be used via the address bar.
|
||||
@ -2614,6 +2612,8 @@ bindings.default:
|
||||
yD: yank domain -s
|
||||
yp: yank pretty-url
|
||||
yP: yank pretty-url -s
|
||||
ym: yank markdown
|
||||
yM: yank markdown -s
|
||||
pp: open -- {clipboard}
|
||||
pP: open -- {primary}
|
||||
Pp: open -t -- {clipboard}
|
||||
|
@ -19,6 +19,7 @@
|
||||
|
||||
"""Code to show a diff of the legacy config format."""
|
||||
|
||||
import typing # pylint: disable=unused-import,useless-suppression
|
||||
import difflib
|
||||
import os.path
|
||||
|
||||
@ -727,10 +728,10 @@ scroll right
|
||||
"""
|
||||
|
||||
|
||||
def get_diff():
|
||||
def get_diff() -> str:
|
||||
"""Get a HTML diff for the old config files."""
|
||||
old_conf_lines = []
|
||||
old_key_lines = []
|
||||
old_conf_lines = [] # type: typing.MutableSequence[str]
|
||||
old_key_lines = [] # type: typing.MutableSequence[str]
|
||||
|
||||
for filename, dest in [('qutebrowser.conf', old_conf_lines),
|
||||
('keys.conf', old_key_lines)]:
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user