Compare commits

..

24 Commits

Author SHA1 Message Date
Florian Bruhin
293e322905 Release v0.6.2 2016-04-30 15:05:19 +02:00
Florian Bruhin
354bd5d606 Update changelog for v0.6.2 2016-04-30 14:50:04 +02:00
Florian Bruhin
8e619fa74e Fix dictionary hints crash 2016-04-30 14:44:25 +02:00
Florian Bruhin
5b944fb272 Add @pyqtSlot for qApp.focusChanged slot 2016-04-30 14:43:11 +02:00
Daniel Schadt
36d2fc4b92 cache: fix crash when cache_dir is None
Issue #1412

When passing --cachedir="" on the command line, standarddir.cache()
returns None, which stands for "deactivate cache" and has to be
properly handled in DiskCache.__init__() (i.e. don't pass it to
os.path.join)
2016-04-30 14:42:52 +02:00
Florian Bruhin
26f4acb10a Split IPCServer.on_ready_read into two methods 2016-04-30 14:42:44 +02:00
Florian Bruhin
991277e9de Work around PyQt 5.6 segfault when using IPC
PyQt 5.6 seems to segfault when emitting None with a signal which is
declared as emitting a string:

https://www.riverbankcomputing.com/pipermail/pyqt/2016-April/037375.html

We now avoid this by using an empty string explicitly instead of None.
2016-04-30 14:42:17 +02:00
Florian Bruhin
133e959ecc Remove @pyqtSlot for on_new_window
This worked fine with Python 3.5 but causes a circular import which is
hard to break with Python 3.4.

The original solution was to do @pyqtSlot(object), but that doesn't work
with PyQt 5.6 anymore...
2016-04-30 14:42:17 +02:00
Florian Bruhin
be1630f7d0 Fix types in @pyqtSlot decorations
PyQt 5.5 enforces correct type signatures, and there were a lot of
places where we were simply wrong, causing qutebrowser to not start at
all...
2016-04-30 14:42:00 +02:00
Ryan Roden-Corrent
c57bf8701e Don't crash when undoing twice on default page.
Avoid a crash when undoing twice on the default page with last-close set to
default-page.
This was caused by logic to reuse the current tab if it is on the default page
and has no history. The fix is using openurl rather than removeTab/tabopen.
2016-04-30 14:41:48 +02:00
Florian Bruhin
ea2ae94cd0 Fix crash with :tab-{prev,next,focus} with 0 tabs
When using :tab-prev/:tab-next (or :tab-focus which uses :tab-next
internally) immediately after the last tab, those functions could be
called with 0 tabs open, which caused a ZeroDivisionError when trying to
do % 0.

Fixes #1448.
2016-04-30 14:41:26 +02:00
Florian Bruhin
356eb7e5e7 Release v0.6.1 2016-04-10 21:13:33 +02:00
Florian Bruhin
bdd2afa1a2 Make sure the cheatsheet PNG is included in sdist
Fixes #1403
2016-04-10 21:01:41 +02:00
Florian Bruhin
e02ff26d0e Fix cheatsheet link URL in quickstart 2016-04-10 21:01:41 +02:00
Florian Bruhin
128fb2826a Add missing file 2016-04-10 20:35:07 +02:00
Florian Bruhin
3521ee16e4 Fix downloading of non-ascii files with LC_ALL=C
Fixes #908.
2016-04-10 20:35:07 +02:00
Florian Bruhin
af5cb36591 Rename test_cmdline_args to test_invocations 2016-04-10 20:35:07 +02:00
Florian Bruhin
8aaae5b78c Fix test_last_window_session_none 2016-04-10 18:23:10 +02:00
Florian Bruhin
ced87b163f Don't crash if data is None while saving session
Under some circumstances I can't reproduce (switching/turning off
monitors?) it seems it's possible that SessionManager.save gets called
with last_window=True, without on_last_window_closed being called.

This might be to one of the Qt screen management bugs fixed in Qt 5.6,
which would explain why I can't reproduce it.

Instead of crashing, let's log the error and not save the session.
2016-04-10 18:03:29 +02:00
Florian Bruhin
c5459abb65 Add some more logging for standarddir 2016-04-10 18:03:29 +02:00
Florian Bruhin
83b7c0dd6f Fix #1414 with a weird workaround 2016-04-10 18:03:29 +02:00
Florian Bruhin
93fff9a69c freeze: Add pkg_resources._vendor.six
Another package added by pkg_resources and not picked up by cx_Freeze...
2016-04-10 18:03:29 +02:00
Florian Bruhin
b2247fa406 tox: Update setuptools in cxfreeze-windows env
In 43a4a6a3e7 we added
pkg_resources._vendor.pyparsing to the frozen modules - however, older
setuptools (and thus pkg_resources) versions don't have that module yet,
so freezing would fail.
2016-04-10 18:03:29 +02:00
Florian Bruhin
fafba9af3f build_release: Don't call update_3rdparty.main
main tries to parse sys.argv which will fail when called from
build_release when e.g. --asciidoc is given.
2016-04-10 18:03:29 +02:00
375 changed files with 8046 additions and 15939 deletions

View File

@@ -11,7 +11,7 @@ environment:
- TESTENV: pylint
install:
- C:\Python27\python -u scripts\dev\ci\appveyor_install.py
- C:\Python27\python -u scripts\dev\ci\install.py
test_script:
- C:\Python34\Scripts\tox -e %TESTENV%

View File

@@ -1,17 +0,0 @@
[*]
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
charset = utf-8
max_line_length = 79
indent_style = space
indent_size = 4
[*.yml]
indent_size = 2
[*.feature]
max_line_length = 9999

6
.gitignore vendored
View File

@@ -1,5 +1,4 @@
__pycache__
*.py~
*.pyc
*.swp
/build
@@ -31,9 +30,4 @@ __pycache__
/.testmondata
/.hypothesis
/prof
/venv
TODO
/scripts/testbrowser_cpp/Makefile
/scripts/testbrowser_cpp/main.o
/scripts/testbrowser_cpp/testbrowser
/scripts/dev/pylint_checkers/qute_pylint.egg-info

View File

@@ -31,8 +31,7 @@ disable=no-self-use,
wrong-import-order,
ungrouped-imports,
redefined-variable-type,
suppressed-message,
too-many-return-statements
suppressed-message
[BASIC]
function-rgx=[a-z_][a-z0-9_]{2,50}$
@@ -58,9 +57,6 @@ dummy-variables-rgx=_.*
[DESIGN]
max-args=10
[CLASSES]
valid-metaclass-classmethod-first-arg=cls
[TYPECHECK]
# MsgType added as WORKAROUND for
# https://bitbucket.org/logilab/pylint/issues/690/

View File

@@ -1,11 +1,17 @@
sudo: required
dist: trusty
language: generic
env:
global:
- PATH=/home/travis/bin:/home/travis/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
# Not really, but this is here so we can do stuff by hand.
language: c
matrix:
include:
- os: linux
env: TESTENV=py34-cov
- os: linux
env: TESTENV=unittests-nodisp
- os: linux
env: DOCKER=debian-jessie
services: docker
@@ -15,9 +21,6 @@ matrix:
- os: linux
env: DOCKER=ubuntu-wily
services: docker
- os: linux
env: DOCKER=ubuntu-xenial
services: docker
- os: osx
env: TESTENV=py35
- os: linux
@@ -45,12 +48,8 @@ cache:
- $HOME/.cache/pip
- $HOME/build/The-Compiler/qutebrowser/.cache
before_install:
# We need to do this so we pick up the system-wide python properly
- 'export PATH="/usr/bin:$PATH"'
install:
- bash scripts/dev/ci/travis_install.sh
- python scripts/dev/ci/install.py
script:
- bash scripts/dev/ci/travis_run.sh

View File

@@ -14,92 +14,6 @@ This project adheres to http://semver.org/[Semantic Versioning].
// `Fixed` for any bug fixes.
// `Security` to invite users to upgrade in case of vulnerabilities.
v0.7.0
------
Added
~~~~~
- New `:edit-url` command to edit the URL in an external editor.
- New `network -> custom-headers` setting to send custom headers with every request.
- New `{url:pretty}` commandline replacement which gets replaced by the decoded URL.
- New marks to remember a scroll position:
- New `:jump-mark` command to jump to a mark, bound to `'`
- New `:set-mark` command to set a mark, bound to ```(backtick)
- The `'` mark gets set when moving away (hinting link with anchor, searching, etc.) so you can move back with `''`
- New `--force-color` argument to force colored logging even if stdout is not a
terminal
- New `:messages` command to show error messages
- New pop-up showing possible keybinding when the first key of a keychain is
pressed. This can be turned off using `:set ui keyhint-blacklist *`.
- New `hints -> auto-follow-timeout` setting to ignore keypresses after
following a hint when filtering in number mode.
- New `:history-clear` command to clear the entire history
- New `hints -> find-implementation` to select which implementation (JS/Python)
should be used to find hints on a page. The `javascript` implementation is
better, but slower.
Changed
~~~~~~~
- qutebrowser got a new (slightly updated) logo
- `:tab-focus` can now take a negative index to focus the nth tab counted from
the right.
- `:yank` can now yank the pretty/decoded URL by adding `--pretty`
- `:navigate` now clears the URL fragment
- `:completion-item-del` (`Ctrl-D`) can now be used in `:buffer` completion to
close a tab
- Counts can now be used with special keybindings (e.g. with modifiers)
- Various SSL ciphers are now disabled by default. With recent Qt/OpenSSL
versions those already all are disabled, but with older versions they might
not be.
- Show favicons as window icon with `tabs-are-windows` set.
- `:bind <key>` without a command now shows the existing binding
- The optional `colorlog` dependency got removed, as qutebrowser now displays
colored logs without it.
- URLs are now shown decoded when hovering.
- Keybindings are now shown in the command completion
- Improved behavior when pasting multiple lines
- Rapid hints can now also be used for the `normal` hint target, which can be
useful with javascript click handlers or checkboxes which don't actually open
a new page.
- `:zoom-in` or `:zoom-out` (`+`/`-`) with a too large count now zooms to the
smallest/largest zoom instead of doing nothing.
- The commandline now accepts partially typed commands if they're unique.
- Number hints are now kept filtered after following a hint in rapid mode.
- Number hints are now renumbered after filtering
- Number hints can now be filtered with multiple space-separated search terms
- `hints -> scatter` is now ignored for number hints
- Better history implementation which also stores titles.
As a consequence, URLs which redirect to another URL are now added to the
history too, marked with a `-r` suffix to the timestamp field.
Fixed
-----
- Fixed using `:hint links spawn` with flags - you can now use things like the
`-v` argument for `:spawn` or pass flags to the spawned commands.
- Various fixes for hinting corner-cases where following a link didn't work or
the hint was drawn at the wrong position.
- Fixed crash when downloading from an URL with SSL errors
- Close file handles correctly when a download failed
- Fixed crash when using `;Y` (`:hint links yank-primary`) on a system without
primary selection
- Don't display quit confirmation with finished downloads
- Fixed updating the tab index in the statusbar when opening a background tab
- Fixed a crash when entering `:-- ` in the commandline
- Fixed `:debug-console` with PyQt 5.6
- Fixed qutebrowser not starting when `sys.stderr` is `None`
- Fixed crash when cancelling a download which belongs to a MHTML download
- Fixed rebinding of keybindings being case-sensitive
- Fix for tab indicators getting lost when moving tabs
- Fixed handling of backspace in number hinting mode
- Fixed `FileNotFoundError` when starting in some cases on old Qt versions
- Fixed sharing of cookies between tabs when `private-browsing` is enabled
- Toggling values with `:set` now uses lower-case values
- Hints now work with (non-standard) links with spaces around the URL
- Strip off trailing spaces for history entries with no title
v0.6.2
------
@@ -115,7 +29,7 @@ Fixed
- Fixed various crashes related to PyQt 5.6
v0.6.1
-----
------
Fixed
~~~~~~

View File

@@ -158,11 +158,11 @@ Examples:
# run only pytest tests which failed in last run:
tox -e py35 -- --lf
# run only the end2end feature tests:
tox -e py35 -- tests/end2end/features
# run only the integration feature tests:
tox -e py35 -- tests/integration/features
# run everything with undo in the generated name, based on the scenario text
tox -e py35 -- tests/end2end/features/test_tabs_bdd.py -k undo
tox -e py35 -- tests/integration/features/test_tabs.py -k undo
# run coverage test for specific file (updates htmlcov/index.html)
tox -e py35-cov -- tests/unit/browser/test_webelem.py
@@ -174,14 +174,10 @@ Profiling
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.
It uses the built-in Python
https://docs.python.org/3.4/library/profile.html[cProfile] module and can show
the output in four different ways:
* Raw profile file (`--profile-tool=none`)
* https://pypi.python.org/pypi/pyprof2calltree/[pyprof2calltree] and http://kcachegrind.sourceforge.net/html/Home.html[KCacheGrind] (`--profile-tool=kcachegrind`)
* https://jiffyclub.github.io/snakeviz/[SnakeViz] (`--profile-tool=snakeviz`)
* https://github.com/jrfonseca/gprof2dot[gprof2dot] (needs `dot` from http://graphviz.org/[Graphviz] and http://feh.finalrewind.org/[feh])
It needs https://pypi.python.org/pypi/pyprof2calltree/[pyprof2calltree] and
http://kcachegrind.sourceforge.net/html/Home.html[KCacheGrind]. It uses the
built-in Python https://docs.python.org/3.4/library/profile.html[cProfile]
module.
Debugging
~~~~~~~~~
@@ -216,6 +212,7 @@ Documentation of used Python libraries:
* http://pythonhosted.org/setuptools/[setuptools]
* http://cx-freeze.readthedocs.org/en/latest/overview.html[cx_Freeze]
* https://pypi.python.org/pypi/colorama[colorama]
* https://pypi.python.org/pypi/colorlog[colorlog]
Related RFCs and standards:
@@ -374,7 +371,7 @@ The following logging levels are available for every logger:
[width="75%",cols="25%,75%"]
|=======================================================================
|critical |Critical issue, qutebrowser can't continue to run.
|criticial |Critical issue, qutebrowser can't continue to run.
|error |There was an issue and some kind of operation was abandoned.
|warning |There was an issue but the operation can continue running.
|info |General informational messages.
@@ -420,59 +417,35 @@ The types of the function arguments are inferred based on their default values,
e.g. an argument `foo=True` will be converted to a flag `-f`/`--foo` in
qutebrowser's commandline.
The type can be overridden using Python's
http://legacy.python.org/dev/peps/pep-3107/[function annotations]:
This behavior can be overridden using Python's
http://legacy.python.org/dev/peps/pep-3107/[function annotations]. The
annotation should always be a `dict`, like this:
[source,python]
----
@cmdutils.register(...)
def foo(bar: int, baz=True):
def foo(bar: {'type': int}, baz=True):
...
----
Possible values:
The following keys are supported in the dict:
* `type`: The type this value should have. The value entered by the user is
then automatically checked. Possible values:
- A callable (`int`, `float`, etc.): Gets called to validate/convert the
value.
- A string: The value must match exactly (mainly useful with tuples to get
a choice of values, see below).
- A python enum type: All members of the enum are possible values.
- A `typing.Union` of multiple types above: Any of these types are valid
values, e.g. `typing.Union[str, int]`
You can customize how an argument is handled using the `@cmdutils.argument`
decorator *after* `@cmdutils.register`. This can e.g. be used to customize the
flag an argument should get:
[source,python]
----
@cmdutils.register(...)
@cmdutils.argument('bar', flag='c')
def foo(bar):
...
----
For a `str` argument, you can restrict the allowed strings using `choices`:
[source,python]
----
@cmdutils.register(...)
@cmdutils.argument('bar', choices=['val1', 'val2'])
def foo(bar: str):
...
----
For `typing.Union` types, the given `choices` are only checked if other types
(like `int`) don't match.
The following arguments are supported for `@cmdutils.argument`:
- `flag`: Customize the short flag (`-x`) the argument will get.
- `win_id=True`: Mark the argument as special window ID argument
- `count=True`: Mark the argument as special count argument
- `hide=True`: Hide the argument from the documentation
- `completion`: A `usertypes.Completion` member to use as completion.
- `choices`: The allowed string choices for the argument.
- A tuple of multiple types above: Any of these types are valid values,
e.g. `('foo', 'bar')` or `(int, 'foo')`.
* `flag`: The flag to be used, as 1-char string (default: First char of the
long name).
* `nargs`: Gets passed to argparse, see
https://docs.python.org/dev/library/argparse.html#nargs[its documentation].
The name of an argument will always be the parameter name, with any trailing
underscores stripped and underscores replaced by dashes.
underscores stripped.
[[handling-urls]]
Handling URLs
@@ -595,7 +568,10 @@ Return:
* The layout of a class should be like this:
- docstring
- `__magic__` methods
- other methods
- properties
- _private methods
- public methods
- `on_*` methods
- overrides of Qt methods
Checklists
@@ -617,14 +593,6 @@ workaround.
* Check relevant
https://github.com/The-Compiler/qutebrowser/issues?q=is%3Aopen+is%3Aissue+label%3Aqt[qutebrowser
bugs] and check if they're fixed.
* As soon as Homebrew updated, update the custom OS X bottle:
- Update https://github.com/The-Compiler/homebrew-qt5-webkit/blob/master/Formula/qt5.rb[qt5.rb]
- `brew install --build-from-source --verbose qt5.rb`
- `brew bottle qt5`
- `brew install --build-from-source --verbose pyqt5`
- `brew bottle pyqt5`
- Upload bottles to github
- Adjust `scripts/dev/ci/travis_install.sh`
New PyQt release
~~~~~~~~~~~~~~~~
@@ -638,11 +606,6 @@ qutebrowser release
* Make sure there are no unstaged changes and the tests are green.
* Add newest config to `tests/unit/config/old_configs` and update `test_upgrade_version`
- `python -m qutebrowser --basedir conf :quit`
- `sed '/^#/d' conf/config/qutebrowser.conf > tests/unit/config/old_configs/qutebrowser-v0.x.y.conf`
- `rm -r conf`
- commit
* Adjust `__version_info__` in `qutebrowser/__init__.py`.
* Remove *(unreleased)* from changelog.
* Run tests again
@@ -650,6 +613,7 @@ qutebrowser release
* Commit
* Create annotated git tag (`git tag -s "v0.X.Y" -m "Release v0.X.Y"`)
* If it's a new minor, create git branch `v0.X.x`
* If committing on minor branch, cherry-pick release commit to master.
* `git push origin`; `git push origin v0.X.Y`
* Create release on github
@@ -658,16 +622,12 @@ as closed.
* Run `scripts/dev/build_release.py` on Linux to build an sdist
* Upload to PyPI: `twine upload dist/foo{,.asc}`
* Create Windows packages via `C:\Python34_x32\python scripts\dev\build_release.py --asciidoc C:\Python27\python C:\asciidoc-8.6.9\asciidoc.py`
* Upload to github
* Create Windows packages via `scripts/dev/build_release.py` and upload.
* Upload to qutebrowser.org with checksum/GPG
- On server: `sudo mkdir -p /srv/http/qutebrowser/releases/v0.X.Y/windows`
- `rsync -avPh dist/ tonks:`
- On server: `sudo mv qutebrowser-0.X.Y.tar.gz* /srv/http/qutebrowser/releases/v0.X.Y`
- Upload windows release:
- `scp bb-win8:proj/qutebrowser/qutebrowser-0.X.Y-windows.zip .`
- `aunpack qutebrowser-0.X.Y-windows.zip`
- `sudo mv qutebrowser-0.X.Y-windows/* /srv/http/qutebrowser/releases/v0.X.Y/windows`
* Update `qutebrowser-git` PKGBUILD
* Update AUR package
* Announce to qutebrowser mailinglist

View File

@@ -80,27 +80,13 @@ How do I play Youtube videos with mpv?::
player - optionally even with hinting for links:
+
----
:bind m spawn mpv {url}
:bind M hint links spawn mpv {hint-url}
:bind x spawn mpv {url}
:bind ;x hint links spawn mpv {hint-url}
----
+
Note that you might need an additional package (e.g.
https://www.archlinux.org/packages/community/any/youtube-dl/[youtube-dl] on
Archlinux) to play web videos with mpv.
+
There is a very useful script for mpv, which emulates "unique application"
functionality. This way you can add links to the mpv playlist instead of
playing them all at once.
+
You can find the script here: https://github.com/mpv-player/mpv/blob/master/TOOLS/umpv
+
It also works nicely with rapid hints:
+
----
:bind m spawn umpv {url}
:bind M hint links spawn umpv {hint-url}
:bind ;M hint --rapid links spawn umpv {hint-url}
----
How do I use qutebrowser with mutt?::
Due to a Qt limitation, local files without `.html` extensions are

View File

@@ -10,31 +10,9 @@ qutebrowser should run on these systems:
* 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
still relatively easy!
You can use packages that are built for every release or build it yourself from git.
Using the packages
~~~~~~~~~~~~~~~~~~
Install the dependencies via apt-get:
----
# apt-get install python3-lxml python-tox python3-pyqt5 python3-pyqt5.qtwebkit python3-sip python3-jinja2 python3-pygments python3-yaml
----
Get the packages from the https://github.com/The-Compiler/qutebrowser/releases[release page]
Install the packages:
----
# dpkg -i python3-pypeg2_*_all.deb
# dpkg -i qutebrowser_*_all.deb
----
Build it from git
~~~~~~~~~~~~~~~~~
Unfortunately there is no Debian package yet, but installing qutebrowser is
still relatively easy! If you want to help packaging it for Debian, please
https://github.com/The-Compiler/qutebrowser/issues/582[get in touch]!
Install the dependencies via apt-get:
@@ -143,13 +121,6 @@ Make sure you have `python3_4` in your `PYTHON_TARGETS`
(`/etc/portage/make.conf`) and rebuild your system (`emerge -uDNav @world`) if
necessary.
If video or sound don't seem to work, try installing the gstreamer plugins:
----
# emerge -av gst-plugins-{base,good,bad,ugly,libav}
----
On Void Linux
-------------
@@ -229,25 +200,21 @@ Then <<tox,install qutebrowser via tox>>.
On OS X
-------
*Using qutebrowser with Homebrew on OS X is currently broken, as Homebrew
dropped QtWebKit support with Qt 5.6. I'm working on building a standalone
`.app` for OS X instead, but it'll still take a few days until it's ready.*
To install qutebrowser on OS X, you'll want a package manager, e.g.
http://brew.sh/[Homebrew] or https://www.macports.org/[MacPorts].
For Homebrew, a few extra steps are necessary since Homebrew dropped QtWebKit
from Qt 5.6.
This installs a Qt 5.5 and symlinks it so PyQt5 will work with it instead of Qt
5.6. This requires that `qt5` is not installed via Homebrew:
http://brew.sh/[Homebrew] or https://www.macports.org/[MacPorts]. Also make
sure, you have https://itunes.apple.com/en/app/xcode/id497799835[XCode]
installed to compile PyQt5 in a later step.
----
$ brew install python3 d-bus mysql sip xz
$ brew install homebrew/versions/qt55
$ brew install --ignore-dependencies pyqt5
$ ln -s /usr/local/opt/qt55 /usr/local/opt/qt5
$ brew install python3 pyqt5
$ pip3.5 install qutebrowser
----
For MacPorts, run:
if you are using Homebrew. For MacPorts, run:
----
$ sudo port install python34 py34-jinja2 asciidoc py34-pygments py34-pyqt5

View File

@@ -18,14 +18,12 @@ include qutebrowser.py
prune www
prune scripts/dev
prune scripts/testbrowser_cpp
exclude scripts/asciidoc2html.py
exclude doc/notes
recursive-exclude doc *.asciidoc
include doc/qutebrowser.1.asciidoc
prune tests
prune qutebrowser/3rdparty
exclude .editorconfig
exclude pytest.ini
exclude qutebrowser.rcc
exclude .coveragerc
@@ -35,9 +33,7 @@ exclude .eslintignore
exclude doc/help
exclude .appveyor.yml
exclude .travis.yml
exclude codecov.yml
exclude .pydocstylerc
exclude misc/appveyor_install.py
exclude .flake8
global-exclude __pycache__ *.pyc *.pyo

View File

@@ -15,8 +15,6 @@ image:https://requires.io/github/The-Compiler/qutebrowser/requirements.svg?branc
image:https://travis-ci.org/The-Compiler/qutebrowser.svg?branch=master["Build Status", link="https://travis-ci.org/The-Compiler/qutebrowser"]
image:https://ci.appveyor.com/api/projects/status/9gmnuip6i1oq7046?svg=true["AppVeyor build status", link="https://ci.appveyor.com/project/The-Compiler/qutebrowser"]
image:https://codecov.io/github/The-Compiler/qutebrowser/coverage.svg?branch=master["coverage badge",link="https://codecov.io/github/The-Compiler/qutebrowser?branch=master"]
link:http://www.qutebrowser.org[website] | link:http://blog.qutebrowser.org[blog] | link:https://github.com/The-Compiler/qutebrowser/releases[releases]
// QUTE_WEB_HIDE_END
qutebrowser is a keyboard-focused browser with a minimal GUI. It's based
@@ -24,6 +22,12 @@ on Python, PyQt5 and QtWebKit and free software, licensed under the GPL.
It was inspired by other browsers/addons like dwb and Vimperator/Pentadactyl.
// QUTE_WEB_HIDE
**qutebrowser is currently running a crowdfunding campaign to add support for
the QtWebEngine backend, which fixes many issues. Please
link:http://igg.me/at/qutebrowser[check it out]!**
// QUTE_WEB_HIDE_END
Screenshots
-----------
@@ -111,8 +115,11 @@ The following libraries are optional and provide a better user experience:
To generate the documentation for the `:help` command, when using the git
repository (rather than a release), http://asciidoc.org/[asciidoc] is needed.
On Windows, https://pypi.python.org/pypi/colorama/[colorama] is needed to
display colored log output.
The following libraries are optional and provide colored logging in the
console:
* https://pypi.python.org/pypi/colorlog/[colorlog]
* On Windows: https://pypi.python.org/pypi/colorama/[colorama]
See link:INSTALL.asciidoc[INSTALL] for directions on how to install qutebrowser
and its dependencies.
@@ -142,89 +149,76 @@ Contributors, sorted by the number of commits in descending order:
* Florian Bruhin
* Daniel Schadt
* Antoni Boucher
* Ryan Roden-Corrent
* Lamar Pavel
* Bruno Oliveira
* Alexander Cogneau
* Felix Van der Jeugt
* Martin Tournoij
* Jakub Klinkovský
* Felix Van der Jeugt
* Raphael Pierzina
* Joel Torstensson
* Tarcisio Fedrizzi
* Patric Schmitz
* Claude
* Corentin Julé
* meles5
* Philipp Hansch
* Panagiotis Ktistakis
* Tarcisio Fedrizzi
* Artur Shaik
* Nathan Isom
* Thorsten Wißmann
* Philipp Hansch
* Kevin Velghe
* Austin Anderson
* Jimmy
* Alexey "Averrin" Nabrodov
* avk
* ZDarian
* Milan Svoboda
* John ShaggyTwoDope Jenkins
* Jimmy
* Peter Vilim
* Clayton Craft
* Oliver Caldwell
* Jonas Schürmann
* error800
* Liam BEGUIN
* Panagiotis Ktistakis
* Jakub Klinkovský
* skinnay
* error800
* Zach-Button
* Tomasz Kramkowski
* Halfwit
* rikn00
* kanikaa1234
* haitaka
* Nick Ginther
* Michael Ilsaas
* Martin Zimmermann
* Fritz Reichwald
* Brian Jackson
* sbinix
* neeasade
* jnphilipp
* Tobias Patzl
* Stefan Tatschner
* Samuel Loury
* Peter Michely
* Link
* Larry Hynes
* Johannes Altmanninger
* Ismail
* adam
* Samir Benmendil
* Ryan Roden-Corrent
* Regina Hug
* Mathias Fussenegger
* Marcelo Santos
* Jan Verbeek
* Fritz V155 Reichwald
* Franz Fellner
* Corentin Jule
* zwarag
* xd1le
* oniondreams
* issue
* haxwithaxe
* evan
* dylan araps
* Xitian9
* Tomasz Kramkowski
* Tomas Orsava
* Tobias Werth
* Tim Harder
* Thiago Barroso Perrotta
* Stefan Tatschner
* Sorokin Alexei
* Noah Huesser
* Samuel Loury
* Matthias Lisin
* Marcel Schilling
* Johannes Martinsson
* Jean-Christophe Petkovich
* Jay Kamat
* Helen Sherwood-Taylor
* HalosGhost
* Gregor Pohl
@@ -237,8 +231,7 @@ Contributors, sorted by the number of commits in descending order:
The following people have contributed graphics:
* Jad/link:http://yelostudio.com[yelo] (new icon)
* WOFall (original icon)
* WOFall (icon)
* regines (key binding cheatsheet)
Thanks / Similar projects

