Merge branch 'qutebrowser-master'

This commit is contained in:
Penaz91 2017-09-20 13:23:40 +02:00
commit f5c15b6ce8
311 changed files with 15765 additions and 16868 deletions

View File

@ -7,7 +7,7 @@ environment:
PYTHONUNBUFFERED: 1 PYTHONUNBUFFERED: 1
PYTHON: C:\Python36\python.exe PYTHON: C:\Python36\python.exe
matrix: matrix:
- TESTENV: py36-pyqt58 - TESTENV: py36-pyqt59
- TESTENV: pylint - TESTENV: pylint
install: install:

View File

@ -43,6 +43,8 @@ putty-ignore =
tests/helpers/fixtures.py : +N806 tests/helpers/fixtures.py : +N806
tests/unit/browser/webkit/http/test_content_disposition.py : +D400 tests/unit/browser/webkit/http/test_content_disposition.py : +D400
scripts/dev/ci/appveyor_install.py : +FI53 scripts/dev/ci/appveyor_install.py : +FI53
# FIXME:conf
tests/unit/completion/test_models.py : +F821
copyright-check = True copyright-check = True
copyright-regexp = # Copyright [\d-]+ .* copyright-regexp = # Copyright [\d-]+ .*
copyright-min-file-size = 110 copyright-min-file-size = 110

9
.github/CONTRIBUTING.asciidoc vendored Normal file
View File

@ -0,0 +1,9 @@
- Before you start to work on something, please leave a comment on the relevant
issue (or open one). This makes sure there is no duplicate work done.
- Either run the testsuite locally, or keep an eye on Travis CI / AppVeyor
after pushing changes.
See the full contribution docs for details:
include::../doc/contributing.asciidoc[]

5
.gitignore vendored
View File

