Merge remote-tracking branch 'upstream/master' into hints_clicking
* upstream/master: (327 commits) Remove unused import tox: Update Werkzeug to 0.11.8 Regenerate authors Use __file__ instead of sys.argv[0] Regenerate authors Make update_3rdparty.py install correctly when run from any directory Open command line urls explicitly. tox: Update Werkzeug to 0.11.6 Move qutebrowser.rcc to misc/ Regenerate resources Fix CHANGELOG/link in README New qutebrowser logo! www: Add releases link Release v0.6.1 release checklist: Clarify how to build on Windows Make sure the cheatsheet PNG is included in sdist Fix cheatsheet link URL in quickstart Mark segfault on exit in test_smoke as xfail Add a xfail test for #797 Add missing file ... Conflicts: tests/integration/features/hints.feature
@ -11,7 +11,7 @@ environment:
|
||||
- TESTENV: pylint
|
||||
|
||||
install:
|
||||
- C:\Python27\python -u scripts\dev\ci_install.py
|
||||
- C:\Python27\python -u scripts\dev\ci\install.py
|
||||
|
||||
test_script:
|
||||
- C:\Python34\Scripts\tox -e %TESTENV% -- -v --junitxml=junit.xml
|
||||
- C:\Python34\Scripts\tox -e %TESTENV%
|
||||
|
17
.editorconfig
Normal file
@ -0,0 +1,17 @@
|
||||
[*]
|
||||
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
|
||||
|
3
.gitignore
vendored
@ -31,3 +31,6 @@ __pycache__
|
||||
/.hypothesis
|
||||
/prof
|
||||
TODO
|
||||
/scripts/testbrowser_cpp/Makefile
|
||||
/scripts/testbrowser_cpp/main.o
|
||||
/scripts/testbrowser_cpp/testbrowser
|
||||
|
97
.travis.yml
@ -1,67 +1,70 @@
|
||||
# So we get Ubuntu Trusty - using "dist: trusty" breaks OS X.
|
||||
sudo: required
|
||||
dist: trusty
|
||||
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
|
||||
|
||||
env:
|
||||
global:
|
||||
- PATH=/home/travis/bin:/home/travis/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
matrix:
|
||||
- TESTENV=py35
|
||||
- TESTENV=py34-cov
|
||||
- TESTENV=unittests-nodisp
|
||||
- TESTENV=misc
|
||||
- TESTENV=vulture
|
||||
- TESTENV=flake8
|
||||
- TESTENV=pyroma
|
||||
- TESTENV=check-manifest
|
||||
- TESTENV=pylint
|
||||
- TESTENV=eslint
|
||||
|
||||
# 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: DOCKER=debian-jessie
|
||||
services: docker
|
||||
- os: linux
|
||||
env: DOCKER=archlinux
|
||||
services: docker
|
||||
- os: linux
|
||||
env: DOCKER=ubuntu-wily
|
||||
services: docker
|
||||
- os: osx
|
||||
env: TESTENV=py35
|
||||
- os: linux
|
||||
env: TESTENV=pylint
|
||||
- os: linux
|
||||
env: TESTENV=flake8
|
||||
- os: linux
|
||||
env: TESTENV=docs
|
||||
- os: linux
|
||||
env: TESTENV=vulture
|
||||
- os: linux
|
||||
env: TESTENV=misc
|
||||
- os: linux
|
||||
env: TESTENV=pyroma
|
||||
- os: linux
|
||||
env: TESTENV=check-manifest
|
||||
- os: linux
|
||||
env: TESTENV=eslint
|
||||
allow_failures:
|
||||
- os: osx
|
||||
env: TESTENV=py35
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- $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:
|
||||
- python scripts/dev/ci_install.py
|
||||
- python scripts/dev/ci/install.py
|
||||
|
||||
script:
|
||||
- tox -e $TESTENV -- -v --cov-report term tests
|
||||
- bash scripts/dev/ci/travis_run.sh
|
||||
|
||||
after_success:
|
||||
- '[[ $TESTENV == *-cov ]] && codecov -e TESTENV -X gcov'
|
||||
|
||||
matrix:
|
||||
exclude:
|
||||
- os: linux
|
||||
env: TESTENV=py35
|
||||
- os: osx
|
||||
env: TESTENV=py34-cov
|
||||
- os: osx
|
||||
env: TESTENV=unittests-nodisp
|
||||
- os: osx
|
||||
env: TESTENV=misc
|
||||
- os: osx
|
||||
env: TESTENV=vulture
|
||||
- os: osx
|
||||
env: TESTENV=flake8
|
||||
- os: osx
|
||||
env: TESTENV=pyroma
|
||||
- os: osx
|
||||
env: TESTENV=check-manifest
|
||||
- os: osx
|
||||
env: TESTENV=pylint
|
||||
- os: osx
|
||||
env: TESTENV=eslint
|
||||
|
||||
notifications:
|
||||
webhooks:
|
||||
- https://buildtimetrend.herokuapp.com/travis
|
||||
irc:
|
||||
channels:
|
||||
- "chat.freenode.net#qutebrowser"
|
||||
on_success: change
|
||||
on_failure: always
|
||||
skip_join: true
|
||||
template:
|
||||
- "%{repository}#%{build_number} (%{branch} - %{commit} : %{author}): %{message}"
|
||||
- "%{compare_url} - %{build_url}"
|
||||
|
@ -14,12 +14,42 @@ 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.6.0 (unreleased)
|
||||
v0.7.0 (unreleased)
|
||||
-------------------
|
||||
|
||||
Added
|
||||
~~~~~
|
||||
|
||||
- New `:edit-url` command to edit the URL in an external editor.
|
||||
|
||||
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.
|
||||
|
||||
v0.6.1
|
||||
-----
|
||||
|
||||
Fixed
|
||||
~~~~~~
|
||||
|
||||
- Fixed broken cheatsheet image which was missing from package
|
||||
- Fixed occasional crash when switching/disconnecting monitors
|
||||
- Fixed crash when downloading non-ascii files with a broken locale (`LC_ALL=C`)
|
||||
- Added workaround for a Qt/PyQt bug which is too weird to describe here
|
||||
|
||||
v0.6.0
|
||||
------
|
||||
|
||||
Added
|
||||
~~~~~
|
||||
|
||||
- New `:buffer` command to easily switch tabs by name. This is not bound to a
|
||||
key by default for existing users due to a conflict with the `gt`/`gT`
|
||||
bindings (which are now removed from the default bindings).
|
||||
You can bind it by hand by running `:bind -f gt set-cmd-text -s :buffer`.
|
||||
- New `--quiet` argument for the `:debug-pyeval` command to not open a tab with
|
||||
the results. Note `:debug-pyeval` is still only intended for debugging.
|
||||
- The completion now matches each entered word separately.
|
||||
@ -28,6 +58,10 @@ Added
|
||||
clipboard.
|
||||
- New mode `word` for `hints -> mode` which uses a dictionary and link-texts
|
||||
for hints instead of single characters.
|
||||
- New `--all` argument for `:download-cancel` to cancel all running downloads.
|
||||
- New `password_fill` userscript to fill passwords using the `pass` executable.
|
||||
- New `current` hinting mode which forces opening hints in the current tab
|
||||
(even with `target="_blank"`)
|
||||
|
||||
Changed
|
||||
~~~~~~~
|
||||
@ -37,6 +71,13 @@ Changed
|
||||
- `general -> editor` can now also handle `{}` inside another argument (e.g. to open `vim` via `termite`)
|
||||
- Improved performance when scrolling with many tabs open.
|
||||
- Shift-Insert now also pastes primary selection for prompts.
|
||||
- `:download-remove --all` got un-deprecated to provide symmetry with
|
||||
`:download-cancel --all`. It does the same as `:download-clear`.
|
||||
- Improved detection of URLs/search terms when pasting multiple lines.
|
||||
- Don't remove `qutebrowser-editor-*` temporary file if editor subprocess crashed
|
||||
- Userscripts are also searched in `/usr/share/qutebrowser/userscripts`.
|
||||
- Blocked hosts are now also read from a `blocked-hosts` file in the config dir
|
||||
(e.g. `~/.config/qutebrowser/blocked-hosts`).
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
@ -51,6 +92,25 @@ Fixed
|
||||
- Fixed crashes when opening an empty URL (e.g. via pasting).
|
||||
- Fixed validation of duplicate values in `hints -> chars`.
|
||||
- Fixed crash when PDF.js was partially installed.
|
||||
- Fixed crash when XDG_DOWNLOAD_DIR was not an absolute path.
|
||||
- Fixed very long filenames when downloading `data://`-URLs.
|
||||
- Fixed ugly UI fonts on Windows when Liberation Mono is installed
|
||||
- Fixed crash when unbinding key from a section which doesn't exist in the config
|
||||
- Fixed report window after a segfault
|
||||
- Fixed some directory browser issues on Windows
|
||||
- Fixed crash when closing a window with a finished download and delayed
|
||||
`remove-finished-downloads` setting.
|
||||
- Fixed crash when hitting `<Tab>` then `<Ctrl-C>` on pages without keyboard
|
||||
focus.
|
||||
- Fixed "Frame load interrupted by policy change" error showing up when
|
||||
downloading files with Qt 5.6.
|
||||
|
||||
Removed
|
||||
~~~~~~~
|
||||
|
||||
- The `gt`/`gT` bindings (luakit-like alternatives to `J`/`K`) were removed
|
||||
(except for existing configs) to make room for the `gt` binding to show
|
||||
buffers.
|
||||
|
||||
v0.5.1
|
||||
------
|
||||
|
@ -95,7 +95,7 @@ Currently, following tox environments are available:
|
||||
- `py34`: Run pytest for python-3.4.
|
||||
- `py35`: Run pytest for python-3.5.
|
||||
- `py34-cov`: Run pytest for python-3.4 with code coverage report.
|
||||
- `py35-cov`: Run pytest for python-3.4 with code coverage report.
|
||||
- `py35-cov`: Run pytest for python-3.5 with code coverage report.
|
||||
* `flake8`: Run https://pypi.python.org/pypi/flake8[flake8] checks:
|
||||
https://pypi.python.org/pypi/pyflakes[pyflakes],
|
||||
https://pypi.python.org/pypi/pep8[pep8],
|
||||
@ -163,6 +163,9 @@ tox -e py35 -- tests/integration/features
|
||||
|
||||
# run everything with undo in the generated name, based on the scenario text
|
||||
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
|
||||
----
|
||||
|
||||
Profiling
|
||||
@ -603,6 +606,11 @@ 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
|
||||
@ -610,7 +618,6 @@ 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
|
||||
@ -619,12 +626,15 @@ 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 `scripts/dev/build_release.py` and upload.
|
||||
* 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
|
||||
|
||||
* 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`
|
||||
|
||||
* Update AUR package
|
||||
- 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.6.0/windows`
|
||||
* Announce to qutebrowser mailinglist
|
||||
|
@ -61,34 +61,23 @@ repository (rather than a release):
|
||||
$ python3 scripts/asciidoc2html.py
|
||||
----
|
||||
|
||||
If video or sound don't seem to work, try installing the gstreamer plugins:
|
||||
|
||||
----
|
||||
# apt-get install gstreamer1.0-plugins-{bad,base,good,ugly}
|
||||
----
|
||||
|
||||
Then <<tox,install qutebrowser via tox>>.
|
||||
|
||||
On Fedora
|
||||
---------
|
||||
|
||||
qutebrowser should run on Fedora 22.
|
||||
|
||||
Unfortunately there is no Fedora package yet, but installing qutebrowser is
|
||||
still relatively easy! If you want to help packaging it for Fedora, please
|
||||
mailto:mail@qutebrowser.org[get in touch]!
|
||||
|
||||
Install the dependencies via dnf:
|
||||
qutebrowser is available in the official repositories for Fedora 22 and newer.
|
||||
|
||||
----
|
||||
# dnf update
|
||||
# dnf install python3-qt5 python-tox python3-sip
|
||||
# dnf install qutebrowser
|
||||
----
|
||||
|
||||
To generate the documentation for the `:help` command, when using the git
|
||||
repository (rather than a release):
|
||||
|
||||
----
|
||||
# dnf install asciidoc source-highlight
|
||||
$ python3 scripts/asciidoc2html.py
|
||||
----
|
||||
|
||||
Then <<tox,install qutebrowser via tox>>.
|
||||
|
||||
On Archlinux
|
||||
------------
|
||||
|
||||
@ -113,6 +102,12 @@ $ rm -r qutebrowser-git
|
||||
|
||||
or you could use an AUR helper, e.g. `yaourt -S qutebrowser-git`.
|
||||
|
||||
If video or sound don't seem to work, try installing the gstreamer plugins:
|
||||
|
||||
----
|
||||
# pacman -S gst-plugins-{base,good,bad,ugly} gst-libav
|
||||
----
|
||||
|
||||
On Gentoo
|
||||
---------
|
||||
|
||||
@ -146,6 +141,21 @@ it with:
|
||||
$ nix-env -i qutebrowser
|
||||
----
|
||||
|
||||
On openSUSE
|
||||
-----------
|
||||
|
||||
There are prebuilt RPMs available for Tumbleweed and Leap 42.1:
|
||||
|
||||
http://software.opensuse.org/download.html?project=home%3Aarpraher&package=qutebrowser[One Click Install]
|
||||
|
||||
Or add the repo manually:
|
||||
|
||||
----
|
||||
# zypper addrepo http://download.opensuse.org/repositories/home:arpraher/openSUSE_Tumbleweed/home:arpraher.repo
|
||||
# zypper refresh
|
||||
# zypper install qutebrowser
|
||||
----
|
||||
|
||||
On Windows
|
||||
----------
|
||||
|
||||
@ -191,16 +201,24 @@ On OS X
|
||||
-------
|
||||
|
||||
To install qutebrowser on OS X, you'll want a package manager, e.g.
|
||||
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.
|
||||
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:
|
||||
|
||||
----
|
||||
$ brew install python3 pyqt5
|
||||
$ 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
|
||||
|
||||
$ pip3.5 install qutebrowser
|
||||
----
|
||||
|
||||
if you are using Homebrew. For MacPorts, run:
|
||||
For MacPorts, run:
|
||||
|
||||
----
|
||||
$ sudo port install python34 py34-jinja2 asciidoc py34-pygments py34-pyqt5
|
||||
@ -225,7 +243,16 @@ it as part of the packaging process.
|
||||
Installing qutebrowser with tox
|
||||
-------------------------------
|
||||
|
||||
Run tox inside the qutebrowser repository to set up a
|
||||
First of all, clone the repository using http://git-scm.org/[git] and switch
|
||||
into the repository folder:
|
||||
|
||||
----
|
||||
$ git clone https://github.com/The-Compiler/qutebrowser.git
|
||||
$ cd qutebrowser
|
||||
----
|
||||
|
||||
|
||||
Then run tox inside the qutebrowser repository to set up a
|
||||
https://docs.python.org/3/library/venv.html[virtual environment]:
|
||||
|
||||
----
|
||||
@ -243,6 +270,14 @@ your `$PATH` (e.g. `/usr/local/bin/qutebrowser` or `~/bin/qutebrowser`):
|
||||
~/path/to/qutebrowser/.venv/bin/python3 -m qutebrowser "$@"
|
||||
----
|
||||
|
||||
If you are developing on qutebrowser, you may want to redirect it to a local
|
||||
config:
|
||||
|
||||
----
|
||||
#!/bin/bash
|
||||
~/path/to/qutebrowser/.venv/bin/python3 -m qutebrowser -c .qutebrowser-local "$@"
|
||||
----
|
||||
|
||||
Updating
|
||||
~~~~~~~~
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
recursive-include qutebrowser *.py
|
||||
recursive-include qutebrowser/html *.html
|
||||
recursive-include qutebrowser/img *.svg *.png
|
||||
recursive-include qutebrowser/test *.py
|
||||
recursive-include qutebrowser/javascript *.js
|
||||
graft qutebrowser/html
|
||||
graft qutebrowser/3rdparty
|
||||
graft icons
|
||||
graft doc/img
|
||||
@ -18,12 +18,14 @@ 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
|
||||
|
@ -22,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
|
||||
-----------
|
||||
|
||||
@ -150,22 +156,26 @@ Contributors, sorted by the number of commits in descending order:
|
||||
* Felix Van der Jeugt
|
||||
* Raphael Pierzina
|
||||
* Joel Torstensson
|
||||
* Claude
|
||||
* Patric Schmitz
|
||||
* Claude
|
||||
* meles5
|
||||
* Tarcisio Fedrizzi
|
||||
* Artur Shaik
|
||||
* Nathan Isom
|
||||
* Austin Anderson
|
||||
* Thorsten Wißmann
|
||||
* Philipp Hansch
|
||||
* Kevin Velghe
|
||||
* Austin Anderson
|
||||
* Alexey "Averrin" Nabrodov
|
||||
* avk
|
||||
* ZDarian
|
||||
* Milan Svoboda
|
||||
* John ShaggyTwoDope Jenkins
|
||||
* Jimmy
|
||||
* Peter Vilim
|
||||
* Tarcisio Fedrizzi
|
||||
* Clayton Craft
|
||||
* Oliver Caldwell
|
||||
* Jonas Schürmann
|
||||
* Jimmy
|
||||
* Panagiotis Ktistakis
|
||||
* Jakub Klinkovský
|
||||
* skinnay
|
||||
@ -173,6 +183,7 @@ Contributors, sorted by the number of commits in descending order:
|
||||
* Zach-Button
|
||||
* Halfwit
|
||||
* rikn00
|
||||
* Ryan Roden-Corrent
|
||||
* Michael Ilsaas
|
||||
* Martin Zimmermann
|
||||
* Brian Jackson
|
||||
@ -184,17 +195,23 @@ Contributors, sorted by the number of commits in descending order:
|
||||
* Link
|
||||
* Larry Hynes
|
||||
* Johannes Altmanninger
|
||||
* avk
|
||||
* Samir Benmendil
|
||||
* Regina Hug
|
||||
* Mathias Fussenegger
|
||||
* Marcelo Santos
|
||||
* Fritz V155 Reichwald
|
||||
* Franz Fellner
|
||||
* Corentin Jule
|
||||
* zwarag
|
||||
* xd1le
|
||||
* issue
|
||||
* haxwithaxe
|
||||
* evan
|
||||
* dylan araps
|
||||
* Xitian9
|
||||
* Tomasz Kramkowski
|
||||
* Tomas Orsava
|
||||
* Tobias Werth
|
||||
* Tim Harder
|
||||
* Thiago Barroso Perrotta
|
||||
* Stefan Tatschner
|
||||
@ -202,7 +219,10 @@ Contributors, sorted by the number of commits in descending order:
|
||||
* Samuel Loury
|
||||
* Matthias Lisin
|
||||
* Marcel Schilling
|
||||
* Johannes Martinsson
|
||||
* Jean-Christophe Petkovich
|
||||
* Jay Kamat
|
||||
* Jan Verbeek
|
||||
* Helen Sherwood-Taylor
|
||||
* HalosGhost
|
||||
* Gregor Pohl
|
||||
@ -215,7 +235,8 @@ Contributors, sorted by the number of commits in descending order:
|
||||
|
||||
The following people have contributed graphics:
|
||||
|
||||
* WOFall (icon)
|
||||
* Jad/http://yelostudio.com[yelo] (new icon)
|
||||
* WOFall (original icon)
|
||||
* regines (key binding cheatsheet)
|
||||
|
||||
Thanks / Similar projects
|
||||
|
@ -11,6 +11,7 @@
|
||||
|<<bookmark-add,bookmark-add>>|Save the current page as a bookmark.
|
||||
|<<bookmark-del,bookmark-del>>|Delete a bookmark.
|
||||
|<<bookmark-load,bookmark-load>>|Load a bookmark.
|
||||
|<<buffer,buffer>>|Select tab by index or url/title best match.
|
||||
|<<close,close>>|Close the current window.
|
||||
|<<download,download>>|Download a given URL, or current page if no URL given.
|
||||
|<<download-cancel,download-cancel>>|Cancel the last/[count]th download.
|
||||
@ -19,6 +20,7 @@
|
||||
|<<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.
|
||||
@ -72,6 +74,8 @@
|
||||
=== adblock-update
|
||||
Update the adblock block lists.
|
||||
|
||||
This updates ~/.local/share/qutebrowser/blocked-hosts with downloaded host lists and re-reads ~/.config/qutebrowser/blocked-hosts.
|
||||
|
||||
[[back]]
|
||||
=== back
|
||||
Syntax: +:back [*--tab*] [*--bg*] [*--window*]+
|
||||
@ -140,6 +144,18 @@ Load a bookmark.
|
||||
* 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
|
||||
Syntax: +:buffer 'index'+
|
||||
|
||||
Select tab by index or url/title best match.
|
||||
|
||||
Focuses window if necessary.
|
||||
|
||||
==== positional arguments
|
||||
* +'index'+: The [win_id/]index of the tab to focus. Or a substring in which case the closest match will be focused.
|
||||
|
||||
|
||||
[[close]]
|
||||
=== close
|
||||
Close the current window.
|
||||
@ -161,8 +177,13 @@ The form `:download [url] [dest]` is deprecated, use `:download --dest [dest] [u
|
||||
|
||||
[[download-cancel]]
|
||||
=== download-cancel
|
||||
Syntax: +:download-cancel [*--all*]+
|
||||
|
||||
Cancel the last/[count]th download.
|
||||
|
||||
==== optional arguments
|
||||
* +*-a*+, +*--all*+: Cancel all running downloads
|
||||
|
||||
==== count
|
||||
The index of the download to cancel.
|
||||
|
||||
@ -175,14 +196,14 @@ Remove all finished downloads from the list.
|
||||
Delete the last/[count]th download from disk.
|
||||
|
||||
==== count
|
||||
The index of the download to cancel.
|
||||
The index of the download to delete.
|
||||
|
||||
[[download-open]]
|
||||
=== download-open
|
||||
Open the last/[count]th download.
|
||||
|
||||
==== count
|
||||
The index of the download to cancel.
|
||||
The index of the download to open.
|
||||
|
||||
[[download-remove]]
|
||||
=== download-remove
|
||||
@ -191,17 +212,36 @@ Syntax: +:download-remove [*--all*]+
|
||||
Remove the last/[count]th download from the list.
|
||||
|
||||
==== optional arguments
|
||||
* +*-a*+, +*--all*+: Deprecated argument for removing all finished downloads.
|
||||
* +*-a*+, +*--all*+: Remove all finished downloads.
|
||||
|
||||
==== count
|
||||
The index of the download to cancel.
|
||||
The index of the download to remove.
|
||||
|
||||
[[download-retry]]
|
||||
=== download-retry
|
||||
Retry the first failed/[count]th download.
|
||||
|
||||
==== count
|
||||
The index of the download to cancel.
|
||||
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
|
||||
@ -270,7 +310,8 @@ Start hinting.
|
||||
|
||||
* +'target'+: What to do with the selected element.
|
||||
|
||||
- `normal`: Open the link in the current tab.
|
||||
- `normal`: Open the link.
|
||||
- `current`: Open the link in the current tab.
|
||||
- `tab`: Open the link in a new tab (honoring the
|
||||
background-tabs setting).
|
||||
- `tab-fg`: Open the link in a new foreground tab.
|
||||
@ -620,8 +661,11 @@ Note the {url} variable which gets replaced by the current URL might be useful h
|
||||
* +'cmdline'+: The commandline to execute.
|
||||
|
||||
==== optional arguments
|
||||
* +*-u*+, +*--userscript*+: Run the command as a userscript. Either store the userscript in `~/.local/share/qutebrowser/userscripts`
|
||||
(or `$XDG_DATA_DIR`), or use an absolute path.
|
||||
* +*-u*+, +*--userscript*+: Run the command as a userscript. You can use an absolute path, or store the userscript in one of those
|
||||
locations:
|
||||
- `~/.local/share/qutebrowser/userscripts`
|
||||
(or `$XDG_DATA_DIR`)
|
||||
- `/usr/share/qutebrowser/userscripts`
|
||||
|
||||
* +*-v*+, +*--verbose*+: Show notifications when the command started/exited.
|
||||
* +*-d*+, +*--detach*+: Whether the command should be detached from qutebrowser.
|
||||
@ -675,7 +719,8 @@ 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.
|
||||
* +'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.
|
||||
|
||||
|
||||
==== count
|
||||
|
@ -180,7 +180,7 @@
|
||||
|<<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>>|Whether to auto-follow a hint if there's only one left.
|
||||
|<<hints-auto-follow,auto-follow>>|Follow a hint immediately when the hint text is completely matched.
|
||||
|<<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.
|
||||
|==============
|
||||
@ -1585,7 +1585,7 @@ Default: +pass:[/usr/share/dict/words]+
|
||||
|
||||
[[hints-auto-follow]]
|
||||
=== auto-follow
|
||||
Whether to auto-follow a hint if there's only one left.
|
||||
Follow a hint immediately when the hint text is completely matched.
|
||||
|
||||
Valid values:
|
||||
|
||||
@ -2041,7 +2041,7 @@ Fonts used for the UI, with optional style/weight/size.
|
||||
=== _monospace
|
||||
Default monospace fonts.
|
||||
|
||||
Default: +pass:[Terminus, Monospace, "DejaVu Sans Mono", Monaco, "Bitstream Vera Sans Mono", "Andale Mono", "Liberation Mono", "Courier New", Courier, monospace, Fixed, Consolas, Terminal]+
|
||||
Default: +pass:[Terminus, Monospace, "DejaVu Sans Mono", Monaco, "Bitstream Vera Sans Mono", "Andale Mono", "Courier New", Courier, "Liberation Mono", monospace, Fixed, Consolas, Terminal]+
|
||||
|
||||
[[fonts-completion]]
|
||||
=== completion
|
||||
|
BIN
doc/img/cheatsheet-big.png
Normal file
After Width: | Height: | Size: 989 KiB |
BIN
doc/img/cheatsheet-small.png
Normal file
After Width: | Height: | Size: 43 KiB |
@ -8,7 +8,7 @@ time, use the `:help` command.
|
||||
What to do now
|
||||
--------------
|
||||
|
||||
* View the http://qutebrowser.org/img/cheatsheet-big.png[key binding cheatsheet]
|
||||
* View the link:http://qutebrowser.org/img/cheatsheet-big.png[key binding cheatsheet]
|
||||
to make yourself familiar with the key bindings: +
|
||||
image:http://qutebrowser.org/img/cheatsheet-small.png["qutebrowser key binding cheatsheet",link="http://qutebrowser.org/img/cheatsheet-big.png"]
|
||||
* Run `:adblock-update` to download adblock lists and activate adblocking.
|
||||
|
@ -66,7 +66,7 @@ show it.
|
||||
How URLs should be opened if there is already a qutebrowser instance running.
|
||||
|
||||
=== debug arguments
|
||||
*-l* 'LOGLEVEL', *--loglevel* 'LOGLEVEL'::
|
||||
*-l* '{critical,error,warning,info,debug,vdebug}', *--loglevel* '{critical,error,warning,info,debug,vdebug}'::
|
||||
Set loglevel
|
||||
|
||||
*--logfilter* 'LOGFILTER'::
|
||||
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 8.7 KiB |
Before Width: | Height: | Size: 872 B After Width: | Height: | Size: 945 B |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.5 KiB |
2183
icons/qutebrowser-all.svg
Normal file
After Width: | Height: | Size: 128 KiB |
107
icons/qutebrowser-favicon.svg
Normal file
After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 16 KiB |
@ -13,7 +13,7 @@
|
||||
height="640"
|
||||
id="svg2"
|
||||
sodipodi:version="0.32"
|
||||
inkscape:version="0.48.5 r10040"
|
||||
inkscape:version="0.91 r13725"
|
||||
version="1.0"
|
||||
sodipodi:docname="cheatsheet.svg"
|
||||
inkscape:output_extension="org.inkscape.output.svg.inkscape"
|
||||
@ -32,18 +32,18 @@
|
||||
objecttolerance="10"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.8791156"
|
||||
inkscape:cx="768.67127"
|
||||
inkscape:cy="133.80749"
|
||||
inkscape:zoom="1.7582312"
|
||||
inkscape:cx="875.18895"
|
||||
inkscape:cy="136.8726"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
width="1024px"
|
||||
height="640px"
|
||||
showgrid="false"
|
||||
inkscape:window-width="636"
|
||||
inkscape:window-height="536"
|
||||
inkscape:window-x="2560"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-width="1362"
|
||||
inkscape:window-height="740"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="24"
|
||||
showguides="true"
|
||||
inkscape:guide-bbox="true"
|
||||
inkscape:window-maximized="0"
|
||||
@ -3040,17 +3040,13 @@
|
||||
style="font-weight:bold;-inkscape-font-specification:'Sans Bold';fill:#ff0000"
|
||||
id="flowSpan3852">(10)</flowSpan> misc. commands:</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara3725-0"><flowSpan
|
||||
style="fill:#0000ff"
|
||||
id="flowSpan5471">gm</flowSpan> - move tab</flowPara><flowPara
|
||||
id="flowPara3725-0">gt - switch tabs by name</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara3854"><flowSpan
|
||||
id="flowPara4052"><flowSpan
|
||||
style="fill:#0000ff"
|
||||
id="flowSpan5473">gl</flowSpan> - move tab to left</flowPara><flowPara
|
||||
id="flowSpan4054">gm/gl/lr</flowSpan> - move tab</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara3856"><flowSpan
|
||||
style="fill:#0000ff"
|
||||
id="flowSpan5475">gr</flowSpan> - move tab to right</flowPara><flowPara
|
||||
id="flowPara4056"> (to index/left/right)</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara3858">gC - clone tab</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
|
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 137 KiB |
35
misc/docker/archlinux/Dockerfile
Normal file
@ -0,0 +1,35 @@
|
||||
FROM base/archlinux
|
||||
MAINTAINER Florian Bruhin <me@the-compiler.org>
|
||||
|
||||
RUN echo 'Server = http://mirror.de.leaseweb.net/archlinux/$repo/os/$arch' > /etc/pacman.d/mirrorlist
|
||||
RUN pacman-key --init && pacman-key --populate archlinux && pacman -Sy --noconfirm archlinux-keyring
|
||||
|
||||
RUN pacman -Suyy --noconfirm
|
||||
RUN pacman-db-upgrade
|
||||
|
||||
RUN pacman -S --noconfirm \
|
||||
git \
|
||||
python-tox \
|
||||
qt5-base \
|
||||
qt5-webkit \
|
||||
python-pyqt5 \
|
||||
xorg-xinit \
|
||||
herbstluftwm \
|
||||
xorg-server-xvfb
|
||||
|
||||
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
|
||||
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
|
35
misc/docker/debian-jessie/Dockerfile
Normal file
@ -0,0 +1,35 @@
|
||||
FROM debian:jessie
|
||||
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 \
|
||||
locales \
|
||||
libjs-pdf
|
||||
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
|
||||
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 py34
|
34
misc/docker/ubuntu-wily/Dockerfile
Normal file
@ -0,0 +1,34 @@
|
||||
FROM ubuntu:wily
|
||||
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
|
||||
|
||||
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 py34
|
@ -30,9 +30,10 @@ import os
|
||||
import re
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
from urllib.parse import urljoin
|
||||
|
||||
with open(os.environ['QUTE_HTML'], 'r') as f:
|
||||
soup = BeautifulSoup(f)
|
||||
with open(os.environ['QUTE_FIFO'], 'w') as f:
|
||||
for link in soup.find_all('link', rel='alternate', type=re.compile(r'application/((rss|rdf|atom)\+)?xml|text/xml')):
|
||||
f.write('open -t %s\n' % link.get('href'))
|
||||
f.write('open -t %s\n' % urljoin(os.environ['QUTE_URL'], link.get('href')))
|
||||
|
364
misc/userscripts/password_fill
Executable file
@ -0,0 +1,364 @@
|
||||
#!/bin/bash -e
|
||||
help() {
|
||||
blink=$'\e[1;31m' reset=$'\e[0m'
|
||||
cat <<EOF
|
||||
This script can only be used as a userscript for qutebrowser
|
||||
2015, Thorsten Wißmann <edu _at_ thorsten-wissmann _dot_ de>
|
||||
In case of questions or suggestions, do not hesitate to send me an E-Mail or to
|
||||
directly ask me via IRC (nickname thorsten\`) in #qutebrowser on freenode.
|
||||
|
||||
$blink!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!$reset
|
||||
WARNING: the passwords are stored in qutebrowser's
|
||||
debug log reachable via the url qute:log
|
||||
$blink!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!$reset
|
||||
|
||||
Usage: run as a userscript form qutebrowser, e.g.:
|
||||
spawn --userscript ~/.config/qutebrowser/password_fill
|
||||
|
||||
Pass backend: (see also passwordstore.org)
|
||||
This script expects pass to store the credentials of each page in an extra
|
||||
file, where the filename (or filepath) contains the domain of the respective
|
||||
page. The first line of the file must contain the password, the login name
|
||||
must be contained in a later line beginning with "user:", "login:", or
|
||||
"username:" (configurable by the user_pattern variable).
|
||||
|
||||
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
|
||||
entry field. If multiple entries are found, a zenity menu is offered.
|
||||
|
||||
If no entry is found, then it crops subdomains from the url if at least one
|
||||
entry is found in the backend. (In that case, it always shows a menu)
|
||||
|
||||
Configuration:
|
||||
This script loads the bash script ~/.config/qutebrowser/password_fill_rc (if
|
||||
it exists), so you can change any configuration variable and overwrite any
|
||||
function you like.
|
||||
|
||||
EOF
|
||||
}
|
||||
|
||||
set -o pipefail
|
||||
shopt -s nocasematch # make regexp matching in bash case insensitive
|
||||
|
||||
if [ -z "$QUTE_FIFO" ] ; then
|
||||
help
|
||||
exit
|
||||
fi
|
||||
|
||||
error() {
|
||||
local msg="$*"
|
||||
echo "message-error '${msg//\'/\\\'}'" >> "$QUTE_FIFO"
|
||||
}
|
||||
msg() {
|
||||
local msg="$*"
|
||||
echo "message-info '${msg//\'/\\\'}'" >> "$QUTE_FIFO"
|
||||
}
|
||||
die() {
|
||||
error "$*"
|
||||
exit 0
|
||||
}
|
||||
|
||||
javascript_escape() {
|
||||
# print the first argument in a escaped way, such that it can savely
|
||||
# be used within javascripts double quotes
|
||||
sed "s,[\\\'\"],\\\&,g" <<< "$1"
|
||||
}
|
||||
|
||||
# ======================================================= #
|
||||
# CONFIGURATION
|
||||
# ======================================================= #
|
||||
# The configuration file is per default located in
|
||||
# ~/.config/qutebrowser/password_fill_rc and is a bash script that is loaded
|
||||
# later in the present script. So basically you can replace all of the
|
||||
# following definitions and make them fit your needs.
|
||||
|
||||
# The following simplifies a URL to the domain (e.g. "wiki.qutebrowser.org")
|
||||
# which is later used to search the correct entries in the password backend. If
|
||||
# you e.g. don't want the "www." to be removed or if you want to distinguish
|
||||
# between different paths on the same domain.
|
||||
|
||||
simplify_url() {
|
||||
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
|
||||
simple_url="${simple_url##www.}" # remove www. subdomain
|
||||
}
|
||||
|
||||
# no_entries_found() is called if the first query_entries() call did not find
|
||||
# any matching entries. Multiple implementations are possible:
|
||||
# The easiest behaviour is to quit:
|
||||
#no_entries_found() {
|
||||
# if [ 0 -eq "${#files[@]}" ] ; then
|
||||
# die "No entry found for »$simple_url«"
|
||||
# fi
|
||||
#}
|
||||
# But you could also fill the files array with all entries from your pass db
|
||||
# if the first db query did not find anything
|
||||
# no_entries_found() {
|
||||
# if [ 0 -eq "${#files[@]}" ] ; then
|
||||
# query_entries ""
|
||||
# if [ 0 -eq "${#files[@]}" ] ; then
|
||||
# die "No entry found for »$simple_url«"
|
||||
# fi
|
||||
# fi
|
||||
# }
|
||||
|
||||
# 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
|
||||
shorter_simple_url=$(sed 's,^[^.]*\.,,' <<< "$simple_url")
|
||||
if [ "$shorter_simple_url" = "$simple_url" ] ; then
|
||||
# if no dot, then even remove the top level domain
|
||||
simple_url=""
|
||||
query_entries "$simple_url"
|
||||
break
|
||||
fi
|
||||
simple_url="$shorter_simple_url"
|
||||
query_entries "$simple_url"
|
||||
#die "No entry found for »$simple_url«"
|
||||
# enforce menu if we do "fuzzy" matching
|
||||
menu_if_one_entry=1
|
||||
done
|
||||
if [ 0 -eq "${#files[@]}" ] ; then
|
||||
die "No entry found for »$simple_url«"
|
||||
fi
|
||||
}
|
||||
|
||||
# Backend implementations tell, how the actual password store is accessed.
|
||||
# Right now, there is only one fully functional password backend, namely for
|
||||
# the program "pass".
|
||||
# A password backend consists of three actions:
|
||||
# - init() initializes backend-specific things and does sanity checks.
|
||||
# - query_entries() is called with a simplified url and is expected to fill
|
||||
# the bash array $files with the names of matching password entries. There
|
||||
# are no requirements how these names should look like.
|
||||
# - open_entry() is called with some specific entry of the $files array and is
|
||||
# expected to write the username of that entry to the $username variable and
|
||||
# the corresponding password to $password
|
||||
|
||||
reset_backend() {
|
||||
init() { true ; }
|
||||
query_entries() { true ; }
|
||||
open_entry() { true ; }
|
||||
}
|
||||
|
||||
# choose_entry() is expected to choose one entry from the array $files and
|
||||
# write it to the variable $file.
|
||||
choose_entry() {
|
||||
choose_entry_zenity
|
||||
}
|
||||
|
||||
# The default implementation chooses a random entry from the array. So if there
|
||||
# are multiple matching entries, multiple calls to this userscript will
|
||||
# eventually pick the "correct" entry. I.e. if this userscript is bound to
|
||||
# "zl", the user has to press "zl" until the correct username shows up in the
|
||||
# login form.
|
||||
choose_entry_random() {
|
||||
local nr=${#files[@]}
|
||||
file="${files[$((RANDOM % nr))]}"
|
||||
# Warn user, that there might be other matching password entries
|
||||
if [ "$nr" -gt 1 ] ; then
|
||||
msg "Picked $file out of $nr entries: ${files[*]}"
|
||||
fi
|
||||
}
|
||||
|
||||
# another implementation would be to ask the user via some menu (like rofi or
|
||||
# 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 entrie in it
|
||||
menu_if_one_entry=0
|
||||
choose_entry_menu() {
|
||||
local nr=${#files[@]}
|
||||
if [ "$nr" -eq 1 ] && ! ((menu_if_one_entry)) ; then
|
||||
file="${files[0]}"
|
||||
else
|
||||
file=$( printf "%s\n" "${files[@]}" | "${MENU_COMMAND[@]}" )
|
||||
fi
|
||||
}
|
||||
|
||||
choose_entry_rofi() {
|
||||
MENU_COMMAND=( rofi -p "qutebrowser> " -dmenu
|
||||
-mesg $'Pick a password entry for <b>'"${QUTE_URL//&/&}"'</b>' )
|
||||
choose_entry_menu || true
|
||||
}
|
||||
|
||||
choose_entry_zenity() {
|
||||
MENU_COMMAND=( zenity --list --title "Qutebrowser password fill"
|
||||
--text "Pick the password entry:"
|
||||
--column "Name" )
|
||||
choose_entry_menu || true
|
||||
}
|
||||
|
||||
choose_entry_zenity_radio() {
|
||||
zenity_helper() {
|
||||
awk '{ print $0 ; print $0 }' \
|
||||
| zenity --list --radiolist \
|
||||
--title "Qutebrowser password fill" \
|
||||
--text "Pick the password entry:" \
|
||||
--column " " --column "Name"
|
||||
}
|
||||
MENU_COMMAND=( zenity_helper )
|
||||
choose_entry_menu || true
|
||||
}
|
||||
|
||||
# =======================================================
|
||||
# backend: PASS
|
||||
|
||||
# configuration options:
|
||||
match_filename=1 # whether allowing entry match by filepath
|
||||
match_line=0 # whether allowing entry match by URL-Pattern in file
|
||||
# Note: match_line=1 gets very slow, even for small password stores!
|
||||
match_line_pattern='^url: .*' # applied using grep -iE
|
||||
user_pattern='^(user|username|login): '
|
||||
|
||||
GPG_OPTS=( "--quiet" "--yes" "--compress-algo=none" "--no-encrypt-to" )
|
||||
GPG="gpg"
|
||||
export GPG_TTY="${GPG_TTY:-$(tty 2>/dev/null)}"
|
||||
which gpg2 &>/dev/null && GPG="gpg2"
|
||||
[[ -n $GPG_AGENT_INFO || $GPG == "gpg2" ]] && GPG_OPTS+=( "--batch" "--use-agent" )
|
||||
|
||||
pass_backend() {
|
||||
init() {
|
||||
PREFIX="${PASSWORD_STORE_DIR:-$HOME/.password-store}"
|
||||
if ! [ -d "$PREFIX" ] ; then
|
||||
die "Can not open password store dir »$PREFIX«"
|
||||
fi
|
||||
}
|
||||
query_entries() {
|
||||
local url="$1"
|
||||
|
||||
if ((match_line)) ; then
|
||||
# add entries with matching URL-tag
|
||||
while read -r -d "" passfile ; do
|
||||
if $GPG "${GPG_OPTS}" -d "$passfile" \
|
||||
| grep --max-count=1 -iE "${match_line_pattern}${url}" > /dev/null
|
||||
then
|
||||
passfile="${passfile#$PREFIX}"
|
||||
passfile="${passfile#/}"
|
||||
files+=( "${passfile%.gpg}" )
|
||||
fi
|
||||
done < <(find -L "$PREFIX" -iname '*.gpg' -print0)
|
||||
fi
|
||||
if ((match_filename)) ; then
|
||||
# add entries wth matching filepath
|
||||
while read -r passfile ; do
|
||||
passfile="${passfile#$PREFIX}"
|
||||
passfile="${passfile#/}"
|
||||
files+=( "${passfile%.gpg}" )
|
||||
done < <(find -L "$PREFIX" -iname '*.gpg' | grep "$url")
|
||||
fi
|
||||
}
|
||||
open_entry() {
|
||||
local path="$PREFIX/${1}.gpg"
|
||||
password=""
|
||||
local firstline=1
|
||||
while read -r line ; do
|
||||
if ((firstline)) ; then
|
||||
password="$line"
|
||||
firstline=0
|
||||
else
|
||||
if [[ $line =~ $user_pattern ]] ; then
|
||||
# remove the matching prefix "user: " from the beginning of the line
|
||||
username=${line#${BASH_REMATCH[0]}}
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done < <($GPG "${GPG_OPTS}" -d "$path" )
|
||||
}
|
||||
}
|
||||
# =======================================================
|
||||
|
||||
# =======================================================
|
||||
# backend: secret
|
||||
secret_backend() {
|
||||
init() {
|
||||
return
|
||||
}
|
||||
query_entries() {
|
||||
local domain="$1"
|
||||
while read -r line ; do
|
||||
if [[ "$line" =~ "attribute.username = " ]] ; then
|
||||
files+=("$domain ${line#${BASH_REMATCH[0]}}")
|
||||
fi
|
||||
done < <( secret-tool search --unlock --all domain "$domain" 2>&1 )
|
||||
}
|
||||
open_entry() {
|
||||
local domain="${1%% *}"
|
||||
username="${1#* }"
|
||||
password=$(secret-tool lookup domain "$domain" username "$username")
|
||||
}
|
||||
}
|
||||
# =======================================================
|
||||
|
||||
# load some sane default backend
|
||||
reset_backend
|
||||
pass_backend
|
||||
# load configuration
|
||||
QUTE_CONFIG_DIR=${QUTE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/qutebrowser/}
|
||||
PWFILL_CONFIG=${PWFILL_CONFIG:-${QUTE_CONFIG_DIR}/password_fill_rc}
|
||||
if [ -f "$PWFILL_CONFIG" ] ; then
|
||||
source "$PWFILL_CONFIG"
|
||||
fi
|
||||
init
|
||||
|
||||
simplify_url "$QUTE_URL"
|
||||
query_entries "${simple_url}"
|
||||
no_entries_found
|
||||
# remove duplicates
|
||||
mapfile -t files < <(printf "%s\n" "${files[@]}" | sort | uniq )
|
||||
choose_entry
|
||||
if [ -z "$file" ] ; then
|
||||
# choose_entry didn't want any of these entries
|
||||
exit 0
|
||||
fi
|
||||
open_entry "$file"
|
||||
#username="$(date)"
|
||||
#password="XYZ"
|
||||
#msg "$username, ${#password}"
|
||||
|
||||
[ -n "$username" ] || die "Username not set in entry $file"
|
||||
[ -n "$password" ] || die "Password not set in entry $file"
|
||||
|
||||
js() {
|
||||
cat <<EOF
|
||||
function hasPasswordField(form) {
|
||||
var inputs = form.getElementsByTagName("input");
|
||||
for (var j = 0; j < inputs.length; j++) {
|
||||
var input = inputs[j];
|
||||
if (input.type == "password") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
function loadData2Form (form) {
|
||||
var inputs = form.getElementsByTagName("input");
|
||||
for (var j = 0; j < inputs.length; j++) {
|
||||
var input = inputs[j];
|
||||
if (input.type == "text" || input.type == "email") {
|
||||
input.value = "$(javascript_escape "${username}")";
|
||||
}
|
||||
if (input.type == "password") {
|
||||
input.value = "$(javascript_escape "${password}")";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var forms = document.getElementsByTagName("form");
|
||||
for (i = 0; i < forms.length; i++) {
|
||||
if (hasPasswordField(forms[i])) {
|
||||
loadData2Form(forms[i]);
|
||||
}
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
printjs() {
|
||||
js | sed 's,//.*$,,' | tr '\n' ' '
|
||||
}
|
||||
echo "jseval -q $(printjs)" >> "$QUTE_FIFO"
|
@ -1,6 +1,6 @@
|
||||
[pytest]
|
||||
norecursedirs = .tox .venv
|
||||
addopts = --strict -rfEsw --faulthandler-timeout=70 --instafail
|
||||
addopts = --strict -rfEw --faulthandler-timeout=70 --instafail
|
||||
markers =
|
||||
gui: Tests using the GUI (e.g. spawning widgets)
|
||||
posix: Tests which only can run on a POSIX OS.
|
||||
@ -15,8 +15,8 @@ markers =
|
||||
skip: Always skipped test.
|
||||
pyqt531_or_newer: Needs PyQt 5.3.1 or newer.
|
||||
xfail_norun: xfail the test with out running it
|
||||
flaky: Tests which are flaky and should be rerun
|
||||
ci: Tests which should only run on CI.
|
||||
flaky: Tests which are flaky and should be rerun
|
||||
qt_log_level_fail = WARNING
|
||||
qt_log_ignore =
|
||||
^SpellCheck: .*
|
||||
@ -38,3 +38,4 @@ qt_log_ignore =
|
||||
^QObject::connect: Cannot connect \(null\)::stateChanged\(QNetworkSession::State\) to QNetworkReplyHttpImpl::_q_networkSessionStateChanged\(QNetworkSession::State\)
|
||||
^QXcbClipboard: Cannot transfer data, no data available
|
||||
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, 5, 1)
|
||||
__version_info__ = (0, 6, 1)
|
||||
__version__ = '.'.join(str(e) for e in __version_info__)
|
||||
__description__ = "A keyboard-driven, vim-like browser based on PyQt5 and QtWebKit."
|
||||
|
||||
|
@ -35,7 +35,7 @@ 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,
|
||||
QObject, Qt, QEvent)
|
||||
QObject, Qt, QEvent, pyqtSignal)
|
||||
try:
|
||||
import hunter
|
||||
except ImportError:
|
||||
@ -282,7 +282,8 @@ 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)
|
||||
tabbed_browser.tabopen(url, background=background,
|
||||
explicit=True)
|
||||
|
||||
|
||||
def _open_startpage(win_id=None):
|
||||
@ -742,6 +743,8 @@ class Application(QApplication):
|
||||
_args: ArgumentParser instance.
|
||||
"""
|
||||
|
||||
new_window = pyqtSignal(mainwindow.MainWindow)
|
||||
|
||||
def __init__(self, args):
|
||||
"""Constructor.
|
||||
|
||||
|
@ -92,9 +92,11 @@ class HostBlocker:
|
||||
|
||||
Attributes:
|
||||
_blocked_hosts: A set of blocked hosts.
|
||||
_config_blocked_hosts: A set of blocked hosts from ~/.config.
|
||||
_in_progress: The DownloadItems which are currently downloading.
|
||||
_done_count: How many files have been read successfully.
|
||||
_hosts_file: The path to the blocked-hosts file.
|
||||
_local_hosts_file: The path to the blocked-hosts file.
|
||||
_config_hosts_file: The path to a blocked-hosts in ~/.config
|
||||
|
||||
Class attributes:
|
||||
WHITELISTED: Hosts which never should be blocked.
|
||||
@ -105,13 +107,22 @@ class HostBlocker:
|
||||
|
||||
def __init__(self):
|
||||
self._blocked_hosts = set()
|
||||
self._config_blocked_hosts = set()
|
||||
self._in_progress = []
|
||||
self._done_count = 0
|
||||
|
||||
data_dir = standarddir.data()
|
||||
if data_dir is None:
|
||||
self._hosts_file = None
|
||||
self._local_hosts_file = None
|
||||
else:
|
||||
self._hosts_file = os.path.join(data_dir, 'blocked-hosts')
|
||||
self._local_hosts_file = os.path.join(data_dir, 'blocked-hosts')
|
||||
|
||||
config_dir = standarddir.config()
|
||||
if config_dir is None:
|
||||
self._config_hosts_file = None
|
||||
else:
|
||||
self._config_hosts_file = os.path.join(config_dir, 'blocked-hosts')
|
||||
|
||||
objreg.get('config').changed.connect(self.on_config_changed)
|
||||
|
||||
def is_blocked(self, url):
|
||||
@ -119,21 +130,46 @@ class HostBlocker:
|
||||
if not config.get('content', 'host-blocking-enabled'):
|
||||
return False
|
||||
host = url.host()
|
||||
return host in self._blocked_hosts and not is_whitelisted_host(host)
|
||||
return ((host in self._blocked_hosts or
|
||||
host in self._config_blocked_hosts) and
|
||||
not is_whitelisted_host(host))
|
||||
|
||||
def _read_hosts_file(self, filename, target):
|
||||
"""Read hosts from the given filename.
|
||||
|
||||
Args:
|
||||
filename: The file to read.
|
||||
target: The set to store the hosts in.
|
||||
|
||||
Return:
|
||||
True if a read was attempted, False otherwise
|
||||
"""
|
||||
if filename is None or not os.path.exists(filename):
|
||||
return False
|
||||
|
||||
try:
|
||||
with open(filename, 'r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
target.add(line.strip())
|
||||
except OSError:
|
||||
log.misc.exception("Failed to read host blocklist!")
|
||||
|
||||
return True
|
||||
|
||||
def read_hosts(self):
|
||||
"""Read hosts from the existing blocked-hosts file."""
|
||||
self._blocked_hosts = set()
|
||||
if self._hosts_file is None:
|
||||
|
||||
if self._local_hosts_file is None:
|
||||
return
|
||||
if os.path.exists(self._hosts_file):
|
||||
try:
|
||||
with open(self._hosts_file, 'r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
self._blocked_hosts.add(line.strip())
|
||||
except OSError:
|
||||
log.misc.exception("Failed to read host blocklist!")
|
||||
else:
|
||||
|
||||
self._read_hosts_file(self._config_hosts_file,
|
||||
self._config_blocked_hosts)
|
||||
|
||||
found = self._read_hosts_file(self._local_hosts_file,
|
||||
self._blocked_hosts)
|
||||
|
||||
if not found:
|
||||
args = objreg.get('args')
|
||||
if (config.get('content', 'host-block-lists') is not None and
|
||||
args.basedir is None):
|
||||
@ -142,8 +178,14 @@ class HostBlocker:
|
||||
|
||||
@cmdutils.register(instance='host-blocker', win_id='win_id')
|
||||
def adblock_update(self, win_id):
|
||||
"""Update the adblock block lists."""
|
||||
if self._hosts_file is None:
|
||||
"""Update the adblock block lists.
|
||||
|
||||
This updates ~/.local/share/qutebrowser/blocked-hosts with downloaded
|
||||
host lists and re-reads ~/.config/qutebrowser/blocked-hosts.
|
||||
"""
|
||||
self._read_hosts_file(self._config_hosts_file,
|
||||
self._config_blocked_hosts)
|
||||
if self._local_hosts_file is None:
|
||||
raise cmdexc.CommandError("No data storage is configured!")
|
||||
self._blocked_hosts = set()
|
||||
self._done_count = 0
|
||||
@ -221,7 +263,7 @@ class HostBlocker:
|
||||
|
||||
def on_lists_downloaded(self):
|
||||
"""Install block lists after files have been downloaded."""
|
||||
with open(self._hosts_file, 'w', encoding='utf-8') as f:
|
||||
with open(self._local_hosts_file, 'w', encoding='utf-8') as f:
|
||||
for host in sorted(self._blocked_hosts):
|
||||
f.write(host + '\n')
|
||||
message.info('current', "adblock: Read {} hosts from {} sources."
|
||||
@ -233,7 +275,7 @@ class HostBlocker:
|
||||
urls = config.get('content', 'host-block-lists')
|
||||
if urls is None:
|
||||
try:
|
||||
os.remove(self._hosts_file)
|
||||
os.remove(self._local_hosts_file)
|
||||
except OSError:
|
||||
log.misc.exception("Failed to delete hosts file.")
|
||||
|
||||
|
@ -45,6 +45,7 @@ from qutebrowser.utils import (message, usertypes, log, qtutils, urlutils,
|
||||
objreg, utils)
|
||||
from qutebrowser.utils.usertypes import KeyMode
|
||||
from qutebrowser.misc import editor, guiprocess
|
||||
from qutebrowser.completion.models import instances, sortfilter
|
||||
|
||||
|
||||
class CommandDispatcher:
|
||||
@ -64,7 +65,6 @@ class CommandDispatcher:
|
||||
"""
|
||||
|
||||
def __init__(self, win_id, tabbed_browser):
|
||||
self._editor = None
|
||||
self._win_id = win_id
|
||||
self._tabbed_browser = tabbed_browser
|
||||
|
||||
@ -248,7 +248,10 @@ class CommandDispatcher:
|
||||
try:
|
||||
url = urlutils.fuzzy_url(url)
|
||||
except urlutils.InvalidUrlError as e:
|
||||
raise cmdexc.CommandError(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
|
||||
if tab or bg or window:
|
||||
self._open(url, tab, bg, window)
|
||||
else:
|
||||
@ -818,10 +821,13 @@ class CommandDispatcher:
|
||||
text = utils.get_clipboard(selection=sel)
|
||||
if not text.strip():
|
||||
raise cmdexc.CommandError("{} is empty.".format(target))
|
||||
log.misc.debug("{} contained: '{}'".format(target,
|
||||
text.replace('\n', '\\n')))
|
||||
text_urls = enumerate(u for u in text.split('\n') if u.strip())
|
||||
for i, text_url in text_urls:
|
||||
log.misc.debug("{} contained: {!r}".format(target, text))
|
||||
text_urls = [u for u in text.split('\n') if u.strip()]
|
||||
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):
|
||||
text_urls = [text]
|
||||
for i, text_url in enumerate(text_urls):
|
||||
if not window and i > 0:
|
||||
tab = False
|
||||
bg = True
|
||||
@ -831,6 +837,60 @@ class CommandDispatcher:
|
||||
raise cmdexc.CommandError(e)
|
||||
self._open(url, tab, bg, window)
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window',
|
||||
completion=[usertypes.Completion.tab])
|
||||
def buffer(self, index):
|
||||
"""Select tab by index or url/title best match.
|
||||
|
||||
Focuses window if necessary.
|
||||
|
||||
Args:
|
||||
index: The [win_id/]index of the tab to focus. Or a substring
|
||||
in which case the closest match will be focused.
|
||||
"""
|
||||
index_parts = index.split('/', 1)
|
||||
|
||||
try:
|
||||
for part in index_parts:
|
||||
int(part)
|
||||
except ValueError:
|
||||
model = instances.get(usertypes.Completion.tab)
|
||||
sf = sortfilter.CompletionFilterModel(source=model)
|
||||
sf.set_pattern(index)
|
||||
if sf.count() > 0:
|
||||
index = sf.data(sf.first_item())
|
||||
index_parts = index.split('/', 1)
|
||||
else:
|
||||
raise cmdexc.CommandError(
|
||||
"No matching tab for: {}".format(index))
|
||||
|
||||
if len(index_parts) == 2:
|
||||
win_id = int(index_parts[0])
|
||||
idx = int(index_parts[1])
|
||||
elif len(index_parts) == 1:
|
||||
idx = int(index_parts[0])
|
||||
active_win = objreg.get('app').activeWindow()
|
||||
if active_win is None:
|
||||
# Not sure how you enter a command without an active window...
|
||||
raise cmdexc.CommandError(
|
||||
"No window specified and couldn't find active window!")
|
||||
win_id = active_win.win_id
|
||||
|
||||
if win_id not in objreg.window_registry:
|
||||
raise cmdexc.CommandError(
|
||||
"There's no window with id {}!".format(win_id))
|
||||
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=win_id)
|
||||
if not 0 < idx <= tabbed_browser.count():
|
||||
raise cmdexc.CommandError(
|
||||
"There's no tab with index {}!".format(idx))
|
||||
|
||||
window = objreg.window_registry[win_id]
|
||||
window.activateWindow()
|
||||
window.raise_()
|
||||
tabbed_browser.setCurrentIndex(idx-1)
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window',
|
||||
count='count')
|
||||
def tab_focus(self, index: {'type': (int, 'last')}=None, count=None):
|
||||
@ -840,7 +900,8 @@ class CommandDispatcher:
|
||||
|
||||
Args:
|
||||
index: The tab index to focus, starting with 1. The special value
|
||||
`last` focuses the last focused tab.
|
||||
`last` focuses the last focused tab. Negative indexes
|
||||
counts from the end, such that -1 is the last tab.
|
||||
count: The tab index to focus, starting with 1.
|
||||
"""
|
||||
if index == 'last':
|
||||
@ -849,6 +910,8 @@ 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())
|
||||
@ -915,9 +978,12 @@ class CommandDispatcher:
|
||||
useful here.
|
||||
|
||||
Args:
|
||||
userscript: Run the command as a userscript. Either store the
|
||||
userscript in `~/.local/share/qutebrowser/userscripts`
|
||||
(or `$XDG_DATA_DIR`), or use an absolute path.
|
||||
userscript: Run the command as a userscript. You can use an
|
||||
absolute path, or store the userscript in one of those
|
||||
locations:
|
||||
- `~/.local/share/qutebrowser/userscripts`
|
||||
(or `$XDG_DATA_DIR`)
|
||||
- `/usr/share/qutebrowser/userscripts`
|
||||
verbose: Show notifications when the command started/exited.
|
||||
detach: Whether the command should be detached from qutebrowser.
|
||||
cmdline: The commandline to execute.
|
||||
@ -1269,11 +1335,10 @@ class CommandDispatcher:
|
||||
text = str(elem)
|
||||
else:
|
||||
text = elem.evaluateJavaScript('this.value')
|
||||
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)
|
||||
ed = editor.ExternalEditor(self._win_id, self._tabbed_browser)
|
||||
ed.editing_finished.connect(functools.partial(
|
||||
self.on_editing_finished, elem))
|
||||
ed.edit(text)
|
||||
|
||||
def on_editing_finished(self, elem, text):
|
||||
"""Write the editor text into the form field and clean up tempfile.
|
||||
@ -1786,3 +1851,29 @@ 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',
|
||||
count='count')
|
||||
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())
|
||||
|
@ -27,6 +27,7 @@ import shutil
|
||||
import functools
|
||||
import collections
|
||||
|
||||
import sip
|
||||
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, QObject, QTimer,
|
||||
Qt, QVariant, QAbstractListModel, QModelIndex, QUrl)
|
||||
from PyQt5.QtGui import QDesktopServices
|
||||
@ -89,7 +90,7 @@ def path_suggestion(filename):
|
||||
return filename
|
||||
elif suggestion == 'both':
|
||||
return os.path.join(download_dir(), filename)
|
||||
else:
|
||||
else: # pragma: no cover
|
||||
raise ValueError("Invalid suggestion value {}!".format(suggestion))
|
||||
|
||||
|
||||
@ -103,6 +104,11 @@ def create_full_filename(basename, filename):
|
||||
Return:
|
||||
The full absolute path, or None if filename creation was not possible.
|
||||
"""
|
||||
# Remove chars which can't be encoded in the filename encoding.
|
||||
# See https://github.com/The-Compiler/qutebrowser/issues/427
|
||||
encoding = sys.getfilesystemencoding()
|
||||
filename = utils.force_encoding(filename, encoding)
|
||||
basename = utils.force_encoding(basename, encoding)
|
||||
if os.path.isabs(filename) and os.path.isdir(filename):
|
||||
# We got an absolute directory from the user, so we save it under
|
||||
# the default filename in that directory.
|
||||
@ -517,15 +523,11 @@ class DownloadItem(QObject):
|
||||
None: special value to stop the download.
|
||||
"""
|
||||
global last_used_directory
|
||||
if self.fileobj is not None:
|
||||
if self.fileobj is not None: # pragma: no cover
|
||||
raise ValueError("fileobj was already set! filename: {}, "
|
||||
"existing: {}, fileobj {}".format(
|
||||
filename, self._filename, self.fileobj))
|
||||
filename = os.path.expanduser(filename)
|
||||
# Remove chars which can't be encoded in the filename encoding.
|
||||
# See https://github.com/The-Compiler/qutebrowser/issues/427
|
||||
encoding = sys.getfilesystemencoding()
|
||||
filename = utils.force_encoding(filename, encoding)
|
||||
self._filename = create_full_filename(self.basename, filename)
|
||||
if self._filename is None:
|
||||
# We only got a filename (without directory) or a relative path
|
||||
@ -534,6 +536,22 @@ 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
|
||||
# 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
|
||||
# may be set for XDG_DOWNLOAD_DIR
|
||||
if self._filename is None:
|
||||
message.error(
|
||||
self._win_id,
|
||||
"XDG_DOWNLOAD_DIR points to a relative path - please check"
|
||||
" your ~/.config/user-dirs.dirs. The download is saved in"
|
||||
" your home directory.",
|
||||
)
|
||||
# fall back to $HOME as download_dir
|
||||
self._filename = create_full_filename(
|
||||
self.basename, os.path.expanduser(os.path.join('~', filename)))
|
||||
|
||||
self.basename = os.path.basename(self._filename)
|
||||
last_used_directory = os.path.dirname(self._filename)
|
||||
|
||||
@ -558,7 +576,7 @@ class DownloadItem(QObject):
|
||||
Args:
|
||||
fileobj: A file-like object.
|
||||
"""
|
||||
if self.fileobj is not None:
|
||||
if self.fileobj is not None: # pragma: no cover
|
||||
raise ValueError("fileobj was already set! Old: {}, new: "
|
||||
"{}".format(self.fileobj, fileobj))
|
||||
self.fileobj = fileobj
|
||||
@ -768,14 +786,32 @@ class DownloadManager(QAbstractListModel):
|
||||
|
||||
If not, None.
|
||||
"""
|
||||
if fileobj is not None and filename is not None:
|
||||
if fileobj is not None and filename is not None: # pragma: no cover
|
||||
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
|
||||
request.setAttribute(QNetworkRequest.CacheLoadControlAttribute,
|
||||
QNetworkRequest.AlwaysNetwork)
|
||||
|
||||
suggested_fn = urlutils.filename_from_url(request.url())
|
||||
if request.url().scheme().lower() != 'data':
|
||||
suggested_fn = urlutils.filename_from_url(request.url())
|
||||
else:
|
||||
# We might be downloading a binary blob embedded on a page or even
|
||||
# generated dynamically via javascript. We try to figure out a more
|
||||
# sensible name than the base64 content of the data.
|
||||
origin = request.originatingObject()
|
||||
try:
|
||||
origin_url = origin.url()
|
||||
except AttributeError:
|
||||
# Raised either if origin is None or some object that doesn't
|
||||
# have its own url. We're probably fine with a default fallback
|
||||
# then.
|
||||
suggested_fn = 'binary blob'
|
||||
else:
|
||||
# Use the originating URL as a base for the filename (works
|
||||
# e.g. for pdf.js).
|
||||
suggested_fn = urlutils.filename_from_url(origin_url)
|
||||
|
||||
if suggested_fn is None:
|
||||
suggested_fn = 'qutebrowser-download'
|
||||
|
||||
@ -834,7 +870,7 @@ class DownloadManager(QAbstractListModel):
|
||||
Return:
|
||||
The created DownloadItem.
|
||||
"""
|
||||
if fileobj is not None and filename is not None:
|
||||
if fileobj is not None and filename is not None: # pragma: no cover
|
||||
raise TypeError("Only one of fileobj/filename may be given!")
|
||||
if not suggested_filename:
|
||||
if filename is not None:
|
||||
@ -914,22 +950,30 @@ class DownloadManager(QAbstractListModel):
|
||||
|
||||
@cmdutils.register(instance='download-manager', scope='window',
|
||||
count='count')
|
||||
def download_cancel(self, count=0):
|
||||
def download_cancel(self, all_=False, count=0):
|
||||
"""Cancel the last/[count]th download.
|
||||
|
||||
Args:
|
||||
all_: Cancel all running downloads
|
||||
count: The index of the download to cancel.
|
||||
"""
|
||||
try:
|
||||
download = self.downloads[count - 1]
|
||||
except IndexError:
|
||||
self.raise_no_download(count)
|
||||
if download.done:
|
||||
if not count:
|
||||
count = len(self.downloads)
|
||||
raise cmdexc.CommandError("Download {} is already done!"
|
||||
.format(count))
|
||||
download.cancel()
|
||||
if all_:
|
||||
# We need to make a copy as we're indirectly mutating
|
||||
# self.downloads here
|
||||
for download in self.downloads[:]:
|
||||
if not download.done:
|
||||
download.cancel()
|
||||
else:
|
||||
try:
|
||||
download = self.downloads[count - 1]
|
||||
except IndexError:
|
||||
self.raise_no_download(count)
|
||||
if download.done:
|
||||
if not count:
|
||||
count = len(self.downloads)
|
||||
raise cmdexc.CommandError("Download {} is already done!"
|
||||
.format(count))
|
||||
download.cancel()
|
||||
|
||||
@cmdutils.register(instance='download-manager', scope='window',
|
||||
count='count')
|
||||
@ -937,7 +981,7 @@ class DownloadManager(QAbstractListModel):
|
||||
"""Delete the last/[count]th download from disk.
|
||||
|
||||
Args:
|
||||
count: The index of the download to cancel.
|
||||
count: The index of the download to delete.
|
||||
"""
|
||||
try:
|
||||
download = self.downloads[count - 1]
|
||||
@ -956,7 +1000,7 @@ class DownloadManager(QAbstractListModel):
|
||||
"""Open the last/[count]th download.
|
||||
|
||||
Args:
|
||||
count: The index of the download to cancel.
|
||||
count: The index of the download to open.
|
||||
"""
|
||||
try:
|
||||
download = self.downloads[count - 1]
|
||||
@ -974,7 +1018,7 @@ class DownloadManager(QAbstractListModel):
|
||||
"""Retry the first failed/[count]th download.
|
||||
|
||||
Args:
|
||||
count: The index of the download to cancel.
|
||||
count: The index of the download to retry.
|
||||
"""
|
||||
if count:
|
||||
try:
|
||||
@ -1060,12 +1104,10 @@ class DownloadManager(QAbstractListModel):
|
||||
"""Remove the last/[count]th download from the list.
|
||||
|
||||
Args:
|
||||
all_: Deprecated argument for removing all finished downloads.
|
||||
count: The index of the download to cancel.
|
||||
all_: Remove all finished downloads.
|
||||
count: The index of the download to remove.
|
||||
"""
|
||||
if all_:
|
||||
message.warning(self._win_id, ":download-remove --all is "
|
||||
"deprecated - use :download-clear instead!")
|
||||
self.download_clear()
|
||||
else:
|
||||
try:
|
||||
@ -1090,6 +1132,9 @@ class DownloadManager(QAbstractListModel):
|
||||
|
||||
def remove_item(self, download):
|
||||
"""Remove a given download."""
|
||||
if sip.isdeleted(self):
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/1242
|
||||
return
|
||||
try:
|
||||
idx = self.downloads.index(download)
|
||||
except ValueError:
|
||||
@ -1184,7 +1229,8 @@ 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()):
|
||||
|
@ -79,6 +79,7 @@ class DownloadView(QListView):
|
||||
self.setResizeMode(QListView.Adjust)
|
||||
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||
self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed)
|
||||
self.setFocusPolicy(Qt.NoFocus)
|
||||
self.setFlow(QListView.LeftToRight)
|
||||
self.setSpacing(1)
|
||||
self._menu = None
|
||||
|
@ -42,10 +42,10 @@ from qutebrowser.misc import guiprocess
|
||||
ElemTuple = collections.namedtuple('ElemTuple', ['elem', 'label'])
|
||||
|
||||
|
||||
Target = usertypes.enum('Target', ['normal', 'tab', 'tab_fg', 'tab_bg',
|
||||
'window', 'yank', 'yank_primary', 'run',
|
||||
'fill', 'hover', 'download', 'userscript',
|
||||
'spawn'])
|
||||
Target = usertypes.enum('Target', ['normal', 'current', 'tab', 'tab_fg',
|
||||
'tab_bg', 'window', 'yank', 'yank_primary',
|
||||
'run', 'fill', 'hover', 'download',
|
||||
'userscript', 'spawn'])
|
||||
|
||||
|
||||
class WordHintingError(Exception):
|
||||
@ -71,7 +71,8 @@ class HintContext:
|
||||
elems: A mapping from key strings to (elem, label) namedtuples.
|
||||
baseurl: The URL of the current page.
|
||||
target: What to do with the opened links.
|
||||
normal/tab/tab_fg/tab_bg/window: Get passed to BrowserTab.
|
||||
normal/current/tab/tab_fg/tab_bg/window: Get passed to
|
||||
BrowserTab.
|
||||
yank/yank_primary: Yank to clipboard/primary selection.
|
||||
run: Run a command.
|
||||
fill: Fill commandline with link.
|
||||
@ -128,6 +129,7 @@ class HintManager(QObject):
|
||||
|
||||
HINT_TEXTS = {
|
||||
Target.normal: "Follow hint",
|
||||
Target.current: "Follow hint in current tab",
|
||||
Target.tab: "Follow hint in new tab",
|
||||
Target.tab_fg: "Follow hint in foreground tab",
|
||||
Target.tab_bg: "Follow hint in background tab",
|
||||
@ -469,6 +471,7 @@ class HintManager(QObject):
|
||||
"""
|
||||
target_mapping = {
|
||||
Target.normal: usertypes.ClickTarget.normal,
|
||||
Target.current: usertypes.ClickTarget.normal,
|
||||
Target.tab_fg: usertypes.ClickTarget.tab,
|
||||
Target.tab_bg: usertypes.ClickTarget.tab_bg,
|
||||
Target.window: usertypes.ClickTarget.window,
|
||||
@ -507,6 +510,8 @@ class HintManager(QObject):
|
||||
QMouseEvent(QEvent.MouseButtonRelease, pos, Qt.LeftButton,
|
||||
Qt.NoButton, modifiers),
|
||||
]
|
||||
if context.target == Target.current:
|
||||
elem.remove_blank_target()
|
||||
for evt in events:
|
||||
self.mouse_event.emit(evt)
|
||||
if elem.is_text_input() and elem.is_editable():
|
||||
@ -785,7 +790,8 @@ class HintManager(QObject):
|
||||
|
||||
target: What to do with the selected element.
|
||||
|
||||
- `normal`: Open the link in the current tab.
|
||||
- `normal`: Open the link.
|
||||
- `current`: Open the link in the current tab.
|
||||
- `tab`: Open the link in a new tab (honoring the
|
||||
background-tabs setting).
|
||||
- `tab-fg`: Open the link in a new foreground tab.
|
||||
@ -935,6 +941,7 @@ class HintManager(QObject):
|
||||
# Handlers which take a QWebElement
|
||||
elem_handlers = {
|
||||
Target.normal: self._click,
|
||||
Target.current: self._click,
|
||||
Target.tab: self._click,
|
||||
Target.tab_fg: self._click,
|
||||
Target.tab_bg: self._click,
|
||||
|
@ -57,9 +57,29 @@ def is_root(directory):
|
||||
Return:
|
||||
Whether the directory is a root directory or not.
|
||||
"""
|
||||
# If you're curious as why this works:
|
||||
# dirname('/') = '/'
|
||||
# dirname('/home') = '/'
|
||||
# dirname('/home/') = '/home'
|
||||
# dirname('/home/foo') = '/home'
|
||||
# basically, for files (no trailing slash) it removes the file part, and
|
||||
# for directories, it removes the trailing slash, so the only way for this
|
||||
# to be equal is if the directory is the root directory.
|
||||
return os.path.dirname(directory) == directory
|
||||
|
||||
|
||||
def parent_dir(directory):
|
||||
"""Return the parent directory for the given directory.
|
||||
|
||||
Args:
|
||||
directory: The path to the directory.
|
||||
|
||||
Return:
|
||||
The path to the parent directory.
|
||||
"""
|
||||
return os.path.normpath(os.path.join(directory, os.pardir))
|
||||
|
||||
|
||||
def dirbrowser_html(path):
|
||||
"""Get the directory browser web page.
|
||||
|
||||
@ -70,30 +90,25 @@ def dirbrowser_html(path):
|
||||
The HTML of the web page.
|
||||
"""
|
||||
title = "Browse directory: {}".format(path)
|
||||
template = jinja.env.get_template('dirbrowser.html')
|
||||
# pylint: disable=no-member
|
||||
# WORKAROUND for https://bitbucket.org/logilab/pylint/issue/490/
|
||||
|
||||
if is_root(path):
|
||||
parent = None
|
||||
else:
|
||||
parent = os.path.dirname(path)
|
||||
parent = parent_dir(path)
|
||||
|
||||
try:
|
||||
all_files = os.listdir(path)
|
||||
except OSError as e:
|
||||
html = jinja.env.get_template('error.html').render(
|
||||
title="Error while reading directory",
|
||||
url='file://{}'.format(path),
|
||||
error=str(e),
|
||||
icon='')
|
||||
html = jinja.render('error.html',
|
||||
title="Error while reading directory",
|
||||
url='file:///{}'.format(path), error=str(e),
|
||||
icon='')
|
||||
return html.encode('UTF-8', errors='xmlcharrefreplace')
|
||||
|
||||
files = get_file_list(path, all_files, os.path.isfile)
|
||||
directories = get_file_list(path, all_files, os.path.isdir)
|
||||
html = template.render(title=title, url=path, icon='',
|
||||
parent=parent, files=files,
|
||||
directories=directories)
|
||||
html = jinja.render('dirbrowser.html', title=title, url=path, icon='',
|
||||
parent=parent, files=files, directories=directories)
|
||||
return html.encode('UTF-8', errors='xmlcharrefreplace')
|
||||
|
||||
|
||||
|
@ -23,8 +23,6 @@ import urllib.parse
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QUrl
|
||||
|
||||
from qutebrowser.misc import httpclient
|
||||
|
||||
|
||||
class PastebinClient(QObject):
|
||||
|
||||
@ -47,11 +45,17 @@ class PastebinClient(QObject):
|
||||
success = pyqtSignal(str)
|
||||
error = pyqtSignal(str)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
def __init__(self, client, parent=None):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
client: The HTTPClient to use. Will be reparented.
|
||||
"""
|
||||
super().__init__(parent)
|
||||
self._client = httpclient.HTTPClient(self)
|
||||
self._client.error.connect(self.error)
|
||||
self._client.success.connect(self.on_client_success)
|
||||
client.setParent(self)
|
||||
client.error.connect(self.error)
|
||||
client.success.connect(self.on_client_success)
|
||||
self._client = client
|
||||
|
||||
def paste(self, name, title, text, parent=None):
|
||||
"""Paste the text into a pastebin and return the URL.
|
||||
|
@ -16,12 +16,6 @@
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# pylint complains when using .render() on jinja templates, so we make it shut
|
||||
# up for this whole module.
|
||||
|
||||
# pylint: disable=no-member
|
||||
# WORKAROUND for https://bitbucket.org/logilab/pylint/issue/490/
|
||||
|
||||
"""Handler functions for different qute:... pages.
|
||||
|
||||
@ -149,17 +143,17 @@ class JSBridge(QObject):
|
||||
@add_handler('pyeval')
|
||||
def qute_pyeval(_win_id, _request):
|
||||
"""Handler for qute:pyeval. Return HTML content as bytes."""
|
||||
html = jinja.env.get_template('pre.html').render(
|
||||
title='pyeval', content=pyeval_output)
|
||||
html = jinja.render('pre.html', title='pyeval', content=pyeval_output)
|
||||
return html.encode('UTF-8', errors='xmlcharrefreplace')
|
||||
|
||||
|
||||
@add_handler('version')
|
||||
@add_handler('verizon')
|
||||
def qute_version(_win_id, _request):
|
||||
"""Handler for qute:version. Return HTML content as bytes."""
|
||||
html = jinja.env.get_template('version.html').render(
|
||||
title='Version info', version=version.version(),
|
||||
copyright=qutebrowser.__copyright__)
|
||||
html = jinja.render('version.html', title='Version info',
|
||||
version=version.version(),
|
||||
copyright=qutebrowser.__copyright__)
|
||||
return html.encode('UTF-8', errors='xmlcharrefreplace')
|
||||
|
||||
|
||||
@ -170,7 +164,7 @@ def qute_plainlog(_win_id, _request):
|
||||
text = "Log output was disabled."
|
||||
else:
|
||||
text = log.ram_handler.dump_log()
|
||||
html = jinja.env.get_template('pre.html').render(title='log', content=text)
|
||||
html = jinja.render('pre.html', title='log', content=text)
|
||||
return html.encode('UTF-8', errors='xmlcharrefreplace')
|
||||
|
||||
|
||||
@ -181,8 +175,7 @@ def qute_log(_win_id, _request):
|
||||
html_log = None
|
||||
else:
|
||||
html_log = log.ram_handler.dump_log(html=True)
|
||||
html = jinja.env.get_template('log.html').render(
|
||||
title='log', content=html_log)
|
||||
html = jinja.render('log.html', title='log', content=html_log)
|
||||
return html.encode('UTF-8', errors='xmlcharrefreplace')
|
||||
|
||||
|
||||
@ -198,7 +191,8 @@ def qute_help(win_id, request):
|
||||
try:
|
||||
utils.read_file('html/doc/index.html')
|
||||
except OSError:
|
||||
html = jinja.env.get_template('error.html').render(
|
||||
html = jinja.render(
|
||||
'error.html',
|
||||
title="Error while loading documentation",
|
||||
url=request.url().toDisplayString(),
|
||||
error="This most likely means the documentation was not generated "
|
||||
@ -217,16 +211,19 @@ def qute_help(win_id, request):
|
||||
message.error(win_id, "Your documentation is outdated! Please re-run "
|
||||
"scripts/asciidoc2html.py.")
|
||||
path = 'html/doc/{}'.format(urlpath)
|
||||
return utils.read_file(path).encode('UTF-8', errors='xmlcharrefreplace')
|
||||
if urlpath.endswith('.png'):
|
||||
return utils.read_file(path, binary=True)
|
||||
else:
|
||||
data = utils.read_file(path)
|
||||
return data.encode('UTF-8', errors='xmlcharrefreplace')
|
||||
|
||||
|
||||
@add_handler('settings')
|
||||
def qute_settings(win_id, _request):
|
||||
"""Handler for qute:settings. View/change qute configuration."""
|
||||
config_getter = functools.partial(objreg.get('config').get, raw=True)
|
||||
html = jinja.env.get_template('settings.html').render(
|
||||
win_id=win_id, title='settings', config=configdata,
|
||||
confget=config_getter)
|
||||
html = jinja.render('settings.html', win_id=win_id, title='settings',
|
||||
config=configdata, confget=config_getter)
|
||||
return html.encode('UTF-8', errors='xmlcharrefreplace')
|
||||
|
||||
|
||||
|
@ -285,6 +285,19 @@ class WebElementWrapper(collections.abc.MutableMapping):
|
||||
tag = self._elem.tagName().lower()
|
||||
return self.get('role', None) in roles or tag in ('input', 'textarea')
|
||||
|
||||
def remove_blank_target(self):
|
||||
"""Remove target from link."""
|
||||
elem = self._elem
|
||||
for _ in range(5):
|
||||
if elem is None:
|
||||
break
|
||||
tag = elem.tagName().lower()
|
||||
if tag == 'a' or tag == 'area':
|
||||
if elem.attribute('target') == '_blank':
|
||||
elem.setAttribute('target', '_top')
|
||||
break
|
||||
elem = elem.parent()
|
||||
|
||||
def debug_text(self):
|
||||
"""Get a text based on an element suitable for debug output."""
|
||||
self._check_vanished()
|
||||
|
@ -122,7 +122,10 @@ class BrowserPage(QWebPage):
|
||||
"""
|
||||
ignored_errors = [
|
||||
(QWebPage.QtNetwork, QNetworkReply.OperationCanceledError),
|
||||
(QWebPage.WebKit, 203), # "Loading is handled by the media engine"
|
||||
# "Loading is handled by the media engine"
|
||||
(QWebPage.WebKit, 203),
|
||||
# "Frame load interrupted by policy change"
|
||||
(QWebPage.WebKit, 102),
|
||||
]
|
||||
errpage.baseUrl = info.url
|
||||
urlstr = info.url.toDisplayString()
|
||||
@ -166,10 +169,8 @@ class BrowserPage(QWebPage):
|
||||
log.webview.debug("Error domain: {}, error code: {}".format(
|
||||
info.domain, info.error))
|
||||
title = "Error loading page: {}".format(urlstr)
|
||||
template = jinja.env.get_template('error.html')
|
||||
# pylint: disable=no-member
|
||||
# WORKAROUND for https://bitbucket.org/logilab/pylint/issue/490/
|
||||
html = template.render(
|
||||
html = jinja.render(
|
||||
'error.html',
|
||||
title=title, url=urlstr, error=error_str, icon='')
|
||||
errpage.content = html.encode('utf-8')
|
||||
errpage.encoding = 'utf-8'
|
||||
@ -221,14 +222,12 @@ class BrowserPage(QWebPage):
|
||||
def _show_pdfjs(self, reply):
|
||||
"""Show the reply with pdfjs."""
|
||||
try:
|
||||
page = pdfjs.generate_pdfjs_page(reply.url()).encode('utf-8')
|
||||
page = pdfjs.generate_pdfjs_page(reply.url())
|
||||
except pdfjs.PDFJSNotFound:
|
||||
# pylint: disable=no-member
|
||||
# WORKAROUND for https://bitbucket.org/logilab/pylint/issue/490/
|
||||
page = (jinja.env.get_template('no_pdfjs.html')
|
||||
.render(url=reply.url().toDisplayString())
|
||||
.encode('utf-8'))
|
||||
self.mainFrame().setContent(page, 'text/html', reply.url())
|
||||
page = jinja.render('no_pdfjs.html',
|
||||
url=reply.url().toDisplayString())
|
||||
self.mainFrame().setContent(page.encode('utf-8'), 'text/html',
|
||||
reply.url())
|
||||
reply.deleteLater()
|
||||
|
||||
def shutdown(self):
|
||||
|
@ -27,7 +27,7 @@ from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QTimer, QUrl
|
||||
from PyQt5.QtGui import QPalette
|
||||
from PyQt5.QtWidgets import QApplication, QStyleFactory
|
||||
from PyQt5.QtWebKit import QWebSettings
|
||||
from PyQt5.QtWebKitWidgets import QWebView, QWebPage
|
||||
from PyQt5.QtWebKitWidgets import QWebView, QWebPage, QWebFrame
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.keyinput import modeman
|
||||
@ -352,9 +352,14 @@ class WebView(QWebView):
|
||||
frame = self.page().mainFrame()
|
||||
frame.javaScriptWindowObjectCleared.connect(self.add_js_bridge)
|
||||
|
||||
@pyqtSlot(QWebFrame)
|
||||
def add_js_bridge(self):
|
||||
"""Add the javascript bridge for qute:... pages."""
|
||||
frame = self.sender()
|
||||
if not isinstance(frame, QWebFrame):
|
||||
log.webview.error("Got non-QWebFrame in add_js_bridge")
|
||||
return
|
||||
|
||||
if frame.url().scheme() == 'qute':
|
||||
bridge = objreg.get('js-bridge')
|
||||
frame.addToJavaScriptWindowObject('qute', bridge)
|
||||
|
@ -261,7 +261,8 @@ class CommandRunner(QObject):
|
||||
"""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:
|
||||
|
@ -344,14 +344,18 @@ def run(cmd, *args, win_id, env, verbose=False):
|
||||
user_agent = config.get('network', 'user-agent')
|
||||
if user_agent is not None:
|
||||
env['QUTE_USER_AGENT'] = user_agent
|
||||
cmd = os.path.expanduser(cmd)
|
||||
cmd_path = os.path.expanduser(cmd)
|
||||
|
||||
# if cmd is not given as an absolute path, look it up
|
||||
# ~/.local/share/qutebrowser/userscripts (or $XDG_DATA_DIR)
|
||||
if not os.path.isabs(cmd):
|
||||
log.misc.debug("{} is no absolute path".format(cmd))
|
||||
cmd = os.path.join(standarddir.data(), "userscripts", cmd)
|
||||
if not os.path.isabs(cmd_path):
|
||||
log.misc.debug("{} is no absolute path".format(cmd_path))
|
||||
cmd_path = os.path.join(standarddir.data(), "userscripts", cmd)
|
||||
if not os.path.exists(cmd_path):
|
||||
cmd_path = os.path.join(standarddir.system_data(),
|
||||
"userscripts", cmd)
|
||||
log.misc.debug("Userscript to run: {}".format(cmd_path))
|
||||
|
||||
runner.run(cmd, *args, env=env, verbose=verbose)
|
||||
runner.run(cmd_path, *args, env=env, verbose=verbose)
|
||||
runner.finished.connect(commandrunner.deleteLater)
|
||||
runner.finished.connect(runner.deleteLater)
|
||||
|
@ -59,6 +59,14 @@ def _init_url_completion():
|
||||
_instances[usertypes.Completion.url] = model
|
||||
|
||||
|
||||
def _init_tab_completion():
|
||||
"""Initialize the tab completion model."""
|
||||
log.completion.debug("Initializing tab completion.")
|
||||
with debug.log_time(log.completion, 'tab completion init'):
|
||||
model = miscmodels.TabCompletionModel()
|
||||
_instances[usertypes.Completion.tab] = model
|
||||
|
||||
|
||||
def _init_setting_completions():
|
||||
"""Initialize setting completion models."""
|
||||
log.completion.debug("Initializing setting completion.")
|
||||
@ -115,6 +123,7 @@ INITIALIZERS = {
|
||||
usertypes.Completion.command: _init_command_completion,
|
||||
usertypes.Completion.helptopic: _init_helptopic_completion,
|
||||
usertypes.Completion.url: _init_url_completion,
|
||||
usertypes.Completion.tab: _init_tab_completion,
|
||||
usertypes.Completion.section: _init_setting_completions,
|
||||
usertypes.Completion.option: _init_setting_completions,
|
||||
usertypes.Completion.value: _init_setting_completions,
|
||||
|
@ -19,6 +19,9 @@
|
||||
|
||||
"""Misc. CompletionModels."""
|
||||
|
||||
from PyQt5.QtCore import Qt, QTimer, pyqtSlot
|
||||
|
||||
from qutebrowser.browser import webview
|
||||
from qutebrowser.config import config, configdata
|
||||
from qutebrowser.utils import objreg, log
|
||||
from qutebrowser.commands import cmdutils
|
||||
@ -138,3 +141,78 @@ class SessionCompletionModel(base.BaseCompletionModel):
|
||||
self.new_item(cat, name)
|
||||
except OSError:
|
||||
log.completion.exception("Failed to list sessions!")
|
||||
|
||||
|
||||
class TabCompletionModel(base.BaseCompletionModel):
|
||||
|
||||
"""A model to complete on open tabs across all windows.
|
||||
|
||||
Used for switching the buffer command.
|
||||
"""
|
||||
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/545
|
||||
# pylint: disable=abstract-method
|
||||
|
||||
#IDX_COLUMN = 0
|
||||
URL_COLUMN = 1
|
||||
TEXT_COLUMN = 2
|
||||
|
||||
COLUMN_WIDTHS = (6, 40, 54)
|
||||
DUMB_SORT = Qt.DescendingOrder
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.columns_to_filter = [self.URL_COLUMN, self.TEXT_COLUMN]
|
||||
|
||||
for win_id in objreg.window_registry:
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=win_id)
|
||||
for i in range(tabbed_browser.count()):
|
||||
tab = tabbed_browser.widget(i)
|
||||
tab.url_text_changed.connect(self.rebuild)
|
||||
tab.shutting_down.connect(self.delayed_rebuild)
|
||||
tabbed_browser.new_tab.connect(self.on_new_tab)
|
||||
objreg.get("app").new_window.connect(self.on_new_window)
|
||||
self.rebuild()
|
||||
|
||||
# slot argument should be mainwindow.MainWindow but can't import
|
||||
# that at module level because of import loops.
|
||||
@pyqtSlot(object)
|
||||
def on_new_window(self, window):
|
||||
"""Add hooks to new windows."""
|
||||
window.tabbed_browser.new_tab.connect(self.on_new_tab)
|
||||
|
||||
@pyqtSlot(webview.WebView)
|
||||
def on_new_tab(self, tab):
|
||||
"""Add hooks to new tabs."""
|
||||
tab.url_text_changed.connect(self.rebuild)
|
||||
tab.shutting_down.connect(self.delayed_rebuild)
|
||||
self.rebuild()
|
||||
|
||||
@pyqtSlot()
|
||||
def delayed_rebuild(self):
|
||||
"""Fire a rebuild indirectly so widgets get a chance to update."""
|
||||
QTimer.singleShot(0, self.rebuild)
|
||||
|
||||
@pyqtSlot()
|
||||
def rebuild(self):
|
||||
"""Rebuild completion model from current tabs.
|
||||
|
||||
Very lazy method of keeping the model up to date. We could connect to
|
||||
signals for new tab, tab url/title changed, tab close, tab moved and
|
||||
make sure we handled background loads too ... but iterating over a
|
||||
few/few dozen/few hundred tabs doesn't take very long at all.
|
||||
"""
|
||||
self.removeRows(0, self.rowCount())
|
||||
for win_id in objreg.window_registry:
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=win_id)
|
||||
if tabbed_browser.shutting_down:
|
||||
continue
|
||||
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,7 +32,8 @@ 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
|
||||
|
@ -896,7 +896,8 @@ def data(readonly=False):
|
||||
|
||||
('auto-follow',
|
||||
SettingValue(typ.Bool(), 'true'),
|
||||
"Whether to auto-follow a hint if there's only one left."),
|
||||
"Follow a hint immediately when the hint text is completely "
|
||||
"matched."),
|
||||
|
||||
('next-regexes',
|
||||
SettingValue(typ.RegexList(flags=re.IGNORECASE),
|
||||
@ -1199,7 +1200,7 @@ def data(readonly=False):
|
||||
SettingValue(typ.Font(), 'Terminus, Monospace, '
|
||||
'"DejaVu Sans Mono", Monaco, '
|
||||
'"Bitstream Vera Sans Mono", "Andale Mono", '
|
||||
'"Liberation Mono", "Courier New", Courier, '
|
||||
'"Courier New", Courier, "Liberation Mono", '
|
||||
'monospace, Fixed, Consolas, Terminal'),
|
||||
"Default monospace fonts."),
|
||||
|
||||
@ -1396,8 +1397,8 @@ KEY_DATA = collections.OrderedDict([
|
||||
('tab-move', ['gm']),
|
||||
('tab-move -', ['gl']),
|
||||
('tab-move +', ['gr']),
|
||||
('tab-focus', ['J', 'gt']),
|
||||
('tab-prev', ['K', 'gT']),
|
||||
('tab-focus', ['J']),
|
||||
('tab-prev', ['K']),
|
||||
('tab-clone', ['gC']),
|
||||
('reload', ['r']),
|
||||
('reload -f', ['R']),
|
||||
@ -1476,6 +1477,7 @@ KEY_DATA = collections.OrderedDict([
|
||||
('download-cancel', ['ad']),
|
||||
('download-clear', ['cd']),
|
||||
('view-source', ['gf']),
|
||||
('set-cmd-text -s :buffer', ['gt']),
|
||||
('tab-focus last', ['<Ctrl-Tab>']),
|
||||
('enter-mode passthrough', ['<Ctrl-V>']),
|
||||
('quit', ['<Ctrl-Q>']),
|
||||
|
@ -1159,6 +1159,8 @@ 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):
|
||||
@ -1196,7 +1198,7 @@ class SearchEngineUrl(BaseType):
|
||||
self._basic_validation(value)
|
||||
if not value:
|
||||
return
|
||||
elif '{}' not in value:
|
||||
elif not ('{}' in value or '{0}' in value):
|
||||
raise configexc.ValidationError(value, "must contain \"{}\"")
|
||||
try:
|
||||
value.format("")
|
||||
|
@ -201,7 +201,7 @@ class KeyConfigParser(QObject):
|
||||
sect = self.keybindings[mode]
|
||||
except KeyError:
|
||||
raise cmdexc.CommandError("Can't find mode section '{}'!".format(
|
||||
sect))
|
||||
mode))
|
||||
try:
|
||||
del sect[key]
|
||||
except KeyError:
|
||||
|
@ -46,21 +46,21 @@ ul.files > li {
|
||||
<p id="dirbrowserTitleText">Browse directory: {{url}}</p>
|
||||
</div>
|
||||
|
||||
{% if parent %}
|
||||
{% if parent is not none %}
|
||||
<ul class="parent">
|
||||
<li><a href="{{parent}}">..</a></li>
|
||||
<li><a href="{{ file_url(parent) }}">..</a></li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
<ul class="folders">
|
||||
{% for item in directories %}
|
||||
<li><a href="file://{{item.absname}}">{{item.name}}</a></li>
|
||||
<li><a href="{{ file_url(item.absname) }}">{{item.name}}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<ul class="files">
|
||||
{% for item in files %}
|
||||
<li><a href="file://{{item.absname}}">{{item.name}}</a></li>
|
||||
<li><a href="{{ file_url(item.absname) }}">{{item.name}}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
22
qutebrowser/html/undef_error.html
Normal file
@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
vim: ft=html fileencoding=utf-8 sts=4 sw=4 et:
|
||||
-->
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Error while rendering HTML</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Error while rendering internal qutebrowser page</h1>
|
||||
<p>There was an error while rendering {pagename}.</p>
|
||||
|
||||
<p>This most likely happened because you updated qutebrowser but didn't restart yet.</p>
|
||||
|
||||
<p>If you believe this isn't the case and this is a bug, please do :report.<p>
|
||||
|
||||
<h2>Traceback</h2>
|
||||
<pre>{traceback}</pre>
|
||||
</body>
|
||||
</html>
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 57 KiB |
@ -23,7 +23,6 @@ import functools
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, Qt, QObject, QEvent
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PyQt5.QtWebKitWidgets import QWebView
|
||||
|
||||
from qutebrowser.keyinput import modeparsers, keyparser
|
||||
from qutebrowser.config import config
|
||||
@ -171,13 +170,9 @@ class ModeManager(QObject):
|
||||
is_non_alnum = (
|
||||
event.modifiers() not in (Qt.NoModifier, Qt.ShiftModifier) or
|
||||
not event.text().strip())
|
||||
focus_widget = QApplication.instance().focusWidget()
|
||||
is_tab = event.key() in (Qt.Key_Tab, Qt.Key_Backtab)
|
||||
|
||||
if handled:
|
||||
filter_this = True
|
||||
elif is_tab and not isinstance(focus_widget, QWebView):
|
||||
filter_this = True
|
||||
elif (parser.passthrough or
|
||||
self._forward_unbound_keys == 'all' or
|
||||
(self._forward_unbound_keys == 'auto' and is_non_alnum)):
|
||||
@ -189,11 +184,12 @@ class ModeManager(QObject):
|
||||
self._releaseevents_to_pass.add(KeyEvent(event))
|
||||
|
||||
if curmode != usertypes.KeyMode.insert:
|
||||
focus_widget = QApplication.instance().focusWidget()
|
||||
log.modes.debug("handled: {}, forward-unbound-keys: {}, "
|
||||
"passthrough: {}, is_non_alnum: {}, is_tab {} --> "
|
||||
"passthrough: {}, is_non_alnum: {} --> "
|
||||
"filter: {} (focused: {!r})".format(
|
||||
handled, self._forward_unbound_keys,
|
||||
parser.passthrough, is_non_alnum, is_tab,
|
||||
parser.passthrough, is_non_alnum,
|
||||
filter_this, focus_widget))
|
||||
return filter_this
|
||||
|
||||
|
@ -187,6 +187,8 @@ class MainWindow(QWidget):
|
||||
#self.tabWidget.setCurrentIndex(0)
|
||||
#QtCore.QMetaObject.connectSlotsByName(MainWindow)
|
||||
|
||||
objreg.get("app").new_window.emit(self)
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self)
|
||||
|
||||
|
@ -63,7 +63,7 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
tabbar -> new-tab-position set to 'left'.
|
||||
_tab_insert_idx_right: Same as above, for 'right'.
|
||||
_undo_stack: List of UndoEntry namedtuples of closed tabs.
|
||||
_shutting_down: Whether we're currently shutting down.
|
||||
shutting_down: Whether we're currently shutting down.
|
||||
|
||||
Signals:
|
||||
cur_progress: Progress of the current tab changed (loadProgress).
|
||||
@ -82,6 +82,7 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
widget can adjust its size to it.
|
||||
arg: The new size.
|
||||
current_tab_changed: The current tab changed to the emitted WebView.
|
||||
new_tab: Emits the new WebView and its index when a new tab is opened.
|
||||
"""
|
||||
|
||||
cur_progress = pyqtSignal(int)
|
||||
@ -96,13 +97,14 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
resized = pyqtSignal('QRect')
|
||||
got_cmd = pyqtSignal(str)
|
||||
current_tab_changed = pyqtSignal(webview.WebView)
|
||||
new_tab = pyqtSignal(webview.WebView, int)
|
||||
|
||||
def __init__(self, win_id, parent=None):
|
||||
super().__init__(win_id, parent)
|
||||
self._win_id = win_id
|
||||
self._tab_insert_idx_left = 0
|
||||
self._tab_insert_idx_right = -1
|
||||
self._shutting_down = False
|
||||
self.shutting_down = False
|
||||
self.tabCloseRequested.connect(self.on_tab_close_requested)
|
||||
self.currentChanged.connect(self.on_current_changed)
|
||||
self.cur_load_started.connect(self.on_cur_load_started)
|
||||
@ -234,7 +236,7 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
|
||||
def shutdown(self):
|
||||
"""Try to shut down all tabs cleanly."""
|
||||
self._shutting_down = True
|
||||
self.shutting_down = True
|
||||
for tab in self.widgets():
|
||||
self._remove_tab(tab)
|
||||
|
||||
@ -398,6 +400,7 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
if not background:
|
||||
self.setCurrentWidget(tab)
|
||||
tab.show()
|
||||
self.new_tab.emit(tab, idx)
|
||||
return tab
|
||||
|
||||
def _get_new_tab_idx(self, explicit):
|
||||
@ -546,7 +549,7 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
@pyqtSlot(int)
|
||||
def on_current_changed(self, idx):
|
||||
"""Set last-focused-tab and leave hinting mode when focus changed."""
|
||||
if idx == -1 or self._shutting_down:
|
||||
if idx == -1 or self.shutting_down:
|
||||
# closing the last tab (before quitting) or shutting down
|
||||
return
|
||||
tab = self.widget(idx)
|
||||
|
@ -36,7 +36,7 @@ from PyQt5.QtWidgets import (QDialog, QLabel, QTextEdit, QPushButton,
|
||||
|
||||
import qutebrowser
|
||||
from qutebrowser.utils import version, log, utils, objreg, qtutils
|
||||
from qutebrowser.misc import miscwidgets, autoupdate, msgbox
|
||||
from qutebrowser.misc import miscwidgets, autoupdate, msgbox, httpclient
|
||||
from qutebrowser.browser.network import pastebin
|
||||
from qutebrowser.config import config
|
||||
|
||||
@ -96,7 +96,8 @@ def get_fatal_crash_dialog(debug, data):
|
||||
|
||||
def _get_environment_vars():
|
||||
"""Gather environment variables for the crash info."""
|
||||
masks = ('DESKTOP_SESSION', 'DE', 'QT_*', 'PYTHON*', 'LC_*', 'LANG')
|
||||
masks = ('DESKTOP_SESSION', 'DE', 'QT_*', 'PYTHON*', 'LC_*', 'LANG',
|
||||
'XDG_*')
|
||||
info = []
|
||||
for key, value in os.environ.items():
|
||||
for m in masks:
|
||||
@ -140,7 +141,8 @@ class _CrashDialog(QDialog):
|
||||
self.setWindowTitle("Whoops!")
|
||||
self.resize(QSize(640, 600))
|
||||
self._vbox = QVBoxLayout(self)
|
||||
self._paste_client = pastebin.PastebinClient(self)
|
||||
http_client = httpclient.HTTPClient()
|
||||
self._paste_client = pastebin.PastebinClient(http_client, self)
|
||||
self._pypi_client = autoupdate.PyPIVersionClient(self)
|
||||
self._init_text()
|
||||
|
||||
@ -196,7 +198,8 @@ class _CrashDialog(QDialog):
|
||||
def _init_text(self):
|
||||
"""Initialize the main text to be displayed on an exception.
|
||||
|
||||
Should be extended by subclasses to set the actual text."""
|
||||
Should be extended by subclasses to set the actual text.
|
||||
"""
|
||||
self._lbl = QLabel(wordWrap=True, openExternalLinks=True,
|
||||
textInteractionFlags=Qt.LinksAccessibleByMouse)
|
||||
self._vbox.addWidget(self._lbl)
|
||||
@ -507,11 +510,23 @@ class FatalCrashDialog(_CrashDialog):
|
||||
def _init_text(self):
|
||||
super()._init_text()
|
||||
text = ("<b>qutebrowser was restarted after a fatal crash.</b><br/>"
|
||||
"<br/>Note: Crash reports for fatal crashes sometimes don't "
|
||||
"QTWEBENGINE_NOTE"
|
||||
"<br/>Crash reports for fatal crashes sometimes don't "
|
||||
"contain the information necessary to fix an issue. Please "
|
||||
"follow the steps in <a href='https://github.com/The-Compiler/"
|
||||
"qutebrowser/blob/master/doc/stacktrace.asciidoc'>"
|
||||
"stacktrace.asciidoc</a> to submit a stacktrace.<br/>")
|
||||
|
||||
if datetime.datetime.now() < datetime.datetime(2016, 4, 23):
|
||||
note = ("<br/>Fatal crashes like this are often caused by the "
|
||||
"current QtWebKit backend.<br/><b>I'm currently running a "
|
||||
"crowdfunding for the new QtWebEngine backend, based on "
|
||||
"Chromium:</b> <a href='http://igg.me/at/qutebrowser'>"
|
||||
"igg.me/at/qutebrowser</a><br/>")
|
||||
text = text.replace('QTWEBENGINE_NOTE', note)
|
||||
else:
|
||||
text = text.replace('QTWEBENGINE_NOTE', '')
|
||||
|
||||
self._lbl.setText(text)
|
||||
|
||||
def _init_checkboxes(self):
|
||||
|
@ -72,7 +72,7 @@ class CrashHandler(QObject):
|
||||
|
||||
def handle_segfault(self):
|
||||
"""Handle a segfault from a previous run."""
|
||||
data_dir = None
|
||||
data_dir = standarddir.data()
|
||||
if data_dir is None:
|
||||
return
|
||||
logname = os.path.join(data_dir, 'crash.log')
|
||||
|
@ -267,7 +267,8 @@ def remove_inputhook():
|
||||
"""Remove the PyQt input hook.
|
||||
|
||||
Doing this means we can't use the interactive shell anymore (which we don't
|
||||
anyways), but we can use pdb instead."""
|
||||
anyways), but we can use pdb instead.
|
||||
"""
|
||||
from PyQt5.QtCore import pyqtRemoveInputHook
|
||||
pyqtRemoveInputHook()
|
||||
|
||||
|
@ -58,7 +58,8 @@ class ExternalEditor(QObject):
|
||||
return
|
||||
try:
|
||||
os.close(self._oshandle)
|
||||
os.remove(self._filename)
|
||||
if self._proc.exit_status() != QProcess.CrashExit:
|
||||
os.remove(self._filename)
|
||||
except OSError as e:
|
||||
# NOTE: Do not replace this with "raise CommandError" as it's
|
||||
# executed async.
|
||||
|
@ -160,3 +160,6 @@ class GUIProcess(QObject):
|
||||
else:
|
||||
message.error(self._win_id, "Error while spawning {}: {}.".format(
|
||||
self._what, self._proc.error()), immediately=True)
|
||||
|
||||
def exit_status(self):
|
||||
return self._proc.exitStatus()
|
||||
|
@ -143,10 +143,20 @@ class SessionManager(QObject):
|
||||
history = tab.page().history()
|
||||
for idx, item in enumerate(history.items()):
|
||||
qtutils.ensure_valid(item)
|
||||
|
||||
item_data = {
|
||||
'url': bytes(item.url().toEncoded()).decode('ascii'),
|
||||
'title': item.title(),
|
||||
}
|
||||
|
||||
if item.title():
|
||||
item_data['title'] = item.title()
|
||||
else:
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/879
|
||||
if history.currentItemIndex() == idx:
|
||||
item_data['title'] = tab.page().mainFrame().title()
|
||||
else:
|
||||
item_data['title'] = item_data['url']
|
||||
|
||||
if item.originalUrl() != item.url():
|
||||
encoded = item.originalUrl().toEncoded()
|
||||
item_data['original-url'] = bytes(encoded).decode('ascii')
|
||||
@ -231,7 +241,9 @@ class SessionManager(QObject):
|
||||
log.sessions.debug("Saving session {} to {}...".format(name, path))
|
||||
if last_window:
|
||||
data = self._last_window_session
|
||||
assert data is not None
|
||||
if data is None:
|
||||
log.sessions.error("last_window_session is None while saving!")
|
||||
return
|
||||
else:
|
||||
data = self._save_all()
|
||||
log.sessions.vdebug("Saving data: {}".format(data))
|
||||
|
@ -74,7 +74,9 @@ def get_argparser():
|
||||
|
||||
debug = parser.add_argument_group('debug arguments')
|
||||
debug.add_argument('-l', '--loglevel', dest='loglevel',
|
||||
help="Set loglevel", default='info')
|
||||
help="Set loglevel", default='info',
|
||||
choices=['critical', 'error', 'warning', 'info',
|
||||
'debug', 'vdebug'])
|
||||
debug.add_argument('--logfilter',
|
||||
help="Comma-separated list of things to be logged "
|
||||
"to the debug log on stdout.")
|
||||
|
11037
qutebrowser/resources.py
@ -21,10 +21,12 @@
|
||||
|
||||
import os
|
||||
import os.path
|
||||
import traceback
|
||||
|
||||
import jinja2
|
||||
import jinja2.exceptions
|
||||
|
||||
from qutebrowser.utils import utils
|
||||
from qutebrowser.utils import utils, log
|
||||
|
||||
from PyQt5.QtCore import QUrl
|
||||
|
||||
@ -71,5 +73,28 @@ def resource_url(path):
|
||||
image = utils.resource_filename(path)
|
||||
return QUrl.fromLocalFile(image).toString(QUrl.FullyEncoded)
|
||||
|
||||
env = jinja2.Environment(loader=Loader('html'), autoescape=_guess_autoescape)
|
||||
env.globals['resource_url'] = resource_url
|
||||
|
||||
def file_url(path):
|
||||
"""Return a file:// url (as string) to the given local path.
|
||||
|
||||
Arguments:
|
||||
path: The absolute path to the local file
|
||||
"""
|
||||
return QUrl.fromLocalFile(path).toString(QUrl.FullyEncoded)
|
||||
|
||||
|
||||
def render(template, **kwargs):
|
||||
"""Render the given template and pass the given arguments to it."""
|
||||
try:
|
||||
return _env.get_template(template).render(**kwargs)
|
||||
except jinja2.exceptions.UndefinedError:
|
||||
log.misc.exception("UndefinedError while rendering " + template)
|
||||
err_path = os.path.join('html', 'undef_error.html')
|
||||
err_template = utils.read_file(err_path)
|
||||
tb = traceback.format_exc()
|
||||
return err_template.format(pagename=template, traceback=tb)
|
||||
|
||||
|
||||
_env = jinja2.Environment(loader=Loader('html'), autoescape=_guess_autoescape)
|
||||
_env.globals['resource_url'] = resource_url
|
||||
_env.globals['file_url'] = file_url
|
||||
|
@ -25,7 +25,7 @@ import os.path
|
||||
|
||||
from PyQt5.QtCore import QCoreApplication, QStandardPaths
|
||||
|
||||
from qutebrowser.utils import log, qtutils
|
||||
from qutebrowser.utils import log, qtutils, debug
|
||||
|
||||
|
||||
# The argparse namespace passed to init()
|
||||
@ -65,6 +65,17 @@ def data():
|
||||
return path
|
||||
|
||||
|
||||
def system_data():
|
||||
"""Get a location for system-wide data. This path may be read-only."""
|
||||
if sys.platform.startswith('linux'):
|
||||
path = "/usr/share/qutebrowser"
|
||||
if not os.path.exists(path):
|
||||
path = data()
|
||||
else:
|
||||
path = data()
|
||||
return path
|
||||
|
||||
|
||||
def cache():
|
||||
"""Get a location for the cache."""
|
||||
typ = QStandardPaths.CacheLocation
|
||||
@ -113,6 +124,8 @@ def _writable_location(typ):
|
||||
"""Wrapper around QStandardPaths.writableLocation."""
|
||||
with qtutils.unset_organization():
|
||||
path = QStandardPaths.writableLocation(typ)
|
||||
typ_str = debug.qenum_key(QStandardPaths, typ)
|
||||
log.misc.debug("writable location for {}: {}".format(typ_str, path))
|
||||
if not path:
|
||||
raise ValueError("QStandardPaths returned an empty value!")
|
||||
# Qt seems to use '/' as path separator even on Windows...
|
||||
|
@ -80,7 +80,7 @@ def _parse_search_term(s):
|
||||
engine = None
|
||||
term = s
|
||||
|
||||
log.url.debug("engine {}, term '{}'".format(engine, term))
|
||||
log.url.debug("engine {}, term {!r}".format(engine, term))
|
||||
return (engine, term)
|
||||
|
||||
|
||||
@ -93,7 +93,7 @@ def _get_search_url(txt):
|
||||
Return:
|
||||
The search URL as a QUrl.
|
||||
"""
|
||||
log.url.debug("Finding search engine for '{}'".format(txt))
|
||||
log.url.debug("Finding search engine for {!r}".format(txt))
|
||||
engine, term = _parse_search_term(txt)
|
||||
assert term
|
||||
if engine is None:
|
||||
@ -171,22 +171,10 @@ def fuzzy_url(urlstr, cwd=None, relative=False, do_search=True):
|
||||
A target QUrl to a search page or the original URL.
|
||||
"""
|
||||
urlstr = urlstr.strip()
|
||||
expanded = os.path.expanduser(urlstr)
|
||||
path = get_path_if_valid(urlstr, cwd=cwd, relative=relative,
|
||||
check_exists=True)
|
||||
|
||||
if os.path.isabs(expanded):
|
||||
path = expanded
|
||||
elif relative and cwd:
|
||||
path = os.path.join(cwd, expanded)
|
||||
elif relative:
|
||||
try:
|
||||
path = os.path.abspath(expanded)
|
||||
except OSError:
|
||||
path = None
|
||||
else:
|
||||
path = None
|
||||
|
||||
if path is not None and os.path.exists(path):
|
||||
log.url.debug("URL is a local file")
|
||||
if path is not None:
|
||||
url = QUrl.fromLocalFile(path)
|
||||
elif (not do_search) or is_url(urlstr):
|
||||
# probably an address
|
||||
@ -198,7 +186,7 @@ def fuzzy_url(urlstr, cwd=None, relative=False, do_search=True):
|
||||
url = _get_search_url(urlstr)
|
||||
except ValueError: # invalid search engine
|
||||
url = qurl_from_user_input(urlstr)
|
||||
log.url.debug("Converting fuzzy term {} to URL -> {}".format(
|
||||
log.url.debug("Converting fuzzy term {!r} to URL -> {}".format(
|
||||
urlstr, url.toDisplayString()))
|
||||
if do_search and config.get('general', 'auto-search') and urlstr:
|
||||
qtutils.ensure_valid(url)
|
||||
@ -246,7 +234,7 @@ def is_url(urlstr):
|
||||
"""
|
||||
autosearch = config.get('general', 'auto-search')
|
||||
|
||||
log.url.debug("Checking if '{}' is a URL (autosearch={}).".format(
|
||||
log.url.debug("Checking if {!r} is a URL (autosearch={}).".format(
|
||||
urlstr, autosearch))
|
||||
|
||||
urlstr = urlstr.strip()
|
||||
@ -349,6 +337,44 @@ def raise_cmdexc_if_invalid(url):
|
||||
raise cmdexc.CommandError(get_errstring(url))
|
||||
|
||||
|
||||
def get_path_if_valid(pathstr, cwd=None, relative=False, check_exists=False):
|
||||
"""Check if path is a valid path.
|
||||
|
||||
Args:
|
||||
pathstr: The path as string.
|
||||
cwd: The current working directory, or None.
|
||||
relative: Whether to resolve relative files.
|
||||
check_exists: Whether to check if the file
|
||||
actually exists of filesystem.
|
||||
|
||||
Return:
|
||||
The path if it is a valid path, None otherwise.
|
||||
"""
|
||||
pathstr = pathstr.strip()
|
||||
log.url.debug("Checking if {!r} is a path".format(pathstr))
|
||||
expanded = os.path.expanduser(pathstr)
|
||||
|
||||
if os.path.isabs(expanded):
|
||||
path = expanded
|
||||
elif relative and cwd:
|
||||
path = os.path.join(cwd, expanded)
|
||||
elif relative:
|
||||
try:
|
||||
path = os.path.abspath(expanded)
|
||||
except OSError:
|
||||
path = None
|
||||
else:
|
||||
path = None
|
||||
|
||||
if check_exists:
|
||||
if path is not None and os.path.exists(path):
|
||||
log.url.debug("URL is a local file")
|
||||
else:
|
||||
path = None
|
||||
|
||||
return path
|
||||
|
||||
|
||||
def filename_from_url(url):
|
||||
"""Get a suitable filename from an URL.
|
||||
|
||||
|
@ -237,7 +237,7 @@ KeyMode = enum('KeyMode', ['normal', 'hint', 'command', 'yesno', 'prompt',
|
||||
# Available command completions
|
||||
Completion = enum('Completion', ['command', 'section', 'option', 'value',
|
||||
'helptopic', 'quickmark_by_name',
|
||||
'bookmark_by_url', 'url', 'sessions'])
|
||||
'bookmark_by_url', 'url', 'tab', 'sessions'])
|
||||
|
||||
|
||||
# Exit statuses for errors. Needs to be an int for sys.exit.
|
||||
|
@ -1,8 +1,8 @@
|
||||
Jinja2==2.8.0
|
||||
MarkupSafe==0.23
|
||||
Pygments==2.1.1
|
||||
Pygments==2.1.3
|
||||
pyPEG2==2.15.2
|
||||
PyYAML==3.11
|
||||
colorama==0.3.6
|
||||
colorama==0.3.7
|
||||
colorlog==2.6.1
|
||||
cssutils==1.0.1
|
||||
|
@ -76,6 +76,7 @@ class AsciiDoc:
|
||||
self._build_website()
|
||||
else:
|
||||
self._build_docs()
|
||||
self._copy_images()
|
||||
|
||||
def _build_docs(self):
|
||||
"""Render .asciidoc files to .html sites."""
|
||||
@ -84,8 +85,38 @@ class AsciiDoc:
|
||||
name, _ext = os.path.splitext(os.path.basename(src))
|
||||
dst = 'qutebrowser/html/doc/{}.html'.format(name)
|
||||
files.append((src, dst))
|
||||
|
||||
# patch image links to use local copy
|
||||
replacements = [
|
||||
("http://qutebrowser.org/img/cheatsheet-big.png",
|
||||
"qute://help/img/cheatsheet-big.png"),
|
||||
("http://qutebrowser.org/img/cheatsheet-small.png",
|
||||
"qute://help/img/cheatsheet-small.png")
|
||||
]
|
||||
|
||||
for src, dst in files:
|
||||
self.call(src, dst)
|
||||
src_basename = os.path.basename(src)
|
||||
modified_src = os.path.join(self._tempdir, src_basename)
|
||||
with open(modified_src, 'w', encoding='utf-8') as modified_f, \
|
||||
open(src, 'r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
for orig, repl in replacements:
|
||||
line = line.replace(orig, repl)
|
||||
modified_f.write(line)
|
||||
self.call(modified_src, dst)
|
||||
|
||||
def _copy_images(self):
|
||||
"""Copy image files to qutebrowser/html/doc."""
|
||||
print("Copying files...")
|
||||
dst_path = os.path.join('qutebrowser', 'html', 'doc', 'img')
|
||||
try:
|
||||
os.mkdir(dst_path)
|
||||
except FileExistsError:
|
||||
pass
|
||||
for filename in ['cheatsheet-big.png', 'cheatsheet-small.png']:
|
||||
src = os.path.join('doc', 'img', filename)
|
||||
dst = os.path.join(dst_path, filename)
|
||||
shutil.copy(src, dst)
|
||||
|
||||
def _build_website_file(self, root, filename):
|
||||
"""Build a single website file."""
|
||||
@ -249,6 +280,8 @@ def main(colors=False):
|
||||
"asciidoc.py. If not given, it's searched in PATH.",
|
||||
nargs=2, required=False,
|
||||
metavar=('PYTHON', 'ASCIIDOC'))
|
||||
parser.add_argument('--no-authors', help=argparse.SUPPRESS,
|
||||
action='store_true')
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
os.mkdir('qutebrowser/html/doc')
|
||||
|
@ -91,7 +91,7 @@ def smoke_test(executable):
|
||||
def build_windows():
|
||||
"""Build windows executables/setups."""
|
||||
utils.print_title("Updating 3rdparty content")
|
||||
update_3rdparty.main()
|
||||
update_3rdparty.update_pdfjs()
|
||||
|
||||
utils.print_title("Building Windows binaries")
|
||||
parts = str(sys.version_info.major), str(sys.version_info.minor)
|
||||
|
@ -65,6 +65,8 @@ PERFECT_FILES = [
|
||||
'qutebrowser/browser/network/filescheme.py'),
|
||||
('tests/unit/browser/network/test_networkreply.py',
|
||||
'qutebrowser/browser/network/networkreply.py'),
|
||||
('tests/unit/browser/network/test_pastebin.py',
|
||||
'qutebrowser/browser/network/pastebin.py'),
|
||||
('tests/unit/browser/test_signalfilter.py',
|
||||
'qutebrowser/browser/signalfilter.py'),
|
||||
|
||||
@ -102,6 +104,8 @@ PERFECT_FILES = [
|
||||
'qutebrowser/mainwindow/statusbar/textbase.py'),
|
||||
('tests/unit/mainwindow/statusbar/test_prompt.py',
|
||||
'qutebrowser/mainwindow/statusbar/prompt.py'),
|
||||
('tests/unit/mainwindow/statusbar/test_url.py',
|
||||
'qutebrowser/mainwindow/statusbar/url.py'),
|
||||
|
||||
('tests/unit/config/test_configtypes.py',
|
||||
'qutebrowser/config/configtypes.py'),
|
||||
@ -223,8 +227,16 @@ def main_check():
|
||||
print(e)
|
||||
messages = []
|
||||
|
||||
for msg in messages:
|
||||
print(msg.text)
|
||||
if messages:
|
||||
print()
|
||||
print()
|
||||
utils.print_title("Coverage check failed")
|
||||
for msg in messages:
|
||||
print(msg.text)
|
||||
print()
|
||||
print("You can run 'tox -e py35-cov' (or py34-cov) locally and check "
|
||||
"htmlcov/index.html to debug this.")
|
||||
print()
|
||||
|
||||
if 'CI' in os.environ:
|
||||
print("Keeping coverage.xml on CI.")
|
||||
|
35
scripts/dev/check_doc_changes.py
Executable file
@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env python3
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
# qutebrowser is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# qutebrowser is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Check if docs changed and output an error if so."""
|
||||
|
||||
import sys
|
||||
import subprocess
|
||||
|
||||
code = subprocess.call(['git', '--no-pager', 'diff', '--exit-code', '--stat'])
|
||||
if code != 0:
|
||||
print()
|
||||
print('The autogenerated docs changed, please run this to update them:')
|
||||
print(' tox -e docs')
|
||||
print(' git commit -am "Update docs"')
|
||||
print()
|
||||
print('(Or you have uncommitted changes, in which case you can ignore '
|
||||
'this.)')
|
||||
sys.exit(code)
|
@ -34,19 +34,20 @@ import sys
|
||||
import subprocess
|
||||
import urllib
|
||||
import contextlib
|
||||
import time
|
||||
|
||||
try:
|
||||
import _winreg as winreg
|
||||
except ImportError:
|
||||
winreg = None
|
||||
|
||||
TESTENV = os.environ['TESTENV']
|
||||
TESTENV = os.environ.get('TESTENV', None)
|
||||
TRAVIS_OS = os.environ.get('TRAVIS_OS_NAME', None)
|
||||
INSTALL_PYQT = TESTENV in ('py34', 'py35', 'py34-cov', 'py35-cov',
|
||||
'unittests-nodisp', 'vulture', 'pylint')
|
||||
'unittests-nodisp', 'vulture', 'pylint', 'docs')
|
||||
XVFB = TRAVIS_OS == 'linux' and TESTENV == 'py34'
|
||||
pip_packages = ['tox']
|
||||
if TESTENV.endswith('-cov'):
|
||||
if TESTENV is not None and TESTENV.endswith('-cov'):
|
||||
pip_packages.append('codecov')
|
||||
|
||||
|
||||
@ -68,14 +69,15 @@ def folded_cmd(argv):
|
||||
subprocess.check_call(argv)
|
||||
|
||||
|
||||
def fix_sources_list():
|
||||
"""The mirror used by Travis has trouble a lot, so switch to another."""
|
||||
subprocess.check_call(['sudo', 'sed', '-i', r's/us-central1\.gce/us/',
|
||||
'/etc/apt/sources.list'])
|
||||
|
||||
|
||||
def apt_get(args):
|
||||
folded_cmd(['sudo', 'apt-get', '-y', '-q'] + args)
|
||||
try:
|
||||
folded_cmd(['sudo', 'apt-get', '-y', '-q'] + args)
|
||||
except subprocess.CalledProcessError:
|
||||
print()
|
||||
print("apt-get failed... trying a second time in 30s...")
|
||||
print()
|
||||
time.sleep(30)
|
||||
folded_cmd(['sudo', 'apt-get', '-y', '-q'] + args)
|
||||
|
||||
|
||||
def brew(args):
|
||||
@ -108,6 +110,7 @@ if 'APPVEYOR' in os.environ:
|
||||
print("Installing PyQt5...")
|
||||
subprocess.check_call([r'C:\install-PyQt5.exe', '/S'])
|
||||
|
||||
folded_cmd([r'C:\Python34\python', '-m', 'pip', 'install', '-U', 'pip'])
|
||||
folded_cmd([r'C:\Python34\Scripts\pip', 'install', '-U'] + pip_packages)
|
||||
|
||||
print("Linking Python...")
|
||||
@ -115,8 +118,11 @@ if 'APPVEYOR' in os.environ:
|
||||
f.write(r'@C:\Python34\python %*')
|
||||
|
||||
check_setup(r'C:\Python34\python')
|
||||
elif TRAVIS_OS == 'linux' and 'DOCKER' in os.environ:
|
||||
pass
|
||||
elif TRAVIS_OS == 'linux':
|
||||
folded_cmd(['sudo', 'pip', 'install'] + pip_packages)
|
||||
folded_cmd(['sudo', '-H', 'pip', 'install', '-U', 'pip'])
|
||||
folded_cmd(['sudo', '-H', 'pip', 'install', '-U'] + pip_packages)
|
||||
|
||||
pkgs = []
|
||||
|
||||
@ -126,20 +132,21 @@ elif TRAVIS_OS == 'linux':
|
||||
pkgs += ['python3-pyqt5', 'python3-pyqt5.qtwebkit']
|
||||
if TESTENV == 'eslint':
|
||||
pkgs += ['npm', 'nodejs', 'nodejs-legacy']
|
||||
if TESTENV == 'docs':
|
||||
pkgs += ['asciidoc']
|
||||
|
||||
if pkgs:
|
||||
fix_sources_list()
|
||||
apt_get(['update'])
|
||||
apt_get(['install'] + pkgs)
|
||||
apt_get(['install', '--no-install-recommends'] + pkgs)
|
||||
|
||||
if TESTENV == 'flake8':
|
||||
fix_sources_list()
|
||||
apt_get(['update'])
|
||||
# We need an up-to-date Python because of:
|
||||
# https://github.com/google/yapf/issues/46
|
||||
apt_get(['install', '-t', 'trusty-updates', 'python3.4'])
|
||||
|
||||
if TESTENV == 'eslint':
|
||||
folded_cmd(['sudo', 'npm', 'install', '-g', 'npm'])
|
||||
folded_cmd(['sudo', 'npm', 'install', '-g', 'eslint'])
|
||||
else:
|
||||
check_setup('python3')
|
15
scripts/dev/ci/travis_run.sh
Normal file
@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [[ $DOCKER ]]; then
|
||||
# To build a fresh image:
|
||||
# docker build -t img misc/docker/$DOCKER
|
||||
# docker run --privileged -v $PWD:/outside img
|
||||
|
||||
docker run --privileged -v $PWD:/outside \
|
||||
thecompiler/qutebrowser-manual:$DOCKER
|
||||
else
|
||||
args=()
|
||||
[[ $TESTENV == docs ]] && args=('--no-authors')
|
||||
|
||||
tox -e $TESTENV -- "${args[@]}"
|
||||
fi
|
@ -86,7 +86,9 @@ def get_build_exe_options(skip_html=False):
|
||||
'include_msvcr': True,
|
||||
'includes': [],
|
||||
'excludes': ['tkinter'],
|
||||
'packages': ['pygments', 'pkg_resources._vendor.packaging'],
|
||||
'packages': ['pygments', 'pkg_resources._vendor.packaging',
|
||||
'pkg_resources._vendor.pyparsing',
|
||||
'pkg_resources._vendor.six'],
|
||||
}
|
||||
|
||||
|
||||
|
@ -482,6 +482,19 @@ def regenerate_manpage(filename):
|
||||
_format_block(filename, 'options', options)
|
||||
|
||||
|
||||
def regenerate_cheatsheet():
|
||||
"""Generate cheatsheet PNGs based on the SVG."""
|
||||
files = [
|
||||
('doc/img/cheatsheet-small.png', 300, 185),
|
||||
('doc/img/cheatsheet-big.png', 3342, 2060),
|
||||
]
|
||||
|
||||
for filename, x, y in files:
|
||||
subprocess.check_call(['inkscape', '-e', filename, '-b', 'white',
|
||||
'-w', str(x), '-h', str(y),
|
||||
'misc/cheatsheet.svg'])
|
||||
|
||||
|
||||
def main():
|
||||
"""Regenerate all documentation."""
|
||||
utils.change_cwd()
|
||||
@ -491,8 +504,12 @@ def main():
|
||||
generate_settings('doc/help/settings.asciidoc')
|
||||
print("Generating command help...")
|
||||
generate_commands('doc/help/commands.asciidoc')
|
||||
print("Generating authors in README...")
|
||||
regenerate_authors('README.asciidoc')
|
||||
if '--no-authors' not in sys.argv:
|
||||
print("Generating authors in README...")
|
||||
regenerate_authors('README.asciidoc')
|
||||
if '--cheatsheet' in sys.argv:
|
||||
print("Regenerating cheatsheet .pngs")
|
||||
regenerate_cheatsheet()
|
||||
if '--html' in sys.argv:
|
||||
asciidoc2html.main()
|
||||
|
||||
|
@ -31,7 +31,8 @@ import os
|
||||
def get_latest_pdfjs_url():
|
||||
"""Get the URL of the latest pdf.js prebuilt package.
|
||||
|
||||
Returns a (version, url)-tuple."""
|
||||
Returns a (version, url)-tuple.
|
||||
"""
|
||||
github_api = 'https://api.github.com'
|
||||
endpoint = 'repos/mozilla/pdf.js/releases/latest'
|
||||
request_url = '{}/{}'.format(github_api, endpoint)
|
||||
@ -64,6 +65,8 @@ def update_pdfjs(target_version=None):
|
||||
url = ('https://github.com/mozilla/pdf.js/releases/download/'
|
||||
'v{0}/pdfjs-{0}-dist.zip').format(target_version)
|
||||
|
||||
os.chdir(os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||
'..', '..'))
|
||||
target_path = os.path.join('qutebrowser', '3rdparty', 'pdfjs')
|
||||
print("=> Downloading pdf.js {}".format(version))
|
||||
try:
|
||||
|
@ -21,7 +21,7 @@
|
||||
|
||||
"""Tool to import data from other browsers.
|
||||
|
||||
Currently only importing bookmarks from Chromium is supported.
|
||||
Currently only importing bookmarks from Netscape Bookmark files is supported.
|
||||
"""
|
||||
|
||||
|
||||
@ -30,34 +30,46 @@ import argparse
|
||||
|
||||
def main():
|
||||
args = get_args()
|
||||
if args.browser == 'chromium':
|
||||
import_chromium(args.bookmarks)
|
||||
if args.browser in ['chromium', 'firefox', 'ie']:
|
||||
import_netscape_bookmarks(args.bookmarks, args.bookmark_format)
|
||||
|
||||
|
||||
def get_args():
|
||||
"""Get the argparse parser."""
|
||||
parser = argparse.ArgumentParser(
|
||||
epilog="To import bookmarks from Chromium, export them to HTML in "
|
||||
"Chromium's bookmark manager.")
|
||||
parser.add_argument('browser', help="Which browser?", choices=['chromium'],
|
||||
epilog="To import bookmarks from Chromium, Firefox or IE, "
|
||||
"export them to HTML in your browsers bookmark manager. "
|
||||
"By default, this script will output in a quickmarks format.")
|
||||
parser.add_argument('browser', help="Which browser? (chromium, firefox)",
|
||||
choices=['chromium', 'firefox', 'ie'],
|
||||
metavar='browser')
|
||||
parser.add_argument('bookmarks', help="Bookmarks file")
|
||||
parser.add_argument('-b', help="Output in bookmark format.",
|
||||
dest='bookmark_format', action='store_true',
|
||||
default=False, required=False)
|
||||
parser.add_argument('bookmarks', help="Bookmarks file (html format)")
|
||||
args = parser.parse_args()
|
||||
return args
|
||||
|
||||
|
||||
def import_chromium(bookmarks_file):
|
||||
"""Import bookmarks from a HTML file generated by Chromium."""
|
||||
def import_netscape_bookmarks(bookmarks_file, is_bookmark_format):
|
||||
"""Import bookmarks from a NETSCAPE-Bookmark-file v1.
|
||||
|
||||
Generated by Chromium, Firefox, IE and possibly more browsers
|
||||
"""
|
||||
import bs4
|
||||
with open(bookmarks_file, encoding='utf-8') as f:
|
||||
soup = bs4.BeautifulSoup(f, 'html.parser')
|
||||
|
||||
html_tags = soup.findAll('a')
|
||||
if is_bookmark_format:
|
||||
output_template = '{tag[href]} {tag.string}'
|
||||
else:
|
||||
output_template = '{tag.string} {tag[href]}'
|
||||
|
||||
bookmarks = []
|
||||
for tag in html_tags:
|
||||
if tag['href'] not in bookmarks:
|
||||
bookmarks.append('{tag.string} {tag[href]}'.format(tag=tag))
|
||||
bookmarks.append(output_template.format(tag=tag))
|
||||
|
||||
for bookmark in bookmarks:
|
||||
print(bookmark)
|
||||
|
13
scripts/testbrowser_cpp/main.cpp
Normal file
@ -0,0 +1,13 @@
|
||||
#include <QApplication>
|
||||
#include <QWebView>
|
||||
#include <QUrl>
|
||||
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
QApplication app(argc, argv);
|
||||
QWebView view;
|
||||
view.load(QUrl(argv[1]));
|
||||
view.show();
|
||||
return app.exec();
|
||||
}
|
6
scripts/testbrowser_cpp/testbrowser.pro
Normal file
@ -0,0 +1,6 @@
|
||||
QT += core widgets webkit webkitwidgets
|
||||
|
||||
TARGET = testbrowser
|
||||
TEMPLATE = app
|
||||
|
||||
SOURCES += main.cpp
|
19
setup.cfg
@ -22,7 +22,6 @@ exclude = .venv,.hypothesis,.git,__pycache__,resources.py
|
||||
# P101: format string does contain unindexed parameters
|
||||
# P102: docstring does contain unindexed parameters
|
||||
# P103: other string does contain unindexed parameters
|
||||
# D001: found assert_ replace it with assertTrue
|
||||
# D102: Missing docstring in public method (will be handled by others)
|
||||
# D103: Missing docstring in public function (will be handled by others)
|
||||
# D104: Missing docstring in public package (will be handled by others)
|
||||
@ -31,27 +30,29 @@ exclude = .venv,.hypothesis,.git,__pycache__,resources.py
|
||||
# D211: No blank lines allowed before class docstring
|
||||
# (PEP257 got changed, but let's stick to the old standard)
|
||||
# D402: First line should not be function's signature (false-positives)
|
||||
# FI10 - FI15: __future__ import missing
|
||||
# H201: bare except
|
||||
# H238: Use new-stule classes
|
||||
# H301: one import per line
|
||||
# H306: imports not in alphabetical order
|
||||
ignore =
|
||||
E128,E226,E265,E501,E402,E266,
|
||||
F401,
|
||||
N802,
|
||||
L101,L102,L103,L201,L202,L203,L204,L207,L302,
|
||||
P101,P102,P103,
|
||||
D001,
|
||||
D102,D103,D104,D105,D209,D211,D402
|
||||
D102,D103,D104,D105,D209,D211,D402,
|
||||
FI10,FI11,FI12,FI13,FI14,FI15,
|
||||
H201,H238,H301,H306
|
||||
max-complexity = 12
|
||||
putty-auto-ignore = True
|
||||
putty-ignore =
|
||||
/# pylint: disable=invalid-name/ : +N801,N806
|
||||
/# pylint: disable=wildcard-import/ : +F403
|
||||
/# pragma: no mccabe/ : +C901
|
||||
/# flake8: disable=E131/ : +E131
|
||||
/# flake8: disable=N803/ : +N803
|
||||
/# flake8: disable=T002/ : +T002
|
||||
/# flake8: disable=F841/ : +F841
|
||||
/# flake8: disable=S001/ : +S001
|
||||
tests/*/*/test_*.py : +D100,D101,D401
|
||||
tests/*/test_*.py : +D100,D101,D401
|
||||
tests/unit/browser/http/test_content_disposition.py : +D400
|
||||
scripts/dev/ci/install.py : +C901,FI53
|
||||
copyright-check = True
|
||||
copyright-regexp = # Copyright [\d-]+ .*
|
||||
copyright-min-file-size = 110
|
||||
|
@ -54,7 +54,6 @@ def _apply_platform_markers(item):
|
||||
"Can't be run when frozen"),
|
||||
('frozen', not getattr(sys, 'frozen', False),
|
||||
"Can only run when frozen"),
|
||||
('skip', True, "Always skipped."),
|
||||
('pyqt531_or_newer', PYQT_VERSION < 0x050301,
|
||||
"Needs PyQt 5.3.1 or newer"),
|
||||
('ci', 'CI' not in os.environ, "Only runs on CI."),
|
||||
@ -101,13 +100,6 @@ def pytest_collection_modifyitems(items):
|
||||
for item in items:
|
||||
if 'qapp' in getattr(item, 'fixturenames', ()):
|
||||
item.add_marker('gui')
|
||||
if sys.platform == 'linux' and not os.environ.get('DISPLAY', ''):
|
||||
if ('CI' in os.environ and
|
||||
not os.environ.get('QUTE_NO_DISPLAY', '')):
|
||||
raise Exception("No display available on CI!")
|
||||
skip_marker = pytest.mark.skipif(
|
||||
True, reason="No DISPLAY available")
|
||||
item.add_marker(skip_marker)
|
||||
|
||||
if hasattr(item, 'module'):
|
||||
module_path = os.path.relpath(
|
||||
@ -152,12 +144,15 @@ def pytest_addoption(parser):
|
||||
|
||||
|
||||
@pytest.fixture(scope='session', autouse=True)
|
||||
def prevent_xvfb_on_buildbot(request):
|
||||
def check_display(request):
|
||||
if (not request.config.getoption('--no-xvfb') and
|
||||
'QUTE_BUILDBOT' in os.environ and
|
||||
request.config.xvfb is not None):
|
||||
raise Exception("Xvfb is running on buildbot!")
|
||||
|
||||
if sys.platform == 'linux' and not os.environ.get('DISPLAY', ''):
|
||||
raise Exception("No display and no Xvfb available!")
|
||||
|
||||
|
||||
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
|
||||
def pytest_runtest_makereport(item, call):
|
||||
|
@ -38,6 +38,10 @@ from qutebrowser.utils import objreg
|
||||
from PyQt5.QtCore import QEvent, QSize, Qt
|
||||
from PyQt5.QtWidgets import QWidget, QHBoxLayout, QVBoxLayout
|
||||
from PyQt5.QtNetwork import QNetworkCookieJar
|
||||
try:
|
||||
from PyQt5 import QtWebEngineWidgets
|
||||
except ImportError as e:
|
||||
QtWebEngineWidgets = None
|
||||
|
||||
|
||||
class WinRegistryHelper:
|
||||
@ -213,6 +217,14 @@ def qnam(qapp):
|
||||
return nam
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def webengineview():
|
||||
"""Get a QWebEngineView if QtWebEngine is available."""
|
||||
if QtWebEngineWidgets is None:
|
||||
pytest.skip("QtWebEngine unavailable")
|
||||
return QtWebEngineWidgets.QWebEngineView()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def webpage(qnam):
|
||||
"""Get a new QWebPage object."""
|
||||
|
@ -22,6 +22,7 @@
|
||||
|
||||
import re
|
||||
import pprint
|
||||
import os.path
|
||||
|
||||
|
||||
def print_i(text, indent, error=False):
|
||||
@ -101,3 +102,13 @@ def pattern_match(*, pattern, value):
|
||||
"""
|
||||
re_pattern = '.*'.join(re.escape(part) for part in pattern.split('*'))
|
||||
return re.fullmatch(re_pattern, value) is not None
|
||||
|
||||
|
||||
def abs_datapath():
|
||||
"""Get the absolute path to the integration data directory.
|
||||
|
||||
Return:
|
||||
The absolute path to the tests/integration/data directory.
|
||||
"""
|
||||
file_abs = os.path.abspath(os.path.dirname(__file__))
|
||||
return os.path.join(file_abs, '..', 'integration', 'data')
|
||||
|
11
tests/integration/data/click_element.html
Normal file
@ -0,0 +1,11 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>quteprocess.click_element test</title>
|
||||
</head>
|
||||
<body>
|
||||
<span onclick='console.log("click_element clicked")'>Test Element</span>
|
||||
<span onclick='console.log("click_element special chars")'>"Don't", he shouted</span>
|
||||
<span>Duplicate</span>
|
||||
<span>Duplicate</span>
|
||||
</body>
|
||||
</html>
|
10
tests/integration/data/downloads/issue1214.html
Normal file
@ -0,0 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Wrong filename when using data: links</title>
|
||||
</head>
|
||||
<body>
|
||||
<a href="data:;base64,cXV0ZWJyb3dzZXI=">download</a>
|
||||
</body>
|
||||
</html>
|
BIN
tests/integration/data/downloads/ä-issue908.bin
Normal file
10
tests/integration/data/hints/link_blank.html
Normal file
@ -0,0 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>A link to use hints on</title>
|
||||
</head>
|
||||
<body>
|
||||
<a href="/data/hello.txt" target="_blank">Follow me!</a>
|
||||
</body>
|
||||
</html>
|
10
tests/integration/data/hints/link_span.html
Normal file
@ -0,0 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>A link to use hints on</title>
|
||||
</head>
|
||||
<body>
|
||||
<a href="/data/hello.txt" target="_blank"><span style="font-size: large">Follow me!</span></a>
|
||||
</body>
|
||||
</html>
|
16
tests/integration/data/sessions/history_replace_state.html
Normal file
@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Test title</title>
|
||||
<script type="text/javascript">
|
||||
window.onload = function () {
|
||||
console.log("Calling history.replaceState");
|
||||
history.replaceState({}, '', window.location + '?state=2');
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
This page calls history.replaceState() via JS.
|
||||
</body>
|
||||
</html>
|