View File

@@ -1,9 +0,0 @@
status:
project:
enabled: no
patch:
enabled: no
changes:
enabled: no
comment: off

View File

@@ -20,19 +20,15 @@
|<<download-open,download-open>>|Open the last/[count]th download.
|<<download-remove,download-remove>>|Remove the last/[count]th download from the list.
|<<download-retry,download-retry>>|Retry the first failed/[count]th download.
|<<edit-url,edit-url>>|Navigate to a url formed in an external editor.
|<<fake-key,fake-key>>|Send a fake keypress or key string to the website or qutebrowser.
|<<forward,forward>>|Go forward in the history of the current tab.
|<<fullscreen,fullscreen>>|Toggle fullscreen mode.
|<<help,help>>|Show help about a command or setting.
|<<hint,hint>>|Start hinting.
|<<history-clear,history-clear>>|Clear all browsing history.
|<<home,home>>|Open main startpage in current tab.
|<<inspector,inspector>>|Toggle the web inspector.
|<<jseval,jseval>>|Evaluate a JavaScript string.
|<<jump-mark,jump-mark>>|Jump to the mark named by `key`.
|<<later,later>>|Execute a command after some time.
|<<messages,messages>>|Show a log of past messages.
|<<navigate,navigate>>|Open typical prev/next links or navigate using the URL path.
|<<open,open>>|Open a URL in the current/[count]th tab.
|<<paste,paste>>|Open a page from the clipboard.
@@ -53,7 +49,6 @@
|<<session-save,session-save>>|Save a session.
|<<set,set>>|Set an option.
|<<set-cmd-text,set-cmd-text>>|Preset the statusbar to some text.
|<<set-mark,set-mark>>|Set a mark at the current scroll position in the current tab.
|<<spawn,spawn>>|Spawn a command in a shell.
|<<stop,stop>>|Stop loading in the current/[count]th tab.
|<<tab-clone,tab-clone>>|Duplicate the current tab.
@@ -96,14 +91,13 @@ How many pages to go back.
[[bind]]
=== bind
Syntax: +:bind [*--mode* 'mode'] [*--force*] 'key' ['command']+
Syntax: +:bind [*--mode* 'MODE'] [*--force*] 'key' 'command'+
Bind a key to a command.
==== positional arguments
* +'key'+: The keychain or special key (inside `<...>`) to bind.
* +'command'+: The command to execute, with optional args, or not given to print the current binding.
* +'command'+: The command to execute, with optional args.
==== optional arguments
* +*-m*+, +*--mode*+: A comma-separated list of modes to bind the key in (default: `normal`).
@@ -129,6 +123,7 @@ Delete a bookmark.
==== note
* This command does not split arguments after the last argument and handles quotes literally.
* With this command, +;;+ is interpreted literally instead of splitting off a second command.
[[bookmark-load]]
=== bookmark-load
@@ -146,6 +141,7 @@ Load a bookmark.
==== note
* This command does not split arguments after the last argument and handles quotes literally.
* With this command, +;;+ is interpreted literally instead of splitting off a second command.
[[buffer]]
=== buffer
@@ -165,7 +161,7 @@ Close the current window.
[[download]]
=== download
Syntax: +:download [*--mhtml*] [*--dest* 'dest'] ['url'] ['dest-old']+
Syntax: +:download [*--mhtml*] [*--dest* 'DEST'] ['url'] ['dest-old']+
Download a given URL, or current page if no URL given.
@@ -227,25 +223,6 @@ Retry the first failed/[count]th download.
==== count
The index of the download to retry.
[[edit-url]]
=== edit-url
Syntax: +:edit-url [*--bg*] [*--tab*] [*--window*] ['url']+
Navigate to a url formed in an external editor.
The editor which should be launched can be configured via the `general -> editor` config option.
==== positional arguments
* +'url'+: URL to edit; defaults to the current page url.
==== optional arguments
* +*-b*+, +*--bg*+: Open in a new background tab.
* +*-t*+, +*--tab*+: Open in a new tab.
* +*-w*+, +*--window*+: Open in a new window.
==== count
The tab index to open the URL in.
[[fake-key]]
=== fake-key
Syntax: +:fake-key [*--global*] 'keystring'+
@@ -354,15 +331,6 @@ Start hinting.
`window`, `run`, `hover`, `userscript` and `spawn`.
==== note
* This command does not split arguments after the last argument and handles quotes literally.
[[history-clear]]
=== history-clear
Clear all browsing history.
Note this only clears the global history (e.g. `~/.local/share/qutebrowser/history` on Linux) but not cookies, the back/forward history of a tab, cache or other persistent data.
[[home]]
=== home
Open main startpage in current tab.
@@ -389,15 +357,6 @@ Evaluate a JavaScript string.
* This command does not split arguments after the last argument and handles quotes literally.
* With this command, +;;+ is interpreted literally instead of splitting off a second command.
[[jump-mark]]
=== jump-mark
Syntax: +:jump-mark 'key'+
Jump to the mark named by `key`.
==== positional arguments
* +'key'+: mark identifier; capital indicates a global mark
[[later]]
=== later
Syntax: +:later 'ms' 'command'+
@@ -412,22 +371,6 @@ Execute a command after some time.
* This command does not split arguments after the last argument and handles quotes literally.
* With this command, +;;+ is interpreted literally instead of splitting off a second command.
[[messages]]
=== messages
Syntax: +:messages [*--plain*] [*--tab*] [*--bg*] [*--window*] ['level']+
Show a log of past messages.
==== positional arguments
* +'level'+: Include messages with `level` or higher severity. Valid values: vdebug, debug, info, warning, error, critical.
==== optional arguments
* +*-p*+, +*--plain*+: Whether to show plaintext (as opposed to html).
* +*-t*+, +*--tab*+: Open in a new tab.
* +*-b*+, +*--bg*+: Open in a background tab.
* +*-w*+, +*--window*+: Open in a new window.
[[navigate]]
=== navigate
Syntax: +:navigate [*--tab*] [*--bg*] [*--window*] 'where'+
@@ -472,6 +415,7 @@ The tab index to open the URL in.
==== note
* This command does not split arguments after the last argument and handles quotes literally.
* With this command, +;;+ is interpreted literally instead of splitting off a second command.
[[paste]]
=== paste
@@ -520,6 +464,7 @@ Delete a quickmark.
==== note
* This command does not split arguments after the last argument and handles quotes literally.
* With this command, +;;+ is interpreted literally instead of splitting off a second command.
[[quickmark-load]]
=== quickmark-load
@@ -537,6 +482,7 @@ Load a quickmark.
==== note
* This command does not split arguments after the last argument and handles quotes literally.
* With this command, +;;+ is interpreted literally instead of splitting off a second command.
[[quickmark-save]]
=== quickmark-save
@@ -604,6 +550,7 @@ Search for a text on the current page. With no text, clear results.
==== note
* This command does not split arguments after the last argument and handles quotes literally.
* With this command, +;;+ is interpreted literally instead of splitting off a second command.
[[session-delete]]
=== session-delete
@@ -671,8 +618,6 @@ Syntax: +:set-cmd-text [*--space*] [*--append*] 'text'+
Preset the statusbar to some text.
You can use the `{url}` and `{url:pretty}` variables here which will get replaced by the encoded/decoded URL.
==== positional arguments
* +'text'+: The commandline to set.
@@ -682,15 +627,7 @@ You can use the `{url}` and `{url:pretty}` variables here which will get replace
==== note
* This command does not split arguments after the last argument and handles quotes literally.
[[set-mark]]
=== set-mark
Syntax: +:set-mark 'key'+
Set a mark at the current scroll position in the current tab.
==== positional arguments
* +'key'+: mark identifier; capital indicates a global mark
* With this command, +;;+ is interpreted literally instead of splitting off a second command.
[[spawn]]
=== spawn
@@ -698,7 +635,7 @@ Syntax: +:spawn [*--userscript*] [*--verbose*] [*--detach*] 'cmdline'+
Spawn a command in a shell.
Note the `{url}` and `{url:pretty}` variables might be useful here. `{url}` gets replaced by the URL in fully encoded format and `{url:pretty}` uses a "pretty form" with most percent-encoded characters decoded.
Note the {url} variable which gets replaced by the current URL might be useful here.
==== positional arguments
* +'cmdline'+: The commandline to execute.
@@ -715,6 +652,7 @@ Note the `{url}` and `{url:pretty}` variables might be useful here. `{url}` gets
==== note
* This command does not split arguments after the last argument and handles quotes literally.
* With this command, +;;+ is interpreted literally instead of splitting off a second command.
[[stop]]
=== stop
@@ -761,8 +699,7 @@ Select the tab given as argument/[count].
If neither count nor index are given, it behaves like tab-next.
==== positional arguments
* +'index'+: The tab index to focus, starting with 1. The special value `last` focuses the last focused tab. Negative indexes
counts from the end, such that -1 is the last tab.
* +'index'+: The tab index to focus, starting with 1. The special value `last` focuses the last focused tab.
==== count
@@ -836,7 +773,7 @@ Save open pages and quit.
[[yank]]
=== yank
Syntax: +:yank [*--title*] [*--sel*] [*--domain*] [*--pretty*]+
Syntax: +:yank [*--title*] [*--sel*] [*--domain*]+
Yank the current URL/title to the clipboard or primary selection.
@@ -844,7 +781,6 @@ Yank the current URL/title to the clipboard or primary selection.
* +*-t*+, +*--title*+: Yank the title instead of the URL.
* +*-s*+, +*--sel*+: Use the primary selection instead of the clipboard.
* +*-d*+, +*--domain*+: Yank only the scheme, domain, and port number.
* +*-p*+, +*--pretty*+: Yank the URL in pretty decoded form.
[[yank-selected]]
=== yank-selected
@@ -1286,7 +1222,7 @@ Scroll the current tab by 'count * dx/dy' pixels.
==== positional arguments
* +'dx'+: How much to scroll in x-direction.
* +'dy'+: How much to scroll in y-direction.
* +'dy'+: How much to scroll in x-direction.
==== count
multiplier

View File