@ -15,11 +15,8 @@ __pycache__
/qutebrowser/3rdparty /qutebrowser/3rdparty
/doc/*.html /doc/*.html
/README.html /README.html
/CHANGELOG.html
/CONTRIBUTING.html
/FAQ.html
/INSTALL.html
/qutebrowser/html/doc/ /qutebrowser/html/doc/
/qutebrowser/html/*.html
/.venv* /.venv*
/.coverage /.coverage
/htmlcov /htmlcov

View File

@ -30,6 +30,7 @@ disable=no-self-use,
broad-except, broad-except,
bare-except, bare-except,
eval-used, eval-used,
exec-used,
ungrouped-imports, ungrouped-imports,
suppressed-message, suppressed-message,
too-many-return-statements, too-many-return-statements,
@ -42,6 +43,7 @@ function-rgx=[a-z_][a-z0-9_]{2,50}$
const-rgx=[A-Za-z_][A-Za-z0-9_]{0,30}$ const-rgx=[A-Za-z_][A-Za-z0-9_]{0,30}$
method-rgx=[a-z_][A-Za-z0-9_]{1,50}$ method-rgx=[a-z_][A-Za-z0-9_]{1,50}$
attr-rgx=[a-z_][a-z0-9_]{0,30}$ attr-rgx=[a-z_][a-z0-9_]{0,30}$
class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{1,30}|(__.*__))$
argument-rgx=[a-z_][a-z0-9_]{0,30}$ argument-rgx=[a-z_][a-z0-9_]{0,30}$
variable-rgx=[a-z_][a-z0-9_]{0,30}$ variable-rgx=[a-z_][a-z0-9_]{0,30}$
docstring-min-length=3 docstring-min-length=3
@ -63,6 +65,7 @@ valid-metaclass-classmethod-first-arg=cls
[TYPECHECK] [TYPECHECK]
ignored-modules=PyQt5,PyQt5.QtWebKit ignored-modules=PyQt5,PyQt5.QtWebKit
ignored-classes=_CountingAttr
[IMPORTS] [IMPORTS]
# WORKAROUND # WORKAROUND

View File

@ -1 +1 @@
schedule: "every week" schedule: "every week on monday"

View File

@ -1,15 +1,11 @@
sudo: required sudo: false
dist: trusty dist: trusty
language: generic language: python
group: edge group: edge
python: 3.6
matrix: matrix:
include: include:
- os: linux
env: TESTENV=py34-cov
- os: linux
env: DOCKER=debian-jessie
services: docker
- os: linux - os: linux
env: DOCKER=archlinux env: DOCKER=archlinux
services: docker services: docker
@ -17,39 +13,32 @@ matrix:
env: DOCKER=archlinux-webengine QUTE_BDD_WEBENGINE=true env: DOCKER=archlinux-webengine QUTE_BDD_WEBENGINE=true
services: docker services: docker
- os: linux - os: linux
env: DOCKER=ubuntu-xenial
services: docker
- os: linux
language: python
python: 3.6
env: TESTENV=py36-pyqt571 env: TESTENV=py36-pyqt571
- os: linux - os: linux
language: python
python: 3.6
env: TESTENV=py36-pyqt58 env: TESTENV=py36-pyqt58
- os: linux - os: linux
language: python
python: 3.5 python: 3.5
env: TESTENV=py35-pyqt59 env: TESTENV=py35-pyqt59
- os: linux - os: linux
language: python env: TESTENV=py36-pyqt59-cov
python: 3.6
env: TESTENV=py36-pyqt59
- os: osx - os: osx
env: TESTENV=py36 OSX=elcapitan env: TESTENV=py36 OSX=sierra
osx_image: xcode7.3 osx_image: xcode8.3
language: generic
# https://github.com/qutebrowser/qutebrowser/issues/2013 # https://github.com/qutebrowser/qutebrowser/issues/2013
# - os: osx # - os: osx
# env: TESTENV=py35 OSX=yosemite # env: TESTENV=py35 OSX=yosemite
# osx_image: xcode6.4 # osx_image: xcode6.4
- os: linux - os: linux
env: TESTENV=pylint PYTHON=python3.6 env: TESTENV=pylint PYTHON=python3.6
language: python
python: 3.6
- os: linux - os: linux
env: TESTENV=flake8 env: TESTENV=flake8
- os: linux - os: linux
env: TESTENV=docs env: TESTENV=docs
addons:
apt:
packages:
- asciidoc
- os: linux - os: linux
env: TESTENV=vulture env: TESTENV=vulture
- os: linux - os: linux
@ -60,10 +49,9 @@ matrix:
env: TESTENV=check-manifest env: TESTENV=check-manifest
- os: linux - os: linux
env: TESTENV=eslint env: TESTENV=eslint
allow_failures: language: node_js
- os: osx python: null
env: TESTENV=py36 OSX=elcapitan node_js: node
osx_image: xcode7.3
fast_finish: true fast_finish: true
cache: cache:
@ -71,10 +59,6 @@ cache:
- $HOME/.cache/pip - $HOME/.cache/pip
- $HOME/build/qutebrowser/qutebrowser/.cache - $HOME/build/qutebrowser/qutebrowser/.cache
before_install:
# We need to do this so we pick up the system-wide python properly
- 'export PATH="/usr/bin:$PATH"'
install: install:
- bash scripts/dev/ci/travis_install.sh - bash scripts/dev/ci/travis_install.sh
- ulimit -c unlimited - ulimit -c unlimited

View File

View File

@ -11,12 +11,13 @@ graft misc/userscripts
recursive-include scripts *.py *.sh recursive-include scripts *.py *.sh
include qutebrowser/utils/testfile include qutebrowser/utils/testfile
include qutebrowser/git-commit-id include qutebrowser/git-commit-id
include COPYING doc/* README.asciidoc CONTRIBUTING.asciidoc FAQ.asciidoc INSTALL.asciidoc CHANGELOG.asciidoc include LICENSE doc/* README.asciidoc
include qutebrowser.desktop include misc/qutebrowser.desktop
include requirements.txt include requirements.txt
include tox.ini include tox.ini
include qutebrowser.py include qutebrowser.py
include misc/cheatsheet.svg include misc/cheatsheet.svg
include qutebrowser/config/configdata.yml
prune www prune www
prune scripts/dev prune scripts/dev
@ -36,7 +37,6 @@ exclude qutebrowser/javascript/.eslintrc.yaml
exclude qutebrowser/javascript/.eslintignore exclude qutebrowser/javascript/.eslintignore
exclude doc/help exclude doc/help
exclude .* exclude .*
exclude codecov.yml
exclude misc/appveyor_install.py exclude misc/appveyor_install.py
exclude misc/qutebrowser.spec exclude misc/qutebrowser.spec
exclude misc/qutebrowser.nsi exclude misc/qutebrowser.nsi

View File

@ -9,7 +9,7 @@ qutebrowser
// QUTE_WEB_HIDE // QUTE_WEB_HIDE
image:icons/qutebrowser-64x64.png[qutebrowser logo] *A keyboard-driven, vim-like browser based on PyQt5 and Qt.* image:icons/qutebrowser-64x64.png[qutebrowser logo] *A keyboard-driven, vim-like browser based on PyQt5 and Qt.*
image:https://img.shields.io/pypi/l/qutebrowser.svg?style=flat["license badge",link="https://github.com/qutebrowser/qutebrowser/blob/master/COPYING"] image:https://img.shields.io/pypi/l/qutebrowser.svg?style=flat["license badge",link="https://github.com/qutebrowser/qutebrowser/blob/master/LICENSE"]
image:https://img.shields.io/pypi/v/qutebrowser.svg?style=flat["version badge",link="https://pypi.python.org/pypi/qutebrowser/"] image:https://img.shields.io/pypi/v/qutebrowser.svg?style=flat["version badge",link="https://pypi.python.org/pypi/qutebrowser/"]
image:https://requires.io/github/qutebrowser/qutebrowser/requirements.svg?branch=master["requirements badge",link="https://requires.io/github/qutebrowser/qutebrowser/requirements/?branch=master"] image:https://requires.io/github/qutebrowser/qutebrowser/requirements.svg?branch=master["requirements badge",link="https://requires.io/github/qutebrowser/qutebrowser/requirements/?branch=master"]
image:https://travis-ci.org/qutebrowser/qutebrowser.svg?branch=master["Build Status", link="https://travis-ci.org/qutebrowser/qutebrowser"] image:https://travis-ci.org/qutebrowser/qutebrowser.svg?branch=master["Build Status", link="https://travis-ci.org/qutebrowser/qutebrowser"]
@ -36,7 +36,7 @@ Downloads
--------- ---------
See the https://github.com/qutebrowser/qutebrowser/releases[github releases See the https://github.com/qutebrowser/qutebrowser/releases[github releases
page] for available downloads and the link:INSTALL.asciidoc[INSTALL] file for page] for available downloads and the link:doc/install.asciidoc[INSTALL] file for
detailed instructions on how to get qutebrowser running on various platforms. detailed instructions on how to get qutebrowser running on various platforms.
Documentation Documentation
@ -49,10 +49,11 @@ available:
image:https://qutebrowser.org/img/cheatsheet-small.png["qutebrowser key binding cheatsheet",link="https://qutebrowser.org/img/cheatsheet-big.png"] image:https://qutebrowser.org/img/cheatsheet-small.png["qutebrowser key binding cheatsheet",link="https://qutebrowser.org/img/cheatsheet-big.png"]
* link:doc/quickstart.asciidoc[Quick start guide] * link:doc/quickstart.asciidoc[Quick start guide]
* A https://www.shortcutfoo.com/app/dojos/qutebrowser[free training course] to remember those key bindings. * A https://www.shortcutfoo.com/app/dojos/qutebrowser[free training course] to remember those key bindings.
* link:FAQ.asciidoc[Frequently asked questions] * link:doc/faq.asciidoc[Frequently asked questions]
* link:CONTRIBUTING.asciidoc[Contributing to qutebrowser] * link:doc/help/configuring.asciidoc[Configuring qutebrowser]
* link:INSTALL.asciidoc[INSTALL] * link:doc/contributing.asciidoc[Contributing to qutebrowser]
* link:CHANGELOG.asciidoc[Change Log] * link:doc/install.asciidoc[Installing qutebrowser]
* link:doc/changelog.asciidoc[Change Log]
* link:doc/stacktrace.asciidoc[Reporting segfaults] * link:doc/stacktrace.asciidoc[Reporting segfaults]
* link:doc/userscripts.asciidoc[How to write userscripts] * link:doc/userscripts.asciidoc[How to write userscripts]
@ -78,7 +79,7 @@ Contributions / Bugs
-------------------- --------------------
You want to contribute to qutebrowser? Awesome! Please read You want to contribute to qutebrowser? Awesome! Please read
link:CONTRIBUTING.asciidoc[the contribution guidelines] for details and link:doc/contributing.asciidoc[the contribution guidelines] for details and
useful hints. useful hints.
If you found a bug or have a feature request, you can report it in several If you found a bug or have a feature request, you can report it in several
@ -98,26 +99,24 @@ Requirements
The following software and libraries are required to run qutebrowser: The following software and libraries are required to run qutebrowser:
* http://www.python.org/[Python] 3.4 or newer (3.6 recommended) - note that * http://www.python.org/[Python] 3.5 or newer (3.6 recommended)
support for Python 3.4 * http://qt.io/[Qt] 5.7.1 or newer with the following modules:
https://github.com/qutebrowser/qutebrowser/issues/2742[will be dropped soon].
* http://qt.io/[Qt] 5.2.0 or newer (5.9 recommended - note that support for Qt
< 5.7.1 will be dropped soon) with the following modules:
- QtCore / qtbase - QtCore / qtbase
- QtQuick (part of qtbase in some distributions) - QtQuick (part of qtbase in some distributions)
- QtSQL (part of qtbase in some distributions) - QtSQL (part of qtbase in some distributions)
- QtOpenGL
- QtWebEngine, or - QtWebEngine, or
- QtWebKit (old or link:https://github.com/annulen/webkit/wiki[reloaded]/NG). - QtWebKit - only the
Note that support for legacy QtWebKit (before 5.212) will be link:https://github.com/annulen/webkit/wiki[updated fork] (5.212) is
dropped soon. supported.
* http://www.riverbankcomputing.com/software/pyqt/intro[PyQt] 5.2.0 or newer * http://www.riverbankcomputing.com/software/pyqt/intro[PyQt] 5.7.0 or newer
(5.9 recommended) for Python 3. Note that support for PyQt < 5.7 will be (5.9 recommended) for Python 3.
dropped soon.
* https://pypi.python.org/pypi/setuptools/[pkg_resources/setuptools] * https://pypi.python.org/pypi/setuptools/[pkg_resources/setuptools]
* http://fdik.org/pyPEG/[pyPEG2] * http://fdik.org/pyPEG/[pyPEG2]
* http://jinja.pocoo.org/[jinja2] * http://jinja.pocoo.org/[jinja2]
* http://pygments.org/[pygments] * http://pygments.org/[pygments]
* http://pyyaml.org/wiki/PyYAML[PyYAML] * http://pyyaml.org/wiki/PyYAML[PyYAML]
* http://www.attrs.org/[attrs]
The following libraries are optional: The following libraries are optional:
@ -128,8 +127,8 @@ The following libraries are optional:
* http://asciidoc.org/[asciidoc] to generate the documentation for the `:help` * http://asciidoc.org/[asciidoc] to generate the documentation for the `:help`
command, when using the git repository (rather than a release). command, when using the git repository (rather than a release).
See link:INSTALL.asciidoc[INSTALL] for directions on how to install qutebrowser See link:doc/install.asciidoc[the documentation] for directions on how to
and its dependencies. install qutebrowser and its dependencies.
Donating Donating
-------- --------

View File

@ -20,16 +20,26 @@ v1.0.0 (unreleased)
Breaking changes Breaking changes
~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~
- Support for legacy QtWebKit (before 5.212 which is distributed - Dependency changes
independently from Qt) is dropped. - Support for legacy QtWebKit (before 5.212 which is distributed independently
- Support for Python 3.4 is dropped. from Qt) is dropped.
- Support for Qt before 5.7 is dropped. - Support for Python 3.4 is dropped.
- New dependency on the QtSql module and Qt sqlite support. - Support for Qt before 5.7.1 and PyQt before 5.7 is dropped.
- New dependency on ruamel.yaml; dropped PyYAML dependency. - (TODO) New dependency on ruamel.yaml; dropped PyYAML dependency.
- The QtWebEngine backend is now used by default if available. - New dependency on the QtSql module and Qt sqlite support.
- New dependency on the `attrs` Python module.
- The depedency on PyOpenGL (when using QtWebEngine) got removed. Note
that PyQt5.QtOpenGL is still a dependency.
- PyQt5.QtOpenGL is now always required, even with QtWebKit.
- (TODO) The QtWebEngine backend is now used by default if available.
- New config system which ignores the old config file. - New config system which ignores the old config file.
- The depedency on PyOpenGL (when using QtWebEngine) got removed. Note - Various documentation files got moved to the doc/ subfolder,
that PyQt5.QtOpenGL is still a dependency. `qutebrowser.desktop` got moved to misc/.
- Migrating QtWebEngine data written by versions before 2016-11-15 (before
v0.9.0) is now not supported anymore.
- Upgrading qutebrowser with a version older than v0.4.0 still running now won't
work properly anymore.
- The `--harfbuzz` commandline argument got dropped.
Major changes Major changes
~~~~~~~~~~~~~ ~~~~~~~~~~~~~
@ -42,12 +52,15 @@ Added
~~~~~ ~~~~~
- New back/forward indicator in the statusbar - New back/forward indicator in the statusbar
- New `bindings.key_mappings` setting to map keys to other keys
- New `qt_args` setting to pass additional arguments to Qt/Chromium
- New `backend` setting to select the backend to use (auto/webengine/webkit).
Together with the previous setting, this should make wrapper scripts
unnecessary.
Changed Changed
~~~~~~~ ~~~~~~~
- Upgrading qutebrowser with a version older than v0.4.0 still running now won't
work properly anymore.
- Using `:download` now uses the page's title as filename. - Using `:download` now uses the page's title as filename.
- Using `:back` or `:forward` with a count now skips intermediate pages. - Using `:back` or `:forward` with a count now skips intermediate pages.
- When there are multiple messages shown, the timeout is increased. - When there are multiple messages shown, the timeout is increased.

View File

@ -5,12 +5,6 @@ The Compiler <mail@qutebrowser.org>
:data-uri: :data-uri:
:toc: :toc:
IMPORTANT: I'm currently (July 2017) more busy than usual until September,
because of exams coming up. In addition to that, a new config system is coming
which will conflict with many non-trivial contributions. Because of that, please
refrain from contributing new features until then. If you're reading this note
after mid-September, please open an issue.
I `&lt;3` footnote:[Of course, that says `<3` in HTML.] contributors! I `&lt;3` footnote:[Of course, that says `<3` in HTML.] contributors!
This document contains guidelines for contributing to qutebrowser, as well as This document contains guidelines for contributing to qutebrowser, as well as
@ -104,10 +98,9 @@ unittests and several linters/checkers.
Currently, the following tox environments are available: Currently, the following tox environments are available:
* Tests using https://www.pytest.org[pytest]: * Tests using https://www.pytest.org[pytest]:
- `py34`: Run pytest for python-3.4. - `py35`, `py36`: Run pytest for python 3.5/3.6 with the system-wide PyQt.
- `py35`: Run pytest for python-3.5. - `py36-pyqt57`, ..., `py36-pyqt59`: Run pytest with the given PyQt version (`py35-*` also works)
- `py34-cov`: Run pytest for python-3.4 with code coverage report. - `py36-pyqt59-cov`: Run with coverage support (other Python/PyQt versions work too)
- `py35-cov`: Run pytest for python-3.5 with code coverage report.
* `flake8`: Run https://pypi.python.org/pypi/flake8[flake8] checks: * `flake8`: Run https://pypi.python.org/pypi/flake8[flake8] checks:
https://pypi.python.org/pypi/pyflakes[pyflakes], https://pypi.python.org/pypi/pyflakes[pyflakes],
https://pypi.python.org/pypi/pep8[pep8], https://pypi.python.org/pypi/pep8[pep8],
@ -187,7 +180,7 @@ In the _scripts/_ subfolder there's a `run_profile.py` which profiles the code
and shows a graphical representation of what takes how much time. and shows a graphical representation of what takes how much time.
It uses the built-in Python It uses the built-in Python
https://docs.python.org/3.4/library/profile.html[cProfile] module and can show https://docs.python.org/3.6/library/profile.html[cProfile] module and can show
the output in four different ways: the output in four different ways:
* Raw profile file (`--profile-tool=none`) * Raw profile file (`--profile-tool=none`)
@ -479,7 +472,8 @@ The following arguments are supported for `@cmdutils.argument`:
- `win_id=True`: Mark the argument as special window ID argument - `win_id=True`: Mark the argument as special window ID argument
- `count=True`: Mark the argument as special count argument - `count=True`: Mark the argument as special count argument
- `hide=True`: Hide the argument from the documentation - `hide=True`: Hide the argument from the documentation
- `completion`: A `usertypes.Completion` member to use as completion. - `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. - `choices`: The allowed string choices for the argument.
The name of an argument will always be the parameter name, with any trailing The name of an argument will always be the parameter name, with any trailing
@ -541,11 +535,11 @@ ____
Setting up a Windows Development Environment Setting up a Windows Development Environment
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* Install https://www.python.org/downloads/release/python-344/[Python 3.4] * Install https://www.python.org/downloads/release/python-362/[Python 3.6].
* Install https://sourceforge.net/projects/pyqt/files/PyQt5/PyQt-5.5.1/[PyQt 5.5] * Install PyQt via `pip install PyQt5`
* Create a file at `C:\Windows\system32\python3.bat` with the following content: * Create a file at `C:\Windows\system32\python3.bat` with the following content (adjust the path as necessary):
`@C:\Python34\python %*` `@C:\Python36\python %*`
This will make the Python 3.4 interpreter available as `python3`, which is used by various development scripts. This will make the Python 3.6 interpreter available as `python3`, which is used by various development scripts.
* Install git from the https://git-scm.com/download/win[git-scm downloads page] * Install git from the https://git-scm.com/download/win[git-scm downloads page]
Try not to enable `core.autocrlf`, since that will cause `flake8` to complain a lot. Use an editor that can deal with plain line feeds instead. Try not to enable `core.autocrlf`, since that will cause `flake8` to complain a lot. Use an editor that can deal with plain line feeds instead.
* Clone your favourite qutebrowser repository. * Clone your favourite qutebrowser repository.

View File

@ -193,7 +193,7 @@ Configuration not saved after modifying config.::
Unable to view flash content.:: Unable to view flash content.::
If you have flash installed for on your system, it's necessary to enable plugins If you have flash installed for on your system, it's necessary to enable plugins
to use the flash plugin. Using the command `:set content allow-plugins true` to use the flash plugin. Using the command `:set content.plugins true`
in qutebrowser will enable plugins. Packages for flash should in qutebrowser will enable plugins. Packages for flash should
be provided for your platform or it can be obtained from be provided for your platform or it can be obtained from
http://get.adobe.com/flashplayer/[Adobe]. http://get.adobe.com/flashplayer/[Adobe].
@ -205,26 +205,6 @@ Experiencing freezing on sites like duckduckgo and youtube.::
See https://github.com/qutebrowser/qutebrowser/issues/357[Issue #357] See https://github.com/qutebrowser/qutebrowser/issues/357[Issue #357]
for more details. for more details.
Experiencing segfaults (crashes) on Debian systems.::
For Debian it's highly recommended to install the `gstreamer0.10-plugins-base` package.
This is a workaround for a bug in Qt, it has been fixed upstream in Qt 5.4
More details can be found
https://bugs.webkit.org/show_bug.cgi?id=119951[here].
Segfaults on Facebook, Medium, Amazon, ...::
If you are on a Debian or Ubuntu based system, you might experience some crashes
visiting these sites. This is caused by various bugs in Qt which have been
fixed in Qt 5.4. However Debian and Ubuntu are slow to adopt or upgrade
some packages. On Debian Jessie, it's recommended to use the experimental
repos as described in https://github.com/qutebrowser/qutebrowser/blob/master/INSTALL.asciidoc#on-debian--ubuntu[INSTALL].
+
Since Ubuntu Trusty (using Qt 5.2.1),
https://bugreports.qt.io/browse/QTBUG-42417?jql=component%20%3D%20WebKit%20and%20resolution%20%3D%20Done%20and%20fixVersion%20in%20(5.3.0%2C%20%225.3.0%20Alpha%22%2C%20%225.3.0%20Beta1%22%2C%20%225.3.0%20RC1%22%2C%205.3.1%2C%205.3.2%2C%205.4.0%2C%20%225.4.0%20Alpha%22%2C%20%225.4.0%20Beta%22%2C%20%225.4.0%20RC%22)%20and%20priority%20in%20(%22P2%3A%20Important%22%2C%20%22P1%3A%20Critical%22%2C%20%22P0%3A%20Blocker%22)[over
70 important bugs] have been fixed in QtWebKit. For Debian Jessie (using Qt 5.3.2)
it's still
https://bugreports.qt.io/browse/QTBUG-42417?jql=component%20%3D%20WebKit%20and%20resolution%20%3D%20Done%20and%20fixVersion%20in%20(5.4.0%2C%20%225.4.0%20Alpha%22%2C%20%225.4.0%20Beta%22%2C%20%225.4.0%20RC%22)%20and%20priority%20in%20(%22P2%3A%20Important%22%2C%20%22P1%3A%20Critical%22%2C%20%22P0%3A%20Blocker%22)[nearly
20 important bugs].
When using QtWebEngine, qutebrowser reports "Render Process Crashed" and the console prints a traceback on Gentoo Linux or another Source-Based Distro:: When using QtWebEngine, qutebrowser reports "Render Process Crashed" and the console prints a traceback on Gentoo Linux or another Source-Based Distro::
As stated in https://gcc.gnu.org/gcc-6/changes.html[GCC's Website] GCC 6 has introduced some optimizations that could break non-conforming codebases, like QtWebEngine. + As stated in https://gcc.gnu.org/gcc-6/changes.html[GCC's Website] GCC 6 has introduced some optimizations that could break non-conforming codebases, like QtWebEngine. +
As a workaround, you can disable the nullpointer check optimization by adding the -fno-delete-null-pointer-checks flag while compiling. + As a workaround, you can disable the nullpointer check optimization by adding the -fno-delete-null-pointer-checks flag while compiling. +

View File

@ -88,7 +88,6 @@ It is possible to run or bind multiple commands by separating them with `;;`.
|<<undo,undo>>|Re-open a closed tab. |<<undo,undo>>|Re-open a closed tab.
|<<view-source,view-source>>|Show the source of the current page in a new tab. |<<view-source,view-source>>|Show the source of the current page in a new tab.
|<<window-only,window-only>>|Close all windows except for the current one. |<<window-only,window-only>>|Close all windows except for the current one.
|<<wq,wq>>|Save open pages and quit.
|<<yank,yank>>|Yank something to the clipboard or primary selection. |<<yank,yank>>|Yank something to the clipboard or primary selection.
|<<zoom,zoom>>|Set the zoom level for the current tab. |<<zoom,zoom>>|Set the zoom level for the current tab.
|<<zoom-in,zoom-in>>|Increase the zoom level for the current tab. |<<zoom-in,zoom-in>>|Increase the zoom level for the current tab.
@ -126,7 +125,8 @@ Bind a key to a command.
==== optional arguments ==== optional arguments
* +*-m*+, +*--mode*+: A comma-separated list of modes to bind the key in (default: `normal`). * +*-m*+, +*--mode*+: A comma-separated list of modes to bind the key in (default: `normal`). See `:help bindings.commands` for the
available modes.
* +*-f*+, +*--force*+: Rebind the key if it is already bound. * +*-f*+, +*--force*+: Rebind the key if it is already bound.
@ -281,7 +281,7 @@ Syntax: +:edit-url [*--bg*] [*--tab*] [*--window*] ['url']+
Navigate to a url formed in an external editor. Navigate to a url formed in an external editor.
The editor which should be launched can be configured via the `general -> editor` config option. The editor which should be launched can be configured via the `editor.command` config option.
==== positional arguments ==== positional arguments
* +'url'+: URL to edit; defaults to the current page url. * +'url'+: URL to edit; defaults to the current page url.
@ -338,7 +338,7 @@ Show help about a command or setting.
* +'topic'+: The topic to show help for. * +'topic'+: The topic to show help for.
- :__command__ for commands. - :__command__ for commands.
- __section__\->__option__ for settings. - __section__.__option__ for settings.
==== optional arguments ==== optional arguments
@ -368,7 +368,7 @@ Start hinting.
- `normal`: Open the link. - `normal`: Open the link.
- `current`: Open the link in the current tab. - `current`: Open the link in the current tab.
- `tab`: Open the link in a new tab (honoring the - `tab`: Open the link in a new tab (honoring the
background-tabs setting). `tabs.background_tabs` setting).
- `tab-fg`: Open the link in a new foreground tab. - `tab-fg`: Open the link in a new foreground tab.
- `tab-bg`: Open the link in a new background tab. - `tab-bg`: Open the link in a new background tab.
- `window`: Open the link in a new window. - `window`: Open the link in a new window.
@ -402,13 +402,13 @@ Start hinting.
==== optional arguments ==== optional arguments
* +*-r*+, +*--rapid*+: Whether to do rapid hinting. This is only possible with targets `tab` (with background-tabs=true), `tab-bg`, * +*-r*+, +*--rapid*+: Whether to do rapid hinting. This is only possible with targets `tab` (with `tabs.background_tabs=true`), `tab-bg`,
`window`, `run`, `hover`, `userscript` and `spawn`. `window`, `run`, `hover`, `userscript` and `spawn`.
* +*-m*+, +*--mode*+: The hinting mode to use. * +*-m*+, +*--mode*+: The hinting mode to use.
- `number`: Use numeric hints. - `number`: Use numeric hints.
- `letter`: Use the chars in the hints->chars settings. - `letter`: Use the chars in the hints.chars setting.
- `word`: Use hint words based on the html elements and the - `word`: Use hint words based on the html elements and the
extra words. extra words.
@ -545,7 +545,7 @@ For `increment` and `decrement`, the number to change the URL by. For `up`, the
[[open]] [[open]]
=== open === open
Syntax: +:open [*--implicit*] [*--bg*] [*--tab*] [*--window*] [*--secure*] [*--private*] Syntax: +:open [*--related*] [*--bg*] [*--tab*] [*--window*] [*--secure*] [*--private*]
['url']+ ['url']+
Open a URL in the current/[count]th tab. Open a URL in the current/[count]th tab.
@ -556,7 +556,7 @@ If the URL contains newlines, each line gets opened in its own tab.
* +'url'+: The URL to open. * +'url'+: The URL to open.
==== optional arguments ==== optional arguments
* +*-i*+, +*--implicit*+: If opening a new tab, treat the tab as implicit (like clicking on a link). * +*-r*+, +*--related*+: If opening a new tab, position the tab as related to the current one (like clicking on a link).
* +*-b*+, +*--bg*+: Open in a new background tab. * +*-b*+, +*--bg*+: Open in a new background tab.
* +*-t*+, +*--tab*+: Open in a new tab. * +*-t*+, +*--tab*+: Open in a new tab.
@ -632,8 +632,17 @@ Save the current page as a quickmark.
[[quit]] [[quit]]
=== quit === quit
Syntax: +:quit [*--save*] ['session']+
Quit qutebrowser. Quit qutebrowser.
==== positional arguments
* +'session'+: The name of the session to save.
==== optional arguments
* +*-s*+, +*--save*+: When given, save the open windows even if auto_save.session is turned off.
[[record-macro]] [[record-macro]]
=== record-macro === record-macro
Syntax: +:record-macro ['register']+ Syntax: +:record-macro ['register']+
@ -752,7 +761,7 @@ Syntax: +:session-save [*--current*] [*--quiet*] [*--force*] [*--only-active-win
Save a session. Save a session.
==== positional arguments ==== positional arguments
* +'name'+: The name of the session. If not given, the session configured in general -> session-default-name is saved. * +'name'+: The name of the session. If not given, the session configured in session_default_name is saved.
==== optional arguments ==== optional arguments
@ -764,19 +773,18 @@ Save a session.
[[set]] [[set]]
=== set === set
Syntax: +:set [*--temp*] [*--print*] ['section'] ['option'] ['values' ['values' ...]]+ Syntax: +:set [*--temp*] [*--print*] ['option'] ['values' ['values' ...]]+
Set an option. Set an option.
If the option name ends with '?', the value of the option is shown instead. If the option name ends with '!' and it is a boolean value, toggle it. If the option name ends with '?', the value of the option is shown instead. If the option name ends with '!' and it is a boolean value, toggle it.
==== positional arguments ==== positional arguments
* +'section'+: The section where the option is in.
* +'option'+: The name of the option. * +'option'+: The name of the option.
* +'values'+: The value to set, or the values to cycle through. * +'values'+: The value to set, or the values to cycle through.
==== optional arguments ==== optional arguments
* +*-t*+, +*--temp*+: Set value temporarily. * +*-t*+, +*--temp*+: Set value temporarily until qutebrowser is closed.
* +*-p*+, +*--print*+: Print the value after setting. * +*-p*+, +*--print*+: Print the value after setting.
[[set-cmd-text]] [[set-cmd-text]]
@ -843,7 +851,7 @@ Close the current/[count]th tab.
==== optional arguments ==== optional arguments
* +*-p*+, +*--prev*+: Force selecting the tab before the current tab. * +*-p*+, +*--prev*+: Force selecting the tab before the current tab.
* +*-n*+, +*--next*+: Force selecting the tab after the current tab. * +*-n*+, +*--next*+: Force selecting the tab after the current tab.
* +*-o*+, +*--opposite*+: Force selecting the tab in the opposite direction of what's configured in 'tabs->select-on-remove'. * +*-o*+, +*--opposite*+: Force selecting the tab in the opposite direction of what's configured in 'tabs.select_on_remove'.
* +*-f*+, +*--force*+: Avoid confirmation for pinned tabs. * +*-f*+, +*--force*+: Avoid confirmation for pinned tabs.
@ -911,7 +919,7 @@ Close all tabs except for the current one.
=== tab-pin === tab-pin
Pin/Unpin the current/[count]th tab. Pin/Unpin the current/[count]th tab.
Pinning a tab shrinks it to tabs->pinned-width size. Attempting to close a pinned tab will cause a confirmation, unless --force is passed. Pinning a tab shrinks it to `tabs.width.pinned` size. Attempting to close a pinned tab will cause a confirmation, unless --force is passed.
==== count ==== count
The tab index to pin or unpin The tab index to pin or unpin
@ -925,13 +933,15 @@ How many tabs to switch back.
[[unbind]] [[unbind]]
=== unbind === unbind
Syntax: +:unbind 'key' ['mode']+ Syntax: +:unbind [*--mode* 'mode'] 'key'+
Unbind a keychain. Unbind a keychain.
==== positional arguments ==== positional arguments
* +'key'+: The keychain or special key (inside <...>) to unbind. * +'key'+: The keychain or special key (inside <...>) to unbind.
* +'mode'+: A comma-separated list of modes to unbind the key in (default: `normal`).
==== optional arguments
* +*-m*+, +*--mode*+: A mode to unbind the key in (default: `normal`). See `:help bindings.commands` for the available modes.
[[undo]] [[undo]]
@ -946,15 +956,6 @@ Show the source of the current page in a new tab.
=== window-only === window-only
Close all windows except for the current one. Close all windows except for the current one.
[[wq]]
=== wq
Syntax: +:wq ['name']+
Save open pages and quit.
==== positional arguments
* +'name'+: The name of the session.
[[yank]] [[yank]]
=== yank === yank
Syntax: +:yank [*--sel*] [*--keep*] ['what']+ Syntax: +:yank [*--sel*] [*--keep*] ['what']+
@ -1043,6 +1044,7 @@ How many steps to zoom out.
|<<move-to-start-of-line,move-to-start-of-line>>|Move the cursor or selection to the start of the line. |<<move-to-start-of-line,move-to-start-of-line>>|Move the cursor or selection to the start of the line.
|<<move-to-start-of-next-block,move-to-start-of-next-block>>|Move the cursor or selection to the start of next block. |<<move-to-start-of-next-block,move-to-start-of-next-block>>|Move the cursor or selection to the start of next block.
|<<move-to-start-of-prev-block,move-to-start-of-prev-block>>|Move the cursor or selection to the start of previous block. |<<move-to-start-of-prev-block,move-to-start-of-prev-block>>|Move the cursor or selection to the start of previous block.
|<<nop,nop>>|Do nothing.
|<<open-editor,open-editor>>|Open an external editor with the currently selected form field. |<<open-editor,open-editor>>|Open an external editor with the currently selected form field.
|<<prompt-accept,prompt-accept>>|Accept the current prompt. |<<prompt-accept,prompt-accept>>|Accept the current prompt.
|<<prompt-item-focus,prompt-item-focus>>|Shift the focus of the prompt file completion menu to another item. |<<prompt-item-focus,prompt-item-focus>>|Shift the focus of the prompt file completion menu to another item.
@ -1290,11 +1292,15 @@ Move the cursor or selection to the start of previous block.
==== count ==== count
How many blocks to move. How many blocks to move.
[[nop]]
=== nop
Do nothing.
[[open-editor]] [[open-editor]]
=== open-editor === open-editor
Open an external editor with the currently selected form field. Open an external editor with the currently selected form field.
The editor which should be launched can be configured via the `general -> editor` config option. The editor which should be launched can be configured via the `editor.command` config option.
[[prompt-accept]] [[prompt-accept]]
=== prompt-accept === prompt-accept

View File

@ -0,0 +1,223 @@
Configuring qutebrowser
=======================
IMPORTANT: qutebrowser's configuration system was completely rewritten in
September 2017. This information is not applicable to older releases, and older
information elsewhere might be outdated. **If you had an old configuration
around and upgraded, this page will automatically open once**. To view it at a
later time, use the `:help` command.
Migrating older configurations
------------------------------
qutebrowser does no automatic migration for the new configuration. However,
there's a special link:qute://configdiff[] page in qutebrowser, which will show
you the changes you did in your old configuration, compared to the old defaults.
Other changes in default settings:
- `<Up>` and `<Down>` in the completion now navigate through command history
instead of selecting completion items. You can get back the old behavior by
doing:
+
----
:bind -f -m command <Up> completion-item-focus prev
:bind -f -m command <Down> completion-item-focus next
----
- The default for `completion.web_history_max_items` is now set to `-1`, showing
an unlimited number of items in the completion for `:open` as the new
sqlite-based completion is much faster. If the `:open` completion is too slow
on your machine, set an appropriate limit again.
Configuring qutebrowser via the user interface
----------------------------------------------
The easy (but less flexible) way to configure qutebrowser is using its user
interface or command line. Changes you make this way are immediately active
(with the exception of a few settings, where this is pointed out in the
documentation) and are persisted in an `autoconfig.yml` file.
The `autoconfig.yml` file is located in the "config" folder listed on the
link:qute://version[] page. On macOS, the "auto config" folder is used, which is
different from where hand-written config files are kept.
However, **do not** edit `autoconfig.yml` by hand. Instead, see the next
section.
If you want to customize many settings, you can open the link:qute://settings[]
page by running `:set` without any arguments, where all settings are listed and
customizable.
Using the link:commands.html#set[`:set`] command and command completion, you
can quickly set settings interactively, for example `:set tabs.position left`.
To get more help about a setting, use e.g. `:help tabs.position`.
To bind and unbind keys, you can use the link:commands.html#bind[`:bind`] and
link:commands.html#unbind[`:unbind`] commands:
- Binding the key chain "`,`, `v`" to the `:spawn mpv {url}` command:
`:bind ,v spawn mpv {url}`
- Unbinding the same key chain: `:unbind ,v`
- Changing an existing binding: `bind --force ,v message-info foo`. Without
`--force`, qutebrowser will show an error because `,v` is already bound.
Key chains starting with a comma are ideal for custom bindings, as the comma key
will never be used in a default keybinding.
See the help pages linked above (or `:help :bind`, `:help :unbind`) for more
information.
Configuring qutebrowser via config.py
-------------------------------------
For more powerful configuration possibilities, you can create a `config.py`
file. Since it's a Python file, you have much more flexibility for
configuration. Note that qutebrowser will never touch this file - this means
you'll be responsible for updating it when upgrading to a newer qutebrowser
version.
The file should be located in the "config" location listed on
link:qute://version[], which is typically `~/.config/qutebrowser/config.py` on
Linux, `~/.qutebrowser/config.py` on macOS, and
`%APPDATA%/qutebrowser/config.py` on Windows.
Two global objects are pre-defined when running `config.py`: `c` and `config`.
Changing settings
~~~~~~~~~~~~~~~~~
`c` is a shorthand object to easily set settings like this:
.config.py:
[source,python]
----
c.tabs.position = "left"
c.completion.shrink = True
----
Note that qutebrowser does some Python magic so it's able to warn you about
mistyped config settings. As an example, if you do `c.tabs.possition = "left"`,
you'll get an error when starting.
See the link:settings.html[settings help page] for all available settings. The
accepted values depend on the type of the option. Commonly used are:
- Strings: `c.tabs.position = "left"`
- Booleans: `c.completion.shrink = True`
- Integers: `c.messages.timeout = 5000`
- Dictionaries:
* `c.headers.custom = {'X-Hello': 'World', 'X-Awesome': 'yes'}` to override
any other values in the dictionary.
* `c.aliases['foo'] = ':message-info foo'` to add a single value.
- Lists:
* `c.url.start_pages = ["https://www.qutebrowser.org/"]` to override any
previous elements.
* `c.url.start_pages.append("https://www.python.org/")` to add a new value.
Any other config types (e.g. a color) are specified as a string. The only
exception is the `Regex` type, which can take either a string (with an `r`
prefix to preserve backslashes) or a Python regex object:
- `c.hints.next_regexes.append(r'\bvor\b')`
- `c.hints.prev_regexes.append(re.compile(r'\bzurück\b'))`
If you want to read a setting, you can use the `c` object to do so as well:
`c.colors.tabs.even.bg = c.colors.tabs.odd.bg`.
Using strings for setting names
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you want to set settings based on their name as a string, use the
`config.set` method:
.config.py:
[source,python]
----
config.set('content.javascript.enabled', False)
----
To read a setting, use the `config.get` method:
[source,python]
----
color = config.get('colors.completion.fg')
----
Binding keys
~~~~~~~~~~~~
While it's possible to change the `bindings.commands` setting to bind keys, it's
preferred to use the `config.bind` command. Doing so ensures the commands are
valid and normalizes different expressions which map to the same key.
For details on how to specify keys and the available modes, see the
link:settings.html#bindings.commands[documentation] for the `bindings.commands`
setting.
To bind a key:
.config.py:
[source,python]
----
config.bind('<Ctrl-v>', 'spawn mpv {url}')
----
To bind a key in a mode other than `'normal'`, add a `mode` argument:
[source,python]
----
config.bind('<Ctrl-y>', 'prompt-yes', mode='prompt')
----
If the key is already bound, `force=True` needs to be given to rebind it:
[source,python]
----
config.bind('<Ctrl-v>', 'message-info foo', force=True)
----
To unbind a key (either a key which has been bound before, or a default binding):
[source,python]
----
config.unbind('<Ctrl-v>', mode='normal')
----
To bind keys without modifiers, specify a key chain to bind as a string. Key
chains starting with a comma are ideal for custom bindings, as the comma key
will never be used in a default keybinding.
[source,python]
----
config.bind(',v', 'spawn mpv {url}')
----
To suppress loading of any default keybindings, you can set
`c.bindings.default = {}`.
Prevent loading `autoconfig.yml`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you want all customization done via `:set`, `:bind` and `:unbind` to be
temporary, you can suppress loading `autoconfig.yml` in your `config.py` by
doing:
.config.py:
[source,python]
----
config.load_autoconfig = False
----
Handling errors
~~~~~~~~~~~~~~~
If there are errors in your `config.py`, qutebrowser will try to apply as much
of it as possible, and show an error dialog before starting.
qutebrowser tries to display errors which are easy to understand even for people
who are not used to writing Python. If you see a config error which you find
confusing or you think qutebrowser could handle better, please
https://github.com/qutebrowser/qutebrowser/issues[open an issue]!

View File

@ -7,12 +7,13 @@ Documentation
The following help pages are currently available: The following help pages are currently available:
* link:../quickstart.html[Quick start guide] * link:../quickstart.html[Quick start guide]
* link:../../FAQ.html[Frequently asked questions] * link:../doc.html[Frequently asked questions]
* link:../../CHANGELOG.html[Change Log] * link:../changelog.html[Change Log]
* link:commands.html[Documentation of commands] * link:commands.html[Documentation of commands]
* link:configuring.html[Configuring qutebrowser]
* link:settings.html[Documentation of settings] * link:settings.html[Documentation of settings]
* link:../userscripts.html[How to write userscripts] * link:../userscripts.html[How to write userscripts]
* link:../../CONTRIBUTING.html[Contributing to qutebrowser] * link:../contributing.html[Contributing to qutebrowser]
Getting help Getting help
------------ ------------

File diff suppressed because it is too large Load Diff

View File

@ -3,39 +3,50 @@ Installing qutebrowser
toc::[] toc::[]
NOTE: qutebrowser recently had some bigger dependency changes for v1.0.0, which
means those instructions might be out of date in some places.
https://github.com/qutebrowser/qutebrowser/blob/master/doc/contributing.asciidoc[Please help]
updating them if you notice something being broken!
On Debian / Ubuntu On Debian / Ubuntu
------------------ ------------------
qutebrowser should run on these systems: How to install qutebrowser depends a lot on the version of Debian/Ubuntu you're
running.
* Debian jessie or newer Debian Jessie / Ubuntu 14.04 LTS / Linux Mint < 18
* Ubuntu Trusty (14.04 LTS) or newer ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* Any other distribution based on these (e.g. Linux Mint 17+)
Unfortunately there is no Debian package in the official repos yet, but installing qutebrowser is Those distributions only have Python 3.4 and a too old Qt version available,
still relatively easy! while qutebrowser requires Python 3.5 and Qt 5.7.1 or newer.
You can use packages that are built for every release or build it yourself from git. It should be possible to install Python 3.5 e.g. from the
https://launchpad.net/~deadsnakes/+archive/ubuntu/ppa[deadsnakes PPA] or via_ipca
https://github.com/pyenv/pyenv[pyenv], but nobody tried that yet.
On Ubuntu 16.04 and 16.10 it's recommended to <<tox,install qutebrowser via tox>> If you get qutebrowser running on those distributions, please
instead in order to be able to use the new QtWebEngine backend. Newer versions https://github.com/qutebrowser/qutebrowser/blob/master/doc/contributing.asciidoc[contribute]
have a QtWebEngine package in the repositories. to update this documentation!
Using the packages Ubuntu 16.04 LTS / Linux Mint 18
~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Ubuntu 16.04 doesn't come with an up-to-date engine (a new enough QtWebKit, or
QtWebEngine). However, it comes with Python 3.5, so you can
<<tox,install qutebrowser via tox>>.
Debian Stretch / Ubuntu 17.04 and newer
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Those versions come with QtWebEngine in the repositories. This makes it possible
to install qutebrowser via the Debian package.
Install the dependencies via apt-get: Install the dependencies via apt-get:
---- ----
# apt-get install python3-lxml python-tox python3-pyqt5 python3-pyqt5.qtwebkit python3-pyqt5.qtquick python3-sip python3-jinja2 python3-pygments python3-yaml python3-pyqt5.qtsql libqt5sql5-sqlite # apt install python-tox python3-{lxml,pyqt5,sip,jinja2,pygments,yaml} python3-pyqt5.qt{webengine,quick,opengl,sql} libqt5sql5-sqlite
---- ----
On Debian Stretch or Ubuntu 17.04 or later, it's also recommended to use the
newer QtWebEngine backend.
To do so, install `python3-pyqt5.qtwebengine` and `python3-pyqt5.qtopengl`, then
start qutebrowser with `--backend webengine`.
Get the qutebrowser package from the Get the qutebrowser package from the
https://github.com/qutebrowser/qutebrowser/releases[release page] and download https://github.com/qutebrowser/qutebrowser/releases[release page] and download
the https://qutebrowser.org/python3-pypeg2_2.15.2-1_all.deb[PyPEG2 package]. the https://qutebrowser.org/python3-pypeg2_2.15.2-1_all.deb[PyPEG2 package].
@ -47,35 +58,27 @@ Install the packages:
# dpkg -i qutebrowser_*_all.deb # dpkg -i qutebrowser_*_all.deb
---- ----
Build it from git Some additional hints:
~~~~~~~~~~~~~~~~~
Install the dependencies via apt-get:
- Alternatively, you can <<tox,install qutebrowser via tox>> to get a newer
QtWebEngine version.
- If running from git, run the following to generate the documentation for the
`:help` command:
+
---- ----
# apt-get install python3-pyqt5 python3-pyqt5.qtwebkit python3-pyqt5.qtquick python-tox python3-sip python3-dev python3-pyqt5.qtsql libqt5sql5-sqlite # apt-get install --no-install-recommends asciidoc source-highlight
----
On Debian Stretch or Ubuntu 17.04 or later, it's also recommended to install
`python3-pyqt5.qtwebengine` and start qutebrowser with `--backend webengine` in
order to use the new backend.
To generate the documentation for the `:help` command, when using the git
repository (rather than a release):
----
# apt-get install asciidoc source-highlight
$ python3 scripts/asciidoc2html.py $ python3 scripts/asciidoc2html.py
---- ----
If video or sound don't seem to work, try installing the gstreamer plugins: - If you prefer using QtWebKit, there's an up-to-date version available in
Debian experimental, or from http://repo.paretje.be/unstable/[this repository]
for Debian Stretch.
- If video or sound don't work with QtWebKit, try installing the gstreamer plugins:
+
---- ----
# apt-get install gstreamer1.0-plugins-{bad,base,good,ugly} # apt-get install gstreamer1.0-plugins-{bad,base,good,ugly}
---- ----
Then <<tox,install qutebrowser via tox>>.
On Fedora On Fedora
--------- ---------
@ -85,7 +88,7 @@ qutebrowser is available in the official repositories for Fedora 22 and newer.
# dnf install qutebrowser # dnf install qutebrowser
---- ----
It's also recommended to install `qt5-qtwebengine` and start with `--backend It's also recommended to install `python3-qt5-webengine` and start with `--backend
webengine` to use the new backend. webengine` to use the new backend.
On Archlinux On Archlinux
@ -116,7 +119,7 @@ $ rm -r qutebrowser-git
or you could use an AUR helper, e.g. `yaourt -S qutebrowser-git`. or you could use an AUR helper, e.g. `yaourt -S qutebrowser-git`.
If video or sound don't seem to work, try installing the gstreamer plugins: If video or sound don't work with QtWebKit, try installing the gstreamer plugins:
---- ----
# pacman -S gst-plugins-{base,good,bad,ugly} gst-libav # pacman -S gst-plugins-{base,good,bad,ugly} gst-libav
@ -125,6 +128,8 @@ If video or sound don't seem to work, try installing the gstreamer plugins:
On Gentoo On Gentoo
--------- ---------
WARNING: The Gentoo packages (even the live version) are lagging behind a lot, which means those instructions probably won't work anymore. Until things are looking better, it's recommended to <<tox,install qutebrowser via tox>>.
A version of qutebrowser is available in the main repository and can be installed with: A version of qutebrowser is available in the main repository and can be installed with:
---- ----
@ -161,20 +166,22 @@ To update to the last Live version, remember to do
To include qutebrowser among the updates. To include qutebrowser among the updates.
Make sure you have `python3_4` in your `PYTHON_TARGETS` You'll also need to install `dev-qt/qtwebengine` or a newer QtWebKit using
(`/etc/portage/make.conf`) and rebuild your system (`emerge -uDNav @world`) if https://gist.github.com/annulen/309569fb61e5d64a703c055c1e726f71[this ebuild].
necessary.
It's also recommended to install QtWebKit-NG via If video or sound don't work with QtWebKit, try installing the gstreamer
https://gist.github.com/annulen/309569fb61e5d64a703c055c1e726f71[this ebuild], plugins:
or install Qt >= 5.7.1 with QtWebEngine in order to use an up-to-date backend.
If video or sound don't seem to work, try installing the gstreamer plugins:
---- ----
# emerge -av gst-plugins-{base,good,bad,ugly,libav} # emerge -av gst-plugins-{base,good,bad,ugly,libav}
---- ----
To be able to play videos with proprietary codecs with QtWebEngine, you will
need to turn off the `bindist` flag for `dev-qt/qtwebengine`.
See the https://wiki.gentoo.org/wiki/Qutebrowser#USE_flags[Gentoo Wiki] for
more information.
On Void Linux On Void Linux
------------- -------------
@ -206,17 +213,9 @@ It's recommended to install `qt5.qtwebengine` and start with
On openSUSE On openSUSE
----------- -----------
There are prebuilt RPMs available for Tumbleweed and Leap 42.1: There are prebuilt RPMs available at https://software.opensuse.org/download.html?project=network&package=qutebrowser[OBS].
http://software.opensuse.org/download.html?project=home%3Aarpraher&package=qutebrowser[One Click Install] To use the QtWebEngine backend, install `libqt5-qtwebengine`.
Or add the repo manually:
----
# zypper addrepo http://download.opensuse.org/repositories/home:arpraher/openSUSE_Tumbleweed/home:arpraher.repo
# zypper refresh
# zypper install qutebrowser
----
On OpenBSD On OpenBSD
---------- ----------
@ -332,6 +331,9 @@ it as part of the packaging process.
Installing qutebrowser with tox Installing qutebrowser with tox
------------------------------- -------------------------------
Getting the repository
~~~~~~~~~~~~~~~~~~~~~~
First of all, clone the repository using http://git-scm.org/[git] and switch First of all, clone the repository using http://git-scm.org/[git] and switch
into the repository folder: into the repository folder:
@ -340,6 +342,8 @@ $ git clone https://github.com/qutebrowser/qutebrowser.git
$ cd qutebrowser $ cd qutebrowser
---- ----
Installing depdendencies (including Qt)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Then run tox inside the qutebrowser repository to set up a Then run tox inside the qutebrowser repository to set up a
https://docs.python.org/3/library/venv.html[virtual environment]: https://docs.python.org/3/library/venv.html[virtual environment]:
@ -348,20 +352,39 @@ https://docs.python.org/3/library/venv.html[virtual environment]:
$ tox -e mkvenv-pypi $ tox -e mkvenv-pypi
---- ----
If your distribution uses OpenSSL 1.1 (like Debian Stretch or Archlinux), you'll This installs all needed Python dependencies in a `.venv` subfolder.
need to set `LD_LIBRARY_PATH` to the OpenSSL 1.0 directory
(`export LD_LIBRARY_PATH=/usr/lib/openssl-1.0` on Archlinux) before starting This comes with an up-to-date Qt/PyQt including QtWebEngine, but has a few
qutebrowser. caveats:
- Make sure your `python3` is Python 3.5 or newer, otherwise you'll get a "No
matching distribution found" error. Note that qutebrowser itself also requires
this.
- It only works on 64-bit x86 systems, with other architectures you'll get the
same error.
- If your distribution uses OpenSSL 1.1 (like Debian Stretch or Archlinux),
you'll need to set `LD_LIBRARY_PATH` to the OpenSSL 1.0 directory
(`export LD_LIBRARY_PATH=/usr/lib/openssl-1.0` on Archlinux) before starting
qutebrowser if you want SSL to work in certain downloads (e.g. for
`:adblock-update` or `:download`).
- It comes with a QtWebEngine compiled without proprietary codec support (such
as h.264).
See the next section for an alternative.
Installing dependencies (system-wide Qt)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Alternatively, you can use `tox -e mkvenv` (without `-pypi`) to symlink your Alternatively, you can use `tox -e mkvenv` (without `-pypi`) to symlink your
local Qt install instead of installing PyQt in the virtualenv. However, unless local Qt install instead of installing PyQt in the virtualenv. However, unless
you have QtWebKit-NG or QtWebEngine available, qutebrowser will use the legacy you have a new QtWebKit or QtWebEngine available, qutebrowser will not work. It
QtWebKit backend. also typically means you'll be using an older release of QtWebEngine.
On Windows, run `tox -e 'mkvenv-win' instead, however make sure that ONLY On Windows, run `tox -e 'mkvenv-win' instead, however make sure that ONLY
Python3 is in your PATH before running tox. Python3 is in your PATH before running tox.
This installs all needed Python dependencies in a `.venv` subfolder. Creating a wrapper script
~~~~~~~~~~~~~~~~~~~~~~~~~
You can then create a simple wrapper script to start qutebrowser somewhere in You can then create a simple wrapper script to start qutebrowser somewhere in
your `$PATH` (e.g. `/usr/local/bin/qutebrowser` or `~/bin/qutebrowser`): your `$PATH` (e.g. `/usr/local/bin/qutebrowser` or `~/bin/qutebrowser`):

View File

@ -44,7 +44,7 @@ show it.
*-V*, *--version*:: *-V*, *--version*::
Show version and quit. Show version and quit.
*-s* 'SECTION' 'OPTION' 'VALUE', *--set* 'SECTION' 'OPTION' 'VALUE':: *-s* 'OPTION' 'VALUE', *--set* 'OPTION' 'VALUE'::
Set a temporary setting for this session. Set a temporary setting for this session.
*-r* 'SESSION', *--restore* 'SESSION':: *-r* 'SESSION', *--restore* 'SESSION'::
@ -84,9 +84,6 @@ show it.
*--force-color*:: *--force-color*::
Force colored logging Force colored logging
*--harfbuzz* '{old,new,system,auto}'::
HarfBuzz engine version to use. Default: auto.
*--relaxed-config*:: *--relaxed-config*::
Silently remove unknown config options. Silently remove unknown config options.

View File

@ -23,7 +23,7 @@ SetCompressor /solid lzma
!define MUI_ICON "../icons/qutebrowser.ico" !define MUI_ICON "../icons/qutebrowser.ico"
!define MUI_UNICON "../icons/qutebrowser.ico" !define MUI_UNICON "../icons/qutebrowser.ico"
!insertmacro MUI_PAGE_LICENSE "..\COPYING" !insertmacro MUI_PAGE_LICENSE "..\LICENSE"
!insertmacro MUI_PAGE_DIRECTORY !insertmacro MUI_PAGE_DIRECTORY
!insertmacro MUI_PAGE_INSTFILES !insertmacro MUI_PAGE_INSTFILES
!insertmacro MUI_UNPAGE_CONFIRM !insertmacro MUI_UNPAGE_CONFIRM

View File

@ -15,7 +15,8 @@ def get_data_files():
('../qutebrowser/img', 'img'), ('../qutebrowser/img', 'img'),
('../qutebrowser/javascript', 'javascript'), ('../qutebrowser/javascript', 'javascript'),
('../qutebrowser/html/doc', 'html/doc'), ('../qutebrowser/html/doc', 'html/doc'),
('../qutebrowser/git-commit-id', '') ('../qutebrowser/git-commit-id', ''),
('../qutebrowser/config/configdata.yml', 'config'),
] ]
# if os.path.exists(os.path.join('qutebrowser', '3rdparty', 'pdfjs')): # if os.path.exists(os.path.join('qutebrowser', '3rdparty', 'pdfjs')):

View File

@ -4,6 +4,6 @@ certifi==2017.7.27.1
chardet==3.0.4 chardet==3.0.4
codecov==2.0.9 codecov==2.0.9
coverage==4.4.1 coverage==4.4.1
idna==2.5 idna==2.6
requests==2.18.2 requests==2.18.4
urllib3==1.22 urllib3==1.22

View File

@ -18,6 +18,6 @@ packaging==16.8
pep8-naming==0.4.1 pep8-naming==0.4.1
pycodestyle==2.3.1 pycodestyle==2.3.1
pydocstyle==1.1.1 # rq.filter: < 2.0.0 pydocstyle==1.1.1 # rq.filter: < 2.0.0
pyflakes==1.5.0 pyflakes==1.6.0
pyparsing==2.2.0 pyparsing==2.2.0
six==1.10.0 six==1.11.0

View File

@ -3,6 +3,6 @@
appdirs==1.4.3 appdirs==1.4.3
packaging==16.8 packaging==16.8
pyparsing==2.2.0 pyparsing==2.2.0
setuptools==36.2.5 setuptools==36.2.7
six==1.10.0 six==1.10.0
wheel==0.29.0 wheel==0.29.0

View File

@ -1,3 +1,7 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py # This file is automatically generated by scripts/dev/recompile_requirements.py
-e git+https://github.com/xoviat/pyinstaller.git@qtweb#egg=PyInstaller altgraph==0.14
future==0.16.0
macholib==1.8
pefile==2017.9.3
-e git+https://github.com/pyinstaller/pyinstaller.git@develop#egg=PyInstaller

View File

@ -1,4 +1,4 @@
-e git+https://github.com/xoviat/pyinstaller.git@qtweb#egg=PyInstaller -e git+https://github.com/pyinstaller/pyinstaller.git@develop#egg=PyInstaller
# remove @commit-id for scm installs # remove @commit-id for scm installs
#@ replace: @.*# @qtweb# #@ replace: @.*# @develop#

View File

@ -4,15 +4,15 @@
certifi==2017.7.27.1 certifi==2017.7.27.1
chardet==3.0.4 chardet==3.0.4
github3.py==0.9.6 github3.py==0.9.6
idna==2.5 idna==2.6
isort==4.2.15 isort==4.2.15
lazy-object-proxy==1.3.1 lazy-object-proxy==1.3.1
mccabe==0.6.1 mccabe==0.6.1
-e git+https://github.com/PyCQA/pylint.git#egg=pylint -e git+https://github.com/PyCQA/pylint.git#egg=pylint
./scripts/dev/pylint_checkers ./scripts/dev/pylint_checkers
requests==2.18.2 requests==2.18.4
six==1.10.0 six==1.11.0
uritemplate==3.0.0 uritemplate==3.0.0
uritemplate.py==3.0.2 uritemplate.py==3.0.2
urllib3==1.22 urllib3==1.22
wrapt==1.10.10 wrapt==1.10.11

View File

@ -4,15 +4,15 @@ astroid==1.5.3
certifi==2017.7.27.1 certifi==2017.7.27.1
chardet==3.0.4 chardet==3.0.4
github3.py==0.9.6 github3.py==0.9.6
idna==2.5 idna==2.6
isort==4.2.15 isort==4.2.15
lazy-object-proxy==1.3.1 lazy-object-proxy==1.3.1
mccabe==0.6.1 mccabe==0.6.1
pylint==1.7.2 pylint==1.7.2
./scripts/dev/pylint_checkers ./scripts/dev/pylint_checkers
requests==2.18.2 requests==2.18.4
six==1.10.0 six==1.11.0
uritemplate==3.0.0 uritemplate==3.0.0
uritemplate.py==3.0.2 uritemplate.py==3.0.2
urllib3==1.22 urllib3==1.22
wrapt==1.10.10 wrapt==1.10.11

View File

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

View File

@ -4,3 +4,4 @@ pyPEG2
PyYAML PyYAML
colorama colorama
cssutils cssutils
attrs

View File

@ -4,7 +4,6 @@ hg+https://bitbucket.org/ned/coveragepy
git+https://github.com/micheles/decorator.git git+https://github.com/micheles/decorator.git
git+https://github.com/pallets/flask.git git+https://github.com/pallets/flask.git
git+https://github.com/miracle2k/python-glob2.git git+https://github.com/miracle2k/python-glob2.git
git+https://github.com/Runscope/httpbin.git
git+https://github.com/HypothesisWorks/hypothesis-python.git git+https://github.com/HypothesisWorks/hypothesis-python.git
git+https://github.com/pallets/itsdangerous.git git+https://github.com/pallets/itsdangerous.git
git+https://bitbucket.org/zzzeek/mako.git git+https://bitbucket.org/zzzeek/mako.git
@ -41,6 +40,7 @@ git+https://github.com/pallets/jinja.git
git+https://github.com/pallets/markupsafe.git git+https://github.com/pallets/markupsafe.git
hg+http://bitbucket.org/birkenfeld/pygments-main hg+http://bitbucket.org/birkenfeld/pygments-main
hg+https://bitbucket.org/fdik/pypeg hg+https://bitbucket.org/fdik/pypeg
git+https://github.com/python-attrs/attrs.git
# Fails to build: # Fails to build:
# gcc: error: ext/_yaml.c: No such file or directory # gcc: error: ext/_yaml.c: No such file or directory

View File

@ -1,18 +1,17 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py # This file is automatically generated by scripts/dev/recompile_requirements.py
attrs==17.2.0
beautifulsoup4==4.6.0 beautifulsoup4==4.6.0
cheroot==5.7.0 cheroot==5.8.3
click==6.7 click==6.7
# colorama==0.3.9 # colorama==0.3.9
coverage==4.4.1 coverage==4.4.1
decorator==4.1.2
EasyProcess==0.2.3 EasyProcess==0.2.3
fields==5.0.0 fields==5.0.0
Flask==0.12.2 Flask==0.12.2
glob2==0.5 glob2==0.6
httpbin==0.5.0 hunter==2.0.1
hunter==1.4.1 hypothesis==3.28.3
hypothesis==3.14.0
itsdangerous==0.24 itsdangerous==0.24
# Jinja2==2.9.6 # Jinja2==2.9.6
Mako==1.0.7 Mako==1.0.7
@ -20,20 +19,21 @@ Mako==1.0.7
parse==1.8.2 parse==1.8.2
parse-type==0.3.4 parse-type==0.3.4
py==1.4.34 py==1.4.34
pytest==3.1.3 py-cpuinfo==3.3.0
pytest==3.2.2
pytest-bdd==2.18.2 pytest-bdd==2.18.2
pytest-benchmark==3.1.1 pytest-benchmark==3.1.1
pytest-catchlog==1.2.2 pytest-catchlog==1.2.2
pytest-cov==2.5.1 pytest-cov==2.5.1
pytest-faulthandler==1.3.1 pytest-faulthandler==1.3.1
pytest-instafail==0.3.0 pytest-instafail==0.3.0
pytest-mock==1.6.2 pytest-mock==1.6.3
pytest-qt==2.1.2 pytest-qt==2.2.0
pytest-repeat==0.4.1 pytest-repeat==0.4.1
pytest-rerunfailures==2.2 pytest-rerunfailures==3.1
pytest-travis-fold==1.2.0 pytest-travis-fold==1.2.0
pytest-xvfb==1.0.0 pytest-xvfb==1.0.0
PyVirtualDisplay==0.2.1 PyVirtualDisplay==0.2.1
six==1.10.0 six==1.11.0
vulture==0.21 vulture==0.26
Werkzeug==0.12.2 Werkzeug==0.12.2

View File

@ -3,7 +3,6 @@ cheroot
coverage coverage
Flask Flask
hunter hunter
httpbin
hypothesis hypothesis
pytest pytest
pytest-bdd pytest-bdd

View File

@ -2,5 +2,5 @@
pluggy==0.4.0 pluggy==0.4.0
py==1.4.34 py==1.4.34
tox==2.7.0 tox==2.8.2
virtualenv==15.1.0 virtualenv==15.1.0

View File

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

View File

@ -23,7 +23,7 @@
# If run from qutebrowser as a userscript, it runs :open on the URL # If run from qutebrowser as a userscript, it runs :open on the URL
# If not, it opens a new qutebrowser window at the URL # If not, it opens a new qutebrowser window at the URL
# #
# Ideal for use with tabs-are-windows. Set a hotkey to launch this script, then: # Ideal for use with tabs_are_windows. Set a hotkey to launch this script, then:
# :bind o spawn --userscript dmenu_qutebrowser # :bind o spawn --userscript dmenu_qutebrowser
# #
# Use the hotkey to open in new tab/window, press 'o' to open URL in current tab/window # Use the hotkey to open in new tab/window, press 'o' to open URL in current tab/window

47
misc/userscripts/format_json Executable file
View File

@ -0,0 +1,47 @@
#!/bin/sh
#
# Behavior:
# Userscript for qutebrowser which will take the raw JSON text of the current
# page, format it using `jq`, will add syntax highlighting using `pygments`,
# and open the syntax highlighted pretty printed html in a new tab. If the file
# is larger than 10MB then this script will only indent the json and will forego
# syntax highlighting using pygments.
#
# In order to use this script, just start it using `spawn --userscript` from
# qutebrowser. I recommend using an alias, e.g. put this in the
# [alias]-section of qutebrowser.conf:
#
# json = spawn --userscript /path/to/json_format
#
# Note that the color style defaults to monokai, but a different pygments style
# can be passed as the first parameter to the script. A full list of the pygments
# styles can be found at: https://help.farbox.com/pygments.html
#
# Bryan Gilbert, 2017
# default style to monokai if none is provided
STYLE=${1:-monokai}
# format json using jq
FORMATTED_JSON="$(cat "$QUTE_TEXT" | jq '.')"
# if jq command failed or formatted json is empty, assume failure and terminate
if [ $? -ne 0 ] || [ -z "$FORMATTED_JSON" ]; then
echo "Invalid json, aborting..."
exit 1
fi
# calculate the filesize of the json document
FILE_SIZE=$(ls -s --block-size=1048576 "$QUTE_TEXT" | cut -d' ' -f1)
# use pygments to pretty-up the json (syntax highlight) if file is less than 10MB
if [ "$FILE_SIZE" -lt "10" ]; then
FORMATTED_JSON="$(echo "$FORMATTED_JSON" | pygmentize -l json -f html -O full,style=$STYLE)"
fi
# create a temp file and write the formatted json to that file
TEMP_FILE="$(mktemp --suffix '.html')"
echo "$FORMATTED_JSON" > $TEMP_FILE
# send the command to qutebrowser to open the new file containing the formatted json
echo "open -t file://$TEMP_FILE" >> "$QUTE_FIFO"

View File

@ -12,7 +12,7 @@
# - rofi (in a recent version) # - rofi (in a recent version)
# - xdg-open and xdg-mime # - xdg-open and xdg-mime
# - You should configure qutebrowser to download files to a single directory # - You should configure qutebrowser to download files to a single directory
# - It comes in handy if you enable remove-finished-downloads. If you want to # - It comes in handy if you enable downloads.remove_finished. If you want to
# see the recent downloads, just press "sd". # see the recent downloads, just press "sd".
# #
# Thorsten Wißmann, 2015 (thorsten` on freenode) # Thorsten Wißmann, 2015 (thorsten` on freenode)

View File

@ -21,7 +21,7 @@
# Opens all links to feeds defined in the head of a site # Opens all links to feeds defined in the head of a site
# #
# Ideal for use with tabs-are-windows. Set a hotkey to launch this script, then: # Ideal for use with tabs_are_windows. Set a hotkey to launch this script, then:
# :bind gF spawn --userscript openfeeds # :bind gF spawn --userscript openfeeds
# #
# Use the hotkey to open the feeds in new tab/window, press 'gF' to open # Use the hotkey to open the feeds in new tab/window, press 'gF' to open

View File

@ -327,6 +327,17 @@ open_entry "$file"
js() { js() {
cat <<EOF cat <<EOF
function isVisible(elem) {
var style = elem.ownerDocument.defaultView.getComputedStyle(elem, null);
if (style.getPropertyValue("visibility") !== "visible" ||
style.getPropertyValue("display") === "none" ||
style.getPropertyValue("opacity") === "0") {
return false;
}
return elem.offsetWidth > 0 && elem.offsetHeight > 0;
};
function hasPasswordField(form) { function hasPasswordField(form) {
var inputs = form.getElementsByTagName("input"); var inputs = form.getElementsByTagName("input");
for (var j = 0; j < inputs.length; j++) { for (var j = 0; j < inputs.length; j++) {
@ -341,7 +352,7 @@ cat <<EOF
var inputs = form.getElementsByTagName("input"); var inputs = form.getElementsByTagName("input");
for (var j = 0; j < inputs.length; j++) { for (var j = 0; j < inputs.length; j++) {
var input = inputs[j]; var input = inputs[j];
if (input.type == "text" || input.type == "email") { if (isVisible(input) && (input.type == "text" || input.type == "email")) {
input.focus(); input.focus();
input.value = "$(javascript_escape "${username}")"; input.value = "$(javascript_escape "${username}")";
input.blur(); input.blur();

View File

@ -15,11 +15,10 @@ markers =
end2end: End to end tests which run qutebrowser as subprocess end2end: End to end tests which run qutebrowser as subprocess
xfail_norun: xfail the test with out running it xfail_norun: xfail the test with out running it
ci: Tests which should only run on CI. ci: Tests which should only run on CI.
no_ci: Tests which should not run on CI.
qtwebengine_todo: Features still missing with QtWebEngine qtwebengine_todo: Features still missing with QtWebEngine
qtwebengine_skip: Tests not applicable with QtWebEngine qtwebengine_skip: Tests not applicable with QtWebEngine
qtwebkit_skip: Tests not applicable with QtWebKit qtwebkit_skip: Tests not applicable with QtWebKit
qtwebkit_ng_xfail: Tests failing with QtWebKit-NG
qtwebkit_ng_skip: Tests skipped with QtWebKit-NG
qtwebengine_flaky: Tests which are flaky (and currently skipped) with QtWebEngine qtwebengine_flaky: Tests which are flaky (and currently skipped) with QtWebEngine
qtwebengine_mac_xfail: Tests which fail on macOS with QtWebEngine qtwebengine_mac_xfail: Tests which fail on macOS with QtWebEngine
js_prompt: Tests needing to display a javascript prompt js_prompt: Tests needing to display a javascript prompt
@ -44,16 +43,14 @@ qt_log_ignore =
^QWaitCondition: Destroyed while threads are still waiting ^QWaitCondition: Destroyed while threads are still waiting
^QXcbXSettings::QXcbXSettings\(QXcbScreen\*\) Failed to get selection owner for XSETTINGS_S atom ^QXcbXSettings::QXcbXSettings\(QXcbScreen\*\) Failed to get selection owner for XSETTINGS_S atom
^QStandardPaths: XDG_RUNTIME_DIR not set, defaulting to .* ^QStandardPaths: XDG_RUNTIME_DIR not set, defaulting to .*
^QGeoclueMaster error creating GeoclueMasterClient\.
^Geoclue error: Process org\.freedesktop\.Geoclue\.Master exited with status 127
^QDBusConnection: name 'org.freedesktop.Geoclue.Master' had owner '' but we thought it was ':1.1'
^QObject::connect: Cannot connect \(null\)::stateChanged\(QNetworkSession::State\) to QNetworkReplyHttpImpl::_q_networkSessionStateChanged\(QNetworkSession::State\) ^QObject::connect: Cannot connect \(null\)::stateChanged\(QNetworkSession::State\) to QNetworkReplyHttpImpl::_q_networkSessionStateChanged\(QNetworkSession::State\)
^QXcbClipboard: Cannot transfer data, no data available ^QXcbClipboard: Cannot transfer data, no data available
^load glyph failed ^load glyph failed
^Error when parsing the netrc file ^Error when parsing the netrc file
^Image of format '' blocked because it is not considered safe. If you are sure it is safe to do so, you can white-list the format by setting the environment variable QTWEBKIT_IMAGEFORMAT_WHITELIST= ^Image of format '' blocked because it is not considered safe. If you are sure it is safe to do so, you can white-list the format by setting the environment variable QTWEBKIT_IMAGEFORMAT_WHITELIST=
^QPainter::end: Painter ended with \d+ saved states ^QPainter::end: Painter ended with \d+ saved states
^QSslSocket: cannot resolve SSLv[23]_(client|server)_method ^QSslSocket: cannot resolve *
^Incompatible version of OpenSSL
^QQuickWidget::invalidateRenderControl could not make context current ^QQuickWidget::invalidateRenderControl could not make context current
^libpng warning: iCCP: known incorrect sRGB profile ^libpng warning: iCCP: known incorrect sRGB profile
xfail_strict = true xfail_strict = true

View File

@ -22,7 +22,6 @@
import os import os
import sys import sys
import subprocess import subprocess
import configparser
import functools import functools
import json import json
import shutil import shutil
@ -44,8 +43,7 @@ import qutebrowser
import qutebrowser.resources import qutebrowser.resources
from qutebrowser.completion.models import miscmodels from qutebrowser.completion.models import miscmodels
from qutebrowser.commands import cmdutils, runners, cmdexc from qutebrowser.commands import cmdutils, runners, cmdexc
from qutebrowser.config import style, config, websettings, configexc from qutebrowser.config import config, websettings, configexc, configfiles
from qutebrowser.config.parsers import keyconf
from qutebrowser.browser import (urlmarks, adblock, history, browsertab, from qutebrowser.browser import (urlmarks, adblock, history, browsertab,
downloads) downloads)
from qutebrowser.browser.network import proxy from qutebrowser.browser.network import proxy
@ -54,11 +52,14 @@ from qutebrowser.browser.webkit.network import networkmanager
from qutebrowser.keyinput import macros from qutebrowser.keyinput import macros
from qutebrowser.mainwindow import mainwindow, prompt from qutebrowser.mainwindow import mainwindow, prompt
from qutebrowser.misc import (readline, ipc, savemanager, sessions, from qutebrowser.misc import (readline, ipc, savemanager, sessions,
crashsignal, earlyinit, objects, sql) crashsignal, earlyinit, sql, cmdhistory)
from qutebrowser.misc import utilcmds # pylint: disable=unused-import from qutebrowser.utils import (log, version, message, utils, urlutils, objreg,
from qutebrowser.utils import (log, version, message, utils, qtutils, urlutils, usertypes, standarddir, error)
objreg, usertypes, standarddir, error) # pylint: disable=unused-import
# We import utilcmds to run the cmdutils.register decorators. # We import those to run the cmdutils.register decorators.
from qutebrowser.mainwindow.statusbar import command
from qutebrowser.misc import utilcmds
# pylint: enable=unused-import
qApp = None qApp = None
@ -72,6 +73,12 @@ def run(args):
quitter = Quitter(args) quitter = Quitter(args)
objreg.register('quitter', quitter) objreg.register('quitter', quitter)
log.init.debug("Initializing directories...")
standarddir.init(args)
log.init.debug("Initializing config...")
config.early_init(args)
global qApp global qApp
qApp = Application(args) qApp = Application(args)
qApp.setOrganizationName("qutebrowser") qApp.setOrganizationName("qutebrowser")
@ -79,9 +86,6 @@ def run(args):
qApp.setApplicationVersion(qutebrowser.__version__) qApp.setApplicationVersion(qutebrowser.__version__)
qApp.lastWindowClosed.connect(quitter.on_last_window_closed) qApp.lastWindowClosed.connect(quitter.on_last_window_closed)
log.init.debug("Initializing directories...")
standarddir.init(args)
if args.version: if args.version:
print(version.version()) print(version.version())
sys.exit(usertypes.Exit.ok) sys.exit(usertypes.Exit.ok)
@ -148,8 +152,6 @@ def init(args, crash_handler):
objreg.register('event-filter', event_filter) objreg.register('event-filter', event_filter)
log.init.debug("Connecting signals...") log.init.debug("Connecting signals...")
config_obj = objreg.get('config')
config_obj.style_changed.connect(style.get_stylesheet.cache_clear)
qApp.focusChanged.connect(on_focus_changed) qApp.focusChanged.connect(on_focus_changed)
_process_args(args) _process_args(args)
@ -184,11 +186,10 @@ def _init_icon():
def _process_args(args): def _process_args(args):
"""Open startpage etc. and process commandline args.""" """Open startpage etc. and process commandline args."""
config_obj = objreg.get('config') for opt, val in args.temp_settings:
for sect, opt, val in args.temp_settings:
try: try:
config_obj.set('temp', sect, opt, val) config.instance.set_str(opt, val)
except (configexc.Error, configparser.Error) as e: except configexc.Error as e:
message.error("set: {} - {}".format(e.__class__.__name__, e)) message.error("set: {} - {}".format(e.__class__.__name__, e))
if not args.override_restore: if not args.override_restore:
@ -215,13 +216,12 @@ def _load_session(name):
Args: Args:
name: The name of the session to load, or None to read state file. name: The name of the session to load, or None to read state file.
""" """
state_config = objreg.get('state-config')
session_manager = objreg.get('session-manager') session_manager = objreg.get('session-manager')
if name is None and session_manager.exists('_autosave'): if name is None and session_manager.exists('_autosave'):
name = '_autosave' name = '_autosave'
elif name is None: elif name is None:
try: try:
name = state_config['general']['session'] name = configfiles.state['general']['session']
except KeyError: except KeyError:
# No session given as argument and none in the session file -> # No session given as argument and none in the session file ->
# start without loading a session # start without loading a session
@ -234,7 +234,7 @@ def _load_session(name):
except sessions.SessionError as e: except sessions.SessionError as e:
message.error("Failed to load session {}: {}".format(name, e)) message.error("Failed to load session {}: {}".format(name, e))
try: try:
del state_config['general']['session'] del configfiles.state['general']['session']
except KeyError: except KeyError:
pass pass
# If this was a _restart session, delete it. # If this was a _restart session, delete it.
@ -274,7 +274,7 @@ def process_pos_args(args, via_ipc=False, cwd=None, target_arg=None):
if via_ipc and target_arg and target_arg != 'auto': if via_ipc and target_arg and target_arg != 'auto':
open_target = target_arg open_target = target_arg
else: else:
open_target = config.get('general', 'new-instance-open-target') open_target = config.val.new_instance_open_target
win_id = mainwindow.get_window(via_ipc, force_target=open_target) win_id = mainwindow.get_window(via_ipc, force_target=open_target)
tabbed_browser = objreg.get('tabbed-browser', scope='window', tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id) window=win_id)
@ -289,7 +289,7 @@ def process_pos_args(args, via_ipc=False, cwd=None, target_arg=None):
else: else:
background = open_target in ['tab-bg', 'tab-bg-silent'] background = open_target in ['tab-bg', 'tab-bg-silent']
tabbed_browser.tabopen(url, background=background, tabbed_browser.tabopen(url, background=background,
explicit=True) related=False)
def _open_startpage(win_id=None): def _open_startpage(win_id=None):
@ -309,15 +309,9 @@ def _open_startpage(win_id=None):
tabbed_browser = objreg.get('tabbed-browser', scope='window', tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=cur_win_id) window=cur_win_id)
if tabbed_browser.count() == 0: if tabbed_browser.count() == 0:
log.init.debug("Opening startpage") log.init.debug("Opening start pages")
for urlstr in config.get('general', 'startpage'): for url in config.val.url.start_pages:
try: tabbed_browser.tabopen(url)
url = urlutils.fuzzy_url(urlstr, do_search=False)
except urlutils.InvalidUrlError as e:
message.error("Error when opening startpage: {}".format(e))
tabbed_browser.tabopen(QUrl('about:blank'))
else:
tabbed_browser.tabopen(url)
def _open_special_pages(args): def _open_special_pages(args):
@ -334,36 +328,29 @@ def _open_special_pages(args):
# With --basedir given, don't open anything. # With --basedir given, don't open anything.
return return
state_config = objreg.get('state-config') general_sect = configfiles.state['general']
tabbed_browser = objreg.get('tabbed-browser', scope='window', tabbed_browser = objreg.get('tabbed-browser', scope='window',
window='last-focused') window='last-focused')
# Legacy QtWebKit warning
needs_warning = (objects.backend == usertypes.Backend.QtWebKit and
not qtutils.is_qtwebkit_ng())
warning_shown = state_config['general'].get('backend-warning-shown') == '1'
if not warning_shown and needs_warning:
tabbed_browser.tabopen(QUrl('qute://backend-warning'),
background=False)
state_config['general']['backend-warning-shown'] = '1'
# Quickstart page # Quickstart page
quickstart_done = state_config['general'].get('quickstart-done') == '1' quickstart_done = general_sect.get('quickstart-done') == '1'
if not quickstart_done: if not quickstart_done:
tabbed_browser.tabopen( tabbed_browser.tabopen(
QUrl('https://www.qutebrowser.org/quickstart.html')) QUrl('https://www.qutebrowser.org/quickstart.html'))
state_config['general']['quickstart-done'] = '1' general_sect['quickstart-done'] = '1'
# Setting migration page
def _save_version(): needs_migration = os.path.exists(
"""Save the current version to the state config.""" os.path.join(standarddir.config(), 'qutebrowser.conf'))
state_config = objreg.get('state-config', None) migration_shown = general_sect.get('config-migration-shown') == '1'
if state_config is not None:
state_config['general']['version'] = qutebrowser.__version__ if needs_migration and not migration_shown:
tabbed_browser.tabopen(QUrl('qute://help/configuring.html'),
background=False)
general_sect['config-migration-shown'] = '1'
def on_focus_changed(_old, new): def on_focus_changed(_old, new):
@ -406,7 +393,7 @@ def _init_modules(args, crash_handler):
log.init.debug("Initializing save manager...") log.init.debug("Initializing save manager...")
save_manager = savemanager.SaveManager(qApp) save_manager = savemanager.SaveManager(qApp)
objreg.register('save-manager', save_manager) objreg.register('save-manager', save_manager)
save_manager.add_saveable('version', _save_version) config.late_init(save_manager)
log.init.debug("Initializing network...") log.init.debug("Initializing network...")
networkmanager.init() networkmanager.init()
@ -418,13 +405,6 @@ def _init_modules(args, crash_handler):
readline_bridge = readline.ReadlineBridge() readline_bridge = readline.ReadlineBridge()
objreg.register('readline-bridge', readline_bridge) objreg.register('readline-bridge', readline_bridge)
log.init.debug("Initializing config...")
config.init(qApp)
save_manager.init_autosave()
log.init.debug("Initializing keys...")
keyconf.init(qApp)
log.init.debug("Initializing sql...") log.init.debug("Initializing sql...")
try: try:
sql.init(os.path.join(standarddir.data(), 'history.sqlite')) sql.init(os.path.join(standarddir.data(), 'history.sqlite'))
@ -433,6 +413,9 @@ def _init_modules(args, crash_handler):
pre_text='Error initializing SQL') pre_text='Error initializing SQL')
sys.exit(usertypes.Exit.err_init) sys.exit(usertypes.Exit.err_init)
log.init.debug("Initializing command history...")
cmdhistory.init()
log.init.debug("Initializing web history...") log.init.debug("Initializing web history...")
history.init(qApp) history.init(qApp)
@ -470,7 +453,7 @@ def _init_modules(args, crash_handler):
objreg.register('cache', diskcache) objreg.register('cache', diskcache)
log.init.debug("Misc initialization...") log.init.debug("Misc initialization...")
if config.get('ui', 'hide-wayland-decoration'): if config.val.window.hide_wayland_decoration:
os.environ['QT_WAYLAND_DISABLE_WINDOWDECORATION'] = '1' os.environ['QT_WAYLAND_DISABLE_WINDOWDECORATION'] = '1'
else: else:
os.environ.pop('QT_WAYLAND_DISABLE_WINDOWDECORATION', None) os.environ.pop('QT_WAYLAND_DISABLE_WINDOWDECORATION', None)
@ -640,8 +623,25 @@ class Quitter:
else: else:
return True return True
@cmdutils.register(instance='quitter', name=['quit', 'q'], @cmdutils.register(instance='quitter', name='quit')
ignore_args=True) @cmdutils.argument('session', completion=miscmodels.session)
def quit(self, save=False, session=None):
"""Quit qutebrowser.
Args:
save: When given, save the open windows even if auto_save.session
is turned off.
session: The name of the session to save.
"""
if session is not None and not save:
raise cmdexc.CommandError("Session name given without --save!")
if save:
if session is None:
session = sessions.default
self.shutdown(session=session)
else:
self.shutdown()
def shutdown(self, status=0, session=None, last_window=False, def shutdown(self, status=0, session=None, last_window=False,
restart=False): restart=False):
"""Quit qutebrowser. """Quit qutebrowser.
@ -663,7 +663,7 @@ class Quitter:
if session is not None: if session is not None:
session_manager.save(session, last_window=last_window, session_manager.save(session, last_window=last_window,
load_next_time=True) load_next_time=True)
elif config.get('general', 'save-session'): elif config.val.auto_save.session:
session_manager.save(sessions.default, last_window=last_window, session_manager.save(sessions.default, last_window=last_window,
load_next_time=True) load_next_time=True)
@ -742,16 +742,6 @@ class Quitter:
# segfaults. # segfaults.
QTimer.singleShot(0, functools.partial(qApp.exit, status)) QTimer.singleShot(0, functools.partial(qApp.exit, status))
@cmdutils.register(instance='quitter', name='wq')
@cmdutils.argument('name', completion=miscmodels.session)
def save_and_quit(self, name=sessions.default):
"""Save open pages and quit.
Args:
name: The name of the session.
"""
self.shutdown(session=name)
class Application(QApplication): class Application(QApplication):
@ -772,7 +762,7 @@ class Application(QApplication):
""" """
self._last_focus_object = None self._last_focus_object = None
qt_args = qtutils.get_args(args) qt_args = config.qt_args(args)
log.init.debug("Qt arguments: {}, based on {}".format(qt_args, args)) log.init.debug("Qt arguments: {}, based on {}".format(qt_args, args))
super().__init__(qt_args) super().__init__(qt_args)

View File

@ -67,11 +67,7 @@ def is_whitelisted_host(host):
Args: Args:
host: The host of the request as string. host: The host of the request as string.
""" """
whitelist = config.get('content', 'host-blocking-whitelist') for pattern in config.val.content.host_blocking.whitelist:
if whitelist is None:
return False
for pattern in whitelist:
if fnmatch.fnmatch(host, pattern.lower()): if fnmatch.fnmatch(host, pattern.lower()):
return True return True
return False return False
@ -114,16 +110,16 @@ class HostBlocker:
data_dir = standarddir.data() data_dir = standarddir.data()
self._local_hosts_file = os.path.join(data_dir, 'blocked-hosts') self._local_hosts_file = os.path.join(data_dir, 'blocked-hosts')
self.on_config_changed() self._update_files()
config_dir = standarddir.config() config_dir = standarddir.config()
self._config_hosts_file = os.path.join(config_dir, 'blocked-hosts') self._config_hosts_file = os.path.join(config_dir, 'blocked-hosts')
objreg.get('config').changed.connect(self.on_config_changed) config.instance.changed.connect(self._update_files)
def is_blocked(self, url): def is_blocked(self, url):
"""Check if the given URL (as QUrl) is blocked.""" """Check if the given URL (as QUrl) is blocked."""
if not config.get('content', 'host-blocking-enabled'): if not config.val.content.host_blocking.enabled:
return False return False
host = url.host() host = url.host()
return ((host in self._blocked_hosts or return ((host in self._blocked_hosts or
@ -164,9 +160,9 @@ class HostBlocker:
if not found: if not found:
args = objreg.get('args') args = objreg.get('args')
if (config.get('content', 'host-block-lists') is not None and if (config.val.content.host_blocking.lists and
args.basedir is None and args.basedir is None and
config.get('content', 'host-blocking-enabled')): config.val.content.host_blocking.enabled):
message.info("Run :adblock-update to get adblock lists.") message.info("Run :adblock-update to get adblock lists.")
@cmdutils.register(instance='host-blocker') @cmdutils.register(instance='host-blocker')
@ -180,18 +176,16 @@ class HostBlocker:
self._config_blocked_hosts) self._config_blocked_hosts)
self._blocked_hosts = set() self._blocked_hosts = set()
self._done_count = 0 self._done_count = 0
urls = config.get('content', 'host-block-lists')
download_manager = objreg.get('qtnetwork-download-manager', download_manager = objreg.get('qtnetwork-download-manager',
scope='window', window='last-focused') scope='window', window='last-focused')
if urls is None: for url in config.val.content.host_blocking.lists:
return
for url in urls:
if url.scheme() == 'file': if url.scheme() == 'file':
filename = url.toLocalFile()
try: try:
fileobj = open(url.path(), 'rb') fileobj = open(filename, 'rb')
except OSError as e: except OSError as e:
message.error("adblock: Error while reading {}: {}".format( message.error("adblock: Error while reading {}: {}".format(
url.path(), e.strerror)) filename, e.strerror))
continue continue
download = FakeDownload(fileobj) download = FakeDownload(fileobj)
self._in_progress.append(download) self._in_progress.append(download)
@ -292,11 +286,10 @@ class HostBlocker:
message.info("adblock: Read {} hosts from {} sources.".format( message.info("adblock: Read {} hosts from {} sources.".format(
len(self._blocked_hosts), self._done_count)) len(self._blocked_hosts), self._done_count))
@config.change_filter('content', 'host-block-lists') @config.change_filter('content.host_blocking.lists')
def on_config_changed(self): def _update_files(self):
"""Update files when the config changed.""" """Update files when the config changed."""
urls = config.get('content', 'host-block-lists') if not config.val.content.host_blocking.lists:
if urls is None:
try: try:
os.remove(self._local_hosts_file) os.remove(self._local_hosts_file)
except FileNotFoundError: except FileNotFoundError:

View File

@ -21,6 +21,7 @@
import itertools import itertools
import attr
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QUrl, QObject, QSizeF, Qt from PyQt5.QtCore import pyqtSignal, pyqtSlot, QUrl, QObject, QSizeF, Qt
from PyQt5.QtGui import QIcon from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QWidget, QApplication from PyQt5.QtWidgets import QWidget, QApplication
@ -61,9 +62,6 @@ def init():
if objects.backend == usertypes.Backend.QtWebEngine: if objects.backend == usertypes.Backend.QtWebEngine:
from qutebrowser.browser.webengine import webenginetab from qutebrowser.browser.webengine import webenginetab
webenginetab.init() webenginetab.init()
else:
from qutebrowser.browser.webkit import webkittab
webkittab.init()
class WebTabError(Exception): class WebTabError(Exception):
@ -85,6 +83,7 @@ TerminationStatus = usertypes.enum('TerminationStatus', [
]) ])
@attr.s
class TabData: class TabData:
"""A simple namespace with a fixed set of attributes. """A simple namespace with a fixed set of attributes.
@ -100,13 +99,12 @@ class TabData:
fullscreen: Whether the tab has a video shown fullscreen currently. fullscreen: Whether the tab has a video shown fullscreen currently.
""" """
def __init__(self): keep_icon = attr.ib(False)
self.keep_icon = False viewing_source = attr.ib(False)
self.viewing_source = False inspector = attr.ib(None)
self.inspector = None override_target = attr.ib(None)
self.override_target = None pinned = attr.ib(False)
self.pinned = False fullscreen = attr.ib(False)
self.fullscreen = False
class AbstractAction: class AbstractAction:
@ -188,13 +186,28 @@ class AbstractSearch(QObject):
self.text = None self.text = None
self.search_displayed = False self.search_displayed = False
def search(self, text, *, ignore_case=False, reverse=False, def _is_case_sensitive(self, ignore_case):
"""Check if case-sensitivity should be used.
This assumes self.text is already set properly.
Arguments:
ignore_case: The ignore_case value from the config.
"""
mapping = {
'smart': not self.text.islower(),
'never': True,
'always': False,
}
return mapping[ignore_case]
def search(self, text, *, ignore_case='never', reverse=False,
result_cb=None): result_cb=None):
"""Find the given text on the page. """Find the given text on the page.
Args: Args:
text: The text to search for. text: The text to search for.
ignore_case: Search case-insensitively. (True/False/'smart') ignore_case: Search case-insensitively. ('always'/'never/'smart')
reverse: Reverse search direction. reverse: Reverse search direction.
result_cb: Called with a bool indicating whether a match was found. result_cb: Called with a bool indicating whether a match was found.
""" """
@ -236,7 +249,7 @@ class AbstractZoom(QObject):
self._win_id = win_id self._win_id = win_id
self._default_zoom_changed = False self._default_zoom_changed = False
self._init_neighborlist() self._init_neighborlist()
objreg.get('config').changed.connect(self._on_config_changed) config.instance.changed.connect(self._on_config_changed)
# # FIXME:qtwebengine is this needed? # # FIXME:qtwebengine is this needed?
# # For some reason, this signal doesn't get disconnected automatically # # For some reason, this signal doesn't get disconnected automatically
@ -245,21 +258,21 @@ class AbstractZoom(QObject):
# self.destroyed.connect(functools.partial( # self.destroyed.connect(functools.partial(
# cfg.changed.disconnect, self.init_neighborlist)) # cfg.changed.disconnect, self.init_neighborlist))
@pyqtSlot(str, str) @pyqtSlot(str)
def _on_config_changed(self, section, option): def _on_config_changed(self, option):
if section == 'ui' and option in ['zoom-levels', 'default-zoom']: if option in ['zoom.levels', 'zoom.default']:
if not self._default_zoom_changed: if not self._default_zoom_changed:
factor = float(config.get('ui', 'default-zoom')) / 100 factor = float(config.val.zoom.default) / 100
self._set_factor_internal(factor) self._set_factor_internal(factor)
self._default_zoom_changed = False self._default_zoom_changed = False
self._init_neighborlist() self._init_neighborlist()
def _init_neighborlist(self): def _init_neighborlist(self):
"""Initialize self._neighborlist.""" """Initialize self._neighborlist."""
levels = config.get('ui', 'zoom-levels') levels = config.val.zoom.levels
self._neighborlist = usertypes.NeighborList( self._neighborlist = usertypes.NeighborList(
levels, mode=usertypes.NeighborList.Modes.edge) levels, mode=usertypes.NeighborList.Modes.edge)
self._neighborlist.fuzzyval = config.get('ui', 'default-zoom') self._neighborlist.fuzzyval = config.val.zoom.default
def offset(self, offset): def offset(self, offset):
"""Increase/Decrease the zoom level by the given offset. """Increase/Decrease the zoom level by the given offset.
@ -295,8 +308,7 @@ class AbstractZoom(QObject):
raise NotImplementedError raise NotImplementedError
def set_default(self): def set_default(self):
default_zoom = config.get('ui', 'default-zoom') self._set_factor_internal(float(config.val.zoom.default) / 100)
self._set_factor_internal(float(default_zoom) / 100)
class AbstractCaret(QObject): class AbstractCaret(QObject):
@ -705,10 +717,8 @@ class AbstractTab(QWidget):
self.load_started.emit() self.load_started.emit()
def _handle_auto_insert_mode(self, ok): def _handle_auto_insert_mode(self, ok):
"""Handle auto-insert-mode after loading finished.""" """Handle `input.insert_mode.auto_load` after loading finished."""
# Checks if the tab is in foreground first, then eventually sets the mode if not config.val.input.insert_mode.auto_load or not ok:
foreground = self is objreg.get('tabbed-browser', scope='window', window=self.win_id).currentWidget()
if not config.get('input', 'auto-insert-mode') or not foreground or not ok:
return return
cur_mode = self._mode_manager.mode cur_mode = self._mode_manager.mode

View File

@ -24,6 +24,7 @@ import sys
import os.path import os.path
import shlex import shlex
import functools import functools
import typing
from PyQt5.QtWidgets import QApplication, QTabBar, QDialog from PyQt5.QtWidgets import QApplication, QTabBar, QDialog
from PyQt5.QtCore import Qt, QUrl, QEvent, QUrlQuery from PyQt5.QtCore import Qt, QUrl, QEvent, QUrlQuery
@ -34,15 +35,16 @@ import pygments.lexers
import pygments.formatters import pygments.formatters
from qutebrowser.commands import userscripts, cmdexc, cmdutils, runners from qutebrowser.commands import userscripts, cmdexc, cmdutils, runners
from qutebrowser.config import config, configexc from qutebrowser.config import config, configdata
from qutebrowser.browser import (urlmarks, browsertab, inspector, navigate, from qutebrowser.browser import (urlmarks, browsertab, inspector, navigate,
webelem, downloads) webelem, downloads)
from qutebrowser.keyinput import modeman from qutebrowser.keyinput import modeman
from qutebrowser.utils import (message, usertypes, log, qtutils, urlutils, from qutebrowser.utils import (message, usertypes, log, qtutils, urlutils,
objreg, utils, typing, debug) objreg, utils, debug)
from qutebrowser.utils.usertypes import KeyMode from qutebrowser.utils.usertypes import KeyMode
from qutebrowser.misc import editor, guiprocess from qutebrowser.misc import editor, guiprocess
from qutebrowser.completion.models import urlmodel, miscmodels from qutebrowser.completion.models import urlmodel, miscmodels
from qutebrowser.mainwindow import mainwindow
class CommandDispatcher: class CommandDispatcher:
@ -70,7 +72,6 @@ class CommandDispatcher:
def _new_tabbed_browser(self, private): def _new_tabbed_browser(self, private):
"""Get a tabbed-browser from a new window.""" """Get a tabbed-browser from a new window."""
from qutebrowser.mainwindow import mainwindow
new_window = mainwindow.MainWindow(private=private) new_window = mainwindow.MainWindow(private=private)
new_window.show() new_window.show()
return new_window.tabbed_browser return new_window.tabbed_browser
@ -111,7 +112,7 @@ class CommandDispatcher:
return widget return widget
def _open(self, url, tab=False, background=False, window=False, def _open(self, url, tab=False, background=False, window=False,
explicit=True, private=None): related=False, private=None):
"""Helper function to open a page. """Helper function to open a page.
Args: Args:
@ -132,9 +133,9 @@ class CommandDispatcher:
tabbed_browser = self._new_tabbed_browser(private) tabbed_browser = self._new_tabbed_browser(private)
tabbed_browser.tabopen(url) tabbed_browser.tabopen(url)
elif tab: elif tab:
tabbed_browser.tabopen(url, background=False, explicit=explicit) tabbed_browser.tabopen(url, background=False, related=related)
elif background: elif background:
tabbed_browser.tabopen(url, background=True, explicit=explicit) tabbed_browser.tabopen(url, background=True, related=related)
else: else:
widget = self._current_widget() widget = self._current_widget()
widget.openurl(url) widget.openurl(url)
@ -179,7 +180,7 @@ class CommandDispatcher:
prev: Force selecting the tab before the current tab. prev: Force selecting the tab before the current tab.
next_: Force selecting the tab after the current tab. next_: Force selecting the tab after the current tab.
opposite: Force selecting the tab in the opposite direction of opposite: Force selecting the tab in the opposite direction of
what's configured in 'tabs->select-on-remove'. what's configured in 'tabs.select_on_remove'.
Return: Return:
QTabBar.SelectLeftTab, QTabBar.SelectRightTab, or None if no change QTabBar.SelectLeftTab, QTabBar.SelectRightTab, or None if no change
@ -191,17 +192,17 @@ class CommandDispatcher:
elif next_: elif next_:
return QTabBar.SelectRightTab return QTabBar.SelectRightTab
elif opposite: elif opposite:
conf_selection = config.get('tabs', 'select-on-remove') conf_selection = config.val.tabs.select_on_remove
if conf_selection == QTabBar.SelectLeftTab: if conf_selection == QTabBar.SelectLeftTab:
return QTabBar.SelectRightTab return QTabBar.SelectRightTab
elif conf_selection == QTabBar.SelectRightTab: elif conf_selection == QTabBar.SelectRightTab:
return QTabBar.SelectLeftTab return QTabBar.SelectLeftTab
elif conf_selection == QTabBar.SelectPreviousTab: elif conf_selection == QTabBar.SelectPreviousTab:
raise cmdexc.CommandError( raise cmdexc.CommandError(
"-o is not supported with 'tabs->select-on-remove' set to " "-o is not supported with 'tabs.select_on_remove' set to "
"'last-used'!") "'last-used'!")
else: # pragma: no cover else: # pragma: no cover
raise ValueError("Invalid select-on-remove value " raise ValueError("Invalid select_on_remove value "
"{!r}!".format(conf_selection)) "{!r}!".format(conf_selection))
return None return None
@ -213,7 +214,7 @@ class CommandDispatcher:
prev: Force selecting the tab before the current tab. prev: Force selecting the tab before the current tab.
next_: Force selecting the tab after the current tab. next_: Force selecting the tab after the current tab.
opposite: Force selecting the tab in the opposite direction of opposite: Force selecting the tab in the opposite direction of
what's configured in 'tabs->select-on-remove'. what's configured in 'tabs.select_on_remove'.
count: The tab index to close, or None count: The tab index to close, or None
""" """
tabbar = self._tabbed_browser.tabBar() tabbar = self._tabbed_browser.tabBar()
@ -238,7 +239,7 @@ class CommandDispatcher:
prev: Force selecting the tab before the current tab. prev: Force selecting the tab before the current tab.
next_: Force selecting the tab after the current tab. next_: Force selecting the tab after the current tab.
opposite: Force selecting the tab in the opposite direction of opposite: Force selecting the tab in the opposite direction of
what's configured in 'tabs->select-on-remove'. what's configured in 'tabs.select_on_remove'.
force: Avoid confirmation for pinned tabs. force: Avoid confirmation for pinned tabs.
count: The tab index to close, or None count: The tab index to close, or None
""" """
@ -256,7 +257,7 @@ class CommandDispatcher:
def tab_pin(self, count=None): def tab_pin(self, count=None):
"""Pin/Unpin the current/[count]th tab. """Pin/Unpin the current/[count]th tab.
Pinning a tab shrinks it to tabs->pinned-width size. Pinning a tab shrinks it to `tabs.width.pinned` size.
Attempting to close a pinned tab will cause a confirmation, Attempting to close a pinned tab will cause a confirmation,
unless --force is passed. unless --force is passed.
@ -274,7 +275,7 @@ class CommandDispatcher:
maxsplit=0, scope='window') maxsplit=0, scope='window')
@cmdutils.argument('url', completion=urlmodel.url) @cmdutils.argument('url', completion=urlmodel.url)
@cmdutils.argument('count', count=True) @cmdutils.argument('count', count=True)
def openurl(self, url=None, implicit=False, def openurl(self, url=None, related=False,
bg=False, tab=False, window=False, count=None, secure=False, bg=False, tab=False, window=False, count=None, secure=False,
private=False): private=False):
"""Open a URL in the current/[count]th tab. """Open a URL in the current/[count]th tab.
@ -286,14 +287,14 @@ class CommandDispatcher:
bg: Open in a new background tab. bg: Open in a new background tab.
tab: Open in a new tab. tab: Open in a new tab.
window: Open in a new window. window: Open in a new window.
implicit: If opening a new tab, treat the tab as implicit (like related: If opening a new tab, position the tab as related to the
clicking on a link). current one (like clicking on a link).
count: The tab index to open the URL in, or None. count: The tab index to open the URL in, or None.
secure: Force HTTPS. secure: Force HTTPS.
private: Open a new window in private browsing mode. private: Open a new window in private browsing mode.
""" """
if url is None: if url is None:
urls = [config.get('general', 'default-page')] urls = [config.val.url.default_page]
else: else:
urls = self._parse_url_input(url) urls = self._parse_url_input(url)
@ -305,7 +306,7 @@ class CommandDispatcher:
bg = True bg = True
if tab or bg or window or private: if tab or bg or window or private:
self._open(cur_url, tab, bg, window, explicit=not implicit, self._open(cur_url, tab, bg, window, related=related,
private=private) private=private)
else: else:
curtab = self._cntwidget(count) curtab = self._cntwidget(count)
@ -490,7 +491,7 @@ class CommandDispatcher:
raise cmdexc.CommandError(e) raise cmdexc.CommandError(e)
# The new tab could be in a new tabbed_browser (e.g. because of # The new tab could be in a new tabbed_browser (e.g. because of
# tabs-are-windows being set) # tabs.tabs_are_windows being set)
if window: if window:
new_tabbed_browser = self._new_tabbed_browser( new_tabbed_browser = self._new_tabbed_browser(
private=self._tabbed_browser.private) private=self._tabbed_browser.private)
@ -502,9 +503,9 @@ class CommandDispatcher:
idx = new_tabbed_browser.indexOf(newtab) idx = new_tabbed_browser.indexOf(newtab)
new_tabbed_browser.set_page_title(idx, cur_title) new_tabbed_browser.set_page_title(idx, cur_title)
if config.get('tabs', 'show-favicons'): if config.val.tabs.favicons.show:
new_tabbed_browser.setTabIcon(idx, curtab.icon()) new_tabbed_browser.setTabIcon(idx, curtab.icon())
if config.get('tabs', 'tabs-are-windows'): if config.val.tabs.tabs_are_windows:
new_tabbed_browser.window().setWindowIcon(curtab.icon()) new_tabbed_browser.window().setWindowIcon(curtab.icon())
newtab.data.keep_icon = True newtab.data.keep_icon = True
@ -622,7 +623,7 @@ class CommandDispatcher:
tab=tab, background=bg, window=window) tab=tab, background=bg, window=window)
elif where in ['up', 'increment', 'decrement']: elif where in ['up', 'increment', 'decrement']:
new_url = handlers[where](url, count) new_url = handlers[where](url, count)
self._open(new_url, tab, bg, window, explicit=False) self._open(new_url, tab, bg, window, related=True)
else: # pragma: no cover else: # pragma: no cover
raise ValueError("Got called with invalid value {} for " raise ValueError("Got called with invalid value {} for "
"`where'.".format(where)) "`where'.".format(where))
@ -771,7 +772,7 @@ class CommandDispatcher:
url_query.setQueryDelimiters('=', ';') url_query.setQueryDelimiters('=', ';')
url_query.setQuery(url_query_str) url_query.setQuery(url_query_str)
for key in dict(url_query.queryItems()): for key in dict(url_query.queryItems()):
if key in config.get('general', 'yank-ignored-url-parameters'): if key in config.val.url.yank_ignored_parameters:
url_query.removeQueryItem(key) url_query.removeQueryItem(key)
url.setQuery(url_query) url.setQuery(url_query)
return url.toString(flags) return url.toString(flags)
@ -842,7 +843,7 @@ class CommandDispatcher:
perc = tab.zoom.offset(count) perc = tab.zoom.offset(count)
except ValueError as e: except ValueError as e:
raise cmdexc.CommandError(e) raise cmdexc.CommandError(e)
message.info("Zoom level: {}%".format(perc), replace=True) message.info("Zoom level: {}%".format(int(perc)), replace=True)
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('count', count=True) @cmdutils.argument('count', count=True)
@ -857,7 +858,7 @@ class CommandDispatcher:
perc = tab.zoom.offset(-count) perc = tab.zoom.offset(-count)
except ValueError as e: except ValueError as e:
raise cmdexc.CommandError(e) raise cmdexc.CommandError(e)
message.info("Zoom level: {}%".format(perc), replace=True) message.info("Zoom level: {}%".format(int(perc)), replace=True)
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('count', count=True) @cmdutils.argument('count', count=True)
@ -881,14 +882,14 @@ class CommandDispatcher:
level = count if count is not None else zoom level = count if count is not None else zoom
if level is None: if level is None:
level = config.get('ui', 'default-zoom') level = config.val.zoom.default
tab = self._current_widget() tab = self._current_widget()
try: try:
tab.zoom.set_factor(float(level) / 100) tab.zoom.set_factor(float(level) / 100)
except ValueError: except ValueError:
raise cmdexc.CommandError("Can't zoom {}%!".format(level)) raise cmdexc.CommandError("Can't zoom {}%!".format(level))
message.info("Zoom level: {}%".format(level), replace=True) message.info("Zoom level: {}%".format(int(level)), replace=True)
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window')
def tab_only(self, prev=False, next_=False, force=False): def tab_only(self, prev=False, next_=False, force=False):
@ -947,7 +948,7 @@ class CommandDispatcher:
newidx = self._current_index() - count newidx = self._current_index() - count
if newidx >= 0: if newidx >= 0:
self._set_current_index(newidx) self._set_current_index(newidx)
elif config.get('tabs', 'wrap'): elif config.val.tabs.wrap:
self._set_current_index(newidx % self._count()) self._set_current_index(newidx % self._count())
else: else:
raise cmdexc.CommandError("First tab") raise cmdexc.CommandError("First tab")
@ -967,7 +968,7 @@ class CommandDispatcher:
newidx = self._current_index() + count newidx = self._current_index() + count
if newidx < self._count(): if newidx < self._count():
self._set_current_index(newidx) self._set_current_index(newidx)
elif config.get('tabs', 'wrap'): elif config.val.tabs.wrap:
self._set_current_index(newidx % self._count()) self._set_current_index(newidx % self._count())
else: else:
raise cmdexc.CommandError("Last tab") raise cmdexc.CommandError("Last tab")
@ -1124,7 +1125,7 @@ class CommandDispatcher:
elif index == '+': # pragma: no branch elif index == '+': # pragma: no branch
new_idx += delta new_idx += delta
if config.get('tabs', 'wrap'): if config.val.tabs.wrap:
new_idx %= self._count() new_idx %= self._count()
else: else:
# absolute moving # absolute moving
@ -1186,7 +1187,7 @@ class CommandDispatcher:
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window')
def home(self): def home(self):
"""Open main startpage in current tab.""" """Open main startpage in current tab."""
self.openurl(config.get('general', 'startpage')[0]) self._current_widget().openurl(config.val.url.start_pages[0])
def _run_userscript(self, cmd, *args, verbose=False): def _run_userscript(self, cmd, *args, verbose=False):
"""Run a userscript given as argument. """Run a userscript given as argument.
@ -1532,7 +1533,7 @@ class CommandDispatcher:
topic: The topic to show help for. topic: The topic to show help for.
- :__command__ for commands. - :__command__ for commands.
- __section__\->__option__ for settings. - __section__.__option__ for settings.
""" """
if topic is None: if topic is None:
path = 'index.html' path = 'index.html'
@ -1542,20 +1543,8 @@ class CommandDispatcher:
raise cmdexc.CommandError("Invalid command {}!".format( raise cmdexc.CommandError("Invalid command {}!".format(
command)) command))
path = 'commands.html#{}'.format(command) path = 'commands.html#{}'.format(command)
elif '->' in topic: elif topic in configdata.DATA:
parts = topic.split('->') path = 'settings.html#{}'.format(topic)
if len(parts) != 2:
raise cmdexc.CommandError("Invalid help topic {}!".format(
topic))
try:
config.get(*parts)
except configexc.NoSectionError:
raise cmdexc.CommandError("Invalid section {}!".format(
parts[0]))
except configexc.NoOptionError:
raise cmdexc.CommandError("Invalid option {}!".format(
parts[1]))
path = 'settings.html#{}'.format(topic.replace('->', '-'))
else: else:
raise cmdexc.CommandError("Invalid help topic {}!".format(topic)) raise cmdexc.CommandError("Invalid help topic {}!".format(topic))
url = QUrl('qute://help/{}'.format(path)) url = QUrl('qute://help/{}'.format(path))
@ -1608,7 +1597,7 @@ class CommandDispatcher:
"""Open an external editor with the currently selected form field. """Open an external editor with the currently selected form field.
The editor which should be launched can be configured via the The editor which should be launched can be configured via the
`general -> editor` config option. `editor.command` config option.
""" """
tab = self._current_widget() tab = self._current_widget()
tab.elements.find_focused(self._open_editor_cb) tab.elements.find_focused(self._open_editor_cb)
@ -1749,7 +1738,7 @@ class CommandDispatcher:
return return
options = { options = {
'ignore_case': config.get('general', 'ignore-case'), 'ignore_case': config.val.ignore_case,
'reverse': reverse, 'reverse': reverse,
} }
@ -2048,8 +2037,9 @@ class CommandDispatcher:
message.info(out) message.info(out)
if file: if file:
path = os.path.expanduser(js_code)
try: try:
with open(js_code, 'r', encoding='utf-8') as f: with open(path, 'r', encoding='utf-8') as f:
js_code = f.read() js_code = f.read()
except OSError as e: except OSError as e:
raise cmdexc.CommandError(str(e)) raise cmdexc.CommandError(str(e))
@ -2102,7 +2092,7 @@ class CommandDispatcher:
"""Navigate to a url formed in an external editor. """Navigate to a url formed in an external editor.
The editor which should be launched can be configured via the The editor which should be launched can be configured via the
`general -> editor` config option. `editor.command` config option.
Args: Args:
url: URL to edit; defaults to the current page url. url: URL to edit; defaults to the current page url.

View File

@ -69,15 +69,22 @@ class UnsupportedOperationError(Exception):
def download_dir(): def download_dir():
"""Get the download directory to use.""" """Get the download directory to use."""
directory = config.get('storage', 'download-directory') directory = config.val.downloads.location.directory
remember_dir = config.get('storage', 'remember-download-directory') remember_dir = config.val.downloads.location.remember
if remember_dir and last_used_directory is not None: if remember_dir and last_used_directory is not None:
return last_used_directory ddir = last_used_directory
elif directory is None: elif directory is None:
return standarddir.download() ddir = standarddir.download()
else: else:
return directory ddir = directory
try:
os.makedirs(ddir)
except FileExistsError:
pass
return ddir
def immediate_download_path(prompt_download_directory=None): def immediate_download_path(prompt_download_directory=None):
@ -88,11 +95,10 @@ def immediate_download_path(prompt_download_directory=None):
Args: Args:
prompt_download_directory: If this is something else than None, it prompt_download_directory: If this is something else than None, it
will overwrite the will overwrite the
storage->prompt-download-directory setting. downloads.location.prompt setting.
""" """
if prompt_download_directory is None: if prompt_download_directory is None:
prompt_download_directory = config.get('storage', prompt_download_directory = config.val.downloads.location.prompt
'prompt-download-directory')
if not prompt_download_directory: if not prompt_download_directory:
return download_dir() return download_dir()
@ -104,7 +110,7 @@ def _path_suggestion(filename):
Args: Args:
filename: The filename to use if included in the suggestion. filename: The filename to use if included in the suggestion.
""" """
suggestion = config.get('completion', 'download-path-suggestion') suggestion = config.val.downloads.location.suggestion
if suggestion == 'path': if suggestion == 'path':
# add trailing '/' if not present # add trailing '/' if not present
return os.path.join(download_dir(), '') return os.path.join(download_dir(), '')
@ -494,13 +500,13 @@ class AbstractDownloadItem(QObject):
Args: Args:
position: The color type requested, can be 'fg' or 'bg'. position: The color type requested, can be 'fg' or 'bg'.
""" """
# pylint: disable=bad-config-call
# WORKAROUND for https://bitbucket.org/logilab/astroid/issue/104/
assert position in ["fg", "bg"] assert position in ["fg", "bg"]
start = config.get('colors', 'downloads.{}.start'.format(position)) # pylint: disable=bad-config-option
stop = config.get('colors', 'downloads.{}.stop'.format(position)) start = getattr(config.val.colors.downloads.start, position)
system = config.get('colors', 'downloads.{}.system'.format(position)) stop = getattr(config.val.colors.downloads.stop, position)
error = config.get('colors', 'downloads.{}.error'.format(position)) system = getattr(config.val.colors.downloads.system, position)
error = getattr(config.val.colors.downloads.error, position)
# pylint: enable=bad-config-option
if self.error_msg is not None: if self.error_msg is not None:
assert not self.successful assert not self.successful
return error return error
@ -572,7 +578,7 @@ class AbstractDownloadItem(QObject):
Args: Args:
cmdline: The command to use as string. A `{}` is expanded to the cmdline: The command to use as string. A `{}` is expanded to the
filename. None means to use the system's default filename. None means to use the system's default
application or `default-open-dispatcher` if set. If no application or `downloads.open_dispatcher` if set. If no
`{}` is found, the filename is appended to the cmdline. `{}` is found, the filename is appended to the cmdline.
""" """
assert self.successful assert self.successful
@ -757,7 +763,7 @@ class AbstractDownloadManager(QObject):
download.remove_requested.connect(functools.partial( download.remove_requested.connect(functools.partial(
self._remove_item, download)) self._remove_item, download))
delay = config.get('ui', 'remove-finished-downloads') delay = config.val.downloads.remove_finished
if delay > -1: if delay > -1:
download.finished.connect( download.finished.connect(
lambda: QTimer.singleShot(delay, download.remove)) lambda: QTimer.singleShot(delay, download.remove))

View File

@ -26,7 +26,7 @@ from PyQt5.QtCore import pyqtSlot, QSize, Qt, QTimer
from PyQt5.QtWidgets import QListView, QSizePolicy, QMenu, QStyleFactory from PyQt5.QtWidgets import QListView, QSizePolicy, QMenu, QStyleFactory
from qutebrowser.browser import downloads from qutebrowser.browser import downloads
from qutebrowser.config import style from qutebrowser.config import config
from qutebrowser.utils import qtutils, utils, objreg from qutebrowser.utils import qtutils, utils, objreg
@ -64,8 +64,8 @@ class DownloadView(QListView):
STYLESHEET = """ STYLESHEET = """
QListView { QListView {
background-color: {{ color['downloads.bg.bar'] }}; background-color: {{ conf.colors.downloads.bar.bg }};
font: {{ font['downloads'] }}; font: {{ conf.fonts.downloads }};
} }
QListView::item { QListView::item {
@ -76,7 +76,7 @@ class DownloadView(QListView):
def __init__(self, win_id, parent=None): def __init__(self, win_id, parent=None):
super().__init__(parent) super().__init__(parent)
self.setStyle(QStyleFactory.create('Fusion')) self.setStyle(QStyleFactory.create('Fusion'))
style.set_register_stylesheet(self) config.set_register_stylesheet(self)
self.setResizeMode(QListView.Adjust) self.setResizeMode(QListView.Adjust)
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed) self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed)

View File

@ -26,10 +26,11 @@ import re
import html import html
from string import ascii_lowercase from string import ascii_lowercase
import attr
from PyQt5.QtCore import pyqtSlot, QObject, Qt, QUrl from PyQt5.QtCore import pyqtSlot, QObject, Qt, QUrl
from PyQt5.QtWidgets import QLabel from PyQt5.QtWidgets import QLabel
from qutebrowser.config import config, style from qutebrowser.config import config
from qutebrowser.keyinput import modeman, modeparsers from qutebrowser.keyinput import modeman, modeparsers
from qutebrowser.browser import webelem from qutebrowser.browser import webelem
from qutebrowser.commands import userscripts, cmdexc, cmdutils, runners from qutebrowser.commands import userscripts, cmdexc, cmdutils, runners
@ -65,10 +66,10 @@ class HintLabel(QLabel):
STYLESHEET = """ STYLESHEET = """
QLabel { QLabel {
background-color: {{ color['hints.bg'] }}; background-color: {{ conf.colors.hints.bg }};
color: {{ color['hints.fg'] }}; color: {{ conf.colors.hints.fg }};
font: {{ font['hints'] }}; font: {{ conf.fonts.hints }};
border: {{ config.get('hints', 'border') }}; border: {{ conf.hints.border }};
padding-left: -3px; padding-left: -3px;
padding-right: -3px; padding-right: -3px;
} }
@ -80,7 +81,7 @@ class HintLabel(QLabel):
self.elem = elem self.elem = elem
self.setAttribute(Qt.WA_StyledBackground, True) self.setAttribute(Qt.WA_StyledBackground, True)
style.set_register_stylesheet(self) config.set_register_stylesheet(self)
self._context.tab.contents_size_changed.connect(self._move_to_elem) self._context.tab.contents_size_changed.connect(self._move_to_elem)
self._move_to_elem() self._move_to_elem()
@ -100,7 +101,7 @@ class HintLabel(QLabel):
matched: The part of the text which was typed. matched: The part of the text which was typed.
unmatched: The part of the text which was not typed yet. unmatched: The part of the text which was not typed yet.
""" """
if (config.get('hints', 'uppercase') and if (config.val.hints.uppercase and
self._context.hint_mode in ['letter', 'word']): self._context.hint_mode in ['letter', 'word']):
matched = html.escape(matched.upper()) matched = html.escape(matched.upper())
unmatched = html.escape(unmatched.upper()) unmatched = html.escape(unmatched.upper())
@ -108,7 +109,7 @@ class HintLabel(QLabel):
matched = html.escape(matched) matched = html.escape(matched)
unmatched = html.escape(unmatched) unmatched = html.escape(unmatched)
match_color = html.escape(config.get('colors', 'hints.fg.match')) match_color = html.escape(config.val.colors.hints.match.fg)
self.setText('<font color="{}">{}</font>{}'.format( self.setText('<font color="{}">{}</font>{}'.format(
match_color, matched, unmatched)) match_color, matched, unmatched))
self.adjustSize() self.adjustSize()
@ -121,7 +122,7 @@ class HintLabel(QLabel):
log.hints.debug("Frame for {!r} vanished!".format(self)) log.hints.debug("Frame for {!r} vanished!".format(self))
self.hide() self.hide()
return return
no_js = config.get('hints', 'find-implementation') != 'javascript' no_js = config.val.hints.find_implementation != 'javascript'
rect = self.elem.rect_on_view(no_js=no_js) rect = self.elem.rect_on_view(no_js=no_js)
self.move(rect.x(), rect.y()) self.move(rect.x(), rect.y())
@ -131,6 +132,7 @@ class HintLabel(QLabel):
self.deleteLater() self.deleteLater()
@attr.s
class HintContext: class HintContext:
"""Context namespace used for hinting. """Context namespace used for hinting.
@ -158,19 +160,18 @@ class HintContext:
group: The group of web elements to hint. group: The group of web elements to hint.
""" """
def __init__(self): all_labels = attr.ib(attr.Factory(list))
self.all_labels = [] labels = attr.ib(attr.Factory(dict))
self.labels = {} target = attr.ib(None)
self.target = None baseurl = attr.ib(None)
self.baseurl = None to_follow = attr.ib(None)
self.to_follow = None rapid = attr.ib(False)
self.rapid = False add_history = attr.ib(False)
self.add_history = False filterstr = attr.ib(None)
self.filterstr = None args = attr.ib(attr.Factory(list))
self.args = [] tab = attr.ib(None)
self.tab = None group = attr.ib(None)
self.group = None hint_mode = attr.ib(None)
self.hint_mode = None
def get_args(self, urlstr): def get_args(self, urlstr):
"""Get the arguments, with {hint-url} replaced by the given URL.""" """Get the arguments, with {hint-url} replaced by the given URL."""
@ -203,7 +204,7 @@ class HintActions:
Target.window: usertypes.ClickTarget.window, Target.window: usertypes.ClickTarget.window,
Target.hover: usertypes.ClickTarget.normal, Target.hover: usertypes.ClickTarget.normal,
} }
if config.get('tabs', 'background-tabs'): if config.val.tabs.background:
target_mapping[Target.tab] = usertypes.ClickTarget.tab_bg target_mapping[Target.tab] = usertypes.ClickTarget.tab_bg
else: else:
target_mapping[Target.tab] = usertypes.ClickTarget.tab target_mapping[Target.tab] = usertypes.ClickTarget.tab
@ -389,6 +390,7 @@ class HintManager(QObject):
def _cleanup(self): def _cleanup(self):
"""Clean up after hinting.""" """Clean up after hinting."""
# pylint: disable=not-an-iterable
for label in self._context.all_labels: for label in self._context.all_labels:
label.cleanup() label.cleanup()
@ -421,9 +423,9 @@ class HintManager(QObject):
if hint_mode == 'number': if hint_mode == 'number':
chars = '0123456789' chars = '0123456789'
else: else:
chars = config.get('hints', 'chars') chars = config.val.hints.chars
min_chars = config.get('hints', 'min-chars') min_chars = config.val.hints.min_chars
if config.get('hints', 'scatter') and hint_mode != 'number': if config.val.hints.scatter and hint_mode != 'number':
return self._hint_scattered(min_chars, chars, elems) return self._hint_scattered(min_chars, chars, elems)
else: else:
return self._hint_linear(min_chars, chars, elems) return self._hint_linear(min_chars, chars, elems)
@ -603,7 +605,7 @@ class HintManager(QObject):
modeman.enter(self._win_id, usertypes.KeyMode.hint, modeman.enter(self._win_id, usertypes.KeyMode.hint,
'HintManager.start') 'HintManager.start')
# to make auto-follow == 'always' work # to make auto_follow == 'always' work
self._handle_auto_follow() self._handle_auto_follow()
@cmdutils.register(instance='hintmanager', scope='tab', name='hint', @cmdutils.register(instance='hintmanager', scope='tab', name='hint',
@ -615,7 +617,7 @@ class HintManager(QObject):
Args: Args:
rapid: Whether to do rapid hinting. This is only possible with rapid: Whether to do rapid hinting. This is only possible with
targets `tab` (with background-tabs=true), `tab-bg`, targets `tab` (with `tabs.background_tabs=true`), `tab-bg`,
`window`, `run`, `hover`, `userscript` and `spawn`. `window`, `run`, `hover`, `userscript` and `spawn`.
add_history: Whether to add the spawned or yanked link to the add_history: Whether to add the spawned or yanked link to the
browsing history. browsing history.
@ -631,7 +633,7 @@ class HintManager(QObject):
- `normal`: Open the link. - `normal`: Open the link.
- `current`: Open the link in the current tab. - `current`: Open the link in the current tab.
- `tab`: Open the link in a new tab (honoring the - `tab`: Open the link in a new tab (honoring the
background-tabs setting). `tabs.background_tabs` setting).
- `tab-fg`: Open the link in a new foreground tab. - `tab-fg`: Open the link in a new foreground tab.
- `tab-bg`: Open the link in a new background tab. - `tab-bg`: Open the link in a new background tab.
- `window`: Open the link in a new window. - `window`: Open the link in a new window.
@ -649,7 +651,7 @@ class HintManager(QObject):
mode: The hinting mode to use. mode: The hinting mode to use.
- `number`: Use numeric hints. - `number`: Use numeric hints.
- `letter`: Use the chars in the hints->chars settings. - `letter`: Use the chars in the hints.chars setting.
- `word`: Use hint words based on the html elements and the - `word`: Use hint words based on the html elements and the
extra words. extra words.
@ -684,8 +686,7 @@ class HintManager(QObject):
Target.hover, Target.userscript, Target.spawn, Target.hover, Target.userscript, Target.spawn,
Target.download, Target.normal, Target.current]: Target.download, Target.normal, Target.current]:
pass pass
elif (target == Target.tab and elif target == Target.tab and config.val.tabs.background:
config.get('tabs', 'background-tabs')):
pass pass
else: else:
name = target.name.replace('_', '-') name = target.name.replace('_', '-')
@ -693,7 +694,7 @@ class HintManager(QObject):
"target {}!".format(name)) "target {}!".format(name))
if mode is None: if mode is None:
mode = config.get('hints', 'mode') mode = config.val.hints.mode
self._check_args(target, *args) self._check_args(target, *args)
self._context = HintContext() self._context = HintContext()
@ -720,7 +721,7 @@ class HintManager(QObject):
return self._context.hint_mode return self._context.hint_mode
def _handle_auto_follow(self, keystr="", filterstr="", visible=None): def _handle_auto_follow(self, keystr="", filterstr="", visible=None):
"""Handle the auto-follow option.""" """Handle the auto_follow option."""
if visible is None: if visible is None:
visible = {string: label visible = {string: label
for string, label in self._context.labels.items() for string, label in self._context.labels.items()
@ -729,7 +730,7 @@ class HintManager(QObject):
if len(visible) != 1: if len(visible) != 1:
return return
auto_follow = config.get('hints', 'auto-follow') auto_follow = config.val.hints.auto_follow
if auto_follow == "always": if auto_follow == "always":
follow = True follow = True
@ -746,8 +747,8 @@ class HintManager(QObject):
self._context.to_follow = list(visible.keys())[0] self._context.to_follow = list(visible.keys())[0]
if follow: if follow:
# apply auto-follow-timeout # apply auto_follow_timeout
timeout = config.get('hints', 'auto-follow-timeout') timeout = config.val.hints.auto_follow_timeout
keyparsers = objreg.get('keyparsers', scope='window', keyparsers = objreg.get('keyparsers', scope='window',
window=self._win_id) window=self._win_id)
normal_parser = keyparsers[usertypes.KeyMode.normal] normal_parser = keyparsers[usertypes.KeyMode.normal]
@ -771,9 +772,9 @@ class HintManager(QObject):
label.show() label.show()
else: else:
# element doesn't match anymore -> hide it, unless in rapid # element doesn't match anymore -> hide it, unless in rapid
# mode and hide-unmatched-rapid-hints is false (see #1799) # mode and hide_unmatched_rapid_hints is false (see #1799)
if (not self._context.rapid or if (not self._context.rapid or
config.get('hints', 'hide-unmatched-rapid-hints')): config.val.hints.hide_unmatched_rapid_hints):
label.hide() label.hide()
except webelem.Error: except webelem.Error:
pass pass
@ -793,7 +794,10 @@ class HintManager(QObject):
else: else:
self._context.filterstr = filterstr self._context.filterstr = filterstr
log.hints.debug("Filtering hints on {!r}".format(filterstr))
visible = [] visible = []
# pylint: disable=not-an-iterable
for label in self._context.all_labels: for label in self._context.all_labels:
try: try:
if self._filter_matches(filterstr, str(label.elem)): if self._filter_matches(filterstr, str(label.elem)):
@ -938,7 +942,7 @@ class WordHinter:
def ensure_initialized(self): def ensure_initialized(self):
"""Generate the used words if yet uninitialized.""" """Generate the used words if yet uninitialized."""
dictionary = config.get("hints", "dictionary") dictionary = config.val.hints.dictionary
if not self.words or self.dictionary != dictionary: if not self.words or self.dictionary != dictionary:
self.words.clear() self.words.clear()
self.dictionary = dictionary self.dictionary = dictionary

View File

@ -30,6 +30,10 @@ from qutebrowser.utils import (utils, objreg, log, usertypes, message,
from qutebrowser.misc import objects, sql from qutebrowser.misc import objects, sql
# increment to indicate that HistoryCompletion must be regenerated
_USER_VERSION = 1
class CompletionHistory(sql.SqlTable): class CompletionHistory(sql.SqlTable):
"""History which only has the newest entry for each URL.""" """History which only has the newest entry for each URL."""
@ -48,6 +52,11 @@ class WebHistory(sql.SqlTable):
super().__init__("History", ['url', 'title', 'atime', 'redirect'], super().__init__("History", ['url', 'title', 'atime', 'redirect'],
parent=parent) parent=parent)
self.completion = CompletionHistory(parent=self) self.completion = CompletionHistory(parent=self)
if sql.Query('pragma user_version').run().value() < _USER_VERSION:
self.completion.delete_all()
if not self.completion:
# either the table is out-of-date or the user wiped it manually
self._rebuild_completion()
self.create_index('HistoryIndex', 'url') self.create_index('HistoryIndex', 'url')
self.create_index('HistoryAtimeIndex', 'atime') self.create_index('HistoryAtimeIndex', 'atime')
self._contains_query = self.contains_query('url') self._contains_query = self.contains_query('url')
@ -71,6 +80,18 @@ class WebHistory(sql.SqlTable):
def __contains__(self, url): def __contains__(self, url):
return self._contains_query.run(val=url).value() return self._contains_query.run(val=url).value()
def _rebuild_completion(self):
data = {'url': [], 'title': [], 'last_atime': []}
# select the latest entry for each url
q = sql.Query('SELECT url, title, max(atime) AS atime FROM History '
'WHERE NOT redirect GROUP BY url ORDER BY atime asc')
for entry in q.run():
data['url'].append(self._format_completion_url(QUrl(entry.url)))
data['title'].append(entry.title)
data['last_atime'].append(entry.atime)
self.completion.insert_batch(data, replace=True)
sql.Query('pragma user_version = {}'.format(_USER_VERSION)).run()
def get_recent(self): def get_recent(self):
"""Get the most recent history entries.""" """Get the most recent history entries."""
return self.select(sort_by='atime', sort_order='desc', limit=100) return self.select(sort_by='atime', sort_order='desc', limit=100)
@ -111,7 +132,7 @@ class WebHistory(sql.SqlTable):
self._do_clear() self._do_clear()
else: else:
message.confirm_async(self._do_clear, title="Clear all browsing " message.confirm_async(self._do_clear, title="Clear all browsing "
"history?") "history?")
def _do_clear(self): def _do_clear(self):
self.delete_all() self.delete_all()
@ -152,20 +173,20 @@ class WebHistory(sql.SqlTable):
(hidden in completion) (hidden in completion)
atime: Override the atime used to add the entry atime: Override the atime used to add the entry
""" """
if not url.isValid(): # pragma: no cover if not url.isValid():
# the no cover pragma is a WORKAROUND for this not being covered in
# old Qt versions.
log.misc.warning("Ignoring invalid URL being added to history") log.misc.warning("Ignoring invalid URL being added to history")
return return
if 'no-sql-history' in objreg.get('args').debug_flags:
return
atime = int(atime) if (atime is not None) else int(time.time()) atime = int(atime) if (atime is not None) else int(time.time())
url_str = url.toString(QUrl.FullyEncoded | QUrl.RemovePassword) self.insert({'url': self._format_url(url),
self.insert({'url': url_str,
'title': title, 'title': title,
'atime': atime, 'atime': atime,
'redirect': redirect}) 'redirect': redirect})
if not redirect: if not redirect:
self.completion.insert({'url': url_str, self.completion.insert({'url': self._format_completion_url(url),
'title': title, 'title': title,
'last_atime': atime}, 'last_atime': atime},
replace=True) replace=True)
@ -185,7 +206,8 @@ class WebHistory(sql.SqlTable):
# http://xn--pple-43d.com/ with # http://xn--pple-43d.com/ with
# https://bugreports.qt.io/browse/QTBUG-60364 # https://bugreports.qt.io/browse/QTBUG-60364
if url in ['http://.com/', 'https://www..com/']: if url in ['http://.com/', 'https://.com/',
'http://www..com/', 'https://www..com/']:
return None return None
url = QUrl(url) url = QUrl(url)
@ -248,12 +270,13 @@ class WebHistory(sql.SqlTable):
if parsed is None: if parsed is None:
continue continue
url, title, atime, redirect = parsed url, title, atime, redirect = parsed
data['url'].append(url) data['url'].append(self._format_url(url))
data['title'].append(title) data['title'].append(title)
data['atime'].append(atime) data['atime'].append(atime)
data['redirect'].append(redirect) data['redirect'].append(redirect)
if not redirect: if not redirect:
completion_data['url'].append(url) completion_data['url'].append(
self._format_completion_url(url))
completion_data['title'].append(title) completion_data['title'].append(title)
completion_data['last_atime'].append(atime) completion_data['last_atime'].append(atime)
except ValueError as ex: except ValueError as ex:
@ -262,6 +285,12 @@ class WebHistory(sql.SqlTable):
self.insert_batch(data) self.insert_batch(data)
self.completion.insert_batch(completion_data, replace=True) self.completion.insert_batch(completion_data, replace=True)
def _format_url(self, url):
return url.toString(QUrl.RemovePassword | QUrl.FullyEncoded)
def _format_completion_url(self, url):
return url.toString(QUrl.RemovePassword)
@cmdutils.register(instance='web-history', debug=True) @cmdutils.register(instance='web-history', debug=True)
def debug_dump_history(self, dest): def debug_dump_history(self, dest):
"""Dump the history to a file in the old pre-SQL format. """Dump the history to a file in the old pre-SQL format.
@ -292,6 +321,6 @@ def init(parent=None):
history = WebHistory(parent=parent) history = WebHistory(parent=parent)
objreg.register('web-history', history) objreg.register('web-history', history)
if objects.backend == usertypes.Backend.QtWebKit: if objects.backend == usertypes.Backend.QtWebKit: # pragma: no cover
from qutebrowser.browser.webkit import webkithistory from qutebrowser.browser.webkit import webkithistory
webkithistory.init(history) webkithistory.init(history)

View File

@ -24,7 +24,8 @@ import binascii
from PyQt5.QtWidgets import QWidget from PyQt5.QtWidgets import QWidget
from qutebrowser.utils import log, objreg, usertypes from qutebrowser.config import configfiles
from qutebrowser.utils import log, usertypes
from qutebrowser.misc import miscwidgets, objects from qutebrowser.misc import miscwidgets, objects
@ -67,9 +68,8 @@ class AbstractWebInspector(QWidget):
def _load_state_geometry(self): def _load_state_geometry(self):
"""Load the geometry from the state file.""" """Load the geometry from the state file."""
state_config = objreg.get('state-config')
try: try:
data = state_config['geometry']['inspector'] data = configfiles.state['geometry']['inspector']
geom = base64.b64decode(data, validate=True) geom = base64.b64decode(data, validate=True)
except KeyError: except KeyError:
# First start # First start
@ -84,10 +84,9 @@ class AbstractWebInspector(QWidget):
def closeEvent(self, e): def closeEvent(self, e):
"""Save the geometry when closed.""" """Save the geometry when closed."""
state_config = objreg.get('state-config')
data = bytes(self.saveGeometry()) data = bytes(self.saveGeometry())
geom = base64.b64encode(data).decode('ASCII') geom = base64.b64encode(data).decode('ASCII')
state_config['geometry']['inspector'] = geom configfiles.state['geometry']['inspector'] = geom
super().closeEvent(e) super().closeEvent(e)
def inspect(self, page): def inspect(self, page):

View File

@ -85,7 +85,7 @@ class MouseEventFilter(QObject):
def _handle_mouse_press(self, e): def _handle_mouse_press(self, e):
"""Handle pressing of a mouse button.""" """Handle pressing of a mouse button."""
is_rocker_gesture = (config.get('input', 'rocker-gestures') and is_rocker_gesture = (config.val.input.rocker_gestures and
e.buttons() == Qt.LeftButton | Qt.RightButton) e.buttons() == Qt.LeftButton | Qt.RightButton)
if e.button() in [Qt.XButton1, Qt.XButton2] or is_rocker_gesture: if e.button() in [Qt.XButton1, Qt.XButton2] or is_rocker_gesture:
@ -119,7 +119,7 @@ class MouseEventFilter(QObject):
return True return True
if e.modifiers() & Qt.ControlModifier: if e.modifiers() & Qt.ControlModifier:
divider = config.get('input', 'mouse-zoom-divider') divider = config.val.zoom.mouse_divider
if divider == 0: if divider == 0:
return False return False
factor = self._tab.zoom.factor() + (e.angleDelta().y() / divider) factor = self._tab.zoom.factor() + (e.angleDelta().y() / divider)
@ -139,7 +139,7 @@ class MouseEventFilter(QObject):
def _handle_context_menu(self, _e): def _handle_context_menu(self, _e):
"""Suppress context menus if rocker gestures are turned on.""" """Suppress context menus if rocker gestures are turned on."""
return config.get('input', 'rocker-gestures') return config.val.input.rocker_gestures
def _mousepress_insertmode_cb(self, elem): def _mousepress_insertmode_cb(self, elem):
"""Check if the clicked element is editable.""" """Check if the clicked element is editable."""
@ -157,7 +157,7 @@ class MouseEventFilter(QObject):
'click', only_if_normal=True) 'click', only_if_normal=True)
else: else:
log.mouse.debug("Clicked non-editable element!") log.mouse.debug("Clicked non-editable element!")
if config.get('input', 'auto-leave-insert-mode'): if config.val.input.insert_mode.auto_leave:
modeman.leave(self._tab.win_id, usertypes.KeyMode.insert, modeman.leave(self._tab.win_id, usertypes.KeyMode.insert,
'click', maybe=True) 'click', maybe=True)
@ -179,7 +179,7 @@ class MouseEventFilter(QObject):
'click-delayed', only_if_normal=True) 'click-delayed', only_if_normal=True)
else: else:
log.mouse.debug("Clicked non-editable element (delayed)!") log.mouse.debug("Clicked non-editable element (delayed)!")
if config.get('input', 'auto-leave-insert-mode'): if config.val.input.insert_mode.auto_leave:
modeman.leave(self._tab.win_id, usertypes.KeyMode.insert, modeman.leave(self._tab.win_id, usertypes.KeyMode.insert,
'click-delayed', maybe=True) 'click-delayed', maybe=True)

View File

@ -24,6 +24,7 @@ import posixpath
from qutebrowser.browser import webelem from qutebrowser.browser import webelem
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.utils import objreg, urlutils, log, message, qtutils from qutebrowser.utils import objreg, urlutils, log, message, qtutils
from qutebrowser.mainwindow import mainwindow
class Error(Exception): class Error(Exception):
@ -42,7 +43,7 @@ def incdec(url, count, inc_or_dec):
background: Open the link in a new background tab. background: Open the link in a new background tab.
window: Open the link in a new window. window: Open the link in a new window.
""" """
segments = set(config.get('general', 'url-incdec-segments')) segments = set(config.val.url.incdec_segments)
try: try:
new_url = urlutils.incdec_number(url, inc_or_dec, count, new_url = urlutils.incdec_number(url, inc_or_dec, count,
segments=segments) segments=segments)
@ -80,10 +81,13 @@ def _find_prevnext(prev, elems):
# Then check for regular links/buttons. # Then check for regular links/buttons.
elems = [e for e in elems if e.tag_name() != 'link'] elems = [e for e in elems if e.tag_name() != 'link']
option = 'prev-regexes' if prev else 'next-regexes' option = 'prev_regexes' if prev else 'next_regexes'
if not elems: if not elems:
return None return None
for regex in config.get('hints', option):
# pylint: disable=bad-config-option
for regex in getattr(config.val.hints, option):
# pylint: enable=bad-config-option
log.hints.vdebug("== Checking regex '{}'.".format(regex.pattern)) log.hints.vdebug("== Checking regex '{}'.".format(regex.pattern))
for e in elems: for e in elems:
text = str(e) text = str(e)
@ -131,7 +135,6 @@ def prevnext(*, browsertab, win_id, baseurl, prev=False,
window=win_id) window=win_id)
if window: if window:
from qutebrowser.mainwindow import mainwindow
new_window = mainwindow.MainWindow( new_window = mainwindow.MainWindow(
private=cur_tabbed_browser.private) private=cur_tabbed_browser.private)
new_window.show() new_window.show()

View File

@ -247,10 +247,21 @@ class PACFetcher(QObject):
self._pac_url = url self._pac_url = url
self._manager = QNetworkAccessManager() self._manager = QNetworkAccessManager()
self._manager.setProxy(QNetworkProxy(QNetworkProxy.NoProxy)) self._manager.setProxy(QNetworkProxy(QNetworkProxy.NoProxy))
self._reply = self._manager.get(QNetworkRequest(url))
self._reply.finished.connect(self._finish)
self._pac = None self._pac = None
self._error_message = None self._error_message = None
self._reply = None
def __eq__(self, other):
# pylint: disable=protected-access
return self._pac_url == other._pac_url
def __repr__(self):
return utils.get_repr(self, url=self._pac_url, constructor=True)
def fetch(self):
"""Fetch the proxy from the remote URL."""
self._reply = self._manager.get(QNetworkRequest(self._pac_url))
self._reply.finished.connect(self._finish)
@pyqtSlot() @pyqtSlot()
def _finish(self): def _finish(self):

View File

@ -44,7 +44,7 @@ class ProxyFactory(QNetworkProxyFactory):
Return: Return:
None if proxy is correct, otherwise an error message. None if proxy is correct, otherwise an error message.
""" """
proxy = config.get('network', 'proxy') proxy = config.val.content.proxy
if isinstance(proxy, pac.PACFetcher): if isinstance(proxy, pac.PACFetcher):
return proxy.fetch_error() return proxy.fetch_error()
else: else:
@ -59,7 +59,7 @@ class ProxyFactory(QNetworkProxyFactory):
Return: Return:
A list of QNetworkProxy objects in order of preference. A list of QNetworkProxy objects in order of preference.
""" """
proxy = config.get('network', 'proxy') proxy = config.val.content.proxy
if proxy is configtypes.SYSTEM_PROXY: if proxy is configtypes.SYSTEM_PROXY:
proxies = QNetworkProxyFactory.systemProxyForQuery(query) proxies = QNetworkProxyFactory.systemProxyForQuery(query)
elif isinstance(proxy, pac.PACFetcher): elif isinstance(proxy, pac.PACFetcher):
@ -69,7 +69,7 @@ class ProxyFactory(QNetworkProxyFactory):
for p in proxies: for p in proxies:
if p.type() != QNetworkProxy.NoProxy: if p.type() != QNetworkProxy.NoProxy:
capabilities = p.capabilities() capabilities = p.capabilities()
if config.get('network', 'proxy-dns-requests'): if config.val.content.proxy_dns_requests:
capabilities |= QNetworkProxy.HostNameLookupCapability capabilities |= QNetworkProxy.HostNameLookupCapability
else: else:
capabilities &= ~QNetworkProxy.HostNameLookupCapability capabilities &= ~QNetworkProxy.HostNameLookupCapability

View File

@ -22,8 +22,8 @@
import io import io
import shutil import shutil
import functools import functools
import collections
import attr
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QTimer from PyQt5.QtCore import pyqtSlot, pyqtSignal, QTimer
from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply
@ -34,7 +34,11 @@ from qutebrowser.browser.webkit import http
from qutebrowser.browser.webkit.network import networkmanager from qutebrowser.browser.webkit.network import networkmanager
_RetryInfo = collections.namedtuple('_RetryInfo', ['request', 'manager']) @attr.s
class _RetryInfo:
request = attr.ib()
manager = attr.ib()
class DownloadItem(downloads.AbstractDownloadItem): class DownloadItem(downloads.AbstractDownloadItem):
@ -368,7 +372,7 @@ class DownloadManager(downloads.AbstractDownloadManager):
super().__init__(parent) super().__init__(parent)
self._networkmanager = networkmanager.NetworkManager( self._networkmanager = networkmanager.NetworkManager(
win_id=win_id, tab_id=None, win_id=win_id, tab_id=None,
private=config.get('general', 'private-browsing'), parent=self) private=config.val.content.private_browsing, parent=self)
@pyqtSlot('QUrl') @pyqtSlot('QUrl')
def get(self, url, *, user_agent=None, **kwargs): def get(self, url, *, user_agent=None, **kwargs):
@ -483,7 +487,7 @@ class DownloadManager(downloads.AbstractDownloadManager):
reply: The QNetworkReply to download. reply: The QNetworkReply to download.
target: Where to save the download as downloads.DownloadTarget. target: Where to save the download as downloads.DownloadTarget.
auto_remove: Whether to remove the download even if auto_remove: Whether to remove the download even if
ui -> remove-finished-downloads is set to -1. downloads.remove_finished is set to -1.
Return: Return:
The created DownloadItem. The created DownloadItem.

View File

@ -28,15 +28,15 @@ import json
import os import os
import time import time
import urllib.parse import urllib.parse
import datetime import textwrap
import pkg_resources import pkg_resources
from PyQt5.QtCore import QUrlQuery, QUrl from PyQt5.QtCore import QUrlQuery, QUrl
import qutebrowser import qutebrowser
from qutebrowser.config import config from qutebrowser.config import config, configdata, configexc, configdiff
from qutebrowser.utils import (version, utils, jinja, log, message, docutils, from qutebrowser.utils import (version, utils, jinja, log, message, docutils,
objreg, usertypes, qtutils) objreg)
from qutebrowser.misc import objects from qutebrowser.misc import objects
@ -122,8 +122,7 @@ class add_handler: # pylint: disable=invalid-name
title="Error while opening qute://url", title="Error while opening qute://url",
url=url.toDisplayString(), url=url.toDisplayString(),
error='{} is not available with this ' error='{} is not available with this '
'backend'.format(url.toDisplayString()), 'backend'.format(url.toDisplayString()))
icon='')
return 'text/html', html return 'text/html', html
@ -224,50 +223,13 @@ def qute_history(url):
return 'text/html', json.dumps(history_data(start_time, offset)) return 'text/html', json.dumps(history_data(start_time, offset))
else: else:
if ( if not config.val.content.javascript.enabled:
config.get('content', 'allow-javascript') and return 'text/plain', b'JavaScript is required for qute://history'
(objects.backend == usertypes.Backend.QtWebEngine or return 'text/html', jinja.render(
qtutils.is_qtwebkit_ng()) 'history.html',
): title='History',
return 'text/html', jinja.render( gap_interval=config.val.history_gap_interval
'history.html', )
title='History',
session_interval=config.get('ui', 'history-session-interval')
)
else:
# Get current date from query parameter, if not given choose today.
curr_date = datetime.date.today()
try:
query_date = QUrlQuery(url).queryItemValue("date")
if query_date:
curr_date = datetime.datetime.strptime(query_date,
"%Y-%m-%d").date()
except ValueError:
log.misc.debug("Invalid date passed to qute:history: " +
query_date)
one_day = datetime.timedelta(days=1)
next_date = curr_date + one_day
prev_date = curr_date - one_day
# start_time is the last second of curr_date
start_time = time.mktime(next_date.timetuple()) - 1
history = [
(i["url"], i["title"],
datetime.datetime.fromtimestamp(i["time"]),
QUrl(i["url"]).host())
for i in history_data(start_time)
]
return 'text/html', jinja.render(
'history_nojs.html',
title='History',
history=history,
curr_date=curr_date,
next_date=next_date,
prev_date=prev_date,
today=datetime.date.today(),
)
@add_handler('javascript') @add_handler('javascript')
@ -345,26 +307,12 @@ def qute_log(url):
@add_handler('gpl') @add_handler('gpl')
def qute_gpl(_url): def qute_gpl(_url):
"""Handler for qute://gpl. Return HTML content as string.""" """Handler for qute://gpl. Return HTML content as string."""
return 'text/html', utils.read_file('html/COPYING.html') return 'text/html', utils.read_file('html/LICENSE.html')
@add_handler('help') @add_handler('help')
def qute_help(url): def qute_help(url):
"""Handler for qute://help.""" """Handler for qute://help."""
try:
utils.read_file('html/doc/index.html')
except OSError:
html = jinja.render(
'error.html',
title="Error while loading documentation",
url=url.toDisplayString(),
error="This most likely means the documentation was not generated "
"properly. If you are running qutebrowser from the git "
"repository, please run scripts/asciidoc2html.py. "
"If you're running a released version this is a bug, please "
"use :report to report it.",
icon='')
return 'text/html', html
urlpath = url.path() urlpath = url.path()
if not urlpath or urlpath == '/': if not urlpath or urlpath == '/':
urlpath = 'index.html' urlpath = 'index.html'
@ -373,11 +321,45 @@ def qute_help(url):
if not docutils.docs_up_to_date(urlpath): if not docutils.docs_up_to_date(urlpath):
message.error("Your documentation is outdated! Please re-run " message.error("Your documentation is outdated! Please re-run "
"scripts/asciidoc2html.py.") "scripts/asciidoc2html.py.")
path = 'html/doc/{}'.format(urlpath) path = 'html/doc/{}'.format(urlpath)
if urlpath.endswith('.png'): if urlpath.endswith('.png'):
return 'image/png', utils.read_file(path, binary=True) return 'image/png', utils.read_file(path, binary=True)
else:
try:
data = utils.read_file(path) data = utils.read_file(path)
except OSError:
# No .html around, let's see if we find the asciidoc
asciidoc_path = path.replace('.html', '.asciidoc')
if asciidoc_path.startswith('html/doc/'):
asciidoc_path = asciidoc_path.replace('html/doc/', '../doc/help/')
try:
asciidoc = utils.read_file(asciidoc_path)
except OSError:
asciidoc = None
if asciidoc is None:
raise
preamble = textwrap.dedent("""
There was an error loading the documentation!
This most likely means the documentation was not generated
properly. If you are running qutebrowser from the git repository,
please (re)run scripts/asciidoc2html.py and reload this page.
If you're running a released version this is a bug, please use
:report to report it.
Falling back to the plaintext version.
---------------------------------------------------------------
""")
return 'text/plain', (preamble + asciidoc).encode('utf-8')
else:
return 'text/html', data return 'text/html', data
@ -390,3 +372,47 @@ def qute_backend_warning(_url):
version=pkg_resources.parse_version, version=pkg_resources.parse_version,
title="Legacy backend warning") title="Legacy backend warning")
return 'text/html', html return 'text/html', html
def _qute_settings_set(url):
"""Handler for qute://settings/set."""
query = QUrlQuery(url)
option = query.queryItemValue('option', QUrl.FullyDecoded)
value = query.queryItemValue('value', QUrl.FullyDecoded)
# https://github.com/qutebrowser/qutebrowser/issues/727
if option == 'content.javascript.enabled' and value == 'false':
msg = ("Refusing to disable javascript via qute://settings "
"as it needs javascript support.")
message.error(msg)
return 'text/html', b'error: ' + msg.encode('utf-8')
try:
config.instance.set_str(option, value, save_yaml=True)
return 'text/html', b'ok'
except configexc.Error as e:
message.error(str(e))
return 'text/html', b'error: ' + str(e).encode('utf-8')
@add_handler('settings')
def qute_settings(url):
"""Handler for qute://settings. View/change qute configuration."""
if url.path() == '/set':
return _qute_settings_set(url)
html = jinja.render('settings.html', title='settings',
configdata=configdata,
confget=config.instance.get_str)
return 'text/html', html
@add_handler('configdiff')
def qute_configdiff(_url):
"""Handler for qute://configdiff."""
try:
return 'text/html', configdiff.get_diff()
except OSError as e:
error = (b'Failed to read old config: ' +
str(e.strerror).encode('utf-8'))
return 'text/plain', error

View File

@ -21,10 +21,9 @@
import html import html
import jinja2
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.utils import usertypes, message, log, objreg from qutebrowser.utils import usertypes, message, log, objreg, jinja
from qutebrowser.mainwindow import mainwindow
class CallSuper(Exception): class CallSuper(Exception):
@ -35,16 +34,18 @@ class CallSuper(Exception):
def custom_headers(): def custom_headers():
"""Get the combined custom headers.""" """Get the combined custom headers."""
headers = {} headers = {}
dnt = b'1' if config.get('network', 'do-not-track') else b'0'
headers[b'DNT'] = dnt
headers[b'X-Do-Not-Track'] = dnt
config_headers = config.get('network', 'custom-headers') dnt_config = config.val.content.headers.do_not_track
if config_headers is not None: if dnt_config is not None:
for header, value in config_headers.items(): dnt = b'1' if dnt_config else b'0'
headers[header.encode('ascii')] = value.encode('ascii') headers[b'DNT'] = dnt
headers[b'X-Do-Not-Track'] = dnt
accept_language = config.get('network', 'accept-language') conf_headers = config.val.content.headers.custom
for header, value in conf_headers.items():
headers[header.encode('ascii')] = value.encode('ascii')
accept_language = config.val.content.headers.accept_language
if accept_language is not None: if accept_language is not None:
headers[b'Accept-Language'] = accept_language.encode('ascii') headers[b'Accept-Language'] = accept_language.encode('ascii')
@ -72,7 +73,7 @@ def authentication_required(url, authenticator, abort_on):
def javascript_confirm(url, js_msg, abort_on): def javascript_confirm(url, js_msg, abort_on):
"""Display a javascript confirm prompt.""" """Display a javascript confirm prompt."""
log.js.debug("confirm: {}".format(js_msg)) log.js.debug("confirm: {}".format(js_msg))
if config.get('ui', 'modal-js-dialog'): if config.val.content.javascript.modal_dialog:
raise CallSuper raise CallSuper
msg = 'From <b>{}</b>:<br/>{}'.format(html.escape(url.toDisplayString()), msg = 'From <b>{}</b>:<br/>{}'.format(html.escape(url.toDisplayString()),
@ -86,9 +87,9 @@ def javascript_confirm(url, js_msg, abort_on):
def javascript_prompt(url, js_msg, default, abort_on): def javascript_prompt(url, js_msg, default, abort_on):
"""Display a javascript prompt.""" """Display a javascript prompt."""
log.js.debug("prompt: {}".format(js_msg)) log.js.debug("prompt: {}".format(js_msg))
if config.get('ui', 'modal-js-dialog'): if config.val.content.javascript.modal_dialog:
raise CallSuper raise CallSuper
if config.get('content', 'ignore-javascript-prompt'): if not config.val.content.javascript.prompt:
return (False, "") return (False, "")
msg = '<b>{}</b> asks:<br/>{}'.format(html.escape(url.toDisplayString()), msg = '<b>{}</b> asks:<br/>{}'.format(html.escape(url.toDisplayString()),
@ -107,10 +108,10 @@ def javascript_prompt(url, js_msg, default, abort_on):
def javascript_alert(url, js_msg, abort_on): def javascript_alert(url, js_msg, abort_on):
"""Display a javascript alert.""" """Display a javascript alert."""
log.js.debug("alert: {}".format(js_msg)) log.js.debug("alert: {}".format(js_msg))
if config.get('ui', 'modal-js-dialog'): if config.val.content.javascript.modal_dialog:
raise CallSuper raise CallSuper
if config.get('content', 'ignore-javascript-alert'): if not config.val.content.javascript.alert:
return return
msg = 'From <b>{}</b>:<br/>{}'.format(html.escape(url.toDisplayString()), msg = 'From <b>{}</b>:<br/>{}'.format(html.escape(url.toDisplayString()),
@ -119,6 +120,22 @@ def javascript_alert(url, js_msg, abort_on):
abort_on=abort_on) abort_on=abort_on)
def javascript_log_message(level, source, line, msg):
"""Display a JavaScript log message."""
logstring = "[{}:{}] {}".format(source, line, msg)
# Needs to line up with the values allowed for the
# content.javascript.log setting.
logmap = {
'none': lambda arg: None,
'debug': log.js.debug,
'info': log.js.info,
'warning': log.js.warning,
'error': log.js.error,
}
logger = logmap[config.val.content.javascript.log[level.name]]
logger(logstring)
def ignore_certificate_errors(url, errors, abort_on): def ignore_certificate_errors(url, errors, abort_on):
"""Display a certificate error question. """Display a certificate error question.
@ -129,7 +146,7 @@ def ignore_certificate_errors(url, errors, abort_on):
Return: Return:
True if the error should be ignored, False otherwise. True if the error should be ignored, False otherwise.
""" """
ssl_strict = config.get('network', 'ssl-strict') ssl_strict = config.val.content.ssl_strict
log.webview.debug("Certificate errors {!r}, strict {}".format( log.webview.debug("Certificate errors {!r}, strict {}".format(
errors, ssl_strict)) errors, ssl_strict))
@ -137,7 +154,7 @@ def ignore_certificate_errors(url, errors, abort_on):
assert error.is_overridable(), repr(error) assert error.is_overridable(), repr(error)
if ssl_strict == 'ask': if ssl_strict == 'ask':
err_template = jinja2.Template(""" err_template = jinja.environment.from_string("""
Errors while loading <b>{{url.toDisplayString()}}</b>:<br/> Errors while loading <b>{{url.toDisplayString()}}</b>:<br/>
<ul> <ul>
{% for err in errors %} {% for err in errors %}
@ -155,7 +172,7 @@ def ignore_certificate_errors(url, errors, abort_on):
ignore = False ignore = False
return ignore return ignore
elif ssl_strict is False: elif ssl_strict is False:
log.webview.debug("ssl-strict is False, only warning about errors") log.webview.debug("ssl_strict is False, only warning about errors")
for err in errors: for err in errors:
# FIXME we might want to use warn here (non-fatal error) # FIXME we might want to use warn here (non-fatal error)
# https://github.com/qutebrowser/qutebrowser/issues/114 # https://github.com/qutebrowser/qutebrowser/issues/114
@ -173,7 +190,7 @@ def feature_permission(url, option, msg, yes_action, no_action, abort_on):
Args: Args:
url: The URL the request was done for. url: The URL the request was done for.
option: A (section, option) tuple for the option to check. option: An option name to check.
msg: A string like "show notifications" msg: A string like "show notifications"
yes_action: A callable to call if the request was approved yes_action: A callable to call if the request was approved
no_action: A callable to call if the request was denied no_action: A callable to call if the request was denied
@ -182,7 +199,7 @@ def feature_permission(url, option, msg, yes_action, no_action, abort_on):
Return: Return:
The Question object if a question was asked, None otherwise. The Question object if a question was asked, None otherwise.
""" """
config_val = config.get(*option) config_val = config.instance.get(option)
if config_val == 'ask': if config_val == 'ask':
if url.isValid(): if url.isValid():
text = "Allow the website at <b>{}</b> to {}?".format( text = "Allow the website at <b>{}</b> to {}?".format(
@ -218,7 +235,6 @@ def get_tab(win_id, target):
elif target == usertypes.ClickTarget.window: elif target == usertypes.ClickTarget.window:
tabbed_browser = objreg.get('tabbed-browser', scope='window', tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id) window=win_id)
from qutebrowser.mainwindow import mainwindow
window = mainwindow.MainWindow(private=tabbed_browser.private) window = mainwindow.MainWindow(private=tabbed_browser.private)
window.show() window.show()
win_id = window.win_id win_id = window.win_id
@ -233,15 +249,14 @@ def get_tab(win_id, target):
def get_user_stylesheet(): def get_user_stylesheet():
"""Get the combined user-stylesheet.""" """Get the combined user-stylesheet."""
filename = config.get('ui', 'user-stylesheet') css = ''
stylesheets = config.val.content.user_stylesheets
if filename is None: for filename in stylesheets:
css = ''
else:
with open(filename, 'r', encoding='utf-8') as f: with open(filename, 'r', encoding='utf-8') as f:
css = f.read() css += f.read()
if config.get('ui', 'hide-scrollbar'): if not config.val.scrolling.bar:
css += '\nhtml > ::-webkit-scrollbar { width: 0px; height: 0px; }' css += '\nhtml > ::-webkit-scrollbar { width: 0px; height: 0px; }'
return css return css

View File

@ -31,6 +31,7 @@ from PyQt5.QtGui import QMouseEvent
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.keyinput import modeman from qutebrowser.keyinput import modeman
from qutebrowser.mainwindow import mainwindow
from qutebrowser.utils import log, usertypes, utils, qtutils, objreg from qutebrowser.utils import log, usertypes, utils, qtutils, objreg
@ -182,7 +183,7 @@ class AbstractWebElement(collections.abc.MutableMapping):
# at least a classid attribute. Oh, and let's hope images/... # at least a classid attribute. Oh, and let's hope images/...
# DON'T have a classid attribute. HTML sucks. # DON'T have a classid attribute. HTML sucks.
log.webelem.debug("<object type='{}'> clicked.".format(objtype)) log.webelem.debug("<object type='{}'> clicked.".format(objtype))
return config.get('input', 'insert-mode-on-plugins') return config.val.input.insert_mode.plugins
else: else:
# Image/Audio/... # Image/Audio/...
return False return False
@ -247,7 +248,7 @@ class AbstractWebElement(collections.abc.MutableMapping):
return self.is_writable() return self.is_writable()
elif tag in ['embed', 'applet']: elif tag in ['embed', 'applet']:
# Flash/Java/... # Flash/Java/...
return config.get('input', 'insert-mode-on-plugins') and not strict return config.val.input.insert_mode.plugins and not strict
elif tag == 'object': elif tag == 'object':
return self._is_editable_object() and not strict return self._is_editable_object() and not strict
elif tag in ['div', 'pre']: elif tag in ['div', 'pre']:
@ -329,7 +330,7 @@ class AbstractWebElement(collections.abc.MutableMapping):
usertypes.ClickTarget.tab: Qt.ControlModifier, usertypes.ClickTarget.tab: Qt.ControlModifier,
usertypes.ClickTarget.tab_bg: Qt.ControlModifier, usertypes.ClickTarget.tab_bg: Qt.ControlModifier,
} }
if config.get('tabs', 'background-tabs'): if config.val.tabs.background:
modifiers[usertypes.ClickTarget.tab] |= Qt.ShiftModifier modifiers[usertypes.ClickTarget.tab] |= Qt.ShiftModifier
else: else:
modifiers[usertypes.ClickTarget.tab_bg] |= Qt.ShiftModifier modifiers[usertypes.ClickTarget.tab_bg] |= Qt.ShiftModifier
@ -372,7 +373,6 @@ class AbstractWebElement(collections.abc.MutableMapping):
background = click_target == usertypes.ClickTarget.tab_bg background = click_target == usertypes.ClickTarget.tab_bg
tabbed_browser.tabopen(url, background=background) tabbed_browser.tabopen(url, background=background)
elif click_target == usertypes.ClickTarget.window: elif click_target == usertypes.ClickTarget.window:
from qutebrowser.mainwindow import mainwindow
window = mainwindow.MainWindow(private=tabbed_browser.private) window = mainwindow.MainWindow(private=tabbed_browser.private)
window.show() window.show()
window.tabbed_browser.tabopen(url) window.tabbed_browser.tabopen(url)

View File

@ -63,6 +63,6 @@ class RequestInterceptor(QWebEngineUrlRequestInterceptor):
for header, value in shared.custom_headers(): for header, value in shared.custom_headers():
info.setHttpHeader(header, value) info.setHttpHeader(header, value)
user_agent = config.get('network', 'user-agent') user_agent = config.val.content.headers.user_agent
if user_agent is not None: if user_agent is not None:
info.setHttpHeader(b'User-Agent', user_agent.encode('ascii')) info.setHttpHeader(b'User-Agent', user_agent.encode('ascii'))

View File

@ -162,7 +162,7 @@ class WebEngineElement(webelem.AbstractWebElement):
top = rect['top'] top = rect['top']
if width > 1 and height > 1: if width > 1 and height > 1:
# Fix coordinates according to zoom level # Fix coordinates according to zoom level
# We're not checking for zoom-text-only here as that doesn't # We're not checking for zoom.text_only here as that doesn't
# exist for QtWebEngine. # exist for QtWebEngine.
zoom = self._tab.zoom.factor() zoom = self._tab.zoom.factor()
rect = QRect(left * zoom, top * zoom, rect = QRect(left * zoom, top * zoom,

View File

@ -38,7 +38,7 @@ from PyQt5.QtWebEngineWidgets import (QWebEngineSettings, QWebEngineProfile,
from qutebrowser.browser import shared from qutebrowser.browser import shared
from qutebrowser.config import config, websettings from qutebrowser.config import config, websettings
from qutebrowser.utils import objreg, utils, standarddir, javascript, qtutils from qutebrowser.utils import utils, standarddir, javascript, qtutils
# The default QWebEngineProfile # The default QWebEngineProfile
@ -112,7 +112,7 @@ class DefaultProfileSetter(websettings.Base):
class PersistentCookiePolicy(DefaultProfileSetter): class PersistentCookiePolicy(DefaultProfileSetter):
"""The cookies -> store setting is different from other settings.""" """The content.cookies.store setting is different from other settings."""
def __init__(self): def __init__(self):
super().__init__('setPersistentCookiesPolicy') super().__init__('setPersistentCookiesPolicy')
@ -158,26 +158,29 @@ def _init_stylesheet(profile):
profile.scripts().insert(script) profile.scripts().insert(script)
def _set_user_agent(profile): def _set_http_headers(profile):
"""Set the user agent for the given profile. """Set the user agent and accept-language for the given profile.
We override this per request in the URL interceptor (to allow for We override those per request in the URL interceptor (to allow for
per-domain user agents), but this one still gets used for things like per-domain values), but this one still gets used for things like
window.navigator.userAgent in JS. window.navigator.userAgent/.languages in JS.
""" """
user_agent = config.get('network', 'user-agent') profile.setHttpUserAgent(config.val.content.headers.user_agent)
profile.setHttpUserAgent(user_agent) accept_language = config.val.content.headers.accept_language
if accept_language is not None:
profile.setHttpAcceptLanguage(accept_language)
def update_settings(section, option): def _update_settings(option):
"""Update global settings when qwebsettings changed.""" """Update global settings when qwebsettings changed."""
websettings.update_mappings(MAPPINGS, section, option) websettings.update_mappings(MAPPINGS, option)
if section == 'ui' and option in ['hide-scrollbar', 'user-stylesheet']: if option in ['scrollbar.hide', 'content.user_stylesheets']:
_init_stylesheet(default_profile) _init_stylesheet(default_profile)
_init_stylesheet(private_profile) _init_stylesheet(private_profile)
elif section == 'network' and option == 'user-agent': elif option in ['content.headers.user_agent',
_set_user_agent(default_profile) 'content.headers.accept_language']:
_set_user_agent(private_profile) _set_http_headers(default_profile)
_set_http_headers(private_profile)
def _init_profiles(): def _init_profiles():
@ -189,12 +192,12 @@ def _init_profiles():
default_profile.setPersistentStoragePath( default_profile.setPersistentStoragePath(
os.path.join(standarddir.data(), 'webengine')) os.path.join(standarddir.data(), 'webengine'))
_init_stylesheet(default_profile) _init_stylesheet(default_profile)
_set_user_agent(default_profile) _set_http_headers(default_profile)
private_profile = QWebEngineProfile() private_profile = QWebEngineProfile()
assert private_profile.isOffTheRecord() assert private_profile.isOffTheRecord()
_init_stylesheet(private_profile) _init_stylesheet(private_profile)
_set_user_agent(private_profile) _set_http_headers(private_profile)
def init(args): def init(args):
@ -212,11 +215,11 @@ def init(args):
# We need to do this here as a WORKAROUND for # We need to do this here as a WORKAROUND for
# https://bugreports.qt.io/browse/QTBUG-58650 # https://bugreports.qt.io/browse/QTBUG-58650
if not qtutils.version_check('5.9'): if not qtutils.version_check('5.9'):
PersistentCookiePolicy().set(config.get('content', 'cookies-store')) PersistentCookiePolicy().set(config.val.content.cookies.store)
Attribute(QWebEngineSettings.FullScreenSupportEnabled).set(True) Attribute(QWebEngineSettings.FullScreenSupportEnabled).set(True)
websettings.init_mappings(MAPPINGS) websettings.init_mappings(MAPPINGS)
objreg.get('config').changed.connect(update_settings) config.instance.changed.connect(_update_settings)
def shutdown(): def shutdown():
@ -237,79 +240,70 @@ def shutdown():
MAPPINGS = { MAPPINGS = {
'content': { 'content.images':
'allow-images': Attribute(QWebEngineSettings.AutoLoadImages),
Attribute(QWebEngineSettings.AutoLoadImages), 'content.javascript.enabled':
'allow-javascript': Attribute(QWebEngineSettings.JavascriptEnabled),
Attribute(QWebEngineSettings.JavascriptEnabled), 'content.javascript.can_open_tabs_automatically':
'javascript-can-open-windows-automatically': Attribute(QWebEngineSettings.JavascriptCanOpenWindows),
Attribute(QWebEngineSettings.JavascriptCanOpenWindows), 'content.javascript.can_access_clipboard':
'javascript-can-access-clipboard': Attribute(QWebEngineSettings.JavascriptCanAccessClipboard),
Attribute(QWebEngineSettings.JavascriptCanAccessClipboard), 'content.plugins':
'allow-plugins': Attribute(QWebEngineSettings.PluginsEnabled),
Attribute(QWebEngineSettings.PluginsEnabled), 'content.hyperlink_auditing':
'hyperlink-auditing': Attribute(QWebEngineSettings.HyperlinkAuditingEnabled),
Attribute(QWebEngineSettings.HyperlinkAuditingEnabled), 'content.local_content_can_access_remote_urls':
'local-content-can-access-remote-urls': Attribute(QWebEngineSettings.LocalContentCanAccessRemoteUrls),
Attribute(QWebEngineSettings.LocalContentCanAccessRemoteUrls), 'content.local_content_can_access_file_urls':
'local-content-can-access-file-urls': Attribute(QWebEngineSettings.LocalContentCanAccessFileUrls),
Attribute(QWebEngineSettings.LocalContentCanAccessFileUrls), 'content.webgl':
'webgl': Attribute(QWebEngineSettings.WebGLEnabled),
Attribute(QWebEngineSettings.WebGLEnabled), 'content.local_storage':
}, Attribute(QWebEngineSettings.LocalStorageEnabled),
'input': { 'content.cache.size':
'spatial-navigation': # 0: automatically managed by QtWebEngine
Attribute(QWebEngineSettings.SpatialNavigationEnabled), DefaultProfileSetter('setHttpCacheMaximumSize', default=0),
'links-included-in-focus-chain': 'content.xss_auditing':
Attribute(QWebEngineSettings.LinksIncludedInFocusChain), Attribute(QWebEngineSettings.XSSAuditingEnabled),
}, 'content.default_encoding':
'fonts': { Setter(QWebEngineSettings.setDefaultTextEncoding),
'web-family-standard':
FontFamilySetter(QWebEngineSettings.StandardFont), 'input.spatial_navigation':
'web-family-fixed': Attribute(QWebEngineSettings.SpatialNavigationEnabled),
FontFamilySetter(QWebEngineSettings.FixedFont), 'input.links_included_in_focus_chain':
'web-family-serif': Attribute(QWebEngineSettings.LinksIncludedInFocusChain),
FontFamilySetter(QWebEngineSettings.SerifFont),
'web-family-sans-serif': 'fonts.web.family.standard':
FontFamilySetter(QWebEngineSettings.SansSerifFont), FontFamilySetter(QWebEngineSettings.StandardFont),
'web-family-cursive': 'fonts.web.family.fixed':
FontFamilySetter(QWebEngineSettings.CursiveFont), FontFamilySetter(QWebEngineSettings.FixedFont),
'web-family-fantasy': 'fonts.web.family.serif':
FontFamilySetter(QWebEngineSettings.FantasyFont), FontFamilySetter(QWebEngineSettings.SerifFont),
'web-size-minimum': 'fonts.web.family.sans_serif':
Setter(QWebEngineSettings.setFontSize, FontFamilySetter(QWebEngineSettings.SansSerifFont),
args=[QWebEngineSettings.MinimumFontSize]), 'fonts.web.family.cursive':
'web-size-minimum-logical': FontFamilySetter(QWebEngineSettings.CursiveFont),
Setter(QWebEngineSettings.setFontSize, 'fonts.web.family.fantasy':
args=[QWebEngineSettings.MinimumLogicalFontSize]), FontFamilySetter(QWebEngineSettings.FantasyFont),
'web-size-default': 'fonts.web.size.minimum':
Setter(QWebEngineSettings.setFontSize, Setter(QWebEngineSettings.setFontSize,
args=[QWebEngineSettings.DefaultFontSize]), args=[QWebEngineSettings.MinimumFontSize]),
'web-size-default-fixed': 'fonts.web.size.minimum_logical':
Setter(QWebEngineSettings.setFontSize, Setter(QWebEngineSettings.setFontSize,
args=[QWebEngineSettings.DefaultFixedFontSize]), args=[QWebEngineSettings.MinimumLogicalFontSize]),
}, 'fonts.web.size.default':
'ui': { Setter(QWebEngineSettings.setFontSize,
'smooth-scrolling': args=[QWebEngineSettings.DefaultFontSize]),
Attribute(QWebEngineSettings.ScrollAnimatorEnabled), 'fonts.web.size.default_fixed':
}, Setter(QWebEngineSettings.setFontSize,
'storage': { args=[QWebEngineSettings.DefaultFixedFontSize]),
'local-storage':
Attribute(QWebEngineSettings.LocalStorageEnabled), 'scrolling.smooth':
'cache-size': Attribute(QWebEngineSettings.ScrollAnimatorEnabled),
# 0: automatically managed by QtWebEngine
DefaultProfileSetter('setHttpCacheMaximumSize', default=0),
},
'general': {
'xss-auditing':
Attribute(QWebEngineSettings.XSSAuditingEnabled),
'default-encoding':
Setter(QWebEngineSettings.setDefaultTextEncoding),
}
} }
try: try:
MAPPINGS['general']['print-element-backgrounds'] = Attribute( MAPPINGS['content.print_element_backgrounds'] = Attribute(
QWebEngineSettings.PrintElementBackgrounds) QWebEngineSettings.PrintElementBackgrounds)
except AttributeError: except AttributeError:
# Added in Qt 5.8 # Added in Qt 5.8
@ -318,4 +312,4 @@ except AttributeError:
if qtutils.version_check('5.9'): if qtutils.version_check('5.9'):
# https://bugreports.qt.io/browse/QTBUG-58650 # https://bugreports.qt.io/browse/QTBUG-58650
MAPPINGS['content']['cookies-store'] = PersistentCookiePolicy() MAPPINGS['content.cookies.store'] = PersistentCookiePolicy()

View File

@ -153,20 +153,16 @@ class WebEngineSearch(browsertab.AbstractSearch):
callback(found) callback(found)
self._widget.findText(text, flags, wrapped_callback) self._widget.findText(text, flags, wrapped_callback)
def search(self, text, *, ignore_case=False, reverse=False, def search(self, text, *, ignore_case='never', reverse=False,
result_cb=None): result_cb=None):
flags = QWebEnginePage.FindFlags(0)
if ignore_case == 'smart':
if not text.islower():
flags |= QWebEnginePage.FindCaseSensitively
elif not ignore_case:
flags |= QWebEnginePage.FindCaseSensitively
if reverse:
flags |= QWebEnginePage.FindBackward
self.text = text self.text = text
self._flags = flags self._flags = QWebEnginePage.FindFlags(0)
self._find(text, flags, result_cb, 'search') if self._is_case_sensitive(ignore_case):
self._flags |= QWebEnginePage.FindCaseSensitively
if reverse:
self._flags |= QWebEnginePage.FindBackward
self._find(text, self._flags, result_cb, 'search')
def clear(self): def clear(self):
self.search_displayed = False self.search_displayed = False
@ -699,7 +695,7 @@ class WebEngineTab(browsertab.AbstractTab):
error_page = jinja.render( error_page = jinja.render(
'error.html', 'error.html',
title="Error loading page: {}".format(url_string), title="Error loading page: {}".format(url_string),
url=url_string, error="Authentication required", icon='') url=url_string, error="Authentication required")
self.set_html(error_page) self.set_html(error_page)
@pyqtSlot('QWebEngineFullScreenRequest') @pyqtSlot('QWebEngineFullScreenRequest')

View File

@ -28,8 +28,7 @@ from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage
from qutebrowser.browser import shared from qutebrowser.browser import shared
from qutebrowser.browser.webengine import certificateerror, webenginesettings from qutebrowser.browser.webengine import certificateerror, webenginesettings
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.utils import (log, debug, usertypes, jinja, urlutils, message, from qutebrowser.utils import log, debug, usertypes, jinja, urlutils, message
objreg)
class WebEngineView(QWebEngineView): class WebEngineView(QWebEngineView):
@ -80,10 +79,10 @@ class WebEngineView(QWebEngineView):
The new QWebEngineView object. The new QWebEngineView object.
""" """
debug_type = debug.qenum_key(QWebEnginePage, wintype) debug_type = debug.qenum_key(QWebEnginePage, wintype)
background_tabs = config.get('tabs', 'background-tabs') background = config.val.tabs.background
log.webview.debug("createWindow with type {}, background_tabs " log.webview.debug("createWindow with type {}, background {}".format(
"{}".format(debug_type, background_tabs)) debug_type, background))
if wintype == QWebEnginePage.WebBrowserWindow: if wintype == QWebEnginePage.WebBrowserWindow:
# Shift-Alt-Click # Shift-Alt-Click
@ -95,13 +94,13 @@ class WebEngineView(QWebEngineView):
elif wintype == QWebEnginePage.WebBrowserTab: elif wintype == QWebEnginePage.WebBrowserTab:
# Middle-click / Ctrl-Click with Shift # Middle-click / Ctrl-Click with Shift
# FIXME:qtwebengine this also affects target=_blank links... # FIXME:qtwebengine this also affects target=_blank links...
if background_tabs: if background:
target = usertypes.ClickTarget.tab target = usertypes.ClickTarget.tab
else: else:
target = usertypes.ClickTarget.tab_bg target = usertypes.ClickTarget.tab_bg
elif wintype == QWebEnginePage.WebBrowserBackgroundTab: elif wintype == QWebEnginePage.WebBrowserBackgroundTab:
# Middle-click / Ctrl-Click # Middle-click / Ctrl-Click
if background_tabs: if background:
target = usertypes.ClickTarget.tab_bg target = usertypes.ClickTarget.tab_bg
else: else:
target = usertypes.ClickTarget.tab target = usertypes.ClickTarget.tab
@ -135,11 +134,11 @@ class WebEnginePage(QWebEnginePage):
self._on_feature_permission_requested) self._on_feature_permission_requested)
self._theme_color = theme_color self._theme_color = theme_color
self._set_bg_color() self._set_bg_color()
objreg.get('config').changed.connect(self._set_bg_color) config.instance.changed.connect(self._set_bg_color)
@config.change_filter('colors', 'webpage.bg') @config.change_filter('colors.webpage.bg')
def _set_bg_color(self): def _set_bg_color(self):
col = config.get('colors', 'webpage.bg') col = config.val.colors.webpage.bg
if col is None: if col is None:
col = self._theme_color col = self._theme_color
self.setBackgroundColor(col) self.setBackgroundColor(col)
@ -148,11 +147,10 @@ class WebEnginePage(QWebEnginePage):
def _on_feature_permission_requested(self, url, feature): def _on_feature_permission_requested(self, url, feature):
"""Ask the user for approval for geolocation/media/etc..""" """Ask the user for approval for geolocation/media/etc.."""
options = { options = {
QWebEnginePage.Geolocation: ('content', 'geolocation'), QWebEnginePage.Geolocation: 'content.geolocation',
QWebEnginePage.MediaAudioCapture: ('content', 'media-capture'), QWebEnginePage.MediaAudioCapture: 'content.media_capture',
QWebEnginePage.MediaVideoCapture: ('content', 'media-capture'), QWebEnginePage.MediaVideoCapture: 'content.media_capture',
QWebEnginePage.MediaAudioVideoCapture: QWebEnginePage.MediaAudioVideoCapture: 'content.media_capture',
('content', 'media-capture'),
} }
messages = { messages = {
QWebEnginePage.Geolocation: 'access your location', QWebEnginePage.Geolocation: 'access your location',
@ -214,7 +212,7 @@ class WebEnginePage(QWebEnginePage):
url_string = url.toDisplayString() url_string = url.toDisplayString()
error_page = jinja.render( error_page = jinja.render(
'error.html', title="Error loading page: {}".format(url_string), 'error.html', title="Error loading page: {}".format(url_string),
url=url_string, error=str(error), icon='') url=url_string, error=str(error))
if error.is_overridable(): if error.is_overridable():
ignore = shared.ignore_certificate_errors( ignore = shared.ignore_certificate_errors(
@ -276,19 +274,12 @@ class WebEnginePage(QWebEnginePage):
def javaScriptConsoleMessage(self, level, msg, line, source): def javaScriptConsoleMessage(self, level, msg, line, source):
"""Log javascript messages to qutebrowser's log.""" """Log javascript messages to qutebrowser's log."""
# FIXME:qtwebengine maybe unify this in the tab api somehow? level_map = {
setting = config.get('general', 'log-javascript-console') QWebEnginePage.InfoMessageLevel: usertypes.JsLogLevel.info,
if setting == 'none': QWebEnginePage.WarningMessageLevel: usertypes.JsLogLevel.warning,
return QWebEnginePage.ErrorMessageLevel: usertypes.JsLogLevel.error,
level_to_logger = {
QWebEnginePage.InfoMessageLevel: log.js.info,
QWebEnginePage.WarningMessageLevel: log.js.warning,
QWebEnginePage.ErrorMessageLevel: log.js.error,
} }
logstring = "[{}:{}] {}".format(source, line, msg) shared.javascript_log_message(level_map[level], source, line, msg)
logger = level_to_logger[level]
logger(logstring)
def acceptNavigationRequest(self, def acceptNavigationRequest(self,
url: QUrl, url: QUrl,

View File

@ -24,7 +24,7 @@ import os.path
from PyQt5.QtNetwork import QNetworkDiskCache from PyQt5.QtNetwork import QNetworkDiskCache
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.utils import utils, objreg, qtutils from qutebrowser.utils import utils, qtutils
class DiskCache(QNetworkDiskCache): class DiskCache(QNetworkDiskCache):
@ -35,21 +35,20 @@ class DiskCache(QNetworkDiskCache):
super().__init__(parent) super().__init__(parent)
self.setCacheDirectory(os.path.join(cache_dir, 'http')) self.setCacheDirectory(os.path.join(cache_dir, 'http'))
self._set_cache_size() self._set_cache_size()
objreg.get('config').changed.connect(self._set_cache_size) config.instance.changed.connect(self._set_cache_size)
def __repr__(self): def __repr__(self):
return utils.get_repr(self, size=self.cacheSize(), return utils.get_repr(self, size=self.cacheSize(),
maxsize=self.maximumCacheSize(), maxsize=self.maximumCacheSize(),
path=self.cacheDirectory()) path=self.cacheDirectory())
@config.change_filter('storage', 'cache-size') @config.change_filter('content.cache.size')
def _set_cache_size(self): def _set_cache_size(self):
"""Set the cache size based on the config.""" """Set the cache size based on the config."""
size = config.get('storage', 'cache-size') size = config.val.content.cache.size
if size is None: if size is None:
size = 1024 * 1024 * 50 # default from QNetworkDiskCachePrivate size = 1024 * 1024 * 50 # default from QNetworkDiskCachePrivate
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-59909 # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-59909
if (qtutils.version_check('5.7.1') and if not qtutils.version_check('5.9'): # pragma: no cover
not qtutils.version_check('5.9')): # pragma: no cover
size = 0 size = 0
self.setMaximumCacheSize(size) self.setMaximumCacheSize(size)

View File

@ -38,12 +38,7 @@ class CertificateErrorWrapper(usertypes.AbstractCertificateErrorWrapper):
string=str(self)) string=str(self))
def __hash__(self): def __hash__(self):
try: return hash(self._error)
# Qt >= 5.4
return hash(self._error)
except TypeError: # pragma: no cover
return hash((self._error.certificate().toDer(),
self._error.error()))
def __eq__(self, other): def __eq__(self, other):
return self._error == other._error # pylint: disable=protected-access return self._error == other._error # pylint: disable=protected-access

View File

@ -50,7 +50,7 @@ class RAMCookieJar(QNetworkCookieJar):
Return: Return:
True if one or more cookies are set for 'url', otherwise False. True if one or more cookies are set for 'url', otherwise False.
""" """
if config.get('content', 'cookies-accept') == 'never': if config.val.content.cookies.accept == 'never':
return False return False
else: else:
self.changed.emit() self.changed.emit()
@ -74,10 +74,10 @@ class CookieJar(RAMCookieJar):
self._lineparser = lineparser.LineParser( self._lineparser = lineparser.LineParser(
standarddir.data(), 'cookies', binary=True, parent=self) standarddir.data(), 'cookies', binary=True, parent=self)
self.parse_cookies() self.parse_cookies()
objreg.get('config').changed.connect(self.cookies_store_changed) config.instance.changed.connect(self._on_cookies_store_changed)
objreg.get('save-manager').add_saveable( objreg.get('save-manager').add_saveable(
'cookies', self.save, self.changed, 'cookies', self.save, self.changed,
config_opt=('content', 'cookies-store')) config_opt='content.cookies.store')
def parse_cookies(self): def parse_cookies(self):
"""Parse cookies from lineparser and store them.""" """Parse cookies from lineparser and store them."""
@ -105,10 +105,10 @@ class CookieJar(RAMCookieJar):
self._lineparser.data = lines self._lineparser.data = lines
self._lineparser.save() self._lineparser.save()
@config.change_filter('content', 'cookies-store') @config.change_filter('content.cookies.store')
def cookies_store_changed(self): def _on_cookies_store_changed(self):
"""Delete stored cookies if cookies-store changed.""" """Delete stored cookies if cookies.store changed."""
if not config.get('content', 'cookies-store'): if not config.val.content.cookies.store:
self._lineparser.data = [] self._lineparser.data = []
self._lineparser.save() self._lineparser.save()
self.changed.emit() self.changed.emit()

View File

@ -25,7 +25,6 @@ import io
import os import os
import re import re
import sys import sys
import collections
import uuid import uuid
import email.policy import email.policy
import email.generator import email.generator
@ -34,15 +33,21 @@ import email.mime.multipart
import email.message import email.message
import quopri import quopri
import attr
from PyQt5.QtCore import QUrl from PyQt5.QtCore import QUrl
from qutebrowser.browser import downloads from qutebrowser.browser import downloads
from qutebrowser.browser.webkit import webkitelem from qutebrowser.browser.webkit import webkitelem
from qutebrowser.utils import log, objreg, message, usertypes, utils, urlutils from qutebrowser.utils import log, objreg, message, usertypes, utils, urlutils
_File = collections.namedtuple('_File',
['content', 'content_type', 'content_location', @attr.s
'transfer_encoding']) class _File:
content = attr.ib()
content_type = attr.ib()
content_location = attr.ib()
transfer_encoding = attr.ib()
_CSS_URL_PATTERNS = [re.compile(x) for x in [ _CSS_URL_PATTERNS = [re.compile(x) for x in [
@ -174,7 +179,7 @@ class MHTMLWriter:
root_content: The root content as bytes. root_content: The root content as bytes.
content_location: The url of the page as str. content_location: The url of the page as str.
content_type: The MIME-type of the root content as str. content_type: The MIME-type of the root content as str.
_files: Mapping of location->_File namedtuple. _files: Mapping of location->_File object.
""" """
def __init__(self, root_content, content_location, content_type): def __init__(self, root_content, content_location, content_type):

View File

@ -101,13 +101,12 @@ def dirbrowser_html(path):
except OSError as e: except OSError as e:
html = jinja.render('error.html', html = jinja.render('error.html',
title="Error while reading directory", title="Error while reading directory",
url='file:///{}'.format(path), error=str(e), url='file:///{}'.format(path), error=str(e))
icon='')
return html.encode('UTF-8', errors='xmlcharrefreplace') return html.encode('UTF-8', errors='xmlcharrefreplace')
files = get_file_list(path, all_files, os.path.isfile) files = get_file_list(path, all_files, os.path.isfile)
directories = get_file_list(path, all_files, os.path.isdir) directories = get_file_list(path, all_files, os.path.isdir)
html = jinja.render('dirbrowser.html', title=title, url=path, icon='', html = jinja.render('dirbrowser.html', title=title, url=path,
parent=parent, files=files, directories=directories) parent=parent, files=files, directories=directories)
return html.encode('UTF-8', errors='xmlcharrefreplace') return html.encode('UTF-8', errors='xmlcharrefreplace')

View File

@ -24,13 +24,13 @@ import collections
import netrc import netrc
import html import html
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, PYQT_VERSION, QCoreApplication, import attr
QUrl, QByteArray) from PyQt5.QtCore import (pyqtSlot, pyqtSignal, QCoreApplication, QUrl,
QByteArray)
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkReply, QSslSocket from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkReply, QSslSocket
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.utils import (message, log, usertypes, utils, objreg, qtutils, from qutebrowser.utils import message, log, usertypes, utils, objreg, urlutils
urlutils)
from qutebrowser.browser import shared from qutebrowser.browser import shared
from qutebrowser.browser.webkit import certificateerror from qutebrowser.browser.webkit import certificateerror
from qutebrowser.browser.webkit.network import (webkitqutescheme, networkreply, from qutebrowser.browser.webkit.network import (webkitqutescheme, networkreply,
@ -38,10 +38,19 @@ from qutebrowser.browser.webkit.network import (webkitqutescheme, networkreply,
HOSTBLOCK_ERROR_STRING = '%HOSTBLOCK%' HOSTBLOCK_ERROR_STRING = '%HOSTBLOCK%'
ProxyId = collections.namedtuple('ProxyId', 'type, hostname, port')
_proxy_auth_cache = {} _proxy_auth_cache = {}
@attr.s(frozen=True)
class ProxyId:
"""Information identifying a proxy server."""
type = attr.ib()
hostname = attr.ib()
port = attr.ib()
def _is_secure_cipher(cipher): def _is_secure_cipher(cipher):
"""Check if a given SSL cipher (hopefully) isn't broken yet.""" """Check if a given SSL cipher (hopefully) isn't broken yet."""
tokens = [e.upper() for e in cipher.name().split('-')] tokens = [e.upper() for e in cipher.name().split('-')]
@ -88,15 +97,9 @@ def _is_secure_cipher(cipher):
def init(): def init():
"""Disable insecure SSL ciphers on old Qt versions.""" """Disable insecure SSL ciphers on old Qt versions."""
if qtutils.version_check('5.3.0'): default_ciphers = QSslSocket.defaultCiphers()
default_ciphers = QSslSocket.defaultCiphers() log.init.debug("Default Qt ciphers: {}".format(
log.init.debug("Default Qt ciphers: {}".format( ', '.join(c.name() for c in default_ciphers)))
', '.join(c.name() for c in default_ciphers)))
else:
# https://codereview.qt-project.org/#/c/75943/
default_ciphers = QSslSocket.supportedCiphers()
log.init.debug("Supported Qt ciphers: {}".format(
', '.join(c.name() for c in default_ciphers)))
good_ciphers = [] good_ciphers = []
bad_ciphers = [] bad_ciphers = []
@ -274,7 +277,7 @@ class NetworkManager(QNetworkAccessManager):
# altogether. # altogether.
reply.netrc_used = True reply.netrc_used = True
try: try:
net = netrc.netrc(config.get('network', 'netrc-file')) net = netrc.netrc(config.val.content.netrc_file)
authenticators = net.authenticators(reply.url().host()) authenticators = net.authenticators(reply.url().host())
if authenticators is not None: if authenticators is not None:
(user, _account, password) = authenticators (user, _account, password) = authenticators
@ -338,7 +341,7 @@ class NetworkManager(QNetworkAccessManager):
def set_referer(self, req, current_url): def set_referer(self, req, current_url):
"""Set the referer header.""" """Set the referer header."""
referer_header_conf = config.get('network', 'referer-header') referer_header_conf = config.val.content.headers.referer
try: try:
if referer_header_conf == 'never': if referer_header_conf == 'never':
@ -409,24 +412,11 @@ class NetworkManager(QNetworkAccessManager):
tab = objreg.get('tab', scope='tab', window=self._win_id, tab = objreg.get('tab', scope='tab', window=self._win_id,
tab=self._tab_id) tab=self._tab_id)
current_url = tab.url() current_url = tab.url()
except (KeyError, RuntimeError, TypeError): except (KeyError, RuntimeError):
# https://github.com/qutebrowser/qutebrowser/issues/889 # https://github.com/qutebrowser/qutebrowser/issues/889
# Catching RuntimeError and TypeError because we could be in # Catching RuntimeError because we could be in the middle of
# the middle of the webpage shutdown here. # the webpage shutdown here.
current_url = QUrl() current_url = QUrl()
self.set_referer(req, current_url) self.set_referer(req, current_url)
return super().createRequest(op, req, outgoing_data)
if PYQT_VERSION < 0x050301:
# WORKAROUND (remove this when we bump the requirements to 5.3.1)
#
# If we don't disable our message handler, we get a freeze if a
# warning is printed due to a PyQt bug, e.g. when clicking a
# currency on http://ch.mouser.com/localsites/
#
# See http://www.riverbankcomputing.com/pipermail/pyqt/2014-June/034420.html
with log.disable_qt_msghandler():
reply = super().createRequest(op, req, outgoing_data)
else:
reply = super().createRequest(op, req, outgoing_data)
return reply

View File

@ -20,16 +20,12 @@
"""QtWebKit specific qute://* handlers and glue code.""" """QtWebKit specific qute://* handlers and glue code."""
import mimetypes import mimetypes
import functools
import configparser
from PyQt5.QtCore import pyqtSlot, QObject
from PyQt5.QtNetwork import QNetworkReply from PyQt5.QtNetwork import QNetworkReply
from qutebrowser.browser import pdfjs, qutescheme from qutebrowser.browser import pdfjs, qutescheme
from qutebrowser.browser.webkit.network import schemehandler, networkreply from qutebrowser.browser.webkit.network import schemehandler, networkreply
from qutebrowser.utils import jinja, log, message, objreg, usertypes, qtutils from qutebrowser.utils import log, usertypes, qtutils
from qutebrowser.config import configexc, configdata
class QuteSchemeHandler(schemehandler.SchemeHandler): class QuteSchemeHandler(schemehandler.SchemeHandler):
@ -70,34 +66,6 @@ class QuteSchemeHandler(schemehandler.SchemeHandler):
self.parent()) self.parent())
class JSBridge(QObject):
"""Javascript-bridge for special qute://... pages."""
@pyqtSlot(str, str, str)
def set(self, sectname, optname, value):
"""Slot to set a setting from qute://settings."""
# https://github.com/qutebrowser/qutebrowser/issues/727
if ((sectname, optname) == ('content', 'allow-javascript') and
value == 'false'):
message.error("Refusing to disable javascript via qute://settings "
"as it needs javascript support.")
return
try:
objreg.get('config').set('conf', sectname, optname, value)
except (configexc.Error, configparser.Error) as e:
message.error(str(e))
@qutescheme.add_handler('settings', backend=usertypes.Backend.QtWebKit)
def qute_settings(_url):
"""Handler for qute://settings. View/change qute configuration."""
config_getter = functools.partial(objreg.get('config').get, raw=True)
html = jinja.render('settings.html', title='settings', config=configdata,
confget=config_getter)
return 'text/html', html
@qutescheme.add_handler('pdfjs', backend=usertypes.Backend.QtWebKit) @qutescheme.add_handler('pdfjs', backend=usertypes.Backend.QtWebKit)
def qute_pdfjs(url): def qute_pdfjs(url):
"""Handler for qute://pdfjs. Return the pdf.js viewer.""" """Handler for qute://pdfjs. Return the pdf.js viewer."""

View File

@ -19,11 +19,11 @@
"""pyPEG parsing for the RFC 6266 (Content-Disposition) header.""" """pyPEG parsing for the RFC 6266 (Content-Disposition) header."""
import collections
import urllib.parse import urllib.parse
import string import string
import re import re
import attr
import pypeg2 as peg import pypeg2 as peg
from qutebrowser.utils import utils from qutebrowser.utils import utils
@ -210,7 +210,13 @@ class ContentDispositionValue:
peg.optional(';')) peg.optional(';'))
LangTagged = collections.namedtuple('LangTagged', ['string', 'langtag']) @attr.s
class LangTagged:
"""A string with an associated language."""
string = attr.ib()
langtag = attr.ib()
class Error(Exception): class Error(Exception):

View File

@ -25,13 +25,7 @@ from PyQt5.QtCore import QByteArray, QDataStream, QIODevice, QUrl
from qutebrowser.utils import qtutils from qutebrowser.utils import qtutils
def _encode_url(url): def _serialize_items(items, current_idx, stream):
"""Encode a QUrl suitable to pass to QWebHistory."""
data = bytes(QUrl.toPercentEncoding(url.toString(), b':/#?&+=@%*'))
return data.decode('ascii')
def _serialize_ng(items, current_idx, stream):
# {'currentItemIndex': 0, # {'currentItemIndex': 0,
# 'history': [{'children': [], # 'history': [{'children': [],
# 'documentSequenceNumber': 1485030525573123, # 'documentSequenceNumber': 1485030525573123,
@ -47,13 +41,13 @@ def _serialize_ng(items, current_idx, stream):
# 'urlString': 'about:blank'}]} # 'urlString': 'about:blank'}]}
data = {'currentItemIndex': current_idx, 'history': []} data = {'currentItemIndex': current_idx, 'history': []}
for item in items: for item in items:
data['history'].append(_serialize_item_ng(item)) data['history'].append(_serialize_item(item))
stream.writeInt(3) # history stream version stream.writeInt(3) # history stream version
stream.writeQVariantMap(data) stream.writeQVariantMap(data)
def _serialize_item_ng(item): def _serialize_item(item):
data = { data = {
'originalURLString': item.original_url.toString(QUrl.FullyEncoded), 'originalURLString': item.original_url.toString(QUrl.FullyEncoded),
'scrollPosition': {'x': 0, 'y': 0}, 'scrollPosition': {'x': 0, 'y': 0},
@ -68,82 +62,6 @@ def _serialize_item_ng(item):
return data return data
def _serialize_old(items, current_idx, stream):
### Source/WebKit/qt/Api/qwebhistory.cpp operator<<
stream.writeInt(2) # history stream version
stream.writeInt(len(items))
stream.writeInt(current_idx)
for i, item in enumerate(items):
_serialize_item_old(i, item, stream)
def _serialize_item_old(i, item, stream):
"""Serialize a single WebHistoryItem into a QDataStream.
Args:
i: The index of the current item.
item: The WebHistoryItem to write.
stream: The QDataStream to write to.
"""
### Source/WebCore/history/qt/HistoryItemQt.cpp restoreState
## urlString
stream.writeQString(_encode_url(item.url))
## title
stream.writeQString(item.title)
## originalURLString
stream.writeQString(_encode_url(item.original_url))
### Source/WebCore/history/HistoryItem.cpp decodeBackForwardTree
## backForwardTreeEncodingVersion
stream.writeUInt32(2)
## size (recursion stack)
stream.writeUInt64(0)
## node->m_documentSequenceNumber
# If two HistoryItems have the same document sequence number, then they
# refer to the same instance of a document. Traversing history from one
# such HistoryItem to another preserves the document.
stream.writeInt64(i + 1)
## size (node->m_documentState)
stream.writeUInt64(0)
## node->m_formContentType
# info used to repost form data
stream.writeQString(None)
## hasFormData
stream.writeBool(False)
## node->m_itemSequenceNumber
# If two HistoryItems have the same item sequence number, then they are
# clones of one another. Traversing history from one such HistoryItem to
# another is a no-op. HistoryItem clones are created for parent and
# sibling frames when only a subframe navigates.
stream.writeInt64(i + 1)
## node->m_referrer
stream.writeQString(None)
## node->m_scrollPoint (x)
try:
stream.writeInt32(item.user_data['scroll-pos'].x())
except (KeyError, TypeError):
stream.writeInt32(0)
## node->m_scrollPoint (y)
try:
stream.writeInt32(item.user_data['scroll-pos'].y())
except (KeyError, TypeError):
stream.writeInt32(0)
## node->m_pageScaleFactor
stream.writeFloat(1)
## hasStateObject
# Support for HTML5 History
stream.writeBool(False)
## node->m_target
stream.writeQString(None)
### Source/WebCore/history/qt/HistoryItemQt.cpp restoreState
## validUserData
# We could restore the user data here, but we prefer to use the
# QWebHistoryItem API for that.
stream.writeBool(False)
def serialize(items): def serialize(items):
"""Serialize a list of QWebHistoryItems to a data stream. """Serialize a list of QWebHistoryItems to a data stream.
@ -180,10 +98,7 @@ def serialize(items):
else: else:
current_idx = 0 current_idx = 0
if qtutils.is_qtwebkit_ng(): _serialize_items(items, current_idx, stream)
_serialize_ng(items, current_idx, stream)
else:
_serialize_old(items, current_idx, stream)
user_data += [item.user_data for item in items] user_data += [item.user_data for item in items]

View File

@ -168,7 +168,7 @@ class WebKitElement(webelem.AbstractWebElement):
if width > 1 and height > 1: if width > 1 and height > 1:
# fix coordinates according to zoom level # fix coordinates according to zoom level
zoom = self._elem.webFrame().zoomFactor() zoom = self._elem.webFrame().zoomFactor()
if not config.get('ui', 'zoom-text-only'): if not config.val.zoom.text_only:
rect["left"] *= zoom rect["left"] *= zoom
rect["top"] *= zoom rect["top"] *= zoom
width *= zoom width *= zoom
@ -292,9 +292,6 @@ class WebKitElement(webelem.AbstractWebElement):
elem = elem._parent() # pylint: disable=protected-access elem = elem._parent() # pylint: disable=protected-access
def _move_text_cursor(self): def _move_text_cursor(self):
if self is None:
# old PyQt versions call the slot after the element is deleted.
return
if self.is_text_input() and self.is_editable(): if self.is_text_input() and self.is_editable():
self._tab.caret.move_to_end_of_document() self._tab.caret.move_to_end_of_document()

View File

@ -36,9 +36,9 @@ class WebKitInspector(inspector.AbstractWebInspector):
self._set_widget(qwebinspector) self._set_widget(qwebinspector)
def inspect(self, page): def inspect(self, page):
if not config.get('general', 'developer-extras'): if not config.val.content.developer_extras:
raise inspector.WebInspectorError( raise inspector.WebInspectorError(
"Please enable developer-extras before using the " "Please enable content.developer_extras before using the "
"webinspector!") "webinspector!")
self._widget.setPage(page) self._widget.setPage(page)
self.show() self.show()

View File

@ -33,7 +33,7 @@ from PyQt5.QtGui import QFont
from PyQt5.QtWebKit import QWebSettings from PyQt5.QtWebKit import QWebSettings
from qutebrowser.config import config, websettings from qutebrowser.config import config, websettings
from qutebrowser.utils import standarddir, objreg, urlutils, qtutils from qutebrowser.utils import standarddir, urlutils
from qutebrowser.browser import shared from qutebrowser.browser import shared
@ -111,12 +111,11 @@ def _set_user_stylesheet():
QWebSettings.globalSettings().setUserStyleSheetUrl(url) QWebSettings.globalSettings().setUserStyleSheetUrl(url)
def update_settings(section, option): def _update_settings(option):
"""Update global settings when qwebsettings changed.""" """Update global settings when qwebsettings changed."""
if section == 'ui' and option in ['hide-scrollbar', 'user-stylesheet']: if option in ['scrollbar.hide', 'content.user_stylesheets']:
_set_user_stylesheet() _set_user_stylesheet()
websettings.update_mappings(MAPPINGS, option)
websettings.update_mappings(MAPPINGS, section, option)
def init(_args): def init(_args):
@ -132,16 +131,9 @@ def init(_args):
QWebSettings.setOfflineStoragePath( QWebSettings.setOfflineStoragePath(
os.path.join(data_path, 'offline-storage')) os.path.join(data_path, 'offline-storage'))
if (config.get('general', 'private-browsing') and
not qtutils.version_check('5.4.2')):
# WORKAROUND for https://codereview.qt-project.org/#/c/108936/
# Won't work when private browsing is not enabled globally, but that's
# the best we can do...
QWebSettings.setIconDatabasePath('')
websettings.init_mappings(MAPPINGS) websettings.init_mappings(MAPPINGS)
_set_user_stylesheet() _set_user_stylesheet()
objreg.get('config').changed.connect(update_settings) config.instance.changed.connect(_update_settings)
def shutdown(): def shutdown():
@ -152,96 +144,79 @@ def shutdown():
MAPPINGS = { MAPPINGS = {
'content': { 'content.images':
'allow-images': Attribute(QWebSettings.AutoLoadImages),
Attribute(QWebSettings.AutoLoadImages), 'content.javascript.enabled':
'allow-javascript': Attribute(QWebSettings.JavascriptEnabled),
Attribute(QWebSettings.JavascriptEnabled), 'content.javascript.can_open_tabs_automatically':
'javascript-can-open-windows-automatically': Attribute(QWebSettings.JavascriptCanOpenWindows),
Attribute(QWebSettings.JavascriptCanOpenWindows), 'content.javascript.can_close_tabs':
'javascript-can-close-windows': Attribute(QWebSettings.JavascriptCanCloseWindows),
Attribute(QWebSettings.JavascriptCanCloseWindows), 'content.javascript.can_access_clipboard':
'javascript-can-access-clipboard': Attribute(QWebSettings.JavascriptCanAccessClipboard),
Attribute(QWebSettings.JavascriptCanAccessClipboard), 'content.plugins':
'allow-plugins': Attribute(QWebSettings.PluginsEnabled),
Attribute(QWebSettings.PluginsEnabled), 'content.webgl':
'webgl': Attribute(QWebSettings.WebGLEnabled),
Attribute(QWebSettings.WebGLEnabled), 'content.hyperlink_auditing':
'hyperlink-auditing': Attribute(QWebSettings.HyperlinkAuditingEnabled),
Attribute(QWebSettings.HyperlinkAuditingEnabled), 'content.local_content_can_access_remote_urls':
'local-content-can-access-remote-urls': Attribute(QWebSettings.LocalContentCanAccessRemoteUrls),
Attribute(QWebSettings.LocalContentCanAccessRemoteUrls), 'content.local_content_can_access_file_urls':
'local-content-can-access-file-urls': Attribute(QWebSettings.LocalContentCanAccessFileUrls),
Attribute(QWebSettings.LocalContentCanAccessFileUrls), 'content.cookies.accept':
'cookies-accept': CookiePolicy(),
CookiePolicy(), 'content.dns_prefetch':
}, Attribute(QWebSettings.DnsPrefetchEnabled),
'network': { 'content.frame_flattening':
'dns-prefetch': Attribute(QWebSettings.FrameFlatteningEnabled),
Attribute(QWebSettings.DnsPrefetchEnabled), 'content.cache.appcache':
}, Attribute(QWebSettings.OfflineWebApplicationCacheEnabled),
'input': { 'content.local_storage':
'spatial-navigation': Attribute(QWebSettings.LocalStorageEnabled,
Attribute(QWebSettings.SpatialNavigationEnabled), QWebSettings.OfflineStorageDatabaseEnabled),
'links-included-in-focus-chain': 'content.cache.maximum_pages':
Attribute(QWebSettings.LinksIncludedInFocusChain), StaticSetter(QWebSettings.setMaximumPagesInCache),
}, 'content.developer_extras':
'fonts': { Attribute(QWebSettings.DeveloperExtrasEnabled),
'web-family-standard': 'content.print_element_backgrounds':
FontFamilySetter(QWebSettings.StandardFont), Attribute(QWebSettings.PrintElementBackgrounds),
'web-family-fixed': 'content.xss_auditing':
FontFamilySetter(QWebSettings.FixedFont), Attribute(QWebSettings.XSSAuditingEnabled),
'web-family-serif': 'content.default_encoding':
FontFamilySetter(QWebSettings.SerifFont), Setter(QWebSettings.setDefaultTextEncoding),
'web-family-sans-serif': # content.user_stylesheets is handled separately
FontFamilySetter(QWebSettings.SansSerifFont),
'web-family-cursive': 'input.spatial_navigation':
FontFamilySetter(QWebSettings.CursiveFont), Attribute(QWebSettings.SpatialNavigationEnabled),
'web-family-fantasy': 'input.links_included_in_focus_chain':
FontFamilySetter(QWebSettings.FantasyFont), Attribute(QWebSettings.LinksIncludedInFocusChain),
'web-size-minimum':
Setter(QWebSettings.setFontSize, 'fonts.web.family.standard':
args=[QWebSettings.MinimumFontSize]), FontFamilySetter(QWebSettings.StandardFont),
'web-size-minimum-logical': 'fonts.web.family.fixed':
Setter(QWebSettings.setFontSize, FontFamilySetter(QWebSettings.FixedFont),
args=[QWebSettings.MinimumLogicalFontSize]), 'fonts.web.family.serif':
'web-size-default': FontFamilySetter(QWebSettings.SerifFont),
Setter(QWebSettings.setFontSize, 'fonts.web.family.sans_serif':
args=[QWebSettings.DefaultFontSize]), FontFamilySetter(QWebSettings.SansSerifFont),
'web-size-default-fixed': 'fonts.web.family.cursive':
Setter(QWebSettings.setFontSize, FontFamilySetter(QWebSettings.CursiveFont),
args=[QWebSettings.DefaultFixedFontSize]), 'fonts.web.family.fantasy':
}, FontFamilySetter(QWebSettings.FantasyFont),
'ui': { 'fonts.web.size.minimum':
'zoom-text-only': Setter(QWebSettings.setFontSize, args=[QWebSettings.MinimumFontSize]),
Attribute(QWebSettings.ZoomTextOnly), 'fonts.web.size.minimum_logical':
'frame-flattening': Setter(QWebSettings.setFontSize,
Attribute(QWebSettings.FrameFlatteningEnabled), args=[QWebSettings.MinimumLogicalFontSize]),
# user-stylesheet is handled separately 'fonts.web.size.default':
'smooth-scrolling': Setter(QWebSettings.setFontSize, args=[QWebSettings.DefaultFontSize]),
Attribute(QWebSettings.ScrollAnimatorEnabled), 'fonts.web.size.default_fixed':
#'accelerated-compositing': Setter(QWebSettings.setFontSize,
# Attribute(QWebSettings.AcceleratedCompositingEnabled), args=[QWebSettings.DefaultFixedFontSize]),
#'tiled-backing-store':
# Attribute(QWebSettings.TiledBackingStoreEnabled), 'zoom.text_only':
}, Attribute(QWebSettings.ZoomTextOnly),
'storage': { 'scrolling.smooth':
'offline-web-application-cache': Attribute(QWebSettings.ScrollAnimatorEnabled),
Attribute(QWebSettings.OfflineWebApplicationCacheEnabled),
'local-storage':
Attribute(QWebSettings.LocalStorageEnabled,
QWebSettings.OfflineStorageDatabaseEnabled),
'maximum-pages-in-cache':
StaticSetter(QWebSettings.setMaximumPagesInCache),
},
'general': {
'developer-extras':
Attribute(QWebSettings.DeveloperExtrasEnabled),
'print-element-backgrounds':
Attribute(QWebSettings.PrintElementBackgrounds),
'xss-auditing':
Attribute(QWebSettings.XSSAuditingEnabled),
'default-encoding':
Setter(QWebSettings.setDefaultTextEncoding),
}
} }

View File

@ -27,25 +27,15 @@ import sip
from PyQt5.QtCore import (pyqtSlot, Qt, QEvent, QUrl, QPoint, QTimer, QSizeF, from PyQt5.QtCore import (pyqtSlot, Qt, QEvent, QUrl, QPoint, QTimer, QSizeF,
QSize) QSize)
from PyQt5.QtGui import QKeyEvent from PyQt5.QtGui import QKeyEvent
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWebKitWidgets import QWebPage, QWebFrame from PyQt5.QtWebKitWidgets import QWebPage, QWebFrame
from PyQt5.QtWebKit import QWebSettings from PyQt5.QtWebKit import QWebSettings
from PyQt5.QtPrintSupport import QPrinter from PyQt5.QtPrintSupport import QPrinter
from qutebrowser.browser import browsertab from qutebrowser.browser import browsertab
from qutebrowser.browser.webkit import webview, tabhistory, webkitelem from qutebrowser.browser.webkit import webview, tabhistory, webkitelem
from qutebrowser.browser.webkit.network import webkitqutescheme
from qutebrowser.utils import qtutils, objreg, usertypes, utils, log, debug from qutebrowser.utils import qtutils, objreg, usertypes, utils, log, debug
def init():
"""Initialize QtWebKit-specific modules."""
qapp = QApplication.instance()
log.init.debug("Initializing js-bridge...")
js_bridge = webkitqutescheme.JSBridge(qapp)
objreg.register('js-bridge', js_bridge)
class WebKitAction(browsertab.AbstractAction): class WebKitAction(browsertab.AbstractAction):
"""QtWebKit implementations related to web actions.""" """QtWebKit implementations related to web actions."""
@ -65,20 +55,14 @@ class WebKitPrinting(browsertab.AbstractPrinting):
"""QtWebKit implementations related to printing.""" """QtWebKit implementations related to printing."""
def _do_check(self):
if not qtutils.check_print_compat():
# WORKAROUND (remove this when we bump the requirements to 5.3.0)
raise browsertab.WebTabError(
"Printing on Qt < 5.3.0 on Windows is broken, please upgrade!")
def check_pdf_support(self): def check_pdf_support(self):
self._do_check() pass
def check_printer_support(self): def check_printer_support(self):
self._do_check() pass
def check_preview_support(self): def check_preview_support(self):
self._do_check() pass
def to_pdf(self, filename): def to_pdf(self, filename):
printer = QPrinter() printer = QPrinter()
@ -133,24 +117,21 @@ class WebKitSearch(browsertab.AbstractSearch):
self._widget.findText('') self._widget.findText('')
self._widget.findText('', QWebPage.HighlightAllOccurrences) self._widget.findText('', QWebPage.HighlightAllOccurrences)
def search(self, text, *, ignore_case=False, reverse=False, def search(self, text, *, ignore_case='never', reverse=False,
result_cb=None): result_cb=None):
self.search_displayed = True self.search_displayed = True
flags = QWebPage.FindWrapsAroundDocument self.text = text
if ignore_case == 'smart': self._flags = QWebPage.FindWrapsAroundDocument
if not text.islower(): if self._is_case_sensitive(ignore_case):
flags |= QWebPage.FindCaseSensitively self._flags |= QWebPage.FindCaseSensitively
elif not ignore_case:
flags |= QWebPage.FindCaseSensitively
if reverse: if reverse:
flags |= QWebPage.FindBackward self._flags |= QWebPage.FindBackward
# We actually search *twice* - once to highlight everything, then again # We actually search *twice* - once to highlight everything, then again
# to get a mark so we can navigate. # to get a mark so we can navigate.
found = self._widget.findText(text, flags) found = self._widget.findText(text, self._flags)
self._widget.findText(text, flags | QWebPage.HighlightAllOccurrences) self._widget.findText(text,
self.text = text self._flags | QWebPage.HighlightAllOccurrences)
self._flags = flags self._call_cb(result_cb, found, text, self._flags, 'search')
self._call_cb(result_cb, found, text, flags, 'search')
def next_result(self, *, result_cb=None): def next_result(self, *, result_cb=None):
self.search_displayed = True self.search_displayed = True

View File

@ -22,7 +22,7 @@
import html import html
import functools import functools
from PyQt5.QtCore import pyqtSlot, pyqtSignal, PYQT_VERSION, Qt, QUrl, QPoint from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QUrl, QPoint
from PyQt5.QtGui import QDesktopServices from PyQt5.QtGui import QDesktopServices
from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest
from PyQt5.QtWidgets import QFileDialog from PyQt5.QtWidgets import QFileDialog
@ -33,8 +33,8 @@ from qutebrowser.config import config
from qutebrowser.browser import pdfjs, shared from qutebrowser.browser import pdfjs, shared
from qutebrowser.browser.webkit import http from qutebrowser.browser.webkit import http
from qutebrowser.browser.webkit.network import networkmanager from qutebrowser.browser.webkit.network import networkmanager
from qutebrowser.utils import (message, usertypes, log, jinja, qtutils, utils, from qutebrowser.utils import (message, usertypes, log, jinja, objreg, debug,
objreg, debug, urlutils) urlutils)
class BrowserPage(QWebPage): class BrowserPage(QWebPage):
@ -87,22 +87,16 @@ class BrowserPage(QWebPage):
self.restoreFrameStateRequested.connect( self.restoreFrameStateRequested.connect(
self.on_restore_frame_state_requested) self.on_restore_frame_state_requested)
if PYQT_VERSION > 0x050300: def javaScriptPrompt(self, frame, js_msg, default):
# WORKAROUND (remove this when we bump the requirements to 5.3.1) """Override javaScriptPrompt to use qutebrowser prompts."""
# We can't override javaScriptPrompt with older PyQt-versions because if self._is_shutting_down:
# of a bug in PyQt. return (False, "")
# See http://www.riverbankcomputing.com/pipermail/pyqt/2014-June/034385.html try:
return shared.javascript_prompt(frame.url(), js_msg, default,
def javaScriptPrompt(self, frame, js_msg, default): abort_on=[self.loadStarted,
"""Override javaScriptPrompt to use qutebrowser prompts.""" self.shutting_down])
if self._is_shutting_down: except shared.CallSuper:
return (False, "") return super().javaScriptPrompt(frame, js_msg, default)
try:
return shared.javascript_prompt(frame.url(), js_msg, default,
abort_on=[self.loadStarted,
self.shutting_down])
except shared.CallSuper:
return super().javaScriptPrompt(frame, js_msg, default)
def _handle_errorpage(self, info, errpage): def _handle_errorpage(self, info, errpage):
"""Display an error page if needed. """Display an error page if needed.
@ -170,7 +164,7 @@ class BrowserPage(QWebPage):
title = "Error loading page: {}".format(urlstr) title = "Error loading page: {}".format(urlstr)
error_html = jinja.render( error_html = jinja.render(
'error.html', 'error.html',
title=title, url=urlstr, error=error_str, icon='') title=title, url=urlstr, error=error_str)
errpage.content = error_html.encode('utf-8') errpage.content = error_html.encode('utf-8')
errpage.encoding = 'utf-8' errpage.encoding = 'utf-8'
return True return True
@ -225,10 +219,6 @@ class BrowserPage(QWebPage):
def on_print_requested(self, frame): def on_print_requested(self, frame):
"""Handle printing when requested via javascript.""" """Handle printing when requested via javascript."""
if not qtutils.check_print_compat():
message.error("Printing on Qt < 5.3.0 on Windows is broken, "
"please upgrade!")
return
printdiag = QPrintDialog() printdiag = QPrintDialog()
printdiag.setAttribute(Qt.WA_DeleteOnClose) printdiag.setAttribute(Qt.WA_DeleteOnClose)
printdiag.open(lambda: frame.print(printdiag.printer())) printdiag.open(lambda: frame.print(printdiag.printer()))
@ -277,7 +267,7 @@ class BrowserPage(QWebPage):
reply.finished.connect(functools.partial( reply.finished.connect(functools.partial(
self.display_content, reply, 'image/jpeg')) self.display_content, reply, 'image/jpeg'))
elif (mimetype in ['application/pdf', 'application/x-pdf'] and elif (mimetype in ['application/pdf', 'application/x-pdf'] and
config.get('content', 'enable-pdfjs')): config.val.content.pdfjs):
# Use pdf.js to display the page # Use pdf.js to display the page
self._show_pdfjs(reply) self._show_pdfjs(reply)
else: else:
@ -304,8 +294,8 @@ class BrowserPage(QWebPage):
return return
options = { options = {
QWebPage.Notifications: ('content', 'notifications'), QWebPage.Notifications: 'content.notifications',
QWebPage.Geolocation: ('content', 'geolocation'), QWebPage.Geolocation: 'content.geolocation',
} }
messages = { messages = {
QWebPage.Notifications: 'show notifications', QWebPage.Notifications: 'show notifications',
@ -350,15 +340,7 @@ class BrowserPage(QWebPage):
frame: The QWebFrame which gets saved. frame: The QWebFrame which gets saved.
item: The QWebHistoryItem to be saved. item: The QWebHistoryItem to be saved.
""" """
try: if frame != self.mainFrame():
if frame != self.mainFrame():
return
except RuntimeError:
# With Qt 5.2.1 (Ubuntu Trusty) we get this when closing a tab:
# RuntimeError: wrapped C/C++ object of type BrowserPage has
# been deleted
# Since the information here isn't that important for closing web
# views anyways, we ignore this error.
return return
data = { data = {
'zoom': frame.zoomFactor(), 'zoom': frame.zoomFactor(),
@ -384,7 +366,7 @@ class BrowserPage(QWebPage):
def userAgentForUrl(self, url): def userAgentForUrl(self, url):
"""Override QWebPage::userAgentForUrl to customize the user agent.""" """Override QWebPage::userAgentForUrl to customize the user agent."""
ua = config.get('network', 'user-agent') ua = config.val.content.headers.user_agent
if ua is None: if ua is None:
return super().userAgentForUrl(url) return super().userAgentForUrl(url)
else: else:
@ -401,9 +383,6 @@ class BrowserPage(QWebPage):
""" """
return ext in self._extension_handlers return ext in self._extension_handlers
# WORKAROUND for:
# http://www.riverbankcomputing.com/pipermail/pyqt/2014-August/034722.html
@utils.prevent_exceptions(False, PYQT_VERSION < 0x50302)
def extension(self, ext, opt, out): def extension(self, ext, opt, out):
"""Override QWebPage::extension to provide error pages. """Override QWebPage::extension to provide error pages.
@ -446,15 +425,8 @@ class BrowserPage(QWebPage):
def javaScriptConsoleMessage(self, msg, line, source): def javaScriptConsoleMessage(self, msg, line, source):
"""Override javaScriptConsoleMessage to use debug log.""" """Override javaScriptConsoleMessage to use debug log."""
log_javascript_console = config.get('general', shared.javascript_log_message(usertypes.JsLogLevel.unknown,
'log-javascript-console') source, line, msg)
logstring = "[{}:{}] {}".format(source, line, msg)
logmap = {
'debug': log.js.debug,
'info': log.js.info,
'none': lambda arg: None
}
logmap[log_javascript_console](logstring)
def acceptNavigationRequest(self, def acceptNavigationRequest(self,
_frame: QWebFrame, _frame: QWebFrame,

View File

@ -25,11 +25,11 @@ from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QUrl
from PyQt5.QtGui import QPalette from PyQt5.QtGui import QPalette
from PyQt5.QtWidgets import QStyleFactory from PyQt5.QtWidgets import QStyleFactory
from PyQt5.QtWebKit import QWebSettings from PyQt5.QtWebKit import QWebSettings
from PyQt5.QtWebKitWidgets import QWebView, QWebPage, QWebFrame from PyQt5.QtWebKitWidgets import QWebView, QWebPage
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.keyinput import modeman from qutebrowser.keyinput import modeman
from qutebrowser.utils import log, usertypes, utils, qtutils, objreg, debug from qutebrowser.utils import log, usertypes, utils, objreg, debug
from qutebrowser.browser.webkit import webpage from qutebrowser.browser.webkit import webpage
@ -57,7 +57,7 @@ class WebView(QWebView):
def __init__(self, *, win_id, tab_id, tab, private, parent=None): def __init__(self, *, win_id, tab_id, tab, private, parent=None):
super().__init__(parent) super().__init__(parent)
if sys.platform == 'darwin' and qtutils.version_check('5.4'): if sys.platform == 'darwin':
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-42948 # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-42948
# See https://github.com/qutebrowser/qutebrowser/issues/462 # See https://github.com/qutebrowser/qutebrowser/issues/462
self.setStyle(QStyleFactory.create('Fusion')) self.setStyle(QStyleFactory.create('Fusion'))
@ -74,13 +74,9 @@ class WebView(QWebView):
page = webpage.BrowserPage(win_id=self.win_id, tab_id=self._tab_id, page = webpage.BrowserPage(win_id=self.win_id, tab_id=self._tab_id,
tabdata=tab.data, private=private, tabdata=tab.data, private=private,
parent=self) parent=self)
page.setVisibilityState(
try: QWebPage.VisibilityStateVisible if self.isVisible()
page.setVisibilityState( else QWebPage.VisibilityStateHidden)
QWebPage.VisibilityStateVisible if self.isVisible()
else QWebPage.VisibilityStateHidden)
except AttributeError:
pass
self.setPage(page) self.setPage(page)
@ -88,7 +84,7 @@ class WebView(QWebView):
window=win_id) window=win_id)
mode_manager.entered.connect(self.on_mode_entered) mode_manager.entered.connect(self.on_mode_entered)
mode_manager.left.connect(self.on_mode_left) mode_manager.left.connect(self.on_mode_left)
objreg.get('config').changed.connect(self._set_bg_color) config.instance.changed.connect(self._set_bg_color)
def __repr__(self): def __repr__(self):
url = utils.elide(self.url().toDisplayString(QUrl.EncodeUnicode), 100) url = utils.elide(self.url().toDisplayString(QUrl.EncodeUnicode), 100)
@ -107,10 +103,10 @@ class WebView(QWebView):
# deleted # deleted
pass pass
@config.change_filter('colors', 'webpage.bg') @config.change_filter('colors.webpage.bg')
def _set_bg_color(self): def _set_bg_color(self):
"""Set the webpage background color as configured.""" """Set the webpage background color as configured."""
col = config.get('colors', 'webpage.bg') col = config.val.colors.webpage.bg
palette = self.palette() palette = self.palette()
if col is None: if col is None:
col = self.style().standardPalette().color(QPalette.Base) col = self.style().standardPalette().color(QPalette.Base)
@ -135,22 +131,6 @@ class WebView(QWebView):
url: The URL to load as QUrl url: The URL to load as QUrl
""" """
self.load(url) self.load(url)
if url.scheme() == 'qute':
frame = self.page().mainFrame()
frame.javaScriptWindowObjectCleared.connect(self.add_js_bridge)
@pyqtSlot()
def add_js_bridge(self):
"""Add the javascript bridge for qute://... pages."""
frame = self.sender()
if not isinstance(frame, QWebFrame):
log.webview.error("Got non-QWebFrame {!r} in "
"add_js_bridge!".format(frame))
return
if frame.url().scheme() == 'qute':
bridge = objreg.get('js-bridge')
frame.addToJavaScriptWindowObject('qute', bridge)
@pyqtSlot(usertypes.KeyMode) @pyqtSlot(usertypes.KeyMode)
def on_mode_entered(self, mode): def on_mode_entered(self, mode):
@ -256,12 +236,8 @@ class WebView(QWebView):
Return: Return:
The superclass event return value. The superclass event return value.
""" """
try:
self.page().setVisibilityState(QWebPage.VisibilityStateVisible)
except AttributeError:
pass
super().showEvent(e) super().showEvent(e)
self.page().setVisibilityState(QWebPage.VisibilityStateVisible)
def hideEvent(self, e): def hideEvent(self, e):
"""Extend hideEvent to set the page visibility state to hidden. """Extend hideEvent to set the page visibility state to hidden.
@ -272,12 +248,8 @@ class WebView(QWebView):
Return: Return:
The superclass event return value. The superclass event return value.
""" """
try:
self.page().setVisibilityState(QWebPage.VisibilityStateHidden)
except AttributeError:
pass
super().hideEvent(e) super().hideEvent(e)
self.page().setVisibilityState(QWebPage.VisibilityStateHidden)
def mousePressEvent(self, e): def mousePressEvent(self, e):
"""Set the tabdata ClickTarget on a mousepress. """Set the tabdata ClickTarget on a mousepress.
@ -285,10 +257,10 @@ class WebView(QWebView):
This is implemented here as we don't need it for QtWebEngine. This is implemented here as we don't need it for QtWebEngine.
""" """
if e.button() == Qt.MidButton or e.modifiers() & Qt.ControlModifier: if e.button() == Qt.MidButton or e.modifiers() & Qt.ControlModifier:
background_tabs = config.get('tabs', 'background-tabs') background = config.val.tabs.background
if e.modifiers() & Qt.ShiftModifier: if e.modifiers() & Qt.ShiftModifier:
background_tabs = not background_tabs background = not background
if background_tabs: if background:
target = usertypes.ClickTarget.tab_bg target = usertypes.ClickTarget.tab_bg
else: else:
target = usertypes.ClickTarget.tab target = usertypes.ClickTarget.tab

View File

@ -23,33 +23,33 @@ Defined here to avoid circular dependency hell.
""" """
class CommandError(Exception): class Error(Exception):
"""Base class for all cmdexc errors."""
class CommandError(Error):
"""Raised when a command encounters an error while running.""" """Raised when a command encounters an error while running."""
pass pass
class CommandMetaError(Exception): class NoSuchCommandError(Error):
"""Common base class for exceptions occurring before a command is run."""
class NoSuchCommandError(CommandMetaError):
"""Raised when a command wasn't found.""" """Raised when a command wasn't found."""
pass pass
class ArgumentTypeError(CommandMetaError): class ArgumentTypeError(Error):
"""Raised when an argument had an invalid type.""" """Raised when an argument had an invalid type."""
pass pass
class PrerequisitesError(CommandMetaError): class PrerequisitesError(Error):
"""Raised when a cmd can't be used because some prerequisites aren't met. """Raised when a cmd can't be used because some prerequisites aren't met.

View File

@ -21,7 +21,6 @@
Module attributes: Module attributes:
cmd_dict: A mapping from command-strings to command objects. cmd_dict: A mapping from command-strings to command objects.
aliases: A list of all aliases, needed for doc generation.
""" """
import inspect import inspect
@ -30,7 +29,6 @@ from qutebrowser.utils import qtutils, log
from qutebrowser.commands import command, cmdexc from qutebrowser.commands import command, cmdexc
cmd_dict = {} cmd_dict = {}
aliases = []
def check_overflow(arg, ctype): def check_overflow(arg, ctype):
@ -88,28 +86,6 @@ class register: # pylint: disable=invalid-name
self._name = name self._name = name
self._kwargs = kwargs self._kwargs = kwargs
def _get_names(self, func):
"""Get the name(s) which should be used for the current command.
If the name hasn't been overridden explicitly, the function name is
transformed.
If it has been set, it can either be a string which is
used directly, or an iterable.
Args:
func: The function to get the name of.
Return:
A list of names, with the main name being the first item.
"""
if self._name is None:
return [func.__name__.lower().replace('_', '-')]
elif isinstance(self._name, str):
return [self._name]
else:
return self._name
def __call__(self, func): def __call__(self, func):
"""Register the command before running the function. """Register the command before running the function.
@ -124,17 +100,17 @@ class register: # pylint: disable=invalid-name
Return: Return:
The original function (unmodified). The original function (unmodified).
""" """
global aliases if self._name is None:
names = self._get_names(func) name = func.__name__.lower().replace('_', '-')
log.commands.vdebug("Registering command {}".format(names[0])) else:
for name in names: assert isinstance(self._name, str), self._name
if name in cmd_dict: name = self._name
raise ValueError("{} is already registered!".format(name)) log.commands.vdebug("Registering command {}".format(name))
cmd = command.Command(name=names[0], instance=self._instance, if name in cmd_dict:
raise ValueError("{} is already registered!".format(name))
cmd = command.Command(name=name, instance=self._instance,
handler=func, **self._kwargs) handler=func, **self._kwargs)
for name in names: cmd_dict[name] = cmd
cmd_dict[name] = cmd
aliases += names[1:]
return func return func

View File

@ -22,44 +22,34 @@
import inspect import inspect
import collections import collections
import traceback import traceback
import typing
import attr
from qutebrowser.commands import cmdexc, argparser from qutebrowser.commands import cmdexc, argparser
from qutebrowser.utils import (log, utils, message, docutils, objreg, from qutebrowser.utils import log, message, docutils, objreg, usertypes
usertypes, typing)
from qutebrowser.utils import debug as debug_utils from qutebrowser.utils import debug as debug_utils
from qutebrowser.misc import objects from qutebrowser.misc import objects
@attr.s
class ArgInfo: class ArgInfo:
"""Information about an argument.""" """Information about an argument."""
def __init__(self, win_id=False, count=False, hide=False, metavar=None, win_id = attr.ib(False)
flag=None, completion=None, choices=None): count = attr.ib(False)
if win_id and count: hide = attr.ib(False)
metavar = attr.ib(None)
flag = attr.ib(None)
completion = attr.ib(None)
choices = attr.ib(None)
@win_id.validator
@count.validator
def _validate_exclusive(self, _attr, _value):
if self.win_id and self.count:
raise TypeError("Argument marked as both count/win_id!") raise TypeError("Argument marked as both count/win_id!")
self.win_id = win_id
self.count = count
self.flag = flag
self.hide = hide
self.metavar = metavar
self.completion = completion
self.choices = choices
def __eq__(self, other):
return (self.win_id == other.win_id and
self.count == other.count and
self.flag == other.flag and
self.hide == other.hide and
self.metavar == other.metavar and
self.completion == other.completion and
self.choices == other.choices)
def __repr__(self):
return utils.get_repr(self, win_id=self.win_id, count=self.count,
flag=self.flag, hide=self.hide,
metavar=self.metavar, completion=self.completion,
choices=self.choices, constructor=True)
class Command: class Command:
@ -90,7 +80,7 @@ class Command:
def __init__(self, *, handler, name, instance=None, maxsplit=None, def __init__(self, *, handler, name, instance=None, maxsplit=None,
hide=False, modes=None, not_modes=None, debug=False, hide=False, modes=None, not_modes=None, debug=False,
ignore_args=False, deprecated=False, no_cmd_split=False, deprecated=False, no_cmd_split=False,
star_args_optional=False, scope='global', backend=None, star_args_optional=False, scope='global', backend=None,
no_replace_variables=False): no_replace_variables=False):
# I really don't know how to solve this in a better way, I tried. # I really don't know how to solve this in a better way, I tried.
@ -121,7 +111,6 @@ class Command:
self._scope = scope self._scope = scope
self._star_args_optional = star_args_optional self._star_args_optional = star_args_optional
self.debug = debug self.debug = debug
self.ignore_args = ignore_args
self.handler = handler self.handler = handler
self.no_cmd_split = no_cmd_split self.no_cmd_split = no_cmd_split
self.backend = backend self.backend = backend
@ -225,33 +214,31 @@ class Command:
else: else:
self.desc = "" self.desc = ""
if not self.ignore_args: for param in signature.parameters.values():
for param in signature.parameters.values(): # https://docs.python.org/3/library/inspect.html#inspect.Parameter.kind
# https://docs.python.org/3/library/inspect.html#inspect.Parameter.kind # "Python has no explicit syntax for defining positional-only
# "Python has no explicit syntax for defining positional-only # parameters, but many built-in and extension module functions
# parameters, but many built-in and extension module functions # (especially those that accept only one or two parameters) accept
# (especially those that accept only one or two parameters) # them."
# accept them." assert param.kind != inspect.Parameter.POSITIONAL_ONLY
assert param.kind != inspect.Parameter.POSITIONAL_ONLY if param.name == 'self':
if param.name == 'self': continue
continue if self._inspect_special_param(param):
if self._inspect_special_param(param): continue
continue if (param.kind == inspect.Parameter.KEYWORD_ONLY and
if (param.kind == inspect.Parameter.KEYWORD_ONLY and param.default is inspect.Parameter.empty):
param.default is inspect.Parameter.empty): raise TypeError("{}: handler has keyword only argument {!r} "
raise TypeError("{}: handler has keyword only argument " "without default!".format(
"{!r} without default!".format(self.name, self.name, param.name))
param.name)) typ = self._get_type(param)
typ = self._get_type(param) is_bool = typ is bool
is_bool = typ is bool kwargs = self._param_to_argparse_kwargs(param, is_bool)
kwargs = self._param_to_argparse_kwargs(param, is_bool) args = self._param_to_argparse_args(param, is_bool)
args = self._param_to_argparse_args(param, is_bool) callsig = debug_utils.format_call(self.parser.add_argument, args,
callsig = debug_utils.format_call( kwargs, full=False)
self.parser.add_argument, args, kwargs, log.commands.vdebug('Adding arg {} of type {} -> {}'.format(
full=False) param.name, typ, callsig))
log.commands.vdebug('Adding arg {} of type {} -> {}'.format( self.parser.add_argument(*args, **kwargs)
param.name, typ, callsig))
self.parser.add_argument(*args, **kwargs)
return signature.parameters.values() return signature.parameters.values()
def _param_to_argparse_kwargs(self, param, is_bool): def _param_to_argparse_kwargs(self, param, is_bool):
@ -419,9 +406,10 @@ class Command:
# support that. # support that.
# pylint: disable=no-member,useless-suppression # pylint: disable=no-member,useless-suppression
try: try:
types = list(typ.__union_params__)
except AttributeError:
types = list(typ.__args__) types = list(typ.__args__)
except AttributeError:
# Older Python 3.5 patch versions
types = list(typ.__union_params__)
# pylint: enable=no-member,useless-suppression # pylint: enable=no-member,useless-suppression
if param.default is not inspect.Parameter.empty: if param.default is not inspect.Parameter.empty:
types.append(type(param.default)) types.append(type(param.default))
@ -453,12 +441,6 @@ class Command:
kwargs = {} kwargs = {}
signature = inspect.signature(self.handler) signature = inspect.signature(self.handler)
if self.ignore_args:
if self._instance is not None:
param = list(signature.parameters.values())[0]
self._get_self_arg(win_id, param, args)
return args, kwargs
for i, param in enumerate(signature.parameters.values()): for i, param in enumerate(signature.parameters.values()):
arg_info = self.get_arg_info(param) arg_info = self.get_arg_info(param)
if i == 0 and self._instance is not None: if i == 0 and self._instance is not None:

View File

@ -19,22 +19,31 @@
"""Module containing command managers (SearchRunner and CommandRunner).""" """Module containing command managers (SearchRunner and CommandRunner)."""
import collections
import traceback import traceback
import re import re
import attr
from PyQt5.QtCore import pyqtSlot, QUrl, QObject from PyQt5.QtCore import pyqtSlot, QUrl, QObject
from qutebrowser.config import config, configexc from qutebrowser.config import config
from qutebrowser.commands import cmdexc, cmdutils from qutebrowser.commands import cmdexc, cmdutils
from qutebrowser.utils import message, objreg, qtutils, usertypes, utils from qutebrowser.utils import message, objreg, qtutils, usertypes, utils
from qutebrowser.misc import split from qutebrowser.misc import split
ParseResult = collections.namedtuple('ParseResult', ['cmd', 'args', 'cmdline'])
last_command = {} last_command = {}
@attr.s
class ParseResult:
"""The result of parsing a commandline."""
cmd = attr.ib()
args = attr.ib()
cmdline = attr.ib()
def _current_url(tabbed_browser): def _current_url(tabbed_browser):
"""Convenience method to get the current url.""" """Convenience method to get the current url."""
try: try:
@ -81,19 +90,17 @@ def replace_variables(win_id, arglist):
return args return args
class CommandRunner(QObject): class CommandParser:
"""Parse and run qutebrowser commandline commands. """Parse qutebrowser commandline commands.
Attributes: Attributes:
_win_id: The window this CommandRunner is associated with.
_partial_match: Whether to allow partial command matches. _partial_match: Whether to allow partial command matches.
""" """
def __init__(self, win_id, partial_match=False, parent=None): def __init__(self, partial_match=False):
super().__init__(parent)
self._partial_match = partial_match self._partial_match = partial_match
self._win_id = win_id
def _get_alias(self, text, default=None): def _get_alias(self, text, default=None):
"""Get an alias from the config. """Get an alias from the config.
@ -108,9 +115,10 @@ class CommandRunner(QObject):
""" """
parts = text.strip().split(maxsplit=1) parts = text.strip().split(maxsplit=1)
try: try:
alias = config.get('aliases', parts[0]) alias = config.val.aliases[parts[0]]
except (configexc.NoOptionError, configexc.NoSectionError): except KeyError:
return default return default
try: try:
new_cmd = '{} {}'.format(alias, parts[1]) new_cmd = '{} {}'.format(alias, parts[1])
except IndexError: except IndexError:
@ -119,7 +127,7 @@ class CommandRunner(QObject):
new_cmd += ' ' new_cmd += ' '
return new_cmd return new_cmd
def parse_all(self, text, aliases=True, *args, **kwargs): def _parse_all_gen(self, text, aliases=True, *args, **kwargs):
"""Split a command on ;; and parse all parts. """Split a command on ;; and parse all parts.
If the first command in the commandline is a non-split one, it only If the first command in the commandline is a non-split one, it only
@ -154,6 +162,10 @@ class CommandRunner(QObject):
for sub in sub_texts: for sub in sub_texts:
yield self.parse(sub, *args, **kwargs) yield self.parse(sub, *args, **kwargs)
def parse_all(self, *args, **kwargs):
"""Wrapper over parse_all."""
return list(self._parse_all_gen(*args, **kwargs))
def parse(self, text, *, fallback=False, keep=False): def parse(self, text, *, fallback=False, keep=False):
"""Split the commandline text into command and arguments. """Split the commandline text into command and arguments.
@ -253,6 +265,20 @@ class CommandRunner(QObject):
# already. # already.
return split_args return split_args
class CommandRunner(QObject):
"""Parse and run qutebrowser commandline commands.
Attributes:
_win_id: The window this CommandRunner is associated with.
"""
def __init__(self, win_id, partial_match=False, parent=None):
super().__init__(parent)
self._parser = CommandParser(partial_match=partial_match)
self._win_id = win_id
def run(self, text, count=None): def run(self, text, count=None):
"""Parse a command from a line of text and run it. """Parse a command from a line of text and run it.
@ -267,7 +293,7 @@ class CommandRunner(QObject):
window=self._win_id) window=self._win_id)
cur_mode = mode_manager.mode cur_mode = mode_manager.mode
for result in self.parse_all(text): for result in self._parser.parse_all(text):
if result.cmd.no_replace_variables: if result.cmd.no_replace_variables:
args = result.args args = result.args
else: else:
@ -294,7 +320,7 @@ class CommandRunner(QObject):
"""Run a command and display exceptions in the statusbar.""" """Run a command and display exceptions in the statusbar."""
try: try:
self.run(text, count) self.run(text, count)
except (cmdexc.CommandMetaError, cmdexc.CommandError) as e: except cmdexc.Error as e:
message.error(str(e), stack=traceback.format_exc()) message.error(str(e), stack=traceback.format_exc())
@pyqtSlot(str, int) @pyqtSlot(str, int)
@ -306,5 +332,5 @@ class CommandRunner(QObject):
""" """
try: try:
self.run(text, count) self.run(text, count)
except (cmdexc.CommandMetaError, cmdexc.CommandError) as e: except cmdexc.Error as e:
message.error(str(e), stack=traceback.format_exc()) message.error(str(e), stack=traceback.format_exc())

View File

@ -376,7 +376,7 @@ def _lookup_path(cmd):
""" """
directories = [ directories = [
os.path.join(standarddir.data(), "userscripts"), os.path.join(standarddir.data(), "userscripts"),
os.path.join(standarddir.system_data(), "userscripts"), os.path.join(standarddir.data(system=True), "userscripts"),
] ]
for directory in directories: for directory in directories:
cmd_path = os.path.join(directory, cmd) cmd_path = os.path.join(directory, cmd)
@ -417,7 +417,7 @@ def run_async(tab, cmd, *args, win_id, env, verbose=False):
lambda cmd: lambda cmd:
log.commands.debug("Got userscript command: {}".format(cmd))) log.commands.debug("Got userscript command: {}".format(cmd)))
runner.got_cmd.connect(commandrunner.run_safely) runner.got_cmd.connect(commandrunner.run_safely)
user_agent = config.get('network', 'user-agent') user_agent = config.val.content.headers.user_agent
if user_agent is not None: if user_agent is not None:
env['QUTE_USER_AGENT'] = user_agent env['QUTE_USER_AGENT'] = user_agent

View File

@ -19,6 +19,7 @@
"""Completer attached to a CompletionView.""" """Completer attached to a CompletionView."""
import attr
from PyQt5.QtCore import pyqtSlot, QObject, QTimer from PyQt5.QtCore import pyqtSlot, QObject, QTimer
from qutebrowser.config import config from qutebrowser.config import config
@ -27,6 +28,15 @@ from qutebrowser.utils import log, utils, debug
from qutebrowser.completion.models import miscmodels from qutebrowser.completion.models import miscmodels
@attr.s
class CompletionInfo:
"""Context passed into all completion functions."""
config = attr.ib()
keyconf = attr.ib()
class Completer(QObject): class Completer(QObject):
"""Completer which manages completions in a CompletionView. """Completer which manages completions in a CompletionView.
@ -34,7 +44,6 @@ class Completer(QObject):
Attributes: Attributes:
_cmd: The statusbar Command object this completer belongs to. _cmd: The statusbar Command object this completer belongs to.
_ignore_change: Whether to ignore the next completion update. _ignore_change: Whether to ignore the next completion update.
_win_id: The window ID this completer is in.
_timer: The timer used to trigger the completion update. _timer: The timer used to trigger the completion update.
_last_cursor_pos: The old cursor position so we avoid double completion _last_cursor_pos: The old cursor position so we avoid double completion
updates. updates.
@ -42,9 +51,8 @@ class Completer(QObject):
_last_completion_func: The completion function used for the last text. _last_completion_func: The completion function used for the last text.
""" """
def __init__(self, cmd, win_id, parent=None): def __init__(self, cmd, parent=None):
super().__init__(parent) super().__init__(parent)
self._win_id = win_id
self._cmd = cmd self._cmd = cmd
self._ignore_change = False self._ignore_change = False
self._timer = QTimer() self._timer = QTimer()
@ -106,7 +114,7 @@ class Completer(QObject):
""" """
if not s: if not s:
return "''" return "''"
elif any(c in s for c in ' \'\t\n\\'): elif any(c in s for c in ' "\'\t\n\\'):
# use single quotes, and put single quotes into double quotes # use single quotes, and put single quotes into double quotes
# the string $'b is then quoted as '$'"'"'b' # the string $'b is then quoted as '$'"'"'b'
return "'" + s.replace("'", "'\"'\"'") + "'" return "'" + s.replace("'", "'\"'\"'") + "'"
@ -123,9 +131,11 @@ class Completer(QObject):
if not text or not text.strip(): if not text or not text.strip():
# Only ":", empty part under the cursor with nothing before/after # Only ":", empty part under the cursor with nothing before/after
return [], '', [] return [], '', []
runner = runners.CommandRunner(self._win_id) parser = runners.CommandParser()
result = runner.parse(text, fallback=True, keep=True) result = parser.parse(text, fallback=True, keep=True)
# pylint: disable=not-an-iterable
parts = [x for x in result.cmdline if x] parts = [x for x in result.cmdline if x]
# pylint: enable=not-an-iterable
pos = self._cmd.cursorPosition() - len(self._cmd.prefix()) pos = self._cmd.cursorPosition() - len(self._cmd.prefix())
pos = min(pos, len(text)) # Qt treats 2-byte UTF-16 chars as 2 chars pos = min(pos, len(text)) # Qt treats 2-byte UTF-16 chars as 2 chars
log.completion.debug('partitioning {} around position {}'.format(parts, log.completion.debug('partitioning {} around position {}'.format(parts,
@ -164,7 +174,7 @@ class Completer(QObject):
if maxsplit is None: if maxsplit is None:
text = self._quote(text) text = self._quote(text)
model = self._model() model = self._model()
if model.count() == 1 and config.get('completion', 'quick-complete'): if model.count() == 1 and config.val.completion.quick:
# If we only have one item, we want to apply it immediately # If we only have one item, we want to apply it immediately
# and go on to the next part. # and go on to the next part.
self._change_completed_part(text, before, after, immediate=True) self._change_completed_part(text, before, after, immediate=True)
@ -233,7 +243,9 @@ class Completer(QObject):
args = (x for x in before_cursor[1:] if not x.startswith('-')) args = (x for x in before_cursor[1:] if not x.startswith('-'))
with debug.log_time(log.completion, with debug.log_time(log.completion,
'Starting {} completion'.format(func.__name__)): 'Starting {} completion'.format(func.__name__)):
model = func(*args) info = CompletionInfo(config=config.instance,
keyconf=config.key_instance)
model = func(*args, info=info)
with debug.log_time(log.completion, 'Set completion model'): with debug.log_time(log.completion, 'Set completion model'):
completion.set_model(model) completion.set_model(model)

View File

@ -30,8 +30,8 @@ from PyQt5.QtCore import QRectF, QSize, Qt
from PyQt5.QtGui import (QIcon, QPalette, QTextDocument, QTextOption, from PyQt5.QtGui import (QIcon, QPalette, QTextDocument, QTextOption,
QAbstractTextDocumentLayout) QAbstractTextDocumentLayout)
from qutebrowser.config import config, configexc, style from qutebrowser.config import config
from qutebrowser.utils import qtutils from qutebrowser.utils import qtutils, jinja
class CompletionItemDelegate(QStyledItemDelegate): class CompletionItemDelegate(QStyledItemDelegate):
@ -147,16 +147,15 @@ class CompletionItemDelegate(QStyledItemDelegate):
# We can't use drawContents because then the color would be ignored. # We can't use drawContents because then the color would be ignored.
clip = QRectF(0, 0, rect.width(), rect.height()) clip = QRectF(0, 0, rect.width(), rect.height())
self._painter.save() self._painter.save()
if self._opt.state & QStyle.State_Selected: if self._opt.state & QStyle.State_Selected:
option = 'completion.item.selected.fg' color = config.val.colors.completion.item.selected.fg
elif not self._opt.state & QStyle.State_Enabled: elif not self._opt.state & QStyle.State_Enabled:
option = 'completion.category.fg' color = config.val.colors.completion.category.fg
else: else:
option = 'completion.fg' color = config.val.colors.completion.fg
try: self._painter.setPen(color)
self._painter.setPen(config.get('colors', option))
except configexc.NoOptionError:
self._painter.setPen(config.get('colors', 'completion.fg'))
ctx = QAbstractTextDocumentLayout.PaintContext() ctx = QAbstractTextDocumentLayout.PaintContext()
ctx.palette.setColor(QPalette.Text, self._painter.pen().color()) ctx.palette.setColor(QPalette.Text, self._painter.pen().color())
if clip.isValid(): if clip.isValid():
@ -188,13 +187,17 @@ class CompletionItemDelegate(QStyledItemDelegate):
self._doc = QTextDocument(self) self._doc = QTextDocument(self)
self._doc.setDefaultFont(self._opt.font) self._doc.setDefaultFont(self._opt.font)
self._doc.setDefaultTextOption(text_option) self._doc.setDefaultTextOption(text_option)
self._doc.setDefaultStyleSheet(style.get_stylesheet("""
.highlight {
color: {{ color['completion.match.fg'] }};
}
"""))
self._doc.setDocumentMargin(2) self._doc.setDocumentMargin(2)
stylesheet = """
.highlight {
color: {{ conf.colors.completion.match.fg }};
}
"""
with jinja.environment.no_autoescape():
template = jinja.environment.from_string(stylesheet)
self._doc.setDefaultStyleSheet(template.render(conf=config.val))
if index.parent().isValid(): if index.parent().isValid():
view = self.parent() view = self.parent()
pattern = view.pattern pattern = view.pattern
@ -209,7 +212,7 @@ class CompletionItemDelegate(QStyledItemDelegate):
else: else:
self._doc.setHtml( self._doc.setHtml(
'<span style="font: {};">{}</span>'.format( '<span style="font: {};">{}</span>'.format(
html.escape(config.get('fonts', 'completion.category')), html.escape(config.val.fonts.completion.category),
html.escape(self._opt.text))) html.escape(self._opt.text)))
def _draw_focus_rect(self): def _draw_focus_rect(self):

View File

@ -26,9 +26,9 @@ subclasses to provide completions.
from PyQt5.QtWidgets import QStyle, QTreeView, QSizePolicy, QStyleFactory from PyQt5.QtWidgets import QStyle, QTreeView, QSizePolicy, QStyleFactory
from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QItemSelectionModel, QSize from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QItemSelectionModel, QSize
from qutebrowser.config import config, style from qutebrowser.config import config
from qutebrowser.completion import completiondelegate from qutebrowser.completion import completiondelegate
from qutebrowser.utils import utils, usertypes, objreg, debug, log from qutebrowser.utils import utils, usertypes, debug, log
from qutebrowser.commands import cmdexc, cmdutils from qutebrowser.commands import cmdexc, cmdutils
@ -57,27 +57,27 @@ class CompletionView(QTreeView):
# don't define that in this stylesheet. # don't define that in this stylesheet.
STYLESHEET = """ STYLESHEET = """
QTreeView { QTreeView {
font: {{ font['completion'] }}; font: {{ conf.fonts.completion.entry }};
background-color: {{ color['completion.bg'] }}; background-color: {{ conf.colors.completion.even.bg }};
alternate-background-color: {{ color['completion.alternate-bg'] }}; alternate-background-color: {{ conf.colors.completion.odd.bg }};
outline: 0; outline: 0;
border: 0px; border: 0px;
} }
QTreeView::item:disabled { QTreeView::item:disabled {
background-color: {{ color['completion.category.bg'] }}; background-color: {{ conf.colors.completion.category.bg }};
border-top: 1px solid border-top: 1px solid
{{ color['completion.category.border.top'] }}; {{ conf.colors.completion.category.border.top }};
border-bottom: 1px solid border-bottom: 1px solid
{{ color['completion.category.border.bottom'] }}; {{ conf.colors.completion.category.border.bottom }};
} }
QTreeView::item:selected, QTreeView::item:selected:hover { QTreeView::item:selected, QTreeView::item:selected:hover {
border-top: 1px solid border-top: 1px solid
{{ color['completion.item.selected.border.top'] }}; {{ conf.colors.completion.item.selected.border.top }};
border-bottom: 1px solid border-bottom: 1px solid
{{ color['completion.item.selected.border.bottom'] }}; {{ conf.colors.completion.item.selected.border.bottom }};
background-color: {{ color['completion.item.selected.bg'] }}; background-color: {{ conf.colors.completion.item.selected.bg }};
} }
QTreeView:item::hover { QTreeView:item::hover {
@ -85,14 +85,14 @@ class CompletionView(QTreeView):
} }
QTreeView QScrollBar { QTreeView QScrollBar {
width: {{ config.get('completion', 'scrollbar-width') }}px; width: {{ conf.completion.scrollbar.width }}px;
background: {{ color['completion.scrollbar.bg'] }}; background: {{ conf.colors.completion.scrollbar.bg }};
} }
QTreeView QScrollBar::handle { QTreeView QScrollBar::handle {
background: {{ color['completion.scrollbar.fg'] }}; background: {{ conf.colors.completion.scrollbar.fg }};
border: {{ config.get('completion', 'scrollbar-padding') }}px solid border: {{ conf.completion.scrollbar.padding }}px solid
{{ color['completion.scrollbar.bg'] }}; {{ conf.colors.completion.scrollbar.bg }};
min-height: 10px; min-height: 10px;
} }
@ -109,14 +109,14 @@ class CompletionView(QTreeView):
super().__init__(parent) super().__init__(parent)
self.pattern = '' self.pattern = ''
self._win_id = win_id self._win_id = win_id
objreg.get('config').changed.connect(self._on_config_changed) config.instance.changed.connect(self._on_config_changed)
self._active = False self._active = False
self._delegate = completiondelegate.CompletionItemDelegate(self) self._delegate = completiondelegate.CompletionItemDelegate(self)
self.setItemDelegate(self._delegate) self.setItemDelegate(self._delegate)
self.setStyle(QStyleFactory.create('Fusion')) self.setStyle(QStyleFactory.create('Fusion'))
style.set_register_stylesheet(self) config.set_register_stylesheet(self)
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self.setHeaderHidden(True) self.setHeaderHidden(True)
self.setAlternatingRowColors(True) self.setAlternatingRowColors(True)
@ -139,11 +139,9 @@ class CompletionView(QTreeView):
def __repr__(self): def __repr__(self):
return utils.get_repr(self) return utils.get_repr(self)
@pyqtSlot(str, str) @pyqtSlot(str)
def _on_config_changed(self, section, option): def _on_config_changed(self, option):
if section != 'completion': if option in ['completion.height', 'completion.shrink']:
return
if option in ['height', 'shrink']:
self.update_geometry.emit() self.update_geometry.emit()
def _resize_columns(self): def _resize_columns(self):
@ -262,9 +260,9 @@ class CompletionView(QTreeView):
count = self.model().count() count = self.model().count()
if count == 0: if count == 0:
self.hide() self.hide()
elif count == 1 and config.get('completion', 'quick-complete'): elif count == 1 and config.val.completion.quick:
self.hide() self.hide()
elif config.get('completion', 'show') == 'auto': elif config.val.completion.show == 'auto':
self.show() self.show()
def set_model(self, model): def set_model(self, model):
@ -306,7 +304,7 @@ class CompletionView(QTreeView):
self._maybe_show() self._maybe_show()
def _maybe_show(self): def _maybe_show(self):
if (config.get('completion', 'show') == 'always' and if (config.val.completion.show == 'always' and
self.model().count() > 0): self.model().count() > 0):
self.show() self.show()
else: else:
@ -314,7 +312,7 @@ class CompletionView(QTreeView):
def _maybe_update_geometry(self): def _maybe_update_geometry(self):
"""Emit the update_geometry signal if the config says so.""" """Emit the update_geometry signal if the config says so."""
if config.get('completion', 'shrink'): if config.val.completion.shrink:
self.update_geometry.emit() self.update_geometry.emit()
@pyqtSlot() @pyqtSlot()
@ -329,14 +327,14 @@ class CompletionView(QTreeView):
def sizeHint(self): def sizeHint(self):
"""Get the completion size according to the config.""" """Get the completion size according to the config."""
# Get the configured height/percentage. # Get the configured height/percentage.
confheight = str(config.get('completion', 'height')) confheight = str(config.val.completion.height)
if confheight.endswith('%'): if confheight.endswith('%'):
perc = int(confheight.rstrip('%')) perc = int(confheight.rstrip('%'))
height = self.window().height() * perc / 100 height = self.window().height() * perc / 100
else: else:
height = int(confheight) height = int(confheight)
# Shrink to content size if needed and shrinking is enabled # Shrink to content size if needed and shrinking is enabled
if config.get('completion', 'shrink'): if config.val.completion.shrink:
contents_height = ( contents_height = (
self.viewportSizeHint().height() + self.viewportSizeHint().height() +
self.horizontalScrollBar().sizeHint().height()) self.horizontalScrollBar().sizeHint().height())

View File

@ -20,77 +20,63 @@
"""Functions that return config-related completion models.""" """Functions that return config-related completion models."""
from qutebrowser.config import configdata, configexc from qutebrowser.config import configdata, configexc
from qutebrowser.completion.models import completionmodel, listcategory from qutebrowser.completion.models import completionmodel, listcategory, util
from qutebrowser.utils import objreg from qutebrowser.commands import cmdutils
def section(): def option(*, info):
"""A CompletionModel filled with settings sections.""" """A CompletionModel filled with settings and their descriptions."""
model = completionmodel.CompletionModel(column_widths=(20, 70, 10)) model = completionmodel.CompletionModel(column_widths=(20, 70, 10))
sections = ((name, configdata.SECTION_DESC[name].splitlines()[0].strip()) options = ((opt.name, opt.description, info.config.get_str(opt.name))
for name in configdata.DATA) for opt in configdata.DATA.values())
model.add_category(listcategory.ListCategory("Sections", sections)) model.add_category(listcategory.ListCategory("Options", sorted(options)))
return model return model
def option(sectname): def value(optname, *_values, info):
"""A CompletionModel filled with settings and their descriptions.
Args:
sectname: The name of the config section this model shows.
"""
model = completionmodel.CompletionModel(column_widths=(20, 70, 10))
try:
sectdata = configdata.DATA[sectname]
except KeyError:
return None
options = []
for name in sectdata:
try:
desc = sectdata.descriptions[name]
except (KeyError, AttributeError):
# Some stuff (especially ValueList items) don't have a
# description.
desc = ""
else:
desc = desc.splitlines()[0]
config = objreg.get('config')
val = config.get(sectname, name, raw=True)
options.append((name, desc, val))
model.add_category(listcategory.ListCategory(sectname, options))
return model
def value(sectname, optname):
"""A CompletionModel filled with setting values. """A CompletionModel filled with setting values.
Args: Args:
sectname: The name of the config section this model shows.
optname: The name of the config option this model shows. optname: The name of the config option this model shows.
_values: The values already provided on the command line.
info: A CompletionInfo instance.
""" """
model = completionmodel.CompletionModel(column_widths=(20, 70, 10)) model = completionmodel.CompletionModel(column_widths=(30, 70, 0))
config = objreg.get('config')
try: try:
current = config.get(sectname, optname, raw=True) or '""' current = info.config.get_str(optname) or '""'
except (configexc.NoSectionError, configexc.NoOptionError): except configexc.NoOptionError:
return None return None
default = configdata.DATA[sectname][optname].default() or '""' opt = info.config.get_opt(optname)
default = opt.typ.to_str(opt.default)
if hasattr(configdata.DATA[sectname], 'valtype'):
# Same type for all values (ValueList)
vals = configdata.DATA[sectname].valtype.complete()
else:
if optname is None:
raise ValueError("optname may only be None for ValueList "
"sections, but {} is not!".format(sectname))
# Different type for each value (KeyValue)
vals = configdata.DATA[sectname][optname].typ.complete()
cur_cat = listcategory.ListCategory("Current/Default", cur_cat = listcategory.ListCategory("Current/Default",
[(current, "Current value"), (default, "Default value")]) [(current, "Current value"), (default, "Default value")])
model.add_category(cur_cat) model.add_category(cur_cat)
vals = opt.typ.complete()
if vals is not None: if vals is not None:
model.add_category(listcategory.ListCategory("Completions", vals)) model.add_category(listcategory.ListCategory("Completions",
sorted(vals)))
return model
def bind(key, *, info):
"""A CompletionModel filled with all bindable commands and descriptions.
Args:
key: the key being bound.
"""
model = completionmodel.CompletionModel(column_widths=(20, 60, 20))
cmd_text = info.keyconf.get_command(key, 'normal')
if cmd_text:
cmd_name = cmd_text.split(' ')[0]
cmd = cmdutils.cmd_dict.get(cmd_name)
data = [(cmd_text, cmd.desc, key)]
model.add_category(listcategory.ListCategory("Current", data))
cmdlist = util.get_cmd_completions(info, include_hidden=True,
include_aliases=True)
model.add_category(listcategory.ListCategory("Commands", cmdlist))
return model return model

View File

@ -38,9 +38,9 @@ class HistoryCategory(QSqlQueryModel):
self.name = "History" self.name = "History"
# replace ' in timestamp-format to avoid breaking the query # replace ' in timestamp-format to avoid breaking the query
timestamp_format = config.val.completion.timestamp_format
timefmt = ("strftime('{}', last_atime, 'unixepoch', 'localtime')" timefmt = ("strftime('{}', last_atime, 'unixepoch', 'localtime')"
.format(config.get('completion', 'timestamp-format') .format(timestamp_format.replace("'", "`")))
.replace("'", "`")))
self._query = sql.Query(' '.join([ self._query = sql.Query(' '.join([
"SELECT url, title, {}".format(timefmt), "SELECT url, title, {}".format(timefmt),
@ -58,7 +58,7 @@ class HistoryCategory(QSqlQueryModel):
def _atime_expr(self): def _atime_expr(self):
"""If max_items is set, return an expression to limit the query.""" """If max_items is set, return an expression to limit the query."""
max_items = config.get('completion', 'web-history-max-items') max_items = config.val.completion.web_history_max_items
# HistoryCategory should not be added to the completion in that case. # HistoryCategory should not be added to the completion in that case.
assert max_items != 0 assert max_items != 0

View File

@ -60,33 +60,19 @@ class ListCategory(QSortFilterProxyModel):
sortcol = 0 sortcol = 0
self.sort(sortcol) self.sort(sortcol)
def lessThan(self, lindex, rindex): def lessThan(self, _lindex, rindex):
"""Custom sorting implementation. """Custom sorting implementation.
Prefers all items which start with self._pattern. Other than that, uses Prefers all items which start with self._pattern. Other than that, keep
normal Python string sorting. items in their original order.
Args: Args:
lindex: The QModelIndex of the left item (*left* < right) _lindex: The QModelIndex of the left item (*left* < right)
rindex: The QModelIndex of the right item (left < *right*) rindex: The QModelIndex of the right item (left < *right*)
Return: Return:
True if left < right, else False True if left < right, else False
""" """
qtutils.ensure_valid(lindex)
qtutils.ensure_valid(rindex) qtutils.ensure_valid(rindex)
left = self.srcmodel.data(lindex)
right = self.srcmodel.data(rindex) right = self.srcmodel.data(rindex)
return not right.startswith(self._pattern)
leftstart = left.startswith(self._pattern)
rightstart = right.startswith(self._pattern)
if leftstart and rightstart:
return left < right
elif leftstart:
return True
elif rightstart:
return False
else:
return left < right

View File

@ -19,46 +19,35 @@
"""Functions that return miscellaneous completion models.""" """Functions that return miscellaneous completion models."""
from qutebrowser.config import config, configdata from qutebrowser.config import configdata
from qutebrowser.utils import objreg, log from qutebrowser.utils import objreg, log
from qutebrowser.commands import cmdutils from qutebrowser.completion.models import completionmodel, listcategory, util
from qutebrowser.completion.models import completionmodel, listcategory
def command(): def command(*, info):
"""A CompletionModel filled with non-hidden commands and descriptions.""" """A CompletionModel filled with non-hidden commands and descriptions."""
model = completionmodel.CompletionModel(column_widths=(20, 60, 20)) model = completionmodel.CompletionModel(column_widths=(20, 60, 20))
cmdlist = _get_cmd_completions(include_aliases=True, include_hidden=False) cmdlist = util.get_cmd_completions(info, include_aliases=True,
include_hidden=False)
model.add_category(listcategory.ListCategory("Commands", cmdlist)) model.add_category(listcategory.ListCategory("Commands", cmdlist))
return model return model
def helptopic(): def helptopic(*, info):
"""A CompletionModel filled with help topics.""" """A CompletionModel filled with help topics."""
model = completionmodel.CompletionModel() model = completionmodel.CompletionModel()
cmdlist = _get_cmd_completions(include_aliases=False, include_hidden=True, cmdlist = util.get_cmd_completions(info, include_aliases=False,
prefix=':') include_hidden=True, prefix=':')
settings = [] settings = ((opt.name, opt.description)
for sectname, sectdata in configdata.DATA.items(): for opt in configdata.DATA.values())
for optname in sectdata:
try:
desc = sectdata.descriptions[optname]
except (KeyError, AttributeError):
# Some stuff (especially ValueList items) don't have a
# description.
desc = ""
else:
desc = desc.splitlines()[0]
name = '{}->{}'.format(sectname, optname)
settings.append((name, desc))
model.add_category(listcategory.ListCategory("Commands", cmdlist)) model.add_category(listcategory.ListCategory("Commands", cmdlist))
model.add_category(listcategory.ListCategory("Settings", settings)) model.add_category(listcategory.ListCategory("Settings", sorted(settings)))
return model return model
def quickmark(): def quickmark(*, info=None): # pylint: disable=unused-argument
"""A CompletionModel filled with all quickmarks.""" """A CompletionModel filled with all quickmarks."""
def delete(data): def delete(data):
"""Delete a quickmark from the completion menu.""" """Delete a quickmark from the completion menu."""
@ -74,7 +63,7 @@ def quickmark():
return model return model
def bookmark(): def bookmark(*, info=None): # pylint: disable=unused-argument
"""A CompletionModel filled with all bookmarks.""" """A CompletionModel filled with all bookmarks."""
def delete(data): def delete(data):
"""Delete a bookmark from the completion menu.""" """Delete a bookmark from the completion menu."""
@ -90,7 +79,7 @@ def bookmark():
return model return model
def session(): def session(*, info=None): # pylint: disable=unused-argument
"""A CompletionModel filled with session names.""" """A CompletionModel filled with session names."""
model = completionmodel.CompletionModel() model = completionmodel.CompletionModel()
try: try:
@ -103,7 +92,7 @@ def session():
return model return model
def buffer(): def buffer(*, info=None): # pylint: disable=unused-argument
"""A model to complete on open tabs across all windows. """A model to complete on open tabs across all windows.
Used for switching the buffer command. Used for switching the buffer command.
@ -133,51 +122,3 @@ def buffer():
model.add_category(cat) model.add_category(cat)
return model return model
def bind(key):
"""A CompletionModel filled with all bindable commands and descriptions.
Args:
key: the key being bound.
"""
model = completionmodel.CompletionModel(column_widths=(20, 60, 20))
cmd_text = objreg.get('key-config').get_bindings_for('normal').get(key)
if cmd_text:
cmd_name = cmd_text.split(' ')[0]
cmd = cmdutils.cmd_dict.get(cmd_name)
data = [(cmd_text, cmd.desc, key)]
model.add_category(listcategory.ListCategory("Current", data))
cmdlist = _get_cmd_completions(include_hidden=True, include_aliases=True)
model.add_category(listcategory.ListCategory("Commands", cmdlist))
return model
def _get_cmd_completions(include_hidden, include_aliases, prefix=''):
"""Get a list of completions info for commands, sorted by name.
Args:
include_hidden: True to include commands annotated with hide=True.
include_aliases: True to include command aliases.
prefix: String to append to the command name.
Return: A list of tuples of form (name, description, bindings).
"""
assert cmdutils.cmd_dict
cmdlist = []
cmd_to_keys = objreg.get('key-config').get_reverse_bindings_for('normal')
for obj in set(cmdutils.cmd_dict.values()):
hide_debug = obj.debug and not objreg.get('args').debug
hide_hidden = obj.hide and not include_hidden
if not (hide_debug or hide_hidden or obj.deprecated):
bindings = ', '.join(cmd_to_keys.get(obj.name, []))
cmdlist.append((prefix + obj.name, obj.desc, bindings))
if include_aliases:
for name, cmd in config.section('aliases').items():
bindings = ', '.join(cmd_to_keys.get(name, []))
cmdlist.append((name, "Alias for '{}'".format(cmd), bindings))
return cmdlist

View File

@ -22,7 +22,6 @@
from qutebrowser.completion.models import (completionmodel, listcategory, from qutebrowser.completion.models import (completionmodel, listcategory,
histcategory) histcategory)
from qutebrowser.utils import log, objreg from qutebrowser.utils import log, objreg
from qutebrowser.config import config
_URLCOL = 0 _URLCOL = 0
@ -50,7 +49,7 @@ def _delete_quickmark(data):
quickmark_manager.delete(name) quickmark_manager.delete(name)
def url(): def url(*, info):
"""A model which combines bookmarks, quickmarks and web history URLs. """A model which combines bookmarks, quickmarks and web history URLs.
Used for the `open` command. Used for the `open` command.
@ -66,7 +65,7 @@ def url():
model.add_category(listcategory.ListCategory( model.add_category(listcategory.ListCategory(
'Bookmarks', bookmarks, delete_func=_delete_bookmark)) 'Bookmarks', bookmarks, delete_func=_delete_bookmark))
if config.get('completion', 'web-history-max-items') != 0: if info.config.get('completion.web_history_max_items') != 0:
hist_cat = histcategory.HistoryCategory(delete_func=_delete_history) hist_cat = histcategory.HistoryCategory(delete_func=_delete_history)
model.add_category(hist_cat) model.add_category(hist_cat)
return model return model

View File

@ -0,0 +1,52 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2017 Ryan Roden-Corrent (rcorre) <ryan@rcorre.net>
#
# 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/>.
"""Utility functions for completion models."""
from qutebrowser.utils import objreg
from qutebrowser.commands import cmdutils
def get_cmd_completions(info, include_hidden, include_aliases, prefix=''):
"""Get a list of completions info for commands, sorted by name.
Args:
info: The CompletionInfo.
include_hidden: True to include commands annotated with hide=True.
include_aliases: True to include command aliases.
prefix: String to append to the command name.
Return: A list of tuples of form (name, description, bindings).
"""
assert cmdutils.cmd_dict
cmdlist = []
cmd_to_keys = info.keyconf.get_reverse_bindings_for('normal')
for obj in set(cmdutils.cmd_dict.values()):
hide_debug = obj.debug and not objreg.get('args').debug
hide_hidden = obj.hide and not include_hidden
if not (hide_debug or hide_hidden or obj.deprecated):
bindings = ', '.join(cmd_to_keys.get(obj.name, []))
cmdlist.append((prefix + obj.name, obj.desc, bindings))
if include_aliases:
for name, cmd in info.config.get('aliases').items():
bindings = ', '.join(cmd_to_keys.get(name, []))
cmdlist.append((name, "Alias for '{}'".format(cmd), bindings))
return sorted(cmdlist)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,760 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2017 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/>.
"""Code to show a diff of the legacy config format."""
import difflib
import os.path
import pygments
import pygments.lexers
import pygments.formatters
from qutebrowser.utils import standarddir
OLD_CONF = """
[general]
ignore-case = smart
startpage = https://start.duckduckgo.com
yank-ignored-url-parameters = ref,utm_source,utm_medium,utm_campaign,utm_term,utm_content
default-open-dispatcher =
default-page = ${startpage}
auto-search = naive
auto-save-config = true
auto-save-interval = 15000
editor = gvim -f "{}"
editor-encoding = utf-8
private-browsing = false
developer-extras = false
print-element-backgrounds = true
xss-auditing = false
default-encoding = iso-8859-1
new-instance-open-target = tab
new-instance-open-target.window = last-focused
log-javascript-console = debug
save-session = false
session-default-name =
url-incdec-segments = path,query
[ui]
history-session-interval = 30
zoom-levels = 25%,33%,50%,67%,75%,90%,100%,110%,125%,150%,175%,200%,250%,300%,400%,500%
default-zoom = 100%
downloads-position = top
status-position = bottom
message-timeout = 2000
message-unfocused = false
confirm-quit = never
zoom-text-only = false
frame-flattening = false
user-stylesheet =
hide-scrollbar = true
smooth-scrolling = false
remove-finished-downloads = -1
hide-statusbar = false
statusbar-padding = 1,1,0,0
window-title-format = {perc}{title}{title_sep}qutebrowser
modal-js-dialog = false
hide-wayland-decoration = false
keyhint-blacklist =
keyhint-delay = 500
prompt-radius = 8
prompt-filebrowser = true
[network]
do-not-track = true
accept-language = en-US,en
referer-header = same-domain
user-agent =
proxy = system
proxy-dns-requests = true
ssl-strict = ask
dns-prefetch = true
custom-headers =
netrc-file =
[completion]
show = always
download-path-suggestion = path
timestamp-format = %Y-%m-%d
height = 50%
cmd-history-max-items = 100
web-history-max-items = -1
quick-complete = true
shrink = false
scrollbar-width = 12
scrollbar-padding = 2
[input]
timeout = 500
partial-timeout = 5000
insert-mode-on-plugins = false
auto-leave-insert-mode = true
auto-insert-mode = false
forward-unbound-keys = auto
spatial-navigation = false
links-included-in-focus-chain = true
rocker-gestures = false
mouse-zoom-divider = 512
[tabs]
background-tabs = false
select-on-remove = next
new-tab-position = next
new-tab-position-explicit = last
last-close = ignore
show = always
show-switching-delay = 800
wrap = true
movable = true
close-mouse-button = middle
position = top
show-favicons = true
favicon-scale = 1.0
width = 20%
pinned-width = 43
indicator-width = 3
tabs-are-windows = false
title-format = {index}: {title}
title-format-pinned = {index}
title-alignment = left
mousewheel-tab-switching = true
padding = 0,0,5,5
indicator-padding = 2,2,0,4
[storage]
download-directory =
prompt-download-directory = true
remember-download-directory = true
maximum-pages-in-cache = 0
offline-web-application-cache = true
local-storage = true
cache-size =
[content]
allow-images = true
allow-javascript = true
allow-plugins = false
webgl = true
hyperlink-auditing = false
geolocation = ask
notifications = ask
media-capture = ask
javascript-can-open-windows-automatically = false
javascript-can-close-windows = false
javascript-can-access-clipboard = false
ignore-javascript-prompt = false
ignore-javascript-alert = false
local-content-can-access-remote-urls = false
local-content-can-access-file-urls = true
cookies-accept = no-3rdparty
cookies-store = true
host-block-lists = https://www.malwaredomainlist.com/hostslist/hosts.txt,http://someonewhocares.org/hosts/hosts,http://winhelp2002.mvps.org/hosts.zip,http://malwaredomains.lehigh.edu/files/justdomains.zip,https://pgl.yoyo.org/adservers/serverlist.php?hostformat=hosts&mimetype=plaintext
host-blocking-enabled = true
host-blocking-whitelist = piwik.org
enable-pdfjs = false
[hints]
border = 1px solid #E3BE23
mode = letter
chars = asdfghjkl
min-chars = 1
scatter = true
uppercase = false
dictionary = /usr/share/dict/words
auto-follow = unique-match
auto-follow-timeout = 0
next-regexes = \\bnext\\b,\\bmore\\b,\\bnewer\\b,\\b[>\u2192\u226b]\\b,\\b(>>|\xbb)\\b,\\bcontinue\\b
prev-regexes = \\bprev(ious)?\\b,\\bback\\b,\\bolder\\b,\\b[<\u2190\u226a]\\b,\\b(<<|\xab)\\b
find-implementation = python
hide-unmatched-rapid-hints = true
[searchengines]
DEFAULT = https://duckduckgo.com/?q={}
[aliases]
[colors]
completion.fg = white
completion.bg = #333333
completion.alternate-bg = #444444
completion.category.fg = white
completion.category.bg = qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #888888, stop:1 #505050)
completion.category.border.top = black
completion.category.border.bottom = ${completion.category.border.top}
completion.item.selected.fg = black
completion.item.selected.bg = #e8c000
completion.item.selected.border.top = #bbbb00
completion.item.selected.border.bottom = ${completion.item.selected.border.top}
completion.match.fg = #ff4444
completion.scrollbar.fg = ${completion.fg}
completion.scrollbar.bg = ${completion.bg}
statusbar.fg = white
statusbar.bg = black
statusbar.fg.private = ${statusbar.fg}
statusbar.bg.private = #666666
statusbar.fg.insert = ${statusbar.fg}
statusbar.bg.insert = darkgreen
statusbar.fg.command = ${statusbar.fg}
statusbar.bg.command = ${statusbar.bg}
statusbar.fg.command.private = ${statusbar.fg.private}
statusbar.bg.command.private = ${statusbar.bg.private}
statusbar.fg.caret = ${statusbar.fg}
statusbar.bg.caret = purple
statusbar.fg.caret-selection = ${statusbar.fg}
statusbar.bg.caret-selection = #a12dff
statusbar.progress.bg = white
statusbar.url.fg = ${statusbar.fg}
statusbar.url.fg.success = white
statusbar.url.fg.success.https = lime
statusbar.url.fg.error = orange
statusbar.url.fg.warn = yellow
statusbar.url.fg.hover = aqua
tabs.fg.odd = white
tabs.bg.odd = grey
tabs.fg.even = white
tabs.bg.even = darkgrey
tabs.fg.selected.odd = white
tabs.bg.selected.odd = black
tabs.fg.selected.even = ${tabs.fg.selected.odd}
tabs.bg.selected.even = ${tabs.bg.selected.odd}
tabs.bg.bar = #555555
tabs.indicator.start = #0000aa
tabs.indicator.stop = #00aa00
tabs.indicator.error = #ff0000
tabs.indicator.system = rgb
hints.fg = black
hints.bg = qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 rgba(255, 247, 133, 0.8), stop:1 rgba(255, 197, 66, 0.8))
hints.fg.match = green
downloads.bg.bar = black
downloads.fg.start = white
downloads.bg.start = #0000aa
downloads.fg.stop = ${downloads.fg.start}
downloads.bg.stop = #00aa00
downloads.fg.system = rgb
downloads.bg.system = rgb
downloads.fg.error = white
downloads.bg.error = red
webpage.bg = white
keyhint.fg = #FFFFFF
keyhint.fg.suffix = #FFFF00
keyhint.bg = rgba(0, 0, 0, 80%)
messages.fg.error = white
messages.bg.error = red
messages.border.error = #bb0000
messages.fg.warning = white
messages.bg.warning = darkorange
messages.border.warning = #d47300
messages.fg.info = white
messages.bg.info = black
messages.border.info = #333333
prompts.fg = white
prompts.bg = darkblue
prompts.selected.bg = #308cc6
[fonts]
_monospace = xos4 Terminus, Terminus, Monospace, "DejaVu Sans Mono", Monaco, "Bitstream Vera Sans Mono", "Andale Mono", "Courier New", Courier, "Liberation Mono", monospace, Fixed, Consolas, Terminal
completion = 8pt ${_monospace}
completion.category = bold ${completion}
tabbar = 8pt ${_monospace}
statusbar = 8pt ${_monospace}
downloads = 8pt ${_monospace}
hints = bold 13px ${_monospace}
debug-console = 8pt ${_monospace}
web-family-standard =
web-family-fixed =
web-family-serif =
web-family-sans-serif =
web-family-cursive =
web-family-fantasy =
web-size-minimum = 0
web-size-minimum-logical = 6
web-size-default = 16
web-size-default-fixed = 13
keyhint = 8pt ${_monospace}
messages.error = 8pt ${_monospace}
messages.warning = 8pt ${_monospace}
messages.info = 8pt ${_monospace}
prompts = 8pt sans-serif
"""
OLD_KEYS_CONF = """
[!normal]
leave-mode
<escape>
<ctrl-[>
[normal]
clear-keychain ;; search ;; fullscreen --leave
<escape>
<ctrl-[>
set-cmd-text -s :open
o
set-cmd-text :open {url:pretty}
go
set-cmd-text -s :open -t
O
set-cmd-text :open -t -i {url:pretty}
gO
set-cmd-text -s :open -b
xo
set-cmd-text :open -b -i {url:pretty}
xO
set-cmd-text -s :open -w
wo
set-cmd-text :open -w {url:pretty}
wO
set-cmd-text /
/
set-cmd-text ?
?
set-cmd-text :
:
open -t
ga
<ctrl-t>
open -w
<ctrl-n>
tab-close
d
<ctrl-w>
tab-close -o
D
tab-only
co
tab-focus
T
tab-move
gm
tab-move -
gl
tab-move +
gr
tab-next
J
<ctrl-pgdown>
tab-prev
K
<ctrl-pgup>
tab-clone
gC
reload
r
<f5>
reload -f
R
<ctrl-f5>
back
H
<back>
back -t
th
back -w
wh
forward
L
<forward>
forward -t
tl
forward -w
wl
fullscreen
<f11>
hint
f
hint all tab
F
hint all window
wf
hint all tab-bg
;b
hint all tab-fg
;f
hint all hover
;h
hint images
;i
hint images tab
;I
hint links fill :open {hint-url}
;o
hint links fill :open -t -i {hint-url}
;O
hint links yank
;y
hint links yank-primary
;Y
hint --rapid links tab-bg
;r
hint --rapid links window
;R
hint links download
;d
hint inputs
;t
scroll left
h
scroll down
j
scroll up
k
scroll right
l
undo
u
<ctrl-shift-t>
scroll-perc 0
gg
scroll-perc
G
search-next
n
search-prev
N
enter-mode insert
i
enter-mode caret
v
enter-mode set_mark
`
enter-mode jump_mark
'
yank
yy
yank -s
yY
yank title
yt
yank title -s
yT
yank domain
yd
yank domain -s
yD
yank pretty-url
yp
yank pretty-url -s
yP
open -- {clipboard}
pp
open -- {primary}
pP
open -t -- {clipboard}
Pp
open -t -- {primary}
PP
open -w -- {clipboard}
wp
open -w -- {primary}
wP
quickmark-save
m
set-cmd-text -s :quickmark-load
b
set-cmd-text -s :quickmark-load -t
B
set-cmd-text -s :quickmark-load -w
wb
bookmark-add
M
set-cmd-text -s :bookmark-load
gb
set-cmd-text -s :bookmark-load -t
gB
set-cmd-text -s :bookmark-load -w
wB
save
sf
set-cmd-text -s :set
ss
set-cmd-text -s :set -t
sl
set-cmd-text -s :bind
sk
zoom-out
-
zoom-in
+
zoom
=
navigate prev
[[
navigate next
]]
navigate prev -t
{{
navigate next -t
}}
navigate up
gu
navigate up -t
gU
navigate increment
<ctrl-a>
navigate decrement
<ctrl-x>
inspector
wi
download
gd
download-cancel
ad
download-clear
cd
view-source
gf
set-cmd-text -s :buffer
gt
tab-focus last
<ctrl-tab>
<ctrl-6>
<ctrl-^>
enter-mode passthrough
<ctrl-v>
quit
<ctrl-q>
ZQ
wq
ZZ
scroll-page 0 1
<ctrl-f>
scroll-page 0 -1
<ctrl-b>
scroll-page 0 0.5
<ctrl-d>
scroll-page 0 -0.5
<ctrl-u>
tab-focus 1
<alt-1>
g0
g^
tab-focus 2
<alt-2>
tab-focus 3
<alt-3>
tab-focus 4
<alt-4>
tab-focus 5
<alt-5>
tab-focus 6
<alt-6>
tab-focus 7
<alt-7>
tab-focus 8
<alt-8>
tab-focus -1
<alt-9>
g$
home
<ctrl-h>
stop
<ctrl-s>
print
<ctrl-alt-p>
open qute://settings
Ss
follow-selected
<return>
<ctrl-m>
<ctrl-j>
<shift-return>
<enter>
<shift-enter>
follow-selected -t
<ctrl-return>
<ctrl-enter>
repeat-command
.
tab-pin
<ctrl-p>
record-macro
q
run-macro
@
[insert]
open-editor
<ctrl-e>
insert-text {primary}
<shift-ins>
[hint]
follow-hint
<return>
<ctrl-m>
<ctrl-j>
<shift-return>
<enter>
<shift-enter>
hint --rapid links tab-bg
<ctrl-r>
hint links
<ctrl-f>
hint all tab-bg
<ctrl-b>
[passthrough]
[command]
command-history-prev
<ctrl-p>
command-history-next
<ctrl-n>
completion-item-focus prev
<shift-tab>
<up>
completion-item-focus next
<tab>
<down>
completion-item-focus next-category
<ctrl-tab>
completion-item-focus prev-category
<ctrl-shift-tab>
completion-item-del
<ctrl-d>
command-accept
<return>
<ctrl-m>
<ctrl-j>
<shift-return>
<enter>
<shift-enter>
[prompt]
prompt-accept
<return>
<ctrl-m>
<ctrl-j>
<shift-return>
<enter>
<shift-enter>
prompt-accept yes
y
prompt-accept no
n
prompt-open-download
<ctrl-x>
prompt-item-focus prev
<shift-tab>
<up>
prompt-item-focus next
<tab>
<down>
[command,prompt]
rl-backward-char
<ctrl-b>
rl-forward-char
<ctrl-f>
rl-backward-word
<alt-b>
rl-forward-word
<alt-f>
rl-beginning-of-line
<ctrl-a>
rl-end-of-line
<ctrl-e>
rl-unix-line-discard
<ctrl-u>
rl-kill-line
<ctrl-k>
rl-kill-word
<alt-d>
rl-unix-word-rubout
<ctrl-w>
rl-backward-kill-word
<alt-backspace>
rl-yank
<ctrl-y>
rl-delete-char
<ctrl-?>
rl-backward-delete-char
<ctrl-h>
[caret]
toggle-selection
v
<space>
drop-selection
<ctrl-space>
enter-mode normal
c
move-to-next-line
j
move-to-prev-line
k
move-to-next-char
l
move-to-prev-char
h
move-to-end-of-word
e
move-to-next-word
w
move-to-prev-word
b
move-to-start-of-next-block
]
move-to-start-of-prev-block
[
move-to-end-of-next-block
}
move-to-end-of-prev-block
{
move-to-start-of-line
0
move-to-end-of-line
$
move-to-start-of-document
gg
move-to-end-of-document
G
yank selection -s
Y
yank selection
y
<return>
<ctrl-m>
<ctrl-j>
<shift-return>
<enter>
<shift-enter>
scroll left
H
scroll down
J
scroll up
K
scroll right
L
"""
def get_diff():
"""Get a HTML diff for the old config files."""
old_conf_lines = []
old_key_lines = []
for filename, dest in [('qutebrowser.conf', old_conf_lines),
('keys.conf', old_key_lines)]:
path = os.path.join(standarddir.config(), filename)
with open(path, 'r', encoding='utf-8') as f:
for line in f:
if not line.strip() or line.startswith('#'):
continue
dest.append(line.rstrip())
conf_delta = difflib.unified_diff(OLD_CONF.lstrip().splitlines(),
old_conf_lines)
key_delta = difflib.unified_diff(OLD_KEYS_CONF.lstrip().splitlines(),
old_key_lines)
conf_diff = '\n'.join(conf_delta)
key_diff = '\n'.join(key_delta)
# pylint: disable=no-member
# WORKAROUND for https://bitbucket.org/logilab/pylint/issue/491/
lexer = pygments.lexers.DiffLexer()
formatter = pygments.formatters.HtmlFormatter(
full=True, linenos='table',
title='Config diff')
# pylint: enable=no-member
return pygments.highlight(conf_diff + key_diff, lexer, formatter)

View File

@ -19,6 +19,10 @@
"""Exceptions related to config parsing.""" """Exceptions related to config parsing."""
import attr
from qutebrowser.utils import jinja
class Error(Exception): class Error(Exception):
@ -41,46 +45,82 @@ class ValidationError(Error):
"""Raised when a value for a config type was invalid. """Raised when a value for a config type was invalid.
Attributes: Attributes:
section: Section in which the error occurred (added when catching and value: Config value that triggered the error.
re-raising the exception). msg: Additional error message.
option: Option in which the error occurred.
""" """
def __init__(self, value, msg): def __init__(self, value, msg):
super().__init__("Invalid value '{}' - {}".format(value, msg)) super().__init__("Invalid value '{}' - {}".format(value, msg))
self.section = None
self.option = None self.option = None
class NoSectionError(Error): class KeybindingError(Error):
"""Raised when no section matches a requested option.""" """Raised for issues with keybindings."""
def __init__(self, section):
super().__init__("Section {!r} does not exist!".format(section)) class DuplicateKeyError(KeybindingError):
self.section = section
"""Raised when there was a duplicate key."""
def __init__(self, key):
super().__init__("Duplicate key {}".format(key))
class NoOptionError(Error): class NoOptionError(Error):
"""Raised when an option was not found.""" """Raised when an option was not found."""
def __init__(self, option, section): def __init__(self, option):
super().__init__("No option {!r} in section {!r}".format( super().__init__("No option {!r}".format(option))
option, section))
self.option = option self.option = option
self.section = section
class InterpolationSyntaxError(Error): @attr.s
class ConfigErrorDesc:
"""Raised when the source text contains invalid syntax. """A description of an error happening while reading the config.
Current implementation raises this exception when the source text into Attributes:
which substitutions are made does not conform to the required syntax. text: The text to show.
exception: The exception which happened.
traceback: The formatted traceback of the exception.
""" """
def __init__(self, option, section, msg): text = attr.ib()
super().__init__(msg) exception = attr.ib()
self.option = option traceback = attr.ib(None)
self.section = section
def __str__(self):
return '{}: {}'.format(self.text, self.exception)
class ConfigFileErrors(Error):
"""Raised when multiple errors occurred inside the config."""
def __init__(self, basename, errors):
super().__init__("Errors occurred while reading {}:\n{}".format(
basename, '\n'.join(' {}'.format(e) for e in errors)))
self.basename = basename
self.errors = errors
def to_html(self):
"""Get the error texts as a HTML snippet."""
template = jinja.environment.from_string("""
Errors occurred while reading {{ basename }}:
<ul>
{% for error in errors %}
<li>
<b>{{ error.text }}</b>: {{ error.exception }}
{% if error.traceback != none %}
<pre>
""".rstrip() + "\n{{ error.traceback }}" + """
</pre>
{% endif %}
</li>
{% endfor %}
</ul>
""")
return template.render(basename=self.basename, errors=self.errors)

View File

@ -0,0 +1,254 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-2017 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/>.
"""Configuration files residing on disk."""
import types
import os.path
import textwrap
import traceback
import configparser
import contextlib
import yaml
from PyQt5.QtCore import QSettings
import qutebrowser
from qutebrowser.config import configexc, config
from qutebrowser.utils import standarddir, utils, qtutils
# The StateConfig instance
state = None
class StateConfig(configparser.ConfigParser):
"""The "state" file saving various application state."""
def __init__(self):
super().__init__()
self._filename = os.path.join(standarddir.data(), 'state')
self.read(self._filename, encoding='utf-8')
for sect in ['general', 'geometry']:
try:
self.add_section(sect)
except configparser.DuplicateSectionError:
pass
deleted_keys = ['fooled', 'backend-warning-shown']
for key in deleted_keys:
self['general'].pop(key, None)
def init_save_manager(self, save_manager):
"""Make sure the config gets saved properly.
We do this outside of __init__ because the config gets created before
the save_manager exists.
"""
save_manager.add_saveable('state-config', self._save)
def _save(self):
"""Save the state file to the configured location."""
with open(self._filename, 'w', encoding='utf-8') as f:
self.write(f)
class YamlConfig:
"""A config stored on disk as YAML file.
Class attributes:
VERSION: The current version number of the config file.
"""
VERSION = 1
def __init__(self):
self._filename = os.path.join(standarddir.config(auto=True),
'autoconfig.yml')
self.values = {}
def init_save_manager(self, save_manager):
"""Make sure the config gets saved properly.
We do this outside of __init__ because the config gets created before
the save_manager exists.
"""
save_manager.add_saveable('yaml-config', self._save)
def _save(self):
"""Save the changed settings to the YAML file."""
data = {'config_version': self.VERSION, 'global': self.values}
with qtutils.savefile_open(self._filename) as f:
f.write(textwrap.dedent("""
# DO NOT edit this file by hand, qutebrowser will overwrite it.
# Instead, create a config.py - see :help for details.
""".lstrip('\n')))
utils.yaml_dump(data, f)
def load(self):
"""Load self.values from the configured YAML file."""
try:
with open(self._filename, 'r', encoding='utf-8') as f:
yaml_data = utils.yaml_load(f)
except FileNotFoundError:
return
except OSError as e:
desc = configexc.ConfigErrorDesc("While reading", e)
raise configexc.ConfigFileErrors('autoconfig.yml', [desc])
except yaml.YAMLError as e:
desc = configexc.ConfigErrorDesc("While parsing", e)
raise configexc.ConfigFileErrors('autoconfig.yml', [desc])
try:
global_obj = yaml_data['global']
except KeyError:
desc = configexc.ConfigErrorDesc(
"While loading data",
"Toplevel object does not contain 'global' key")
raise configexc.ConfigFileErrors('autoconfig.yml', [desc])
except TypeError:
desc = configexc.ConfigErrorDesc("While loading data",
"Toplevel object is not a dict")
raise configexc.ConfigFileErrors('autoconfig.yml', [desc])
if not isinstance(global_obj, dict):
desc = configexc.ConfigErrorDesc(
"While loading data",
"'global' object is not a dict")
raise configexc.ConfigFileErrors('autoconfig.yml', [desc])
self.values = global_obj
class ConfigAPI:
"""Object which gets passed to config.py as "config" object.
This is a small wrapper over the Config object, but with more
straightforward method names (get/set call get_obj/set_obj) and a more
shallow API.
Attributes:
_config: The main Config object to use.
_keyconfig: The KeyConfig object.
load_autoconfig: Whether autoconfig.yml should be loaded.
errors: Errors which occurred while setting options.
"""
def __init__(self, conf, keyconfig):
self._config = conf
self._keyconfig = keyconfig
self.load_autoconfig = True
self.errors = []
@contextlib.contextmanager
def _handle_error(self, action, name):
try:
yield
except configexc.Error as e:
text = "While {} '{}'".format(action, name)
self.errors.append(configexc.ConfigErrorDesc(text, e))
def finalize(self):
"""Do work which needs to be done after reading config.py."""
self._config.update_mutables()
def get(self, name):
with self._handle_error('getting', name):
return self._config.get_obj(name)
def set(self, name, value):
with self._handle_error('setting', name):
self._config.set_obj(name, value)
def bind(self, key, command, mode='normal', *, force=False):
with self._handle_error('binding', key):
self._keyconfig.bind(key, command, mode=mode, force=force)
def unbind(self, key, mode='normal'):
with self._handle_error('unbinding', key):
self._keyconfig.unbind(key, mode=mode)
def read_config_py(filename=None):
"""Read a config.py file."""
api = ConfigAPI(config.instance, config.key_instance)
if filename is None:
filename = os.path.join(standarddir.config(), 'config.py')
if not os.path.exists(filename):
return api
container = config.ConfigContainer(config.instance, configapi=api)
basename = os.path.basename(filename)
module = types.ModuleType('config')
module.config = api
module.c = container
module.__file__ = filename
try:
with open(filename, mode='rb') as f:
source = f.read()
except OSError as e:
text = "Error while reading {}".format(basename)
desc = configexc.ConfigErrorDesc(text, e)
raise configexc.ConfigFileErrors(basename, [desc])
try:
code = compile(source, filename, 'exec')
except (ValueError, TypeError) as e:
# source contains NUL bytes
desc = configexc.ConfigErrorDesc("Error while compiling", e)
raise configexc.ConfigFileErrors(basename, [desc])
except SyntaxError as e:
desc = configexc.ConfigErrorDesc("Syntax Error", e,
traceback=traceback.format_exc())
raise configexc.ConfigFileErrors(basename, [desc])
try:
exec(code, module.__dict__)
except Exception as e:
api.errors.append(configexc.ConfigErrorDesc(
"Unhandled exception",
exception=e, traceback=traceback.format_exc()))
api.finalize()
return api
def init():
"""Initialize config storage not related to the main config."""
global state
state = StateConfig()
state['general']['version'] = qutebrowser.__version__
# Set the QSettings path to something like
# ~/.config/qutebrowser/qsettings/qutebrowser/qutebrowser.conf so it
# doesn't overwrite our config.
#
# This fixes one of the corruption issues here:
# https://github.com/qutebrowser/qutebrowser/issues/515
path = os.path.join(standarddir.config(auto=True), 'qsettings')
for fmt in [QSettings.NativeFormat, QSettings.IniFormat]:
QSettings.setPath(fmt, QSettings.UserScope, path)

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