Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
293e322905 | ||
|
|
354bd5d606 | ||
|
|
8e619fa74e | ||
|
|
5b944fb272 | ||
|
|
36d2fc4b92 | ||
|
|
26f4acb10a | ||
|
|
991277e9de | ||
|
|
133e959ecc | ||
|
|
be1630f7d0 | ||
|
|
c57bf8701e | ||
|
|
ea2ae94cd0 | ||
|
|
356eb7e5e7 | ||
|
|
bdd2afa1a2 | ||
|
|
e02ff26d0e | ||
|
|
128fb2826a | ||
|
|
3521ee16e4 | ||
|
|
af5cb36591 | ||
|
|
8aaae5b78c | ||
|
|
ced87b163f | ||
|
|
c5459abb65 | ||
|
|
83b7c0dd6f | ||
|
|
93fff9a69c | ||
|
|
b2247fa406 | ||
|
|
fafba9af3f |
@@ -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%
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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/
|
||||
|
||||
17
.travis.yml
@@ -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
|
||||
|
||||
@@ -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
|
||||
~~~~~~
|
||||
|
||||
@@ -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
|
||||
|
||||
18
FAQ.asciidoc
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
status:
|
||||
project:
|
||||
enabled: no
|
||||
patch:
|
||||
enabled: no
|
||||
changes:
|
||||
enabled: no
|
||||
|
||||
comment: off
|
||||
@@ -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
|
||||
|
||||
@@ -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[<←≪]\b,\b(<<|«)\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}]+
|
||||
|
||||
@@ -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
|
||||
--------------
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 945 B After Width: | Height: | Size: 872 B |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 128 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 21 KiB |
@@ -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."
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
@@ -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
|
||||
```
|
||||
@@ -1,3 +0,0 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
check-manifest==0.31
|
||||
@@ -1 +0,0 @@
|
||||
check-manifest
|
||||
@@ -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
|
||||
@@ -1 +0,0 @@
|
||||
codecov
|
||||
@@ -1,3 +0,0 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
cx-Freeze==4.3.4
|
||||
@@ -1 +0,0 @@
|
||||
cx_Freeze
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -1 +0,0 @@
|
||||
pip==8.1.2
|
||||
@@ -1,3 +0,0 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
PyInstaller==3.2
|
||||
@@ -1 +0,0 @@
|
||||
PyInstaller
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -1,6 +0,0 @@
|
||||
pylint
|
||||
./scripts/dev/pylint_checkers
|
||||
requests
|
||||
|
||||
# fix qute-pylint location
|
||||
#@ replace: qute-pylint==.* ./scripts/dev/pylint_checkers
|
||||
@@ -1,4 +0,0 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
docutils==0.12
|
||||
pyroma==2.0.2
|
||||
@@ -1 +0,0 @@
|
||||
pyroma
|
||||
@@ -1,6 +0,0 @@
|
||||
Jinja2
|
||||
Pygments
|
||||
pyPEG2
|
||||
PyYAML
|
||||
colorama
|
||||
cssutils
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -1 +0,0 @@
|
||||
tox
|
||||
@@ -1,3 +0,0 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
vulture==0.8.1
|
||||
@@ -1 +0,0 @@
|
||||
vulture
|
||||
@@ -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#/}"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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]))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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']),
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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():
|
||||
|
||||