@@ -49,7 +49,6 @@
|<<ui-hide-mouse-cursor,hide-mouse-cursor>>|Whether to hide the mouse cursor.
|<<ui-modal-js-dialog,modal-js-dialog>>|Use standard JavaScript modal dialog for alert() and confirm()
|<<ui-hide-wayland-decoration,hide-wayland-decoration>>|Hide the window decoration when using wayland (requires restart)
|<<ui-keyhint-blacklist,keyhint-blacklist>>|Keychains that shouldn't be shown in the keyhint dialog
|==============
.Quick reference for section ``network''
@@ -64,7 +63,6 @@
|<<network-proxy-dns-requests,proxy-dns-requests>>|Whether to send DNS requests over the configured proxy.
|<<network-ssl-strict,ssl-strict>>|Whether to validate SSL handshakes.
|<<network-dns-prefetch,dns-prefetch>>|Whether to try to pre-fetch DNS entries to speed up browsing.
|<<network-custom-headers,custom-headers>>|Set custom headers for qutebrowser HTTP requests.
|==============
.Quick reference for section ``completion''
@@ -179,14 +177,12 @@
|<<hints-mode,mode>>|Mode to use for hints.
|<<hints-chars,chars>>|Chars used for hint strings.
|<<hints-min-chars,min-chars>>|Minimum number of chars used for hint strings.
|<<hints-scatter,scatter>>|Whether to scatter hint key chains (like Vimium) or not (like dwb). Ignored for number hints.
|<<hints-scatter,scatter>>|Whether to scatter hint key chains (like Vimium) or not (like dwb).
|<<hints-uppercase,uppercase>>|Make chars in hint strings uppercase.
|<<hints-dictionary,dictionary>>|The dictionary file to be used by the word hints.
|<<hints-auto-follow,auto-follow>>|Follow a hint immediately when the hint text is completely matched.
|<<hints-auto-follow-timeout,auto-follow-timeout>>|A timeout to inhibit normal-mode key bindings after a successfulauto-follow.
|<<hints-next-regexes,next-regexes>>|A comma-separated list of regexes to use for 'next' links.
|<<hints-prev-regexes,prev-regexes>>|A comma-separated list of regexes to use for 'prev' links.
|<<hints-find-implementation,find-implementation>>|Which implementation to use to find elements to hint.
|==============
.Quick reference for section ``colors''
@@ -256,9 +252,6 @@
|<<colors-downloads.fg.error,downloads.fg.error>>|Foreground color for downloads with errors.
|<<colors-downloads.bg.error,downloads.bg.error>>|Background color for downloads with errors.
|<<colors-webpage.bg,webpage.bg>>|Background color for webpages if unset (or empty to use the theme's color)
|<<colors-keyhint.fg,keyhint.fg>>|Text color for the keyhint widget.
|<<colors-keyhint.fg.suffix,keyhint.fg.suffix>>|Highlight color for keys to complete the current keychain
|<<colors-keyhint.bg,keyhint.bg>>|Background color of the keyhint widget.
|==============
.Quick reference for section ``fonts''
@@ -282,7 +275,6 @@
|<<fonts-web-size-minimum-logical,web-size-minimum-logical>>|The minimum logical font size that is applied when zooming out.
|<<fonts-web-size-default,web-size-default>>|The default font size for regular text.
|<<fonts-web-size-default-fixed,web-size-default-fixed>>|The default font size for fixed-pitch text.
|<<fonts-keyhint,keyhint>>|Font used in the keyhint widget.
|==============
== general
@@ -673,14 +665,6 @@ Valid values:
Default: +pass:[false]+
[[ui-keyhint-blacklist]]
=== keyhint-blacklist
Keychains that shouldn't be shown in the keyhint dialog
Globs are supported, so ';*' will blacklist all keychainsstarting with ';'. Use '*' to disable keyhints
Default: empty
== network
Settings related to the network.
@@ -766,12 +750,6 @@ Valid values:
Default: +pass:[true]+
[[network-custom-headers]]
=== custom-headers
Set custom headers for qutebrowser HTTP requests.
Default: empty
== completion
Options related to completion and command history.
@@ -878,17 +856,13 @@ Options related to input modes.
=== timeout
Timeout for ambiguous key bindings.
If the current input forms both a complete match and a partial match, the complete match will be executed after this time.
Default: +pass:[500]+
[[input-partial-timeout]]
=== partial-timeout
Timeout for partially typed key bindings.
If the current input forms only partial matches, the keystring will be cleared after this time.
Default: +pass:[5000]+
Default: +pass:[1000]+
[[input-insert-mode-on-plugins]]
=== insert-mode-on-plugins
@@ -1347,7 +1321,7 @@ Valid values:
* +true+
* +false+
Default: +pass:[false]+
Default: +pass:[true]+
[[content-css-regions]]
=== css-regions
@@ -1583,7 +1557,7 @@ Default: +pass:[1]+
[[hints-scatter]]
=== scatter
Whether to scatter hint key chains (like Vimium) or not (like dwb). Ignored for number hints.
Whether to scatter hint key chains (like Vimium) or not (like dwb).
Valid values:
@@ -1620,12 +1594,6 @@ Valid values:
Default: +pass:[true]+
[[hints-auto-follow-timeout]]
=== auto-follow-timeout
A timeout to inhibit normal-mode key bindings after a successfulauto-follow.
Default: +pass:[0]+
[[hints-next-regexes]]
=== next-regexes
A comma-separated list of regexes to use for 'next' links.
@@ -1638,17 +1606,6 @@ A comma-separated list of regexes to use for 'prev' links.
Default: +pass:[\bprev(ious)?\b,\bback\b,\bolder\b,\b[&lt;←≪]\b,\b(&lt;&lt;|«)\b]+
[[hints-find-implementation]]
=== find-implementation
Which implementation to use to find elements to hint.
Valid values:
* +javascript+: Better but slower
* +python+: Slightly worse but faster
Default: +pass:[javascript]+
== searchengines
Definitions of search engines which can be used via the address bar.
The searchengine named `DEFAULT` is used when `general -> auto-search` is true and something else than a URL was entered to be opened. Other search engines can be used by prepending the search engine name to the search term, e.g. `:open google qutebrowser`. The string `{}` will be replaced by the search term, use `{{` and `}}` for literal `{`/`}` signs.
@@ -2073,24 +2030,6 @@ Background color for webpages if unset (or empty to use the theme's color)
Default: +pass:[white]+
[[colors-keyhint.fg]]
=== keyhint.fg
Text color for the keyhint widget.
Default: +pass:[#FFFFFF]+
[[colors-keyhint.fg.suffix]]
=== keyhint.fg.suffix
Highlight color for keys to complete the current keychain
Default: +pass:[#FFFF00]+
[[colors-keyhint.bg]]
=== keyhint.bg
Background color of the keyhint widget.
Default: +pass:[rgba(0, 0, 0, 80%)]+
== fonts
Fonts used for the UI, with optional style/weight/size.
@@ -2199,9 +2138,3 @@ Default: empty
The default font size for fixed-pitch text.
Default: empty
[[fonts-keyhint]]
=== keyhint
Font used in the keyhint widget.
Default: +pass:[8pt ${_monospace}]+

View File

@@ -5,19 +5,6 @@ The Compiler <mail@qutebrowser.org>
NOTE: This page will only appear on the first start. To view it at a later
time, use the `:help` command.
Basic keybindings to get you started
------------------------------------
* Use the arrow keys or `hjkl` to move around a webpage (vim-like syntax is used in quite a few places)
* To go to a new webpage, press `o`, then type a url, then press Enter (Use `O` to open the url in a new tab). If what you've typed isn't a url, then a search engine will be used instead (DuckDuckGo, by default)
* To switch between tabs, use `J` (next tab) and `K` (previous tab), or press `<Alt-num>`, where `num` is the position of the tab to switch to
* To close the current tab, press `d` (and press `u` to undo closing a tab)
* Use `H` and `L` to go back and forth in the history
* To click on something without using the mouse, press `f` to show the hints, then type the keys next to what you want to click on (if that sounds weird, then just try pressing `f` and see what happens)
* Press `:` to show the commandline
* To search in a page, press `/`, type the phrase to search for, then press Enter. Use `n` and `N` to go back and forth through the matches, and press Esc to stop doing the search.
* To close qutebrowser, press `Alt-F4`, or `:q`, or `:wq` to save the currently open tabs and quit (note that in the settings you can make qutebrowser always save the currently open tabs)
What to do now
--------------

View File

@@ -78,15 +78,9 @@ show it.
*--debug*::
Turn on debugging options.
*--json-logging*::
Output log lines in JSON format (one object per line).
*--nocolor*::
Turn off colored logging.
*--force-color*::
Force colored logging
*--harfbuzz* '{old,new,system,auto}'::
HarfBuzz engine version to use. Default: auto.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 945 B

After

Width:  |  Height:  |  Size: 872 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 128 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -1,207 +0,0 @@
/* XPM */
static char *qutebrowser[] = {
/* columns rows colors chars-per-pixel */
"32 32 169 2 ",
" c #0A396E",
". c #0B3C72",
"X c #0B4077",
"o c #0C437B",
"O c #134175",
"+ c #15467C",
"@ c #18477B",
"# c #1A497D",
"$ c #0D4B86",
"% c #0F4E8D",
"& c #124A80",
"* c #1F4F83",
"= c #0E518C",
"- c #1F5084",
"; c #11508C",
": c #0F5193",
"> c #115799",
", c #115B9C",
"< c #204F83",
"1 c #245287",
"2 c #2A598C",
"3 c #325E8F",
"4 c #11609F",
"5 c #346496",
"6 c #3B6898",
"7 c #115CA1",
"8 c #115EAC",
"9 c #1263A3",
"0 c #1260AD",
"q c #136BAC",
"w c #136BB2",
"e c #1366BA",
"r c #196BB2",
"t c #157ABB",
"y c #1577BB",
"u c #2E6DB0",
"i c #387FB1",
"p c #456E9A",
"a c #4873A1",
"s c #4375AA",
"d c #507AA6",
"f c #597EA4",
"g c #4D7EB3",
"h c #156FCB",
"j c #167AC5",
"k c #1675CA",
"l c #177BCE",
"z c #1777D8",
"x c #1476E4",
"c c #167BE6",
"v c #167DE8",
"b c #197EEF",
"n c #1A7FF0",
"m c #1A80BE",
"M c #5F87AF",
"N c #5D8BBA",
"B c #5A84B1",
"V c #6C8FB3",
"C c #6F96BE",
"Z c #1886CC",
"A c #1883D7",
"S c #198DD5",
"D c #1987D9",
"F c #198ADC",
"G c #1A96DC",
"H c #3090D9",
"J c #1682E9",
"K c #1983ED",
"L c #1689E9",
"P c #1A8DEE",
"I c #1B95ED",
"U c #1C9EEA",
"Y c #1B97E4",
"T c #1A84F2",
"R c #1A8BF2",
"E c #1C94F4",
"W c #1D9CF5",
"Q c #3388E6",
"! c #3D90E9",
"~ c #228EF3",
"^ c #229FF6",
"/ c #3294F4",
"( c #3D9FF6",
") c #339CF4",
"_ c #1CA2E5",
"` c #1DABEE",
"' c #1DA4F6",
"] c #1EA9F7",
"[ c #1EADF8",
"{ c #1FB4F9",
"} c #1FB9FA",
"| c #20ACF8",
" . c #27A4F6",
".. c #3DA9F6",
"X. c #20B9FA",
"o. c #2EB6F9",
"O. c #458DC9",
"+. c #5C8DC1",
"@. c #5795C6",
"#. c #709DCB",
"$. c #74A8DD",
"%. c #4A97EA",
"&. c #4896EA",
"*. c #559EEA",
"=. c #439AF5",
"-. c #46A3F6",
";. c #5FA9F6",
":. c #5EA6F3",
">. c #47BCF9",
",. c #51B5F8",
"<. c #58BDF8",
"1. c #68ABEF",
"2. c #7DB9E7",
"3. c #63AEF7",
"4. c #6FB1F7",
"5. c #66B9F8",
"6. c #61B2F6",
"7. c #71B4F7",
"8. c #78B7F4",
"9. c #72BFF9",
"0. c #3BC0FA",
"q. c #6FCEFB",
"w. c #6CC5FA",
"e. c #7BCAF9",
"r. c #89A7C3",
"t. c #83A2C1",
"y. c #98B6D3",
"u. c #9DB9D3",
"i. c #89B6E4",
"p. c #83B6E9",
"a. c #81BDF7",
"s. c #83BFF8",
"d. c #9EC4E9",
"f. c #8CC2F9",
"g. c #85CDFB",
"h. c #87C4F9",
"j. c #92C6F9",
"k. c #95CAFA",
"l. c #9CCBFA",
"z. c #89D7FC",
"x. c #91D9FC",
"c. c #9CDEFD",
"v. c #9ED2FB",
"b. c #A7CAEC",
"n. c #B5CEE3",
"m. c #A1CEFA",
"M. c #AED0F0",
"N. c #ACD6FA",
"B. c #A0DFFC",
"V. c #AFD8FC",
"C. c #B5D9FB",
"Z. c #BCDDFC",
"A. c #BFDCF5",
"S. c #ACE3FD",
"D. c #B5E5FE",
"F. c #BBE2FC",
"G. c #CFE5F5",
"H. c #C3E1FC",
"J. c #CAE6FD",
"K. c #CCEBFD",
"L. c #C4EBFE",
"P. c #D6EDFE",
"I. c #DAEEFD",
"U. c #DEF1FE",
"Y. c #D6F2FE",
"T. c #E4F4FE",
"R. c #E9F6FE",
"E. c #EBF8FF",
"W. c None",
/* pixels */
"W.W.W.W.W.W.W.W.W.W.W.c.S.L.Y.E.E.S.X.} W.W.W.W.W.W.W.W.W.W.W.W.",
"W.W.W.W.W.W.W.W.W.D.T.E.E.T.L.D.c.z.} } X.} } W.W.W.W.W.W.W.W.W.",
"W.W.W.W.W.W.W.B.T.T.R.T.R.U.0.X.z.S.} } } } { { X.W.W.W.W.W.W.W.",
"W.W.W.W.W.W.x.x.K.T.T.T.L.P.q.o.{ } } ` _ { { { { { W.W.W.W.W.W.",
"W.W.W.W.W.c.P.D.G.u.r.i 9 Z _ { { G 4 X t { { { { { { W.W.W.W.W.",
"W.W.W.W.K.U.n.f O { = t { { { { [ { { W.W.W.W.",
"W.W.W.F.I.t.. ' t { { [ [ [ [ [ >.W.W.W.",
"W.W.x.P.V ' X t ` [ [ [ [ [ [ o.e.W.W.",
"W.W.J.y. X t S Y Z $ ' . y [ [ [ ] [ [ | Z.J.W.W.",
"W.<.e.& , _ ] ] [ ] U . ' . y [ ' [ ] ] ] w.K.J.g.W.",
"W.' S o ' ' [ ' [ ' ] o ' . y Y 9 = = 9 @.J.J.J.F.W.",
"W.| , j ' ' ' ' ' ' ' o ' . $ p A.J.J.g.",
"' .. G ' ' ' ' ' ' ' o ' . M H.H.h.",
",.2. . W ' W ' ' ' ' W . ' . M.A.x.",
"N.M.. . W W W ' W W W W .w 9 I U 0 #.Z.m.",
" .9.O D W W W W ' W j $ % F W W W .5 d Z.C.",
"W W ; 9 9.h.5...Q % o j W W W W W W O. 3 C.N.",
"E W 7 B b.d.a . w E E W W W E W E A @ C.l.",
"I E l u W E W E W E E E E A . - k.6.",
"P E E 7 m.o E E E E E E E E l . = E P ",
"L E E E > . O s.o E E E E E E E E 7 , E L ",
"W.R E R ) #.5 1 6 N i.2 s.+ E E E E E E R L . k R W.",
"W.L R E -.m.m.m.m.m.m.2 m.@ N m.m.s.( R R % X E J W.",
"W.W.K R ~ a.m.l.l.l.l.2 s.+ < i.l.m.j.h % e K W.W.",
"W.W.J R R / l.l.l.l.k.2 s.+ * 5 + 8 R J W.W.",
"W.W.W.v T R 3.k.k.j.k.2 2 j.& . 8 R v W.W.W.",
"W.W.W.W.J T ~ 7.j.j.j.g +.p.j.s.+. . . : z T v W.W.W.W.",
"W.W.W.W.W.c T T =.f.j.j.s.j.j.j.j.$.g s u e h b T T v W.W.W.W.W.",
"W.W.W.W.W.W.c b n 4.f.f.s.m.s.s.s.j.s.j./ T n T b c W.W.W.W.W.W.",
"W.W.W.W.W.W.W.c x 1.s.s.s.s.s.s.s.s.4.=.n T n c c W.W.W.W.W.W.W.",
"W.W.W.W.W.W.W.W.W.&.*.1.a.s.s.s.s.3.n n v x x W.W.W.W.W.W.W.W.W.",
"W.W.W.W.W.W.W.W.W.W.W.%.%.%.%.*.*.Q x x x W.W.W.W.W.W.W.W.W.W.W."
};

View File

@@ -5,18 +5,9 @@ ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get -y update && \
apt-get -y dist-upgrade && \
apt-get -y install --no-install-recommends \
python3-pyqt5 \
python3-pyqt5.qtwebkit \
python-tox \
python3-sip \
xvfb \
git \
python3-setuptools \
wget \
herbstluftwm \
locales \
libjs-pdf
apt-get -y install python3-pyqt5 python3-pyqt5.qtwebkit python-tox \
python3-sip xvfb git python3-setuptools wget \
herbstluftwm locales
RUN echo 'en_US.UTF-8 UTF-8' > /etc/locale.gen && locale-gen
RUN useradd user && mkdir /home/user && chown -R user:users /home/user

View File

@@ -5,21 +5,9 @@ ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get -y update && \
apt-get -y dist-upgrade && \
apt-get -y install --no-install-recommends \
python3-pyqt5 \
python3-pyqt5.qtwebkit \
python-tox \
python3-sip \
xvfb \
git \
python3-setuptools \
wget \
herbstluftwm \
language-pack-en \
libjs-pdf \
dbus
RUN dbus-uuidgen --ensure
apt-get -y install python3-pyqt5 python3-pyqt5.qtwebkit python-tox \
python3-sip xvfb git python3-setuptools wget \
herbstluftwm language-pack-en
RUN useradd user && mkdir /home/user && chown -R user:users /home/user
USER user

View File

@@ -1,37 +0,0 @@
FROM ubuntu:xenial
MAINTAINER Florian Bruhin <me@the-compiler.org>
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get -y update && \
apt-get -y dist-upgrade && \
apt-get -y install --no-install-recommends \
python3-pyqt5 \
python3-pyqt5.qtwebkit \
python-tox \
python3-sip \
xvfb \
git \
python3-setuptools \
wget \
herbstluftwm \
language-pack-en \
libjs-pdf \
dbus
RUN dbus-uuidgen --ensure
RUN useradd user && mkdir /home/user && chown -R user:users /home/user
USER user
WORKDIR /home/user
ENV DISPLAY=:0
ENV LC_ALL=en_US.UTF-8
ENV LANG=en_US.UTF-8
CMD Xvfb -screen 0 800x600x24 :0 & \
sleep 2 && \
herbstluftwm & \
git clone /outside qutebrowser.git && \
cd qutebrowser.git && \
tox -e py35

7
misc/qt_menu.nib/README Normal file
View File

@@ -0,0 +1,7 @@
These files are copied from Qt's source tree in
src/plugins/platforms/cocoa/qt_menu.nib at revision
b8246f08e49eb672974fd3d3d972a5ff13c1524d.
http://code.qt.io/cgit/qt/qtbase.git/tree/src/plugins/platforms/cocoa/qt_menu.nib
They are needed for cx_Freeze and don't seem to be bundled with Qt anymore.

59
misc/qt_menu.nib/classes.nib generated Normal file
View File

@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IBClasses</key>
<array>
<dict>
<key>ACTIONS</key>
<dict>
<key>hide</key>
<string>id</string>
<key>hideOtherApplications</key>
<string>id</string>
<key>orderFrontStandardAboutPanel</key>
<string>id</string>
<key>qtDispatcherToQPAMenuItem</key>
<string>id</string>
<key>terminate</key>
<string>id</string>
<key>unhideAllApplications</key>
<string>id</string>
</dict>
<key>CLASS</key>
<string>QCocoaMenuLoader</string>
<key>LANGUAGE</key>
<string>ObjC</string>
<key>OUTLETS</key>
<dict>
<key>aboutItem</key>
<string>NSMenuItem</string>
<key>aboutQtItem</key>
<string>NSMenuItem</string>
<key>appMenu</key>
<string>NSMenu</string>
<key>hideItem</key>
<string>NSMenuItem</string>
<key>preferencesItem</key>
<string>NSMenuItem</string>
<key>quitItem</key>
<string>NSMenuItem</string>
<key>theMenu</key>
<string>NSMenu</string>
</dict>
<key>SUPERCLASS</key>
<string>NSResponder</string>
</dict>
<dict>
<key>CLASS</key>
<string>FirstResponder</string>
<key>LANGUAGE</key>
<string>ObjC</string>
<key>SUPERCLASS</key>
<string>NSObject</string>
</dict>
</array>
<key>IBVersion</key>
<string>1</string>
</dict>
</plist>

18
misc/qt_menu.nib/info.nib generated Normal file
View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IBFramework Version</key>
<string>672</string>
<key>IBOldestOS</key>
<integer>5</integer>
<key>IBOpenObjects</key>
<array>
<integer>57</integer>
</array>
<key>IBSystem Version</key>
<string>9L31a</string>
<key>targetFramework</key>
<string>IBCocoaFramework</string>
</dict>
</plist>

BIN
misc/qt_menu.nib/keyedobjects.nib generated Normal file

Binary file not shown.

View File

@@ -1,74 +0,0 @@
# -*- mode: python -*-
import sys
import os
sys.path.insert(0, os.getcwd())
from scripts import setupcommon
block_cipher = None
def get_data_files():
data_files = [
('../qutebrowser/html', 'html'),
('../qutebrowser/img', 'img'),
('../qutebrowser/javascript', 'javascript'),
('../qutebrowser/html/doc', 'html/doc'),
('../qutebrowser/git-commit-id', '')
]
if os.path.exists(os.path.join('qutebrowser', '3rdparty', 'pdfjs')):
data_files.append(('../qutebrowser/3rdparty/pdfjs', '3rdparty/pdfjs'))
else:
print("Warning: excluding pdfjs as it's not present!")
return data_files
setupcommon.write_git_file()
if os.name == 'nt':
icon = 'icons/qutebrowser.ico'
elif sys.platform == 'darwin':
icon = 'icons/qutebrowser.icns'
else:
icon = None
a = Analysis(['../qutebrowser/__main__.py'],
pathex=['misc'],
binaries=None,
datas=get_data_files(),
hiddenimports=[],
hookspath=[],
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher)
pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher)
exe = EXE(pyz,
a.scripts,
exclude_binaries=True,
name='qutebrowser',
icon=icon,
debug=False,
strip=False,
upx=True,
console=False )
coll = COLLECT(exe,
a.binaries,
a.zipfiles,
a.datas,
strip=False,
upx=True,
name='qutebrowser')
app = BUNDLE(coll,
name='qutebrowser.app',
icon=icon,
info_plist={'NSHighResolutionCapable': 'True'},
bundle_identifier=None)

View File

@@ -1,20 +0,0 @@
This directory contains various `requirements` files which are used by `tox` to
have reproducable tests with pinned versions.
The files are generated based on unpinned requirements in `*.txt-raw` files.
Those files can also contain some special commands:
- Add an additional comment to a line: `#@ comment: <package> <comment here>`
- Filter a line for requirements.io: `#@ filter: <package> <filter>`
- Don't include a package in the output: `#@ ignore: <package>` (or multiple packages)
- Replace a part of a frozen package specification with another: `#@ replace <regex> <replacement>`
Some examples:
```
#@ comment: mypkg blah blub
#@ filter: mypkg != 1.0.0
#@ ignore: mypkg, otherpkg
#@ replace: foo bar
```

View File

@@ -1,3 +0,0 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
check-manifest==0.31

View File

@@ -1 +0,0 @@
check-manifest

View File

@@ -1,5 +0,0 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
codecov==2.0.5
coverage==4.1
requests==2.10.0

View File

@@ -1 +0,0 @@
codecov

View File

@@ -1,3 +0,0 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
cx-Freeze==4.3.4

View File

@@ -1 +0,0 @@
cx_Freeze

View File

@@ -1,29 +0,0 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
ebb-lint==0.4.4
flake8==2.5.4
flake8-copyright==0.1
flake8-debugger==1.4.0
flake8-deprecated==1.0
flake8-docstrings==0.2.6
flake8-future-import==0.4.1
flake8-mock==0.2
flake8-pep3101==0.3
flake8-putty==0.3.2
flake8-string-format==0.2.2
flake8-tidy-imports==1.0.0
flake8-tuple==0.2.9
hacking==0.11.0
intervaltree==2.1.0
mccabe==0.5.0
packaging==16.7
pbr==1.10.0
pep257==0.7.0 # still needed by flake8-docstrings but ignored
pep8==1.7.0
pep8-naming==0.3.3
pydocstyle==1.0.0
pyflakes==1.2.3
pyparsing==2.1.4
six==1.10.0
sortedcontainers==1.5.3
venusian==1.0

View File

@@ -1,21 +0,0 @@
ebb-lint
flake8
flake8-copyright
flake8-debugger
flake8-deprecated
flake8-docstrings
flake8-future-import
flake8-mock
flake8-pep3101
flake8-putty
flake8-string-format
flake8-tidy-imports
flake8-tuple
hacking
pep8-naming
pydocstyle
pyflakes
mccabe==0.5.0
#@ comment: pep257 still needed by flake8-docstrings but ignored

View File

@@ -1 +0,0 @@
pip==8.1.2

View File

@@ -1,3 +0,0 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
PyInstaller==3.2

View File

@@ -1 +0,0 @@
PyInstaller

View File

@@ -1,11 +0,0 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
-e git+https://github.com/PyCQA/astroid.git#egg=astroid
isort==4.2.5
lazy-object-proxy==1.2.2
mccabe==0.5.0
-e git+https://github.com/PyCQA/pylint.git#egg=pylint
./scripts/dev/pylint_checkers
requests==2.10.0
six==1.10.0
wrapt==1.10.8

View File

@@ -1,13 +0,0 @@
-e git+https://github.com/PyCQA/astroid.git#egg=astroid
-e git+https://github.com/PyCQA/pylint.git#egg=pylint
./scripts/dev/pylint_checkers
requests
# https://github.com/PyCQA/pylint/issues/932
mccabe==0.5.0
# remove @commit-id for scm installs
#@ replace: @.*# #
# fix qute-pylint location
#@ replace: qute-pylint==.* ./scripts/dev/pylint_checkers

View File

@@ -1,10 +0,0 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
astroid==1.4.6
colorama==0.3.7
lazy-object-proxy==1.2.2
pylint==1.5.6
./scripts/dev/pylint_checkers
requests==2.10.0
six==1.10.0
wrapt==1.10.8

View File

@@ -1,6 +0,0 @@
pylint
./scripts/dev/pylint_checkers
requests
# fix qute-pylint location
#@ replace: qute-pylint==.* ./scripts/dev/pylint_checkers

View File

@@ -1,4 +0,0 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
docutils==0.12
pyroma==2.0.2

View File

@@ -1 +0,0 @@
pyroma

View File

@@ -1,6 +0,0 @@
Jinja2
Pygments
pyPEG2
PyYAML
colorama
cssutils

View File

@@ -1,32 +0,0 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
beautifulsoup4==4.4.1
CherryPy==6.0.1
coverage==4.1
decorator==4.0.10
Flask==0.10.1 # rq.filter: < 0.11.0
glob2==0.4.1
httpbin==0.4.1
hypothesis==3.4.0
itsdangerous==0.24
# Jinja2==2.8
Mako==1.0.4
# MarkupSafe==0.23
parse==1.6.6
parse-type==0.3.4
py==1.4.31
pytest==2.9.2
pytest-bdd==2.16.1
pytest-catchlog==1.2.2
pytest-cov==2.2.1
pytest-faulthandler==1.3.0
pytest-instafail==0.3.0
pytest-mock==1.1
pytest-qt==1.11.0
pytest-repeat==0.2
pytest-rerunfailures==2.0.0
pytest-travis-fold==1.2.0
pytest-xvfb==0.2.0
six==1.10.0
vulture==0.8.1
Werkzeug==0.11.10

View File

@@ -1,22 +0,0 @@
beautifulsoup4
CherryPy
coverage
Flask==0.10.1
httpbin
hypothesis
pytest
pytest-bdd
pytest-catchlog
pytest-cov
pytest-faulthandler
pytest-instafail
pytest-mock
pytest-qt
pytest-repeat
pytest-rerunfailures
pytest-travis-fold
pytest-xvfb
vulture
#@ filter: Flask < 0.11.0
#@ ignore: Jinja2, MarkupSafe

View File

@@ -1,6 +0,0 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
pluggy==0.3.1
py==1.4.31
tox==2.3.1
virtualenv==15.0.2

View File

@@ -1 +0,0 @@
tox

View File

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

View File

@@ -1 +0,0 @@
vulture

View File

@@ -22,7 +22,7 @@ Pass backend: (see also passwordstore.org)
must be contained in a later line beginning with "user:", "login:", or
"username:" (configurable by the user_pattern variable).
Behavior:
Behaviour:
It will try to find a username/password entry in the configured backend
(currently only pass) for the current website and will load that pair of
username and password to any form on the current page that has some password
@@ -61,7 +61,7 @@ die() {
}
javascript_escape() {
# print the first argument in a escaped way, such that it can safely
# print the first argument in a escaped way, such that it can savely
# be used within javascripts double quotes
sed "s,[\\\'\"],\\\&,g" <<< "$1"
}
@@ -80,7 +80,7 @@ javascript_escape() {
# between different paths on the same domain.
simplify_url() {
simple_url="${1##*://}" # remove protocol specification
simple_url="${1##*://}" # remove protocoll specification
simple_url="${simple_url%%\?*}" # remove GET parameters
simple_url="${simple_url%%/*}" # remove directory path
simple_url="${simple_url%:*}" # remove port
@@ -89,7 +89,7 @@ simplify_url() {
# no_entries_found() is called if the first query_entries() call did not find
# any matching entries. Multiple implementations are possible:
# The easiest behavior is to quit:
# The easiest behaviour is to quit:
#no_entries_found() {
# if [ 0 -eq "${#files[@]}" ] ; then
# die "No entry found for »$simple_url«"
@@ -106,7 +106,7 @@ simplify_url() {
# fi
# }
# Another behavior is to drop another level of subdomains until search hits
# Another beahviour is to drop another level of subdomains until search hits
# are found:
no_entries_found() {
while [ 0 -eq "${#files[@]}" ] && [ -n "$simple_url" ]; do
@@ -170,7 +170,7 @@ choose_entry_random() {
# dmenu or zenity or even qutebrowser completion in future?) which entry to
# pick
MENU_COMMAND=( head -n 1 )
# whether to show the menu if there is only one entry in it
# whether to show the menu if there is only one entrie in it
menu_if_one_entry=0
choose_entry_menu() {
local nr=${#files[@]}
@@ -245,7 +245,7 @@ pass_backend() {
done < <(find -L "$PREFIX" -iname '*.gpg' -print0)
fi
if ((match_filename)) ; then
# add entries with matching filepath
# add entries wth matching filepath
while read -r passfile ; do
passfile="${passfile#$PREFIX}"
passfile="${passfile#/}"

View File

@@ -1,6 +1,6 @@
#!/bin/bash -e
#
# Behavior:
# Behaviour:
# Userscript for qutebrowser which views the current web page in mpv using
# sensible mpv-flags. While viewing the page in MPV, all <video>, <embed>,
# and <object> tags in the original page are temporarily removed. Clicking on

View File

@@ -11,12 +11,12 @@ markers =
not_frozen: Tests which can't be run if sys.frozen is True.
no_xvfb: Tests which can't be run with Xvfb.
frozen: Tests which can only be run if sys.frozen is True.
integration: Tests which test a bigger portion of code
end2end: End to end tests which run qutebrowser as subprocess
integration: Tests which test a bigger portion of code, run without coverage.
skip: Always skipped test.
pyqt531_or_newer: Needs PyQt 5.3.1 or newer.
xfail_norun: xfail the test with out running it
ci: Tests which should only run on CI.
flaky_once: Try to rerun this test once if it fails
flaky: Tests which are flaky and should be rerun
qt_log_level_fail = WARNING
qt_log_ignore =
^SpellCheck: .*
@@ -37,6 +37,5 @@ qt_log_ignore =
^Geoclue error: Process org\.freedesktop\.Geoclue\.Master exited with status 127
^QObject::connect: Cannot connect \(null\)::stateChanged\(QNetworkSession::State\) to QNetworkReplyHttpImpl::_q_networkSessionStateChanged\(QNetworkSession::State\)
^QXcbClipboard: Cannot transfer data, no data available
^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=
qt_wait_signal_raising = true
xfail_strict = true

View File

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

View File

@@ -31,7 +31,7 @@ import atexit
import datetime
import tokenize
from PyQt5.QtWidgets import QApplication, QWidget
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWebKit import QWebSettings
from PyQt5.QtGui import QDesktopServices, QPixmap, QIcon, QCursor, QWindow
from PyQt5.QtCore import (pyqtSlot, qInstallMessageHandler, QTimer, QUrl,
@@ -279,8 +279,7 @@ def process_pos_args(args, via_ipc=False, cwd=None, target_arg=None):
"{}".format(cmd, e))
else:
background = open_target in ('tab-bg', 'tab-bg-silent')
tabbed_browser.tabopen(url, background=background,
explicit=True)
tabbed_browser.tabopen(url, background=background)
def _open_startpage(win_id=None):
@@ -343,18 +342,18 @@ def _save_version():
@pyqtSlot('QWidget*', 'QWidget*')
def on_focus_changed(_old, new):
"""Register currently focused main window in the object registry."""
if not isinstance(new, QWidget):
log.misc.debug("on_focus_changed called with non-QWidget {!r}".format(
new))
if new is None or not isinstance(new, mainwindow.MainWindow):
if new is None:
window = None
else:
window = new.window()
if window is None or not isinstance(window, mainwindow.MainWindow):
try:
objreg.delete('last-focused-main-window')
except KeyError:
pass
qApp.restoreOverrideCursor()
else:
objreg.register('last-focused-main-window', new.window(), update=True)
objreg.register('last-focused-main-window', window, update=True)
_maybe_hide_mouse_cursor()
@@ -426,9 +425,7 @@ def _init_modules(args, crash_handler):
proxy.init()
log.init.debug("Initializing cookies...")
cookie_jar = cookies.CookieJar(qApp)
ram_cookie_jar = cookies.RAMCookieJar(qApp)
objreg.register('cookie-jar', cookie_jar)
objreg.register('ram-cookie-jar', ram_cookie_jar)
log.init.debug("Initializing cache...")
diskcache = cache.DiskCache(standarddir.cache(), parent=qApp)
objreg.register('cache', diskcache)
@@ -499,7 +496,7 @@ class Quitter:
for dirpath, _dirnames, filenames in os.walk(path):
for fn in filenames:
if os.path.splitext(fn)[1] == '.py' and os.path.isfile(fn):
if os.path.splitext(fn)[1] == '.py':
with tokenize.open(os.path.join(dirpath, fn)) as f:
compile(f.read(), fn, 'exec')
@@ -724,8 +721,8 @@ class Quitter:
# segfaults.
QTimer.singleShot(0, functools.partial(qApp.exit, status))
@cmdutils.register(instance='quitter', name='wq')
@cmdutils.argument('name', completion=usertypes.Completion.sessions)
@cmdutils.register(instance='quitter', name='wq',
completion=[usertypes.Completion.sessions])
def save_and_quit(self, name=sessions.default):
"""Save open pages and quit.

View File

@@ -116,7 +116,6 @@ class HostBlocker:
self._local_hosts_file = None
else:
self._local_hosts_file = os.path.join(data_dir, 'blocked-hosts')
self.on_config_changed()
config_dir = standarddir.config()
if config_dir is None:
@@ -177,8 +176,7 @@ class HostBlocker:
message.info('current',
"Run :adblock-update to get adblock lists.")
@cmdutils.register(instance='host-blocker')
@cmdutils.argument('win_id', win_id=True)
@cmdutils.register(instance='host-blocker', win_id='win_id')
def adblock_update(self, win_id):
"""Update the adblock block lists.
@@ -275,13 +273,11 @@ class HostBlocker:
def on_config_changed(self):
"""Update files when the config changed."""
urls = config.get('content', 'host-block-lists')
if urls is None and self._local_hosts_file is not None:
if urls is None:
try:
os.remove(self._local_hosts_file)
except FileNotFoundError:
pass
except OSError as e:
log.misc.exception("Failed to delete hosts file: {}".format(e))
except OSError:
log.misc.exception("Failed to delete hosts file.")
def on_download_finished(self, download):
"""Check if all downloads are finished and if so, trigger reading.

View File

@@ -72,7 +72,7 @@ class DiskCache(QNetworkDiskCache):
"""Update cache size/activated if the config was changed."""
if (section, option) == ('storage', 'cache-size'):
self.setMaximumCacheSize(config.get('storage', 'cache-size'))
elif (section, option) == ('general', # pragma: no branch
elif (section, option) == ('general', # pragma: no branch
'private-browsing'):
self._maybe_activate()

View File

@@ -42,7 +42,7 @@ from qutebrowser.config import config, configexc
from qutebrowser.browser import webelem, inspector, urlmarks, downloads, mhtml
from qutebrowser.keyinput import modeman
from qutebrowser.utils import (message, usertypes, log, qtutils, urlutils,
objreg, utils, typing)
objreg, utils)
from qutebrowser.utils.usertypes import KeyMode
from qutebrowser.misc import editor, guiprocess
from qutebrowser.completion.models import instances, sortfilter
@@ -65,6 +65,7 @@ class CommandDispatcher:
"""
def __init__(self, win_id, tabbed_browser):
self._editor = None
self._win_id = win_id
self._tabbed_browser = tabbed_browser
@@ -199,8 +200,8 @@ class CommandDispatcher:
"{!r}!".format(conf_selection))
return None
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('count', count=True)
@cmdutils.register(instance='command-dispatcher', scope='window',
count='count')
def tab_close(self, left=False, right=False, opposite=False, count=None):
"""Close the current/[count]th tab.
@@ -226,9 +227,8 @@ class CommandDispatcher:
tabbar.setSelectionBehaviorOnRemove(old_selection_behavior)
@cmdutils.register(instance='command-dispatcher', name='open',
maxsplit=0, scope='window')
@cmdutils.argument('url', completion=usertypes.Completion.url)
@cmdutils.argument('count', count=True)
maxsplit=0, scope='window', count='count',
completion=[usertypes.Completion.url])
def openurl(self, url=None, bg=False, tab=False, window=False, count=None):
"""Open a URL in the current/[count]th tab.
@@ -249,10 +249,7 @@ class CommandDispatcher:
try:
url = urlutils.fuzzy_url(url)
except urlutils.InvalidUrlError as e:
# We don't use cmdexc.CommandError here as this can be called
# async from edit_url
message.error(self._win_id, str(e))
return
raise cmdexc.CommandError(e)
if tab or bg or window:
self._open(url, tab, bg, window)
else:
@@ -269,8 +266,7 @@ class CommandDispatcher:
curtab.openurl(url)
@cmdutils.register(instance='command-dispatcher', name='reload',
scope='window')
@cmdutils.argument('count', count=True)
scope='window', count='count')
def reloadpage(self, force=False, count=None):
"""Reload the current/[count]th tab.
@@ -285,8 +281,8 @@ class CommandDispatcher:
else:
tab.reload()
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('count', count=True)
@cmdutils.register(instance='command-dispatcher', scope='window',
count='count')
def stop(self, count=None):
"""Stop loading in the current/[count]th tab.
@@ -298,8 +294,7 @@ class CommandDispatcher:
tab.stop()
@cmdutils.register(instance='command-dispatcher', name='print',
scope='window')
@cmdutils.argument('count', count=True)
scope='window', count='count')
def printpage(self, preview=False, count=None):
"""Print the current/[count]th tab.
@@ -353,8 +348,6 @@ class CommandDispatcher:
new_tabbed_browser.set_page_title(idx, cur_title)
if config.get('tabs', 'show-favicons'):
new_tabbed_browser.setTabIcon(idx, curtab.icon())
if config.get('tabs', 'tabs-are-windows'):
new_tabbed_browser.window().setWindowIcon(curtab.icon())
newtab.keep_icon = True
newtab.setZoomFactor(curtab.zoomFactor())
history = qtutils.serialize(curtab.history())
@@ -394,8 +387,8 @@ class CommandDispatcher:
raise cmdexc.CommandError("At beginning of history.")
widget.back()
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('count', count=True)
@cmdutils.register(instance='command-dispatcher', scope='window',
count='count')
def back(self, tab=False, bg=False, window=False, count=1):
"""Go back in the history of the current tab.
@@ -407,8 +400,8 @@ class CommandDispatcher:
"""
self._back_forward(tab, bg, window, count, forward=False)
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('count', count=True)
@cmdutils.register(instance='command-dispatcher', scope='window',
count='count')
def forward(self, tab=False, bg=False, window=False, count=1):
"""Go forward in the history of the current tab.
@@ -435,7 +428,6 @@ class CommandDispatcher:
new_url = urlutils.incdec_number(url, incdec, segments=segments)
except urlutils.IncDecError as error:
raise cmdexc.CommandError(error.msg)
self._open(new_url, tab, background, window)
def _navigate_up(self, url, tab, background, window):
@@ -455,9 +447,9 @@ class CommandDispatcher:
self._open(url, tab, background, window)
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('where', choices=['prev', 'next', 'up', 'increment',
'decrement'])
def navigate(self, where: str, tab=False, bg=False, window=False):
def navigate(self, where: {'type': ('prev', 'next', 'up', 'increment',
'decrement')},
tab=False, bg=False, window=False):
"""Open typical prev/next links or navigate using the URL path.
This tries to automatically click on typical _Previous Page_ or
@@ -478,13 +470,10 @@ class CommandDispatcher:
bg: Open in a background tab.
window: Open in a new window.
"""
# save the pre-jump position in the special ' mark
self.set_mark("'")
cmdutils.check_exclusive((tab, bg, window), 'tbw')
widget = self._current_widget()
frame = widget.page().currentFrame()
url = self._current_url().adjusted(QUrl.RemoveFragment)
url = self._current_url()
if frame is None:
raise cmdexc.CommandError("No frame focused!")
hintmanager = objreg.get('hintmanager', scope='tab', tab='current')
@@ -503,14 +492,13 @@ class CommandDispatcher:
"`where'.".format(where))
@cmdutils.register(instance='command-dispatcher', hide=True,
scope='window')
@cmdutils.argument('count', count=True)
def scroll_px(self, dx: int, dy: int, count=1):
scope='window', count='count')
def scroll_px(self, dx: {'type': int}, dy: {'type': int}, count=1):
"""Scroll the current tab by 'count * dx/dy' pixels.
Args:
dx: How much to scroll in x-direction.
dy: How much to scroll in y-direction.
dy: How much to scroll in x-direction.
count: multiplier
"""
dx *= count
@@ -520,9 +508,8 @@ class CommandDispatcher:
self._current_widget().page().currentFrame().scroll(dx, dy)
@cmdutils.register(instance='command-dispatcher', hide=True,
scope='window')
@cmdutils.argument('count', count=True)
def scroll(self, direction: typing.Union[str, int], count=1):
scope='window', count='count')
def scroll(self, direction: {'type': (str, int)}, count=1):
"""Scroll the current tab in the given direction.
Args:
@@ -580,10 +567,9 @@ class CommandDispatcher:
widget.keyReleaseEvent(release_evt)
@cmdutils.register(instance='command-dispatcher', hide=True,
scope='window')
@cmdutils.argument('count', count=True)
@cmdutils.argument('horizontal', flag='x')
def scroll_perc(self, perc: float=None, horizontal=False, count=None):
scope='window', count='count')
def scroll_perc(self, perc: {'type': float}=None,
horizontal: {'flag': 'x'}=False, count=None):
"""Scroll to a specific percentage of the page.
The percentage can be given either as argument or as count.
@@ -594,9 +580,6 @@ class CommandDispatcher:
horizontal: Scroll horizontally instead of vertically.
count: Percentage to scroll.
"""
# save the pre-jump position in the special ' mark
self.set_mark("'")
if perc is None and count is None:
perc = 100
elif perc is None:
@@ -617,14 +600,12 @@ class CommandDispatcher:
frame.setScrollBarValue(orientation, int(m * perc / 100))
@cmdutils.register(instance='command-dispatcher', hide=True,
scope='window')
@cmdutils.argument('count', count=True)
@cmdutils.argument('top_navigate', metavar='ACTION',
choices=('prev', 'decrement'))
@cmdutils.argument('bottom_navigate', metavar='ACTION',
choices=('next', 'increment'))
def scroll_page(self, x: float, y: float, *,
top_navigate: str=None, bottom_navigate: str=None,
scope='window', count='count')
def scroll_page(self, x: {'type': float}, y: {'type': float}, *,
top_navigate: {'type': ('prev', 'decrement'),
'metavar': 'ACTION'}=None,
bottom_navigate: {'type': ('next', 'increment'),
'metavar': 'ACTION'}=None,
count=1):
"""Scroll the frame page-wise.
@@ -671,14 +652,13 @@ class CommandDispatcher:
frame.scroll(dx, dy)
@cmdutils.register(instance='command-dispatcher', scope='window')
def yank(self, title=False, sel=False, domain=False, pretty=False):
def yank(self, title=False, sel=False, domain=False):
"""Yank the current URL/title to the clipboard or primary selection.
Args:
sel: Use the primary selection instead of the clipboard.
title: Yank the title instead of the URL.
domain: Yank only the scheme, domain, and port number.
pretty: Yank the URL in pretty decoded form.
"""
if title:
s = self._tabbed_browser.page_title(self._current_index())
@@ -690,13 +670,11 @@ class CommandDispatcher:
':' + str(port) if port > -1 else '')
what = 'domain'
else:
flags = QUrl.RemovePassword
if not pretty:
flags |= QUrl.FullyEncoded
s = self._current_url().toString(flags)
s = self._current_url().toString(
QUrl.FullyEncoded | QUrl.RemovePassword)
what = 'URL'
if sel and utils.supports_selection():
if sel and QApplication.clipboard().supportsSelection():
target = "primary selection"
else:
sel = False
@@ -706,8 +684,8 @@ class CommandDispatcher:
message.info(self._win_id, "Yanked {} to {}: {}".format(
what, target, s))
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('count', count=True)
@cmdutils.register(instance='command-dispatcher', scope='window',
count='count')
def zoom_in(self, count=1):
"""Increase the zoom level for the current tab.
@@ -721,8 +699,8 @@ class CommandDispatcher:
raise cmdexc.CommandError(e)
message.info(self._win_id, "Zoom level: {}%".format(perc))
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('count', count=True)
@cmdutils.register(instance='command-dispatcher', scope='window',
count='count')
def zoom_out(self, count=1):
"""Decrease the zoom level for the current tab.
@@ -736,9 +714,9 @@ class CommandDispatcher:
raise cmdexc.CommandError(e)
message.info(self._win_id, "Zoom level: {}%".format(perc))
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('count', count=True)
def zoom(self, zoom: int=None, count=None):
@cmdutils.register(instance='command-dispatcher', scope='window',
count='count')
def zoom(self, zoom: {'type': int}=None, count=None):
"""Set the zoom level for the current tab.
The zoom can be given as argument or as [count]. If neither of both is
@@ -788,8 +766,8 @@ class CommandDispatcher:
except IndexError:
raise cmdexc.CommandError("Nothing to undo!")
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('count', count=True)
@cmdutils.register(instance='command-dispatcher', scope='window',
count='count')
def tab_prev(self, count=1):
"""Switch to the previous tab, or switch [count] tabs back.
@@ -808,8 +786,8 @@ class CommandDispatcher:
else:
raise cmdexc.CommandError("First tab")
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('count', count=True)
@cmdutils.register(instance='command-dispatcher', scope='window',
count='count')
def tab_next(self, count=1):
"""Switch to the next tab, or switch [count] tabs forward.
@@ -841,8 +819,7 @@ class CommandDispatcher:
bg: Open in a background tab.
window: Open in new window.
"""
force_search = False
if sel and utils.supports_selection():
if sel and QApplication.clipboard().supportsSelection():
target = "Primary selection"
else:
sel = False
@@ -855,20 +832,19 @@ class CommandDispatcher:
if (len(text_urls) > 1 and not urlutils.is_url(text_urls[0]) and
urlutils.get_path_if_valid(
text_urls[0], check_exists=True) is None):
force_search = True
text_urls = [text]
for i, text_url in enumerate(text_urls):
if not window and i > 0:
tab = False
bg = True
try:
url = urlutils.fuzzy_url(text_url, force_search=force_search)
url = urlutils.fuzzy_url(text_url)
except urlutils.InvalidUrlError as e:
raise cmdexc.CommandError(e)
self._open(url, tab, bg, window)
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('index', completion=usertypes.Completion.tab)
@cmdutils.register(instance='command-dispatcher', scope='window',
completion=[usertypes.Completion.tab])
def buffer(self, index):
"""Select tab by index or url/title best match.
@@ -921,18 +897,16 @@ class CommandDispatcher:
window.raise_()
tabbed_browser.setCurrentIndex(idx-1)
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('index', choices=['last'])
@cmdutils.argument('count', count=True)
def tab_focus(self, index: typing.Union[str, int]=None, count=None):
@cmdutils.register(instance='command-dispatcher', scope='window',
count='count')
def tab_focus(self, index: {'type': (int, 'last')}=None, count=None):
"""Select the tab given as argument/[count].
If neither count nor index are given, it behaves like tab-next.
Args:
index: The tab index to focus, starting with 1. The special value
`last` focuses the last focused tab. Negative indexes
counts from the end, such that -1 is the last tab.
`last` focuses the last focused tab.
count: The tab index to focus, starting with 1.
"""
if index == 'last':
@@ -941,8 +915,6 @@ class CommandDispatcher:
if index is None and count is None:
self.tab_next()
return
if index is not None and index < 0:
index = self._count() + index + 1
try:
idx = cmdutils.arg_or_count(index, count, default=1,
countzero=self._count())
@@ -955,10 +927,9 @@ class CommandDispatcher:
raise cmdexc.CommandError("There's no tab with index {}!".format(
idx))
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('direction', choices=['+', '-'])
@cmdutils.argument('count', count=True)
def tab_move(self, direction: str=None, count=None):
@cmdutils.register(instance='command-dispatcher', scope='window',
count='count')
def tab_move(self, direction: {'type': ('+', '-')}=None, count=None):
"""Move the current tab.
Args:
@@ -995,12 +966,9 @@ class CommandDispatcher:
cmdutils.check_overflow(new_idx, 'int')
self._tabbed_browser.setUpdatesEnabled(False)
try:
color = self._tabbed_browser.tabBar().tab_data(
cur_idx, 'indicator-color')
self._tabbed_browser.removeTab(cur_idx)
self._tabbed_browser.insertTab(new_idx, tab, icon, label)
self._set_current_index(new_idx)
self._tabbed_browser.set_tab_indicator_color(new_idx, color)
finally:
self._tabbed_browser.setUpdatesEnabled(True)
@@ -1009,10 +977,8 @@ class CommandDispatcher:
def spawn(self, cmdline, userscript=False, verbose=False, detach=False):
"""Spawn a command in a shell.
Note the `{url}` and `{url:pretty}` variables might be useful here.
`{url}` gets replaced by the URL in fully encoded format and
`{url:pretty}` uses a "pretty form" with most percent-encoded
characters decoded.
Note the {url} variable which gets replaced by the current URL might be
useful here.
Args:
userscript: Run the command as a userscript. You can use an
@@ -1096,9 +1062,8 @@ class CommandDispatcher:
quickmark_manager.prompt_save(self._win_id, self._current_url())
@cmdutils.register(instance='command-dispatcher', scope='window',
maxsplit=0)
@cmdutils.argument('name',
completion=usertypes.Completion.quickmark_by_name)
maxsplit=0,
completion=[usertypes.Completion.quickmark_by_name])
def quickmark_load(self, name, tab=False, bg=False, window=False):
"""Load a quickmark.
@@ -1128,8 +1093,8 @@ class CommandDispatcher:
"Bookmarked {}!".format(url.toDisplayString()))
@cmdutils.register(instance='command-dispatcher', scope='window',
maxsplit=0)
@cmdutils.argument('url', completion=usertypes.Completion.bookmark_by_url)
maxsplit=0,
completion=[usertypes.Completion.bookmark_by_url])
def bookmark_load(self, url, tab=False, bg=False, window=False):
"""Load a bookmark.
@@ -1206,8 +1171,8 @@ class CommandDispatcher:
cur.inspector.show()
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('dest_old', hide=True)
def download(self, url=None, dest_old=None, *, mhtml_=False, dest=None):
def download(self, url=None, dest_old: {'hide': True}=None, *,
mhtml_=False, dest=None):
"""Download a given URL, or current page if no URL given.
The form `:download [url] [dest]` is deprecated, use `:download --dest
@@ -1220,9 +1185,9 @@ class CommandDispatcher:
mhtml_: Download the current page and all assets as mhtml file.
"""
if dest_old is not None:
message.warning(self._win_id,
":download [url] [dest] is deprecated - use"
" download --dest [dest] [url]")
message.warning(
self._win_id, ":download [url] [dest] is deprecated - use"
" download --dest [dest] [url]")
if dest is not None:
raise cmdexc.CommandError("Can't give two destinations for the"
" download.")
@@ -1278,8 +1243,8 @@ class CommandDispatcher:
frame = widget.page().currentFrame()
html = frame.toHtml()
lexer = pygments.lexers.HtmlLexer()
formatter = pygments.formatters.HtmlFormatter(full=True,
linenos='table')
formatter = pygments.formatters.HtmlFormatter(
full=True, linenos='table')
highlighted = pygments.highlight(html, lexer, formatter)
current_url = self._current_url()
tab = self._tabbed_browser.tabopen(explicit=True)
@@ -1313,8 +1278,8 @@ class CommandDispatcher:
message.info(self._win_id, "Dumped page to {}.".format(dest))
@cmdutils.register(instance='command-dispatcher', name='help',
completion=[usertypes.Completion.helptopic],
scope='window')
@cmdutils.argument('topic', completion=usertypes.Completion.helptopic)
def show_help(self, tab=False, bg=False, window=False, topic=None):
r"""Show help about a command or setting.
@@ -1354,27 +1319,6 @@ class CommandDispatcher:
url = QUrl('qute://help/{}'.format(path))
self._open(url, tab, bg, window)
@cmdutils.register(instance='command-dispatcher', scope='window')
def messages(self, level='error', plain=False, tab=False, bg=False,
window=False):
"""Show a log of past messages.
Args:
level: Include messages with `level` or higher severity.
Valid values: vdebug, debug, info, warning, error, critical.
plain: Whether to show plaintext (as opposed to html).
tab: Open in a new tab.
bg: Open in a background tab.
window: Open in a new window.
"""
if level.upper() not in log.LOG_LEVELS:
raise cmdexc.CommandError("Invalid log level {}!".format(level))
if plain:
url = QUrl('qute://plainlog?level={}'.format(level))
else:
url = QUrl('qute://log?level={}'.format(level))
self._open(url, tab, bg, window)
@cmdutils.register(instance='command-dispatcher',
modes=[KeyMode.insert], hide=True, scope='window')
def open_editor(self):
@@ -1394,10 +1338,11 @@ class CommandDispatcher:
text = str(elem)
else:
text = elem.evaluateJavaScript('this.value')
ed = editor.ExternalEditor(self._win_id, self._tabbed_browser)
ed.editing_finished.connect(functools.partial(
self.on_editing_finished, elem))
ed.edit(text)
self._editor = editor.ExternalEditor(
self._win_id, self._tabbed_browser)
self._editor.editing_finished.connect(
functools.partial(self.on_editing_finished, elem))
self._editor.edit(text)
def on_editing_finished(self, elem, text):
"""Write the editor text into the form field and clean up tempfile.
@@ -1437,7 +1382,7 @@ class CommandDispatcher:
try:
sel = utils.get_clipboard(selection=True)
except utils.SelectionUnsupportedError:
sel = utils.get_clipboard()
return
log.misc.debug("Pasting primary selection into element {}".format(
elem.debug_text()))
@@ -1468,7 +1413,6 @@ class CommandDispatcher:
text: The text to search for.
reverse: Reverse search direction.
"""
self.set_mark("'")
view = self._current_widget()
self._clear_search(view, text)
flags = 0
@@ -1492,15 +1436,13 @@ class CommandDispatcher:
self._tabbed_browser.search_flags = flags
@cmdutils.register(instance='command-dispatcher', hide=True,
scope='window')
@cmdutils.argument('count', count=True)
scope='window', count='count')
def search_next(self, count=1):
"""Continue the search to the ([count]th) next term.
Args:
count: How many elements to ignore.
"""
self.set_mark("'")
view = self._current_widget()
self._clear_search(view, self._tabbed_browser.search_text)
@@ -1514,15 +1456,13 @@ class CommandDispatcher:
view.search(view.search_text, view.search_flags)
@cmdutils.register(instance='command-dispatcher', hide=True,
scope='window')
@cmdutils.argument('count', count=True)
scope='window', count='count')
def search_prev(self, count=1):
"""Continue the search to the ([count]th) previous term.
Args:
count: How many elements to ignore.
"""
self.set_mark("'")
view = self._current_widget()
self._clear_search(view, self._tabbed_browser.search_text)
@@ -1543,8 +1483,7 @@ class CommandDispatcher:
view.search(view.search_text, flags)
@cmdutils.register(instance='command-dispatcher', hide=True,
modes=[KeyMode.caret], scope='window')
@cmdutils.argument('count', count=True)
modes=[KeyMode.caret], scope='window', count='count')
def move_to_next_line(self, count=1):
"""Move the cursor or selection to the next line.
@@ -1560,8 +1499,7 @@ class CommandDispatcher:
webview.triggerPageAction(act)
@cmdutils.register(instance='command-dispatcher', hide=True,
modes=[KeyMode.caret], scope='window')
@cmdutils.argument('count', count=True)
modes=[KeyMode.caret], scope='window', count='count')
def move_to_prev_line(self, count=1):
"""Move the cursor or selection to the prev line.
@@ -1577,8 +1515,7 @@ class CommandDispatcher:
webview.triggerPageAction(act)
@cmdutils.register(instance='command-dispatcher', hide=True,
modes=[KeyMode.caret], scope='window')
@cmdutils.argument('count', count=True)
modes=[KeyMode.caret], scope='window', count='count')
def move_to_next_char(self, count=1):
"""Move the cursor or selection to the next char.
@@ -1594,8 +1531,7 @@ class CommandDispatcher:
webview.triggerPageAction(act)
@cmdutils.register(instance='command-dispatcher', hide=True,
modes=[KeyMode.caret], scope='window')
@cmdutils.argument('count', count=True)
modes=[KeyMode.caret], scope='window', count='count')
def move_to_prev_char(self, count=1):
"""Move the cursor or selection to the previous char.
@@ -1611,8 +1547,7 @@ class CommandDispatcher:
webview.triggerPageAction(act)
@cmdutils.register(instance='command-dispatcher', hide=True,
modes=[KeyMode.caret], scope='window')
@cmdutils.argument('count', count=True)
modes=[KeyMode.caret], scope='window', count='count')
def move_to_end_of_word(self, count=1):
"""Move the cursor or selection to the end of the word.
@@ -1633,8 +1568,7 @@ class CommandDispatcher:
webview.triggerPageAction(a)
@cmdutils.register(instance='command-dispatcher', hide=True,
modes=[KeyMode.caret], scope='window')
@cmdutils.argument('count', count=True)
modes=[KeyMode.caret], scope='window', count='count')
def move_to_next_word(self, count=1):
"""Move the cursor or selection to the next word.
@@ -1655,8 +1589,7 @@ class CommandDispatcher:
webview.triggerPageAction(a)
@cmdutils.register(instance='command-dispatcher', hide=True,
modes=[KeyMode.caret], scope='window')
@cmdutils.argument('count', count=True)
modes=[KeyMode.caret], scope='window', count='count')
def move_to_prev_word(self, count=1):
"""Move the cursor or selection to the previous word.
@@ -1694,8 +1627,7 @@ class CommandDispatcher:
webview.triggerPageAction(act)
@cmdutils.register(instance='command-dispatcher', hide=True,
modes=[KeyMode.caret], scope='window')
@cmdutils.argument('count', count=True)
modes=[KeyMode.caret], scope='window', count='count')
def move_to_start_of_next_block(self, count=1):
"""Move the cursor or selection to the start of next block.
@@ -1714,8 +1646,7 @@ class CommandDispatcher:
webview.triggerPageAction(a)
@cmdutils.register(instance='command-dispatcher', hide=True,
modes=[KeyMode.caret], scope='window')
@cmdutils.argument('count', count=True)
modes=[KeyMode.caret], scope='window', count='count')
def move_to_start_of_prev_block(self, count=1):
"""Move the cursor or selection to the start of previous block.
@@ -1734,8 +1665,7 @@ class CommandDispatcher:
webview.triggerPageAction(a)
@cmdutils.register(instance='command-dispatcher', hide=True,
modes=[KeyMode.caret], scope='window')
@cmdutils.argument('count', count=True)
modes=[KeyMode.caret], scope='window', count='count')
def move_to_end_of_next_block(self, count=1):
"""Move the cursor or selection to the end of next block.
@@ -1754,8 +1684,7 @@ class CommandDispatcher:
webview.triggerPageAction(a)
@cmdutils.register(instance='command-dispatcher', hide=True,
modes=[KeyMode.caret], scope='window')
@cmdutils.argument('count', count=True)
modes=[KeyMode.caret], scope='window', count='count')
def move_to_end_of_prev_block(self, count=1):
"""Move the cursor or selection to the end of previous block.
@@ -1806,7 +1735,7 @@ class CommandDispatcher:
message.info(self._win_id, "Nothing to yank")
return
if sel and utils.supports_selection():
if sel and QApplication.clipboard().supportsSelection():
target = "primary selection"
else:
sel = False
@@ -1834,8 +1763,7 @@ class CommandDispatcher:
self._current_widget().triggerPageAction(QWebPage.MoveToNextChar)
@cmdutils.register(instance='command-dispatcher', scope='window',
debug=True)
@cmdutils.argument('count', count=True)
count='count', debug=True)
def debug_webaction(self, action, count=1):
"""Execute a webaction.
@@ -1927,47 +1855,3 @@ class CommandDispatcher:
"""Clear remembered SSL error answers."""
nam = self._current_widget().page().networkAccessManager()
nam.clear_all_ssl_errors()
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('count', count=True)
def edit_url(self, url=None, bg=False, tab=False, window=False,
count=None):
"""Navigate to a url formed in an external editor.
The editor which should be launched can be configured via the
`general -> editor` config option.
Args:
url: URL to edit; defaults to the current page url.
bg: Open in a new background tab.
tab: Open in a new tab.
window: Open in a new window.
count: The tab index to open the URL in, or None.
"""
cmdutils.check_exclusive((tab, bg, window), 'tbw')
ed = editor.ExternalEditor(self._win_id, self._tabbed_browser)
# Passthrough for openurl args (e.g. -t, -b, -w)
ed.editing_finished.connect(functools.partial(
self.openurl, bg=bg, tab=tab, window=window, count=count))
ed.edit(url or self._current_url().toString())
@cmdutils.register(instance='command-dispatcher', scope='window')
def set_mark(self, key):
"""Set a mark at the current scroll position in the current tab.
Args:
key: mark identifier; capital indicates a global mark
"""
self._tabbed_browser.set_mark(key)
@cmdutils.register(instance='command-dispatcher', scope='window')
def jump_mark(self, key):
"""Jump to the mark named by `key`.
Args:
key: mark identifier; capital indicates a global mark
"""
self._tabbed_browser.jump_mark(key)

View File

@@ -49,7 +49,10 @@ ModelRole = usertypes.enum('ModelRole', ['item'], start=Qt.UserRole,
RetryInfo = collections.namedtuple('RetryInfo', ['request', 'manager'])
DownloadPath = collections.namedtuple('DownloadPath', ['filename', 'question'])
DownloadPath = collections.namedtuple('DownloadPath', ['filename',
'question'])
# Remember the last used directory
last_used_directory = None
@@ -87,7 +90,7 @@ def path_suggestion(filename):
return filename
elif suggestion == 'both':
return os.path.join(download_dir(), filename)
else: # pragma: no cover
else:
raise ValueError("Invalid suggestion value {}!".format(suggestion))
@@ -145,7 +148,8 @@ def ask_for_filename(suggested_filename, win_id, *, parent=None,
return DownloadPath(filename=download_dir(), question=None)
encoding = sys.getfilesystemencoding()
suggested_filename = utils.force_encoding(suggested_filename, encoding)
suggested_filename = utils.force_encoding(suggested_filename,
encoding)
q = usertypes.Question(parent)
q.text = "Save file to:"
@@ -412,8 +416,6 @@ class DownloadItem(QObject):
self.reply = None
self.done = True
self.data_changed.emit()
if self.fileobj is not None:
self.fileobj.close()
def init_reply(self, reply):
"""Set a new reply and connect its signals.
@@ -459,8 +461,8 @@ class DownloadItem(QObject):
elif self.stats.percentage() is None:
return start
else:
return utils.interpolate_color(start, stop,
self.stats.percentage(), system)
return utils.interpolate_color(
start, stop, self.stats.percentage(), system)
@pyqtSlot()
def cancel(self, remove_data=True):
@@ -521,7 +523,7 @@ class DownloadItem(QObject):
None: special value to stop the download.
"""
global last_used_directory
if self.fileobj is not None: # pragma: no cover
if self.fileobj is not None:
raise ValueError("fileobj was already set! filename: {}, "
"existing: {}, fileobj {}".format(
filename, self._filename, self.fileobj))
@@ -534,7 +536,7 @@ class DownloadItem(QObject):
self._filename = create_full_filename(
self.basename, os.path.join(download_dir(), filename))
# At this point, we have a misconfigured XDG_DOWNLOAD_DIR, as
# At this point, we have a misconfigured XDG_DOWNLOAd_DIR, as
# download_dir() + filename is still no absolute path.
# The config value is checked for "absoluteness", but
# ~/.config/user-dirs.dirs may be misconfigured and a non-absolute path
@@ -560,8 +562,8 @@ class DownloadItem(QObject):
txt = self._filename + " already exists. Overwrite?"
self._ask_confirm_question(txt)
# FIFO, device node, etc. Make sure we want to do this
elif (os.path.exists(self._filename) and
not os.path.isdir(self._filename)):
elif (os.path.exists(self._filename) and not
os.path.isdir(self._filename)):
txt = (self._filename + " already exists and is a special file. "
"Write to this?")
self._ask_confirm_question(txt)
@@ -574,7 +576,7 @@ class DownloadItem(QObject):
Args:
fileobj: A file-like object.
"""
if self.fileobj is not None: # pragma: no cover
if self.fileobj is not None:
raise ValueError("fileobj was already set! Old: {}, new: "
"{}".format(self.fileobj, fileobj))
self.fileobj = fileobj
@@ -784,7 +786,7 @@ class DownloadManager(QAbstractListModel):
If not, None.
"""
if fileobj is not None and filename is not None: # pragma: no cover
if fileobj is not None and filename is not None:
raise TypeError("Only one of fileobj/filename may be given!")
# WORKAROUND for Qt corrupting data loaded from cache:
# https://bugreports.qt.io/browse/QTBUG-42757
@@ -868,7 +870,7 @@ class DownloadManager(QAbstractListModel):
Return:
The created DownloadItem.
"""
if fileobj is not None and filename is not None: # pragma: no cover
if fileobj is not None and filename is not None:
raise TypeError("Only one of fileobj/filename may be given!")
if not suggested_filename:
if filename is not None:
@@ -897,8 +899,8 @@ class DownloadManager(QAbstractListModel):
download.redirected.connect(
functools.partial(self.on_redirect, download))
download.basename = suggested_filename
idx = len(self.downloads)
download.index = idx + 1 # "Human readable" index
idx = len(self.downloads) + 1
download.index = idx
self.beginInsertRows(QModelIndex(), idx, idx)
self.downloads.append(download)
self.endInsertRows()
@@ -946,8 +948,8 @@ class DownloadManager(QAbstractListModel):
raise cmdexc.CommandError("There's no download!")
raise cmdexc.CommandError("There's no download {}!".format(count))
@cmdutils.register(instance='download-manager', scope='window')
@cmdutils.argument('count', count=True)
@cmdutils.register(instance='download-manager', scope='window',
count='count')
def download_cancel(self, all_=False, count=0):
"""Cancel the last/[count]th download.
@@ -973,8 +975,8 @@ class DownloadManager(QAbstractListModel):
.format(count))
download.cancel()
@cmdutils.register(instance='download-manager', scope='window')
@cmdutils.argument('count', count=True)
@cmdutils.register(instance='download-manager', scope='window',
count='count')
def download_delete(self, count=0):
"""Delete the last/[count]th download from disk.
@@ -991,10 +993,9 @@ class DownloadManager(QAbstractListModel):
raise cmdexc.CommandError("Download {} is not done!".format(count))
download.delete()
self.remove_item(download)
log.downloads.debug("deleted download {}".format(download))
@cmdutils.register(instance='download-manager', scope='window')
@cmdutils.argument('count', count=True)
@cmdutils.register(instance='download-manager', scope='window',
count='count')
def download_open(self, count=0):
"""Open the last/[count]th download.
@@ -1011,8 +1012,8 @@ class DownloadManager(QAbstractListModel):
raise cmdexc.CommandError("Download {} is not done!".format(count))
download.open_file()
@cmdutils.register(instance='download-manager', scope='window')
@cmdutils.argument('count', count=True)
@cmdutils.register(instance='download-manager', scope='window',
count='count')
def download_retry(self, count=0):
"""Retry the first failed/[count]th download.
@@ -1097,8 +1098,8 @@ class DownloadManager(QAbstractListModel):
finished_items = [d for d in self.downloads if d.done]
self.remove_items(finished_items)
@cmdutils.register(instance='download-manager', scope='window')
@cmdutils.argument('count', count=True)
@cmdutils.register(instance='download-manager', scope='window',
count='count')
def download_remove(self, all_=False, count=0):
"""Remove the last/[count]th download from the list.
@@ -1228,8 +1229,7 @@ class DownloadManager(QAbstractListModel):
def flags(self, _index):
"""Override flags so items aren't selectable.
The default would be Qt.ItemIsEnabled | Qt.ItemIsSelectable.
"""
The default would be Qt.ItemIsEnabled | Qt.ItemIsSelectable."""
return Qt.ItemIsEnabled | Qt.ItemNeverHasChildren
def rowCount(self, parent=QModelIndex()):
@@ -1238,11 +1238,3 @@ class DownloadManager(QAbstractListModel):
# We don't have children
return 0
return len(self.downloads)
def running_downloads(self):
"""Return the amount of still running downloads.
Return:
The number of unfinished downloads.
"""
return sum(1 for download in self.downloads if not download.done)

View File

@@ -23,7 +23,7 @@ import collections
import functools
import math
import re
from string import ascii_lowercase
import string
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, QObject, QEvent, Qt, QUrl,
QTimer)
@@ -36,6 +36,7 @@ from qutebrowser.keyinput import modeman, modeparsers
from qutebrowser.browser import webelem
from qutebrowser.commands import userscripts, cmdexc, cmdutils, runners
from qutebrowser.utils import usertypes, log, qtutils, message, objreg, utils
from qutebrowser.misc import guiprocess
ElemTuple = collections.namedtuple('ElemTuple', ['elem', 'label'])
@@ -67,9 +68,7 @@ class HintContext:
frames: The QWebFrames to use.
destroyed_frames: id()'s of QWebFrames which have been destroyed.
(Workaround for https://github.com/The-Compiler/qutebrowser/issues/152)
all_elems: A list of all (elem, label) namedtuples ever created.
elems: A mapping from key strings to (elem, label) namedtuples.
May contain less elements than `all_elems` due to filtering.
baseurl: The URL of the current page.
target: What to do with the opened links.
normal/current/tab/tab_fg/tab_bg/window: Get passed to
@@ -88,7 +87,6 @@ class HintContext:
"""
def __init__(self):
self.all_elems = []
self.elems = {}
self.target = None
self.baseurl = None
@@ -120,7 +118,6 @@ class HintManager(QObject):
_context: The HintContext for the current invocation.
_win_id: The window ID this HintManager is associated with.
_tab_id: The tab ID this HintManager is associated with.
_filterstr: Used to save the filter string for restoring in rapid mode.
Signals:
mouse_event: Mouse event to be posted in the web view.
@@ -157,7 +154,6 @@ class HintManager(QObject):
self._win_id = win_id
self._tab_id = tab_id
self._context = None
self._filterstr = None
self._word_hinter = WordHinter()
mode_manager = objreg.get('mode-manager', scope='window',
window=win_id)
@@ -173,7 +169,7 @@ class HintManager(QObject):
def _cleanup(self):
"""Clean up after hinting."""
for elem in self._context.all_elems:
for elem in self._context.elems.values():
try:
elem.label.removeFromDocument()
except webelem.IsNullError:
@@ -223,7 +219,7 @@ class HintManager(QObject):
else:
chars = config.get('hints', 'chars')
min_chars = config.get('hints', 'min-chars')
if config.get('hints', 'scatter') and hint_mode != 'number':
if config.get('hints', 'scatter'):
return self._hint_scattered(min_chars, chars, elems)
else:
return self._hint_linear(min_chars, chars, elems)
@@ -355,7 +351,7 @@ class HintManager(QObject):
('display', 'inline !important'),
('z-index', '{} !important'.format(int(2 ** 32 / 2 - 1))),
('pointer-events', 'none !important'),
('position', 'fixed !important'),
('position', 'absolute !important'),
('color', config.get('colors', 'hints.fg') + ' !important'),
('background', config.get('colors', 'hints.bg') + ' !important'),
('font', config.get('fonts', 'hints') + ' !important'),
@@ -381,16 +377,19 @@ class HintManager(QObject):
elem: The QWebElement to set the style attributes for.
label: The label QWebElement.
"""
no_js = config.get('hints', 'find-implementation') != 'javascript'
rect = elem.rect_on_view(adjust_zoom=False, no_js=no_js)
rect = elem.geometry()
left = rect.x()
top = rect.y()
log.hints.vdebug("Drawing label '{!r}' at {}/{} for element '{!r}' "
"(no_js: {})".format(label, left, top, elem, no_js))
zoom = elem.webFrame().zoomFactor()
if not config.get('ui', 'zoom-text-only'):
left /= zoom
top /= zoom
log.hints.vdebug("Drawing label '{!r}' at {}/{} for element '{!r}', "
"zoom level {}".format(label, left, top, elem, zoom))
label.setStyleProperty('left', '{}px !important'.format(left))
label.setStyleProperty('top', '{}px !important'.format(top))
def _draw_label(self, elem, string):
def _draw_label(self, elem, text):
"""Draw a hint label over an element.
Args:
@@ -415,7 +414,7 @@ class HintManager(QObject):
label = webelem.WebElementWrapper(parent.lastChild())
label['class'] = 'qutehint'
self._set_style_properties(elem, label)
label.setPlainText(string)
label.setPlainText(text)
return label
def _show_url_error(self):
@@ -442,22 +441,14 @@ class HintManager(QObject):
target_mapping[Target.tab] = usertypes.ClickTarget.tab_bg
else:
target_mapping[Target.tab] = usertypes.ClickTarget.tab
# Click the center of the largest square fitting into the top/left
# corner of the rectangle, this will help if part of the <a> element
# is hidden behind other elements
# https://github.com/The-Compiler/qutebrowser/issues/1005
rect = elem.rect_on_view()
if rect.width() > rect.height():
rect.setWidth(rect.height())
else:
rect.setHeight(rect.width())
pos = rect.center()
# FIXME Instead of clicking the center, we could have nicer heuristics.
# e.g. parse (-webkit-)border-radius correctly and click text fields at
# the bottom right, and everything else on the top left or so.
# https://github.com/The-Compiler/qutebrowser/issues/70
pos = elem.rect_on_view().center()
action = "Hovering" if context.target == Target.hover else "Clicking"
log.hints.debug("{} on '{}' at position {}".format(
action, elem.debug_text(), pos))
log.hints.debug("{} on '{}' at {}/{}".format(
action, elem, pos.x(), pos.y()))
self.start_hinting.emit(target_mapping[context.target])
if context.target in [Target.tab, Target.tab_fg, Target.tab_bg,
Target.window]:
@@ -475,13 +466,6 @@ class HintManager(QObject):
QMouseEvent(QEvent.MouseButtonRelease, pos, Qt.LeftButton,
Qt.NoButton, modifiers),
]
if context.target in [Target.normal, Target.current]:
# Set the pre-jump mark ', so we can jump back here after following
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=self._win_id)
tabbed_browser.set_mark("'")
if context.target == Target.current:
elem.remove_blank_target()
for evt in events:
@@ -499,9 +483,7 @@ class HintManager(QObject):
url: The URL to open as a QUrl.
context: The HintContext to use.
"""
sel = (context.target == Target.yank_primary and
utils.supports_selection())
sel = context.target == Target.yank_primary
urlstr = url.toString(QUrl.FullyEncoded | QUrl.RemovePassword)
utils.set_clipboard(urlstr, selection=sel)
@@ -590,8 +572,9 @@ class HintManager(QObject):
"""
urlstr = url.toString(QUrl.FullyEncoded | QUrl.RemovePassword)
args = context.get_args(urlstr)
commandrunner = runners.CommandRunner(self._win_id)
commandrunner.run_safely('spawn ' + ' '.join(args))
cmd, *args = args
proc = guiprocess.GUIProcess(self._win_id, what='command', parent=self)
proc.start(cmd, args)
def _resolve_url(self, elem, baseurl):
"""Resolve a URL and check if we want to keep it.
@@ -605,7 +588,7 @@ class HintManager(QObject):
"""
for attr in ('href', 'src'):
if attr in elem:
text = elem[attr].strip()
text = elem[attr]
break
else:
return None
@@ -621,7 +604,8 @@ class HintManager(QObject):
def _find_prevnext(self, frame, prev=False):
"""Find a prev/next element in frame."""
# First check for <link rel="prev(ious)|next">
elems = frame.findAllElements(webelem.SELECTORS[webelem.Group.links])
elems = frame.findAllElements(
webelem.SELECTORS[webelem.Group.links])
rel_values = ('prev', 'previous') if prev else ('next')
for e in elems:
e = webelem.WebElementWrapper(e)
@@ -697,27 +681,15 @@ class HintManager(QObject):
elems = [e for e in elems if filterfunc(e)]
if not elems:
raise cmdexc.CommandError("No elements found.")
strings = self._hint_strings(elems)
log.hints.debug("hints: {}".format(', '.join(strings)))
for e, string in zip(elems, strings):
label = self._draw_label(e, string)
elem = ElemTuple(e, label)
self._context.all_elems.append(elem)
self._context.elems[string] = elem
hints = self._hint_strings(elems)
log.hints.debug("hints: {}".format(', '.join(hints)))
for e, hint in zip(elems, hints):
label = self._draw_label(e, hint)
self._context.elems[hint] = ElemTuple(e, label)
keyparsers = objreg.get('keyparsers', scope='window',
window=self._win_id)
keyparser = keyparsers[usertypes.KeyMode.hint]
keyparser.update_bindings(strings)
def _filter_matches(self, filterstr, elemstr):
"""Return True if `filterstr` matches `elemstr`."""
# Empty string and None always match
if not filterstr:
return True
filterstr = filterstr.casefold()
elemstr = elemstr.casefold()
# Do multi-word matching
return all(word in elemstr for word in filterstr.split())
keyparser.update_bindings(hints)
def follow_prevnext(self, frame, baseurl, prev=False, tab=False,
background=False, window=False):
@@ -757,10 +729,9 @@ class HintManager(QObject):
webview.openurl(url)
@cmdutils.register(instance='hintmanager', scope='tab', name='hint',
star_args_optional=True, maxsplit=2)
@cmdutils.argument('win_id', win_id=True)
win_id='win_id')
def start(self, rapid=False, group=webelem.Group.all, target=Target.normal,
*args, win_id):
*args: {'nargs': '*'}, win_id):
"""Start hinting.
Args:
@@ -824,7 +795,7 @@ class HintManager(QObject):
if rapid:
if target in [Target.tab_bg, Target.window, Target.run,
Target.hover, Target.userscript, Target.spawn,
Target.download, Target.normal, Target.current]:
Target.download]:
pass
elif (target == Target.tab and
config.get('tabs', 'background-tabs')):
@@ -862,115 +833,53 @@ class HintManager(QObject):
def handle_partial_key(self, keystr):
"""Handle a new partial keypress."""
log.hints.debug("Handling new keystring: '{}'".format(keystr))
for string, elem in self._context.elems.items():
for (text, elems) in self._context.elems.items():
try:
if string.startswith(keystr):
matched = string[:len(keystr)]
rest = string[len(keystr):]
if text.startswith(keystr):
matched = text[:len(keystr)]
rest = text[len(keystr):]
match_color = config.get('colors', 'hints.fg.match')
elem.label.setInnerXml(
elems.label.setInnerXml(
'<font color="{}">{}</font>{}'.format(
match_color, matched, rest))
if self._is_hidden(elem.label):
if self._is_hidden(elems.label):
# hidden element which matches again -> show it
self._show_elem(elem.label)
self._show_elem(elems.label)
else:
# element doesn't match anymore -> hide it
self._hide_elem(elem.label)
self._hide_elem(elems.label)
except webelem.IsNullError:
pass
def _filter_number_hints(self):
"""Apply filters for numbered hints and renumber them.
Return:
Elements which are still visible
"""
# renumber filtered hints
elems = []
for e in self._context.all_elems:
try:
if not self._is_hidden(e.label):
elems.append(e)
except webelem.IsNullError:
pass
if not elems:
# Whoops, filtered all hints
modeman.leave(self._win_id, usertypes.KeyMode.hint,
'all filtered')
return {}
strings = self._hint_strings(elems)
self._context.elems = {}
for elem, string in zip(elems, strings):
elem.label.setInnerXml(string)
self._context.elems[string] = elem
keyparsers = objreg.get('keyparsers', scope='window',
window=self._win_id)
keyparser = keyparsers[usertypes.KeyMode.hint]
keyparser.update_bindings(strings, preserve_filter=True)
return self._context.elems
def _filter_non_number_hints(self):
"""Apply filters for letter/word hints.
Return:
Elements which are still visible
"""
visible = {}
for string, elem in self._context.elems.items():
try:
if not self._is_hidden(elem.label):
visible[string] = elem
except webelem.IsNullError:
pass
if not visible:
# Whoops, filtered all hints
modeman.leave(self._win_id, usertypes.KeyMode.hint,
'all filtered')
return visible
def filter_hints(self, filterstr):
"""Filter displayed hints according to a text.
Args:
filterstr: The string to filter with, or None to use the filter
from previous call (saved in `self._filterstr`). If
`filterstr` is an empty string or if both `filterstr`
and `self._filterstr` are None, all hints are shown.
filterstr: The string to filter with, or None to show all.
"""
if filterstr is None:
filterstr = self._filterstr
else:
self._filterstr = filterstr
for elem in self._context.all_elems:
for elems in self._context.elems.values():
try:
if self._filter_matches(filterstr, str(elem.elem)):
if self._is_hidden(elem.label):
if (filterstr is None or
filterstr.casefold() in str(elems.elem).casefold()):
if self._is_hidden(elems.label):
# hidden element which matches again -> show it
self._show_elem(elem.label)
self._show_elem(elems.label)
else:
# element doesn't match anymore -> hide it
self._hide_elem(elem.label)
self._hide_elem(elems.label)
except webelem.IsNullError:
pass
if config.get('hints', 'mode') == 'number':
visible = self._filter_number_hints()
else:
visible = self._filter_non_number_hints()
if (len(visible) == 1 and
config.get('hints', 'auto-follow') and
filterstr is not None):
# apply auto-follow-timeout
timeout = config.get('hints', 'auto-follow-timeout')
keyparsers = objreg.get('keyparsers', scope='window',
window=self._win_id)
normal_parser = keyparsers[usertypes.KeyMode.normal]
normal_parser.set_inhibited_timeout(timeout)
visible = {}
for k, e in self._context.elems.items():
try:
if not self._is_hidden(e.label):
visible[k] = e
except webelem.IsNullError:
pass
if not visible:
# Whoops, filtered all hints
modeman.leave(self._win_id, usertypes.KeyMode.hint, 'all filtered')
elif len(visible) == 1 and config.get('hints', 'auto-follow'):
# unpacking gets us the first (and only) key in the dict.
self.fire(*visible)
@@ -1008,31 +917,30 @@ class HintManager(QObject):
}
elem = self._context.elems[keystr].elem
if elem.webFrame() is None:
message.error(self._win_id,
"This element has no webframe.",
message.error(self._win_id, "This element has no webframe.",
immediately=True)
return
if self._context.target in elem_handlers:
handler = functools.partial(elem_handlers[self._context.target],
elem, self._context)
handler = functools.partial(
elem_handlers[self._context.target], elem, self._context)
elif self._context.target in url_handlers:
url = self._resolve_url(elem, self._context.baseurl)
if url is None:
self._show_url_error()
return
handler = functools.partial(url_handlers[self._context.target],
url, self._context)
handler = functools.partial(
url_handlers[self._context.target], url, self._context)
else:
raise ValueError("No suitable handler found!")
if not self._context.rapid:
modeman.maybe_leave(self._win_id, usertypes.KeyMode.hint,
'followed')
else:
# Reset filtering
# Show all hints again
self.filter_hints(None)
# Undo keystring highlighting
for string, elem in self._context.elems.items():
elem.label.setInnerXml(string)
for (text, elems) in self._context.elems.items():
elems.label.setInnerXml(text)
handler()
@cmdutils.register(instance='hintmanager', scope='tab', hide=True,
@@ -1056,13 +964,13 @@ class HintManager(QObject):
def on_contents_size_changed(self, _size):
"""Reposition hints if contents size changed."""
log.hints.debug("Contents size changed...!")
for e in self._context.all_elems:
for elems in self._context.elems.values():
try:
if e.elem.webFrame() is None:
if elems.elem.webFrame() is None:
# This sometimes happens for some reason...
e.label.removeFromDocument()
elems.label.removeFromDocument()
continue
self._set_style_position(e.elem, e.label)
self._set_style_position(elems.elem, elems.label)
except webelem.IsNullError:
pass
@@ -1092,14 +1000,14 @@ class WordHinter:
self.dictionary = None
def ensure_initialized(self):
"""Generate the used words if yet uninitialized."""
"""Generate the used words if yet uninialized."""
dictionary = config.get("hints", "dictionary")
if not self.words or self.dictionary != dictionary:
self.words.clear()
self.dictionary = dictionary
try:
with open(dictionary, encoding="UTF-8") as wordfile:
alphabet = set(ascii_lowercase)
alphabet = set(string.ascii_lowercase)
hints = set()
lines = (line.rstrip().lower() for line in wordfile)
for word in lines:
@@ -1129,11 +1037,13 @@ class WordHinter:
"text": str,
}
extractable_attrs = collections.defaultdict(list, {
"IMG": ["alt", "title", "src"],
"A": ["title", "href", "text"],
"INPUT": ["name"]
})
extractable_attrs = collections.defaultdict(
list, {
"IMG": ["alt", "title", "src"],
"A": ["title", "href", "text"],
"INPUT": ["name"]
}
)
return (attr_extractors[attr](elem)
for attr in extractable_attrs[elem.tagName()]
@@ -1151,7 +1061,8 @@ class WordHinter:
yield candidate[match.start():match.end()].lower()
def any_prefix(self, hint, existing):
return any(hint.startswith(e) or e.startswith(hint) for e in existing)
return any(hint.startswith(e) or e.startswith(hint)
for e in existing)
def filter_prefixes(self, hints, existing):
return (h for h in hints if not self.any_prefix(h, existing))

View File

@@ -22,144 +22,45 @@
import time
import collections
from PyQt5.QtCore import pyqtSignal, QUrl, QObject
from PyQt5.QtCore import pyqtSignal, QUrl
from PyQt5.QtWebKit import QWebHistoryInterface
from qutebrowser.commands import cmdutils
from qutebrowser.utils import utils, objreg, standarddir, log, qtutils
from qutebrowser.utils import utils, objreg, standarddir, log
from qutebrowser.config import config
from qutebrowser.misc import lineparser
class Entry:
class HistoryEntry:
"""A single entry in the web history.
Attributes:
atime: The time the page was accessed.
url: The URL which was accessed as QUrl.
redirect: If True, don't save this entry to disk
url_string: The URL which was accessed as string.
"""
def __init__(self, atime, url, title, redirect=False):
def __init__(self, atime, url):
self.atime = float(atime)
self.url = url
self.title = title
self.redirect = redirect
qtutils.ensure_valid(url)
self.url = QUrl(url)
self.url_string = url
def __repr__(self):
return utils.get_repr(self, constructor=True, atime=self.atime,
url=self.url_str(), title=self.title,
redirect=self.redirect)
url=self.url.toDisplayString())
def __str__(self):
atime = str(int(self.atime))
if self.redirect:
atime += '-r' # redirect flag
elems = [atime, self.url_str()]
if self.title:
elems.append(self.title)
return ' '.join(elems)
def __eq__(self, other):
return (self.atime == other.atime and
self.title == other.title and
self.url == other.url and
self.redirect == other.redirect)
def url_str(self):
"""Get the URL as a lossless string."""
return self.url.toString(QUrl.FullyEncoded | QUrl.RemovePassword)
@classmethod
def from_str(cls, line):
"""Parse a history line like '12345 http://example.com title'."""
data = line.split(maxsplit=2)
if len(data) == 2:
atime, url = data
title = ""
elif len(data) == 3:
atime, url, title = data
else:
raise ValueError("2 or 3 fields expected")
url = QUrl(url)
if not url.isValid():
raise ValueError("Invalid URL: {}".format(url.errorString()))
if atime.startswith('\0'):
log.init.debug(
"Removing NUL bytes from entry {!r} - see "
"https://github.com/The-Compiler/qutebrowser/issues/"
"670".format(data))
atime = atime.lstrip('\0')
if '-' in atime:
atime, flags = atime.split('-')
else:
flags = ''
if not set(flags).issubset('r'):
raise ValueError("Invalid flags {!r}".format(flags))
redirect = 'r' in flags
return cls(atime, url, title, redirect=redirect)
return '{} {}'.format(int(self.atime), self.url_string)
class WebHistoryInterface(QWebHistoryInterface):
class WebHistory(QWebHistoryInterface):
"""Glue code between WebHistory and Qt's QWebHistoryInterface.
"""A QWebHistoryInterface which supports being written to disk.
Attributes:
_history: The WebHistory object.
"""
def __init__(self, webhistory, parent=None):
super().__init__(parent)
self._history = webhistory
def addHistoryEntry(self, url_string):
"""Required for a QWebHistoryInterface impl, obsoleted by add_url."""
pass
def historyContains(self, url_string):
"""Called by WebKit to determine if an URL is contained in the history.
Args:
url_string: The URL (as string) to check for.
Return:
True if the url is in the history, False otherwise.
"""
return url_string in self._history.history_dict
class WebHistory(QObject):
"""The global history of visited pages.
This is a little more complex as you'd expect so the history can be read
from disk async while new history is already arriving.
self.history_dict is the main place where the history is stored, in an
OrderedDict (sorted by time) of URL strings mapped to Entry objects.
While reading from disk is still ongoing, the history is saved in
self._temp_history instead, and then appended to self.history_dict once
that's fully populated.
All history which is new in this session (rather than read from disk from a
previous browsing session) is also stored in self._new_history.
self._saved_count tracks how many of those entries were already written to
disk, so we can always append to the existing data.
Attributes:
history_dict: An OrderedDict of URLs read from the on-disk history.
_hist_dir: The directory to store the history in
_lineparser: The AppendLineParser used to save the history.
_new_history: A list of Entry items of the current session.
_history_dict: An OrderedDict of URLs read from the on-disk history.
_new_history: A list of HistoryEntry items of the current session.
_saved_count: How many HistoryEntries have been written to disk.
_initial_read_started: Whether async_read was called.
_initial_read_done: Whether async_read has completed.
@@ -167,28 +68,23 @@ class WebHistory(QObject):
async_read was called.
Signals:
add_completion_item: Emitted before a new Entry is added.
Used to sync with the completion.
arg: The new Entry.
item_added: Emitted after a new Entry is added.
Used to tell the savemanager that the history is dirty.
arg: The new Entry.
cleared: Emitted after the history is cleared.
add_completion_item: Emitted before a new HistoryEntry is added.
arg: The new HistoryEntry.
item_added: Emitted after a new HistoryEntry is added.
arg: The new HistoryEntry.
"""
add_completion_item = pyqtSignal(Entry)
item_added = pyqtSignal(Entry)
cleared = pyqtSignal()
add_completion_item = pyqtSignal(HistoryEntry)
item_added = pyqtSignal(HistoryEntry)
async_read_done = pyqtSignal()
def __init__(self, hist_dir, hist_name, parent=None):
def __init__(self, parent=None):
super().__init__(parent)
self._initial_read_started = False
self._initial_read_done = False
self._hist_dir = hist_dir
self._lineparser = lineparser.AppendLineParser(hist_dir, hist_name,
parent=self)
self.history_dict = collections.OrderedDict()
self._lineparser = lineparser.AppendLineParser(
standarddir.data(), 'history', parent=self)
self._history_dict = collections.OrderedDict()
self._temp_history = collections.OrderedDict()
self._new_history = []
self._saved_count = 0
@@ -198,11 +94,14 @@ class WebHistory(QObject):
def __repr__(self):
return utils.get_repr(self, length=len(self))
def __getitem__(self, key):
return self._new_history[key]
def __iter__(self):
return iter(self.history_dict.values())
return iter(self._history_dict.values())
def __len__(self):
return len(self.history_dict)
return len(self._history_dict)
def async_read(self):
"""Read the initial history."""
@@ -211,51 +110,52 @@ class WebHistory(QObject):
return
self._initial_read_started = True
if self._hist_dir is None:
if standarddir.data() is None:
self._initial_read_done = True
self.async_read_done.emit()
assert not self._temp_history
return
with self._lineparser.open():
for line in self._lineparser:
yield
line = line.rstrip()
if not line:
data = line.rstrip().split(maxsplit=1)
if not data:
# empty line
continue
try:
entry = Entry.from_str(line)
except ValueError as e:
log.init.warning("Invalid history entry {!r}: {}!".format(
line, e))
elif len(data) != 2:
# other malformed line
log.init.warning("Invalid history entry {!r}!".format(
line))
continue
atime, url = data
if atime.startswith('\0'):
log.init.warning(
"Removing NUL bytes from entry {!r} - see "
"https://github.com/The-Compiler/qutebrowser/issues/"
"670".format(data))
atime = atime.lstrip('\0')
# This de-duplicates history entries; only the latest
# entry for each URL is kept. If you want to keep
# information about previous hits change the items in
# old_urls to be lists or change Entry to have a
# old_urls to be lists or change HistoryEntry to have a
# list of atimes.
entry = HistoryEntry(atime, url)
self._add_entry(entry)
self._initial_read_done = True
self.async_read_done.emit()
for entry in self._temp_history.values():
self._add_entry(entry)
for url, entry in self._temp_history.items():
self._new_history.append(entry)
if not entry.redirect:
self.add_completion_item.emit(entry)
self._temp_history.clear()
self._add_entry(entry)
self.add_completion_item.emit(entry)
def _add_entry(self, entry, target=None):
"""Add an entry to self.history_dict or another given OrderedDict."""
"""Add an entry to self._history_dict or another given OrderedDict."""
if target is None:
target = self.history_dict
url_str = entry.url_str()
target[url_str] = entry
target.move_to_end(url_str)
target = self._history_dict
target[entry.url_string] = entry
target.move_to_end(entry.url_string)
def get_recent(self):
"""Get the most recent history entries."""
@@ -269,44 +169,36 @@ class WebHistory(QObject):
self._lineparser.save()
self._saved_count = len(self._new_history)
@cmdutils.register(name='history-clear', instance='web-history')
def clear(self):
"""Clear all browsing history.
Note this only clears the global history
(e.g. `~/.local/share/qutebrowser/history` on Linux) but not cookies,
the back/forward history of a tab, cache or other persistent data.
"""
self._lineparser.clear()
self.history_dict.clear()
self._temp_history.clear()
self._new_history.clear()
self._saved_count = 0
self.cleared.emit()
def add_url(self, url, title="", *, redirect=False, atime=None):
def addHistoryEntry(self, url_string):
"""Called by WebKit when an URL should be added to the history.
Args:
url: An url (as QUrl) to add to the history.
redirect: Whether the entry was redirected to another URL
(hidden in completion)
atime: Override the atime used to add the entry
url_string: An url as string to add to the history.
"""
if not url_string:
return
if config.get('general', 'private-browsing'):
return
if atime is None:
atime = time.time()
entry = Entry(atime, url, title, redirect=redirect)
entry = HistoryEntry(time.time(), url_string)
if self._initial_read_done:
self._add_entry(entry)
self.add_completion_item.emit(entry)
self._new_history.append(entry)
self._add_entry(entry)
self.item_added.emit(entry)
if not entry.redirect:
self.add_completion_item.emit(entry)
else:
self._add_entry(entry, target=self._temp_history)
def historyContains(self, url_string):
"""Called by WebKit to determine if an URL is contained in the history.
Args:
url_string: The URL (as string) to check for.
Return:
True if the url is in the history, False otherwise.
"""
return url_string in self._history_dict
def init(parent=None):
"""Initialize the web history.
@@ -314,9 +206,6 @@ def init(parent=None):
Args:
parent: The parent to use for WebHistory.
"""
history = WebHistory(hist_dir=standarddir.data(), hist_name='history',
parent=parent)
history = WebHistory(parent)
objreg.register('web-history', history)
interface = WebHistoryInterface(history, parent=history)
QWebHistoryInterface.setDefaultInterface(interface)
QWebHistoryInterface.setDefaultInterface(history)

View File

@@ -343,9 +343,12 @@ class _Downloader:
item = download_manager.get(url, fileobj=_NoCloseBytesIO(),
auto_remove=True)
self.pending_downloads.add((url, item))
item.finished.connect(functools.partial(self._finished, url, item))
item.error.connect(functools.partial(self._error, url, item))
item.cancelled.connect(functools.partial(self._cancelled, url, item))
item.finished.connect(
functools.partial(self._finished, url, item))
item.error.connect(
functools.partial(self._error, url, item))
item.cancelled.connect(
functools.partial(self._error, url, item))
def _finished(self, url, item):
"""Callback when a single asset is downloaded.
@@ -401,7 +404,7 @@ class _Downloader:
"""Callback when a download error occurred.
Args:
url: The original url of the asset as QUrl.
url: The orignal url of the asset as QUrl.
item: The DownloadItem given by the DownloadManager.
"""
try:
@@ -418,20 +421,6 @@ class _Downloader:
return
self._finish_file()
def _cancelled(self, url, item):
"""Callback when a download is cancelled by the user.
Args:
url: The original url of the asset as QUrl.
item: The DownloadItem given by the DownloadManager.
"""
# This callback is called before _finished, so there's no need to
# remove the item or close the fileobject.
log.downloads.debug("MHTML download cancelled by user: {}".format(url))
# Write an empty file instead
item.fileobj.seek(0)
item.fileobj.truncate()
def _finish_file(self):
"""Save the file to the filename given in __init__."""
if self._finished_file:

View File

@@ -31,6 +31,7 @@ from PyQt5.QtNetwork import (QNetworkAccessManager, QNetworkReply, QSslError,
from qutebrowser.config import config
from qutebrowser.utils import (message, log, usertypes, utils, objreg, qtutils,
urlutils, debug)
from qutebrowser.browser import cookies
from qutebrowser.browser.network import qutescheme, networkreply
from qutebrowser.browser.network import filescheme
@@ -40,73 +41,14 @@ ProxyId = collections.namedtuple('ProxyId', 'type, hostname, port')
_proxy_auth_cache = {}
def _is_secure_cipher(cipher):
"""Check if a given SSL cipher (hopefully) isn't broken yet."""
tokens = [e.upper() for e in cipher.name().split('-')]
if cipher.usedBits() < 128:
# https://codereview.qt-project.org/#/c/75943/
return False
# OpenSSL should already protect against this in a better way
elif cipher.keyExchangeMethod() == 'DH' and os.name == 'nt':
# https://weakdh.org/
return False
elif cipher.encryptionMethod().upper().startswith('RC4'):
# http://en.wikipedia.org/wiki/RC4#Security
# https://codereview.qt-project.org/#/c/148906/
return False
elif cipher.encryptionMethod().upper().startswith('DES'):
# http://en.wikipedia.org/wiki/Data_Encryption_Standard#Security_and_cryptanalysis
return False
elif 'MD5' in tokens:
# http://www.win.tue.nl/hashclash/rogue-ca/
return False
# OpenSSL should already protect against this in a better way
# elif (('CBC3' in tokens or 'CBC' in tokens) and (cipher.protocol() not in
# [QSsl.TlsV1_0, QSsl.TlsV1_1, QSsl.TlsV1_2])):
# # http://en.wikipedia.org/wiki/POODLE
# return False
### These things should never happen as those are already filtered out by
### either the SSL libraries or Qt - but let's be sure.
elif cipher.authenticationMethod() in ['aNULL', 'NULL']:
# Ciphers without authentication.
return False
elif cipher.encryptionMethod() in ['eNULL', 'NULL']:
# Ciphers without encryption.
return False
elif 'EXP' in tokens or 'EXPORT' in tokens:
# Weak export-grade ciphers
return False
elif 'ADH' in tokens:
# No MITM protection
return False
### This *should* happen ;)
else:
return True
def init():
"""Disable insecure SSL ciphers on old Qt versions."""
if qtutils.version_check('5.3.0'):
default_ciphers = QSslSocket.defaultCiphers()
log.init.debug("Default Qt ciphers: {}".format(
', '.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 = []
bad_ciphers = []
for cipher in default_ciphers:
if _is_secure_cipher(cipher):
good_ciphers.append(cipher)
else:
bad_ciphers.append(cipher)
log.init.debug("Disabling bad ciphers: {}".format(
', '.join(c.name() for c in bad_ciphers)))
QSslSocket.setDefaultCiphers(good_ciphers)
if not qtutils.version_check('5.3.0'):
# Disable weak SSL ciphers.
# See https://codereview.qt-project.org/#/c/75943/
good_ciphers = [c for c in QSslSocket.supportedCiphers()
if c.usedBits() >= 128]
QSslSocket.setDefaultCiphers(good_ciphers)
class SslError(QSslError):
@@ -183,15 +125,15 @@ class NetworkManager(QNetworkAccessManager):
private: Whether we're currently in private browsing mode.
"""
if private:
cookie_jar = objreg.get('ram-cookie-jar')
cookie_jar = cookies.RAMCookieJar(self)
self.setCookieJar(cookie_jar)
else:
# We have a shared cookie jar - we restore its parent so we don't
# take ownership of it.
app = QCoreApplication.instance()
cookie_jar = objreg.get('cookie-jar')
# We have a shared cookie jar - we restore its parent so we don't
# take ownership of it.
self.setCookieJar(cookie_jar)
app = QCoreApplication.instance()
cookie_jar.setParent(app)
self.setCookieJar(cookie_jar)
cookie_jar.setParent(app)
def _set_cache(self):
"""Set the cache of the NetworkManager correctly.
@@ -224,13 +166,9 @@ class NetworkManager(QNetworkAccessManager):
self.shutting_down.connect(q.abort)
if owner is not None:
owner.destroyed.connect(q.abort)
# This might be a generic network manager, e.g. one belonging to a
# DownloadManager. In this case, just skip the webview thing.
if self._tab_id is not None:
webview = objreg.get('webview', scope='tab', window=self._win_id,
tab=self._tab_id)
webview.loadStarted.connect(q.abort)
webview = objreg.get('webview', scope='tab', window=self._win_id,
tab=self._tab_id)
webview.loadStarted.connect(q.abort)
bridge = objreg.get('message-bridge', scope='window',
window=self._win_id)
bridge.ask(q, blocking=True)
@@ -272,9 +210,6 @@ class NetworkManager(QNetworkAccessManager):
is_rejected = set(errors).issubset(
self._rejected_ssl_errors[host_tpl])
log.webview.debug("Already accepted: {} / "
"rejected {}".format(is_accepted, is_rejected))
if (ssl_strict and ssl_strict != 'ask') or is_rejected:
return
elif is_accepted:
@@ -285,7 +220,6 @@ class NetworkManager(QNetworkAccessManager):
err_string = '\n'.join('- ' + err.errorString() for err in errors)
answer = self._ask('SSL errors - continue?\n{}'.format(err_string),
mode=usertypes.PromptMode.yesno, owner=reply)
log.webview.debug("Asked for SSL errors, answer {}".format(answer))
if answer:
reply.ignoreSslErrors()
err_dict = self._accepted_ssl_errors
@@ -294,7 +228,6 @@ class NetworkManager(QNetworkAccessManager):
if host_tpl is not None:
err_dict[host_tpl] += errors
else:
log.webview.debug("ssl-strict is False, only warning about errors")
for err in errors:
# FIXME we might want to use warn here (non-fatal error)
# https://github.com/The-Compiler/qutebrowser/issues/114
@@ -343,9 +276,10 @@ class NetworkManager(QNetworkAccessManager):
if user is None:
# netrc check failed
answer = self._ask("Username ({}):".format(authenticator.realm()),
mode=usertypes.PromptMode.user_pwd,
owner=reply)
answer = self._ask(
"Username ({}):".format(authenticator.realm()),
mode=usertypes.PromptMode.user_pwd,
owner=reply)
if answer is not None:
user, password = answer.user, answer.password
if user is not None:
@@ -361,9 +295,8 @@ class NetworkManager(QNetworkAccessManager):
authenticator.setUser(user)
authenticator.setPassword(password)
else:
answer = self._ask(
"Proxy username ({}):".format(authenticator.realm()),
mode=usertypes.PromptMode.user_pwd)
answer = self._ask("Proxy username ({}):".format(
authenticator.realm()), mode=usertypes.PromptMode.user_pwd)
if answer is not None:
authenticator.setUser(answer.user)
authenticator.setPassword(answer.password)
@@ -412,7 +345,7 @@ class NetworkManager(QNetworkAccessManager):
# instead of no header at all
req.setRawHeader('Referer'.encode('ascii'), QByteArray())
elif (referer_header_conf == 'same-domain' and
not urlutils.same_domain(req.url(), current_url)):
not urlutils.same_domain(req.url(), current_url)):
req.setRawHeader('Referer'.encode('ascii'), QByteArray())
# If refer_header_conf is set to 'always', we leave the header
# alone as QtWebKit did set it.
@@ -465,13 +398,6 @@ class NetworkManager(QNetworkAccessManager):
req.setRawHeader('DNT'.encode('ascii'), dnt)
req.setRawHeader('X-Do-Not-Track'.encode('ascii'), dnt)
# Load custom headers
custom_headers = config.get('network', 'custom-headers')
if custom_headers is not None:
for header, value in custom_headers.items():
req.setRawHeader(header.encode('ascii'), value.encode('ascii'))
# There are some scenarios where we can't figure out current_url:
# - There's a generic NetworkManager, e.g. for downloads
# - The download was in a tab which is now closed.

View File

@@ -26,7 +26,6 @@ Module attributes:
import functools
import configparser
import mimetypes
import urllib.parse
from PyQt5.QtCore import pyqtSlot, QObject
from PyQt5.QtNetwork import QNetworkReply
@@ -110,13 +109,13 @@ class QuteSchemeHandler(schemehandler.SchemeHandler):
request, str(e), QNetworkReply.ContentNotFoundError,
self.parent())
except QuteSchemeError as e:
return networkreply.ErrorNetworkReply(request, e.errorstring,
e.error, self.parent())
return networkreply.ErrorNetworkReply(
request, e.errorstring, e.error, self.parent())
mimetype, _encoding = mimetypes.guess_type(request.url().fileName())
if mimetype is None:
mimetype = 'text/html'
return networkreply.FixedDataNetworkReply(request, data, mimetype,
self.parent())
return networkreply.FixedDataNetworkReply(
request, data, mimetype, self.parent())
class JSBridge(QObject):
@@ -159,42 +158,23 @@ def qute_version(_win_id, _request):
@add_handler('plainlog')
def qute_plainlog(_win_id, request):
"""Handler for qute:plainlog. Return HTML content as bytes.
An optional query parameter specifies the minimum log level to print.
For example, qute://log?level=warning prints warnings and errors.
Level can be one of: vdebug, debug, info, warning, error, critical.
"""
def qute_plainlog(_win_id, _request):
"""Handler for qute:plainlog. Return HTML content as bytes."""
if log.ram_handler is None:
text = "Log output was disabled."
else:
try:
level = urllib.parse.parse_qs(request.url().query())['level'][0]
except KeyError:
level = 'vdebug'
text = log.ram_handler.dump_log(html=False, level=level)
text = log.ram_handler.dump_log()
html = jinja.render('pre.html', title='log', content=text)
return html.encode('UTF-8', errors='xmlcharrefreplace')
@add_handler('log')
def qute_log(_win_id, request):
"""Handler for qute:log. Return HTML content as bytes.
An optional query parameter specifies the minimum log level to print.
For example, qute://log?level=warning prints warnings and errors.
Level can be one of: vdebug, debug, info, warning, error, critical.
"""
def qute_log(_win_id, _request):
"""Handler for qute:log. Return HTML content as bytes."""
if log.ram_handler is None:
html_log = None
else:
try:
level = urllib.parse.parse_qs(request.url().query())['level'][0]
except KeyError:
level = 'vdebug'
html_log = log.ram_handler.dump_log(html=True, level=level)
html_log = log.ram_handler.dump_log(html=True)
html = jinja.render('log.html', title='log', content=html_log)
return html.encode('UTF-8', errors='xmlcharrefreplace')
@@ -260,4 +240,4 @@ def qute_pdfjs(_win_id, request):
log.misc.warning(
"pdfjs resource requested but not found: {}".format(e.path))
raise QuteSchemeError("Can't find pdfjs resource '{}'".format(e.path),
QNetworkReply.ContentNotFoundError)
QNetworkReply.ContentNotFoundError)

View File

@@ -51,8 +51,9 @@ def generate_pdfjs_page(url):
"""
viewer = get_pdfjs_res('web/viewer.html').decode('utf-8')
script = _generate_pdfjs_script(url)
html_page = viewer.replace('</body>',
'</body><script>{}</script>'.format(script))
html_page = viewer.replace(
'</body>', '</body><script>{}</script>'.format(script)
)
return html_page

View File

@@ -154,7 +154,8 @@ class QuickmarkManager(UrlMarkManager):
try:
key, url = line.rsplit(maxsplit=1)
except ValueError:
message.error('current', "Invalid quickmark '{}'".format(line))
message.error('current', "Invalid quickmark '{}'".format(
line))
else:
self.marks[key] = url
@@ -173,8 +174,7 @@ class QuickmarkManager(UrlMarkManager):
win_id, "Add quickmark:", usertypes.PromptMode.text,
functools.partial(self.quickmark_add, win_id, urlstr))
@cmdutils.register(instance='quickmark-manager')
@cmdutils.argument('win_id', win_id=True)
@cmdutils.register(instance='quickmark-manager', win_id='win_id')
def quickmark_add(self, win_id, url, name):
"""Add a new quickmark.
@@ -204,9 +204,8 @@ class QuickmarkManager(UrlMarkManager):
else:
set_mark()
@cmdutils.register(instance='quickmark-manager', maxsplit=0)
@cmdutils.argument('name',
completion=usertypes.Completion.quickmark_by_name)
@cmdutils.register(instance='quickmark-manager', maxsplit=0,
completion=[usertypes.Completion.quickmark_by_name])
def quickmark_del(self, name):
"""Delete a quickmark.
@@ -285,8 +284,8 @@ class BookmarkManager(UrlMarkManager):
self.changed.emit()
self.added.emit(title, urlstr)
@cmdutils.register(instance='bookmark-manager', maxsplit=0)
@cmdutils.argument('url', completion=usertypes.Completion.bookmark_by_url)
@cmdutils.register(instance='bookmark-manager', maxsplit=0,
completion=[usertypes.Completion.bookmark_by_url])
def bookmark_del(self, url):
"""Delete a bookmark.

View File

@@ -38,7 +38,7 @@ from qutebrowser.utils import log, usertypes, utils
Group = usertypes.enum('Group', ['all', 'links', 'images', 'url', 'prevnext',
'focus', 'inputs'])
'focus'])
SELECTORS = {
@@ -50,9 +50,6 @@ SELECTORS = {
Group.url: '[src], [href]',
Group.prevnext: 'a, area, button, link, [role=button]',
Group.focus: '*:focus',
Group.inputs: ('input[type=text], input[type=email], input[type=url], '
'input[type=tel], input[type=number], '
'input[type=password], input[type=search], textarea'),
}
@@ -160,7 +157,8 @@ class WebElementWrapper(collections.abc.MutableMapping):
def _check_vanished(self):
"""Raise an exception if the element vanished (is null)."""
if self._elem.isNull():
raise IsNullError('Element {} vanished!'.format(self._elem))
raise IsNullError('Element {} vanished!'.format(
self._elem))
def is_visible(self, mainframe):
"""Check whether the element is currently visible on the screen.
@@ -173,9 +171,9 @@ class WebElementWrapper(collections.abc.MutableMapping):
"""
return is_visible(self._elem, mainframe)
def rect_on_view(self, **kwargs):
def rect_on_view(self):
"""Get the geometry of the element relative to the webview."""
return rect_on_view(self._elem, **kwargs)
return rect_on_view(self._elem)
def is_writable(self):
"""Check whether an element is writable."""
@@ -256,6 +254,7 @@ class WebElementWrapper(collections.abc.MutableMapping):
Return:
True if we should switch to insert mode, False otherwise.
"""
# pylint: disable=too-many-return-statements
self._check_vanished()
roles = ('combobox', 'textbox')
log.misc.debug("Checking if element is editable: {}".format(
@@ -363,80 +362,29 @@ def focus_elem(frame):
return WebElementWrapper(elem)
def rect_on_view(elem, *, elem_geometry=None, adjust_zoom=True, no_js=False):
def rect_on_view(elem, elem_geometry=None):
"""Get the geometry of the element relative to the webview.
We need this as a standalone function (as opposed to a WebElementWrapper
method) because we want to run is_visible before wrapping when hinting for
performance reasons.
Uses the getClientRects() JavaScript method to obtain the collection of
rectangles containing the element and returns the first rectangle which is
large enough (larger than 1px times 1px). If all rectangles returned by
getClientRects() are too small, falls back to elem.rect_on_view().
Skipping of small rectangles is due to <a> elements containing other
elements with "display:block" style, see
https://github.com/The-Compiler/qutebrowser/issues/1298
Args:
elem: The QWebElement to get the rect for.
elem_geometry: The geometry of the element, or None.
Calling QWebElement::geometry is rather expensive so we
want to avoid doing it twice.
adjust_zoom: Whether to adjust the element position based on the
current zoom level.
no_js: Fall back to the Python implementation
"""
if elem.isNull():
raise IsNullError("Got called on a null element!")
# First try getting the element rect via JS, as that's usually more
# accurate
if elem_geometry is None and not no_js:
rects = elem.evaluateJavaScript("this.getClientRects()")
text = utils.compact_text(elem.toOuterXml(), 500)
log.hints.vdebug("Client rectangles of element '{}': {}".format(text,
rects))
for i in range(int(rects.get("length", 0))):
rect = rects[str(i)]
width = rect.get("width", 0)
height = rect.get("height", 0)
if width > 1 and height > 1:
# fix coordinates according to zoom level
zoom = elem.webFrame().zoomFactor()
if not config.get('ui', 'zoom-text-only') and adjust_zoom:
rect["left"] *= zoom
rect["top"] *= zoom
width *= zoom
height *= zoom
rect = QRect(rect["left"], rect["top"], width, height)
frame = elem.webFrame()
while frame is not None:
# Translate to parent frames' position
# (scroll position is taken care of inside getClientRects)
rect.translate(frame.geometry().topLeft())
frame = frame.parentFrame()
return rect
# No suitable rects found via JS, try via the QWebElement API
if elem_geometry is None:
geometry = elem.geometry()
else:
geometry = elem_geometry
elem_geometry = elem.geometry()
frame = elem.webFrame()
rect = QRect(geometry)
rect = QRect(elem_geometry)
while frame is not None:
rect.translate(frame.geometry().topLeft())
rect.translate(frame.scrollPosition() * -1)
frame = frame.parentFrame()
# We deliberately always adjust the zoom here, even with adjust_zoom=False
if elem_geometry is None:
zoom = elem.webFrame().zoomFactor()
if not config.get('ui', 'zoom-text-only'):
rect.moveTo(rect.left() / zoom, rect.top() / zoom)
rect.setWidth(rect.width() / zoom)
rect.setHeight(rect.height() / zoom)
return rect
@@ -473,7 +421,8 @@ def is_visible(elem, mainframe):
else:
# We got an invalid rectangle (width/height 0/0 probably), but this
# can still be a valid link.
visible_on_screen = mainframe_geometry.contains(elem_rect.topLeft())
visible_on_screen = mainframe_geometry.contains(
elem_rect.topLeft())
# Then check if it's visible in its frame if it's not in the main
# frame.
elem_frame = elem.webFrame()

View File

@@ -33,7 +33,7 @@ from qutebrowser.config import config
from qutebrowser.browser import http, tabhistory, pdfjs
from qutebrowser.browser.network import networkmanager
from qutebrowser.utils import (message, usertypes, log, jinja, qtutils, utils,
objreg, debug, urlutils)
objreg, debug)
class BrowserPage(QWebPage):
@@ -570,8 +570,9 @@ class BrowserPage(QWebPage):
if typ != QWebPage.NavigationTypeLinkClicked:
return True
if not url.isValid():
msg = urlutils.get_errstring(url, "Invalid link clicked")
message.error(self._win_id, msg)
message.error(self._win_id, "Invalid link {} clicked!".format(
urlstr))
log.webview.debug(url.errorString())
self.open_target = usertypes.ClickTarget.normal
return False
tabbed_browser = objreg.get('tabbed-browser', scope='window',

View File

@@ -145,19 +145,6 @@ class WebView(QWebView):
self.loadProgress.connect(lambda p: setattr(self, 'progress', p))
objreg.get('config').changed.connect(self.on_config_changed)
@pyqtSlot()
def on_initial_layout_completed(self):
"""Add url to history now that we have displayed something."""
history = objreg.get('web-history')
no_formatting = QUrl.UrlFormattingOption(0)
orig_url = self.page().mainFrame().requestedUrl()
if (orig_url.isValid() and
not orig_url.matches(self.cur_url, no_formatting)):
# If the url of the page is different than the url of the link
# originally clicked, save them both.
history.add_url(orig_url, self.title(), redirect=True)
history.add_url(self.cur_url, self.title())
def _init_page(self):
"""Initialize the QWebPage used by this view."""
page = webpage.BrowserPage(self.win_id, self.tab_id, self)
@@ -165,8 +152,6 @@ class WebView(QWebView):
page.linkHovered.connect(self.linkHovered)
page.mainFrame().loadStarted.connect(self.on_load_started)
page.mainFrame().loadFinished.connect(self.on_load_finished)
page.mainFrame().initialLayoutCompleted.connect(
self.on_initial_layout_completed)
page.statusBarMessage.connect(
lambda msg: setattr(self, 'statusbar_message', msg))
page.networkAccessManager().sslErrors.connect(
@@ -228,7 +213,7 @@ class WebView(QWebView):
"""Initialize the _zoom neighborlist."""
levels = config.get('ui', 'zoom-levels')
self._zoom = usertypes.NeighborList(
levels, mode=usertypes.NeighborList.Modes.edge)
levels, mode=usertypes.NeighborList.Modes.block)
self._zoom.fuzzyval = config.get('ui', 'default-zoom')
def _mousepress_backforward(self, e):
@@ -372,8 +357,7 @@ class WebView(QWebView):
"""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))
log.webview.error("Got non-QWebFrame in add_js_bridge")
return
if frame.url().scheme() == 'qute':

View File

@@ -19,6 +19,7 @@
"""argparse.ArgumentParser subclass to parse qutebrowser commands."""
import argparse
from PyQt5.QtCore import QUrl
@@ -83,81 +84,43 @@ class ArgumentParser(argparse.ArgumentParser):
raise ArgumentParserError(msg.capitalize())
def arg_name(name):
"""Get the name an argument should have based on its Python name."""
return name.rstrip('_').replace('_', '-')
def enum_getter(enum):
"""Function factory to get an enum getter."""
def _get_enum_item(key):
"""Helper function to get an enum item.
def _check_choices(param, value, choices):
if value not in choices:
expected_values = ', '.join(arg_name(val) for val in choices)
raise cmdexc.ArgumentTypeError("{}: Invalid value {} - expected "
"one of: {}".format(
param.name, value, expected_values))
def type_conv(param, typ, value, *, str_choices=None):
"""Convert a value based on a type.
Args:
param: The argparse.Parameter we're checking
types: The allowed type
value: The value to convert
str_choices: The allowed choices if the type ends up being a string
Return:
The converted value
"""
if isinstance(typ, str):
raise TypeError("{}: Legacy string type!".format(param.name))
if value is param.default:
return value
assert isinstance(value, str), repr(value)
if utils.is_enum(typ):
_check_choices(param, value, [arg_name(e.name) for e in typ])
return typ[value.replace('-', '_')]
elif typ is str:
if str_choices is not None:
_check_choices(param, value, str_choices)
return value
elif callable(typ):
# int, float, etc.
Passes through existing items unmodified.
"""
if isinstance(key, enum):
return key
try:
return typ(value)
except (TypeError, ValueError):
msg = '{}: Invalid {} value {}'.format(
param.name, typ.__name__, value)
raise cmdexc.ArgumentTypeError(msg)
else:
raise ValueError("{}: Unknown type {!r}!".format(param.name, typ))
return enum[key.replace('-', '_')]
except KeyError:
raise cmdexc.ArgumentTypeError("Invalid value {}.".format(key))
return _get_enum_item
def multitype_conv(param, types, value, *, str_choices=None):
"""Convert a value based on a choice of types.
def multitype_conv(types):
"""Function factory to get a type converter for a choice of types."""
def _convert(value):
"""Convert a value according to an iterable of possible arg types."""
for typ in set(types):
if isinstance(typ, str):
if value == typ:
return value
elif utils.is_enum(typ):
return enum_getter(typ)(value)
elif callable(typ):
# int, float, etc.
if isinstance(value, typ):
return value
try:
return typ(value)
except (TypeError, ValueError):
pass
else:
raise ValueError("Unknown type {!r}!".format(typ))
raise cmdexc.ArgumentTypeError('Invalid value {}.'.format(value))
Args:
param: The inspect.Parameter we're checking
types: The allowed types ("overloads")
value: The value to convert
str_choices: The allowed choices if the type ends up being a string
Return:
The converted value
"""
types = list(set(types))
if str in types:
# Make sure str is always the last type in the list, so e.g. '23' gets
# returned as 23 if we have typing.Union[str, int]
types.remove(str)
types.append(str)
for typ in types:
try:
return type_conv(param, typ, value, str_choices=str_choices)
except cmdexc.ArgumentTypeError:
pass
raise cmdexc.ArgumentTypeError('{}: Invalid value {}'.format(
param.name, value))
return _convert

View File

@@ -24,8 +24,6 @@ Module attributes:
aliases: A list of all aliases, needed for doc generation.
"""
import inspect
from qutebrowser.utils import qtutils, log
from qutebrowser.commands import command, cmdexc
@@ -166,35 +164,3 @@ class register: # pylint: disable=invalid-name
cmd_dict[name] = cmd
aliases += names[1:]
return func
class argument: # pylint: disable=invalid-name
"""Decorator to customize an argument for @cmdutils.register.
This could also be a function, but as a class (with a "wrong" name) it's
much cleaner to implement.
Attributes:
_argname: The name of the argument to handle.
_kwargs: Keyword arguments, valid ArgInfo members
"""
def __init__(self, argname, **kwargs):
self._argname = argname
self._kwargs = kwargs
def __call__(self, func):
funcname = func.__name__
if self._argname not in inspect.signature(func).parameters:
raise ValueError("{} has no argument {}!".format(funcname,
self._argname))
if not hasattr(func, 'qute_args'):
func.qute_args = {}
elif func.qute_args is None:
raise ValueError("@cmdutils.argument got called above (after) "
"@cmdutils.register for {}!".format(funcname))
func.qute_args[self._argname] = command.ArgInfo(**self._kwargs)
return func

View File

@@ -21,46 +21,17 @@
import inspect
import collections
import traceback
from PyQt5.QtWebKit import QWebSettings
from qutebrowser.commands import cmdexc, argparser
from qutebrowser.utils import (log, utils, message, docutils, objreg,
usertypes, typing)
from qutebrowser.utils import log, utils, message, docutils, objreg, usertypes
from qutebrowser.utils import debug as debug_utils
class ArgInfo:
"""Information about an argument."""
def __init__(self, win_id=False, count=False, flag=None, hide=False,
metavar=None, completion=None, choices=None):
if win_id and count:
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)
def arg_name(name):
"""Get the name an argument should have based on its Python name."""
return name.rstrip('_').replace('_', '-')
class Command:
@@ -78,21 +49,31 @@ class Command:
completion: Completions to use for arguments, as a list of strings.
debug: Whether this is a debugging command (only shown with --debug).
parser: The ArgumentParser to use to parse this command.
count_arg: The name of the count parameter, or None.
win_id_arg: The name of the win_id parameter, or None.
flags_with_args: A list of flags which take an argument.
no_cmd_split: If true, ';;' to split sub-commands is ignored.
_qute_args: The saved data from @cmdutils.argument
_type_conv: A mapping of conversion functions for arguments.
_needs_js: Whether the command needs javascript enabled
_modes: The modes the command can be executed in.
_not_modes: The modes the command can not be executed in.
_count: The count set for the command.
_instance: The object to bind 'self' to.
_scope: The scope to get _instance for in the object registry.
Class attributes:
AnnotationInfo: Named tuple for info from an annotation.
"""
AnnotationInfo = collections.namedtuple('AnnotationInfo',
['kwargs', 'type', 'flag', 'hide',
'metavar'])
def __init__(self, *, handler, name, instance=None, maxsplit=None,
hide=False, modes=None, not_modes=None, needs_js=False,
debug=False, ignore_args=False, deprecated=False,
no_cmd_split=False, star_args_optional=False, scope='global'):
hide=False, completion=None, modes=None, not_modes=None,
needs_js=False, debug=False, ignore_args=False,
deprecated=False, no_cmd_split=False, scope='global',
count=None, win_id=None):
# I really don't know how to solve this in a better way, I tried.
# pylint: disable=too-many-locals
if modes is not None and not_modes is not None:
@@ -108,21 +89,22 @@ class Command:
if scope != 'global' and instance is None:
raise ValueError("Setting scope without setting instance makes "
"no sense!")
self.name = name
self.maxsplit = maxsplit
self.hide = hide
self.deprecated = deprecated
self._instance = instance
self.completion = completion
self._modes = modes
self._not_modes = not_modes
self._scope = scope
self._needs_js = needs_js
self._star_args_optional = star_args_optional
self.debug = debug
self.ignore_args = ignore_args
self.handler = handler
self.no_cmd_split = no_cmd_split
self.count_arg = count
self.win_id_arg = win_id
self.docparser = docutils.DocstringParser(handler)
self.parser = argparser.ArgumentParser(
name, description=self.docparser.short_desc,
@@ -137,19 +119,11 @@ class Command:
self.pos_args = []
self.desc = None
self.flags_with_args = []
# This is checked by future @cmdutils.argument calls so they fail
# (as they'd be silently ignored otherwise)
self._qute_args = getattr(self.handler, 'qute_args', {})
self.handler.qute_args = None
args = self._inspect_func()
self.completion = []
for arg in args:
arg_completion = self.get_arg_info(arg).completion
if arg_completion is not None:
self.completion.append(arg_completion)
self._type_conv = {}
count = self._inspect_func()
if self.completion is not None and len(self.completion) > count:
raise ValueError("Got {} completions, but only {} "
"arguments!".format(len(self.completion), count))
def _check_prerequisites(self, win_id):
"""Check if the command is permitted to run currently.
@@ -192,9 +166,21 @@ class Command:
raise TypeError("{}: functions with varkw arguments are not "
"supported!".format(self.name[0]))
def get_arg_info(self, param):
"""Get an ArgInfo tuple for the given inspect.Parameter."""
return self._qute_args.get(param.name, ArgInfo())
def _get_typeconv(self, param, typ):
"""Get a dict with a type conversion for the parameter.
Args:
param: The inspect.Parameter to handle.
typ: The type of the parameter.
"""
type_conv = {}
if utils.is_enum(typ):
type_conv[param.name] = argparser.enum_getter(typ)
elif isinstance(typ, tuple):
if param.default is not inspect.Parameter.empty:
typ = typ + (type(param.default),)
type_conv[param.name] = argparser.multitype_conv(typ)
return type_conv
def _inspect_special_param(self, param):
"""Check if the given parameter is a special one.
@@ -205,13 +191,12 @@ class Command:
Return:
True if the parameter is special, False otherwise.
"""
arg_info = self.get_arg_info(param)
if arg_info.count:
if param.name == self.count_arg:
if param.default is inspect.Parameter.empty:
raise TypeError("{}: handler has count parameter "
"without default!".format(self.name))
return True
elif arg_info.win_id:
elif param.name == self.win_id_arg:
return True
def _inspect_func(self):
@@ -225,40 +210,53 @@ class Command:
"""
signature = inspect.signature(self.handler)
doc = inspect.getdoc(self.handler)
arg_count = 0
if doc is not None:
self.desc = doc.splitlines()[0].strip()
else:
self.desc = ""
if (self.count_arg is not None and
self.count_arg not in signature.parameters):
raise ValueError("count parameter {} does not exist!".format(
self.count_arg))
if (self.win_id_arg is not None and
self.win_id_arg not in signature.parameters):
raise ValueError("win_id parameter {} does not exist!".format(
self.win_id_arg))
if not self.ignore_args:
for param in signature.parameters.values():
annotation_info = self._parse_annotation(param)
if param.name == 'self':
continue
if self._inspect_special_param(param):
continue
typ = self._get_type(param)
is_bool = typ is bool
kwargs = self._param_to_argparse_kwargs(param, is_bool)
args = self._param_to_argparse_args(param, is_bool)
arg_count += 1
typ = self._get_type(param, annotation_info)
kwargs = self._param_to_argparse_kwargs(param, annotation_info)
args = self._param_to_argparse_args(param, annotation_info)
self._type_conv.update(self._get_typeconv(param, typ))
callsig = debug_utils.format_call(
self.parser.add_argument, args, kwargs,
full=False)
log.commands.vdebug('Adding arg {} of type {} -> {}'.format(
param.name, typ, callsig))
self.parser.add_argument(*args, **kwargs)
return signature.parameters.values()
return arg_count
def _param_to_argparse_kwargs(self, param, is_bool):
def _param_to_argparse_kwargs(self, param, annotation_info):
"""Get argparse keyword arguments for a parameter.
Args:
param: The inspect.Parameter object to get the args for.
is_bool: Whether the parameter is a boolean.
annotation_info: An AnnotationInfo tuple for the parameter.
Return:
A kwargs dict.
"""
kwargs = {}
typ = self._get_type(param, annotation_info)
try:
kwargs['help'] = self.docparser.arg_descs[param.name]
@@ -267,69 +265,92 @@ class Command:
kwargs['dest'] = param.name
arg_info = self.get_arg_info(param)
if is_bool:
if isinstance(typ, tuple):
kwargs['metavar'] = annotation_info.metavar or param.name
elif utils.is_enum(typ):
kwargs['choices'] = [arg_name(e.name) for e in typ]
kwargs['metavar'] = annotation_info.metavar or param.name
elif typ is bool:
kwargs['action'] = 'store_true'
else:
if arg_info.metavar is not None:
kwargs['metavar'] = arg_info.metavar
else:
kwargs['metavar'] = argparser.arg_name(param.name)
elif typ is not None:
kwargs['type'] = typ
if param.kind == inspect.Parameter.VAR_POSITIONAL:
kwargs['nargs'] = '*' if self._star_args_optional else '+'
kwargs['nargs'] = '+'
elif param.kind == inspect.Parameter.KEYWORD_ONLY:
kwargs['default'] = param.default
elif not is_bool and param.default is not inspect.Parameter.empty:
elif typ is not bool and param.default is not inspect.Parameter.empty:
kwargs['default'] = param.default
kwargs['nargs'] = '?'
kwargs.update(annotation_info.kwargs)
return kwargs
def _param_to_argparse_args(self, param, is_bool):
def _param_to_argparse_args(self, param, annotation_info):
"""Get argparse positional arguments for a parameter.
Args:
param: The inspect.Parameter object to get the args for.
is_bool: Whether the parameter is a boolean.
annotation_info: An AnnotationInfo tuple for the parameter.
Return:
A list of args.
"""
args = []
name = argparser.arg_name(param.name)
arg_info = self.get_arg_info(param)
if arg_info.flag is not None:
shortname = arg_info.flag
else:
shortname = name[0]
name = arg_name(param.name)
shortname = annotation_info.flag or name[0]
if len(shortname) != 1:
raise ValueError("Flag '{}' of parameter {} (command {}) must be "
"exactly 1 char!".format(shortname, name,
self.name))
if is_bool or param.kind == inspect.Parameter.KEYWORD_ONLY:
typ = self._get_type(param, annotation_info)
if typ is bool or param.kind == inspect.Parameter.KEYWORD_ONLY:
long_flag = '--{}'.format(name)
short_flag = '-{}'.format(shortname)
args.append(long_flag)
args.append(short_flag)
self.opt_args[param.name] = long_flag, short_flag
if not is_bool:
if typ is not bool:
self.flags_with_args += [short_flag, long_flag]
else:
if not arg_info.hide:
if not annotation_info.hide:
self.pos_args.append((param.name, name))
return args
def _get_type(self, param):
def _parse_annotation(self, param):
"""Get argparse arguments and type from a parameter annotation.
Args:
param: A inspect.Parameter instance.
Return:
An AnnotationInfo namedtuple.
kwargs: A dict of keyword args to add to the
argparse.ArgumentParser.add_argument call.
typ: The type to use for this argument.
flag: The short name/flag if overridden.
name: The long name if overridden.
"""
info = {'kwargs': {}, 'type': None, 'flag': None, 'hide': False,
'metavar': None}
if param.annotation is not inspect.Parameter.empty:
log.commands.vdebug("Parsing annotation {}".format(
param.annotation))
for field in ('type', 'flag', 'name', 'hide', 'metavar'):
if field in param.annotation:
info[field] = param.annotation[field]
if 'nargs' in param.annotation:
info['kwargs'] = {'nargs': param.annotation['nargs']}
return self.AnnotationInfo(**info)
def _get_type(self, param, annotation_info):
"""Get the type of an argument from its default value or annotation.
Args:
param: The inspect.Parameter to look at.
annotation_info: An AnnotationInfo tuple which overrides the type.
"""
if param.annotation is not inspect.Parameter.empty:
return param.annotation
if annotation_info.type is not None:
return annotation_info.type
elif param.default is None or param.default is inspect.Parameter.empty:
return None
else:
@@ -397,29 +418,12 @@ class Command:
def _get_param_value(self, param):
"""Get the converted value for an inspect.Parameter."""
value = getattr(self.namespace, param.name)
typ = self._get_type(param)
if isinstance(typ, tuple):
raise TypeError("{}: Legacy tuple type annotation!".format(
self.name))
elif issubclass(typ, typing.Union):
# this is... slightly evil, I know
types = list(typ.__union_params__)
if param.default is not inspect.Parameter.empty:
types.append(type(param.default))
choices = self.get_arg_info(param).choices
value = argparser.multitype_conv(param, types, value,
str_choices=choices)
elif typ is str:
choices = self.get_arg_info(param).choices
value = argparser.type_conv(param, typ, value, str_choices=choices)
elif typ is None:
pass
elif typ is bool: # no type conversion for flags
assert isinstance(value, bool)
else:
value = argparser.type_conv(param, typ, value)
if param.name in self._type_conv:
# We convert enum types after getting the values from
# argparse, because argparse's choices argument is
# processed after type conversation, which is not what we
# want.
value = self._type_conv[param.name](value)
return value
def _get_call_args(self, win_id):
@@ -442,17 +446,15 @@ class Command:
return args, kwargs
for i, param in enumerate(signature.parameters.values()):
arg_info = self.get_arg_info(param)
if i == 0 and self._instance is not None:
# Special case for 'self'.
self._get_self_arg(win_id, param, args)
continue
elif arg_info.count:
elif param.name == self.count_arg:
# Special case for count parameter.
self._get_count_arg(param, args, kwargs)
continue
# elif arg_info.win_id:
elif arg_info.win_id:
elif param.name == self.win_id_arg:
# Special case for win_id parameter.
self._get_win_id_arg(win_id, param, args, kwargs)
continue
@@ -489,8 +491,7 @@ class Command:
try:
self.namespace = self.parser.parse_args(args)
except argparser.ArgumentParserError as e:
message.error(win_id, '{}: {}'.format(self.name, e),
stack=traceback.format_exc())
message.error(win_id, '{}: {}'.format(self.name, e))
return
except argparser.ArgumentParserExit as e:
log.commands.debug("argparser exited with status {}: {}".format(

View File

@@ -20,7 +20,6 @@
"""Module containing command managers (SearchRunner and CommandRunner)."""
import collections
import traceback
from PyQt5.QtCore import pyqtSlot, QUrl, QObject
@@ -34,33 +33,24 @@ ParseResult = collections.namedtuple('ParseResult', ['cmd', 'args', 'cmdline',
'count'])
def _current_url(tabbed_browser):
"""Convenience method to get the current url."""
try:
return tabbed_browser.current_url()
except qtutils.QtValueError as e:
msg = "Current URL is invalid"
if e.reason:
msg += " ({})".format(e.reason)
msg += "!"
raise cmdexc.CommandError(msg)
def replace_variables(win_id, arglist):
"""Utility function to replace variables like {url} in a list of args."""
args = []
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
if '{url}' in arglist:
url = _current_url(tabbed_browser).toString(QUrl.FullyEncoded |
QUrl.RemovePassword)
if '{url:pretty}' in arglist:
pretty_url = _current_url(tabbed_browser).toString(QUrl.RemovePassword)
try:
url = tabbed_browser.current_url().toString(QUrl.FullyEncoded |
QUrl.RemovePassword)
except qtutils.QtValueError as e:
msg = "Current URL is invalid"
if e.reason:
msg += " ({})".format(e.reason)
msg += "!"
raise cmdexc.CommandError(msg)
for arg in arglist:
if arg == '{url}':
args.append(url)
elif arg == '{url:pretty}':
args.append(pretty_url)
else:
args.append(arg)
return args
@@ -72,12 +62,10 @@ class CommandRunner(QObject):
Attributes:
_win_id: The window this CommandRunner is associated with.
_partial_match: Whether to allow partial command matches.
"""
def __init__(self, win_id, partial_match=False, parent=None):
def __init__(self, win_id, parent=None):
super().__init__(parent)
self._partial_match = partial_match
self._win_id = win_id
def _get_alias(self, text):
@@ -150,15 +138,6 @@ class CommandRunner(QObject):
count = None
return (count, cmdstr)
def _parse_fallback(self, text, count, keep):
"""Parse the given commandline without a valid command."""
if keep:
cmdstr, sep, argstr = text.partition(' ')
cmdline = [cmdstr, sep] + argstr.split()
else:
cmdline = text.split()
return ParseResult(cmd=None, args=None, cmdline=cmdline, count=count)
def parse(self, text, *, aliases=True, fallback=False, keep=False):
"""Split the commandline text into command and arguments.
@@ -177,52 +156,36 @@ class CommandRunner(QObject):
if not cmdstr and not fallback:
raise cmdexc.NoSuchCommandError("No command given")
if aliases:
new_cmd = self._get_alias(text)
if new_cmd is not None:
log.commands.debug("Re-parsing with '{}'.".format(new_cmd))
return self.parse(new_cmd, aliases=False, fallback=fallback,
keep=keep)
if self._partial_match:
cmdstr = self._completion_match(cmdstr)
try:
cmd = cmdutils.cmd_dict[cmdstr]
except KeyError:
if not fallback:
raise cmdexc.NoSuchCommandError(
'{}: no such command'.format(cmdstr))
return self._parse_fallback(text, count, keep)
args = self._split_args(cmd, argstr, keep)
if keep and args:
cmdline = [cmdstr, sep + args[0]] + args[1:]
elif keep:
cmdline = [cmdstr, sep]
if fallback:
cmd = None
args = None
if keep:
cmdstr, sep, argstr = text.partition(' ')
cmdline = [cmdstr, sep] + argstr.split()
else:
cmdline = text.split()
else:
raise cmdexc.NoSuchCommandError('{}: no such command'.format(
cmdstr))
else:
cmdline = [cmdstr] + args[:]
args = self._split_args(cmd, argstr, keep)
if keep and args:
cmdline = [cmdstr, sep + args[0]] + args[1:]
elif keep:
cmdline = [cmdstr, sep]
else:
cmdline = [cmdstr] + args[:]
return ParseResult(cmd=cmd, args=args, cmdline=cmdline, count=count)
def _completion_match(self, cmdstr):
"""Replace cmdstr with a matching completion if there's only one match.
Args:
cmdstr: The string representing the entered command so far
Return:
cmdstr modified to the matching completion or unmodified
"""
matches = []
for valid_command in cmdutils.cmd_dict.keys():
if valid_command.find(cmdstr) == 0:
matches.append(valid_command)
if len(matches) == 1:
cmdstr = matches[0]
return cmdstr
def _split_args(self, cmd, argstr, keep):
"""Split the arguments from an arg string.
@@ -292,17 +255,15 @@ class CommandRunner(QObject):
try:
self.run(text, count)
except (cmdexc.CommandMetaError, cmdexc.CommandError) as e:
message.error(self._win_id, e, immediately=True,
stack=traceback.format_exc())
message.error(self._win_id, e, immediately=True)
@pyqtSlot(str, int)
def run_safely_init(self, text, count=None):
"""Run a command and display exceptions in the statusbar.
Contrary to run_safely, error messages are queued so this is more
suitable to use while initializing.
"""
suitable to use while initializing."""
try:
self.run(text, count)
except (cmdexc.CommandMetaError, cmdexc.CommandError) as e:
message.error(self._win_id, e, stack=traceback.format_exc())
message.error(self._win_id, e)

View File

@@ -71,8 +71,6 @@ class _QtFIFOReader(QObject):
def cleanup(self):
"""Clean up so the FIFO can be closed."""
self._notifier.setEnabled(False)
for line in self._fifo:
self.got_line.emit(line.rstrip('\r\n'))
self._fifo.close()
@@ -84,7 +82,6 @@ class _BaseUserscriptRunner(QObject):
_filepath: The path of the file/FIFO which is being read.
_proc: The GUIProcess which is being executed.
_win_id: The window ID this runner is associated with.
_cleaned_up: Whether temporary files were cleaned up.
Signals:
got_cmd: Emitted when a new command arrived and should be executed.
@@ -96,7 +93,6 @@ class _BaseUserscriptRunner(QObject):
def __init__(self, win_id, parent=None):
super().__init__(parent)
self._cleaned_up = False
self._win_id = win_id
self._filepath = None
self._proc = None
@@ -118,14 +114,10 @@ class _BaseUserscriptRunner(QObject):
additional_env=self._env,
verbose=verbose, parent=self)
self._proc.finished.connect(self.on_proc_finished)
self._proc.error.connect(self.on_proc_error)
self._proc.start(cmd, args)
def _cleanup(self):
"""Clean up temporary files."""
if self._cleaned_up:
return
self._cleaned_up = True
tempfiles = [self._filepath]
if 'QUTE_HTML' in self._env:
tempfiles.append(self._env['QUTE_HTML'])
@@ -158,7 +150,6 @@ class _BaseUserscriptRunner(QObject):
"""
raise NotImplementedError
@pyqtSlot()
def on_proc_finished(self):
"""Called when the process has finished.
@@ -166,14 +157,6 @@ class _BaseUserscriptRunner(QObject):
"""
raise NotImplementedError
@pyqtSlot()
def on_proc_error(self):
"""Called when the process encountered an error.
Needs to be overridden by subclasses.
"""
raise NotImplementedError
class _POSIXUserscriptRunner(_BaseUserscriptRunner):
@@ -201,8 +184,8 @@ class _POSIXUserscriptRunner(_BaseUserscriptRunner):
# pylint: disable=no-member,useless-suppression
os.mkfifo(self._filepath)
except OSError as e:
message.error(self._win_id,
"Error while creating FIFO: {}".format(e))
message.error(self._win_id, "Error while creating FIFO: {}".format(
e))
return
self._reader = _QtFIFOReader(self._filepath)
@@ -210,18 +193,12 @@ class _POSIXUserscriptRunner(_BaseUserscriptRunner):
self._run_process(cmd, *args, env=env, verbose=verbose)
@pyqtSlot()
def on_proc_finished(self):
self._cleanup()
"""Interrupt the reader when the process finished."""
self.finish()
@pyqtSlot()
def on_proc_error(self):
self._cleanup()
def _cleanup(self):
"""Clean up reader and temporary files."""
if self._cleaned_up:
return
def finish(self):
"""Quit the thread and clean up when the reader finished."""
log.procs.debug("Cleaning up")
self._reader.cleanup()
self._reader.deleteLater()
@@ -252,32 +229,23 @@ class _WindowsUserscriptRunner(_BaseUserscriptRunner):
def _cleanup(self):
"""Clean up temporary files after the userscript finished."""
if self._cleaned_up:
return
try:
with open(self._filepath, 'r', encoding='utf-8') as f:
for line in f:
self.got_cmd.emit(line.rstrip())
except OSError:
log.procs.exception("Failed to read command file!")
try:
os.close(self._oshandle)
except OSError:
log.procs.exception("Failed to close file handle!")
super()._cleanup()
self._oshandle = None
self.finished.emit()
@pyqtSlot()
def on_proc_error(self):
self._cleanup()
@pyqtSlot()
def on_proc_finished(self):
"""Read back the commands when the process finished."""
try:
with open(self._filepath, 'r', encoding='utf-8') as f:
for line in f:
self.got_cmd.emit(line.rstrip())
except OSError:
log.procs.exception("Failed to read command file!")
self._cleanup()
self.finished.emit()
def run(self, cmd, *args, env=None, verbose=False):
try:
@@ -367,11 +335,11 @@ def run(cmd, *args, win_id, env, verbose=False):
"""
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
commandrunner = runners.CommandRunner(win_id, parent=tabbed_browser)
commandrunner = runners.CommandRunner(win_id, tabbed_browser)
runner = UserscriptRunner(win_id, tabbed_browser)
runner.got_cmd.connect(
lambda cmd:
log.commands.debug("Got userscript command: {}".format(cmd)))
lambda cmd: log.commands.debug("Got userscript command: {}".format(
cmd)))
runner.got_cmd.connect(commandrunner.run_safely)
user_agent = config.get('network', 'user-agent')
if user_agent is not None:

View File

@@ -204,8 +204,6 @@ class Completer(QObject):
parts, cursor_part = self._filter_cmdline_parts(parts, cursor_part)
log.completion.debug("After filtering flags: parts {}, cursor_part "
"{}".format(parts, cursor_part))
if not parts:
return None
if cursor_part == 0:
# '|' or 'set|'
model = instances.get(usertypes.Completion.command)

View File

@@ -183,8 +183,6 @@ class CompletionItemDelegate(QStyledItemDelegate):
text_option.setAlignment(QStyle.visualAlignment(
self._opt.direction, self._opt.displayAlignment))
if self._doc is not None:
self._doc.deleteLater()
self._doc = QTextDocument(self)
self._doc.setDefaultFont(self._opt.font)
self._doc.setDefaultTextOption(text_option)

View File

@@ -201,7 +201,8 @@ class CompletionView(QTreeView):
idx = self._next_idx(prev)
qtutils.ensure_valid(idx)
self.selectionModel().setCurrentIndex(
idx, QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows)
idx, QItemSelectionModel.ClearAndSelect |
QItemSelectionModel.Rows)
def set_model(self, model):
"""Switch completion to a new model.

View File

@@ -29,8 +29,7 @@ import functools
from PyQt5.QtCore import pyqtSlot
from qutebrowser.completion.models import (miscmodels, urlmodel, configmodel,
base)
from qutebrowser.completion.models import miscmodels, urlmodel, configmodel
from qutebrowser.utils import objreg, usertypes, log, debug
from qutebrowser.config import configdata
@@ -120,13 +119,6 @@ def init_session_completion():
_instances[usertypes.Completion.sessions] = model
def _init_empty_completion():
"""Initialize empty completion model."""
log.completion.debug("Initializing empty completion.")
if usertypes.Completion.empty not in _instances:
_instances[usertypes.Completion.empty] = base.BaseCompletionModel()
INITIALIZERS = {
usertypes.Completion.command: _init_command_completion,
usertypes.Completion.helptopic: _init_helptopic_completion,
@@ -138,7 +130,6 @@ INITIALIZERS = {
usertypes.Completion.quickmark_by_name: init_quickmark_completions,
usertypes.Completion.bookmark_by_url: init_bookmark_completions,
usertypes.Completion.sessions: init_session_completion,
usertypes.Completion.empty: _init_empty_completion,
}
@@ -186,7 +177,3 @@ def init():
history = objreg.get('web-history')
history.async_read_done.connect(
functools.partial(update, [usertypes.Completion.url]))
keyconf = objreg.get('key-config')
keyconf.changed.connect(
functools.partial(update, [usertypes.Completion.command]))

View File

@@ -19,12 +19,11 @@
"""Misc. CompletionModels."""
from collections import defaultdict
from PyQt5.QtCore import Qt, QTimer, pyqtSlot
from qutebrowser.browser import webview
from qutebrowser.config import config, configdata
from qutebrowser.utils import objreg, log, qtutils, utils
from qutebrowser.utils import objreg, log
from qutebrowser.commands import cmdutils
from qutebrowser.completion.models import base
@@ -36,8 +35,6 @@ class CommandCompletionModel(base.BaseCompletionModel):
# https://github.com/The-Compiler/qutebrowser/issues/545
# pylint: disable=abstract-method
COLUMN_WIDTHS = (20, 60, 20)
def __init__(self, parent=None):
super().__init__(parent)
assert cmdutils.cmd_dict
@@ -51,18 +48,8 @@ class CommandCompletionModel(base.BaseCompletionModel):
for name, cmd in config.section('aliases').items():
cmdlist.append((name, "Alias for '{}'".format(cmd)))
cat = self.new_category("Commands")
# map each command to its bound keys and show these in the misc column
keyconf = objreg.get('key-config')
cmd_to_keys = defaultdict(list)
for key, cmd in keyconf.get_bindings_for('normal').items():
# put special bindings last
if utils.is_special_key(key):
cmd_to_keys[cmd].append(key)
else:
cmd_to_keys[cmd].insert(0, key)
for (name, desc) in sorted(cmdlist):
self.new_item(cat, name, desc, ', '.join(cmd_to_keys[name]))
self.new_item(cat, name, desc)
class HelpCompletionModel(base.BaseCompletionModel):
@@ -160,13 +147,12 @@ class TabCompletionModel(base.BaseCompletionModel):
"""A model to complete on open tabs across all windows.
Used for switching the buffer command.
"""
Used for switching the buffer command."""
# https://github.com/The-Compiler/qutebrowser/issues/545
# pylint: disable=abstract-method
IDX_COLUMN = 0
#IDX_COLUMN = 0
URL_COLUMN = 1
TEXT_COLUMN = 2
@@ -180,7 +166,7 @@ class TabCompletionModel(base.BaseCompletionModel):
for win_id in objreg.window_registry:
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
window=win_id)
for i in range(tabbed_browser.count()):
tab = tabbed_browser.widget(i)
tab.url_text_changed.connect(self.rebuild)
@@ -214,56 +200,15 @@ class TabCompletionModel(base.BaseCompletionModel):
make sure we handled background loads too ... but iterating over a
few/few dozen/few hundred tabs doesn't take very long at all.
"""
window_count = 0
self.removeRows(0, self.rowCount())
for win_id in objreg.window_registry:
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
if not tabbed_browser.shutting_down:
window_count += 1
if window_count < self.rowCount():
self.removeRows(window_count, self.rowCount() - window_count)
for i, win_id in enumerate(objreg.window_registry):
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
if tabbed_browser.shutting_down:
continue
if i >= self.rowCount():
c = self.new_category("{}".format(win_id))
else:
c = self.item(i, 0)
c.setData("{}".format(win_id), Qt.DisplayRole)
if tabbed_browser.count() < c.rowCount():
c.removeRows(tabbed_browser.count(),
c.rowCount() - tabbed_browser.count())
for idx in range(tabbed_browser.count()):
tab = tabbed_browser.widget(idx)
if idx >= c.rowCount():
self.new_item(c, "{}/{}".format(win_id, idx + 1),
tab.url().toDisplayString(),
tabbed_browser.page_title(idx))
else:
c.child(idx, 0).setData("{}/{}".format(win_id, idx + 1),
Qt.DisplayRole)
c.child(idx, 1).setData(tab.url().toDisplayString(),
Qt.DisplayRole)
c.child(idx, 2).setData(tabbed_browser.page_title(idx),
Qt.DisplayRole)
def delete_cur_item(self, completion):
"""Delete the selected item.
Args:
completion: The Completion object to use.
"""
index = completion.currentIndex()
qtutils.ensure_valid(index)
category = index.parent()
qtutils.ensure_valid(category)
index = category.child(index.row(), self.IDX_COLUMN)
win_id, tab_index = index.data().split('/')
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=int(win_id))
tabbed_browser.on_tab_close_requested(int(tab_index) - 1)
c = self.new_category("{}".format(win_id))
for i in range(tabbed_browser.count()):
tab = tabbed_browser.widget(i)
self.new_item(c, "{}/{}".format(win_id, i+1),
tab.url().toDisplayString(),
tabbed_browser.page_title(i))

View File

@@ -32,8 +32,7 @@ class UrlCompletionModel(base.BaseCompletionModel):
"""A model which combines bookmarks, quickmarks and web history URLs.
Used for the `open` command.
"""
Used for the `open` command."""
# https://github.com/The-Compiler/qutebrowser/issues/545
# pylint: disable=abstract-method
@@ -74,10 +73,9 @@ class UrlCompletionModel(base.BaseCompletionModel):
self._max_history = config.get('completion', 'web-history-max-items')
history = utils.newest_slice(self._history, self._max_history)
for entry in history:
if not entry.redirect:
self._add_history_entry(entry)
self._history.add_completion_item.connect(self.on_history_item_added)
self._history.cleared.connect(self.on_history_cleared)
self._add_history_entry(entry)
self._history.add_completion_item.connect(
self.on_history_item_added)
objreg.get('config').changed.connect(self.reformat_timestamps)
@@ -101,8 +99,7 @@ class UrlCompletionModel(base.BaseCompletionModel):
def _add_history_entry(self, entry):
"""Add a new history entry to the completion."""
self.new_item(self._history_cat, entry.url.toDisplayString(),
entry.title,
self.new_item(self._history_cat, entry.url.toDisplayString(), "",
self._fmt_atime(entry.atime), sort=int(entry.atime),
userdata=entry.url)
@@ -125,20 +122,14 @@ class UrlCompletionModel(base.BaseCompletionModel):
for i in range(self._history_cat.rowCount()):
url_item = self._history_cat.child(i, self.URL_COLUMN)
atime_item = self._history_cat.child(i, self.TIME_COLUMN)
title_item = self._history_cat.child(i, self.TEXT_COLUMN)
url = url_item.data(base.Role.userdata)
if url == entry.url:
atime_item.setText(self._fmt_atime(entry.atime))
title_item.setText(entry.title)
url_item.setData(int(entry.atime), base.Role.sort)
break
else:
self._add_history_entry(entry)
@pyqtSlot()
def on_history_cleared(self):
self._history_cat.removeRows(0, self._history_cat.rowCount())
def _remove_item(self, data, category, column):
"""Helper function for on_quickmark_removed and on_bookmark_removed.

View File

@@ -341,7 +341,6 @@ class ConfigManager(QObject):
('colors', 'tab.indicator.system'): 'tabs.indicator.system',
('completion', 'history-length'): 'cmd-history-max-items',
('colors', 'downloads.fg'): 'downloads.fg.start',
('ui', 'show-keyhints'): 'keyhint-blacklist',
}
DELETED_OPTIONS = [
('colors', 'tab.separator'),
@@ -361,8 +360,6 @@ class ConfigManager(QObject):
_get_value_transformer({'false': '-1', 'true': '1000'}),
('general', 'log-javascript-console'):
_get_value_transformer({'false': 'none', 'true': 'debug'}),
('ui', 'keyhint-blacklist'):
_get_value_transformer({'false': '*', 'true': ''}),
}
changed = pyqtSignal(str, str)
@@ -689,11 +686,9 @@ class ConfigManager(QObject):
raise cmdexc.CommandError("set: {} - {}".format(
e.__class__.__name__, e))
@cmdutils.register(name='set', instance='config')
@cmdutils.argument('section_', completion=Completion.section)
@cmdutils.argument('option', completion=Completion.option)
@cmdutils.argument('value', completion=Completion.value)
@cmdutils.argument('win_id', win_id=True)
@cmdutils.register(name='set', instance='config', win_id='win_id',
completion=[Completion.section, Completion.option,
Completion.value])
def set_command(self, win_id, section_=None, option=None, value=None,
temp=False, print_=False):
"""Set an option.
@@ -734,7 +729,7 @@ class ConfigManager(QObject):
val = self.get(section_, option)
layer = 'temp' if temp else 'conf'
if isinstance(val, bool):
self.set(layer, section_, option, str(not val).lower())
self.set(layer, section_, option, str(not val))
else:
raise cmdexc.CommandError(
"set: Attempted inversion of non-boolean value.")

View File

@@ -355,12 +355,6 @@ def data(readonly=False):
"Hide the window decoration when using wayland "
"(requires restart)"),
('keyhint-blacklist',
SettingValue(typ.List(none_ok=True), ''),
"Keychains that shouldn't be shown in the keyhint dialog\n\n"
"Globs are supported, so ';*' will blacklist all keychains"
"starting with ';'. Use '*' to disable keyhints"),
readonly=readonly
)),
@@ -407,10 +401,6 @@ def data(readonly=False):
SettingValue(typ.Bool(), 'true'),
"Whether to try to pre-fetch DNS entries to speed up browsing."),
('custom-headers',
SettingValue(typ.HeaderDict(none_ok=True), ''),
"Set custom headers for qutebrowser HTTP requests."),
readonly=readonly
)),
@@ -476,15 +466,11 @@ def data(readonly=False):
('input', sect.KeyValue(
('timeout',
SettingValue(typ.Int(minval=0, maxval=MAXVALS['int']), '500'),
"Timeout for ambiguous key bindings.\n\n"
"If the current input forms both a complete match and a partial "
"match, the complete match will be executed after this time."),
"Timeout for ambiguous key bindings."),
('partial-timeout',
SettingValue(typ.Int(minval=0, maxval=MAXVALS['int']), '5000'),
"Timeout for partially typed key bindings.\n\n"
"If the current input forms only partial matches, the keystring "
"will be cleared after this time."),
SettingValue(typ.Int(minval=0, maxval=MAXVALS['int']), '1000'),
"Timeout for partially typed key bindings."),
('insert-mode-on-plugins',
SettingValue(typ.Bool(), 'false'),
@@ -755,7 +741,7 @@ def data(readonly=False):
"are not affected by this setting."),
('webgl',
SettingValue(typ.Bool(), 'false'),
SettingValue(typ.Bool(), 'true'),
"Enables or disables WebGL."),
('css-regions',
@@ -898,7 +884,7 @@ def data(readonly=False):
('scatter',
SettingValue(typ.Bool(), 'true'),
"Whether to scatter hint key chains (like Vimium) or not (like "
"dwb). Ignored for number hints."),
"dwb)."),
('uppercase',
SettingValue(typ.Bool(), 'false'),
@@ -913,11 +899,6 @@ def data(readonly=False):
"Follow a hint immediately when the hint text is completely "
"matched."),
('auto-follow-timeout',
SettingValue(typ.Int(), '0'),
"A timeout to inhibit normal-mode key bindings after a successful"
"auto-follow."),
('next-regexes',
SettingValue(typ.RegexList(flags=re.IGNORECASE),
r'\bnext\b,\bmore\b,\bnewer\b,\b[>→≫]\b,\b(>>|»)\b,'
@@ -930,14 +911,6 @@ def data(readonly=False):
r'\b(<<|«)\b'),
"A comma-separated list of regexes to use for 'prev' links."),
('find-implementation',
SettingValue(typ.String(
valid_values=typ.ValidValues(
('javascript', "Better but slower"),
('python', "Slightly worse but faster"),
)), 'javascript'),
"Which implementation to use to find elements to hint."),
readonly=readonly
)),
@@ -1219,18 +1192,6 @@ def data(readonly=False):
"Background color for webpages if unset (or empty to use the "
"theme's color)"),
('keyhint.fg',
SettingValue(typ.QssColor(), '#FFFFFF'),
"Text color for the keyhint widget."),
('keyhint.fg.suffix',
SettingValue(typ.CssColor(), '#FFFF00'),
"Highlight color for keys to complete the current keychain"),
('keyhint.bg',
SettingValue(typ.QssColor(), 'rgba(0, 0, 0, 80%)'),
"Background color of the keyhint widget."),
readonly=readonly
)),
@@ -1312,10 +1273,6 @@ def data(readonly=False):
typ.Int(none_ok=True, minval=1, maxval=MAXVALS['int']), ''),
"The default font size for fixed-pitch text."),
('keyhint',
SettingValue(typ.Font(), DEFAULT_FONT_SIZE + ' ${_monospace}'),
"Font used in the keyhint widget."),
readonly=readonly
)),
])
@@ -1425,15 +1382,14 @@ KEY_DATA = collections.OrderedDict([
('normal', collections.OrderedDict([
('clear-keychain ;; search', ['<Escape>']),
('set-cmd-text -s :open', ['o']),
('set-cmd-text :open {url:pretty}', ['go']),
('set-cmd-text :open {url}', ['go']),
('set-cmd-text -s :open -t', ['O']),
('set-cmd-text :open -t {url:pretty}', ['gO']),
('set-cmd-text :open -t {url}', ['gO']),
('set-cmd-text -s :open -b', ['xo']),
('set-cmd-text :open -b {url:pretty}', ['xO']),
('set-cmd-text :open -b {url}', ['xO']),
('set-cmd-text -s :open -w', ['wo']),
('set-cmd-text :open -w {url:pretty}', ['wO']),
('set-cmd-text :open -w {url}', ['wO']),
('open -t', ['ga', '<Ctrl-T>']),
('open -w', ['<Ctrl-N>']),
('tab-close', ['d', '<Ctrl-W>']),
('tab-close -o', ['D']),
('tab-only', ['co']),
@@ -1441,11 +1397,11 @@ KEY_DATA = collections.OrderedDict([
('tab-move', ['gm']),
('tab-move -', ['gl']),
('tab-move +', ['gr']),
('tab-focus', ['J', '<Ctrl-PgDown>']),
('tab-prev', ['K', '<Ctrl-PgUp>']),
('tab-focus', ['J']),
('tab-prev', ['K']),
('tab-clone', ['gC']),
('reload', ['r', '<F5>']),
('reload -f', ['R', '<Ctrl-F5>']),
('reload', ['r']),
('reload -f', ['R']),
('back', ['H']),
('back -t', ['th']),
('back -w', ['wh']),
@@ -1481,16 +1437,12 @@ KEY_DATA = collections.OrderedDict([
('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 -t', ['yt']),
('yank -ts', ['yT']),
('yank -d', ['yd']),
('yank -ds', ['yD']),
('yank -p', ['yp']),
('yank -ps', ['yP']),
('paste', ['pp']),
('paste -s', ['pP']),
('paste -t', ['Pp']),

View File

@@ -20,7 +20,6 @@
"""Setting options used for qutebrowser."""
import re
import json
import shlex
import base64
import codecs
@@ -173,8 +172,8 @@ class BaseType:
if self.valid_values is not None:
if value not in self.valid_values:
raise configexc.ValidationError(
value,
"valid values: {}".format(', '.join(self.valid_values)))
value, "valid values: {}".format(', '.join(
self.valid_values)))
else:
raise NotImplementedError("{} does not implement validate.".format(
self.__class__.__name__))
@@ -263,8 +262,8 @@ class String(BaseType):
if self.valid_values is not None:
if value not in self.valid_values:
raise configexc.ValidationError(
value,
"valid values: {}".format(', '.join(self.valid_values)))
value, "valid values: {}".format(', '.join(
self.valid_values)))
if self.forbidden is not None and any(c in value
for c in self.forbidden):
@@ -742,13 +741,11 @@ class QssColor(CssColor):
color_func_regexes: Valid function regexes.
"""
num = r'[0-9]{1,3}%?'
color_func_regexes = [
r'rgb\({num},\s*{num},\s*{num}\)'.format(num=num),
r'rgba\({num},\s*{num},\s*{num},\s*{num}\)'.format(num=num),
r'hsv\({num},\s*{num},\s*{num}\)'.format(num=num),
r'hsva\({num},\s*{num},\s*{num},\s*{num}\)'.format(num=num),
r'rgb\([0-9]{1,3}%?, [0-9]{1,3}%?, [0-9]{1,3}%?\)',
r'rgba\([0-9]{1,3}%?, [0-9]{1,3}%?, [0-9]{1,3}%?, [0-9]{1,3}%?\)',
r'hsv\([0-9]{1,3}%?, [0-9]{1,3}%?, [0-9]{1,3}%?\)',
r'hsva\([0-9]{1,3}%?, [0-9]{1,3}%?, [0-9]{1,3}%?, [0-9]{1,3}%?\)',
r'qlineargradient\(.*\)',
r'qradialgradient\(.*\)',
r'qconicalgradient\(.*\)',
@@ -1149,8 +1146,8 @@ class Proxy(BaseType):
return
url = QUrl(value)
if not url.isValid():
raise configexc.ValidationError(
value, "invalid url, {}".format(url.errorString()))
raise configexc.ValidationError(value, "invalid url, {}".format(
url.errorString()))
elif url.scheme() not in self.PROXY_TYPES:
raise configexc.ValidationError(value, "must be a proxy URL "
"(http://... or socks://...) or "
@@ -1162,8 +1159,6 @@ class Proxy(BaseType):
out.append((val, self.valid_values.descriptions[val]))
out.append(('http://', 'HTTP proxy URL'))
out.append(('socks://', 'SOCKS proxy URL'))
out.append(('socks://localhost:9050/', 'Tor via SOCKS'))
out.append(('http://localhost:8080/', 'Local HTTP proxy'))
return out
def transform(self, value):
@@ -1213,8 +1208,8 @@ class SearchEngineUrl(BaseType):
url = QUrl(value.replace('{}', 'foobar'))
if not url.isValid():
raise configexc.ValidationError(
value, "invalid url, {}".format(url.errorString()))
raise configexc.ValidationError(value, "invalid url, {}".format(
url.errorString()))
class FuzzyUrl(BaseType):
@@ -1426,61 +1421,6 @@ class UrlList(List):
"{}".format(val.errorString()))
class HeaderDict(BaseType):
"""A JSON-like dictionary for custom HTTP headers."""
def _validate_str(self, value, what):
"""Check if the given thing is an ascii-only string.
Raises ValidationError if not.
Args:
value: The value to check.
what: Either 'key' or 'value'.
"""
if not isinstance(value, str):
msg = "Expected string for {} {!r} but got {}".format(
what, value, type(value))
raise configexc.ValidationError(value, msg)
try:
value.encode('ascii')
except UnicodeEncodeError as e:
msg = "{} {!r} contains non-ascii characters: {}".format(
what.capitalize(), value, e)
raise configexc.ValidationError(value, msg)
def validate(self, value):
self._basic_validation(value)
if not value:
return
try:
json_val = json.loads(value)
except ValueError as e:
raise configexc.ValidationError(value, str(e))
if not isinstance(json_val, dict):
raise configexc.ValidationError(value, "Expected json dict, but "
"got {}".format(type(json_val)))
if not json_val:
if self.none_ok:
return
else:
raise configexc.ValidationError(value, "may not be empty!")
for key, val in json_val.items():
self._validate_str(key, 'key')
self._validate_str(val, 'value')
def transform(self, value):
if not value:
return None
val = json.loads(value)
return val or None
class SessionName(BaseType):
"""The name of a session."""

View File

@@ -27,7 +27,7 @@ from PyQt5.QtCore import pyqtSignal, QObject
from qutebrowser.config import configdata, textwrapper
from qutebrowser.commands import cmdutils, cmdexc
from qutebrowser.utils import log, utils, qtutils, message, usertypes
from qutebrowser.utils import log, utils, qtutils
class KeyConfigError(Exception):
@@ -151,34 +151,18 @@ class KeyConfigParser(QObject):
f.write(data)
@cmdutils.register(instance='key-config', maxsplit=1, no_cmd_split=True)
@cmdutils.argument('win_id', win_id=True)
@cmdutils.argument('key', completion=usertypes.Completion.empty)
@cmdutils.argument('command', completion=usertypes.Completion.command)
def bind(self, key, win_id, command=None, *, mode='normal', force=False):
def bind(self, key, command, *, mode=None, force=False):
"""Bind a key to a command.
Args:
key: The keychain or special key (inside `<...>`) to bind.
command: The command to execute, with optional args, or None to
print the current binding.
command: The command to execute, with optional args.
mode: A comma-separated list of modes to bind the key in
(default: `normal`).
force: Rebind the key if it is already bound.
"""
if utils.is_special_key(key):
# <Ctrl-t>, <ctrl-T>, and <ctrl-t> should be considered equivalent
key = key.lower()
if command is None:
cmd = self.get_bindings_for(mode).get(key, None)
if cmd is None:
message.info(win_id, "{} is unbound in {} mode".format(
key, mode))
else:
message.info(win_id, "{} is bound to '{}' in {} mode".format(
key, cmd, mode))
return
if mode is None:
mode = 'normal'
mode = self._normalize_sectname(mode)
for m in mode.split(','):
if m not in configdata.KEY_DATA:
@@ -199,7 +183,7 @@ class KeyConfigParser(QObject):
self._mark_config_dirty()
@cmdutils.register(instance='key-config')
def unbind(self, key, mode='normal'):
def unbind(self, key, mode=None):
"""Unbind a keychain.
Args:
@@ -207,10 +191,8 @@ class KeyConfigParser(QObject):
mode: A comma-separated list of modes to unbind the key in
(default: `normal`).
"""
if utils.is_special_key(key):
# <Ctrl-t>, <ctrl-T>, and <ctrl-t> should be considered equivalent
key = key.lower()
if mode is None:
mode = 'normal'
mode = self._normalize_sectname(mode)
for m in mode.split(','):
if m not in configdata.KEY_DATA:
@@ -380,9 +362,6 @@ class KeyConfigParser(QObject):
def _add_binding(self, sectname, keychain, command, *, force=False):
"""Add a new binding from keychain to command in section sectname."""
if utils.is_special_key(keychain):
# <Ctrl-t>, <ctrl-T>, and <ctrl-t> should be considered equivalent
keychain = keychain.lower()
log.keyboard.vdebug("Adding binding {} -> {} in mode {}.".format(
keychain, command, sectname))
if sectname not in self.keybindings:

View File

@@ -208,8 +208,8 @@ class ValueList(Section):
def dump_userconfig(self):
changed = []
mapping = collections.ChainMap(self.layers['temp'],
self.layers['conf'])
mapping = collections.ChainMap(
self.layers['temp'], self.layers['conf'])
for k, v in mapping.items():
try:
if v.value() != self.layers['default'][k].value():

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