diff --git a/.flake8 b/.flake8 index 0265c969b..7bfc34c0a 100644 --- a/.flake8 +++ b/.flake8 @@ -6,6 +6,7 @@ exclude = .*,__pycache__,resources.py # E501: Line too long # E402: module level import not at top of file # E266: too many leading '#' for block comment +# E722: do not use bare except # E731: do not assign a lambda expression, use a def # (for pytest's __tracebackhide__) # F401: Unused import @@ -24,7 +25,7 @@ exclude = .*,__pycache__,resources.py # D403: First word of the first line should be properly capitalized # (false-positives) ignore = - E128,E226,E265,E501,E402,E266,E731, + E128,E226,E265,E501,E402,E266,E722,E731, F401, N802, P101,P102,P103, diff --git a/.travis.yml b/.travis.yml index 58e7f09d5..9a82a2b7c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,15 +13,30 @@ matrix: env: DOCKER=archlinux services: docker - os: linux - env: DOCKER=archlinux QUTE_BDD_WEBENGINE=true + env: DOCKER=archlinux-webengine QUTE_BDD_WEBENGINE=true + services: docker + - os: linux + env: DOCKER=archlinux-ng services: docker - os: linux env: DOCKER=ubuntu-xenial services: docker + - os: linux + language: python + python: 3.6 + env: TESTENV=py36-pyqt571 + - os: linux + language: python + python: 3.5 + env: TESTENV=py35-pyqt58 + - os: linux + language: python + python: 3.6 + env: TESTENV=py36-pyqt58 - os: osx - env: TESTENV=py35 OSX=elcapitan + env: TESTENV=py36 OSX=elcapitan osx_image: xcode7.3 - # https://github.com/The-Compiler/qutebrowser/issues/2013 + # https://github.com/qutebrowser/qutebrowser/issues/2013 # - os: osx # env: TESTENV=py35 OSX=yosemite # osx_image: xcode6.4 @@ -43,14 +58,14 @@ matrix: env: TESTENV=eslint allow_failures: - os: osx - env: TESTENV=py35 OSX=elcapitan + env: TESTENV=py36 OSX=elcapitan osx_image: xcode7.3 fast_finish: true cache: directories: - $HOME/.cache/pip - - $HOME/build/The-Compiler/qutebrowser/.cache + - $HOME/build/qutebrowser/qutebrowser/.cache before_install: # We need to do this so we pick up the system-wide python properly @@ -58,6 +73,7 @@ before_install: install: - bash scripts/dev/ci/travis_install.sh + - ulimit -c unlimited script: - bash scripts/dev/ci/travis_run.sh @@ -65,6 +81,9 @@ script: after_success: - '[[ $TESTENV == *-cov ]] && codecov -e TESTENV -X gcov' +after_failure: + - bash scripts/dev/ci/travis_backtrace.sh + notifications: webhooks: - https://buildtimetrend.herokuapp.com/travis diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index d0e412ced..3831c9e42 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -14,12 +14,131 @@ 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.9.0 (unreleased) -------------------- +v0.11.0 (unreleased) +-------------------- Added ~~~~~ +- New `:clear-messages` command to clear shown messages. + +Changed +~~~~~~~ + +- When using QtWebEngine, the underlying Chromium version is now shown in the + version info. + +Fixed +~~~~~ + +- Added a workaround for a black screen with QtWebEngine with some setups + (requires PyOpenGL to be installed) + +v0.10.1 +------- + +Changed +~~~~~~~ + +- `--qt-arg` and `--qt-flag` can now also be used to pass arguments to Chromium when using QtWebEngine. + +Fixed +~~~~~ + +- URLs are now redacted properly (username/password, and path/query for HTTPS) when using Proxy Autoconfig with QtWebKit +- Crash when updating adblock lists with invalid UTF8-chars in them +- Fixed the web inspector with QtWebEngine +- Version checks when starting qutebrowser now also take the Qt version PyQt was compiled against into account +- Hinting a input now doesn't select existing text anymore with QtWebKit +- The cursor now moves to the end when input elements are selected with QtWebEngine +- Download suffixes like (1) are now correctly stripped with QtWebEngine +- Crash when trying to print a tab which was closed in the meantime +- Crash when trying to open a file twice on Windows + +v0.10.0 +------- + +Added +~~~~~ + +- Userscripts now have a new `$QUTE_COMMANDLINE_TEXT` environment variable, containing the current commandline contents +- New `ripbang` userscript to create a searchengine from a duckduckgo bang +- link:https://github.com/annulen/webkit/wiki[QtWebKit Reloaded] (also called QtWebKit-NG) is now fully supported +- Various new functionality with the QtWebEngine backend: + * Printing support with Qt >= 5.8 + * Proxy support with Qt >= 5.8 + * The `general -> print-element-backgrounds` option with Qt >= 5.8 + * The `content -> cookies-store` option + * The `storage -> cache-size` option + * The `colors -> webpage.bg` option + * The HTML5 fullscreen API (e.g. youtube videos) with QtWebEngine + * `:download --mhtml` +- New `qute:history` URL and `:history` command to show the browsing history +- Open tabs are now auto-saved on each successful load and restored in case of a crash +- `:jseval` now has a `--file` flag so you can pass a javascript file +- `:session-save` now has a `--only-active-window` flag to only save the active window +- OS X builds are back, and built with QtWebEngine + +Changed +~~~~~~~ + +- PyQt 5.7/Qt 5.7.1 is now required for the QtWebEngine backend +- Scrolling with the scrollwheel while holding shift now scrolls sideways +- New way of clicking hints which solves various small issues +- When yanking a mailto: link via hints, the mailto: prefix is now stripped +- Zoom level messages are now not stacked on top of each other anymore +- qutebrowser now automatically uses QtWebEngine if QtWebKit is unavailable +- :history-clear now asks for a confirmation, unless it's run with --force. +- `input -> mouse-zoom-divider` can now be 0 to disable zooming by mouse wheel +- `network -> proxy` can also be set to `pac+file://...` now to + use a local proxy autoconfig file (on QtWebKit) + +Fixed +~~~~~ + +- Various bugs with Qt 5.8 and QtWebEngine: + * Segfault when closing a window + * Segfault when closing a tab with a search active + * Fixed various mouse actions (like automatically entering insert mode) not working + * Fixed hints sometimes not working + * Segfault when opening a URL after a QtWebEngine renderer process crash +- Other QtWebEngine fixes: + * Insert mode now gets entered correctly with a non-100% zoom + * Crash reports are now re-enabled when using QtWebEngine + * Fixed crashes when closing tabs while hinting + * Using :undo or :tab-clone with a view-source:// or chrome:// tab is now prevented, as it segfaults +- `:enter-mode` now refuses to enter modes which can't be entered manually (which caused crashes) +- `:record-macro` (`q`) now doesn't try to record macros for special keys without a text +- Fixed PAC (proxy autoconfig) not working with QtWebKit +- `:download --mhtml` now uses the new file dialog +- Word hints are now upper-cased correctly when hints -> uppercase is true +- Font validation is now more permissive in the config, allowing e.g. "Terminus + (TTF)" as font name +- Fixed starting on newer PyQt/sip versions with LibreSSL +- When downloading files with QtWebKit, a User-Agent header is set when possible +- Fixed showing of keybindings in the :help completion +- `:navigate prev/next` now detects `rel` attributes on `` elements, and + handles multiple `rel` attributes correctly +- Fixed a crash when hinting with target `userscript` and spawning a non-existing script +- Lines in Jupyter notebook now trigger insert mode + +v0.9.1 +------ + +Fixed +~~~~~ + +- Prevent websites from downloading files to a location outside of the download + folder with QtWebEngine. + +v0.9.0 +------ + +Added +~~~~~ + +- *New dependency:* qutebrowser now depends on the Qt QML module, which is + packaged separately in some distributions (as Qt Declarative/QML/Quick). - New `:rl-backward-kill-word` command which does what `:rl-unix-word-rubout` did before v0.8.0. - New `:rl-unix-filename-rubout` command which is similar to readline's @@ -52,6 +171,9 @@ Added - New `:record-macro` (`q`) and `:run-macro` (`@`) commands for keyboard macros. - New `ui -> hide-scrollbar` setting to hide the scrollbar independently of the `user-stylesheet` setting. +- New `general -> default-open-dispatcher` setting to configure what to open + downloads with (instead of e.g. `xdg-open` on Linux). +- Support for PAC (proxy autoconfig) with QtWebKit Changed ~~~~~~~ @@ -149,6 +271,8 @@ Changed - `ui -> window-title-format` now has a new `{backend} ` replacement - `:hint` has a new `--add-history` argument to add the URL to the history for yank/spawn targets. +- `:set` now cycles through values if more than one argument is given. +- `:open` now opens `default-page` without an URL even without `-t`/`-b`/`-w` given. Deprecated ~~~~~~~~~~ @@ -186,6 +310,10 @@ Fixed - `:tab-detach` now fails correctly when there's only one tab open. - Various small issues with the command completion - Fixed hang when using multiple spaces in a row with the URL completion +- qutebrowser now still starts with an incorrectly configured + `$XDG_RUNTIME_DIR`. +- Fixed crash when a userscript writes invalid unicode data to the FIFO +- Fixed crash when a included HTML was not found v0.8.3 ------ @@ -794,7 +922,7 @@ Fixed - Fixed horrible completion performance when the `shrink` option was set. - Sessions now store zoom/scroll-position correctly. -https://github.com/The-Compiler/qutebrowser/releases/tag/v0.2.1[v0.2.1] +https://github.com/qutebrowser/qutebrowser/releases/tag/v0.2.1[v0.2.1] ----------------------------------------------------------------------- Fixed @@ -802,7 +930,7 @@ Fixed - Added missing manpage (doc/qutebrowser.1.asciidoc) to archive. -https://github.com/The-Compiler/qutebrowser/releases/tag/v0.2.0[v0.2.0] +https://github.com/qutebrowser/qutebrowser/releases/tag/v0.2.0[v0.2.0] ----------------------------------------------------------------------- Added @@ -945,7 +1073,7 @@ Fixed - Add a timeout to pastebin HTTP replies. - Various other fixes for small/rare bugs. -https://github.com/The-Compiler/qutebrowser/releases/tag/v0.1.4[v0.1.4] +https://github.com/qutebrowser/qutebrowser/releases/tag/v0.1.4[v0.1.4] ----------------------------------------------------------------------- Changed @@ -989,7 +1117,7 @@ Security * Stop the icon database from being created when private-browsing is set to true. * Disable insecure SSL ciphers. -https://github.com/The-Compiler/qutebrowser/releases/tag/v0.1.3[v0.1.3] +https://github.com/qutebrowser/qutebrowser/releases/tag/v0.1.3[v0.1.3] ----------------------------------------------------------------------- Changed @@ -1023,7 +1151,7 @@ Security * Fix for HTTP passwords accidentally being written to debug log. -https://github.com/The-Compiler/qutebrowser/releases/tag/v0.1.2[v0.1.2] +https://github.com/qutebrowser/qutebrowser/releases/tag/v0.1.2[v0.1.2] ----------------------------------------------------------------------- Changed @@ -1055,7 +1183,7 @@ Fixed * Fix user-stylesheet setting with an empty value. -https://github.com/The-Compiler/qutebrowser/releases/tag/v0.1.1[v0.1.1] +https://github.com/qutebrowser/qutebrowser/releases/tag/v0.1.1[v0.1.1] ----------------------------------------------------------------------- Added @@ -1113,7 +1241,7 @@ Fixed * Ensure the docs get included in `freeze.py`. * Fix crash with `:zoom`. -https://github.com/The-Compiler/qutebrowser/releases/tag/v0.1[v0.1] +https://github.com/qutebrowser/qutebrowser/releases/tag/v0.1[v0.1] ------------------------------------------------------------------- Initial release. diff --git a/CONTRIBUTING.asciidoc b/CONTRIBUTING.asciidoc index 05ec6df67..12d52fee7 100644 --- a/CONTRIBUTING.asciidoc +++ b/CONTRIBUTING.asciidoc @@ -34,12 +34,12 @@ this. It might be a good idea to ask on the mailing list or IRC channel to make sure nobody else started working on the same thing already. If you want to find something useful to do, check the -https://github.com/The-Compiler/qutebrowser/issues[issue tracker]. Some +https://github.com/qutebrowser/qutebrowser/issues[issue tracker]. Some pointers: -* https://github.com/The-Compiler/qutebrowser/labels/easy[Issues which should +* https://github.com/qutebrowser/qutebrowser/labels/easy[Issues which should be easy to solve] -* https://github.com/The-Compiler/qutebrowser/labels/not%20code[Issues which +* https://github.com/qutebrowser/qutebrowser/labels/not%20code[Issues which require little/no coding] There are also some things to do if you don't want to write code: @@ -55,7 +55,7 @@ qutebrowser uses http://git-scm.com/[git] for its development. You can clone the repo like this: ---- -git clone https://github.com/The-Compiler/qutebrowser.git +git clone https://github.com/qutebrowser/qutebrowser.git ---- If you don't know git, a http://git-scm.com/[git cheatsheet] might come in @@ -541,6 +541,12 @@ Setting up a Windows Development Environment Note that the `flake8` tox env might not run due to encoding errors despite having LANG/LC_* set correctly. +Rebuilding the website +~~~~~~~~~~~~~~~~~~~~~~ + +If you want to rebuild the website, run `./scripts/asciidoc2html.py --website `. + + Style conventions ----------------- @@ -629,7 +635,7 @@ and make sure all bugs marked as resolved are actually fixed. * Grep for `WORKAROUND` in the code and test if fixed stuff works without the workaround. * Check relevant -https://github.com/The-Compiler/qutebrowser/issues?q=is%3Aopen+is%3Aissue+label%3Aqt[qutebrowser +https://github.com/qutebrowser/qutebrowser/issues?q=is%3Aopen+is%3Aissue+label%3Aqt[qutebrowser bugs] and check if they're fixed. New PyQt release @@ -638,6 +644,7 @@ New PyQt release * See above * Install new PyQt in Windows VM (32- and 64-bit) * Download new installer and update PyQt installer path in `ci_install.py`. +* Update `tox.ini`/`.travis.yml`/`.appveyor.yml` to test new versions qutebrowser release ~~~~~~~~~~~~~~~~~~~ @@ -659,7 +666,7 @@ qutebrowser release * `git push origin`; `git push origin v0.$x.$y` * If committing on minor branch, cherry-pick release commit to master. * Create release on github -* Mark the milestone at https://github.com/The-Compiler/qutebrowser/milestones +* Mark the milestone at https://github.com/qutebrowser/qutebrowser/milestones as closed. * Linux: Run `python3 scripts/dev/build_release.py --upload v0.$x.$y` diff --git a/FAQ.asciidoc b/FAQ.asciidoc index 293f76216..71d5f2834 100644 --- a/FAQ.asciidoc +++ b/FAQ.asciidoc @@ -105,13 +105,25 @@ It also works nicely with rapid hints: How do I use qutebrowser with mutt?:: Due to a Qt limitation, local files without `.html` extensions are "downloaded" instead of displayed, see - https://github.com/The-Compiler/qutebrowser/issues/566[#566]. You can work + https://github.com/qutebrowser/qutebrowser/issues/566[#566]. You can work around this by using this in your `mailcap`: + ---- text/html; mv %s %s.html && qutebrowser %s.html >/dev/null 2>/dev/null; needsterminal; ---- +What is the difference between bookmarks and quickmarks?:: + Bookmarks will always use the title of the website as their name, but with quickmarks + you can set your own title. ++ +For example, if you bookmark multiple food recipe websites and use `:open`, +you have to type the title or address of the website. ++ +When using quickmark, you can give them all names, like +`foodrecipes1`, `foodrecipes2` and so on. When you type +`:open foodrecipes`, you will see a list of all the food recipe sites, +without having to remember the exact website title or address. + == Troubleshooting Configuration not saved after modifying config.:: @@ -129,7 +141,7 @@ Experiencing freezing on sites like duckduckgo and youtube.:: This issue could be caused by stale plugin files installed by `mozplugger` if mozplugger was subsequently removed. Try exiting qutebrowser and removing `~/.mozilla/plugins/mozplugger*.so`. - See https://github.com/The-Compiler/qutebrowser/issues/357[Issue #357] + See https://github.com/qutebrowser/qutebrowser/issues/357[Issue #357] for more details. Experiencing segfaults (crashes) on Debian systems.:: @@ -143,7 +155,7 @@ Segfaults on Facebook, Medium, Amazon, ...:: visiting these sites. This is caused by various bugs in Qt which have been fixed in Qt 5.4. However Debian and Ubuntu are slow to adopt or upgrade some packages. On Debian Jessie, it's recommended to use the experimental - repos as described in https://github.com/The-Compiler/qutebrowser/blob/master/INSTALL.asciidoc#on-debian--ubuntu[INSTALL]. + repos as described in https://github.com/qutebrowser/qutebrowser/blob/master/INSTALL.asciidoc#on-debian--ubuntu[INSTALL]. + Since Ubuntu Trusty (using Qt 5.2.1), https://bugreports.qt.io/browse/QTBUG-42417?jql=component%20%3D%20WebKit%20and%20resolution%20%3D%20Done%20and%20fixVersion%20in%20(5.3.0%2C%20%225.3.0%20Alpha%22%2C%20%225.3.0%20Beta1%22%2C%20%225.3.0%20RC1%22%2C%205.3.1%2C%205.3.2%2C%205.4.0%2C%20%225.4.0%20Alpha%22%2C%20%225.4.0%20Beta%22%2C%20%225.4.0%20RC%22)%20and%20priority%20in%20(%22P2%3A%20Important%22%2C%20%22P1%3A%20Critical%22%2C%20%22P0%3A%20Blocker%22)[over @@ -154,7 +166,7 @@ https://bugreports.qt.io/browse/QTBUG-42417?jql=component%20%3D%20WebKit%20and%2 My issue is not listed.:: If you experience any segfaults or crashes, you can report the issue in - https://github.com/The-Compiler/qutebrowser/issues[the issue tracker] or + https://github.com/qutebrowser/qutebrowser/issues[the issue tracker] or using the `:report` command. If you are reporting a segfault, make sure you read the link:doc/stacktrace.asciidoc[guide] on how to report them with all needed diff --git a/INSTALL.asciidoc b/INSTALL.asciidoc index f5ab70daa..21bf4176b 100644 --- a/INSTALL.asciidoc +++ b/INSTALL.asciidoc @@ -21,11 +21,11 @@ Using the packages Install the dependencies via apt-get: ---- -# apt-get install python3-lxml python-tox python3-pyqt5 python3-pyqt5.qtwebkit python3-sip python3-jinja2 python3-pygments python3-yaml +# apt-get install python3-lxml python-tox python3-pyqt5 python3-pyqt5.qtwebkit python3-pyqt5.qtquick python3-sip python3-jinja2 python3-pygments python3-yaml ---- Get the qutebrowser package from the -https://github.com/The-Compiler/qutebrowser/releases[release page] and download +https://github.com/qutebrowser/qutebrowser/releases[release page] and download the https://qutebrowser.org/python3-pypeg2_2.15.2-1_all.deb[PyPEG2 package]. Install the packages: @@ -56,7 +56,7 @@ Then install the packages like this: ---- # apt-get update -# apt-get install -t experimental python3-pyqt5 python3-pyqt5.qtwebkit python3-sip python3-dev +# apt-get install -t experimental python3-pyqt5 python3-pyqt5.qtwebkit python3-pyqt5.qtquick python3-sip python3-dev # apt-get install python-tox ---- @@ -74,7 +74,7 @@ For distributions other than Debian or if you prefer to not use the experimental repo: ---- -# apt-get install python3-pyqt5 python3-pyqt5.qtwebkit python-tox python3-sip python3-dev +# apt-get install python3-pyqt5 python3-pyqt5.qtwebkit python3-pyqt5.qtquick python-tox python3-sip python3-dev ---- To generate the documentation for the `:help` command, when using the git @@ -214,7 +214,7 @@ Prebuilt binaries ~~~~~~~~~~~~~~~~~ Prebuilt standalone packages and MSI installers -https://github.com/The-Compiler/qutebrowser/releases[are built] for every +https://github.com/qutebrowser/qutebrowser/releases[are built] for every release. https://chocolatey.org/packages/qutebrowser[Chocolatey package] @@ -254,7 +254,7 @@ Prebuilt binary The easiest way to install qutebrowser on OS X is to use the prebuilt `.app` files from the -https://github.com/The-Compiler/qutebrowser/releases[release page]. +https://github.com/qutebrowser/qutebrowser/releases[release page]. This binary is also available through the https://caskroom.github.io/[Homebrew Cask] package manager: @@ -272,29 +272,21 @@ qutebrowser from source. ==== Homebrew -Homebrew's builds of Qt and PyQt no longer include QtWebKit, so it is necessary -to build from source. The build takes several hours on an average laptop. +---- +$ brew install qt5 +$ pip3 install qutebrowser +---- + +Homebrew's builds of Qt and PyQt no longer include QtWebKit - if you need +QtWebKit support, it is necessary to build from source. The build takes several +hours on an average laptop. ---- $ brew install qt5 --with-qtwebkit $ brew install -s pyqt5 - -$ pip3.5 install qutebrowser +$ pip3 install qutebrowser ---- -==== MacPorts - -For MacPorts, run: - ----- -$ sudo port install python34 py34-jinja2 asciidoc py34-pygments py34-pyqt5 -$ sudo pip3.4 install qutebrowser ----- - -The preferences for qutebrowser are stored in -`~/Library/Preferences/qutebrowser`, the application data is stored in -`~/Library/Application Support/qutebrowser`. - Packagers --------- @@ -313,7 +305,7 @@ 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 +$ git clone https://github.com/qutebrowser/qutebrowser.git $ cd qutebrowser ---- diff --git a/README.asciidoc b/README.asciidoc index fea1101b6..eae5a07c8 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -1,26 +1,26 @@ // If you are reading this in plaintext or on PyPi: // // A rendered version is available at: -// https://github.com/The-Compiler/qutebrowser/blob/master/README.asciidoc +// https://github.com/qutebrowser/qutebrowser/blob/master/README.asciidoc qutebrowser =========== // QUTE_WEB_HIDE -image:icons/qutebrowser-64x64.png[qutebrowser logo] *A keyboard-driven, vim-like browser based on PyQt5 and QtWebKit.* +image:icons/qutebrowser-64x64.png[qutebrowser logo] *A keyboard-driven, vim-like browser based on PyQt5 and Qt.* -image:https://img.shields.io/pypi/l/qutebrowser.svg?style=flat["license badge",link="https://github.com/The-Compiler/qutebrowser/blob/master/COPYING"] +image:https://img.shields.io/pypi/l/qutebrowser.svg?style=flat["license badge",link="https://github.com/qutebrowser/qutebrowser/blob/master/COPYING"] image:https://img.shields.io/pypi/v/qutebrowser.svg?style=flat["version badge",link="https://pypi.python.org/pypi/qutebrowser/"] -image:https://requires.io/github/The-Compiler/qutebrowser/requirements.svg?branch=master["requirements badge",link="https://requires.io/github/The-Compiler/qutebrowser/requirements/?branch=master"] -image:https://travis-ci.org/The-Compiler/qutebrowser.svg?branch=master["Build Status", link="https://travis-ci.org/The-Compiler/qutebrowser"] -image:https://ci.appveyor.com/api/projects/status/9gmnuip6i1oq7046?svg=true["AppVeyor build status", link="https://ci.appveyor.com/project/The-Compiler/qutebrowser"] -image:https://codecov.io/github/The-Compiler/qutebrowser/coverage.svg?branch=master["coverage badge",link="https://codecov.io/github/The-Compiler/qutebrowser?branch=master"] +image:https://requires.io/github/qutebrowser/qutebrowser/requirements.svg?branch=master["requirements badge",link="https://requires.io/github/qutebrowser/qutebrowser/requirements/?branch=master"] +image:https://travis-ci.org/qutebrowser/qutebrowser.svg?branch=master["Build Status", link="https://travis-ci.org/qutebrowser/qutebrowser"] +image:https://ci.appveyor.com/api/projects/status/5pyauww2k68bbow2/branch/master?svg=true["AppVeyor build status", link="https://ci.appveyor.com/project/qutebrowser/qutebrowser"] +image:https://codecov.io/github/qutebrowser/qutebrowser/coverage.svg?branch=master["coverage badge",link="https://codecov.io/github/qutebrowser/qutebrowser?branch=master"] -link:https://www.qutebrowser.org[website] | link:https://blog.qutebrowser.org[blog] | link:https://github.com/The-Compiler/qutebrowser/releases[releases] +link:https://www.qutebrowser.org[website] | link:https://blog.qutebrowser.org[blog] | link:https://github.com/qutebrowser/qutebrowser/releases[releases] // QUTE_WEB_HIDE_END qutebrowser is a keyboard-focused browser with a minimal GUI. It's based -on Python, PyQt5 and QtWebKit and free software, licensed under the GPL. +on Python and PyQt5 and free software, licensed under the GPL. It was inspired by other browsers/addons like dwb and Vimperator/Pentadactyl. @@ -35,7 +35,7 @@ image:doc/img/hints.png["screenshot 4",width=300,link="doc/img/hints.png"] Downloads --------- -See the https://github.com/The-Compiler/qutebrowser/releases[github releases +See the https://github.com/qutebrowser/qutebrowser/releases[github releases page] for available downloads (currently a source archive, and standalone packages as well as MSI installers for Windows). @@ -99,7 +99,7 @@ The following software and libraries are required to run qutebrowser: * http://www.python.org/[Python] 3.4 or newer * http://qt.io/[Qt] 5.2.0 or newer (5.5.1 recommended) -* QtWebKit +* QtWebKit (old or link:https://github.com/annulen/webkit/wiki[reloaded]/NG) or QtWebEngine * http://www.riverbankcomputing.com/software/pyqt/intro[PyQt] 5.2.0 or newer (5.5.1 recommended) for Python 3 * https://pypi.python.org/pypi/setuptools/[pkg_resources/setuptools] @@ -146,8 +146,8 @@ Contributors, sorted by the number of commits in descending order: * Florian Bruhin * Daniel Schadt * Ryan Roden-Corrent -* Jakub Klinkovský * Jan Verbeek +* Jakub Klinkovský * Antoni Boucher * Lamar Pavel * Marshall Lochbaum @@ -165,14 +165,17 @@ Contributors, sorted by the number of commits in descending order: * Corentin Julé * meles5 * Philipp Hansch +* Imran Sobir * Panagiotis Ktistakis * Artur Shaik * Nathan Isom * Thorsten Wißmann * Austin Anderson +* Fritz Reichwald * Jimmy -* Spreadyy * Niklas Haas +* Maciej Wołczyk +* Spreadyy * Alexey "Averrin" Nabrodov * nanjekyejoannah * avk @@ -184,22 +187,25 @@ Contributors, sorted by the number of commits in descending order: * knaggita * Oliver Caldwell * Julian Weigt +* Tomasz Kramkowski * Sebastian Frysztak +* Nikolay Amiantov +* Julie Engel * Jonas Schürmann * error800 * Michael Hoang -* Maciej Wołczyk * Liam BEGUIN -* Julie Engel +* Daniel Fiser * skinnay * Zach-Button -* Tomasz Kramkowski +* Samuel Walladge * Peter Rice * Ismail S * Halfwit * David Vogt * Claire Cavanaugh * rikn00 +* pkill9 * kanikaa1234 * haitaka * Nick Ginther @@ -207,19 +213,23 @@ Contributors, sorted by the number of commits in descending order: * Michael Ilsaas * Martin Zimmermann * Jussi Timperi -* Fritz Reichwald +* Cosmin Popescu * Brian Jackson * thuck * sbinix +* rsteube * neeasade * jnphilipp +* Yannis Rohloff * Tobias Patzl * Stefan Tatschner * Samuel Loury * Peter Michely * Panashe M. Fundira +* Lucas Hoffmann * Link * Larry Hynes +* Kirill A. Shutemov * Johannes Altmanninger * Jeremy Kaplan * Ismail @@ -233,12 +243,10 @@ Contributors, sorted by the number of commits in descending order: * Marcelo Santos * Joel Bradshaw * Jean-Louis Fuchs -* Fritz V155 Reichwald * Franz Fellner * Eric Drechsel * zwarag * xd1le -* rsteube * rmortens * oniondreams * issue @@ -247,6 +255,7 @@ Contributors, sorted by the number of commits in descending order: * dylan araps * addictedtoflames * Xitian9 +* Vasilij Schneidermann * Tomas Orsava * Tom Janson * Tobias Werth @@ -260,6 +269,7 @@ Contributors, sorted by the number of commits in descending order: * Matthias Lisin * Marcel Schilling * Lazlow Carmichael +* Kevin Wang * Ján Kobezda * Johannes Martinsson * Jean-Christophe Petkovich @@ -274,6 +284,7 @@ Contributors, sorted by the number of commits in descending order: * Arseniy Seroka * Andy Balaam * Andreas Fischer +* Akselmo // QUTE_AUTHORS_END The following people have contributed graphics: diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc index 4151eb892..02427b97b 100644 --- a/doc/help/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -44,6 +44,7 @@ It is possible to run or bind multiple commands by separating them with `;;`. |<>|Toggle fullscreen mode. |<>|Show help about a command or setting. |<>|Start hinting. +|<>|Show browsing history. |<>|Clear all browsing history. |<>|Open main startpage in current tab. |<>|Insert text at cursor position. @@ -84,7 +85,7 @@ It is possible to run or bind multiple commands by separating them with `;;`. |<>|Switch to the previous tab, or switch [count] tabs back. |<>|Unbind a keychain. |<>|Re-open a closed tab (optionally skipping [count] closed tabs). -|<>|Show the source of the current page. +|<>|Show the source of the current page in a new tab. |<>|Close all windows except for the current one. |<>|Save open pages and quit. |<>|Yank something to the clipboard or primary selection. @@ -319,8 +320,13 @@ How many pages to go forward. [[fullscreen]] === fullscreen +Syntax: +:fullscreen [*--leave*]+ + Toggle fullscreen mode. +==== optional arguments +* +*-l*+, +*--leave*+: Only leave fullscreen if it was entered by the page. + [[help]] === help Syntax: +:help [*--tab*] [*--bg*] [*--window*] ['topic']+ @@ -413,12 +419,28 @@ Start hinting. ==== note * This command does not split arguments after the last argument and handles quotes literally. +[[history]] +=== history +Syntax: +:history [*--tab*] [*--bg*] [*--window*]+ + +Show browsing history. + +==== optional arguments +* +*-t*+, +*--tab*+: Open in a new tab. +* +*-b*+, +*--bg*+: Open in a background tab. +* +*-w*+, +*--window*+: Open in a new window. + [[history-clear]] === history-clear +Syntax: +:history-clear [*--force*]+ + Clear all browsing history. Note this only clears the global history (e.g. `~/.local/share/qutebrowser/history` on Linux) but not cookies, the back/forward history of a tab, cache or other persistent data. +==== optional arguments +* +*-f*+, +*--force*+: Don't ask for confirmation. + [[home]] === home Open main startpage in current tab. @@ -443,14 +465,15 @@ Note: Due a bug in Qt, the inspector will show incorrect request headers in the [[jseval]] === jseval -Syntax: +:jseval [*--quiet*] [*--world* 'world'] 'js-code'+ +Syntax: +:jseval [*--file*] [*--quiet*] [*--world* 'world'] 'js-code'+ Evaluate a JavaScript string. ==== positional arguments -* +'js-code'+: The string to evaluate. +* +'js-code'+: The string/file to evaluate. ==== optional arguments +* +*-f*+, +*--file*+: Interpret js-code as a path to a file. * +*-q*+, +*--quiet*+: Don't show resulting JS object. * +*-w*+, +*--world*+: Ignored on QtWebKit. On QtWebEngine, a world ID or name to run the snippet in. @@ -718,7 +741,8 @@ Load a session. [[session-save]] === session-save -Syntax: +:session-save [*--current*] [*--quiet*] [*--force*] ['name']+ +Syntax: +:session-save [*--current*] [*--quiet*] [*--force*] [*--only-active-window*] + ['name']+ Save a session. @@ -730,10 +754,11 @@ Save a session. * +*-c*+, +*--current*+: Save the current session instead of the default. * +*-q*+, +*--quiet*+: Don't show confirmation message. * +*-f*+, +*--force*+: Force saving internal sessions (starting with an underline). +* +*-o*+, +*--only-active-window*+: Saves only tabs of the currently active window. [[set]] === set -Syntax: +:set [*--temp*] [*--print*] ['section'] ['option'] ['value']+ +Syntax: +:set [*--temp*] [*--print*] ['section'] ['option'] ['values' ['values' ...]]+ Set an option. @@ -742,7 +767,7 @@ If the option name ends with '?', the value of the option is shown instead. If t ==== positional arguments * +'section'+: The section where the option is in. * +'option'+: The name of the option. -* +'value'+: The value to set. +* +'values'+: The value to set, or the values to cycle through. ==== optional arguments * +*-t*+, +*--temp*+: Set value temporarily. @@ -837,8 +862,7 @@ If neither count nor index are given, it behaves like tab-next. If both are give ==== count -The tab index to focus, starting with 1. The special value 0 focuses the rightmost tab. - +The tab index to focus, starting with 1. [[tab-move]] === tab-move @@ -899,7 +923,7 @@ Re-open a closed tab (optionally skipping [count] closed tabs). [[view-source]] === view-source -Show the source of the current page. +Show the source of the current page in a new tab. [[window-only]] === window-only @@ -971,6 +995,7 @@ How many steps to zoom out. |============== |Command|Description |<>|Clear the currently entered key chain. +|<>|Clear all message notifications. |<>|Click the element matching the given filter. |<>|Execute the command currently in the commandline. |<>|Go forward in the commandline history. @@ -1035,9 +1060,13 @@ How many steps to zoom out. === clear-keychain Clear the currently entered key chain. +[[clear-messages]] +=== clear-messages +Clear all message notifications. + [[click-element]] === click-element -Syntax: +:click-element [*--target* 'target'] 'filter' 'value'+ +Syntax: +:click-element [*--target* 'target'] [*--force-event*] 'filter' 'value'+ Click the element matching the given filter. @@ -1050,6 +1079,7 @@ The given filter needs to result in exactly one element, otherwise, an error is ==== optional arguments * +*-t*+, +*--target*+: How to open the clicked element (normal/tab/tab-bg/window). +* +*-f*+, +*--force-event*+: Force generating a fake click event. [[command-accept]] === command-accept @@ -1138,6 +1168,9 @@ Show an info message in the statusbar. ==== positional arguments * +'text'+: The text to show. +==== count +How many times to show the message + [[message-warning]] === message-warning Syntax: +:message-warning 'text'+ @@ -1405,6 +1438,8 @@ Syntax: +:scroll 'direction'+ Scroll the current tab in the given direction. +Note you can use `:run-with-count` to have a keybinding with a bigger scroll increment. + ==== positional arguments * +'direction'+: In which direction to scroll (up/down/left/right/top/bottom). diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index 6e2d3d67c..78b5a7022 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -11,6 +11,7 @@ |<>|Whether to find text on a page case-insensitively. |<>|The default page(s) to open at the start, separated by commas. |<>|The URL parameters to strip with :yank url, separated by commas. +|<>|The default program used to open downloads. Set to an empty string to use the default internal handler. |<>|The page to open if :open -t/-b/-w is used without URL. Use `about:blank` for a blank page. |<>|Whether to start a search when something else than a URL is entered. |<>|Whether to save the config automatically on quit. @@ -21,7 +22,7 @@ |<>|Enable extra tools for Web developers. |<>|Whether the background color and images are also drawn when the page is printed. |<>|Whether load requests should be monitored for cross-site scripting attempts. -|<>|Enable workarounds for broken sites. +|<>|Enable QtWebKit workarounds for broken sites. |<>|Default encoding to use for websites. |<>|How to open links in an existing instance if a new one is launched. |<>|Which window to choose when opening links as new tabs. @@ -47,7 +48,7 @@ |<>|User stylesheet to use (absolute filename or filename relative to the config directory). Will expand environment variables. |<>|Hide the main scrollbar. |<>|Set the CSS media type. -|<>|Whether to enable smooth scrolling for webpages. +|<>|Whether to enable smooth scrolling for web pages. Note smooth scrolling does not work with the :scroll-px command. |<>|Number of milliseconds to wait before removing finished downloads. Will not be removed if value is -1. |<>|Whether to hide the statusbar unless a message is shown. |<>|Padding for statusbar (top, bottom, left, right). @@ -56,6 +57,7 @@ |<>|Hide the window decoration when using wayland (requires restart) |<>|Keychains that shouldn't be shown in the keyhint dialog |<>|The rounding radius for the edges of prompts. +|<>|Show a filebrowser in upload/download prompts. |============== .Quick reference for section ``network'' @@ -146,7 +148,7 @@ |<>|Whether support for the HTML 5 offline storage feature is enabled. |<>|Whether support for the HTML 5 web application cache feature is enabled. |<>|Whether support for the HTML 5 local storage feature is enabled. -|<>|Size of the HTTP network cache. +|<>|Size of the HTTP network cache. Empty to use the default value. |============== .Quick reference for section ``content'' @@ -156,7 +158,7 @@ |<>|Whether images are automatically loaded in web pages. |<>|Enables or disables the running of JavaScript programs. |<>|Enables or disables plugins in Web pages. -|<>|Enables or disables WebGL. For QtWebEngine, Qt/PyQt >= 5.7 is required for this setting. +|<>|Enables or disables WebGL. |<>|Enable or disable support for CSS regions. |<>|Enable or disable hyperlink auditing (). |<>|Allow websites to request geolocations. @@ -170,7 +172,7 @@ |<>|Whether locally loaded documents are allowed to access remote urls. |<>|Whether locally loaded documents are allowed to access other local urls. |<>|Control which cookies to accept. -|<>|Whether to store cookies. +|<>|Whether to store cookies. Note this option needs a restart with QtWebEngine. |<>|List of URLs of lists which contain hosts to block. |<>|Whether host blocking is enabled. |<>|List of domains that should always be loaded, despite being ad-blocked. @@ -330,6 +332,14 @@ The URL parameters to strip with :yank url, separated by commas. Default: +pass:[ref,utm_source,utm_medium,utm_campaign,utm_term,utm_content]+ +[[general-default-open-dispatcher]] +=== default-open-dispatcher +The default program used to open downloads. Set to an empty string to use the default internal handler. + +Any {} in the string will be expanded to the filename, else the filename will be appended. + +Default: empty + [[general-default-page]] === default-page The page to open if :open -t/-b/-w is used without URL. Use `about:blank` for a blank page. @@ -397,7 +407,7 @@ This setting is only available with the QtWebKit backend. === developer-extras Enable extra tools for Web developers. -This needs to be enabled for `:inspector` to work and also adds an _Inspect_ entry to the context menu. +This needs to be enabled for `:inspector` to work and also adds an _Inspect_ entry to the context menu. For QtWebEngine, see 'qutebrowser --help' instead. Valid values: @@ -406,9 +416,12 @@ Valid values: Default: +pass:[false]+ +This setting is only available with the QtWebKit backend. + [[general-print-element-backgrounds]] === print-element-backgrounds Whether the background color and images are also drawn when the page is printed. +This setting only works with Qt 5.8 or newer when using the QtWebEngine backend. Valid values: @@ -417,8 +430,6 @@ Valid values: Default: +pass:[true]+ -This setting is only available with the QtWebKit backend. - [[general-xss-auditing]] === xss-auditing Whether load requests should be monitored for cross-site scripting attempts. @@ -434,7 +445,7 @@ Default: +pass:[false]+ [[general-site-specific-quirks]] === site-specific-quirks -Enable workarounds for broken sites. +Enable QtWebKit workarounds for broken sites. Valid values: @@ -644,7 +655,7 @@ This setting is only available with the QtWebKit backend. [[ui-smooth-scrolling]] === smooth-scrolling -Whether to enable smooth scrolling for webpages. +Whether to enable smooth scrolling for web pages. Note smooth scrolling does not work with the :scroll-px command. Valid values: @@ -727,6 +738,17 @@ The rounding radius for the edges of prompts. Default: +pass:[8]+ +[[ui-prompt-filebrowser]] +=== prompt-filebrowser +Show a filebrowser in upload/download prompts. + +Valid values: + + * +true+ + * +false+ + +Default: +pass:[true]+ + == network Settings related to the network. @@ -773,6 +795,8 @@ The proxy to use. In addition to the listed values, you can use a `socks://...` or `http://...` URL. +This setting only works with Qt 5.8 or newer when using the QtWebEngine backend. + Valid values: * +system+: Use the system wide proxy. @@ -780,8 +804,6 @@ Valid values: Default: +pass:[system]+ -This setting is only available with the QtWebKit backend. - [[network-proxy-dns-requests]] === proxy-dns-requests Whether to send DNS requests over the configured proxy. @@ -1360,9 +1382,9 @@ Default: +pass:[true]+ [[storage-cache-size]] === cache-size -Size of the HTTP network cache. +Size of the HTTP network cache. Empty to use the default value. -Default: +pass:[52428800]+ +Default: empty == content Loaded plugins/scripts and allowed actions. @@ -1404,14 +1426,14 @@ Default: +pass:[false]+ [[content-webgl]] === webgl -Enables or disables WebGL. For QtWebEngine, Qt/PyQt >= 5.7 is required for this setting. +Enables or disables WebGL. Valid values: * +true+ * +false+ -Default: +pass:[false]+ +Default: +pass:[true]+ [[content-css-regions]] === css-regions @@ -1502,6 +1524,7 @@ This setting is only available with the QtWebKit backend. [[content-javascript-can-access-clipboard]] === javascript-can-access-clipboard Whether JavaScript programs can read or write to the clipboard. +With QtWebEngine, writing the clipboard as response to a user interaction is always allowed. Valid values: @@ -1571,7 +1594,7 @@ This setting is only available with the QtWebKit backend. [[content-cookies-store]] === cookies-store -Whether to store cookies. +Whether to store cookies. Note this option needs a restart with QtWebEngine. Valid values: @@ -1580,8 +1603,6 @@ Valid values: Default: +pass:[true]+ -This setting is only available with the QtWebKit backend. - [[content-host-block-lists]] === host-block-lists List of URLs of lists which contain hosts to block. @@ -1643,7 +1664,7 @@ Mode to use for hints. Valid values: - * +number+: Use numeric hints. + * +number+: Use numeric hints. (In this mode you can also type letters form the hinted element to filter and reduce the number of elements that are hinted.) * +letter+: Use the chars in the hints -> chars setting. * +word+: Use hints words based on the html elements and the extra words. @@ -2130,8 +2151,6 @@ Background color for webpages if unset (or empty to use the theme's color) Default: +pass:[white]+ -This setting is only available with the QtWebKit backend. - [[colors-keyhint.fg]] === keyhint.fg Text color for the keyhint widget. diff --git a/doc/img/cheatsheet-big.png b/doc/img/cheatsheet-big.png index 6ff0ea172..420791def 100644 Binary files a/doc/img/cheatsheet-big.png and b/doc/img/cheatsheet-big.png differ diff --git a/doc/img/cheatsheet-small.png b/doc/img/cheatsheet-small.png index f90230ca2..20f271d19 100644 Binary files a/doc/img/cheatsheet-small.png and b/doc/img/cheatsheet-small.png differ diff --git a/doc/quickstart.asciidoc b/doc/quickstart.asciidoc index dc1426931..7d597ed2e 100644 --- a/doc/quickstart.asciidoc +++ b/doc/quickstart.asciidoc @@ -9,7 +9,8 @@ Basic keybindings to get you started ------------------------------------ * Use the arrow keys or `hjkl` to move around a webpage (vim-like syntax is used in quite a few places) -* To go to a new webpage, press `o`, then type a url, then press Enter (Use `O` to open the url in a new tab). If what you've typed isn't a url, then a search engine will be used instead (DuckDuckGo, by default) +* To go to a new webpage, press `o`, then type a url, then press Enter (Use `O` to open the url in a new tab, `go` to edit the current URL) +* If what you've typed isn't a url, then a search engine will be used instead (DuckDuckGo, by default) * To switch between tabs, use `J` (next tab) and `K` (previous tab), or press ``, where `num` is the position of the tab to switch to * To close the current tab, press `d` (and press `u` to undo closing a tab) * Use `H` and `L` to go back and forth in the history @@ -30,7 +31,7 @@ image:http://qutebrowser.org/img/cheatsheet-small.png["qutebrowser key binding c * Run `:adblock-update` to download adblock lists and activate adblocking. * If you just cloned the repository, you'll need to run `scripts/asciidoc2html.py` to generate the documentation. -* Go to the link:qute://settings[settings page] to set up qutebrowser the way you want it. +* Go to the link:qute://settings[settings page] to set up qutebrowser the way you want it. (Currently not available with the QtWebEngine backend and on the OS X build - use the `:set` command instead) * Subscribe to https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser[the mailinglist] or https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser-announce[the announce-only mailinglist]. diff --git a/doc/qutebrowser.1.asciidoc b/doc/qutebrowser.1.asciidoc index d76c4480c..36530bffe 100644 --- a/doc/qutebrowser.1.asciidoc +++ b/doc/qutebrowser.1.asciidoc @@ -10,7 +10,7 @@ :homepage: https://www.qutebrowser.org/ == NAME -qutebrowser - a keyboard-driven, vim-like browser based on PyQt5 and QtWebKit. +qutebrowser - a keyboard-driven, vim-like browser based on PyQt5. == SYNOPSIS *qutebrowser* ['-OPTION' ['...']] [':COMMAND' ['...']] ['URL' ['...']] @@ -59,6 +59,9 @@ show it. *--backend* '{webkit,webengine}':: Which backend to use (webengine backend is EXPERIMENTAL!). +*--enable-webengine-inspector*:: + Enable the web inspector for QtWebEngine. Note that this is a SECURITY RISK and you should not visit untrusted websites with the inspector turned on. See https://bugreports.qt.io/browse/QTBUG-50725 for more details. + === debug arguments *-l* '{critical,error,warning,info,debug,vdebug}', *--loglevel* '{critical,error,warning,info,debug,vdebug}':: Set loglevel @@ -124,7 +127,7 @@ defaults. == BUGS Bugs are tracked in the Github issue tracker at -https://github.com/The-Compiler/qutebrowser/issues. +https://github.com/qutebrowser/qutebrowser/issues. If you found a bug, use the built-in ':report' command to create a bug report with all information needed. @@ -157,7 +160,7 @@ https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser-announce * IRC: irc://irc.freenode.org/#qutebrowser[`#qutebrowser`] on http://freenode.net/[Freenode] -* Github: https://github.com/The-Compiler/qutebrowser +* Github: https://github.com/qutebrowser/qutebrowser == AUTHOR *qutebrowser* was written by Florian Bruhin. All contributors can be found in diff --git a/doc/userscripts.asciidoc b/doc/userscripts.asciidoc index 433c71ff0..b44a6b8ff 100644 --- a/doc/userscripts.asciidoc +++ b/doc/userscripts.asciidoc @@ -38,6 +38,7 @@ The following environment variables will be set when a userscript is launched: - `QUTE_CONFIG_DIR`: Path of the directory containing qutebrowser's configuration. - `QUTE_DATA_DIR`: Path of the directory containing qutebrowser's data. - `QUTE_DOWNLOAD_DIR`: Path of the downloads directory. +- `QUTE_COMMANDLINE_TEXT`: Text currently in qutebrowser's command line. In `command` mode: diff --git a/misc/cheatsheet.svg b/misc/cheatsheet.svg index 29d0a86d7..7068b829c 100644 --- a/misc/cheatsheet.svg +++ b/misc/cheatsheet.svg @@ -9,11 +9,11 @@ xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - width="1024" - height="640" + width="1092.2667" + height="682.66669" id="svg2" sodipodi:version="0.32" - inkscape:version="0.91 r13725" + inkscape:version="0.92.1 r" version="1.0" sodipodi:docname="cheatsheet.svg" inkscape:output_extension="org.inkscape.output.svg.inkscape" @@ -33,17 +33,17 @@ inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="1.7582312" - inkscape:cx="875.18895" - inkscape:cy="136.8726" + inkscape:cx="513.85167" + inkscape:cy="273.37342" inkscape:document-units="px" inkscape:current-layer="layer1" width="1024px" height="640px" showgrid="false" - inkscape:window-width="1362" - inkscape:window-height="740" + inkscape:window-width="2560" + inkscape:window-height="1440" inkscape:window-x="0" - inkscape:window-y="24" + inkscape:window-y="0" showguides="true" inkscape:guide-bbox="true" inkscape:window-maximized="0" @@ -51,10 +51,10 @@ + style="font-size:18px;fill:#babdb6;fill-opacity:1;stroke:none;stroke-width:1.06666672" /> + width="64" + height="64" + x="96" + y="163.79405" + ry="4.7797003" /> + width="63.461262" + height="64" + x="544.48407" + y="163.79405" + ry="4.7797003" /> + style="font-size:18px;fill:#eeeeec;fill-opacity:1;stroke:none;stroke-width:1.06666672" /> + style="font-size:18px;fill:#eeeeec;fill-opacity:1;stroke:none;stroke-width:1.06666672" /> + style="font-size:18px;fill:#eeeeec;fill-opacity:1;stroke:none;stroke-width:1.06666672" /> + width="63.461262" + height="64" + x="693.82019" + y="163.79405" + ry="4.7797003" /> Q + x="99.751244" + y="187.30594" + style="font-size:19.20000076px;line-height:1.25;stroke-width:1.06666672">Q + width="63.461262" + height="64" + x="320.05182" + y="163.79405" + ry="4.7797003" /> R + sodipodi:role="line" + style="font-size:19.20000076px;line-height:1.25;stroke-width:1.06666672">R + width="63.461262" + height="64" + x="171.15358" + y="163.79405" + ry="4.7797003" /> W q + style="font-size:19.20000076px;line-height:1.25;font-family:'DejaVu Sans Mono';stroke-width:1.06666672">q w + x="175.64062" + y="220.43825" + style="font-size:19.20000076px;line-height:1.25;font-family:'DejaVu Sans Mono';stroke-width:1.06666672">w + width="63.461262" + height="64" + x="395.15076" + y="147.2" + ry="4.7797003" /> + style="font-size:18px;fill:#eeeeec;fill-opacity:1;stroke:none;stroke-width:1.06666672" /> + style="font-size:18px;fill:#babdb6;fill-opacity:1;stroke:none;stroke-width:1.06666672" /> E + x="248.78169" + y="187.60356" + style="font-size:19.20000076px;line-height:1.25;stroke-width:1.06666672">E e + x="248.64069" + y="220.16637">e r t + x="399.83853" + y="220.32741">t y u + style="font-size:19.20000076px;line-height:1.25;font-family:'DejaVu Sans Mono';fill:#0000ff;stroke-width:1.06666672">u i + x="622.33124" + y="220.32741" + style="font-size:19.20000076px;line-height:1.25;font-family:'DejaVu Sans Mono';stroke-width:1.06666672">i o + x="698.52502" + y="219.88054" + style="font-size:19.20000076px;line-height:1.25;font-family:'DejaVu Sans Mono';stroke-width:1.06666672">o p + x="772.01251" + y="219.88054">p + sodipodi:role="line">  + style="font-size:18px;fill:#eeeeec;fill-opacity:1;stroke:none;stroke-width:1.06666672" /> + width="63.461262" + height="64" + x="181.38516" + y="238.46074" + ry="4.7797003" /> + width="64.538773" + height="64" + x="255.56491" + y="238.46074" + ry="4.7797003" /> + style="font-size:18px;fill:#eeeeec;fill-opacity:1;stroke:none;stroke-width:1.06666672" /> + width="63.461262" + height="64" + x="405.38232" + y="238.46074" + ry="4.7797003" /> + style="font-size:18px;fill:#eeeeec;fill-opacity:1;stroke:none;stroke-width:1.06666672" /> + width="63.461262" + height="64" + x="629.38507" + y="238.46074" + ry="4.7797003" /> + style="font-size:18px;fill:#eeeeec;fill-opacity:1;stroke:none;stroke-width:1.06666672" /> A + sodipodi:role="line" + style="font-size:19.20000076px;line-height:1.25;stroke-width:1.06666672">A F + x="335.79037" + y="264.53333" + style="font-size:19.20000076px;line-height:1.25;stroke-width:1.06666672">F S + x="185.38518" + y="261.80792">S a + x="110.07188" + y="295.08054">a s + style="font-size:18px;fill:#eeeeec;fill-opacity:1;stroke:none;stroke-width:1.06666672" /> + width="63.461262" + height="33.005924" + x="480.04877" + y="351.5274" + ry="2.4649756" /> D + sodipodi:role="line" + style="font-size:19.20000076px;line-height:1.25;stroke-width:1.06666672">D d + style="font-size:19.20000076px;line-height:1.25;font-family:'DejaVu Sans Mono';fill:#0000ff;stroke-width:1.06666672">d f + x="335.79037" + y="293.96075" + style="font-size:19.20000076px;line-height:1.25;font-family:'DejaVu Sans Mono';stroke-width:1.06666672">f g + style="font-size:19.20000076px;line-height:1.25;font-family:'DejaVu Sans Mono';stroke-width:1.06666672">g h + x="483.63565" + y="292.44882" + style="font-size:19.20000076px;line-height:1.25;font-family:'DejaVu Sans Mono';fill:#0000ff;stroke-width:1.06666672">h j + x="563.19849" + y="293.46237">j k + width="63.461262" + height="64" + x="704" + y="350.93332" + ry="4.7797003" /> + style="font-size:18px;fill:#eeeeec;fill-opacity:1;stroke:none;stroke-width:1.06666672" /> l ; + x="781.47919" + y="292.20032" + style="font-size:19.20000076px;line-height:1.25;font-family:'DejaVu Sans Mono';stroke-width:1.06666672">; + style="font-size:18px;fill:#babdb6;fill-opacity:1;stroke:none;stroke-width:1.06666672" /> + width="137.60541" + height="64" + x="929.06122" + y="238.46074" + ry="4.7797003" /> ' + x="858.5" + y="296.23157">' + sodipodi:role="line" + style="font-size:21.33333397px;line-height:1.25;stroke-width:1.06666672">↲ + width="64" + height="64" + x="116.89825" + y="313.12741" + ry="4.7797003" /> + style="font-size:18px;fill:#babdb6;fill-opacity:1;stroke:none;stroke-width:1.06666672" /> + width="63.461262" + height="64" + x="341.38516" + y="313.12741" + ry="4.7797003" /> + style="font-size:18px;fill:#eeeeec;fill-opacity:1;stroke:none;stroke-width:1.06666672" /> + style="font-size:18px;fill:#eeeeec;fill-opacity:1;stroke:none;stroke-width:1.06666672" /> + width="63.948242" + height="64" + x="789.82019" + y="313.12741" + ry="4.7797003" /> z + style="font-size:19.20000076px;line-height:1.25;font-family:'DejaVu Sans Mono';stroke-width:1.06666672">z x + x="199.07387" + y="367.11551" + style="font-size:19.20000076px;line-height:1.25;font-family:'DejaVu Sans Mono';stroke-width:1.06666672">x + style="font-size:18px;fill:#eeeeec;fill-opacity:1;stroke:none;stroke-width:1.06666672" /> + style="font-size:18px;fill:#eeeeec;fill-opacity:1;stroke:none;stroke-width:1.06666672" /> + width="63.461262" + height="32" + x="565.38226" + y="328.53333" + ry="2.3898501" /> + width="63.461262" + height="64" + x="640.05176" + y="296.53333" + ry="4.7797003" /> + style="font-size:18px;fill:#babdb6;fill-opacity:1;stroke:none;stroke-width:1.06666672" /> c + x="271.65314" + y="368.61978">c v b + x="420.34647" + y="367.93842">b n m + style="font-size:19.20000076px;line-height:1.25;font-family:'DejaVu Sans Mono';stroke-width:1.06666672">m , + x="643.82837" + y="367.86551" + style="font-size:19.20000076px;line-height:1.25;font-family:'DejaVu Sans Mono';stroke-width:1.06666672">, + style="font-size:18px;fill:#babdb6;fill-opacity:1;stroke:none;stroke-width:1.06666672" /> + width="63.461262" + height="32" + x="640.05176" + y="328.53333" + ry="2.3898501" /> . + x="718.6156" + y="367.46667" + style="font-size:19.20000076px;line-height:1.25;font-family:'DejaVu Sans Mono';stroke-width:1.06666672">. / + style="font-size:18px;fill:#eeeeec;fill-opacity:1;stroke:none;stroke-width:1.06666672" /> + width="63.461262" + height="64" + x="160.48691" + y="89.127388" + ry="4.7797003" /> + width="63.461262" + height="64" + x="235.15358" + y="89.127388" + ry="4.7797003" /> + style="font-size:18px;fill:#eeeeec;fill-opacity:1;stroke:none;stroke-width:1.06666672" /> + width="63.461262" + height="64" + x="384.48407" + y="89.127388" + ry="4.7797003" /> + style="font-size:18px;fill:#eeeeec;fill-opacity:1;stroke:none;stroke-width:1.06666672" /> + width="63.461262" + height="64" + x="608.48688" + y="89.127388" + ry="4.7797003" /> + width="63.461262" + height="64" + x="459.15051" + y="89.127388" + ry="4.7797003" /> + width="63.461262" + height="64" + x="757.33331" + y="89.127388" + ry="4.7797003" /> + style="font-size:18px;fill:#eeeeec;fill-opacity:1;stroke:none;stroke-width:1.06666672" /> + style="font-size:18px;fill:#eeeeec;fill-opacity:1;stroke:none;stroke-width:1.06666672" /> 1 + x="88.721916" + y="145.3714">1 2 3 + style="font-size:19.20000076px;line-height:1.25;font-family:'DejaVu Sans Mono';stroke-width:1.06666672">3 4 + x="312.26355" + y="145.3714" + style="font-size:19.20000076px;line-height:1.25;font-family:'DejaVu Sans Mono';stroke-width:1.06666672">4 5 + style="font-size:19.20000076px;line-height:1.25;font-family:'DejaVu Sans Mono';stroke-width:1.06666672">5 6 + x="461.15417" + y="144.91666" + style="font-size:19.20000076px;line-height:1.25;font-family:'DejaVu Sans Mono';stroke-width:1.06666672">6 7 + x="536.29688" + y="145.06665">7 8 9 0 + style="font-size:19.20000076px;line-height:1.25;font-family:'DejaVu Sans Mono';stroke-width:1.06666672">0 + width="63.461262" + height="64" + x="906.71844" + y="89.127388" + ry="4.7797003" /> + style="font-size:18px;fill:#babdb6;fill-opacity:1;stroke:none;stroke-width:1.06666672" /> - + x="840.27075" + y="143.8774" + style="font-size:19.20000076px;line-height:1.25;font-family:'DejaVu Sans Mono';fill:#0000ff;stroke-width:1.06666672">- = + x="913.50781" + y="144.89095">= + style="font-size:18px;fill:#babdb6;fill-opacity:1;stroke:none;stroke-width:1.06666672" /> Backspace + style="font-size:13.86666679px;line-height:1.25;font-family:'DejaVu Sans Mono';stroke-width:1.06666672">Backspace ! + style="font-size:19.20000076px;line-height:1.25;font-family:'DejaVu Sans Mono';stroke-width:1.06666672">! @ + x="162.17561" + y="116.44883" + style="font-size:19.20000076px;line-height:1.25;font-family:'DejaVu Sans Mono';stroke-width:1.06666672">@ # + x="238.91458" + y="117.86665"># $ % + x="387.71487" + y="117.27174">% ^ & + style="font-size:19.20000076px;line-height:1.25;font-family:'DejaVu Sans Mono';stroke-width:1.06666672">& * + x="609.5968" + y="117.19882" + style="font-size:19.20000076px;line-height:1.25;font-family:'DejaVu Sans Mono';stroke-width:1.06666672">* ( + x="685.03265" + y="116.44881" + style="font-size:19.20000076px;line-height:1.25;font-family:'DejaVu Sans Mono';stroke-width:1.06666672">( ) + x="761.91931" + y="116.12902">) + style="font-size:18px;fill:#babdb6;fill-opacity:1;stroke:none;stroke-width:1.06666672" /> + transform="translate(0,-7.1714294)"> Space + sodipodi:role="line" + style="font-size:13.86666679px;line-height:1.25;stroke-width:1.06666672">Space Z + style="font-size:19.20000076px;line-height:1.25;font-family:'DejaVu Sans Mono';stroke-width:1.06666672">Z X + x="197.67885" + y="337.60355" + style="font-size:19.20000076px;line-height:1.25;font-family:'DejaVu Sans Mono';stroke-width:1.06666672">X C + x="271.76355" + y="339.99478">C V B + x="418.95145" + y="338.42645">B N M + style="font-size:19.20000076px;line-height:1.25;font-family:'DejaVu Sans Mono';stroke-width:1.06666672">M < + x="645.10004" + y="338.35355" + style="font-size:19.20000076px;line-height:1.25;font-family:'DejaVu Sans Mono';stroke-width:1.06666672">< > + x="723.39313" + y="336.84164" + style="font-size:19.20000076px;line-height:1.25;font-family:'DejaVu Sans Mono';stroke-width:1.06666672">> ? + x="798.75592" + y="337.85516">? G + style="font-size:19.20000076px;line-height:1.25;font-family:'DejaVu Sans Mono';fill:#0000ff;stroke-width:1.06666672">G H + x="483.63565" + y="263.25632" + style="font-size:19.20000076px;line-height:1.25;font-family:'DejaVu Sans Mono';fill:#0000ff;stroke-width:1.06666672">H J + x="559.00153" + y="264.26987">J K L : + style="font-size:19.20000076px;line-height:1.25;font-family:'DejaVu Sans Mono';stroke-width:1.06666672">: T + x="399.83853" + y="187.92427">T Y U + style="font-size:19.20000076px;line-height:1.25;font-family:'DejaVu Sans Mono';stroke-width:1.06666672">U I + x="621.77612" + y="187.60356" + style="font-size:19.20000076px;line-height:1.25;font-family:'DejaVu Sans Mono';stroke-width:1.06666672">I O + x="698.92633" + y="187.3317" + style="font-size:19.20000076px;line-height:1.25;font-family:'DejaVu Sans Mono';stroke-width:1.06666672">O P + x="771.70312" + y="187.92427">P + style="font-size:18px;fill:#eeeeec;fill-opacity:1;stroke:none;stroke-width:1.06666672" /> + width="63.461262" + height="64" + x="917.38507" + y="163.79405" + ry="4.7797003" /> + style="font-size:18px;fill:#babdb6;fill-opacity:1;stroke:none;stroke-width:1.06666672" /> [ + x="845.2594" + y="219.39616">[ ] \ { + x="844.91248" + y="186.91489">{ } | '"l '"l " + x="857.28125" + y="264.82532">" ` + x="20.090919" + y="145.65862">` ~ + x="18.695885" + y="116.14663">~  reload + style="font-size:8.53333378px;line-height:0.89999998;stroke-width:1.06666672">reload TabTa TabTa Tab + style="font-size:12.80000019px;line-height:1.25;font-family:'DejaVu Sans Mono';stroke-width:1.06666672">Tab + width="338.15115" + height="41.734409" + x="101.31554" + y="20.999462" + ry="3.116843" /> qutebrowser default bindings - qutebrowser default bindings +  scrollscrolldown + style="font-size:8.53333378px;line-height:0.89999998;stroke-width:1.06666672">down  scrollscrollup + x="650.2074" + y="294.60449" + style="font-size:8.53333378px;line-height:0.89999998;stroke-width:1.06666672">up  nextnexttab + x="578.2074" + y="263.37888" + style="font-size:8.53333378px;line-height:0.89999998;stroke-width:1.06666672">tab previous previoustab + style="font-size:8.53333378px;line-height:0.89999998;stroke-width:1.06666672">tab  scrollscrollleft + style="font-size:8.53333378px;line-height:0.89999998;stroke-width:1.06666672">left  scrollscrollright + style="font-size:8.53333378px;line-height:0.89999998;stroke-width:1.06666672">right   open open (6)  open inopen innew tabnew tab(6)  closeclosetab + style="font-size:8.53333378px;line-height:0.89999998;stroke-width:1.06666672">tab  hinthint(label(labellinks) links) (8)  undoundoclosingclosingtab + style="font-size:8.53333378px;line-height:0.89999998;stroke-width:1.06666672">tab  insert-insert-mode + style="font-size:8.53333378px;line-height:0.89999998;stroke-width:1.06666672">mode _ + x="840.11176" + y="108.99891" + style="font-size:19.20000076px;line-height:1.25;font-family:'DejaVu Sans Mono';stroke-width:1.06666672">_ + + x="913.50781" + y="110.18444">+  zoomzoomout + style="font-size:8.53333378px;line-height:0.89999998;stroke-width:1.06666672">out  zoomzoomin + id="tspan5593" + style="font-size:8.53333378px;line-height:0.89999998;stroke-width:1.06666672">in setsetzoomzoomlevel + style="font-size:8.53333378px;line-height:0.89999998;stroke-width:1.06666672">level + style="font-size:18px;fill:#eeeeec;fill-opacity:1;stroke:none;stroke-width:1.06666672" /> Esc + style="font-size:12.80000019px;line-height:1.25;font-family:'DejaVu Sans Mono';stroke-width:1.06666672">Esc  hinthint(new(newtab) + style="font-size:8.53333378px;line-height:0.89999998;stroke-width:1.06666672">tab)  back back (7)   forwardforward(7) + style="font-size:8.53333378px;line-height:0.89999998;fill:#ff0000;stroke-width:1.06666672">(7)  search + id="tspan4975-2-8" + style="font-size:8.53333378px;line-height:0.89999998;stroke-width:1.06666672"> search   cmdcmdmode + style="font-size:8.53333378px;line-height:0.89999998;stroke-width:1.06666672">mode  searchsearchnext + id="tspan5466" + style="font-size:8.53333378px;line-height:0.89999998;stroke-width:1.06666672">next  searchsearchprev + id="tspan5466-8" + style="font-size:8.53333378px;line-height:0.89999998;stroke-width:1.06666672">prev savesavequick-quick-mark + id="tspan5532" + style="font-size:8.53333378px;line-height:0.89999998;stroke-width:1.06666672">mark  yank/yank/copy copy (1)  pastepaste(2) + style="font-size:8.53333378px;line-height:0.89999998;fill:#ff0000;stroke-width:1.06666672">(2)   Website: https://www.qutebrowser.org/ Website: https://www.qutebrowser.org/ IRC: #qutebrowser on FreenodeIRC: #qutebrowser on FreenodeMailinglist: qutebrowser@lists.qutebrowser.org Mailinglist: qutebrowser@lists.qutebrowser.org  sscroll tobottom/bottom/perc.perc. + x="425.7652" + y="273.27655" + id="tspan10562-12-5" + style="font-size:8.53333378px;line-height:0.89999998;stroke-width:1.06666672">   loadloadquick-quick-mark mark (8)  loadloadquickm.quickm.(tab)(tab) + id="tspan5689" + style="font-size:8.53333378px;line-height:0.89999998;stroke-width:1.06666672">  (1) copying/yanking:yy - copy/yank URLyY - copy URL to selectionyt - copy title to clipboardyT - copy title to selection   (2) pasting:pp - open URL from clipboardpP - open URL from selectionPp - like pp, in new tabPP - like pP, in new tabwp - like pp, in new windowwP - like pP, in new window   (3) navigation:[[ - click "previous"-link on page]] - click "next"-link on page]] - click "next"-link on page  {{ - like [[, in new tab}} - like ]], in new tab<Ctrl-A> - increment no. in URL<Ctrl-X> - decrement no. in URL  (3) + style="font-size:8.53333378px;line-height:0.89999998;fill:#ff0000;stroke-width:1.06666672">(3) (3) + style="font-size:8.53333378px;line-height:0.89999998;fill:#ff0000;stroke-width:1.06666672">(3) (3) + style="font-size:8.53333378px;line-height:0.89999998;fill:#ff0000;stroke-width:1.06666672">(3) (3) + style="font-size:8.53333378px;line-height:0.89999998;fill:#ff0000;stroke-width:1.06666672">(3) (4) scrolling:<Ctrl-F> - page down<Ctrl-B> - page up<Ctrl-D> - half page down<Ctrl-U> - half page up in prompt mode:Enter - accept prompty - answer yes to promptn - answer no to prompt   (6) opening:go - open based on cur. URLgO - like go - edit & open current URLgO - like  go, in new tabxO - like go, in bg. tabxo - open in background tabwo - open in new window   gg: gg: (10)scrollscrollto topto top + x="425.7652" + y="303.19141" + id="tspan10562-12-5-9" + style="font-size:8.53333378px;line-height:0.89999998;stroke-width:1.06666672">   normalnormalmode + style="font-size:8.53333378px;line-height:0.89999998;stroke-width:1.06666672">mode (7) back/forward:th - back (in new tab)wh - back (in new window)tl - forward (in new tab)wl - forward (in new window)     ext.ext.hints hints (9) (8)(8)prefix with w - in new window   (9) extended hint mode:;b - open hint in background tab;f - open hint in foreground tab;h - hover over hint (mouse-over);i - hint images;I - hint images in new tab;o - put hinted URL in cmd. line;O - like ;o, in new tab;y - yank hinted URL to clipboard;Y - yank hinted URL to selection;r - rapid hinting;R - like ;r, in new window;d - download hinted URL (10) misc. commands:gt - switch tabs by namegm/gl/lr - move tabgm/gl/gr - move tab (to index/left/right)gC - clone tabgC - clone tab  gf - view page sourcegu - navigate up in URLgU - like gu, in new tabsf - save configss - set settingsl - set temp. settingsk - bind keySs - show settingswi - open web inspectorgd - download pagead - cancel downloadco - close other tabscd - clear downloads  (10) + style="font-size:8.53333378px;line-height:0.89999998;fill:#ff0000;stroke-width:1.06666672">(10) (10) + style="font-size:8.53333378px;line-height:0.89999998;fill:#ff0000;stroke-width:1.06666672">(10) (10) + style="font-size:8.53333378px;line-height:0.89999998;fill:#ff0000;stroke-width:1.06666672">(10) (11) modifier commands:<Alt-num> - select tab<Ctrl-Tab> - select prev. tab<Ctrl-V> - passthrough mode<Ctrl-Q> - quit<Ctrl-H> - home<Ctrl-S> - stop loading<Ctrl-Alt-P> - printin insert mode:<Ctrl-E> - open editorin command mode:<Ctrl-P> - prev. history item<Ctrl-N> - next history item + width="64" + height="49.059277" + x="179.54729" + y="386.13333" + ry="3.6638854" /> + width="64" + height="49.059277" + x="51.100777" + y="386.13333" + ry="3.6638854" /> + width="64" + height="49.059277" + x="654.73492" + y="386.13333" + ry="3.6638854" /> + width="64" + height="49.059277" + x="779.16278" + y="386.13333" + ry="3.6638854" /> + transform="translate(1.4643921,-2.0969564)"> Ctrl + x="66.420761" + y="411.5079">Ctrl (11) + x="71.803055" + y="426.94736">(11) + transform="translate(1.7364258,-12.763623)"> Alt + x="198.77023" + y="422.17459">Alt (11) + x="199.97752" + y="437.61404">(11) + transform="translate(6.0870443,-12.763623)"> Alt + x="669.60724" + y="422.17459">Alt (11) + x="670.81451" + y="437.61404">(11) + transform="translate(1.0914714,-12.763623)"> Ctrl + x="794.85565" + y="422.17459">Ctrl (11) + x="800.23798" + y="437.61404">(11)  selectselecttab + style="font-size:8.53333378px;line-height:0.89999998;stroke-width:1.06666672">tab (10) + x="292.49472" + y="366.05417" + id="tspan4052" + style="font-size:8.53333378px;line-height:0.89999998;stroke-width:1.06666672">(10)  search backw. + id="tspan4977-3-1-2" + style="font-size:8.53333378px;line-height:0.89999998;stroke-width:1.06666672">searchbackw.  pastepaste(2) + style="font-size:8.53333378px;line-height:0.89999998;fill:#ff0000;stroke-width:1.06666672">(2)  yank/yank/copy copy (1) (10) + style="font-size:8.53333378px;line-height:0.89999998;fill:#ff0000;stroke-width:1.06666672">(10) blue keys blue keys can beprefixed by a count  reload reload (bypass cache) visualvisualmode + id="tspan4112" + style="font-size:8.53333378px;line-height:0.89999998;stroke-width:1.06666672">mode diff --git a/misc/qutebrowser.spec b/misc/qutebrowser.spec index 81983aa0b..fe58891ac 100644 --- a/misc/qutebrowser.spec +++ b/misc/qutebrowser.spec @@ -70,6 +70,9 @@ coll = COLLECT(exe, app = BUNDLE(coll, name='qutebrowser.app', icon=icon, - info_plist={'NSHighResolutionCapable': 'True'}, + info_plist={ + 'NSHighResolutionCapable': 'True', + 'NSSupportsAutomaticGraphicsSwitching': 'True', + }, # https://github.com/pyinstaller/pyinstaller/blob/b78bfe530cdc2904f65ce098bdf2de08c9037abb/PyInstaller/hooks/hook-PyQt5.QtWebEngineWidgets.py#L24 bundle_identifier='org.qt-project.Qt.QtWebEngineCore') diff --git a/misc/requirements/requirements-check-manifest.txt b/misc/requirements/requirements-check-manifest.txt index 02d868876..42724e0c7 100644 --- a/misc/requirements/requirements-check-manifest.txt +++ b/misc/requirements/requirements-check-manifest.txt @@ -1,3 +1,3 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -check-manifest==0.34 +check-manifest==0.35 diff --git a/misc/requirements/requirements-codecov.txt b/misc/requirements/requirements-codecov.txt index 4f9fc5b12..4f020b5cd 100644 --- a/misc/requirements/requirements-codecov.txt +++ b/misc/requirements/requirements-codecov.txt @@ -1,5 +1,5 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py codecov==2.0.5 -coverage==4.2 -requests==2.12.1 +coverage==4.3.4 +requests==2.13.0 diff --git a/misc/requirements/requirements-flake8.txt b/misc/requirements/requirements-flake8.txt index 7fa392d0d..9c0aad110 100644 --- a/misc/requirements/requirements-flake8.txt +++ b/misc/requirements/requirements-flake8.txt @@ -4,19 +4,17 @@ flake8==2.6.2 # rq.filter: < 3.0.0 flake8-copyright==0.2.0 flake8-debugger==1.4.0 # rq.filter: != 2.0.0 flake8-deprecated==1.1 -flake8-docstrings==1.0.2 +flake8-docstrings==1.0.3 flake8-future-import==0.4.3 flake8-mock==0.3 -flake8-pep3101==0.6 +flake8-pep3101==1.0 +flake8-polyfill==1.0.1 flake8-putty==0.4.0 flake8-string-format==0.2.3 -flake8-tidy-imports==1.0.3 +flake8-tidy-imports==1.0.6 flake8-tuple==0.2.12 -mccabe==0.5.2 -packaging==16.8 +mccabe==0.6.1 pep8-naming==0.4.1 -pycodestyle==2.2.0 +pycodestyle==2.3.1 pydocstyle==1.1.1 -pyflakes==1.3.0 -pyparsing==2.1.10 -six==1.10.0 +pyflakes==1.5.0 diff --git a/misc/requirements/requirements-flake8.txt-raw b/misc/requirements/requirements-flake8.txt-raw index 73c4eafb4..cf660a8ce 100644 --- a/misc/requirements/requirements-flake8.txt-raw +++ b/misc/requirements/requirements-flake8.txt-raw @@ -15,7 +15,9 @@ pydocstyle pyflakes # Pinned to 2.0.0 otherwise -pycodestyle==2.2.0 +pycodestyle==2.3.1 +# Pinned to 0.5.3 otherwise +mccabe==0.6.1 # Waiting until flake8-putty updated #@ filter: flake8 < 3.0.0 diff --git a/misc/requirements/requirements-pip.txt b/misc/requirements/requirements-pip.txt new file mode 100644 index 000000000..c35abcf09 --- /dev/null +++ b/misc/requirements/requirements-pip.txt @@ -0,0 +1,8 @@ +# This file is automatically generated by scripts/dev/recompile_requirements.py + +appdirs==1.4.3 +packaging==16.8 +pyparsing==2.2.0 +setuptools==34.3.2 +six==1.10.0 +wheel==0.29.0 diff --git a/misc/requirements/requirements-pyinstaller.txt b/misc/requirements/requirements-pyinstaller.txt index 166d2d10d..9b16d9413 100644 --- a/misc/requirements/requirements-pyinstaller.txt +++ b/misc/requirements/requirements-pyinstaller.txt @@ -1,3 +1,3 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py --e git+https://github.com/edrex/pyinstaller.git@0fedc28f65d74e1f5ece453abdfb5ad54e9ac5ba#egg=PyInstaller +-e git+https://github.com/pyinstaller/pyinstaller.git@develop#egg=PyInstaller diff --git a/misc/requirements/requirements-pyinstaller.txt-raw b/misc/requirements/requirements-pyinstaller.txt-raw index c1bf7598d..5ccd44ec7 100644 --- a/misc/requirements/requirements-pyinstaller.txt-raw +++ b/misc/requirements/requirements-pyinstaller.txt-raw @@ -1,2 +1,4 @@ -# https://github.com/pyinstaller/pyinstaller/pull/2238 --e git+https://github.com/edrex/pyinstaller.git@1984_add_QtWebEngineCore#egg=PyInstaller +-e git+https://github.com/pyinstaller/pyinstaller.git@develop#egg=PyInstaller + +# remove @commit-id for scm installs +#@ replace: @.*# @develop# \ No newline at end of file diff --git a/misc/requirements/requirements-pylint-master.txt b/misc/requirements/requirements-pylint-master.txt index 862e0abca..64963a705 100644 --- a/misc/requirements/requirements-pylint-master.txt +++ b/misc/requirements/requirements-pylint-master.txt @@ -4,9 +4,8 @@ editdistance==0.3.1 isort==4.2.5 lazy-object-proxy==1.2.2 -mccabe==0.5.2 +mccabe==0.6.1 -e git+https://github.com/PyCQA/pylint.git#egg=pylint ./scripts/dev/pylint_checkers -requests==2.12.1 -six==1.10.0 -wrapt==1.10.8 +requests==2.13.0 +wrapt==1.10.10 diff --git a/misc/requirements/requirements-pylint.txt b/misc/requirements/requirements-pylint.txt index 0aafe2388..12d94c8eb 100644 --- a/misc/requirements/requirements-pylint.txt +++ b/misc/requirements/requirements-pylint.txt @@ -1,14 +1,13 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -astroid==1.4.8 +astroid==1.4.9 github3.py==0.9.6 isort==4.2.5 lazy-object-proxy==1.2.2 -mccabe==0.5.2 -pylint==1.6.4 +mccabe==0.6.1 +pylint==1.6.5 ./scripts/dev/pylint_checkers -requests==2.12.1 -six==1.10.0 +requests==2.13.0 uritemplate==3.0.0 uritemplate.py==3.0.2 -wrapt==1.10.8 +wrapt==1.10.10 diff --git a/misc/requirements/requirements-pyqt.txt b/misc/requirements/requirements-pyqt.txt new file mode 100644 index 000000000..774c0d79c --- /dev/null +++ b/misc/requirements/requirements-pyqt.txt @@ -0,0 +1,4 @@ +# This file is automatically generated by scripts/dev/recompile_requirements.py + +PyQt5==5.8.1.1 +sip==4.19.1 diff --git a/misc/requirements/requirements-pyqt.txt-raw b/misc/requirements/requirements-pyqt.txt-raw new file mode 100644 index 000000000..37a69c45a --- /dev/null +++ b/misc/requirements/requirements-pyqt.txt-raw @@ -0,0 +1 @@ +PyQt5 \ No newline at end of file diff --git a/misc/requirements/requirements-pyroma.txt b/misc/requirements/requirements-pyroma.txt index 889bf362f..9febd961b 100644 --- a/misc/requirements/requirements-pyroma.txt +++ b/misc/requirements/requirements-pyroma.txt @@ -1,4 +1,4 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -docutils==0.12 +docutils==0.13.1 pyroma==2.2 diff --git a/misc/requirements/requirements-tests-git.txt b/misc/requirements/requirements-tests-git.txt index fa89aa4de..68f93bc7a 100644 --- a/misc/requirements/requirements-tests-git.txt +++ b/misc/requirements/requirements-tests-git.txt @@ -1,5 +1,5 @@ bzr+lp:beautifulsoup -git+https://github.com/cherrypy/cherrypy.git +git+https://github.com/cherrypy/cheroot.git hg+https://bitbucket.org/ned/coveragepy git+https://github.com/micheles/decorator.git git+https://github.com/pallets/flask.git diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt index 9a6588eb9..f00f11f7c 100644 --- a/misc/requirements/requirements-tests.txt +++ b/misc/requirements/requirements-tests.txt @@ -1,23 +1,25 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -beautifulsoup4==4.5.1 -CherryPy==8.1.2 -click==6.6 -coverage==4.2 -decorator==4.0.10 -Flask==0.11.1 +beautifulsoup4==4.5.3 +cheroot==5.3.0 +click==6.7 +coverage==4.3.4 +decorator==4.0.11 +EasyProcess==0.2.3 +Flask==0.12 glob2==0.5 httpbin==0.5.0 -hypothesis==3.6.0 +hypothesis==3.6.1 itsdangerous==0.24 -# Jinja2==2.8 +# Jinja2==2.9.5 Mako==1.0.6 -# MarkupSafe==0.23 -parse==1.6.6 +# MarkupSafe==1.0 +parse==1.8.0 parse-type==0.3.4 -py==1.4.31 -pytest==3.0.4 +py==1.4.33 +pytest==3.0.7 pytest-bdd==2.18.1 +pytest-benchmark==3.0.0 pytest-catchlog==1.2.2 pytest-cov==2.4.0 pytest-faulthandler==1.3.1 @@ -28,7 +30,7 @@ pytest-repeat==0.4.1 pytest-rerunfailures==2.1.0 pytest-travis-fold==1.2.0 pytest-warnings==0.2.0 -pytest-xvfb==0.3.0 -six==1.10.0 -vulture==0.10 -Werkzeug==0.11.11 +pytest-xvfb==1.0.0 +PyVirtualDisplay==0.2.1 +vulture==0.13 +Werkzeug==0.12.1 diff --git a/misc/requirements/requirements-tests.txt-raw b/misc/requirements/requirements-tests.txt-raw index aaba28da6..d0f3bec52 100644 --- a/misc/requirements/requirements-tests.txt-raw +++ b/misc/requirements/requirements-tests.txt-raw @@ -1,11 +1,12 @@ beautifulsoup4 -CherryPy +cheroot coverage Flask httpbin hypothesis pytest pytest-bdd +pytest-benchmark pytest-catchlog pytest-cov pytest-faulthandler diff --git a/misc/requirements/requirements-tox.txt b/misc/requirements/requirements-tox.txt index 4768ddc09..df38c7fca 100644 --- a/misc/requirements/requirements-tox.txt +++ b/misc/requirements/requirements-tox.txt @@ -1,6 +1,6 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py pluggy==0.4.0 -py==1.4.31 -tox==2.5.0 +py==1.4.33 +tox==2.6.0 virtualenv==15.1.0 diff --git a/misc/requirements/requirements-vulture.txt b/misc/requirements/requirements-vulture.txt index 4b19ae298..26ee55a6e 100644 --- a/misc/requirements/requirements-vulture.txt +++ b/misc/requirements/requirements-vulture.txt @@ -1,3 +1,3 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -vulture==0.10 +vulture==0.13 diff --git a/misc/userscripts/ripbang b/misc/userscripts/ripbang new file mode 100755 index 000000000..4b418443d --- /dev/null +++ b/misc/userscripts/ripbang @@ -0,0 +1,27 @@ +#!/usr/bin/env python2 +# +# Adds DuckDuckGo bang as searchengine. +# +# Usage: +# :spawn --userscript ripbang [bang]... +# +# Example: +# :spawn --userscript ripbang amazon maps +# +import os, re, requests, sys, urllib + +for argument in sys.argv[1:]: + bang = '!' + argument + r = requests.get('https://duckduckgo.com/', + params={'q': bang + ' SEARCHTEXT'}) + + searchengine = urllib.unquote(re.search("url=[^']+", r.text).group(0)) + searchengine = searchengine.replace('url=', '') + searchengine = searchengine.replace('/l/?kh=-1&uddg=', '') + searchengine = searchengine.replace('SEARCHTEXT', '{}') + + if os.getenv('QUTE_FIFO'): + with open(os.environ['QUTE_FIFO'], 'w') as fifo: + fifo.write('set searchengines %s %s' % (bang, searchengine)) + else: + print '%s %s' % (bang, searchengine) diff --git a/pytest.ini b/pytest.ini index 2285053a9..bd57e7854 100644 --- a/pytest.ini +++ b/pytest.ini @@ -17,10 +17,13 @@ markers = qtwebengine_todo: Features still missing with QtWebEngine qtwebengine_skip: Tests not applicable with QtWebEngine qtwebkit_skip: Tests not applicable with QtWebKit + qtwebkit_ng_xfail: Tests failing with QtWebKit-NG + qtwebkit_ng_skip: Tests skipped with QtWebKit-NG qtwebengine_flaky: Tests which are flaky (and currently skipped) with QtWebEngine qtwebengine_osx_xfail: Tests which fail on OS X with QtWebEngine js_prompt: Tests needing to display a javascript prompt this: Used to mark tests during development + no_invalid_lines: Don't fail on unparseable lines in end2end tests qt_log_level_fail = WARNING qt_log_ignore = ^SpellCheck: .* @@ -46,4 +49,6 @@ qt_log_ignore = ^load glyph failed ^Error when parsing the netrc file ^Image of format '' blocked because it is not considered safe. If you are sure it is safe to do so, you can white-list the format by setting the environment variable QTWEBKIT_IMAGEFORMAT_WHITELIST= + ^QPainter::end: Painter ended with \d+ saved states + ^QSslSocket: cannot resolve SSLv[23]_(client|server)_method xfail_strict = true diff --git a/qutebrowser.desktop b/qutebrowser.desktop index 0dbfa2db7..e505774a8 100644 --- a/qutebrowser.desktop +++ b/qutebrowser.desktop @@ -8,3 +8,4 @@ Exec=qutebrowser %u Terminal=false StartupNotify=false MimeType=text/html;text/xml;application/xhtml+xml;application/xml;application/rdf+xml;image/gif;image/jpeg;image/png;x-scheme-handler/http;x-scheme-handler/https; +Keywords=Browser diff --git a/qutebrowser/__init__.py b/qutebrowser/__init__.py index 8be034d21..348cf407a 100644 --- a/qutebrowser/__init__.py +++ b/qutebrowser/__init__.py @@ -17,9 +17,7 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -# pylint: disable=line-too-long - -"""A keyboard-driven, vim-like browser based on PyQt5 and QtWebKit.""" +"""A keyboard-driven, vim-like browser based on PyQt5.""" import os.path @@ -28,8 +26,8 @@ __copyright__ = "Copyright 2014-2016 Florian Bruhin (The Compiler)" __license__ = "GPL" __maintainer__ = __author__ __email__ = "mail@qutebrowser.org" -__version_info__ = (0, 8, 4) +__version_info__ = (0, 10, 1) __version__ = '.'.join(str(e) for e in __version_info__) -__description__ = "A keyboard-driven, vim-like browser based on PyQt5 and QtWebKit." +__description__ = "A keyboard-driven, vim-like browser based on PyQt5." basedir = os.path.dirname(os.path.realpath(__file__)) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index edc19931c..73215cbfe 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -47,6 +47,7 @@ from qutebrowser.commands import cmdutils, runners, cmdexc from qutebrowser.config import style, config, websettings, configexc from qutebrowser.browser import (urlmarks, adblock, history, browsertab, downloads) +from qutebrowser.browser.network import proxy from qutebrowser.browser.webkit import cookies, cache from qutebrowser.browser.webkit.network import networkmanager from qutebrowser.keyinput import macros @@ -132,7 +133,6 @@ def init(args, crash_handler): log.init.debug("Starting init...") qApp.setQuitOnLastWindowClosed(False) _init_icon() - utils.actute_warning() try: _init_modules(args, crash_handler) @@ -141,9 +141,6 @@ def init(args, crash_handler): pre_text="Error while initializing") sys.exit(usertypes.Exit.err_init) - QTimer.singleShot(0, functools.partial(_process_args, args)) - QTimer.singleShot(10, functools.partial(_init_late_modules, args)) - log.init.debug("Initializing eventfilter...") event_filter = EventFilter(qApp) qApp.installEventFilter(event_filter) @@ -154,11 +151,13 @@ def init(args, crash_handler): config_obj.style_changed.connect(style.get_stylesheet.cache_clear) qApp.focusChanged.connect(on_focus_changed) + _process_args(args) + QDesktopServices.setUrlHandler('http', open_desktopservices_url) QDesktopServices.setUrlHandler('https', open_desktopservices_url) QDesktopServices.setUrlHandler('qute', open_desktopservices_url) - macros.init() + QTimer.singleShot(10, functools.partial(_init_late_modules, args)) log.init.debug("Init done!") crash_handler.raise_crashdlg() @@ -213,14 +212,17 @@ def _load_session(name): name: The name of the session to load, or None to read state file. """ state_config = objreg.get('state-config') - if name is None: + session_manager = objreg.get('session-manager') + if name is None and session_manager.exists('_autosave'): + name = '_autosave' + elif name is None: try: name = state_config['general']['session'] except KeyError: # No session given as argument and none in the session file -> # start without loading a session return - session_manager = objreg.get('session-manager') + try: session_manager.load(name) except sessions.SessionNotFoundError: @@ -375,6 +377,7 @@ def _init_modules(args, crash_handler): args: The argparse namespace. crash_handler: The CrashHandler instance. """ + # pylint: disable=too-many-statements log.init.debug("Initializing prompts...") prompt.init() @@ -386,6 +389,11 @@ def _init_modules(args, crash_handler): log.init.debug("Initializing network...") networkmanager.init() + if qtutils.version_check('5.8'): + # Otherwise we can only initialize it for QtWebKit because of crashes + log.init.debug("Initializing proxy...") + proxy.init() + log.init.debug("Initializing readline-bridge...") readline_bridge = readline.ReadlineBridge() objreg.register('readline-bridge', readline_bridge) @@ -405,7 +413,7 @@ def _init_modules(args, crash_handler): sessions.init(qApp) log.init.debug("Initializing websettings...") - websettings.init() + websettings.init(args) log.init.debug("Initializing adblock...") host_blocker = adblock.HostBlocker() @@ -438,8 +446,9 @@ def _init_modules(args, crash_handler): os.environ['QT_WAYLAND_DISABLE_WINDOWDECORATION'] = '1' else: os.environ.pop('QT_WAYLAND_DISABLE_WINDOWDECORATION', None) + macros.init() # Init backend-specific stuff - browsertab.init(args) + browsertab.init() def _init_late_modules(args): @@ -527,7 +536,7 @@ class Quitter: if not os.path.isdir(cwd): # Probably running from a python egg. Let's fallback to # cwd=None and see if that works out. - # See https://github.com/The-Compiler/qutebrowser/issues/323 + # See https://github.com/qutebrowser/qutebrowser/issues/323 cwd = None # Add all open pages so they get reopened. @@ -713,6 +722,7 @@ class Quitter: # Now we can hopefully quit without segfaults log.destroy.debug("Deferring QApplication::exit...") objreg.get('signal-handler').deactivate() + objreg.get('session-manager').delete_autosave() # We use a singleshot timer to exit here to minimize the likelihood of # segfaults. QTimer.singleShot(0, functools.partial(qApp.exit, status)) diff --git a/qutebrowser/browser/adblock.py b/qutebrowser/browser/adblock.py index 06d0cabe8..a7d0d43bb 100644 --- a/qutebrowser/browser/adblock.py +++ b/qutebrowser/browser/adblock.py @@ -58,7 +58,7 @@ def get_fileobj(byte_io): byte_io = zf.open(filename, mode='r') else: byte_io.seek(0) # rewind what zipfile.is_zipfile did - return io.TextIOWrapper(byte_io, encoding='utf-8') + return byte_io def is_whitelisted_host(host): @@ -147,7 +147,7 @@ class HostBlocker: with open(filename, 'r', encoding='utf-8') as f: for line in f: target.add(line.strip()) - except OSError: + except (OSError, UnicodeDecodeError): log.misc.exception("Failed to read host blocklist!") return True @@ -165,7 +165,8 @@ class HostBlocker: if not found: args = objreg.get('args') if (config.get('content', 'host-block-lists') is not None and - args.basedir is None): + args.basedir is None and + config.get('content', 'host-blocking-enabled')): message.info("Run :adblock-update to get adblock lists.") @cmdutils.register(instance='host-blocker') @@ -205,6 +206,54 @@ class HostBlocker: download.finished.connect( functools.partial(self.on_download_finished, download)) + def _parse_line(self, line): + """Parse a line from a host file. + + Args: + line: The bytes object to parse. + + Returns: + True if parsing succeeded, False otherwise. + """ + if line.startswith(b'#'): + # Ignoring comments early so we don't have to care about + # encoding errors in them. + return True + + try: + line = line.decode('utf-8') + except UnicodeDecodeError: + log.misc.error("Failed to decode: {!r}".format(line)) + return False + + # Remove comments + try: + hash_idx = line.index('#') + line = line[:hash_idx] + except ValueError: + pass + + line = line.strip() + # Skip empty lines + if not line: + return True + + parts = line.split() + if len(parts) == 1: + # "one host per line" format + host = parts[0] + elif len(parts) == 2: + # /etc/hosts format + host = parts[1] + else: + log.misc.error("Failed to parse: {!r}".format(line)) + return False + + if host not in self.WHITELISTED: + self._blocked_hosts.add(host) + + return True + def _merge_file(self, byte_io): """Read and merge host files. @@ -218,35 +267,18 @@ class HostBlocker: line_count = 0 try: f = get_fileobj(byte_io) - except (OSError, UnicodeDecodeError, zipfile.BadZipFile, - zipfile.LargeZipFile) as e: + except (OSError, zipfile.BadZipFile, zipfile.LargeZipFile, + LookupError) as e: message.error("adblock: Error while reading {}: {} - {}".format( byte_io.name, e.__class__.__name__, e)) return + for line in f: line_count += 1 - # Remove comments - try: - hash_idx = line.index('#') - line = line[:hash_idx] - except ValueError: - pass - line = line.strip() - # Skip empty lines - if not line: - continue - parts = line.split() - if len(parts) == 1: - # "one host per line" format - host = parts[0] - elif len(parts) == 2: - # /etc/hosts format - host = parts[1] - else: + ok = self._parse_line(line) + if not ok: error_count += 1 - continue - if host not in self.WHITELISTED: - self._blocked_hosts.add(host) + log.misc.debug("{}: read {} lines".format(byte_io.name, line_count)) if error_count > 0: message.error("adblock: {} read errors for {}".format( diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index 6e32cab2b..48aacf086 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -28,7 +28,7 @@ from PyQt5.QtWidgets import QWidget, QApplication from qutebrowser.keyinput import modeman from qutebrowser.config import config from qutebrowser.utils import utils, objreg, usertypes, log, qtutils -from qutebrowser.misc import miscwidgets +from qutebrowser.misc import miscwidgets, objects from qutebrowser.browser import mouse, hints @@ -45,7 +45,7 @@ def create(win_id, parent=None): # Importing modules here so we don't depend on QtWebEngine without the # argument and to avoid circular imports. mode_manager = modeman.instance(win_id) - if objreg.get('args').backend == 'webengine': + if objects.backend == usertypes.Backend.QtWebEngine: from qutebrowser.browser.webengine import webenginetab tab_class = webenginetab.WebEngineTab else: @@ -54,9 +54,9 @@ def create(win_id, parent=None): return tab_class(win_id=win_id, mode_manager=mode_manager, parent=parent) -def init(args): +def init(): """Initialize backend-specific modules.""" - if args.backend == 'webengine': + if objects.backend == usertypes.Backend.QtWebEngine: from qutebrowser.browser.webengine import webenginetab webenginetab.init() else: @@ -74,6 +74,15 @@ class UnsupportedOperationError(WebTabError): """Raised when an operation is not supported with the given backend.""" +TerminationStatus = usertypes.enum('TerminationStatus', [ + 'normal', + 'abnormal', # non-zero exit status + 'crashed', # e.g. segfault + 'killed', + 'unknown', +]) + + class TabData: """A simple namespace with a fixed set of attributes. @@ -96,6 +105,22 @@ class TabData: self.pinned = False +class AbstractAction: + + """Attribute of AbstractTab for Qt WebActions.""" + + def __init__(self): + self._widget = None + + def exit_fullscreen(self): + """Exit the fullscreen mode.""" + raise NotImplementedError + + def save_page(self): + """Save the current page.""" + raise NotImplementedError + + class AbstractPrinting: """Attribute of AbstractTab for printing the page.""" @@ -109,10 +134,20 @@ class AbstractPrinting: def check_printer_support(self): raise NotImplementedError + def check_preview_support(self): + raise NotImplementedError + def to_pdf(self, filename): raise NotImplementedError - def to_printer(self, printer): + def to_printer(self, printer, callback=None): + """Print the tab. + + Args: + printer: The QPrinter to print to. + callback: Called with a boolean + (True if printing succeeded, False otherwise) + """ raise NotImplementedError @@ -184,7 +219,7 @@ class AbstractZoom(QObject): # # FIXME:qtwebengine is this needed? # # For some reason, this signal doesn't get disconnected automatically # # when the WebView is destroyed on older PyQt versions. - # # See https://github.com/The-Compiler/qutebrowser/issues/390 + # # See https://github.com/qutebrowser/qutebrowser/issues/390 # self.destroyed.connect(functools.partial( # cfg.changed.disconnect, self.init_neighborlist)) @@ -217,6 +252,9 @@ class AbstractZoom(QObject): self.set_factor(float(level) / 100, fuzzyval=False) return level + def _set_factor_internal(self, factor): + raise NotImplementedError + def set_factor(self, factor, *, fuzzyval=True): """Zoom to a given zoom factor. @@ -485,10 +523,6 @@ class AbstractTab(QWidget): We use this to unify QWebView and QWebEngineView. - Class attributes: - WIDGET_CLASS: The class of the main widget recieving events. - Needs to be overridden by subclasses. - Attributes: history: The AbstractHistory for the current tab. registry: The ObjectRegistry associated with this tab. @@ -506,6 +540,13 @@ class AbstractTab(QWidget): new_tab_requested: Emitted when a new tab should be opened with the given URL. load_status_changed: The loading status changed + fullscreen_requested: Fullscreen display was requested by the page. + arg: True if fullscreen should be turned on, + False if it should be turned off. + renderer_process_terminated: Emitted when the underlying renderer + process terminated. + arg 0: A TerminationStatus member. + arg 1: The exit code. """ window_close_requested = pyqtSignal() @@ -521,8 +562,8 @@ class AbstractTab(QWidget): shutting_down = pyqtSignal() contents_size_changed = pyqtSignal(QSizeF) add_history_item = pyqtSignal(QUrl, QUrl, str) # url, requested url, title - - WIDGET_CLASS = None + fullscreen_requested = pyqtSignal(bool) + renderer_process_terminated = pyqtSignal(TerminationStatus, int) def __init__(self, win_id, mode_manager, parent=None): self.win_id = win_id @@ -543,6 +584,7 @@ class AbstractTab(QWidget): # self.search = AbstractSearch(parent=self) # self.printing = AbstractPrinting() # self.elements = AbstractElements(self) + # self.action = AbstractAction() self.data = TabData() self._layout = miscwidgets.WrapperLayout(self) @@ -552,7 +594,7 @@ class AbstractTab(QWidget): self._mode_manager = mode_manager self._load_status = usertypes.LoadStatus.none self._mouse_event_filter = mouse.MouseEventFilter( - self, widget_class=self.WIDGET_CLASS, parent=self) + self, parent=self) self.backend = None # FIXME:qtwebengine Should this be public api via self.hints? @@ -571,8 +613,11 @@ class AbstractTab(QWidget): self.zoom._widget = widget self.search._widget = widget self.printing._widget = widget + self.action._widget = widget self.elements._widget = widget + self._install_event_filter() + self.zoom.set_default() def _install_event_filter(self): raise NotImplementedError @@ -585,7 +630,7 @@ class AbstractTab(QWidget): self._load_status = val self.load_status_changed.emit(val.name) - def _event_target(self): + def event_target(self): """Return the widget events should be sent to.""" raise NotImplementedError @@ -600,7 +645,7 @@ class AbstractTab(QWidget): if getattr(evt, 'posted', False): raise AssertionError("Can't re-use an event which was already " "posted!") - recipient = self._event_target() + recipient = self.event_target() evt.posted = True QApplication.postEvent(recipient, evt) @@ -641,12 +686,14 @@ class AbstractTab(QWidget): @pyqtSlot(bool) def _on_load_finished(self, ok): + sess_manager = objreg.get('session-manager') + sess_manager.save_autosave() + if ok and not self._has_ssl_errors: if self.url().scheme() == 'https': self._set_load_status(usertypes.LoadStatus.success_https) else: self._set_load_status(usertypes.LoadStatus.success) - elif ok: self._set_load_status(usertypes.LoadStatus.warn) else: @@ -737,6 +784,14 @@ class AbstractTab(QWidget): """ raise NotImplementedError + def user_agent(self): + """Get the user agent for this tab. + + This is only implemented for QtWebKit. + For QtWebEngine, always returns None. + """ + raise NotImplementedError + def __repr__(self): try: url = utils.elide(self.url().toDisplayString(QUrl.EncodeUnicode), diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index b0c8dc9e9..e6f6571c0 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -44,12 +44,6 @@ from qutebrowser.commands import userscripts, cmdexc, cmdutils, runners from qutebrowser.config import config, configexc from qutebrowser.browser import (urlmarks, browsertab, inspector, navigate, webelem, downloads) -try: - from qutebrowser.browser.webkit import mhtml -except ImportError: - # Failing imports on QtWebEngine, only used in QtWebKit commands. - # FIXME:qtwebengine don't import this anymore at all - pass from qutebrowser.keyinput import modeman from qutebrowser.utils import (message, usertypes, log, qtutils, urlutils, objreg, utils, typing) @@ -310,13 +304,10 @@ class CommandDispatcher: count: The tab index to open the URL in, or None. """ if url is None: - if tab or bg or window: - urls = [config.get('general', 'default-page')] - else: - raise cmdexc.CommandError("No URL given, but -t/-b/-w is not " - "set!") + urls = [config.get('general', 'default-page')] else: urls = self._parse_url_input(url) + for i, cur_url in enumerate(urls): if not window and i > 0: tab = False @@ -407,6 +398,43 @@ class CommandDispatcher: if tab is not None: tab.stop() + def _print_preview(self, tab): + """Show a print preview.""" + def print_callback(ok): + if not ok: + message.error("Printing failed!") + + tab.printing.check_preview_support() + diag = QPrintPreviewDialog(tab) + diag.setAttribute(Qt.WA_DeleteOnClose) + diag.setWindowFlags(diag.windowFlags() | Qt.WindowMaximizeButtonHint | + Qt.WindowMinimizeButtonHint) + diag.paintRequested.connect(functools.partial( + tab.printing.to_printer, callback=print_callback)) + diag.exec_() + + def _print_pdf(self, tab, filename): + """Print to the given PDF file.""" + tab.printing.check_pdf_support() + filename = os.path.expanduser(filename) + directory = os.path.dirname(filename) + if directory and not os.path.exists(directory): + os.mkdir(directory) + tab.printing.to_pdf(filename) + log.misc.debug("Print to file: {}".format(filename)) + + def _print(self, tab): + """Print with a QPrintDialog.""" + def print_callback(ok): + """Called when printing finished.""" + if not ok: + message.error("Printing failed!") + diag.deleteLater() + + diag = QPrintDialog(tab) + diag.open(lambda: tab.printing.to_printer(diag.printer(), + print_callback)) + @cmdutils.register(instance='command-dispatcher', name='print', scope='window') @cmdutils.argument('count', count=True) @@ -428,28 +456,17 @@ class CommandDispatcher: tab.printing.check_pdf_support() else: tab.printing.check_printer_support() + if preview: + tab.printing.check_preview_support() except browsertab.WebTabError as e: raise cmdexc.CommandError(e) if preview: - diag = QPrintPreviewDialog() - diag.setAttribute(Qt.WA_DeleteOnClose) - diag.setWindowFlags(diag.windowFlags() | - Qt.WindowMaximizeButtonHint | - Qt.WindowMinimizeButtonHint) - diag.paintRequested.connect(tab.printing.to_printer) - diag.exec_() + self._print_preview(tab) elif pdf: - pdf = os.path.expanduser(pdf) - directory = os.path.dirname(pdf) - if directory and not os.path.exists(directory): - os.mkdir(directory) - tab.printing.to_pdf(pdf) - log.misc.debug("Print to file: {}".format(pdf)) + self._print_pdf(tab, pdf) else: - diag = QPrintDialog() - diag.setAttribute(Qt.WA_DeleteOnClose) - diag.open(lambda: tab.printing.to_printer(diag.printer())) + self._print(tab) @cmdutils.register(instance='command-dispatcher', scope='window') def tab_clone(self, bg=False, window=False): @@ -465,6 +482,11 @@ class CommandDispatcher: cmdutils.check_exclusive((bg, window), 'bw') curtab = self._current_widget() cur_title = self._tabbed_browser.page_title(self._current_index()) + try: + history = curtab.history.serialize() + except browsertab.WebTabError as e: + raise cmdexc.CommandError(e) + # The new tab could be in a new tabbed_browser (e.g. because of # tabs-are-windows being set) if window: @@ -475,13 +497,15 @@ class CommandDispatcher: new_tabbed_browser = objreg.get('tabbed-browser', scope='window', window=newtab.win_id) idx = new_tabbed_browser.indexOf(newtab) + new_tabbed_browser.set_page_title(idx, cur_title) if config.get('tabs', 'show-favicons'): new_tabbed_browser.setTabIcon(idx, curtab.icon()) if config.get('tabs', 'tabs-are-windows'): new_tabbed_browser.window().setWindowIcon(curtab.icon()) + newtab.data.keep_icon = True - newtab.history.deserialize(curtab.history.serialize()) + newtab.history.deserialize(history) newtab.zoom.set_factor(curtab.zoom.factor()) return newtab @@ -626,6 +650,9 @@ class CommandDispatcher: def scroll(self, direction: typing.Union[str, int], count=1): """Scroll the current tab in the given direction. + Note you can use `:run-with-count` to have a keybinding with a bigger + scroll increment. + Args: direction: In which direction to scroll (up/down/left/right/top/bottom). @@ -710,7 +737,7 @@ class CommandDispatcher: """ tab = self._current_widget() if not tab.url().isValid(): - # See https://github.com/The-Compiler/qutebrowser/issues/701 + # See https://github.com/qutebrowser/qutebrowser/issues/701 return if bottom_navigate is not None and tab.scroller.at_bottom(): @@ -813,7 +840,7 @@ class CommandDispatcher: perc = tab.zoom.offset(count) except ValueError as e: raise cmdexc.CommandError(e) - message.info("Zoom level: {}%".format(perc)) + message.info("Zoom level: {}%".format(perc), replace=True) @cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.argument('count', count=True) @@ -828,7 +855,7 @@ class CommandDispatcher: perc = tab.zoom.offset(-count) except ValueError as e: raise cmdexc.CommandError(e) - message.info("Zoom level: {}%".format(perc)) + message.info("Zoom level: {}%".format(perc), replace=True) @cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.argument('count', count=True) @@ -852,7 +879,7 @@ class CommandDispatcher: tab.zoom.set_factor(float(level) / 100) except ValueError: raise cmdexc.CommandError("Can't zoom {}%!".format(level)) - message.info("Zoom level: {}%".format(level)) + message.info("Zoom level: {}%".format(level), replace=True) @cmdutils.register(instance='command-dispatcher', scope='window') def tab_only(self, prev=False, next_=False): @@ -891,7 +918,7 @@ class CommandDispatcher: """ if self._count() == 0: # Running :tab-prev after last tab was closed - # See https://github.com/The-Compiler/qutebrowser/issues/1448 + # See https://github.com/qutebrowser/qutebrowser/issues/1448 return newidx = self._current_index() - count if newidx >= 0: @@ -911,7 +938,7 @@ class CommandDispatcher: """ if self._count() == 0: # Running :tab-next after last tab was closed - # See https://github.com/The-Compiler/qutebrowser/issues/1448 + # See https://github.com/qutebrowser/qutebrowser/issues/1448 return newidx = self._current_index() + count if newidx < self._count(): @@ -1014,7 +1041,7 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.argument('index', choices=['last']) - @cmdutils.argument('count', count=True, zero_count=True) + @cmdutils.argument('count', count=True) def tab_focus(self, index: typing.Union[str, int]=None, count=None): """Select the tab given as argument/[count]. @@ -1027,7 +1054,6 @@ class CommandDispatcher: Negative indices count from the end, such that -1 is the last tab. count: The tab index to focus, starting with 1. - The special value 0 focuses the rightmost tab. """ if index == 'last': self._tab_focus_last() @@ -1037,9 +1063,8 @@ class CommandDispatcher: if index is None: self.tab_next() return - elif index == 0: - index = self._count() - elif index < 0: + + if index < 0: index = self._count() + index + 1 if 1 <= index <= self._count(): @@ -1088,21 +1113,10 @@ class CommandDispatcher: raise cmdexc.CommandError("Can't move tab to position {}!".format( new_idx + 1)) - tab = self._current_widget() cur_idx = self._current_index() - icon = self._tabbed_browser.tabIcon(cur_idx) - label = self._tabbed_browser.page_title(cur_idx) cmdutils.check_overflow(cur_idx, 'int') cmdutils.check_overflow(new_idx, 'int') - self._tabbed_browser.setUpdatesEnabled(False) - try: - color = self._tabbed_browser.tab_indicator_color(cur_idx) - self._tabbed_browser.removeTab(cur_idx) - self._tabbed_browser.insertTab(new_idx, tab, icon, label) - self._set_current_index(new_idx) - self._tabbed_browser.set_tab_indicator_color(new_idx, color) - finally: - self._tabbed_browser.setUpdatesEnabled(True) + self._tabbed_browser.tabBar().moveTab(cur_idx, new_idx) @cmdutils.register(instance='command-dispatcher', scope='window', maxsplit=0, no_replace_variables=True) @@ -1373,58 +1387,39 @@ class CommandDispatcher: # FIXME:qtwebengine do this with the QtWebEngine download manager? download_manager = objreg.get('qtnetwork-download-manager', scope='window', window=self._win_id) + target = None + if dest is not None: + target = downloads.FileDownloadTarget(dest) + + tab = self._current_widget() + user_agent = tab.user_agent() + if url: if mhtml_: raise cmdexc.CommandError("Can only download the current page" " as mhtml.") url = urlutils.qurl_from_user_input(url) urlutils.raise_cmdexc_if_invalid(url) - if dest is None: - target = None - else: - target = downloads.FileDownloadTarget(dest) - download_manager.get(url, target=target) + download_manager.get(url, user_agent=user_agent, target=target) elif mhtml_: - self._download_mhtml(dest) - else: - qnam = self._current_widget().networkaccessmanager() - - if dest is None: - target = None + tab = self._current_widget() + if tab.backend == usertypes.Backend.QtWebEngine: + webengine_download_manager = objreg.get( + 'webengine-download-manager') + try: + webengine_download_manager.get_mhtml(tab, target) + except browsertab.UnsupportedOperationError as e: + raise cmdexc.CommandError(e) else: - target = downloads.FileDownloadTarget(dest) - download_manager.get(self._current_url(), qnam=qnam, target=target) - - def _download_mhtml(self, dest=None): - """Download the current page as an MHTML file, including all assets. - - Args: - dest: The file path to write the download to. - """ - tab = self._current_widget() - if tab.backend == usertypes.Backend.QtWebEngine: - raise cmdexc.CommandError("Download --mhtml is not implemented " - "with QtWebEngine yet") - - if dest is None: - suggested_fn = self._current_title() + ".mht" - suggested_fn = utils.sanitize_filename(suggested_fn) - - filename = downloads.immediate_download_path() - if filename is not None: - mhtml.start_download_checked(filename, tab=tab) - else: - question = downloads.get_filename_question( - suggested_filename=suggested_fn, url=tab.url(), parent=tab) - question.answered.connect(functools.partial( - mhtml.start_download_checked, tab=tab)) - message.global_bridge.ask(question, blocking=False) + download_manager.get_mhtml(tab, target) else: - mhtml.start_download_checked(dest, tab=tab) + qnam = tab.networkaccessmanager() + download_manager.get(self._current_url(), user_agent=user_agent, + qnam=qnam, target=target) @cmdutils.register(instance='command-dispatcher', scope='window') def view_source(self): - """Show the source of the current page.""" + """Show the source of the current page in a new tab.""" # pylint: disable=no-member # WORKAROUND for https://bitbucket.org/logilab/pylint/issue/491/ tab = self._current_widget() @@ -1471,6 +1466,18 @@ class CommandDispatcher: tab.dump_async(callback, plain=plain) + @cmdutils.register(instance='command-dispatcher', scope='window') + def history(self, tab=True, bg=False, window=False): + """Show browsing history. + + Args: + tab: Open in a new tab. + bg: Open in a background tab. + window: Open in a new window. + """ + url = QUrl('qute://history/') + self._open(url, tab, bg, window) + @cmdutils.register(instance='command-dispatcher', name='help', scope='window') @cmdutils.argument('topic', completion=usertypes.Completion.helptopic) @@ -1544,6 +1551,10 @@ class CommandDispatcher: return text = elem.value() + if text is None: + message.error("Could not get text from the focused element.") + return + ed = editor.ExternalEditor(self._tabbed_browser) ed.editing_finished.connect(functools.partial( self.on_editing_finished, elem)) @@ -1612,7 +1623,8 @@ class CommandDispatcher: @cmdutils.argument('filter_', choices=['id']) def click_element(self, filter_: str, value, *, target: usertypes.ClickTarget= - usertypes.ClickTarget.normal): + usertypes.ClickTarget.normal, + force_event=False): """Click the element matching the given filter. The given filter needs to result in exactly one element, otherwise, an @@ -1623,6 +1635,7 @@ class CommandDispatcher: id: Get an element based on its ID. value: The value to filter for. target: How to open the clicked element (normal/tab/tab-bg/window). + force_event: Force generating a fake click event. """ tab = self._current_widget() @@ -1632,7 +1645,7 @@ class CommandDispatcher: message.error("No element found with id {}!".format(value)) return try: - elem.click(target) + elem.click(target, force_event=force_event) except webelem.Error as e: message.error(str(e)) return @@ -1972,12 +1985,13 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', scope='window', maxsplit=0, no_cmd_split=True) - def jseval(self, js_code, quiet=False, *, + def jseval(self, js_code, file=False, quiet=False, *, world: typing.Union[usertypes.JsWorld, int]=None): """Evaluate a JavaScript string. Args: - js_code: The string to evaluate. + js_code: The string/file to evaluate. + file: Interpret js-code as a path to a file. quiet: Don't show resulting JS object. world: Ignored on QtWebKit. On QtWebEngine, a world ID or name to run the snippet in. @@ -2005,6 +2019,13 @@ class CommandDispatcher: out = out[:5000] + ' [...trimmed...]' message.info(out) + if file: + try: + with open(js_code, 'r', encoding='utf-8') as f: + js_code = f.read() + except OSError as e: + raise cmdexc.CommandError(str(e)) + widget = self._current_widget() widget.run_js_async(js_code, callback=jseval_cb, world=world) @@ -2038,11 +2059,6 @@ class CommandDispatcher: QApplication.postEvent(window, press_event) QApplication.postEvent(window, release_event) else: - try: - tab = objreg.get('tab', scope='tab', tab='current') - except objreg.RegistryUnavailableError: - raise cmdexc.CommandError("No focused webview!") - tab = self._current_widget() tab.send_event(press_event) tab.send_event(release_event) @@ -2112,3 +2128,24 @@ class CommandDispatcher: """ if bg or tab or window or url != old_url: self.openurl(url=url, bg=bg, tab=tab, window=window) + + @cmdutils.register(instance='command-dispatcher', scope='window') + def fullscreen(self, leave=False): + """Toggle fullscreen mode. + + Args: + leave: Only leave fullscreen if it was entered by the page. + """ + if leave: + tab = self._current_widget() + try: + tab.action.exit_fullscreen() + except browsertab.UnsupportedOperationError: + pass + return + + window = self._tabbed_browser.window() + if window.isFullScreen(): + window.showNormal() + else: + window.showFullScreen() diff --git a/qutebrowser/browser/downloads.py b/qutebrowser/browser/downloads.py index 12e766ecb..0e6a7af4f 100644 --- a/qutebrowser/browser/downloads.py +++ b/qutebrowser/browser/downloads.py @@ -20,7 +20,6 @@ """Shared QtWebKit/QtWebEngine code for downloads.""" import sys -import shlex import html import os.path import collections @@ -28,15 +27,13 @@ import functools import tempfile import sip -from PyQt5.QtCore import (pyqtSlot, pyqtSignal, Qt, QObject, QUrl, QModelIndex, +from PyQt5.QtCore import (pyqtSlot, pyqtSignal, Qt, QObject, QModelIndex, QTimer, QAbstractListModel) -from PyQt5.QtGui import QDesktopServices from qutebrowser.commands import cmdexc, cmdutils from qutebrowser.config import config from qutebrowser.utils import (usertypes, standarddir, utils, message, log, qtutils) -from qutebrowser.misc import guiprocess ModelRole = usertypes.enum('ModelRole', ['item'], start=Qt.UserRole, @@ -128,7 +125,7 @@ def create_full_filename(basename, filename): 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 + # See https://github.com/qutebrowser/qutebrowser/issues/427 encoding = sys.getfilesystemencoding() filename = utils.force_encoding(filename, encoding) basename = utils.force_encoding(basename, encoding) @@ -158,7 +155,7 @@ def get_filename_question(*, suggested_filename, url, parent=None): q.title = "Save file to:" q.text = "Please enter a location for {}".format( html.escape(url.toDisplayString())) - q.mode = usertypes.PromptMode.text + q.mode = usertypes.PromptMode.download q.completed.connect(q.deleteLater) q.default = _path_suggestion(suggested_filename) return q @@ -197,6 +194,9 @@ class FileDownloadTarget(_DownloadTarget): def suggested_filename(self): return os.path.basename(self.filename) + def __str__(self): + return self.filename + class FileObjDownloadTarget(_DownloadTarget): @@ -216,6 +216,12 @@ class FileObjDownloadTarget(_DownloadTarget): except AttributeError: raise NoFilenameError + def __str__(self): + try: + return 'file object at {}'.format(self.fileobj.name) + except AttributeError: + return 'anonymous file object' + class OpenFileDownloadTarget(_DownloadTarget): @@ -234,6 +240,9 @@ class OpenFileDownloadTarget(_DownloadTarget): def suggested_filename(self): raise NoFilenameError + def __str__(self): + return 'temporary file' + class DownloadItemStats(QObject): @@ -512,30 +521,19 @@ class AbstractDownloadItem(QObject): Args: cmdline: The command to use as string. A `{}` is expanded to the filename. None means to use the system's default - application. If no `{}` is found, the filename is appended - to the cmdline. + application or `default-open-dispatcher` if set. If no + `{}` is found, the filename is appended to the cmdline. """ assert self.successful filename = self._get_open_filename() if filename is None: # pragma: no cover log.downloads.error("No filename to open the download!") return - - if cmdline is None: - log.downloads.debug("Opening {} with the system application" - .format(filename)) - url = QUrl.fromLocalFile(filename) - QDesktopServices.openUrl(url) - return - - cmd, *args = shlex.split(cmdline) - args = [arg.replace('{}', filename) for arg in args] - if '{}' not in cmdline: - args.append(filename) - log.downloads.debug("Opening {} with {}" - .format(filename, [cmd] + args)) - proc = guiprocess.GUIProcess(what='download') - proc.start_detached(cmd, args) + # By using a singleshot timer, we ensure that we return fast. This + # is important on systems where process creation takes long, as + # otherwise the prompt might hang around and cause bugs + # (see issue #2296) + QTimer.singleShot(0, lambda: utils.open_file(filename, cmdline)) def _ensure_can_set_filename(self, filename): """Make sure we can still set a filename.""" @@ -564,13 +562,16 @@ class AbstractDownloadItem(QObject): """Set a temporary file when opening the download.""" raise NotImplementedError - def _set_filename(self, filename, *, force_overwrite=False): + def _set_filename(self, filename, *, force_overwrite=False, + remember_directory=True): """Set the filename to save the download to. Args: filename: The full filename to save the download to. None: special value to stop the download. force_overwrite: Force overwriting existing files. + remember_directory: If True, remember the directory for future + downloads. """ global last_used_directory filename = os.path.expanduser(filename) @@ -600,7 +601,8 @@ class AbstractDownloadItem(QObject): os.path.expanduser('~')) self.basename = os.path.basename(self._filename) - last_used_directory = os.path.dirname(self._filename) + if remember_directory: + last_used_directory = os.path.dirname(self._filename) log.downloads.debug("Setting filename to {}".format(filename)) if force_overwrite: @@ -743,7 +745,7 @@ class AbstractDownloadManager(QObject): def _remove_item(self, download): """Remove a given download.""" if sip.isdeleted(self): - # https://github.com/The-Compiler/qutebrowser/issues/1242 + # https://github.com/qutebrowser/qutebrowser/issues/1242 return try: idx = self.downloads.index(download) @@ -767,7 +769,6 @@ class AbstractDownloadManager(QObject): def _init_filename_question(self, question, download): """Set up an existing filename question with a download.""" - question.mode = usertypes.PromptMode.download question.answered.connect(download.set_target) question.cancelled.connect(download.cancel) download.cancelled.connect(question.abort) diff --git a/qutebrowser/browser/downloadview.py b/qutebrowser/browser/downloadview.py index 783a6a94d..f2a380114 100644 --- a/qutebrowser/browser/downloadview.py +++ b/qutebrowser/browser/downloadview.py @@ -39,8 +39,8 @@ def update_geometry(obj): Here we check if obj ("self") was deleted and just ignore the event if so. - Original bug: https://github.com/The-Compiler/qutebrowser/issues/167 - Workaround bug: https://github.com/The-Compiler/qutebrowser/issues/171 + Original bug: https://github.com/qutebrowser/qutebrowser/issues/167 + Workaround bug: https://github.com/qutebrowser/qutebrowser/issues/171 """ def _update_geometry(): """Actually update the geometry if the object still exists.""" diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py index b895c7db1..144b13f76 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -101,7 +101,7 @@ class HintLabel(QLabel): unmatched: The part of the text which was not typed yet. """ if (config.get('hints', 'uppercase') and - self._context.hint_mode == 'letter'): + self._context.hint_mode in ['letter', 'word']): matched = html.escape(matched.upper()) unmatched = html.escape(unmatched.upper()) else: @@ -235,7 +235,10 @@ class HintActions: sel = (context.target == Target.yank_primary and utils.supports_selection()) - urlstr = url.toString(QUrl.FullyEncoded | QUrl.RemovePassword) + flags = QUrl.FullyEncoded | QUrl.RemovePassword + if url.scheme() == 'mailto': + flags |= QUrl.RemoveScheme + urlstr = url.toString(flags) utils.set_clipboard(urlstr, selection=sel) msg = "Yanked URL to {}: {}".format( @@ -284,11 +287,13 @@ class HintActions: prompt = False if context.rapid else None qnam = context.tab.networkaccessmanager() + user_agent = context.tab.user_agent() # FIXME:qtwebengine do this with QtWebEngine downloads? download_manager = objreg.get('qtnetwork-download-manager', scope='window', window=self._win_id) - download_manager.get(url, qnam=qnam, prompt_download_directory=prompt) + download_manager.get(url, qnam=qnam, user_agent=user_agent, + prompt_download_directory=prompt) def call_userscript(self, elem, context): """Call a userscript from a hint. @@ -311,7 +316,7 @@ class HintActions: try: userscripts.run_async(context.tab, cmd, *args, win_id=self._win_id, env=env) - except userscripts.UnsupportedError as e: + except userscripts.Error as e: raise HintingError(str(e)) def spawn(self, url, context): @@ -567,6 +572,10 @@ class HintManager(QObject): def _start_cb(self, elems): """Initialize the elements and labels based on the context set.""" + if self._context is None: + log.hints.debug("In _start_cb without context!") + return + if elems is None: message.error("There was an error while getting hint elements") return @@ -750,6 +759,9 @@ class HintManager(QObject): def handle_partial_key(self, keystr): """Handle a new partial keypress.""" + if self._context is None: + log.hints.debug("Got key without context!") + return log.hints.debug("Handling new keystring: '{}'".format(keystr)) for string, label in self._context.labels.items(): try: diff --git a/qutebrowser/browser/history.py b/qutebrowser/browser/history.py index 40c497823..2615fd393 100644 --- a/qutebrowser/browser/history.py +++ b/qutebrowser/browser/history.py @@ -26,9 +26,9 @@ from PyQt5.QtCore import pyqtSignal, pyqtSlot, QUrl, QObject from qutebrowser.commands import cmdutils from qutebrowser.utils import (utils, objreg, standarddir, log, qtutils, - usertypes) + usertypes, message) from qutebrowser.config import config -from qutebrowser.misc import lineparser +from qutebrowser.misc import lineparser, objects class Entry: @@ -88,7 +88,7 @@ class Entry: if not url.isValid(): raise ValueError("Invalid URL: {}".format(url.errorString())) - # https://github.com/The-Compiler/qutebrowser/issues/670 + # https://github.com/qutebrowser/qutebrowser/issues/670 atime = atime.lstrip('\0') if '-' in atime: @@ -230,13 +230,23 @@ class WebHistory(QObject): self._saved_count = len(self._new_history) @cmdutils.register(name='history-clear', instance='web-history') - def clear(self): + def clear(self, force=False): """Clear all browsing history. Note this only clears the global history (e.g. `~/.local/share/qutebrowser/history` on Linux) but not cookies, the back/forward history of a tab, cache or other persistent data. + + Args: + force: Don't ask for confirmation. """ + if force: + self._do_clear() + else: + message.confirm_async(self._do_clear, title="Clear all browsing " + "history?") + + def _do_clear(self): self._lineparser.clear() self.history_dict.clear() self._temp_history.clear() @@ -293,7 +303,6 @@ def init(parent=None): parent=parent) objreg.register('web-history', history) - used_backend = usertypes.arg2backend[objreg.get('args').backend] - if used_backend == usertypes.Backend.QtWebKit: + if objects.backend == usertypes.Backend.QtWebKit: from qutebrowser.browser.webkit import webkithistory webkithistory.init(history) diff --git a/qutebrowser/browser/inspector.py b/qutebrowser/browser/inspector.py index 00225d091..a18d1ecf2 100644 --- a/qutebrowser/browser/inspector.py +++ b/qutebrowser/browser/inspector.py @@ -24,9 +24,8 @@ import binascii from PyQt5.QtWidgets import QWidget -from qutebrowser.utils import log, objreg -from qutebrowser.misc import miscwidgets -from qutebrowser.config import config +from qutebrowser.utils import log, objreg, usertypes +from qutebrowser.misc import miscwidgets, objects def create(parent=None): @@ -37,7 +36,7 @@ def create(parent=None): """ # Importing modules here so we don't depend on QtWebEngine without the # argument and to avoid circular imports. - if objreg.get('args').backend == 'webengine': + if objects.backend == usertypes.Backend.QtWebEngine: from qutebrowser.browser.webengine import webengineinspector return webengineinspector.WebEngineInspector(parent) else: @@ -91,13 +90,6 @@ class AbstractWebInspector(QWidget): state_config['geometry']['inspector'] = geom super().closeEvent(e) - def _check_developer_extras(self): - """Check if developer-extras are enabled.""" - if not config.get('general', 'developer-extras'): - raise WebInspectorError( - "Please enable developer-extras before using the " - "webinspector!") - def inspect(self, page): """Inspect the given QWeb(Engine)Page.""" raise NotImplementedError diff --git a/qutebrowser/browser/mouse.py b/qutebrowser/browser/mouse.py index db5055d51..79b6816bb 100644 --- a/qutebrowser/browser/mouse.py +++ b/qutebrowser/browser/mouse.py @@ -64,8 +64,6 @@ class MouseEventFilter(QObject): """Handle mouse events on a tab. Attributes: - _widget_class: The class of the main widget getting the events. - All other events are ignored. _tab: The browsertab object this filter is installed on. _handlers: A dict of handler functions for the handled events. _ignore_wheel_event: Whether to ignore the next wheelEvent. @@ -73,9 +71,8 @@ class MouseEventFilter(QObject): done when the mouse is released. """ - def __init__(self, tab, *, widget_class, parent=None): + def __init__(self, tab, *, parent=None): super().__init__(parent) - self._widget_class = widget_class self._tab = tab self._handlers = { QEvent.MouseButtonPress: self._handle_mouse_press, @@ -96,7 +93,10 @@ class MouseEventFilter(QObject): return True self._ignore_wheel_event = True - self._tab.elements.find_at_pos(e.pos(), self._mousepress_insertmode_cb) + + if e.button() != Qt.NoButton: + self._tab.elements.find_at_pos(e.pos(), + self._mousepress_insertmode_cb) return False @@ -114,18 +114,26 @@ class MouseEventFilter(QObject): e: The QWheelEvent. """ if self._ignore_wheel_event: - # See https://github.com/The-Compiler/qutebrowser/issues/395 + # See https://github.com/qutebrowser/qutebrowser/issues/395 self._ignore_wheel_event = False return True if e.modifiers() & Qt.ControlModifier: divider = config.get('input', 'mouse-zoom-divider') + if divider == 0: + return False factor = self._tab.zoom.factor() + (e.angleDelta().y() / divider) if factor < 0: return False perc = int(100 * factor) - message.info("Zoom level: {}%".format(perc)) + message.info("Zoom level: {}%".format(perc), replace=True) self._tab.zoom.set_factor(factor) + elif e.modifiers() & Qt.ShiftModifier: + if e.angleDelta().y() > 0: + self._tab.scroller.left() + else: + self._tab.scroller.right() + return True return False @@ -201,9 +209,8 @@ class MouseEventFilter(QObject): evtype = event.type() if evtype not in self._handlers: return False - if not isinstance(obj, self._widget_class): - log.mouse.debug("Ignoring {} to {} which is not an instance of " - "{}".format(event.__class__.__name__, obj, - self._widget_class)) + if obj is not self._tab.event_target(): + log.mouse.debug("Ignoring {} to {}".format( + event.__class__.__name__, obj)) return False return self._handlers[evtype](event) diff --git a/qutebrowser/browser/navigate.py b/qutebrowser/browser/navigate.py index 395d4e166..aacec9d3c 100644 --- a/qutebrowser/browser/navigate.py +++ b/qutebrowser/browser/navigate.py @@ -70,11 +70,11 @@ def path_up(url, count): def _find_prevnext(prev, elems): """Find a prev/next element in the given list of elements.""" # First check for - rel_values = ('prev', 'previous') if prev else ('next') + rel_values = {'prev', 'previous'} if prev else {'next'} for e in elems: - if e.tag_name() != 'link' or 'rel' not in e: + if e.tag_name() not in ['link', 'a'] or 'rel' not in e: continue - if e['rel'] in rel_values: + if set(e['rel'].split(' ')) & rel_values: log.hints.debug("Found {!r} with rel={}".format(e, e['rel'])) return e diff --git a/qutebrowser/browser/network/__init__.py b/qutebrowser/browser/network/__init__.py new file mode 100644 index 000000000..c3d713ac2 --- /dev/null +++ b/qutebrowser/browser/network/__init__.py @@ -0,0 +1,3 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +"""Modules related to network operations.""" diff --git a/qutebrowser/browser/network/pac.py b/qutebrowser/browser/network/pac.py new file mode 100644 index 000000000..c19b09880 --- /dev/null +++ b/qutebrowser/browser/network/pac.py @@ -0,0 +1,315 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2016 Florian Bruhin (The Compiler) +# +# 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 . + +"""Evaluation of PAC scripts.""" + +import sys +import functools + +from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, QUrl +from PyQt5.QtNetwork import (QNetworkProxy, QNetworkRequest, QHostInfo, + QNetworkReply, QNetworkAccessManager, + QHostAddress) +from PyQt5.QtQml import QJSEngine, QJSValue + +from qutebrowser.utils import log, utils, qtutils + + +class ParseProxyError(Exception): + + """Error while parsing PAC result string.""" + + pass + + +class EvalProxyError(Exception): + + """Error while evaluating PAC script.""" + + pass + + +def _js_slot(*args): + """Wrap a methods as a JavaScript function. + + Register a PACContext method as a JavaScript function, and catch + exceptions returning them as JavaScript Error objects. + + Args: + args: Types of method arguments. + + Return: Wrapped method. + """ + def _decorator(method): + @functools.wraps(method) + def new_method(self, *args, **kwargs): + try: + return method(self, *args, **kwargs) + except: + e = str(sys.exc_info()[0]) + log.network.exception("PAC evaluation error") + # pylint: disable=protected-access + return self._error_con.callAsConstructor([e]) + # pylint: enable=protected-access + return pyqtSlot(*args, result=QJSValue)(new_method) + return _decorator + + +class _PACContext(QObject): + + """Implementation of PAC API functions that require native calls. + + See https://developer.mozilla.org/en-US/docs/Mozilla/Projects/Necko/Proxy_Auto-Configuration_(PAC)_file + """ + + JS_DEFINITIONS = """ + function dnsResolve(host) { + return PAC.dnsResolve(host); + } + + function myIpAddress() { + return PAC.myIpAddress(); + } + """ + + def __init__(self, engine): + """Create a new PAC API implementation instance. + + Args: + engine: QJSEngine which is used for running PAC. + """ + super().__init__(parent=engine) + self._engine = engine + self._error_con = engine.globalObject().property("Error") + + @_js_slot(str) + def dnsResolve(self, host): + """Resolve a DNS hostname. + + Resolves the given DNS hostname into an IP address, and returns it + in the dot-separated format as a string. + + Args: + host: hostname to resolve. + """ + ips = QHostInfo.fromName(host) + if ips.error() != QHostInfo.NoError or not ips.addresses(): + err_f = "Failed to resolve host during PAC evaluation: {}" + log.network.info(err_f.format(host)) + return QJSValue(QJSValue.NullValue) + else: + return ips.addresses()[0].toString() + + @_js_slot() + def myIpAddress(self): + """Get host IP address. + + Return the server IP address of the current machine, as a string in + the dot-separated integer format. + """ + return QHostAddress(QHostAddress.LocalHost).toString() + + +class PACResolver: + + """Evaluate PAC script files and resolve proxies.""" + + @staticmethod + def _parse_proxy_host(host_str): + host, _colon, port_str = host_str.partition(':') + try: + port = int(port_str) + except ValueError: + raise ParseProxyError("Invalid port number") + return (host, port) + + @staticmethod + def _parse_proxy_entry(proxy_str): + """Parse one proxy string entry, as described in PAC specification.""" + config = [c.strip() for c in proxy_str.split(' ') if c] + if not config: + raise ParseProxyError("Empty proxy entry") + elif config[0] == "DIRECT": + if len(config) != 1: + raise ParseProxyError("Invalid number of parameters for " + + "DIRECT") + return QNetworkProxy(QNetworkProxy.NoProxy) + elif config[0] == "PROXY": + if len(config) != 2: + raise ParseProxyError("Invalid number of parameters for PROXY") + host, port = PACResolver._parse_proxy_host(config[1]) + return QNetworkProxy(QNetworkProxy.HttpProxy, host, port) + elif config[0] == "SOCKS": + if len(config) != 2: + raise ParseProxyError("Invalid number of parameters for SOCKS") + host, port = PACResolver._parse_proxy_host(config[1]) + return QNetworkProxy(QNetworkProxy.Socks5Proxy, host, port) + else: + err = "Unknown proxy type: {}" + raise ParseProxyError(err.format(config[0])) + + @staticmethod + def _parse_proxy_string(proxy_str): + proxies = proxy_str.split(';') + return [PACResolver._parse_proxy_entry(x) for x in proxies] + + def _evaluate(self, js_code, js_file): + ret = self._engine.evaluate(js_code, js_file) + if ret.isError(): + err = "JavaScript error while evaluating PAC file: {}" + raise EvalProxyError(err.format(ret.toString())) + + def __init__(self, pac_str): + """Create a PAC resolver. + + Args: + pac_str: JavaScript code containing PAC resolver. + """ + self._engine = QJSEngine() + + self._ctx = _PACContext(self._engine) + self._engine.globalObject().setProperty( + "PAC", self._engine.newQObject(self._ctx)) + self._evaluate(_PACContext.JS_DEFINITIONS, "pac_js_definitions") + self._evaluate(utils.read_file("javascript/pac_utils.js"), "pac_utils") + proxy_config = self._engine.newObject() + proxy_config.setProperty("bindings", self._engine.newObject()) + self._engine.globalObject().setProperty("ProxyConfig", proxy_config) + + self._evaluate(pac_str, "pac") + global_js_object = self._engine.globalObject() + self._resolver = global_js_object.property("FindProxyForURL") + if not self._resolver.isCallable(): + err = "Cannot resolve FindProxyForURL function, got '{}' instead" + raise EvalProxyError(err.format(self._resolver.toString())) + + def resolve(self, query, from_file=False): + """Resolve a proxy via PAC. + + Args: + query: QNetworkProxyQuery. + from_file: Whether the proxy info is coming from a file. + + Return: + A list of QNetworkProxy objects in order of preference. + """ + if from_file: + string_flags = QUrl.PrettyDecoded + else: + string_flags = QUrl.RemoveUserInfo + if query.url().scheme() == 'https': + string_flags |= QUrl.RemovePath | QUrl.RemoveQuery + + result = self._resolver.call([query.url().toString(string_flags), + query.peerHostName()]) + result_str = result.toString() + if not result.isString(): + err = "Got strange value from FindProxyForURL: '{}'" + raise EvalProxyError(err.format(result_str)) + return self._parse_proxy_string(result_str) + + +class PACFetcher(QObject): + + """Asynchronous fetcher of PAC files.""" + + finished = pyqtSignal() + + def __init__(self, url, parent=None): + """Resolve a PAC proxy from URL. + + Args: + url: QUrl of a PAC proxy. + """ + super().__init__(parent) + + pac_prefix = "pac+" + + assert url.scheme().startswith(pac_prefix) + url.setScheme(url.scheme()[len(pac_prefix):]) + + self._pac_url = url + self._manager = QNetworkAccessManager() + self._manager.setProxy(QNetworkProxy(QNetworkProxy.NoProxy)) + self._reply = self._manager.get(QNetworkRequest(url)) + self._reply.finished.connect(self._finish) + self._pac = None + self._error_message = None + + @pyqtSlot() + def _finish(self): + if self._reply.error() != QNetworkReply.NoError: + error = "Can't fetch PAC file from URL, error code {}: {}" + self._error_message = error.format( + self._reply.error(), self._reply.errorString()) + log.network.error(self._error_message) + else: + try: + pacscript = bytes(self._reply.readAll()).decode("utf-8") + except UnicodeError as e: + error = "Invalid encoding of a PAC file: {}" + self._error_message = error.format(e) + log.network.exception(self._error_message) + try: + self._pac = PACResolver(pacscript) + log.network.debug("Successfully evaluated PAC file.") + except EvalProxyError as e: + error = "Error in PAC evaluation: {}" + self._error_message = error.format(e) + log.network.exception(self._error_message) + self._manager = None + self._reply = None + self.finished.emit() + + def _wait(self): + """Wait until a reply from the remote server is received.""" + if self._manager is not None: + loop = qtutils.EventLoop() + self.finished.connect(loop.quit) + loop.exec_() + + def fetch_error(self): + """Check if PAC script is successfully fetched. + + Return None iff PAC script is downloaded and evaluated successfully, + error string otherwise. + """ + self._wait() + return self._error_message + + def resolve(self, query): + """Resolve a query via PAC. + + Args: QNetworkProxyQuery. + + Return a list of QNetworkProxy objects in order of preference. + """ + self._wait() + from_file = self._pac_url.scheme() == 'file' + try: + return self._pac.resolve(query, from_file=from_file) + except (EvalProxyError, ParseProxyError) as e: + log.network.exception("Error in PAC resolution: {}.".format(e)) + # .invalid is guaranteed to be inaccessible in RFC 6761. + # Port 9 is for DISCARD protocol -- DISCARD servers act like + # /dev/null. + # Later NetworkManager.createRequest will detect this and display + # an error message. + error_host = "pac-resolve-error.qutebrowser.invalid" + return QNetworkProxy(QNetworkProxy.HttpProxy, error_host, 9) diff --git a/qutebrowser/browser/webkit/network/proxy.py b/qutebrowser/browser/network/proxy.py similarity index 75% rename from qutebrowser/browser/webkit/network/proxy.py rename to qutebrowser/browser/network/proxy.py index 2469bd0c7..719c33178 100644 --- a/qutebrowser/browser/webkit/network/proxy.py +++ b/qutebrowser/browser/network/proxy.py @@ -23,17 +23,33 @@ from PyQt5.QtNetwork import QNetworkProxy, QNetworkProxyFactory from qutebrowser.config import config, configtypes +from qutebrowser.utils import objreg +from qutebrowser.browser.network import pac def init(): """Set the application wide proxy factory.""" - QNetworkProxyFactory.setApplicationProxyFactory(ProxyFactory()) + proxy_factory = ProxyFactory() + objreg.register('proxy-factory', proxy_factory) + QNetworkProxyFactory.setApplicationProxyFactory(proxy_factory) class ProxyFactory(QNetworkProxyFactory): """Factory for proxies to be used by qutebrowser.""" + def get_error(self): + """Check if proxy can't be resolved. + + Return: + None if proxy is correct, otherwise an error message. + """ + proxy = config.get('network', 'proxy') + if isinstance(proxy, pac.PACFetcher): + return proxy.fetch_error() + else: + return None + def queryProxy(self, query): """Get the QNetworkProxies for a query. @@ -46,6 +62,8 @@ class ProxyFactory(QNetworkProxyFactory): proxy = config.get('network', 'proxy') if proxy is configtypes.SYSTEM_PROXY: proxies = QNetworkProxyFactory.systemProxyForQuery(query) + elif isinstance(proxy, pac.PACFetcher): + proxies = proxy.resolve(query) else: proxies = [proxy] for p in proxies: diff --git a/qutebrowser/browser/pdfjs.py b/qutebrowser/browser/pdfjs.py index 3bc2c2dc3..d11cf3098 100644 --- a/qutebrowser/browser/pdfjs.py +++ b/qutebrowser/browser/pdfjs.py @@ -100,6 +100,8 @@ SYSTEM_PDFJS_PATHS = [ # Debian pdf.js-common # Arch Linux pdfjs (AUR) '/usr/share/pdf.js/', + # Arch Linux pdf.js (AUR) + '/usr/share/javascript/pdf.js/', # Debian libjs-pdf '/usr/share/javascript/pdf/', # fallback diff --git a/qutebrowser/browser/qtnetworkdownloads.py b/qutebrowser/browser/qtnetworkdownloads.py index eeda5d9be..5ac689e29 100644 --- a/qutebrowser/browser/qtnetworkdownloads.py +++ b/qutebrowser/browser/qtnetworkdownloads.py @@ -27,7 +27,7 @@ import collections from PyQt5.QtCore import pyqtSlot, pyqtSignal, QTimer from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply -from qutebrowser.utils import message, usertypes, log, urlutils +from qutebrowser.utils import message, usertypes, log, urlutils, utils from qutebrowser.browser import downloads from qutebrowser.browser.webkit import http from qutebrowser.browser.webkit.network import networkmanager @@ -366,11 +366,12 @@ class DownloadManager(downloads.AbstractDownloadManager): win_id, None, self) @pyqtSlot('QUrl') - def get(self, url, **kwargs): + def get(self, url, *, user_agent=None, **kwargs): """Start a download with a link URL. Args: url: The URL to get, as QUrl + user_agent: The UA to set for the request, or None. **kwargs: passed to get_request(). Return: @@ -380,8 +381,32 @@ class DownloadManager(downloads.AbstractDownloadManager): urlutils.invalid_url_error(url, "start download") return req = QNetworkRequest(url) + if user_agent is not None: + req.setHeader(QNetworkRequest.UserAgentHeader, user_agent) return self.get_request(req, **kwargs) + def get_mhtml(self, tab, target): + """Download the given tab as mhtml to the given DownloadTarget.""" + assert tab.backend == usertypes.Backend.QtWebKit + from qutebrowser.browser.webkit import mhtml + + if target is not None: + mhtml.start_download_checked(target, tab=tab) + return + + suggested_fn = utils.sanitize_filename(tab.title() + ".mhtml") + + filename = downloads.immediate_download_path() + if filename is not None: + target = downloads.FileDownloadTarget(filename) + mhtml.start_download_checked(target, tab=tab) + else: + question = downloads.get_filename_question( + suggested_filename=suggested_fn, url=tab.url(), parent=tab) + question.answered.connect(functools.partial( + mhtml.start_download_checked, tab=tab)) + message.global_bridge.ask(question, blocking=False) + def get_request(self, request, *, target=None, **kwargs): """Start a download with a QNetworkRequest. diff --git a/qutebrowser/browser/qutescheme.py b/qutebrowser/browser/qutescheme.py index 3c426c232..018582abf 100644 --- a/qutebrowser/browser/qutescheme.py +++ b/qutebrowser/browser/qutescheme.py @@ -24,11 +24,17 @@ Module attributes: _HANDLERS: The handlers registered via decorators. """ +import sys +import time +import datetime import urllib.parse +from PyQt5.QtCore import QUrlQuery + import qutebrowser from qutebrowser.utils import (version, utils, jinja, log, message, docutils, - objreg, usertypes) + objreg) +from qutebrowser.misc import objects pyeval_output = ":pyeval was never called" @@ -89,8 +95,7 @@ class add_handler: # pylint: disable=invalid-name return function def wrapper(self, *args, **kwargs): - used_backend = usertypes.arg2backend[objreg.get('args').backend] - if self._backend is not None and used_backend != self._backend: + if self._backend is not None and objects.backend != self._backend: return self.wrong_backend_handler(*args, **kwargs) else: return self._function(*args, **kwargs) @@ -158,6 +163,87 @@ def qute_bookmarks(_url): return 'text/html', html +@add_handler('history') # noqa +def qute_history(url): + """Handler for qute:history. Display history.""" + # Get current date from query parameter, if not given choose today. + curr_date = datetime.date.today() + try: + query_date = QUrlQuery(url).queryItemValue("date") + if query_date: + curr_date = datetime.datetime.strptime(query_date, "%Y-%m-%d") + curr_date = curr_date.date() + except ValueError: + log.misc.debug("Invalid date passed to qute:history: " + query_date) + + one_day = datetime.timedelta(days=1) + next_date = curr_date + one_day + prev_date = curr_date - one_day + + def history_iter(reverse): + """Iterate through the history and get items we're interested in.""" + curr_timestamp = time.mktime(curr_date.timetuple()) + history = objreg.get('web-history').history_dict.values() + if reverse: + history = reversed(history) + + for item in history: + # If we can't apply the reverse performance trick below, + # at least continue as early as possible with old items. + # This gets us down from 550ms to 123ms with 500k old items on my + # machine. + if item.atime < curr_timestamp and not reverse: + continue + + # Convert timestamp + try: + item_atime = datetime.datetime.fromtimestamp(item.atime) + except (ValueError, OSError, OverflowError): + log.misc.debug("Invalid timestamp {}.".format(item.atime)) + continue + + if reverse and item_atime.date() < curr_date: + # If we could reverse the history in-place, and this entry is + # older than today, only older entries will follow, so we can + # abort here. + return + + # Skip items not on curr_date + # Skip redirects + # Skip qute:// links + is_internal = item.url.scheme() == 'qute' + is_not_today = item_atime.date() != curr_date + if item.redirect or is_internal or is_not_today: + continue + + # Use item's url as title if there's no title. + item_url = item.url.toDisplayString() + item_title = item.title if item.title else item_url + display_atime = item_atime.strftime("%X") + + yield (item_url, item_title, display_atime) + + if sys.hexversion >= 0x03050000: + # On Python >= 3.5 we can reverse the ordereddict in-place and thus + # apply an additional performance improvement in history_iter. + # On my machine, this gets us down from 550ms to 72us with 500k old + # items. + history = list(history_iter(reverse=True)) + else: + # On Python 3.4, we can't do that, so we'd need to copy the entire + # history to a list. There, filter first and then reverse it here. + history = reversed(list(history_iter(reverse=False))) + + html = jinja.render('history.html', + title='History', + history=history, + curr_date=curr_date, + next_date=next_date, + prev_date=prev_date, + today=datetime.date.today()) + return 'text/html', html + + @add_handler('pyeval') def qute_pyeval(_url): """Handler for qute:pyeval.""" diff --git a/qutebrowser/browser/shared.py b/qutebrowser/browser/shared.py index 8536ea837..885e2809d 100644 --- a/qutebrowser/browser/shared.py +++ b/qutebrowser/browser/shared.py @@ -66,6 +66,7 @@ def authentication_required(url, authenticator, abort_on): if answer is not None: authenticator.setUser(answer.user) authenticator.setPassword(answer.password) + return answer def javascript_confirm(url, js_msg, abort_on): @@ -157,7 +158,7 @@ def ignore_certificate_errors(url, errors, abort_on): log.webview.debug("ssl-strict is False, only warning about errors") for err in errors: # FIXME we might want to use warn here (non-fatal error) - # https://github.com/The-Compiler/qutebrowser/issues/114 + # https://github.com/qutebrowser/qutebrowser/issues/114 message.error('Certificate error: {}'.format(err)) return True elif ssl_strict is True: diff --git a/qutebrowser/browser/webelem.py b/qutebrowser/browser/webelem.py index 74304040c..5dd263da3 100644 --- a/qutebrowser/browser/webelem.py +++ b/qutebrowser/browser/webelem.py @@ -33,7 +33,8 @@ from PyQt5.QtCore import QUrl, Qt, QEvent, QTimer from PyQt5.QtGui import QMouseEvent from qutebrowser.config import config -from qutebrowser.utils import log, usertypes, utils, qtutils +from qutebrowser.keyinput import modeman +from qutebrowser.utils import log, usertypes, utils, qtutils, objreg Group = usertypes.enum('Group', ['all', 'links', 'images', 'url', 'prevnext', @@ -119,10 +120,6 @@ class AbstractWebElement(collections.abc.MutableMapping): """Get the geometry for this element.""" raise NotImplementedError - def style_property(self, name, *, strategy): - """Get the element style resolved with the given strategy.""" - raise NotImplementedError - def classes(self): """Get a list of classes assigned to this element.""" raise NotImplementedError @@ -139,7 +136,7 @@ class AbstractWebElement(collections.abc.MutableMapping): raise NotImplementedError def value(self): - """Get the value attribute for this element.""" + """Get the value attribute for this element, or None.""" raise NotImplementedError def set_value(self, value): @@ -160,7 +157,7 @@ class AbstractWebElement(collections.abc.MutableMapping): Skipping of small rectangles is due to elements containing other elements with "display:block" style, see - https://github.com/The-Compiler/qutebrowser/issues/1298 + https://github.com/qutebrowser/qutebrowser/issues/1298 Args: elem_geometry: The geometry of the element, or None. @@ -222,18 +219,22 @@ class AbstractWebElement(collections.abc.MutableMapping): else: return False - def _is_editable_div(self): - """Check if a div-element is editable. + def _is_editable_classes(self): + """Check if an element is editable based on its classes. Return: True if the element is editable, False otherwise. """ # Beginnings of div-classes which are actually some kind of editor. - div_classes = ('CodeMirror', # Javascript editor over a textarea - 'kix-', # Google Docs editor - 'ace_') # http://ace.c9.io/ + classes = { + 'div': ['CodeMirror', # Javascript editor over a textarea + 'kix-', # Google Docs editor + 'ace_'], # http://ace.c9.io/ + 'pre': ['CodeMirror'], + } + relevant_classes = classes[self.tag_name()] for klass in self.classes(): - if any([klass.startswith(e) for e in div_classes]): + if any([klass.strip().startswith(e) for e in relevant_classes]): return True return False @@ -264,10 +265,9 @@ class AbstractWebElement(collections.abc.MutableMapping): return config.get('input', 'insert-mode-on-plugins') and not strict elif tag == 'object': return self._is_editable_object() and not strict - elif tag == 'div': - return self._is_editable_div() and not strict - else: - return False + elif tag in ['div', 'pre']: + return self._is_editable_classes() and not strict + return False def is_text_input(self): """Check if this element is some kind of text box.""" @@ -311,7 +311,7 @@ class AbstractWebElement(collections.abc.MutableMapping): # Click the center of the largest square fitting into the top/left # corner of the rectangle, this will help if part of the element # is hidden behind other elements - # https://github.com/The-Compiler/qutebrowser/issues/1005 + # https://github.com/qutebrowser/qutebrowser/issues/1005 rect = self.rect_on_view() if rect.width() > rect.height(): rect.setWidth(rect.height()) @@ -322,14 +322,12 @@ class AbstractWebElement(collections.abc.MutableMapping): raise Error("Element position is out of view!") return pos - def click(self, click_target): - """Simulate a click on the element.""" - # FIXME:qtwebengine do we need this? - # self._widget.setFocus() - - # For QtWebKit - self._tab.data.override_target = click_target + def _move_text_cursor(self): + """Move cursor to end after clicking.""" + raise NotImplementedError + def _click_fake_event(self, click_target): + """Send a fake click event to the element.""" pos = self._mouse_pos() log.webelem.debug("Sending fake click to {!r} at position {} with " @@ -358,11 +356,74 @@ class AbstractWebElement(collections.abc.MutableMapping): for evt in events: self._tab.send_event(evt) - def after_click(): - """Move cursor to end after clicking.""" - if self.is_text_input() and self.is_editable(): - self._tab.caret.move_to_end_of_document() - QTimer.singleShot(0, after_click) + QTimer.singleShot(0, self._move_text_cursor) + + def _click_editable(self, click_target): + """Fake a click on an editable input field.""" + raise NotImplementedError + + def _click_js(self, click_target): + """Fake a click by using the JS .click() method.""" + raise NotImplementedError + + def _click_href(self, click_target): + """Fake a click on an element with a href by opening the link.""" + baseurl = self._tab.url() + url = self.resolve_url(baseurl) + if url is None: + self._click_fake_event(click_target) + return + + if click_target in [usertypes.ClickTarget.tab, + usertypes.ClickTarget.tab_bg]: + background = click_target == usertypes.ClickTarget.tab_bg + tabbed_browser = objreg.get('tabbed-browser', scope='window', + window=self._tab.win_id) + tabbed_browser.tabopen(url, background=background) + elif click_target == usertypes.ClickTarget.window: + from qutebrowser.mainwindow import mainwindow + window = mainwindow.MainWindow() + window.show() + window.tabbed_browser.tabopen(url) + else: + raise ValueError("Unknown ClickTarget {}".format(click_target)) + + def click(self, click_target, *, force_event=False): + """Simulate a click on the element. + + Args: + click_target: A usertypes.ClickTarget member, what kind of click + to simulate. + force_event: Force generating a fake mouse event. + """ + log.webelem.debug("Clicking {!r} with click_target {}, force_event {}" + .format(self, click_target, force_event)) + + if force_event: + self._click_fake_event(click_target) + return + + href_tags = ['a', 'area', 'link'] + if click_target == usertypes.ClickTarget.normal: + if self.tag_name() in href_tags: + log.webelem.debug("Clicking via JS click()") + self._click_js(click_target) + elif self.is_editable(strict=True): + log.webelem.debug("Clicking via JS focus()") + self._click_editable(click_target) + modeman.enter(self._tab.win_id, usertypes.KeyMode.insert, + 'clicking input') + else: + self._click_fake_event(click_target) + elif click_target in [usertypes.ClickTarget.tab, + usertypes.ClickTarget.tab_bg, + usertypes.ClickTarget.window]: + if self.tag_name() in href_tags: + self._click_href(click_target) + else: + self._click_fake_event(click_target) + else: + raise ValueError("Unknown ClickTarget {}".format(click_target)) def hover(self): """Simulate a mouse hover over the element.""" diff --git a/qutebrowser/browser/webengine/webenginedownloads.py b/qutebrowser/browser/webengine/webenginedownloads.py index 5286a6298..070bcf8c0 100644 --- a/qutebrowser/browser/webengine/webenginedownloads.py +++ b/qutebrowser/browser/webengine/webenginedownloads.py @@ -19,7 +19,9 @@ """QtWebEngine specific code for downloads.""" +import re import os.path +import urllib import functools from PyQt5.QtCore import pyqtSlot, Qt @@ -28,7 +30,7 @@ from PyQt5.QtWebEngineWidgets import QWebEngineDownloadItem # pylint: enable=no-name-in-module,import-error,useless-suppression from qutebrowser.browser import downloads -from qutebrowser.utils import debug, usertypes, message, log +from qutebrowser.utils import debug, usertypes, message, log, qtutils class DownloadItem(downloads.AbstractDownloadItem): @@ -45,6 +47,11 @@ class DownloadItem(downloads.AbstractDownloadItem): qt_item.downloadProgress.connect(self.stats.on_download_progress) qt_item.stateChanged.connect(self._on_state_changed) + def _is_page_download(self): + """Check if this item is a page (i.e. mhtml) download.""" + return (self._qt_item.savePageFormat() != + QWebEngineDownloadItem.UnknownSaveFormat) + @pyqtSlot(QWebEngineDownloadItem.DownloadState) def _on_state_changed(self, state): state_name = debug.qenum_key(QWebEngineDownloadItem, state) @@ -57,6 +64,9 @@ class DownloadItem(downloads.AbstractDownloadItem): pass elif state == QWebEngineDownloadItem.DownloadCompleted: log.downloads.debug("Download {} finished".format(self.basename)) + if self._is_page_download(): + # Same logging as QtWebKit mhtml downloads. + log.downloads.debug("File successfully written.") self.successful = True self.done = True self.finished.emit() @@ -94,7 +104,8 @@ class DownloadItem(downloads.AbstractDownloadItem): raise downloads.UnsupportedOperationError def _set_tempfile(self, fileobj): - self._set_filename(fileobj.name, force_overwrite=True) + self._set_filename(fileobj.name, force_overwrite=True, + remember_directory=False) def _ensure_can_set_filename(self, filename): state = self._qt_item.state() @@ -122,9 +133,38 @@ class DownloadItem(downloads.AbstractDownloadItem): self._qt_item.accept() +def _get_suggested_filename(path): + """Convert a path we got from chromium to a suggested filename. + + Chromium thinks we want to download stuff to ~/Download, so even if we + don't, we get downloads with a suffix like (1) for files existing there. + + We simply strip the suffix off via regex. + + See https://bugreports.qt.io/browse/QTBUG-56978 + """ + filename = os.path.basename(path) + filename = re.sub(r'\([0-9]+\)(?=\.|$)', '', filename) + if not qtutils.version_check('5.8.1'): + # https://bugreports.qt.io/browse/QTBUG-58155 + filename = urllib.parse.unquote(filename) + # Doing basename a *second* time because there could be a %2F in + # there... + filename = os.path.basename(filename) + return filename + + class DownloadManager(downloads.AbstractDownloadManager): - """Manager for currently running downloads.""" + """Manager for currently running downloads. + + Attributes: + _mhtml_target: DownloadTarget for the next MHTML download. + """ + + def __init__(self, parent=None): + super().__init__(parent) + self._mhtml_target = None def install(self, profile): """Set up the download manager on a QWebEngineProfile.""" @@ -134,12 +174,17 @@ class DownloadManager(downloads.AbstractDownloadManager): @pyqtSlot(QWebEngineDownloadItem) def handle_download(self, qt_item): """Start a download coming from a QWebEngineProfile.""" - suggested_filename = os.path.basename(qt_item.path()) + suggested_filename = _get_suggested_filename(qt_item.path()) download = DownloadItem(qt_item) self._init_item(download, auto_remove=False, suggested_filename=suggested_filename) + if self._mhtml_target is not None: + download.set_target(self._mhtml_target) + self._mhtml_target = None + return + filename = downloads.immediate_download_path() if filename is not None: # User doesn't want to be asked, so just use the download_dir @@ -156,3 +201,10 @@ class DownloadManager(downloads.AbstractDownloadManager): message.global_bridge.ask(question, blocking=True) # The filename is set via the question.answered signal, connected in # _init_filename_question. + + def get_mhtml(self, tab, target): + """Download the given tab as mhtml to the given target.""" + assert tab.backend == usertypes.Backend.QtWebEngine + assert self._mhtml_target is None, self._mhtml_target + self._mhtml_target = target + tab.action.save_page() diff --git a/qutebrowser/browser/webengine/webengineelem.py b/qutebrowser/browser/webengine/webengineelem.py index e045d22ed..3e145468b 100644 --- a/qutebrowser/browser/webengine/webengineelem.py +++ b/qutebrowser/browser/webengine/webengineelem.py @@ -22,7 +22,12 @@ """QtWebEngine specific part of the web element API.""" -from PyQt5.QtCore import QRect +from PyQt5.QtCore import QRect, Qt, QPoint, QEventLoop +from PyQt5.QtGui import QMouseEvent +from PyQt5.QtWidgets import QApplication +# pylint: disable=no-name-in-module,import-error,useless-suppression +from PyQt5.QtWebEngineWidgets import QWebEngineSettings +# pylint: enable=no-name-in-module,import-error,useless-suppression from qutebrowser.utils import log, javascript from qutebrowser.browser import webelem @@ -51,9 +56,7 @@ class WebEngineElement(webelem.AbstractWebElement): def __setitem__(self, key, val): self._js_dict['attributes'][key] = val - js_code = javascript.assemble('webelem', 'set_attribute', self._id, - key, val) - self._tab.run_js_async(js_code) + self._js_call('set_attribute', key, val) def __delitem__(self, key): log.stub() @@ -64,6 +67,11 @@ class WebEngineElement(webelem.AbstractWebElement): def __len__(self): return len(self._js_dict['attributes']) + def _js_call(self, name, *args, callback=None): + """Wrapper to run stuff from webelem.js.""" + js_code = javascript.assemble('webelem', name, self._id, *args) + self._tab.run_js_async(js_code, callback=callback) + def has_frame(self): return True @@ -71,10 +79,6 @@ class WebEngineElement(webelem.AbstractWebElement): log.stub() return QRect() - def style_property(self, name, *, strategy): - log.stub() - return '' - def classes(self): """Get a list of classes assigned to this element.""" return self._js_dict['class_name'].split() @@ -91,25 +95,23 @@ class WebEngineElement(webelem.AbstractWebElement): return self._js_dict['outer_xml'] def value(self): - return self._js_dict['value'] + return self._js_dict.get('value', None) def set_value(self, value): - js_code = javascript.assemble('webelem', 'set_value', self._id, value) - self._tab.run_js_async(js_code) + self._js_call('set_value', value) def insert_text(self, text): if not self.is_editable(strict=True): raise webelem.Error("Element is not editable!") log.webelem.debug("Inserting text into element {!r}".format(self)) - js_code = javascript.assemble('webelem', 'insert_text', self._id, text) - self._tab.run_js_async(js_code) + self._js_call('insert_text', text) def rect_on_view(self, *, elem_geometry=None, no_js=False): """Get the geometry of the element relative to the webview. Skipping of small rectangles is due to elements containing other elements with "display:block" style, see - https://github.com/The-Compiler/qutebrowser/issues/1298 + https://github.com/qutebrowser/qutebrowser/issues/1298 Args: elem_geometry: The geometry of the element, or None. @@ -146,6 +148,44 @@ class WebEngineElement(webelem.AbstractWebElement): return QRect() def remove_blank_target(self): - js_code = javascript.assemble('webelem', 'remove_blank_target', - self._id) - self._tab.run_js_async(js_code) + if self._js_dict['attributes'].get('target') == '_blank': + self._js_dict['attributes']['target'] = '_top' + self._js_call('remove_blank_target') + + def _move_text_cursor(self): + if self.is_text_input() and self.is_editable(): + self._js_call('move_cursor_to_end') + + def _click_editable(self, click_target): + # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-58515 + # pylint doesn't know about Qt.MouseEventSynthesizedBySystem + # because it was added in Qt 5.6, but we can be sure we use that with + # QtWebEngine. + # pylint: disable=no-member,useless-suppression + ev = QMouseEvent(QMouseEvent.MouseButtonPress, QPoint(0, 0), + QPoint(0, 0), QPoint(0, 0), Qt.NoButton, Qt.NoButton, + Qt.NoModifier, Qt.MouseEventSynthesizedBySystem) + # pylint: enable=no-member,useless-suppression + self._tab.send_event(ev) + # This actually "clicks" the element by calling focus() on it in JS. + self._js_call('focus') + self._move_text_cursor() + + def _click_js(self, _click_target): + settings = QWebEngineSettings.globalSettings() + attribute = QWebEngineSettings.JavascriptCanOpenWindows + could_open_windows = settings.testAttribute(attribute) + settings.setAttribute(attribute, True) + + # Get QtWebEngine do apply the settings + # (it does so with a 0ms QTimer...) + # This is also used in Qt's tests: + # https://github.com/qt/qtwebengine/commit/5e572e88efa7ba7c2b9138ec19e606d3e345ac90 + qapp = QApplication.instance() + qapp.processEvents(QEventLoop.ExcludeSocketNotifiers | + QEventLoop.ExcludeUserInputEvents) + + def reset_setting(_arg): + settings.setAttribute(attribute, could_open_windows) + + self._js_call('click', callback=reset_setting) diff --git a/qutebrowser/browser/webengine/webengineinspector.py b/qutebrowser/browser/webengine/webengineinspector.py index b0c0a25bf..b822bd253 100644 --- a/qutebrowser/browser/webengine/webengineinspector.py +++ b/qutebrowser/browser/webengine/webengineinspector.py @@ -41,13 +41,12 @@ class WebEngineInspector(inspector.AbstractWebInspector): def inspect(self, _page): """Set up the inspector.""" - self._check_developer_extras() try: port = int(os.environ['QTWEBENGINE_REMOTE_DEBUGGING']) except KeyError: raise inspector.WebInspectorError( - "Debugging is not set up correctly. Did you restart after " - "setting developer-extras?") + "Debugging is not enabled. See 'qutebrowser --help' for " + "details.") url = QUrl('http://localhost:{}/'.format(port)) self._widget.load(url) self.show() diff --git a/qutebrowser/browser/webengine/webenginesettings.py b/qutebrowser/browser/webengine/webenginesettings.py index 1568abad5..b029b4fd5 100644 --- a/qutebrowser/browser/webengine/webenginesettings.py +++ b/qutebrowser/browser/webengine/webenginesettings.py @@ -25,6 +25,7 @@ Module attributes: """ import os +import logging # pylint: disable=no-name-in-module,import-error,useless-suppression from PyQt5.QtWebEngineWidgets import (QWebEngineSettings, QWebEngineProfile, @@ -32,8 +33,8 @@ from PyQt5.QtWebEngineWidgets import (QWebEngineSettings, QWebEngineProfile, # pylint: enable=no-name-in-module,import-error,useless-suppression from qutebrowser.browser import shared -from qutebrowser.config import websettings, config -from qutebrowser.utils import objreg, utils, standarddir, javascript +from qutebrowser.config import config, websettings +from qutebrowser.utils import objreg, utils, standarddir, javascript, log class Attribute(websettings.Attribute): @@ -65,6 +66,47 @@ class StaticSetter(websettings.StaticSetter): GLOBAL_SETTINGS = QWebEngineSettings.globalSettings +class ProfileSetter(websettings.Base): + + """A setting set on the QWebEngineProfile.""" + + def __init__(self, getter, setter): + super().__init__() + self._getter = getter + self._setter = setter + + def get(self, settings=None): + utils.unused(settings) + getter = getattr(QWebEngineProfile.defaultProfile(), self._getter) + return getter() + + def _set(self, value, settings=None): + utils.unused(settings) + setter = getattr(QWebEngineProfile.defaultProfile(), self._setter) + setter(value) + + +class PersistentCookiePolicy(ProfileSetter): + + """The cookies -> store setting is different from other settings.""" + + def __init__(self): + super().__init__(getter='persistentCookiesPolicy', + setter='setPersistentCookiesPolicy') + + def get(self, settings=None): + utils.unused(settings) + return config.get('content', 'cookies-store') + + def _set(self, value, settings=None): + utils.unused(settings) + setter = getattr(QWebEngineProfile.defaultProfile(), self._setter) + setter( + QWebEngineProfile.AllowPersistentCookies if value else + QWebEngineProfile.NoPersistentCookies + ) + + def _init_stylesheet(profile): """Initialize custom stylesheets. @@ -98,6 +140,13 @@ def _init_stylesheet(profile): profile.scripts().insert(script) +def _init_profile(profile): + """Initialize settings set on the QWebEngineProfile.""" + profile.setCachePath(os.path.join(standarddir.cache(), 'webengine')) + profile.setPersistentStoragePath( + os.path.join(standarddir.data(), 'webengine')) + + def update_settings(section, option): """Update global settings when qwebsettings changed.""" websettings.update_mappings(MAPPINGS, section, option) @@ -106,17 +155,31 @@ def update_settings(section, option): _init_stylesheet(profile) -def init(): +def init(args): """Initialize the global QWebSettings.""" - if config.get('general', 'developer-extras'): - # FIXME:qtwebengine Make sure we call globalSettings *after* this... + if args.enable_webengine_inspector: os.environ['QTWEBENGINE_REMOTE_DEBUGGING'] = str(utils.random_port()) + # Workaround for a black screen with some setups + # https://github.com/spyder-ide/spyder/issues/3226 + if not os.environ.get('QUTE_NO_OPENGL_WORKAROUND'): + # Hide "No OpenGL_accelerate module loaded: ..." message + logging.getLogger('OpenGL.acceleratesupport').propagate = False + try: + from OpenGL import GL # pylint: disable=unused-variable + except ImportError: + pass + else: + log.misc.debug("Imported PyOpenGL as workaround") + profile = QWebEngineProfile.defaultProfile() - profile.setCachePath(os.path.join(standarddir.cache(), 'webengine')) - profile.setPersistentStoragePath( - os.path.join(standarddir.data(), 'webengine')) + _init_profile(profile) _init_stylesheet(profile) + # We need to do this here as a WORKAROUND for + # https://bugreports.qt.io/browse/QTBUG-58650 + PersistentCookiePolicy().set(config.get('content', 'cookies-store')) + + Attribute(QWebEngineSettings.FullScreenSupportEnabled).set(True) websettings.init_mappings(MAPPINGS) objreg.get('config').changed.connect(update_settings) @@ -128,24 +191,17 @@ def shutdown(): # Missing QtWebEngine attributes: -# - ErrorPageEnabled (should not be exposed, but set) -# - FullScreenSupportEnabled # - ScreenCaptureEnabled # - Accelerated2dCanvasEnabled # - AutoLoadIconsForPage # - TouchIconsEnabled +# - FocusOnNavigationEnabled (5.8) +# - AllowRunningInsecureContent (5.8) # # Missing QtWebEngine fonts: # - FantasyFont # - PictographFont -# -# TODO settings on profile: -# - httpCacheMaximumSize -# - persistentCookiesPolicy -# - offTheRecord -# -# TODO settings elsewhere: -# - proxy + MAPPINGS = { 'content': { @@ -165,6 +221,11 @@ MAPPINGS = { Attribute(QWebEngineSettings.LocalContentCanAccessRemoteUrls), 'local-content-can-access-file-urls': Attribute(QWebEngineSettings.LocalContentCanAccessFileUrls), + # https://bugreports.qt.io/browse/QTBUG-58650 + # 'cookies-store': + # PersistentCookiePolicy(), + 'webgl': + Attribute(QWebEngineSettings.WebGLEnabled), }, 'input': { 'spatial-navigation': @@ -221,6 +282,9 @@ MAPPINGS = { 'storage': { 'local-storage': Attribute(QWebEngineSettings.LocalStorageEnabled), + 'cache-size': + ProfileSetter(getter='httpCacheMaximumSize', + setter='setHttpCacheMaximumSize') }, 'general': { 'xss-auditing': @@ -232,7 +296,8 @@ MAPPINGS = { } try: - MAPPINGS['content']['webgl'] = Attribute(QWebEngineSettings.WebGLEnabled) + MAPPINGS['general']['print-element-backgrounds'] = Attribute( + QWebEngineSettings.PrintElementBackgrounds) except AttributeError: - # Added in Qt 5.7 + # Added in Qt 5.8 pass diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index ce84a10ba..88c693244 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -24,10 +24,12 @@ import functools +import sip from PyQt5.QtCore import pyqtSlot, Qt, QEvent, QPoint, QUrl, QTimer -from PyQt5.QtGui import QKeyEvent, QIcon +from PyQt5.QtGui import QKeyEvent +from PyQt5.QtNetwork import QAuthenticator # pylint: disable=no-name-in-module,import-error,useless-suppression -from PyQt5.QtWidgets import QOpenGLWidget, QApplication +from PyQt5.QtWidgets import QApplication from PyQt5.QtWebEngineWidgets import (QWebEnginePage, QWebEngineScript, QWebEngineProfile) # pylint: enable=no-name-in-module,import-error,useless-suppression @@ -36,8 +38,9 @@ from qutebrowser.browser import browsertab, mouse, shared from qutebrowser.browser.webengine import (webview, webengineelem, tabhistory, interceptor, webenginequtescheme, webenginedownloads) +from qutebrowser.misc import miscwidgets from qutebrowser.utils import (usertypes, qtutils, log, javascript, utils, - objreg) + objreg, jinja) _qute_scheme_handler = None @@ -77,25 +80,44 @@ _JS_WORLD_MAP = { } +class WebEngineAction(browsertab.AbstractAction): + + """QtWebKit implementations related to web actions.""" + + def _action(self, action): + self._widget.triggerPageAction(action) + + def exit_fullscreen(self): + self._action(QWebEnginePage.ExitFullScreen) + + def save_page(self): + """Save the current page.""" + self._action(QWebEnginePage.SavePage) + + class WebEnginePrinting(browsertab.AbstractPrinting): """QtWebEngine implementations related to printing.""" def check_pdf_support(self): - if not hasattr(self._widget.page(), 'printToPdf'): - raise browsertab.WebTabError( - "Printing to PDF is unsupported with QtWebEngine on Qt < 5.7") + return True def check_printer_support(self): + if not hasattr(self._widget.page(), 'print'): + raise browsertab.WebTabError( + "Printing is unsupported with QtWebEngine on Qt < 5.8") + + def check_preview_support(self): raise browsertab.WebTabError( - "Printing is unsupported with QtWebEngine") + "Print previews are unsupported with QtWebEngine") def to_pdf(self, filename): self._widget.page().printToPdf(filename) - def to_printer(self, printer): - # Should never be called - assert False + def to_printer(self, printer, callback=None): + if callback is None: + callback = lambda _ok: None + self._widget.page().print(printer, callback) class WebEngineSearch(browsertab.AbstractSearch): @@ -223,9 +245,6 @@ class WebEngineScroller(browsertab.AbstractScroller): """QtWebEngine implementations related to scrolling.""" - # FIXME:qtwebengine - # using stuff here with a big count/argument causes memory leaks and hangs - def __init__(self, tab, parent=None): super().__init__(tab, parent) self._pos_perc = (0, 0) @@ -235,15 +254,10 @@ class WebEngineScroller(browsertab.AbstractScroller): def _init_widget(self, widget): super()._init_widget(widget) page = widget.page() - try: - page.scrollPositionChanged.connect(self._update_pos) - except AttributeError: - log.stub('scrollPositionChanged, on Qt < 5.7') - self._pos_perc = (None, None) + page.scrollPositionChanged.connect(self._update_pos) def _key_press(self, key, count=1): - # FIXME:qtwebengine Abort scrolling if the minimum/maximum was reached. - for _ in range(count): + for _ in range(min(count, 5000)): press_evt = QKeyEvent(QEvent.KeyPress, key, Qt.NoModifier, 0, 0, 0) release_evt = QKeyEvent(QEvent.KeyRelease, key, Qt.NoModifier, 0, 0, 0) @@ -355,6 +369,11 @@ class WebEngineHistory(browsertab.AbstractHistory): return self._history.canGoForward() def serialize(self): + # WORKAROUND for https://github.com/qutebrowser/qutebrowser/issues/2289 + # FIXME:qtwebengine can we get rid of this with Qt 5.8.1? + scheme = self._history.currentItem().url().scheme() + if scheme in ['view-source', 'chrome']: + raise browsertab.WebTabError("Can't serialize special URL!") return qtutils.serialize(self._history) def deserialize(self, data): @@ -414,7 +433,7 @@ class WebEngineElements(browsertab.AbstractElements): js_elem: The element serialized from javascript. """ debug_str = ('None' if js_elem is None - else utils.elide(repr(js_elem), 100)) + else utils.elide(repr(js_elem), 1000)) log.webview.debug("Got element from JS: {}".format(debug_str)) if js_elem is None: @@ -442,6 +461,7 @@ class WebEngineElements(browsertab.AbstractElements): def find_at_pos(self, pos, callback): assert pos.x() >= 0 assert pos.y() >= 0 + pos /= self._tab.zoom.factor() js_code = javascript.assemble('webelem', 'element_at_pos', pos.x(), pos.y()) js_cb = functools.partial(self._js_cb_single, callback) @@ -452,8 +472,6 @@ class WebEngineTab(browsertab.AbstractTab): """A QtWebEngine tab in the browser.""" - WIDGET_CLASS = QOpenGLWidget - def __init__(self, win_id, mode_manager, parent=None): super().__init__(win_id=win_id, mode_manager=mode_manager, parent=parent) @@ -466,12 +484,13 @@ class WebEngineTab(browsertab.AbstractTab): self.search = WebEngineSearch(parent=self) self.printing = WebEnginePrinting() self.elements = WebEngineElements(self) + self.action = WebEngineAction() self._set_widget(widget) self._connect_signals() self.backend = usertypes.Backend.QtWebEngine self._init_js() self._child_event_filter = None - self.needs_qtbug54419_workaround = False + self._saved_zoom = None def _init_js(self): js_code = '\n'.join([ @@ -485,13 +504,7 @@ class WebEngineTab(browsertab.AbstractTab): script.setSourceCode(js_code) page = self._widget.page() - try: - page.runJavaScript("", QWebEngineScript.ApplicationWorld) - except TypeError: - # We're unable to pass a world to runJavaScript - script.setWorldId(QWebEngineScript.MainWorld) - else: - script.setWorldId(QWebEngineScript.ApplicationWorld) + script.setWorldId(QWebEngineScript.ApplicationWorld) # FIXME:qtwebengine what about runsOnSubFrames? page.scripts().insert(script) @@ -503,7 +516,15 @@ class WebEngineTab(browsertab.AbstractTab): parent=self) self._widget.installEventFilter(self._child_event_filter) + @pyqtSlot() + def _restore_zoom(self): + if self._saved_zoom is None: + return + self.zoom.set_factor(self._saved_zoom) + self._saved_zoom = None + def openurl(self, url): + self._saved_zoom = self.zoom.factor() self._openurl_prepare(url) self._widget.load(url) @@ -528,22 +549,16 @@ class WebEngineTab(browsertab.AbstractTab): else: world_id = _JS_WORLD_MAP[world] - try: - if callback is None: - self._widget.page().runJavaScript(code, world_id) - else: - self._widget.page().runJavaScript(code, world_id, callback) - except TypeError: - if world is not None and world != usertypes.JsWorld.jseval: - log.webview.warning("Ignoring world ID on Qt < 5.7") - # Qt < 5.7 - if callback is None: - self._widget.page().runJavaScript(code) - else: - self._widget.page().runJavaScript(code, callback) + if callback is None: + self._widget.page().runJavaScript(code, world_id) + else: + self._widget.page().runJavaScript(code, world_id, callback) def shutdown(self): self.shutting_down.emit() + # WORKAROUND for + # https://bugreports.qt.io/browse/QTBUG-58563 + self.search.clear() self._widget.shutdown() def reload(self, *, force=False): @@ -560,23 +575,24 @@ class WebEngineTab(browsertab.AbstractTab): return self._widget.title() def icon(self): - try: - return self._widget.icon() - except AttributeError: - log.stub('on Qt < 5.7') - return QIcon() + return self._widget.icon() - def set_html(self, html, base_url): + def set_html(self, html, base_url=None): # FIXME:qtwebengine # check this and raise an exception if too big: # Warning: The content will be percent encoded before being sent to the # renderer via IPC. This may increase its size. The maximum size of the # percent encoded content is 2 megabytes minus 30 bytes. + if base_url is None: + base_url = QUrl() self._widget.setHtml(html, base_url) def networkaccessmanager(self): return None + def user_agent(self): + return None + def clear_ssl_errors(self): raise browsertab.UnsupportedOperationError @@ -602,31 +618,76 @@ class WebEngineTab(browsertab.AbstractTab): @pyqtSlot(QUrl, 'QAuthenticator*') def _on_authentication_required(self, url, authenticator): # FIXME:qtwebengine support .netrc - shared.authentication_required(url, authenticator, - abort_on=[self.shutting_down, - self.load_started]) + answer = shared.authentication_required( + url, authenticator, abort_on=[self.shutting_down, + self.load_started]) + if answer is None: + try: + # pylint: disable=no-member, useless-suppression + sip.assign(authenticator, QAuthenticator()) + except AttributeError: + # WORKAROUND for + # https://www.riverbankcomputing.com/pipermail/pyqt/2016-December/038400.html + url_string = url.toDisplayString() + error_page = jinja.render( + 'error.html', + title="Error loading page: {}".format(url_string), + url=url_string, error="Authentication required", icon='') + self.set_html(error_page) + + @pyqtSlot('QWebEngineFullScreenRequest') + def _on_fullscreen_requested(self, request): + request.accept() + on = request.toggleOn() + self.fullscreen_requested.emit(on) + if on: + notification = miscwidgets.FullscreenNotification(self) + notification.show() + notification.set_timeout(3000) + + @pyqtSlot(QWebEnginePage.RenderProcessTerminationStatus, int) + def _on_render_process_terminated(self, status, exitcode): + """Show an error when the renderer process terminated.""" + if (status == QWebEnginePage.AbnormalTerminationStatus and + exitcode == 256): + # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-58697 + status = QWebEnginePage.CrashedTerminationStatus + + status_map = { + QWebEnginePage.NormalTerminationStatus: + browsertab.TerminationStatus.normal, + QWebEnginePage.AbnormalTerminationStatus: + browsertab.TerminationStatus.abnormal, + QWebEnginePage.CrashedTerminationStatus: + browsertab.TerminationStatus.crashed, + QWebEnginePage.KilledTerminationStatus: + browsertab.TerminationStatus.killed, + -1: + browsertab.TerminationStatus.unknown, + } + self.renderer_process_terminated.emit(status_map[status], exitcode) def _connect_signals(self): view = self._widget page = view.page() + page.windowCloseRequested.connect(self.window_close_requested) page.linkHovered.connect(self.link_hovered) page.loadProgress.connect(self._on_load_progress) page.loadStarted.connect(self._on_load_started) page.loadFinished.connect(self._on_history_trigger) - view.titleChanged.connect(self.title_changed) - view.urlChanged.connect(self._on_url_changed) + page.loadFinished.connect(self._restore_zoom) page.loadFinished.connect(self._on_load_finished) page.certificate_error.connect(self._on_ssl_errors) page.authenticationRequired.connect(self._on_authentication_required) - try: - view.iconChanged.connect(self.icon_changed) - except AttributeError: - log.stub('iconChanged, on Qt < 5.7') - try: - page.contentsSizeChanged.connect(self.contents_size_changed) - except AttributeError: - log.stub('contentsSizeChanged, on Qt < 5.7') + page.fullScreenRequested.connect(self._on_fullscreen_requested) + page.contentsSizeChanged.connect(self.contents_size_changed) - def _event_target(self): + view.titleChanged.connect(self.title_changed) + view.urlChanged.connect(self._on_url_changed) + view.renderProcessTerminated.connect( + self._on_render_process_terminated) + view.iconChanged.connect(self.icon_changed) + + def event_target(self): return self._widget.focusProxy() diff --git a/qutebrowser/browser/webengine/webview.py b/qutebrowser/browser/webengine/webview.py index f3e9d6a94..67e5fc259 100644 --- a/qutebrowser/browser/webengine/webview.py +++ b/qutebrowser/browser/webengine/webview.py @@ -19,10 +19,10 @@ """The main browser widget for QtWebEngine.""" -import os import functools from PyQt5.QtCore import pyqtSignal, pyqtSlot, QUrl, PYQT_VERSION +from PyQt5.QtGui import QPalette # pylint: disable=no-name-in-module,import-error,useless-suppression from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage # pylint: enable=no-name-in-module,import-error,useless-suppression @@ -30,8 +30,8 @@ from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage from qutebrowser.browser import shared from qutebrowser.browser.webengine import certificateerror from qutebrowser.config import config -from qutebrowser.utils import (log, debug, usertypes, qtutils, jinja, urlutils, - message) +from qutebrowser.utils import (log, debug, usertypes, jinja, urlutils, message, + objreg) class WebEngineView(QWebEngineView): @@ -42,7 +42,10 @@ class WebEngineView(QWebEngineView): super().__init__(parent) self._win_id = win_id self._tabdata = tabdata - self.setPage(WebEnginePage(parent=self)) + + theme_color = self.style().standardPalette().color(QPalette.Base) + page = WebEnginePage(theme_color=theme_color, parent=self) + self.setPage(page) def shutdown(self): self.page().shutdown() @@ -67,7 +70,7 @@ class WebEngineView(QWebEngineView): A window without decoration. QWebEnginePage::WebBrowserBackgroundTab: A web browser tab without hiding the current visible - WebEngineView. (Added in Qt 5.7) + WebEngineView. Return: The new QWebEngineView object. @@ -78,13 +81,6 @@ class WebEngineView(QWebEngineView): log.webview.debug("createWindow with type {}, background_tabs " "{}".format(debug_type, background_tabs)) - try: - background_tab_wintype = QWebEnginePage.WebBrowserBackgroundTab - except AttributeError: - # This is unavailable with an older PyQt, but we still might get - # this with a newer Qt... - background_tab_wintype = 0x0003 - if wintype == QWebEnginePage.WebBrowserWindow: # Shift-Alt-Click target = usertypes.ClickTarget.window @@ -99,7 +95,7 @@ class WebEngineView(QWebEngineView): target = usertypes.ClickTarget.tab else: target = usertypes.ClickTarget.tab_bg - elif wintype == background_tab_wintype: + elif wintype == QWebEnginePage.WebBrowserBackgroundTab: # Middle-click / Ctrl-Click if background_tabs: target = usertypes.ClickTarget.tab_bg @@ -109,15 +105,6 @@ class WebEngineView(QWebEngineView): raise ValueError("Invalid wintype {}".format(debug_type)) tab = shared.get_tab(self._win_id, target) - - # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-54419 - vercheck = qtutils.version_check - qtbug54419_fixed = ((vercheck('5.6.2') and not vercheck('5.7.0')) or - qtutils.version_check('5.7.1') or - os.environ.get('QUTE_QTBUG54419_PATCHED', '')) - if not qtbug54419_fixed: - tab.needs_qtbug54419_workaround = True - return tab._widget # pylint: disable=protected-access @@ -127,6 +114,7 @@ class WebEnginePage(QWebEnginePage): Attributes: _is_shutting_down: Whether the page is currently shutting down. + _theme_color: The theme background color. Signals: certificate_error: Emitted on certificate errors. @@ -136,11 +124,21 @@ class WebEnginePage(QWebEnginePage): certificate_error = pyqtSignal() shutting_down = pyqtSignal() - def __init__(self, parent=None): + def __init__(self, theme_color, parent=None): super().__init__(parent) self._is_shutting_down = False self.featurePermissionRequested.connect( self._on_feature_permission_requested) + self._theme_color = theme_color + self._set_bg_color() + objreg.get('config').changed.connect(self._set_bg_color) + + @config.change_filter('colors', 'webpage.bg') + def _set_bg_color(self): + col = config.get('colors', 'webpage.bg') + if col is None: + col = self._theme_color + self.setBackgroundColor(col) @pyqtSlot(QUrl, 'QWebEnginePage::Feature') def _on_feature_permission_requested(self, url, feature): diff --git a/qutebrowser/browser/webkit/cache.py b/qutebrowser/browser/webkit/cache.py index bc774c250..860a532b0 100644 --- a/qutebrowser/browser/webkit/cache.py +++ b/qutebrowser/browser/webkit/cache.py @@ -48,6 +48,13 @@ class DiskCache(QNetworkDiskCache): maxsize=self.maximumCacheSize(), path=self.cacheDirectory()) + def _set_cache_size(self): + """Set the cache size based on the config.""" + size = config.get('storage', 'cache-size') + if size is None: + size = 1024 * 1024 * 50 # default from QNetworkDiskCachePrivate + self.setMaximumCacheSize(size) + def _maybe_activate(self): """Activate/deactivate the cache based on the config.""" if config.get('general', 'private-browsing'): @@ -55,13 +62,13 @@ class DiskCache(QNetworkDiskCache): else: self._activated = True self.setCacheDirectory(os.path.join(self._cache_dir, 'http')) - self.setMaximumCacheSize(config.get('storage', 'cache-size')) + self._set_cache_size() @pyqtSlot(str, str) def on_config_changed(self, section, option): """Update cache size/activated if the config was changed.""" if (section, option) == ('storage', 'cache-size'): - self.setMaximumCacheSize(config.get('storage', 'cache-size')) + self._set_cache_size() elif (section, option) == ('general', # pragma: no branch 'private-browsing'): self._maybe_activate() diff --git a/qutebrowser/browser/webkit/mhtml.py b/qutebrowser/browser/webkit/mhtml.py index 237898809..bc9ea2695 100644 --- a/qutebrowser/browser/webkit/mhtml.py +++ b/qutebrowser/browser/webkit/mhtml.py @@ -32,6 +32,7 @@ import email.generator import email.encoders import email.mime.multipart import email.message +import quopri from PyQt5.QtCore import QUrl @@ -138,6 +139,22 @@ def _check_rel(element): return any(rel in rels for rel in must_have) +def _encode_quopri_mhtml(msg): + """Encode the message's payload in quoted-printable. + + Substitute for quopri's default 'encode_quopri' method, which needlessly + encodes all spaces and tabs, instead of only those at the end on the + line. + + Args: + msg: Email message to quote. + """ + orig = msg.get_payload(decode=True) + encdata = quopri.encodestring(orig, quotetabs=False) + msg.set_payload(encdata) + msg['Content-Transfer-Encoding'] = 'quoted-printable' + + MHTMLPolicy = email.policy.default.clone(linesep='\r\n', max_line_length=0) @@ -146,7 +163,7 @@ E_BASE64 = email.encoders.encode_base64 # Encode the file using MIME quoted-printable encoding. -E_QUOPRI = email.encoders.encode_quopri +E_QUOPRI = _encode_quopri_mhtml class MHTMLWriter: @@ -225,7 +242,7 @@ class _Downloader: Attributes: tab: The AbstractTab which contains the website that will be saved. - dest: Destination filename. + target: DownloadTarget where the file should be downloaded to. writer: The MHTMLWriter object which is used to save the page. loaded_urls: A set of QUrls of finished asset downloads. pending_downloads: A set of unfinished (url, DownloadItem) tuples. @@ -235,9 +252,9 @@ class _Downloader: _win_id: The window this downloader belongs to. """ - def __init__(self, tab, dest): + def __init__(self, tab, target): self.tab = tab - self.dest = dest + self.target = target self.writer = None self.loaded_urls = {tab.url()} self.pending_downloads = set() @@ -332,8 +349,8 @@ class _Downloader: # Using the download manager to download host-blocked urls might crash # qute, see the comments/discussion on - # https://github.com/The-Compiler/qutebrowser/pull/962#discussion_r40256987 - # and https://github.com/The-Compiler/qutebrowser/issues/1053 + # https://github.com/qutebrowser/qutebrowser/pull/962#discussion_r40256987 + # and https://github.com/qutebrowser/qutebrowser/issues/1053 host_blocker = objreg.get('host-blocker') if host_blocker.is_blocked(url): log.downloads.debug("Skipping {}, host-blocked".format(url)) @@ -445,14 +462,34 @@ class _Downloader: return self._finished_file = True log.downloads.debug("All assets downloaded, ready to finish off!") + + if isinstance(self.target, downloads.FileDownloadTarget): + fobj = open(self.target.filename, 'wb') + elif isinstance(self.target, downloads.FileObjDownloadTarget): + fobj = self.target.fileobj + elif isinstance(self.target, downloads.OpenFileDownloadTarget): + try: + fobj = downloads.temp_download_manager.get_tmpfile( + self.tab.title() + '.mhtml') + except OSError as exc: + msg = "Download error: {}".format(exc) + message.error(msg) + return + else: + raise ValueError("Invalid DownloadTarget given: {!r}" + .format(self.target)) + try: - with open(self.dest, 'wb') as file_output: - self.writer.write_to(file_output) + with fobj: + self.writer.write_to(fobj) except OSError as error: message.error("Could not save file: {}".format(error)) return log.downloads.debug("File successfully written.") - message.info("Page saved as {}".format(self.dest)) + message.info("Page saved as {}".format(self.target)) + + if isinstance(self.target, downloads.OpenFileDownloadTarget): + utils.open_file(fobj.name, self.target.cmdline) def _collect_zombies(self): """Collect done downloads and add their data to the MHTML file. @@ -484,34 +521,37 @@ class _NoCloseBytesIO(io.BytesIO): super().close() -def _start_download(dest, tab): +def _start_download(target, tab): """Start downloading the current page and all assets to an MHTML file. This will overwrite dest if it already exists. Args: - dest: The filename where the resulting file should be saved. + target: The DownloadTarget where the resulting file should be saved. tab: Specify the tab whose page should be loaded. """ - loader = _Downloader(tab, dest) + loader = _Downloader(tab, target) loader.run() -def start_download_checked(dest, tab): +def start_download_checked(target, tab): """First check if dest is already a file, then start the download. Args: - dest: The filename where the resulting file should be saved. + target: The DownloadTarget where the resulting file should be saved. tab: Specify the tab whose page should be loaded. """ - # The default name is 'page title.mht' + if not isinstance(target, downloads.FileDownloadTarget): + _start_download(target, tab) + return + # The default name is 'page title.mhtml' title = tab.title() - default_name = utils.sanitize_filename(title + '.mht') + default_name = utils.sanitize_filename(title + '.mhtml') # Remove characters which cannot be expressed in the file system encoding encoding = sys.getfilesystemencoding() default_name = utils.force_encoding(default_name, encoding) - dest = utils.force_encoding(dest, encoding) + dest = utils.force_encoding(target.filename, encoding) dest = os.path.expanduser(dest) @@ -532,8 +572,9 @@ def start_download_checked(dest, tab): message.error("Directory {} does not exist.".format(folder)) return + target = downloads.FileDownloadTarget(path) if not os.path.isfile(path): - _start_download(path, tab=tab) + _start_download(target, tab=tab) return q = usertypes.Question() @@ -543,5 +584,5 @@ def start_download_checked(dest, tab): html.escape(path)) q.completed.connect(q.deleteLater) q.answered_yes.connect(functools.partial( - _start_download, path, tab=tab)) + _start_download, target, tab=tab)) message.global_bridge.ask(q, blocking=False) diff --git a/qutebrowser/browser/webkit/network/networkmanager.py b/qutebrowser/browser/webkit/network/networkmanager.py index 477a9958a..779d778bc 100644 --- a/qutebrowser/browser/webkit/network/networkmanager.py +++ b/qutebrowser/browser/webkit/network/networkmanager.py @@ -211,7 +211,8 @@ class NetworkManager(QNetworkAccessManager): request.deleteLater() self.shutting_down.emit() - @pyqtSlot('QNetworkReply*', 'QList') + # No @pyqtSlot here, see + # https://github.com/qutebrowser/qutebrowser/issues/2213 def on_ssl_errors(self, reply, errors): # pragma: no mccabe """Decide if SSL errors should be ignored or not. @@ -396,6 +397,14 @@ class NetworkManager(QNetworkAccessManager): Return: A QNetworkReply. """ + proxy_factory = objreg.get('proxy-factory', None) + if proxy_factory is not None: + proxy_error = proxy_factory.get_error() + if proxy_error is not None: + return networkreply.ErrorNetworkReply( + req, proxy_error, QNetworkReply.UnknownProxyError, + self) + scheme = req.url().scheme() if scheme in self._scheme_handlers: result = self._scheme_handlers[scheme].createRequest( @@ -426,7 +435,7 @@ class NetworkManager(QNetworkAccessManager): tab=self._tab_id) current_url = tab.url() except (KeyError, RuntimeError, TypeError): - # https://github.com/The-Compiler/qutebrowser/issues/889 + # https://github.com/qutebrowser/qutebrowser/issues/889 # Catching RuntimeError and TypeError because we could be in # the middle of the webpage shutdown here. current_url = QUrl() diff --git a/qutebrowser/browser/webkit/network/webkitqutescheme.py b/qutebrowser/browser/webkit/network/webkitqutescheme.py index cb4c68d97..61ef760bc 100644 --- a/qutebrowser/browser/webkit/network/webkitqutescheme.py +++ b/qutebrowser/browser/webkit/network/webkitqutescheme.py @@ -74,7 +74,7 @@ class JSBridge(QObject): @pyqtSlot(str, str, str) def set(self, sectname, optname, value): """Slot to set a setting from qute:settings.""" - # https://github.com/The-Compiler/qutebrowser/issues/727 + # https://github.com/qutebrowser/qutebrowser/issues/727 if ((sectname, optname) == ('content', 'allow-javascript') and value == 'false'): message.error("Refusing to disable javascript via qute:settings " diff --git a/qutebrowser/browser/webkit/rfc6266.py b/qutebrowser/browser/webkit/rfc6266.py index cc7f50f1e..89f4c78c6 100644 --- a/qutebrowser/browser/webkit/rfc6266.py +++ b/qutebrowser/browser/webkit/rfc6266.py @@ -117,7 +117,7 @@ class Language(str): """A language-tag (RFC 5646, Section 2.1). FIXME: This grammar is not 100% correct yet. - https://github.com/The-Compiler/qutebrowser/issues/105 + https://github.com/qutebrowser/qutebrowser/issues/105 """ grammar = re.compile('[A-Za-z0-9-]+') @@ -132,7 +132,7 @@ class ValueChars(str): """A value of an attribute. FIXME: Can we merge this with Value? - https://github.com/The-Compiler/qutebrowser/issues/105 + https://github.com/qutebrowser/qutebrowser/issues/105 """ grammar = re.compile('({}|{})*'.format(attr_char_re, hex_digit_re)) diff --git a/qutebrowser/browser/webkit/tabhistory.py b/qutebrowser/browser/webkit/tabhistory.py index 63a973685..d2efca257 100644 --- a/qutebrowser/browser/webkit/tabhistory.py +++ b/qutebrowser/browser/webkit/tabhistory.py @@ -21,21 +21,65 @@ from PyQt5.QtCore import QByteArray, QDataStream, QIODevice, QUrl +from PyQt5.QtWebKit import qWebKitVersion from qutebrowser.utils import qtutils -HISTORY_STREAM_VERSION = 2 -BACK_FORWARD_TREE_VERSION = 2 - - def _encode_url(url): """Encode a QUrl suitable to pass to QWebHistory.""" data = bytes(QUrl.toPercentEncoding(url.toString(), b':/#?&+=@%*')) return data.decode('ascii') -def _serialize_item(i, item, stream): +def _serialize_ng(items, current_idx, stream): + # {'currentItemIndex': 0, + # 'history': [{'children': [], + # 'documentSequenceNumber': 1485030525573123, + # 'documentState': [], + # 'formContentType': '', + # 'itemSequenceNumber': 1485030525573122, + # 'originalURLString': 'about:blank', + # 'pageScaleFactor': 0.0, + # 'referrer': '', + # 'scrollPosition': {'x': 0, 'y': 0}, + # 'target': '', + # 'title': '', + # 'urlString': 'about:blank'}]} + data = {'currentItemIndex': current_idx, 'history': []} + for item in items: + data['history'].append(_serialize_item_ng(item)) + + stream.writeInt(3) # history stream version + stream.writeQVariantMap(data) + + +def _serialize_item_ng(item): + data = { + 'originalURLString': item.original_url.toString(QUrl.FullyEncoded), + 'scrollPosition': {'x': 0, 'y': 0}, + 'title': item.title, + 'urlString': item.url.toString(QUrl.FullyEncoded), + } + try: + data['scrollPosition']['x'] = item.user_data['scroll-pos'].x() + data['scrollPosition']['y'] = item.user_data['scroll-pos'].y() + except (KeyError, TypeError): + pass + return data + + +def _serialize_old(items, current_idx, stream): + ### Source/WebKit/qt/Api/qwebhistory.cpp operator<< + stream.writeInt(2) # history stream version + stream.writeInt(len(items)) + stream.writeInt(current_idx) + + for i, item in enumerate(items): + _serialize_item_old(i, item, stream) + + +def _serialize_item_old(i, item, stream): """Serialize a single WebHistoryItem into a QDataStream. Args: @@ -53,7 +97,7 @@ def _serialize_item(i, item, stream): ### Source/WebCore/history/HistoryItem.cpp decodeBackForwardTree ## backForwardTreeEncodingVersion - stream.writeUInt32(BACK_FORWARD_TREE_VERSION) + stream.writeUInt32(2) ## size (recursion stack) stream.writeUInt64(0) ## node->m_documentSequenceNumber @@ -137,14 +181,12 @@ def serialize(items): else: current_idx = 0 - ### Source/WebKit/qt/Api/qwebhistory.cpp operator<< - stream.writeInt(HISTORY_STREAM_VERSION) - stream.writeInt(len(items)) - stream.writeInt(current_idx) + if qtutils.is_qtwebkit_ng(qWebKitVersion()): + _serialize_ng(items, current_idx, stream) + else: + _serialize_old(items, current_idx, stream) - for i, item in enumerate(items): - _serialize_item(i, item, stream) - user_data.append(item.user_data) + user_data += [item.user_data for item in items] stream.device().reset() qtutils.check_qdatastream(stream) diff --git a/qutebrowser/browser/webkit/webkitelem.py b/qutebrowser/browser/webkit/webkitelem.py index 6de1842bc..34209c290 100644 --- a/qutebrowser/browser/webkit/webkitelem.py +++ b/qutebrowser/browser/webkit/webkitelem.py @@ -20,7 +20,7 @@ """QtWebKit specific part of the web element API.""" from PyQt5.QtCore import QRect -from PyQt5.QtWebKit import QWebElement +from PyQt5.QtWebKit import QWebElement, QWebSettings from qutebrowser.config import config from qutebrowser.utils import log, utils, javascript @@ -96,16 +96,6 @@ class WebKitElement(webelem.AbstractWebElement): self._check_vanished() return self._elem.geometry() - def style_property(self, name, *, strategy): - self._check_vanished() - strategies = { - # FIXME:qtwebengine which ones do we actually need? - 'inline': QWebElement.InlineStyle, - 'computed': QWebElement.ComputedStyle, - } - qt_strategy = strategies[strategy] - return self._elem.styleProperty(name, qt_strategy) - def classes(self): self._check_vanished() return self._elem.classes() @@ -162,7 +152,7 @@ class WebKitElement(webelem.AbstractWebElement): # On e.g. Void Linux with musl libc, the stack size is too small # for jsc, and running JS will fail. If that happens, fall back to # the Python implementation. - # https://github.com/The-Compiler/qutebrowser/issues/1641 + # https://github.com/qutebrowser/qutebrowser/issues/1641 return None text = utils.compact_text(self._elem.toOuterXml(), 500) @@ -216,7 +206,7 @@ class WebKitElement(webelem.AbstractWebElement): Skipping of small rectangles is due to elements containing other elements with "display:block" style, see - https://github.com/The-Compiler/qutebrowser/issues/1298 + https://github.com/qutebrowser/qutebrowser/issues/1298 Args: elem_geometry: The geometry of the element, or None. @@ -248,10 +238,13 @@ class WebKitElement(webelem.AbstractWebElement): hidden_attributes = { 'visibility': 'hidden', 'display': 'none', + 'opacity': '0', } for k, v in hidden_attributes.items(): - if self._elem.styleProperty(k, QWebElement.ComputedStyle) == v: + if (self._elem.styleProperty(k, QWebElement.ComputedStyle) == v and + 'ace_text-input' not in self.classes()): return False + elem_geometry = self._elem.geometry() if not elem_geometry.isValid() and elem_geometry.x() == 0: # Most likely an invisible link @@ -297,6 +290,36 @@ class WebKitElement(webelem.AbstractWebElement): break elem = elem._parent() # pylint: disable=protected-access + def _move_text_cursor(self): + if self is None: + # old PyQt versions call the slot after the element is deleted. + return + if self.is_text_input() and self.is_editable(): + self._tab.caret.move_to_end_of_document() + + def _click_editable(self, click_target): + ok = self._elem.evaluateJavaScript('this.focus(); true;') + if ok: + self._move_text_cursor() + else: + log.webelem.debug("Failed to focus via JS, falling back to event") + self._click_fake_event(click_target) + + def _click_js(self, click_target): + settings = QWebSettings.globalSettings() + attribute = QWebSettings.JavascriptCanOpenWindows + could_open_windows = settings.testAttribute(attribute) + settings.setAttribute(attribute, True) + ok = self._elem.evaluateJavaScript('this.click(); true;') + settings.setAttribute(attribute, could_open_windows) + if not ok: + log.webelem.debug("Failed to click via JS, falling back to event") + self._click_fake_event(click_target) + + def _click_fake_event(self, click_target): + self._tab.data.override_target = click_target + super()._click_fake_event(click_target) + def get_child_frames(startframe): """Get all children recursively of a given QWebFrame. diff --git a/qutebrowser/browser/webkit/webkitinspector.py b/qutebrowser/browser/webkit/webkitinspector.py index 1a54ec784..9e612b28e 100644 --- a/qutebrowser/browser/webkit/webkitinspector.py +++ b/qutebrowser/browser/webkit/webkitinspector.py @@ -23,6 +23,7 @@ from PyQt5.QtWebKitWidgets import QWebInspector from qutebrowser.browser import inspector +from qutebrowser.config import config class WebKitInspector(inspector.AbstractWebInspector): @@ -35,6 +36,9 @@ class WebKitInspector(inspector.AbstractWebInspector): self._set_widget(qwebinspector) def inspect(self, page): - self._check_developer_extras() + if not config.get('general', 'developer-extras'): + raise inspector.WebInspectorError( + "Please enable developer-extras before using the " + "webinspector!") self._widget.setPage(page) self.show() diff --git a/qutebrowser/browser/webkit/webkitsettings.py b/qutebrowser/browser/webkit/webkitsettings.py index 5fe27c48b..caeef296f 100644 --- a/qutebrowser/browser/webkit/webkitsettings.py +++ b/qutebrowser/browser/webkit/webkitsettings.py @@ -26,10 +26,10 @@ Module attributes: import os.path -from PyQt5.QtWebKit import QWebSettings +from PyQt5.QtWebKit import QWebSettings, qWebKitVersion from qutebrowser.config import config, websettings -from qutebrowser.utils import standarddir, objreg, urlutils +from qutebrowser.utils import standarddir, objreg, urlutils, qtutils, message from qutebrowser.browser import shared @@ -88,28 +88,32 @@ def _set_user_stylesheet(): QWebSettings.globalSettings().setUserStyleSheetUrl(url) +def _init_private_browsing(): + if config.get('general', 'private-browsing'): + if qtutils.is_qtwebkit_ng(qWebKitVersion()): + message.warning("Private browsing is not fully implemented by " + "QtWebKit-NG!") + QWebSettings.setIconDatabasePath('') + else: + QWebSettings.setIconDatabasePath(standarddir.cache()) + + def update_settings(section, option): """Update global settings when qwebsettings changed.""" if (section, option) == ('general', 'private-browsing'): - cache_path = standarddir.cache() - if config.get('general', 'private-browsing') or cache_path is None: - QWebSettings.setIconDatabasePath('') - else: - QWebSettings.setIconDatabasePath(cache_path) + _init_private_browsing() elif section == 'ui' and option in ['hide-scrollbar', 'user-stylesheet']: _set_user_stylesheet() websettings.update_mappings(MAPPINGS, section, option) -def init(): +def init(_args): """Initialize the global QWebSettings.""" cache_path = standarddir.cache() data_path = standarddir.data() - if config.get('general', 'private-browsing'): - QWebSettings.setIconDatabasePath('') - else: - QWebSettings.setIconDatabasePath(cache_path) + + _init_private_browsing() QWebSettings.setOfflineWebApplicationCachePath( os.path.join(cache_path, 'application-cache')) diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py index 407ff2591..ac9eea466 100644 --- a/qutebrowser/browser/webkit/webkittab.py +++ b/qutebrowser/browser/webkit/webkittab.py @@ -32,8 +32,9 @@ from PyQt5.QtWebKit import QWebSettings from PyQt5.QtPrintSupport import QPrinter from qutebrowser.browser import browsertab +from qutebrowser.browser.network import proxy from qutebrowser.browser.webkit import webview, tabhistory, webkitelem -from qutebrowser.browser.webkit.network import proxy, webkitqutescheme +from qutebrowser.browser.webkit.network import webkitqutescheme from qutebrowser.utils import qtutils, objreg, usertypes, utils, log @@ -41,14 +42,28 @@ def init(): """Initialize QtWebKit-specific modules.""" qapp = QApplication.instance() - log.init.debug("Initializing proxy...") - proxy.init() + if not qtutils.version_check('5.8'): + # Otherwise we initialize it globally in app.py + log.init.debug("Initializing proxy...") + proxy.init() log.init.debug("Initializing js-bridge...") js_bridge = webkitqutescheme.JSBridge(qapp) objreg.register('js-bridge', js_bridge) +class WebKitAction(browsertab.AbstractAction): + + """QtWebKit implementations related to web actions.""" + + def exit_fullscreen(self): + raise browsertab.UnsupportedOperationError + + def save_page(self): + """Save the current page.""" + raise browsertab.UnsupportedOperationError + + class WebKitPrinting(browsertab.AbstractPrinting): """QtWebKit implementations related to printing.""" @@ -65,13 +80,19 @@ class WebKitPrinting(browsertab.AbstractPrinting): def check_printer_support(self): self._do_check() + def check_preview_support(self): + self._do_check() + def to_pdf(self, filename): printer = QPrinter() printer.setOutputFileName(filename) self.to_printer(printer) - def to_printer(self, printer): + def to_printer(self, printer, callback=None): self._widget.print(printer) + # Can't find out whether there was an error... + if callback is not None: + callback(True) class WebKitSearch(browsertab.AbstractSearch): @@ -422,7 +443,7 @@ class WebKitScroller(browsertab.AbstractScroller): # FIXME:qtwebengine needed? # self._widget.setFocus() - for _ in range(count): + for _ in range(min(count, 5000)): press_evt = QKeyEvent(QEvent.KeyPress, key, Qt.NoModifier, 0, 0, 0) release_evt = QKeyEvent(QEvent.KeyRelease, key, Qt.NoModifier, 0, 0, 0) @@ -589,8 +610,6 @@ class WebKitTab(browsertab.AbstractTab): """A QtWebKit tab in the browser.""" - WIDGET_CLASS = webview.WebView - def __init__(self, win_id, mode_manager, parent=None): super().__init__(win_id=win_id, mode_manager=mode_manager, parent=parent) @@ -603,9 +622,9 @@ class WebKitTab(browsertab.AbstractTab): self.search = WebKitSearch(parent=self) self.printing = WebKitPrinting() self.elements = WebKitElements(self) + self.action = WebKitAction() self._set_widget(widget) self._connect_signals() - self.zoom.set_default() self.backend = usertypes.Backend.QtWebKit def _install_event_filter(self): @@ -671,13 +690,17 @@ class WebKitTab(browsertab.AbstractTab): def networkaccessmanager(self): return self._widget.page().networkAccessManager() + def user_agent(self): + page = self._widget.page() + return page.userAgentForUrl(self.url()) + @pyqtSlot() def _on_frame_load_finished(self): """Make sure we emit an appropriate status when loading finished. While Qt has a bool "ok" attribute for loadFinished, it always is True when using error pages... See - https://github.com/The-Compiler/qutebrowser/issues/84 + https://github.com/qutebrowser/qutebrowser/issues/84 """ self._on_load_finished(not self._widget.page().error_occurred) @@ -690,8 +713,8 @@ class WebKitTab(browsertab.AbstractTab): def _on_frame_created(self, frame): """Connect the contentsSizeChanged signal of each frame.""" # FIXME:qtwebengine those could theoretically regress: - # https://github.com/The-Compiler/qutebrowser/issues/152 - # https://github.com/The-Compiler/qutebrowser/issues/263 + # https://github.com/qutebrowser/qutebrowser/issues/152 + # https://github.com/qutebrowser/qutebrowser/issues/263 frame.contentsSizeChanged.connect(self._on_contents_size_changed) @pyqtSlot(QSize) @@ -717,5 +740,5 @@ class WebKitTab(browsertab.AbstractTab): frame.contentsSizeChanged.connect(self._on_contents_size_changed) frame.initialLayoutCompleted.connect(self._on_history_trigger) - def _event_target(self): + def event_target(self): return self._widget diff --git a/qutebrowser/browser/webkit/webview.py b/qutebrowser/browser/webkit/webview.py index 7795f3962..12670be4f 100644 --- a/qutebrowser/browser/webkit/webview.py +++ b/qutebrowser/browser/webkit/webview.py @@ -59,7 +59,7 @@ class WebView(QWebView): super().__init__(parent) if sys.platform == 'darwin' and qtutils.version_check('5.4'): # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-42948 - # See https://github.com/The-Compiler/qutebrowser/issues/462 + # See https://github.com/qutebrowser/qutebrowser/issues/462 self.setStyle(QStyleFactory.create('Fusion')) # FIXME:qtwebengine this is only used to set the zoom factor from # the QWebPage - we should get rid of it somehow (signals?) @@ -108,11 +108,7 @@ class WebView(QWebView): @config.change_filter('colors', 'webpage.bg') def _set_bg_color(self): - """Set the webpage background color as configured. - - FIXME:qtwebengine - For QtWebEngine, doing the same has no effect, so we do it in here. - """ + """Set the webpage background color as configured.""" col = config.get('colors', 'webpage.bg') palette = self.palette() if col is None: diff --git a/qutebrowser/commands/command.py b/qutebrowser/commands/command.py index a2bc3b50d..d46cc5c77 100644 --- a/qutebrowser/commands/command.py +++ b/qutebrowser/commands/command.py @@ -27,6 +27,7 @@ from qutebrowser.commands import cmdexc, argparser from qutebrowser.utils import (log, utils, message, docutils, objreg, usertypes, typing) from qutebrowser.utils import debug as debug_utils +from qutebrowser.misc import objects class ArgInfo: @@ -34,14 +35,11 @@ class ArgInfo: """Information about an argument.""" def __init__(self, win_id=False, count=False, hide=False, metavar=None, - zero_count=False, flag=None, completion=None, choices=None): + flag=None, completion=None, choices=None): if win_id and count: raise TypeError("Argument marked as both count/win_id!") - if zero_count and not count: - raise TypeError("zero_count argument cannot exist without count!") self.win_id = win_id self.count = count - self.zero_count = zero_count self.flag = flag self.hide = hide self.metavar = metavar @@ -51,7 +49,6 @@ class ArgInfo: def __eq__(self, other): return (self.win_id == other.win_id and self.count == other.count and - self.zero_count == other.zero_count and self.flag == other.flag and self.hide == other.hide and self.metavar == other.metavar and @@ -61,7 +58,6 @@ class ArgInfo: def __repr__(self): return utils.get_repr(self, win_id=self.win_id, count=self.count, flag=self.flag, hide=self.hide, - zero_count=self.zero_count, metavar=self.metavar, completion=self.completion, choices=self.choices, constructor=True) @@ -142,7 +138,6 @@ class Command: self.opt_args = collections.OrderedDict() self.namespace = None self._count = None - self._zero_count = None self.pos_args = [] self.desc = None self.flags_with_args = [] @@ -154,7 +149,7 @@ class Command: self._inspect_func() - def _check_prerequisites(self, win_id, count): + def _check_prerequisites(self, win_id): """Check if the command is permitted to run currently. Args: @@ -164,17 +159,11 @@ class Command: window=win_id) self.validate_mode(mode_manager.mode) - used_backend = usertypes.arg2backend[objreg.get('args').backend] - if self.backend is not None and used_backend != self.backend: + if self.backend is not None and objects.backend != self.backend: raise cmdexc.PrerequisitesError( "{}: Only available with {} " "backend.".format(self.name, self.backend.name)) - if count == 0 and not self._zero_count: - raise cmdexc.PrerequisitesError( - "{}: A zero count is not allowed for this command!" - .format(self.name)) - if self.deprecated: message.warning('{} is deprecated - {}'.format(self.name, self.deprecated)) @@ -246,9 +235,6 @@ class Command: assert param.kind != inspect.Parameter.POSITIONAL_ONLY if param.name == 'self': continue - arg_info = self.get_arg_info(param) - if arg_info.count: - self._zero_count = arg_info.zero_count if self._inspect_special_param(param): continue if (param.kind == inspect.Parameter.KEYWORD_ONLY and @@ -532,7 +518,7 @@ class Command: e.status, e)) return self._count = count - self._check_prerequisites(win_id, count) + self._check_prerequisites(win_id) posargs, kwargs = self._get_call_args(win_id) log.commands.debug('Calling {}'.format( debug_utils.format_call(self.handler, posargs, kwargs))) diff --git a/qutebrowser/commands/userscripts.py b/qutebrowser/commands/userscripts.py index 3c1d4d89e..4533e86b1 100644 --- a/qutebrowser/commands/userscripts.py +++ b/qutebrowser/commands/userscripts.py @@ -65,9 +65,13 @@ class _QtFIFOReader(QObject): """(Try to) read a line from the FIFO.""" log.procs.debug("QSocketNotifier triggered!") self._notifier.setEnabled(False) - for line in self._fifo: - self.got_line.emit(line.rstrip('\r\n')) - self._notifier.setEnabled(True) + try: + for line in self._fifo: + self.got_line.emit(line.rstrip('\r\n')) + self._notifier.setEnabled(True) + except UnicodeDecodeError as e: + log.misc.error("Invalid unicode in userscript output: {}" + .format(e)) def cleanup(self): """Clean up so the FIFO can be closed.""" @@ -289,6 +293,9 @@ class _WindowsUserscriptRunner(_BaseUserscriptRunner): self.got_cmd.emit(line.rstrip()) except OSError: log.procs.exception("Failed to read command file!") + except UnicodeDecodeError as e: + log.misc.error("Invalid unicode in userscript output: {}" + .format(e)) super()._cleanup() self.finished.emit() @@ -412,6 +419,8 @@ def run_async(tab, cmd, *args, win_id, env, verbose=False): env['QUTE_CONFIG_DIR'] = standarddir.config() env['QUTE_DATA_DIR'] = standarddir.data() env['QUTE_DOWNLOAD_DIR'] = downloads.download_dir() + env['QUTE_COMMANDLINE_TEXT'] = objreg.get('status-command', scope='window', + window=win_id).text() cmd_path = os.path.expanduser(cmd) diff --git a/qutebrowser/completion/completer.py b/qutebrowser/completion/completer.py index c5c1915ca..74c759c0d 100644 --- a/qutebrowser/completion/completer.py +++ b/qutebrowser/completion/completer.py @@ -239,7 +239,7 @@ class Completer(QObject): # This is a search or gibberish, so we don't need to complete # anything (yet) # FIXME complete searches - # https://github.com/The-Compiler/qutebrowser/issues/32 + # https://github.com/qutebrowser/qutebrowser/issues/32 completion.set_model(None) return diff --git a/qutebrowser/completion/completiondelegate.py b/qutebrowser/completion/completiondelegate.py index b48952d1c..dfb479b3f 100644 --- a/qutebrowser/completion/completiondelegate.py +++ b/qutebrowser/completion/completiondelegate.py @@ -54,7 +54,7 @@ class CompletionItemDelegate(QStyledItemDelegate): # FIXME this is horribly slow when resizing. # We should probably cache something in _get_textdoc or so, but as soon as # we implement eliding that cache probably isn't worth much anymore... - # https://github.com/The-Compiler/qutebrowser/issues/121 + # https://github.com/qutebrowser/qutebrowser/issues/121 def __init__(self, parent=None): self._painter = None @@ -173,7 +173,7 @@ class CompletionItemDelegate(QStyledItemDelegate): """ # FIXME we probably should do eliding here. See # qcommonstyle.cpp:viewItemDrawText - # https://github.com/The-Compiler/qutebrowser/issues/118 + # https://github.com/qutebrowser/qutebrowser/issues/118 text_option = QTextOption() if self._opt.features & QStyleOptionViewItem.WrapText: text_option.setWrapMode(QTextOption.WordWrap) diff --git a/qutebrowser/completion/completionwidget.py b/qutebrowser/completion/completionwidget.py index 8a8f9cfb6..bd6b8c5be 100644 --- a/qutebrowser/completion/completionwidget.py +++ b/qutebrowser/completion/completionwidget.py @@ -134,7 +134,7 @@ class CompletionView(QTreeView): self.setUniformRowHeights(True) self.hide() # FIXME set elidemode - # https://github.com/The-Compiler/qutebrowser/issues/118 + # https://github.com/qutebrowser/qutebrowser/issues/118 def __repr__(self): return utils.get_repr(self) @@ -150,10 +150,15 @@ class CompletionView(QTreeView): """Resize the completion columns based on column_widths.""" width = self.size().width() pixel_widths = [(width * perc // 100) for perc in self._column_widths] + if self.verticalScrollBar().isVisible(): - pixel_widths[-1] -= self.style().pixelMetric( - QStyle.PM_ScrollBarExtent) + 5 + delta = self.style().pixelMetric(QStyle.PM_ScrollBarExtent) + 5 + if pixel_widths[-1] > delta: + pixel_widths[-1] -= delta + else: + pixel_widths[-2] -= delta for i, w in enumerate(pixel_widths): + assert w >= 0, i self.setColumnWidth(i, w) def _next_idx(self, upwards): diff --git a/qutebrowser/completion/models/configmodel.py b/qutebrowser/completion/models/configmodel.py index 2983dbe5c..4058a5f00 100644 --- a/qutebrowser/completion/models/configmodel.py +++ b/qutebrowser/completion/models/configmodel.py @@ -30,7 +30,7 @@ class SettingSectionCompletionModel(base.BaseCompletionModel): """A CompletionModel filled with settings sections.""" - # https://github.com/The-Compiler/qutebrowser/issues/545 + # https://github.com/qutebrowser/qutebrowser/issues/545 # pylint: disable=abstract-method COLUMN_WIDTHS = (20, 70, 10) @@ -52,7 +52,7 @@ class SettingOptionCompletionModel(base.BaseCompletionModel): _section: The config section this model shows. """ - # https://github.com/The-Compiler/qutebrowser/issues/545 + # https://github.com/qutebrowser/qutebrowser/issues/545 # pylint: disable=abstract-method COLUMN_WIDTHS = (20, 70, 10) @@ -108,7 +108,7 @@ class SettingValueCompletionModel(base.BaseCompletionModel): _option: The config option this model shows. """ - # https://github.com/The-Compiler/qutebrowser/issues/545 + # https://github.com/qutebrowser/qutebrowser/issues/545 # pylint: disable=abstract-method COLUMN_WIDTHS = (20, 70, 10) diff --git a/qutebrowser/completion/models/miscmodels.py b/qutebrowser/completion/models/miscmodels.py index 14a48c399..76c1a8997 100644 --- a/qutebrowser/completion/models/miscmodels.py +++ b/qutebrowser/completion/models/miscmodels.py @@ -32,7 +32,7 @@ class CommandCompletionModel(base.BaseCompletionModel): """A CompletionModel filled with non-hidden commands and descriptions.""" - # https://github.com/The-Compiler/qutebrowser/issues/545 + # https://github.com/qutebrowser/qutebrowser/issues/545 # pylint: disable=abstract-method COLUMN_WIDTHS = (20, 60, 20) @@ -50,9 +50,11 @@ class HelpCompletionModel(base.BaseCompletionModel): """A CompletionModel filled with help topics.""" - # https://github.com/The-Compiler/qutebrowser/issues/545 + # https://github.com/qutebrowser/qutebrowser/issues/545 # pylint: disable=abstract-method + COLUMN_WIDTHS = (20, 60, 20) + def __init__(self, parent=None): super().__init__(parent) self._init_commands() @@ -87,7 +89,7 @@ class QuickmarkCompletionModel(base.BaseCompletionModel): """A CompletionModel filled with all quickmarks.""" - # https://github.com/The-Compiler/qutebrowser/issues/545 + # https://github.com/qutebrowser/qutebrowser/issues/545 # pylint: disable=abstract-method def __init__(self, parent=None): @@ -102,7 +104,7 @@ class BookmarkCompletionModel(base.BaseCompletionModel): """A CompletionModel filled with all bookmarks.""" - # https://github.com/The-Compiler/qutebrowser/issues/545 + # https://github.com/qutebrowser/qutebrowser/issues/545 # pylint: disable=abstract-method def __init__(self, parent=None): @@ -117,7 +119,7 @@ class SessionCompletionModel(base.BaseCompletionModel): """A CompletionModel filled with session names.""" - # https://github.com/The-Compiler/qutebrowser/issues/545 + # https://github.com/qutebrowser/qutebrowser/issues/545 # pylint: disable=abstract-method def __init__(self, parent=None): @@ -160,6 +162,7 @@ class TabCompletionModel(base.BaseCompletionModel): tab.title_changed.connect(self.rebuild) tab.shutting_down.connect(self.delayed_rebuild) tabbed_browser.new_tab.connect(self.on_new_tab) + tabbed_browser.tabBar().tabMoved.connect(self.rebuild) objreg.get("app").new_window.connect(self.on_new_window) self.rebuild() @@ -248,7 +251,7 @@ class BindCompletionModel(base.BaseCompletionModel): """A CompletionModel filled with all bindable commands and descriptions.""" - # https://github.com/The-Compiler/qutebrowser/issues/545 + # https://github.com/qutebrowser/qutebrowser/issues/545 # pylint: disable=abstract-method COLUMN_WIDTHS = (20, 60, 20) diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 57c498400..05d71605f 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -43,6 +43,7 @@ from qutebrowser.config.parsers import ini from qutebrowser.commands import cmdexc, cmdutils from qutebrowser.utils import (message, objreg, utils, standarddir, log, qtutils, error, usertypes) +from qutebrowser.misc import objects from qutebrowser.utils.usertypes import Completion @@ -233,7 +234,7 @@ def _init_misc(): # doesn't overwrite our config. # # This fixes one of the corruption issues here: - # https://github.com/The-Compiler/qutebrowser/issues/515 + # https://github.com/qutebrowser/qutebrowser/issues/515 path = os.path.join(standarddir.config(), 'qsettings') for fmt in [QSettings.NativeFormat, QSettings.IniFormat]: @@ -442,6 +443,7 @@ class ConfigManager(QObject): 'html > ::-webkit-scrollbar { width: 0px; height: 0px; }': '', '::-webkit-scrollbar { width: 0px; height: 0px; }': '', }), + ('contents', 'cache-size'): _get_value_transformer({'52428800': ''}), } changed = pyqtSignal(str, str) @@ -772,12 +774,12 @@ class ConfigManager(QObject): raise cmdexc.CommandError("set: {} - {}".format( e.__class__.__name__, e)) - @cmdutils.register(name='set', instance='config') + @cmdutils.register(name='set', instance='config', star_args_optional=True) @cmdutils.argument('section_', completion=Completion.section) @cmdutils.argument('option', completion=Completion.option) - @cmdutils.argument('value', completion=Completion.value) + @cmdutils.argument('values', completion=Completion.value) @cmdutils.argument('win_id', win_id=True) - def set_command(self, win_id, section_=None, option=None, value=None, + def set_command(self, win_id, section_=None, option=None, *values, temp=False, print_=False): """Set an option. @@ -793,7 +795,7 @@ class ConfigManager(QObject): Args: section_: The section where the option is in. option: The name of the option. - value: The value to set. + values: The value to set, or the values to cycle through. temp: Set value temporarily. print_: Print the value after setting. """ @@ -812,27 +814,46 @@ class ConfigManager(QObject): print_ = True else: with self._handle_config_error(): - if option.endswith('!') and option != '!' and value is None: + if option.endswith('!') and option != '!' and not values: + # Handle inversion as special cases of the cycle code path option = option[:-1] val = self.get(section_, option) - layer = 'temp' if temp else 'conf' if isinstance(val, bool): - self.set(layer, section_, option, str(not val).lower()) + values = ['false', 'true'] else: raise cmdexc.CommandError( "set: Attempted inversion of non-boolean value.") - elif value is not None: - layer = 'temp' if temp else 'conf' - self.set(layer, section_, option, value) - else: + elif not values: raise cmdexc.CommandError("set: The following arguments " "are required: value") + layer = 'temp' if temp else 'conf' + self._set_next(layer, section_, option, values) + if print_: with self._handle_config_error(): val = self.get(section_, option, transformed=False) message.info("{} {} = {}".format(section_, option, val)) + def _set_next(self, layer, section_, option, values): + """Set the next value out of a list of values.""" + if len(values) == 1: + # If we have only one value, just set it directly (avoid + # breaking stuff like aliases or other pseudo-settings) + self.set(layer, section_, option, values[0]) + else: + # Otherwise, use the next valid value from values, or the + # first if the current value does not appear in the list + assert len(values) > 1 + val = self.get(section_, option, transformed=False) + try: + idx = values.index(str(val)) + idx = (idx + 1) % len(values) + value = values[idx] + except ValueError: + value = values[0] + self.set(layer, section_, option, value) + def set(self, layer, sectname, optname, value, validate=True): """Set an option. @@ -863,10 +884,9 @@ class ConfigManager(QObject): # Will be handled later in .setv() pass else: - backend = usertypes.arg2backend[objreg.get('args').backend] if (allowed_backends is not None and - backend not in allowed_backends): - raise configexc.BackendError(backend) + objects.backend not in allowed_backends): + raise configexc.BackendError(objects.backend) else: interpolated = None diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 53378daea..fe75c3bcc 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -35,7 +35,7 @@ from qutebrowser.config import configtypes as typ from qutebrowser.config import sections as sect from qutebrowser.config.value import SettingValue from qutebrowser.utils.qtutils import MAXVALS -from qutebrowser.utils import usertypes +from qutebrowser.utils import usertypes, qtutils FIRST_COMMENT = r""" @@ -147,6 +147,13 @@ def data(readonly=False): "The URL parameters to strip with :yank url, separated by " "commas."), + ('default-open-dispatcher', + SettingValue(typ.String(none_ok=True), ''), + "The default program used to open downloads. Set to an empty " + "string to use the default internal handler.\n\n" + "Any {} in the string will be expanded to the filename, else " + "the filename will be appended."), + ('default-page', SettingValue(typ.FuzzyUrl(), '${startpage}'), "The page to open if :open -t/-b/-w is used without URL. Use " @@ -184,16 +191,22 @@ def data(readonly=False): "icons."), ('developer-extras', - SettingValue(typ.Bool(), 'false'), + SettingValue(typ.Bool(), 'false', + backends=[usertypes.Backend.QtWebKit]), "Enable extra tools for Web developers.\n\n" "This needs to be enabled for `:inspector` to work and also adds " - "an _Inspect_ entry to the context menu."), + "an _Inspect_ entry to the context menu. For QtWebEngine, see " + "'qutebrowser --help' instead."), ('print-element-backgrounds', SettingValue(typ.Bool(), 'true', - backends=[usertypes.Backend.QtWebKit]), + backends=( + None if qtutils.version_check('5.8', strict=True) + else [usertypes.Backend.QtWebKit])), "Whether the background color and images are also drawn when the " - "page is printed."), + "page is printed.\n" + "This setting only works with Qt 5.8 or newer when using the " + "QtWebEngine backend."), ('xss-auditing', SettingValue(typ.Bool(), 'false'), @@ -206,7 +219,7 @@ def data(readonly=False): ('site-specific-quirks', SettingValue(typ.Bool(), 'true', backends=[usertypes.Backend.QtWebKit]), - "Enable workarounds for broken sites."), + "Enable QtWebKit workarounds for broken sites."), ('default-encoding', SettingValue(typ.String(none_ok=True), ''), @@ -338,7 +351,8 @@ def data(readonly=False): ('smooth-scrolling', SettingValue(typ.Bool(), 'false'), - "Whether to enable smooth scrolling for webpages."), + "Whether to enable smooth scrolling for web pages. Note smooth " + "scrolling does not work with the :scroll-px command."), ('remove-finished-downloads', SettingValue(typ.Int(minval=-1), '-1'), @@ -390,6 +404,10 @@ def data(readonly=False): SettingValue(typ.Int(minval=0), '8'), "The rounding radius for the edges of prompts."), + ('prompt-filebrowser', + SettingValue(typ.Bool(), 'true'), + "Show a filebrowser in upload/download prompts."), + readonly=readonly )), @@ -420,10 +438,13 @@ def data(readonly=False): ('proxy', SettingValue(typ.Proxy(), 'system', - backends=[usertypes.Backend.QtWebKit]), + backends=(None if qtutils.version_check('5.8') + else [usertypes.Backend.QtWebKit])), "The proxy to use.\n\n" "In addition to the listed values, you can use a `socks://...` " - "or `http://...` URL."), + "or `http://...` URL.\n\n" + "This setting only works with Qt 5.8 or newer when using the " + "QtWebEngine backend."), ('proxy-dns-requests', SettingValue(typ.Bool(), 'true', @@ -570,7 +591,7 @@ def data(readonly=False): "disables the context menu."), ('mouse-zoom-divider', - SettingValue(typ.Int(minval=1), '512'), + SettingValue(typ.Int(minval=0), '512'), "How much to divide the mouse wheel movements to translate them " "into zoom increments."), @@ -792,9 +813,10 @@ def data(readonly=False): "enabled."), ('cache-size', - SettingValue(typ.Int(minval=0, maxval=MAXVALS['int64']), - '52428800'), - "Size of the HTTP network cache."), + SettingValue(typ.Int(none_ok=True, minval=0, + maxval=MAXVALS['int64']), ''), + "Size of the HTTP network cache. Empty to use the default " + "value."), readonly=readonly )), @@ -815,9 +837,8 @@ def data(readonly=False): "are not affected by this setting."), ('webgl', - SettingValue(typ.Bool(), 'false'), - "Enables or disables WebGL. For QtWebEngine, Qt/PyQt >= 5.7 is " - "required for this setting."), + SettingValue(typ.Bool(), 'true'), + "Enables or disables WebGL."), ('css-regions', SettingValue(typ.Bool(), 'true', @@ -854,7 +875,8 @@ def data(readonly=False): ('javascript-can-access-clipboard', SettingValue(typ.Bool(), 'false'), "Whether JavaScript programs can read or write to the " - "clipboard."), + "clipboard.\nWith QtWebEngine, writing the clipboard as response " + "to a user interaction is always allowed."), ('ignore-javascript-prompt', SettingValue(typ.Bool(), 'false'), @@ -888,9 +910,9 @@ def data(readonly=False): "Control which cookies to accept."), ('cookies-store', - SettingValue(typ.Bool(), 'true', - backends=[usertypes.Backend.QtWebKit]), - "Whether to store cookies."), + SettingValue(typ.Bool(), 'true'), + "Whether to store cookies. Note this option needs a restart with " + "QtWebEngine."), ('host-block-lists', SettingValue( @@ -936,7 +958,9 @@ def data(readonly=False): ('mode', SettingValue(typ.String( valid_values=typ.ValidValues( - ('number', "Use numeric hints."), + ('number', "Use numeric hints. (In this mode you can " + "also type letters form the hinted element to filter " + "and reduce the number of elements that are hinted.)"), ('letter', "Use the chars in the hints -> " "chars setting."), ('word', "Use hints words based on the html " @@ -1268,8 +1292,7 @@ def data(readonly=False): "Background color for downloads with errors."), ('webpage.bg', - SettingValue(typ.QtColor(none_ok=True), 'white', - backends=[usertypes.Backend.QtWebKit]), + SettingValue(typ.QtColor(none_ok=True), 'white'), "Background color for webpages if unset (or empty to use the " "theme's color)"), @@ -1543,7 +1566,8 @@ KEY_DATA = collections.OrderedDict([ ])), ('normal', collections.OrderedDict([ - ('clear-keychain ;; search', ['']), + ('clear-keychain ;; search ;; fullscreen --leave', + ['', '']), ('set-cmd-text -s :open', ['o']), ('set-cmd-text :open {url:pretty}', ['go']), ('set-cmd-text -s :open -t', ['O']), @@ -1569,10 +1593,10 @@ KEY_DATA = collections.OrderedDict([ ('tab-clone', ['gC']), ('reload', ['r', '']), ('reload -f', ['R', '']), - ('back', ['H']), + ('back', ['H', '']), ('back -t', ['th']), ('back -w', ['wh']), - ('forward', ['L']), + ('forward', ['L', '']), ('forward -t', ['tl']), ('forward -w', ['wl']), ('fullscreen', ['']), @@ -1650,7 +1674,8 @@ KEY_DATA = collections.OrderedDict([ ('set-cmd-text -s :buffer', ['gt']), ('tab-focus last', ['']), ('enter-mode passthrough', ['']), - ('quit', ['']), + ('quit', ['', 'ZQ']), + ('wq', ['ZZ']), ('scroll-page 0 1', ['']), ('scroll-page 0 -1', ['']), ('scroll-page 0 0.5', ['']), @@ -1764,8 +1789,12 @@ CHANGED_KEY_COMMANDS = [ (re.compile(r'^download-page$'), r'download'), (re.compile(r'^cancel-download$'), r'download-cancel'), - (re.compile(r"""^search (''|"")$"""), r'clear-keychain ;; search'), - (re.compile(r'^search$'), r'clear-keychain ;; search'), + (re.compile(r"""^search (''|"")$"""), + r'clear-keychain ;; search ;; fullscreen --leave'), + (re.compile(r'^search$'), + r'clear-keychain ;; search ;; fullscreen --leave'), + (re.compile(r'^clear-keychain ;; search$'), + r'clear-keychain ;; search ;; fullscreen --leave'), (re.compile(r"""^set-cmd-text ['"](.*) ['"]$"""), r'set-cmd-text -s \1'), (re.compile(r"""^set-cmd-text ['"](.*)['"]$"""), r'set-cmd-text \1'), @@ -1779,7 +1808,8 @@ CHANGED_KEY_COMMANDS = [ (re.compile(r'^scroll 50 0$'), r'scroll right'), (re.compile(r'^scroll ([-\d]+ [-\d]+)$'), r'scroll-px \1'), - (re.compile(r'^search *;; *clear-keychain$'), r'clear-keychain ;; search'), + (re.compile(r'^search *;; *clear-keychain$'), + r'clear-keychain ;; search ;; fullscreen --leave'), (re.compile(r'^clear-keychain *;; *leave-mode$'), r'leave-mode'), (re.compile(r'^download-remove --all$'), r'download-clear'), diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index 457781e77..2dad85117 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -31,7 +31,6 @@ import datetime from PyQt5.QtCore import QUrl, Qt from PyQt5.QtGui import QColor, QFont -from PyQt5.QtNetwork import QNetworkProxy from PyQt5.QtWidgets import QTabWidget, QTabBar from qutebrowser.commands import cmdutils @@ -695,30 +694,17 @@ class CssColor(BaseType): class QssColor(CssColor): - """Base class for a color value. - - Class attributes: - color_func_regexes: Valid function regexes. - """ - - num = r'[0-9]{1,3}%?' - - color_func_regexes = [ - r'rgb\({num},\s*{num},\s*{num}\)'.format(num=num), - r'rgba\({num},\s*{num},\s*{num},\s*{num}\)'.format(num=num), - r'hsv\({num},\s*{num},\s*{num}\)'.format(num=num), - r'hsva\({num},\s*{num},\s*{num},\s*{num}\)'.format(num=num), - r'qlineargradient\(.*\)', - r'qradialgradient\(.*\)', - r'qconicalgradient\(.*\)', - ] + """Color used in a Qt stylesheet.""" def validate(self, value): + functions = ['rgb', 'rgba', 'hsv', 'hsva', 'qlineargradient', + 'qradialgradient', 'qconicalgradient'] self._basic_validation(value) if not value: return - elif any(re.match(r, value) for r in self.color_func_regexes): - # QColor doesn't handle these, so we do the best we can easily + elif (any(value.startswith(func + '(') for func in functions) and + value.endswith(')')): + # QColor doesn't handle these pass elif QColor.isValidColor(value): pass @@ -742,15 +728,15 @@ class Font(BaseType): ) | # size (pt | px) (?P[0-9]+((\.[0-9]+)?[pP][tT]|[pP][xX])) - )\ # size/weight/style are space-separated - )* # 0-inf size/weight/style tags - (?P[A-Za-z0-9, "-]*)$ # mandatory font family""", re.VERBOSE) + )\ # size/weight/style are space-separated + )* # 0-inf size/weight/style tags + (?P.+)$ # mandatory font family""", re.VERBOSE) def validate(self, value): self._basic_validation(value) if not value: return - elif not self.font_regex.match(value): + elif not self.font_regex.match(value): # pragma: no cover raise configexc.ValidationError(value, "must be a valid font") @@ -763,7 +749,7 @@ class FontFamily(Font): if not value: return match = self.font_regex.match(value) - if not match: + if not match: # pragma: no cover raise configexc.ValidationError(value, "must be a valid font") for group in 'style', 'weight', 'namedweight', 'size': if match.group(group): @@ -1018,12 +1004,6 @@ class Proxy(BaseType): """A proxy URL or special value.""" - PROXY_TYPES = { - 'http': QNetworkProxy.HttpProxy, - 'socks': QNetworkProxy.Socks5Proxy, - 'socks5': QNetworkProxy.Socks5Proxy, - } - def __init__(self, none_ok=False): super().__init__(none_ok) self.valid_values = ValidValues( @@ -1031,19 +1011,17 @@ class Proxy(BaseType): ('none', "Don't use any proxy")) def validate(self, value): + from qutebrowser.utils import urlutils self._basic_validation(value) if not value: return elif value in self.valid_values: return - url = QUrl(value) - if not url.isValid(): - raise configexc.ValidationError( - value, "invalid url, {}".format(url.errorString())) - elif url.scheme() not in self.PROXY_TYPES: - raise configexc.ValidationError(value, "must be a proxy URL " - "(http://... or socks://...) or " - "system/none!") + + try: + self.transform(value) + except (urlutils.InvalidUrlError, urlutils.InvalidProxyTypeError) as e: + raise configexc.ValidationError(value, e) def complete(self): out = [] @@ -1053,25 +1031,21 @@ class Proxy(BaseType): out.append(('socks://', 'SOCKS proxy URL')) out.append(('socks://localhost:9050/', 'Tor via SOCKS')) out.append(('http://localhost:8080/', 'Local HTTP proxy')) + out.append(('pac+https://example.com/proxy.pac', 'Proxy autoconfiguration file URL')) return out def transform(self, value): + from qutebrowser.utils import urlutils if not value: return None elif value == 'system': return SYSTEM_PROXY - elif value == 'none': - return QNetworkProxy(QNetworkProxy.NoProxy) - url = QUrl(value) - typ = self.PROXY_TYPES[url.scheme()] - proxy = QNetworkProxy(typ, url.host()) - if url.port() != -1: - proxy.setPort(url.port()) - if url.userName(): - proxy.setUser(url.userName()) - if url.password(): - proxy.setPassword(url.password()) - return proxy + + if value == 'none': + url = QUrl('direct://') + else: + url = QUrl(value) + return urlutils.proxy_from_url(url) class SearchEngineName(BaseType): diff --git a/qutebrowser/config/parsers/keyconf.py b/qutebrowser/config/parsers/keyconf.py index e602c33d5..6ca5f72b7 100644 --- a/qutebrowser/config/parsers/keyconf.py +++ b/qutebrowser/config/parsers/keyconf.py @@ -280,6 +280,9 @@ class KeyConfigParser(QObject): A binding is considered new if both the command is not bound to any key yet, and the key isn't used anywhere else in the same section. """ + if utils.is_special_key(keychain): + keychain = keychain.lower() + try: bindings = self.keybindings[sectname] except KeyError: @@ -432,11 +435,13 @@ class KeyConfigParser(QObject): def get_reverse_bindings_for(self, section): """Get a dict of commands to a list of bindings for the section.""" cmd_to_keys = {} - for key, cmd in self.get_bindings_for(section).items(): - cmd_to_keys.setdefault(cmd, []) - # put special bindings last - if utils.is_special_key(key): - cmd_to_keys[cmd].append(key) - else: - cmd_to_keys[cmd].insert(0, key) + for key, full_cmd in self.get_bindings_for(section).items(): + for cmd in full_cmd.split(';;'): + cmd = cmd.strip() + cmd_to_keys.setdefault(cmd, []) + # put special bindings last + if utils.is_special_key(key): + cmd_to_keys[cmd].append(key) + else: + cmd_to_keys[cmd].insert(0, key) return cmd_to_keys diff --git a/qutebrowser/config/websettings.py b/qutebrowser/config/websettings.py index 6f1304057..b6e010499 100644 --- a/qutebrowser/config/websettings.py +++ b/qutebrowser/config/websettings.py @@ -20,7 +20,8 @@ """Bridge from QWeb(Engine)Settings to our own settings.""" from qutebrowser.config import config -from qutebrowser.utils import log, utils, debug, objreg +from qutebrowser.utils import log, utils, debug, usertypes +from qutebrowser.misc import objects UNSET = object() @@ -259,19 +260,19 @@ def update_mappings(mappings, section, option): mapping.set(value) -def init(): +def init(args): """Initialize all QWeb(Engine)Settings.""" - if objreg.get('args').backend == 'webengine': + if objects.backend == usertypes.Backend.QtWebEngine: from qutebrowser.browser.webengine import webenginesettings - webenginesettings.init() + webenginesettings.init(args) else: from qutebrowser.browser.webkit import webkitsettings - webkitsettings.init() + webkitsettings.init(args) def shutdown(): """Shut down QWeb(Engine)Settings.""" - if objreg.get('args').backend == 'webengine': + if objects.backend == usertypes.Backend.QtWebEngine: from qutebrowser.browser.webengine import webenginesettings webenginesettings.shutdown() else: diff --git a/qutebrowser/html/bookmarks.html b/qutebrowser/html/bookmarks.html index 2f82453ab..7e6f46ea9 100644 --- a/qutebrowser/html/bookmarks.html +++ b/qutebrowser/html/bookmarks.html @@ -1,34 +1,66 @@ -{% extends "base.html" %} +{% extends "styled.html" %} {% block style %} -table { border: 1px solid grey; border-collapse: collapse; width: 100%;} -th, td { border: 1px solid grey; padding: 0px 5px; } -th { background: lightgrey; } +{{super()}} +h1 { + margin-bottom: 10px; +} + +.url a { + color: #444; +} + +th { + text-align: left; +} + +.qmarks .name { + padding-left: 5px; +} + +.empty-msg { + background-color: #f8f8f8; + color: #444; + display: inline-block; + text-align: center; + width: 100%; +} {% endblock %} {% block content %} - - - - - - {% for url, title in bookmarks %} - - - - - {% endfor %} - - - - +

Quickmarks

+ +{% if quickmarks|length %} +

Bookmark

URL

{{title}}{{url}}

Quickmark

URL

+ {% for name, url in quickmarks %} - - + + {% endfor %} +
{{name}}{{url}}{{name}}{{url}}
+{% else %} +You have no quickmarks +{% endif %} + +

Bookmarks

+ +{% if bookmarks|length %} + + + {% for url, title in bookmarks %} + + + + + {% endfor %} + +
{{title | default(url, true)}}{{url}}
+{% else %} +You have no bookmarks +{% endif %} {% endblock %} diff --git a/qutebrowser/html/history.html b/qutebrowser/html/history.html new file mode 100644 index 000000000..94f82182f --- /dev/null +++ b/qutebrowser/html/history.html @@ -0,0 +1,58 @@ +{% extends "styled.html" %} + +{% block style %} +{{super()}} +body { + max-width: 1440px; +} + +td.title { + word-break: break-all; +} + +td.time { + color: #555; + text-align: right; + white-space: nowrap; +} + +.date { + color: #888; + font-size: 14pt; + padding-left: 25px; +} + +.pagination-link { + display: inline-block; + margin-bottom: 10px; + margin-top: 10px; + padding-right: 10px; +} + +.pagination-link > a { + color: #333; + font-weight: bold; +} +{% endblock %} + +{% block content %} + +

Browsing history {{curr_date.strftime("%a, %d %B %Y")}}

+ + + + {% for url, title, time in history %} + + + + + {% endfor %} + +
{{title}}{{time}}
+ +
+{% if today >= next_date %} + +{% endif %} + +{% endblock %} diff --git a/qutebrowser/html/styled.html b/qutebrowser/html/styled.html new file mode 100644 index 000000000..e2a608538 --- /dev/null +++ b/qutebrowser/html/styled.html @@ -0,0 +1,41 @@ +{% extends "base.html" %} + +{% block style %} +a { + text-decoration: none; + color: #2562dc +} + +a:hover { + text-decoration: underline; +} + +body { + background: #fefefe; + font-family: sans-serif; + margin: 0 auto; + max-width: 1280px; + padding-left: 20px; + padding-right: 20px; +} + +h1 { + color: #444; + font-weight: normal; +} + +table { + border-collapse: collapse; + width: 100%; +} + +tbody tr:nth-child(odd) { + background-color: #f8f8f8; +} + +td { + max-width: 50%; + padding: 2px 5px; + text-align: left; +} +{% endblock %} diff --git a/qutebrowser/javascript/.eslintignore b/qutebrowser/javascript/.eslintignore new file mode 100644 index 000000000..ca4d3c667 --- /dev/null +++ b/qutebrowser/javascript/.eslintignore @@ -0,0 +1,2 @@ +# Upstream Mozilla's code +pac_utils.js diff --git a/qutebrowser/javascript/.eslintrc.yaml b/qutebrowser/javascript/.eslintrc.yaml index de3f9e6ef..ef7ed00b5 100644 --- a/qutebrowser/javascript/.eslintrc.yaml +++ b/qutebrowser/javascript/.eslintrc.yaml @@ -36,3 +36,5 @@ rules: sort-keys: "off" no-warning-comments: "off" max-len: ["error", {"ignoreUrls": true}] + capitalized-comments: "off" + prefer-destructuring: "off" diff --git a/qutebrowser/javascript/pac_utils.js b/qutebrowser/javascript/pac_utils.js new file mode 100644 index 000000000..0aba4c070 --- /dev/null +++ b/qutebrowser/javascript/pac_utils.js @@ -0,0 +1,257 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is mozilla.org code. + * + * The Initial Developer of the Original Code is + * Netscape Communications Corporation. + * Portions created by the Initial Developer are Copyright (C) 1998 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Akhil Arora + * Tomi Leppikangas + * Darin Fisher + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +/* + Script for Proxy Auto Config in the new world order. + - Gagan Saksena 04/24/00 +*/ + +function dnsDomainIs(host, domain) { + return (host.length >= domain.length && + host.substring(host.length - domain.length) == domain); +} + +function dnsDomainLevels(host) { + return host.split('.').length-1; +} + +function convert_addr(ipchars) { + var bytes = ipchars.split('.'); + var result = ((bytes[0] & 0xff) << 24) | + ((bytes[1] & 0xff) << 16) | + ((bytes[2] & 0xff) << 8) | + (bytes[3] & 0xff); + return result; +} + +function isInNet(ipaddr, pattern, maskstr) { + var test = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/ + .exec(ipaddr); + if (test == null) { + ipaddr = dnsResolve(ipaddr); + if (ipaddr == null) + return false; + } else if (test[1] > 255 || test[2] > 255 || + test[3] > 255 || test[4] > 255) { + return false; // not an IP address + } + var host = convert_addr(ipaddr); + var pat = convert_addr(pattern); + var mask = convert_addr(maskstr); + return ((host & mask) == (pat & mask)); +} + +function isPlainHostName(host) { + return (host.search('\\.') == -1); +} + +function isResolvable(host) { + var ip = dnsResolve(host); + return (ip != null); +} + +function localHostOrDomainIs(host, hostdom) { + return (host == hostdom) || + (hostdom.lastIndexOf(host + '.', 0) == 0); +} + +function shExpMatch(url, pattern) { + pattern = pattern.replace(/\./g, '\\.'); + pattern = pattern.replace(/\*/g, '.*'); + pattern = pattern.replace(/\?/g, '.'); + var newRe = new RegExp('^'+pattern+'$'); + return newRe.test(url); +} + +var wdays = {SUN: 0, MON: 1, TUE: 2, WED: 3, THU: 4, FRI: 5, SAT: 6}; + +var months = {JAN: 0, FEB: 1, MAR: 2, APR: 3, MAY: 4, JUN: 5, JUL: 6, + AUG: 7, SEP: 8, OCT: 9, NOV: 10, DEC: 11}; + +function weekdayRange() { + function getDay(weekday) { + if (weekday in wdays) { + return wdays[weekday]; + } + return -1; + } + var date = new Date(); + var argc = arguments.length; + var wday; + if (argc < 1) + return false; + if (arguments[argc - 1] == 'GMT') { + argc--; + wday = date.getUTCDay(); + } else { + wday = date.getDay(); + } + var wd1 = getDay(arguments[0]); + var wd2 = (argc == 2) ? getDay(arguments[1]) : wd1; + return (wd1 == -1 || wd2 == -1) ? false + : (wd1 <= wday && wday <= wd2); +} + +function dateRange() { + function getMonth(name) { + if (name in months) { + return months[name]; + } + return -1; + } + var date = new Date(); + var argc = arguments.length; + if (argc < 1) { + return false; + } + var isGMT = (arguments[argc - 1] == 'GMT'); + + if (isGMT) { + argc--; + } + // function will work even without explict handling of this case + if (argc == 1) { + var tmp = parseInt(arguments[0]); + if (isNaN(tmp)) { + return ((isGMT ? date.getUTCMonth() : date.getMonth()) == + getMonth(arguments[0])); + } else if (tmp < 32) { + return ((isGMT ? date.getUTCDate() : date.getDate()) == tmp); + } else { + return ((isGMT ? date.getUTCFullYear() : date.getFullYear()) == + tmp); + } + } + var year = date.getFullYear(); + var date1, date2; + date1 = new Date(year, 0, 1, 0, 0, 0); + date2 = new Date(year, 11, 31, 23, 59, 59); + var adjustMonth = false; + for (var i = 0; i < (argc >> 1); i++) { + var tmp = parseInt(arguments[i]); + if (isNaN(tmp)) { + var mon = getMonth(arguments[i]); + date1.setMonth(mon); + } else if (tmp < 32) { + adjustMonth = (argc <= 2); + date1.setDate(tmp); + } else { + date1.setFullYear(tmp); + } + } + for (var i = (argc >> 1); i < argc; i++) { + var tmp = parseInt(arguments[i]); + if (isNaN(tmp)) { + var mon = getMonth(arguments[i]); + date2.setMonth(mon); + } else if (tmp < 32) { + date2.setDate(tmp); + } else { + date2.setFullYear(tmp); + } + } + if (adjustMonth) { + date1.setMonth(date.getMonth()); + date2.setMonth(date.getMonth()); + } + if (isGMT) { + var tmp = date; + tmp.setFullYear(date.getUTCFullYear()); + tmp.setMonth(date.getUTCMonth()); + tmp.setDate(date.getUTCDate()); + tmp.setHours(date.getUTCHours()); + tmp.setMinutes(date.getUTCMinutes()); + tmp.setSeconds(date.getUTCSeconds()); + date = tmp; + } + return ((date1 <= date) && (date <= date2)); +} + +function timeRange() { + var argc = arguments.length; + var date = new Date(); + var isGMT= false; + + if (argc < 1) { + return false; + } + if (arguments[argc - 1] == 'GMT') { + isGMT = true; + argc--; + } + + var hour = isGMT ? date.getUTCHours() : date.getHours(); + var date1, date2; + date1 = new Date(); + date2 = new Date(); + + if (argc == 1) { + return (hour == arguments[0]); + } else if (argc == 2) { + return ((arguments[0] <= hour) && (hour <= arguments[1])); + } else { + switch (argc) { + case 6: + date1.setSeconds(arguments[2]); + date2.setSeconds(arguments[5]); + case 4: + var middle = argc >> 1; + date1.setHours(arguments[0]); + date1.setMinutes(arguments[1]); + date2.setHours(arguments[middle]); + date2.setMinutes(arguments[middle + 1]); + if (middle == 2) { + date2.setSeconds(59); + } + break; + default: + throw 'timeRange: bad number of arguments' + } + } + + if (isGMT) { + date.setFullYear(date.getUTCFullYear()); + date.setMonth(date.getUTCMonth()); + date.setDate(date.getUTCDate()); + date.setHours(date.getUTCHours()); + date.setMinutes(date.getUTCMinutes()); + date.setSeconds(date.getUTCSeconds()); + } + return ((date1 <= date) && (date <= date2)); +} diff --git a/qutebrowser/javascript/webelem.js b/qutebrowser/javascript/webelem.js index 84d38b348..206cdf129 100644 --- a/qutebrowser/javascript/webelem.js +++ b/qutebrowser/javascript/webelem.js @@ -17,6 +17,23 @@ * along with qutebrowser. If not, see . */ +/** + * The connection for web elements between Python and Javascript works like + * this: + * + * - Python calls into Javascript and invokes a function to find elements (like + * find_all, focus_element, element_at_pos or element_by_id). + * - Javascript gets the requested element, and calls serialize_elem on it. + * - serialize_elem saves the javascript element object in "elements", gets some + * attributes from the element, and assigns an ID (index into 'elements') to + * it. + * - Python gets this information and constructs a Python wrapper object with + * the information it got right away, and the ID. + * - When Python wants to modify an element, it calls javascript again with the + * element ID. + * - Javascript gets the element from the elements array, and modifies it. + */ + "use strict"; window._qutebrowser.webelem = (function() { @@ -92,14 +109,17 @@ window._qutebrowser.webelem = (function() { } var style = win.getComputedStyle(elem, null); - // FIXME:qtwebengine do we need this handling? - // visibility and display style are misleading for area tags and they - // get "display: none" by default. - // See https://github.com/vimperator/vimperator-labs/issues/236 - if (elem.nodeName.toLowerCase() !== "area" && ( - style.getPropertyValue("visibility") !== "visible" || - style.getPropertyValue("display") === "none")) { - return false; + if (style.getPropertyValue("visibility") !== "visible" || + style.getPropertyValue("display") === "none" || + style.getPropertyValue("opacity") === "0") { + // FIXME:qtwebengine do we need this handling? + // visibility and display style are misleading for area tags and + // they get "display: none" by default. + // See https://github.com/vimperator/vimperator-labs/issues/236 + if (elem.nodeName.toLowerCase() !== "area" && + !elem.classList.contains("ace_text-input")) { + return false; + } } return true; @@ -136,9 +156,8 @@ window._qutebrowser.webelem = (function() { funcs.insert_text = function(id, text) { var elem = elements[id]; - var event = document.createEvent("TextEvent"); - event.initTextEvent("textInput", true, true, null, text); - elem.dispatchEvent(event); + elem.focus(); + document.execCommand("insertText", false, text); }; funcs.element_at_pos = function(x, y) { @@ -174,5 +193,21 @@ window._qutebrowser.webelem = (function() { } }; + funcs.click = function(id) { + var elem = elements[id]; + elem.click(); + }; + + funcs.focus = function(id) { + var elem = elements[id]; + elem.focus(); + }; + + funcs.move_cursor_to_end = function(id) { + var elem = elements[id]; + elem.selectionStart = elem.value.length; + elem.selectionEnd = elem.value.length; + }; + return funcs; })(); diff --git a/qutebrowser/keyinput/basekeyparser.py b/qutebrowser/keyinput/basekeyparser.py index 3114f4663..7325223a3 100644 --- a/qutebrowser/keyinput/basekeyparser.py +++ b/qutebrowser/keyinput/basekeyparser.py @@ -147,6 +147,9 @@ class BaseKeyParser(QObject): (countstr, cmd_input) = re.match(r'^(\d*)(.*)', self._keystring).groups() count = int(countstr) if countstr else None + if count == 0 and not cmd_input: + cmd_input = self._keystring + count = None else: cmd_input = self._keystring count = None diff --git a/qutebrowser/keyinput/modeman.py b/qutebrowser/keyinput/modeman.py index d86b4996f..542081719 100644 --- a/qutebrowser/keyinput/modeman.py +++ b/qutebrowser/keyinput/modeman.py @@ -28,6 +28,7 @@ from qutebrowser.keyinput import modeparsers, keyparser from qutebrowser.config import config from qutebrowser.commands import cmdexc, cmdutils from qutebrowser.utils import usertypes, log, objreg, utils +from qutebrowser.misc import objects class KeyEvent: @@ -265,6 +266,16 @@ class ModeManager(QObject): m = usertypes.KeyMode[mode] except KeyError: raise cmdexc.CommandError("Mode {} does not exist!".format(mode)) + + if m in [usertypes.KeyMode.hint, usertypes.KeyMode.command, + usertypes.KeyMode.yesno, usertypes.KeyMode.prompt]: + raise cmdexc.CommandError( + "Mode {} can't be entered manually!".format(mode)) + elif (m == usertypes.KeyMode.caret and + objects.backend == usertypes.Backend.QtWebEngine): + raise cmdexc.CommandError("Caret mode is not supported with " + "QtWebEngine yet.") + self.enter(m, 'command') @pyqtSlot(usertypes.KeyMode, str, bool) @@ -288,7 +299,7 @@ class ModeManager(QObject): log.modes.debug("Leaving mode {}{}".format( mode, '' if reason is None else ' (reason: {})'.format(reason))) # leaving a mode implies clearing keychain, see - # https://github.com/The-Compiler/qutebrowser/issues/1805 + # https://github.com/qutebrowser/qutebrowser/issues/1805 self.clear_keychain() self.mode = usertypes.KeyMode.normal self.left.emit(mode, self.mode, self._win_id) diff --git a/qutebrowser/keyinput/modeparsers.py b/qutebrowser/keyinput/modeparsers.py index 6fa881ad7..db540b58e 100644 --- a/qutebrowser/keyinput/modeparsers.py +++ b/qutebrowser/keyinput/modeparsers.py @@ -267,7 +267,7 @@ class CaretKeyParser(keyparser.CommandKeyParser): self.read_config('caret') -class RegisterKeyParser(keyparser.BaseKeyParser): +class RegisterKeyParser(keyparser.CommandKeyParser): """KeyParser for modes that record a register key. @@ -280,6 +280,7 @@ class RegisterKeyParser(keyparser.BaseKeyParser): super().__init__(win_id, parent, supports_count=False, supports_chains=False) self._mode = mode + self.read_config('register') def handle(self, e): """Override handle to always match the next key and use the register. @@ -290,12 +291,15 @@ class RegisterKeyParser(keyparser.BaseKeyParser): Return: True if event has been handled, False otherwise. """ - if utils.keyevent_to_string(e) is None: - # this is a modifier key, let it pass and keep going - return False + if super().handle(e): + return True key = e.text() + if key == '' or utils.keyevent_to_string(e) is None: + # this is not a proper register key, let it pass and keep going + return False + tabbed_browser = objreg.get('tabbed-browser', scope='window', window=self._win_id) macro_recorder = objreg.get('macro-recorder') @@ -323,7 +327,3 @@ class RegisterKeyParser(keyparser.BaseKeyParser): def on_keyconfig_changed(self, mode): """RegisterKeyParser has no config section (no bindable keys).""" pass - - def execute(self, cmdstr, _keytype, count=None): - """Should never be called on RegisterKeyParser.""" - assert False diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py index f7d326a09..9858abf7d 100644 --- a/qutebrowser/mainwindow/mainwindow.py +++ b/qutebrowser/mainwindow/mainwindow.py @@ -238,13 +238,25 @@ class MainWindow(QWidget): height_padding = 20 status_position = config.get('ui', 'status-position') if status_position == 'bottom': - top = self.height() - self.status.height() - size_hint.height() + if self.status.isVisible(): + status_height = self.status.height() + bottom = self.status.geometry().top() + else: + status_height = 0 + bottom = self.height() + top = self.height() - status_height - size_hint.height() top = qtutils.check_overflow(top, 'int', fatal=False) topleft = QPoint(left, max(height_padding, top)) - bottomright = QPoint(left + width, self.status.geometry().top()) + bottomright = QPoint(left + width, bottom) elif status_position == 'top': - topleft = QPoint(left, self.status.geometry().bottom()) - bottom = self.status.height() + size_hint.height() + if self.status.isVisible(): + status_height = self.status.height() + top = self.status.geometry().bottom() + else: + status_height = 0 + top = 0 + topleft = QPoint(left, top) + bottom = status_height + size_hint.height() bottom = qtutils.check_overflow(bottom, 'int', fatal=False) bottomright = QPoint(left + width, min(self.height() - height_padding, bottom)) @@ -425,6 +437,9 @@ class MainWindow(QWidget): # messages message.global_bridge.show_message.connect( self._messageview.show_message) + message.global_bridge.flush() + message.global_bridge.clear_messages.connect( + self._messageview.clear_messages) message_bridge.s_set_text.connect(status.set_text) message_bridge.s_maybe_reset_text.connect(status.txt.maybe_reset_text) @@ -444,6 +459,10 @@ class MainWindow(QWidget): tabs.cur_url_changed.connect(status.url.set_url) tabs.cur_link_hovered.connect(status.url.set_hover_url) tabs.cur_load_status_changed.connect(status.url.on_load_status_changed) + tabs.page_fullscreen_requested.connect( + self._on_page_fullscreen_requested) + tabs.page_fullscreen_requested.connect( + status.on_page_fullscreen_requested) # command input / completion mode_manager.left.connect(tabs.on_mode_left) @@ -451,6 +470,13 @@ class MainWindow(QWidget): completion_obj.on_clear_completion_selection) cmd.hide_completion.connect(completion_obj.hide) + @pyqtSlot(bool) + def _on_page_fullscreen_requested(self, on): + if on: + self.showFullScreen() + else: + self.showNormal() + @cmdutils.register(instance='main-window', scope='window') @pyqtSlot() def close(self): @@ -462,14 +488,6 @@ class MainWindow(QWidget): """ super().close() - @cmdutils.register(instance='main-window', scope='window') - def fullscreen(self): - """Toggle fullscreen mode.""" - if self.isFullScreen(): - self.showNormal() - else: - self.showFullScreen() - def resizeEvent(self, e): """Extend resizewindow's resizeEvent to adjust completion. diff --git a/qutebrowser/mainwindow/messageview.py b/qutebrowser/mainwindow/messageview.py index 3afbff102..6565a2a44 100644 --- a/qutebrowser/mainwindow/messageview.py +++ b/qutebrowser/mainwindow/messageview.py @@ -31,8 +31,9 @@ class Message(QLabel): """A single error/warning/info message.""" - def __init__(self, level, text, parent=None): + def __init__(self, level, text, replace, parent=None): super().__init__(text, parent) + self.replace = replace self.setAttribute(Qt.WA_StyledBackground, True) stylesheet = """ padding-top: 2px; @@ -81,7 +82,7 @@ class MessageView(QWidget): self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self._clear_timer = QTimer() - self._clear_timer.timeout.connect(self._clear_messages) + self._clear_timer.timeout.connect(self.clear_messages) self._set_clear_timer_interval() objreg.get('config').changed.connect(self._set_clear_timer_interval) @@ -100,7 +101,7 @@ class MessageView(QWidget): self._clear_timer.setInterval(config.get('ui', 'message-timeout')) @pyqtSlot() - def _clear_messages(self): + def clear_messages(self): """Hide and delete all messages.""" for widget in self._messages: self._vbox.removeWidget(widget) @@ -111,13 +112,17 @@ class MessageView(QWidget): self.hide() self._clear_timer.stop() - @pyqtSlot(usertypes.MessageLevel, str) - def show_message(self, level, text): + @pyqtSlot(usertypes.MessageLevel, str, bool) + def show_message(self, level, text, replace=False): """Show the given message with the given MessageLevel.""" if text == self._last_text: return - widget = Message(level, text, parent=self) + if replace and self._messages and self._messages[-1].replace: + old = self._messages.pop() + old.hide() + + widget = Message(level, text, replace=replace, parent=self) self._vbox.addWidget(widget) widget.show() self._clear_timer.start() diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index 6e2ab68d6..afe8a72dd 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -25,12 +25,12 @@ import collections import sip from PyQt5.QtCore import (pyqtSlot, pyqtSignal, Qt, QTimer, QDir, QModelIndex, - QItemSelectionModel, QObject) + QItemSelectionModel, QObject, QEventLoop) from PyQt5.QtWidgets import (QWidget, QGridLayout, QVBoxLayout, QLineEdit, QLabel, QFileSystemModel, QTreeView, QSizePolicy) from qutebrowser.browser import downloads -from qutebrowser.config import style +from qutebrowser.config import style, config from qutebrowser.utils import usertypes, log, utils, qtutils, objreg, message from qutebrowser.keyinput import modeman from qutebrowser.commands import cmdutils, cmdexc @@ -112,7 +112,7 @@ class PromptQueue(QObject): if not sip.isdeleted(question): # the question could already be deleted, e.g. by a cancelled # download. See - # https://github.com/The-Compiler/qutebrowser/issues/415 + # https://github.com/qutebrowser/qutebrowser/issues/415 self.ask_question(question, blocking=False) def shutdown(self): @@ -153,7 +153,7 @@ class PromptQueue(QObject): if self._shutting_down: # If we're currently shutting down we have to ignore this question # to avoid segfaults - see - # https://github.com/The-Compiler/qutebrowser/issues/95 + # https://github.com/qutebrowser/qutebrowser/issues/95 log.prompt.debug("Ignoring question because we're shutting down.") question.abort() return None @@ -184,7 +184,7 @@ class PromptQueue(QObject): question.completed.connect(loop.quit) question.completed.connect(loop.deleteLater) log.prompt.debug("Starting loop.exec_() for {}".format(question)) - loop.exec_() + loop.exec_(QEventLoop.ExcludeSocketNotifiers) log.prompt.debug("Ending loop.exec_() for {}".format(question)) log.prompt.debug("Restoring old question {}".format(old_question)) @@ -564,7 +564,9 @@ class FilenamePrompt(_BasePrompt): self.setFocusProxy(self._lineedit) self._init_key_label() - self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) + + if config.get('ui', 'prompt-filebrowser'): + self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) @pyqtSlot(str) def _set_fileview_root(self, path, *, tabbed=False): @@ -624,7 +626,12 @@ class FilenamePrompt(_BasePrompt): self._file_model = QFileSystemModel(self) self._file_view.setModel(self._file_model) self._file_view.clicked.connect(self._insert_path) - self._vbox.addWidget(self._file_view) + + if config.get('ui', 'prompt-filebrowser'): + self._vbox.addWidget(self._file_view) + else: + self._file_view.hide() + # Only show name self._file_view.setHeaderHidden(True) for col in range(1, 4): diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py index 21e0d46ea..eaf8e6ffc 100644 --- a/qutebrowser/mainwindow/statusbar/bar.py +++ b/qutebrowser/mainwindow/statusbar/bar.py @@ -46,6 +46,8 @@ class StatusBar(QWidget): _hbox: The main QHBoxLayout. _stack: The QStackedLayout with cmd/txt widgets. _win_id: The window ID the statusbar is associated with. + _page_fullscreen: Whether the webpage (e.g. a video) is shown + fullscreen. Class attributes: _prompt_active: If we're currently in prompt-mode. @@ -143,6 +145,7 @@ class StatusBar(QWidget): self._win_id = win_id self._option = None + self._page_fullscreen = False self._hbox = QHBoxLayout(self) self.set_hbox_padding() @@ -193,7 +196,7 @@ class StatusBar(QWidget): def maybe_hide(self): """Hide the statusbar if it's configured to do so.""" hide = config.get('ui', 'hide-statusbar') - if hide: + if hide or self._page_fullscreen: self.hide() else: self.show() @@ -306,6 +309,11 @@ class StatusBar(QWidget): usertypes.KeyMode.yesno]: self.set_mode_active(old_mode, False) + @pyqtSlot(bool) + def on_page_fullscreen_requested(self, on): + self._page_fullscreen = on + self.maybe_hide() + def resizeEvent(self, e): """Extend resizeEvent of QWidget to emit a resized signal afterwards. diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py index a02308a9f..116befac5 100644 --- a/qutebrowser/mainwindow/tabbedbrowser.py +++ b/qutebrowser/mainwindow/tabbedbrowser.py @@ -98,6 +98,7 @@ class TabbedBrowser(tabwidget.TabWidget): resized = pyqtSignal('QRect') current_tab_changed = pyqtSignal(browsertab.AbstractTab) new_tab = pyqtSignal(browsertab.AbstractTab, int) + page_fullscreen_requested = pyqtSignal(bool) def __init__(self, win_id, parent=None): super().__init__(win_id, parent) @@ -198,8 +199,13 @@ class TabbedBrowser(tabwidget.TabWidget): functools.partial(self.on_load_started, tab)) tab.window_close_requested.connect( functools.partial(self.on_window_close_requested, tab)) + tab.renderer_process_terminated.connect( + functools.partial(self._on_renderer_process_terminated, tab)) tab.new_tab_requested.connect(self.tabopen) tab.add_history_item.connect(objreg.get('web-history').add_from_tab) + tab.fullscreen_requested.connect(self.page_fullscreen_requested) + tab.fullscreen_requested.connect( + self.tabBar().on_page_fullscreen_requested) def current_url(self): """Get the URL of the current tab. @@ -245,12 +251,13 @@ class TabbedBrowser(tabwidget.TabWidget): url = config.get('general', 'default-page') self.openurl(url, newtab=True) - def _remove_tab(self, tab, *, add_undo=True): + def _remove_tab(self, tab, *, add_undo=True, crashed=False): """Remove a tab from the tab list and delete it properly. Args: tab: The QWebView to be closed. add_undo: Whether the tab close can be undone. + crashed: Whether we're closing a tab with crashed renderer process. """ idx = self.indexOf(tab) if idx == -1: @@ -262,25 +269,34 @@ class TabbedBrowser(tabwidget.TabWidget): window=self._win_id): objreg.delete('last-focused-tab', scope='window', window=self._win_id) - if tab.url().isValid(): - history_data = tab.history.serialize() - if add_undo: - entry = UndoEntry(tab.url(), history_data, idx) - self._undo_stack.append(entry) - elif tab.url().isEmpty(): + + if tab.url().isEmpty(): # There are some good reasons why a URL could be empty # (target="_blank" with a download, see [1]), so we silently ignore # this. - # [1] https://github.com/The-Compiler/qutebrowser/issues/163 + # [1] https://github.com/qutebrowser/qutebrowser/issues/163 pass - else: - # We display a warnings for URLs which are not empty but invalid - + elif not tab.url().isValid(): + # We display a warning for URLs which are not empty but invalid - # but we don't return here because we want the tab to close either # way. urlutils.invalid_url_error(tab.url(), "saving tab") + elif add_undo: + try: + history_data = tab.history.serialize() + except browsertab.WebTabError: + pass # special URL + else: + entry = UndoEntry(tab.url(), history_data, idx) + self._undo_stack.append(entry) + tab.shutdown() self.removeTab(idx) - tab.deleteLater() + if not crashed: + # WORKAROUND for a segfault when we delete the crashed tab. + # see https://bugreports.qt.io/browse/QTBUG-58698 + tab.layout().unwrap() + tab.deleteLater() def undo(self): """Undo removing of a tab.""" @@ -347,7 +363,8 @@ class TabbedBrowser(tabwidget.TabWidget): @pyqtSlot('QUrl') @pyqtSlot('QUrl', bool) - def tabopen(self, url=None, background=None, explicit=False, idx=None): + def tabopen(self, url=None, background=None, explicit=False, idx=None, *, + ignore_tabs_are_windows=False): """Open a new tab with a given URL. Inner logic for open-tab and open-tab-bg. @@ -364,6 +381,8 @@ class TabbedBrowser(tabwidget.TabWidget): the current. - Explicitly opened tabs are at the very right. idx: The index where the new tab should be opened. + ignore_tabs_are_windows: If given, never open a new window, even + with tabs-are-windows set. Return: The opened WebView instance. @@ -374,7 +393,8 @@ class TabbedBrowser(tabwidget.TabWidget): "explicit {}, idx {}".format( url, background, explicit, idx)) - if config.get('tabs', 'tabs-are-windows') and self.count() > 0: + if (config.get('tabs', 'tabs-are-windows') and self.count() > 0 and + not ignore_tabs_are_windows): from qutebrowser.mainwindow import mainwindow window = mainwindow.MainWindow() window.show() @@ -523,22 +543,6 @@ class TabbedBrowser(tabwidget.TabWidget): if not self.page_title(idx): self.set_page_title(idx, url.toDisplayString()) - # If needed, re-open the tab as a workaround for QTBUG-54419. - # See https://bugreports.qt.io/browse/QTBUG-54419 - background = self.currentIndex() != idx - - if (tab.backend == usertypes.Backend.QtWebEngine and - tab.needs_qtbug54419_workaround): - log.misc.debug("Doing QTBUG-54419 workaround for {}, " - "url {}".format(tab, url)) - self.setUpdatesEnabled(False) - try: - self.tabopen(url, background=background, idx=idx) - self.close_tab(tab, add_undo=False) - finally: - self.setUpdatesEnabled(True) - tab.needs_qtbug54419_workaround = False - @pyqtSlot(browsertab.AbstractTab, QIcon) def on_icon_changed(self, tab, icon): """Set the icon of a tab. @@ -650,6 +654,28 @@ class TabbedBrowser(tabwidget.TabWidget): self.update_window_title() self.update_tab_title(idx) + def _on_renderer_process_terminated(self, tab, status, code): + """Show an error when a renderer process terminated.""" + if status == browsertab.TerminationStatus.normal: + pass + elif status == browsertab.TerminationStatus.abnormal: + message.error("Renderer process exited with status {}".format( + code)) + elif status == browsertab.TerminationStatus.crashed: + message.error("Renderer process crashed") + elif status == browsertab.TerminationStatus.killed: + message.error("Renderer process was killed") + elif status == browsertab.TerminationStatus.unknown: + message.error("Renderer process did not start") + else: + raise ValueError("Invalid status {}".format(status)) + + # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-58698 + # FIXME:qtwebengine can we disable this with Qt 5.8.1? + self._remove_tab(tab, crashed=True) + if self.count() == 0: + self.tabopen(QUrl('about:blank')) + def resizeEvent(self, e): """Extend resizeEvent of QWidget to emit a resized signal afterwards. diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py index 29daca354..d2da192c9 100644 --- a/qutebrowser/mainwindow/tabwidget.py +++ b/qutebrowser/mainwindow/tabwidget.py @@ -27,8 +27,9 @@ from PyQt5.QtWidgets import (QTabWidget, QTabBar, QSizePolicy, QCommonStyle, QStyle, QStylePainter, QStyleOptionTab) from PyQt5.QtGui import QIcon, QPalette, QColor -from qutebrowser.utils import qtutils, objreg, utils, usertypes +from qutebrowser.utils import qtutils, objreg, utils, usertypes, log from qutebrowser.config import config +from qutebrowser.misc import objects PixelMetrics = usertypes.enum('PixelMetrics', ['icon_padding'], @@ -149,7 +150,7 @@ class TabWidget(QTabWidget): fields['title'] = page_title fields['title_sep'] = ' - ' if page_title else '' fields['perc_raw'] = tab.progress() - fields['backend'] = objreg.get('args').backend + fields['backend'] = objects.backend.name if tab.load_status() == usertypes.LoadStatus.loading: fields['perc'] = '[{}%] '.format(tab.progress()) @@ -284,11 +285,13 @@ class TabBar(QTabBar): fixing this would be a lot of effort, so we'll postpone it until we're reimplementing drag&drop for other reasons. - https://github.com/The-Compiler/qutebrowser/issues/126 + https://github.com/qutebrowser/qutebrowser/issues/126 Attributes: vertical: When the tab bar is currently vertical. win_id: The window ID this TabBar belongs to. + _page_fullscreen: Whether the webpage (e.g. a video) is shown + fullscreen. """ def __init__(self, win_id, parent=None): @@ -299,6 +302,7 @@ class TabBar(QTabBar): config_obj = objreg.get('config') config_obj.changed.connect(self.set_font) self.vertical = False + self._page_fullscreen = False self._auto_hide_timer = QTimer() self._auto_hide_timer.setSingleShot(True) self._auto_hide_timer.setInterval( @@ -327,20 +331,24 @@ class TabBar(QTabBar): self._auto_hide_timer.setInterval( config.get('tabs', 'show-switching-delay')) + @pyqtSlot(bool) + def on_page_fullscreen_requested(self, on): + self._page_fullscreen = on + self._tabhide() + def on_change(self): """Show tab bar when current tab got changed.""" show = config.get('tabs', 'show') - if show == 'switching': + if show == 'switching' or self._page_fullscreen: self.show() self._auto_hide_timer.start() def _tabhide(self): """Hide the tab bar if needed.""" show = config.get('tabs', 'show') - show_never = show == 'never' - switching = show == 'switching' - multiple = show == 'multiple' - if show_never or (multiple and self.count() == 1) or switching: + if (show in ['never', 'switching'] or + (show == 'multiple' and self.count() == 1) or + self._page_fullscreen): self.hide() else: self.show() @@ -660,7 +668,17 @@ class TabBarStyle(QCommonStyle): p: QPainter widget: QWidget """ + if element not in [QStyle.CE_TabBarTab, QStyle.CE_TabBarTabShape, + QStyle.CE_TabBarTabLabel]: + # Let the real style draw it. + self._style.drawControl(element, opt, p, widget) + return + layouts = self._tab_layout(opt) + if layouts is None: + log.misc.warning("Could not get layouts for tab!") + return + if element == QStyle.CE_TabBarTab: # We override this so we can control TabBarTabShape/TabBarTabLabel. self.drawControl(QStyle.CE_TabBarTabShape, opt, p, widget) @@ -680,9 +698,7 @@ class TabBarStyle(QCommonStyle): opt.state & QStyle.State_Enabled, opt.text, QPalette.WindowText) else: - # For any other elements we just delegate the work to our real - # style. - self._style.drawControl(element, opt, p, widget) + raise ValueError("Invalid element {!r}".format(element)) def pixelMetric(self, metric, option=None, widget=None): """Override pixelMetric to not shift the selected tab. @@ -719,6 +735,9 @@ class TabBarStyle(QCommonStyle): """ if sr == QStyle.SE_TabBarTabText: layouts = self._tab_layout(opt) + if layouts is None: + log.misc.warning("Could not get layouts for tab!") + return QRect() return layouts.text elif sr == QStyle.SE_TabWidgetTabBar: # Need to use super() because we also use super() to render @@ -746,8 +765,11 @@ class TabBarStyle(QCommonStyle): indicator_padding = config.get('tabs', 'indicator-padding') text_rect = QRect(opt.rect) + if not text_rect.isValid(): + # This happens sometimes according to crash reports, but no idea + # why... + return None - qtutils.ensure_valid(text_rect) text_rect.adjust(padding.left, padding.top, -padding.right, -padding.bottom) diff --git a/qutebrowser/misc/crashdialog.py b/qutebrowser/misc/crashdialog.py index 397d2ed9d..0add7932a 100644 --- a/qutebrowser/misc/crashdialog.py +++ b/qutebrowser/misc/crashdialog.py @@ -35,9 +35,9 @@ from PyQt5.QtWidgets import (QDialog, QLabel, QTextEdit, QPushButton, QDialogButtonBox, QMessageBox, QApplication) import qutebrowser -from qutebrowser.utils import version, log, utils, objreg +from qutebrowser.utils import version, log, utils, objreg, usertypes from qutebrowser.misc import (miscwidgets, autoupdate, msgbox, httpclient, - pastebin) + pastebin, objects) from qutebrowser.config import config @@ -82,7 +82,9 @@ def get_fatal_crash_dialog(debug, data): ignored_frames = ['qt_mainloop', 'paintEvent'] errtype, frame = parse_fatal_stacktrace(data) - if errtype == 'Segmentation fault' and frame in ignored_frames: + if (errtype == 'Segmentation fault' and + frame in ignored_frames and + objects.backend == usertypes.Backend.QtWebKit): title = "qutebrowser was restarted after a fatal crash!" text = ("qutebrowser was restarted after a fatal crash!
" "Unfortunately, this crash occurred in Qt (the library " @@ -403,17 +405,17 @@ class ExceptionCrashDialog(_CrashDialog): _pages: A list of lists of the open pages (URLs as strings) _cmdhist: A list with the command history (as strings) _exc: An exception tuple (type, value, traceback) - _objects: A list of all QObjects as string. + _qobjects: A list of all QObjects as string. """ - def __init__(self, debug, pages, cmdhist, exc, objects, parent=None): + def __init__(self, debug, pages, cmdhist, exc, qobjects, parent=None): self._chk_log = None self._chk_restore = None super().__init__(debug, parent) self._pages = pages self._cmdhist = cmdhist self._exc = exc - self._objects = objects + self._qobjects = qobjects self.setModal(True) self._set_crash_info() @@ -460,7 +462,7 @@ class ExceptionCrashDialog(_CrashDialog): ("Commandline args", ' '.join(sys.argv[1:])), ("Open Pages", '\n\n'.join('\n'.join(e) for e in self._pages)), ("Command history", '\n'.join(self._cmdhist)), - ("Objects", self._objects), + ("Objects", self._qobjects), ] try: self._crash_info.append( @@ -548,15 +550,15 @@ class ReportDialog(_CrashDialog): Attributes: _pages: A list of the open pages (URLs as strings) _cmdhist: A list with the command history (as strings) - _objects: A list of all QObjects as string. + _qobjects: A list of all QObjects as string. """ - def __init__(self, pages, cmdhist, objects, parent=None): + def __init__(self, pages, cmdhist, qobjects, parent=None): super().__init__(False, parent) self.setAttribute(Qt.WA_DeleteOnClose) self._pages = pages self._cmdhist = cmdhist - self._objects = objects + self._qobjects = qobjects self._set_crash_info() def _init_text(self): @@ -577,7 +579,7 @@ class ReportDialog(_CrashDialog): ("Commandline args", ' '.join(sys.argv[1:])), ("Open Pages", '\n\n'.join('\n'.join(e) for e in self._pages)), ("Command history", '\n'.join(self._cmdhist)), - ("Objects", self._objects), + ("Objects", self._qobjects), ] try: self._crash_info.append(("Debug log", log.ram_handler.dump_log())) @@ -613,14 +615,14 @@ class ReportErrorDialog(QDialog): vbox.addLayout(hbox) -def dump_exception_info(exc, pages, cmdhist, objects): +def dump_exception_info(exc, pages, cmdhist, qobjects): """Dump exception info to stderr. Args: exc: An exception tuple (type, value, traceback) pages: A list of lists of the open pages (URLs as strings) cmdhist: A list with the command history (as strings) - objects: A list of all QObjects as string. + qobjects: A list of all QObjects as string. """ print(file=sys.stderr) print("\n\n===== Handling exception with --no-err-windows... =====\n\n", @@ -645,7 +647,7 @@ def dump_exception_info(exc, pages, cmdhist, objects): print("\n---- Command history ----", file=sys.stderr) print('\n'.join(cmdhist), file=sys.stderr) print("\n---- Objects ----", file=sys.stderr) - print(objects, file=sys.stderr) + print(qobjects, file=sys.stderr) print("\n---- Environment ----", file=sys.stderr) try: print(_get_environment_vars(), file=sys.stderr) diff --git a/qutebrowser/misc/earlyinit.py b/qutebrowser/misc/earlyinit.py index 08c933a6e..3622dc36a 100644 --- a/qutebrowser/misc/earlyinit.py +++ b/qutebrowser/misc/earlyinit.py @@ -34,7 +34,6 @@ import sys import faulthandler import traceback import signal -import operator import importlib import pkg_resources import datetime @@ -59,17 +58,18 @@ def _missing_str(name, *, windows=None, pip=None, webengine=False): webengine: Whether this is checking the QtWebEngine package """ blocks = ["Fatal error: {} is required to run qutebrowser but " - "could not be imported! Maybe it's not installed?".format(name)] + "could not be imported! Maybe it's not installed?".format(name), + "The error encountered was:
%ERROR%"] lines = ['Please search for the python3 version of {} in your ' 'distributions packages, or install it via pip.'.format(name)] blocks.append('
'.join(lines)) if webengine: lines = [ ('Note QtWebEngine is not available for some distributions ' - '(like Debian/Ubuntu), so you need to start without ' - '--backend webengine there.'), + '(like Ubuntu), so you need to start without --backend ' + 'webengine there.'), ('QtWebEngine is currently unsupported with the OS X .app, see ' - 'https://github.com/The-Compiler/qutebrowser/issues/1692'), + 'https://github.com/qutebrowser/qutebrowser/issues/1692'), ] else: lines = ['If you installed a qutebrowser package for your ' @@ -107,7 +107,7 @@ def _die(message, exception=None): print("Exiting because of --no-err-windows.", file=sys.stderr) else: if exception is not None: - message += '


Error:
{}'.format(exception) + message = message.replace('%ERROR%', str(exception)) msgbox = QMessageBox(QMessageBox.Critical, "qutebrowser: Fatal error!", message) msgbox.setTextFormat(Qt.RichText) @@ -226,7 +226,7 @@ def check_pyqt_core(): text = text.replace('', '') text = text.replace('', '') text = text.replace('
', '\n') - text += '\n\nError: {}'.format(e) + text = text.replace('%ERROR%', str(e)) if tkinter and '--no-err-windows' not in sys.argv: root = tkinter.Tk() root.withdraw() @@ -239,18 +239,42 @@ def check_pyqt_core(): sys.exit(1) -def check_qt_version(args): +def get_backend(args): + """Find out what backend to use based on available libraries. + + Note this function returns the backend as a string so we don't have to + import qutebrowser.utils.usertypes yet. + """ + try: + import PyQt5.QtWebKit # pylint: disable=unused-variable + webkit_available = True + except ImportError: + webkit_available = False + + if args.backend is not None: + return args.backend + elif webkit_available: + return 'webkit' + else: + return 'webengine' + + +def check_qt_version(backend): """Check if the Qt version is recent enough.""" - from PyQt5.QtCore import qVersion - from qutebrowser.utils import qtutils - if qtutils.version_check('5.2.0', operator.lt): - text = ("Fatal error: Qt and PyQt >= 5.2.0 are required, but {} is " - "installed.".format(qVersion())) + from PyQt5.QtCore import PYQT_VERSION, PYQT_VERSION_STR + from qutebrowser.utils import qtutils, version + if (not qtutils.version_check('5.2.0', strict=True) or + PYQT_VERSION < 0x050200): + text = ("Fatal error: Qt and PyQt >= 5.2.0 are required, but Qt {} / " + "PyQt {} is installed.".format(version.qt_version(), + PYQT_VERSION_STR)) _die(text) - elif args.backend == 'webengine' and qtutils.version_check('5.6.0', - operator.lt): - text = ("Fatal error: Qt and PyQt >= 5.6.0 are required for " - "QtWebEngine support, but {} is installed.".format(qVersion())) + elif (backend == 'webengine' and ( + not qtutils.version_check('5.7.1', strict=True) or + PYQT_VERSION < 0x050700)): + text = ("Fatal error: Qt >= 5.7.1 and PyQt >= 5.7 are required for " + "QtWebEngine support, but Qt {} / PyQt {} is installed." + .format(version.qt_version(), PYQT_VERSION_STR)) _die(text) @@ -267,7 +291,7 @@ def check_ssl_support(): _die(text) -def check_libraries(args): +def check_libraries(backend): """Check if all needed Python libraries are installed.""" modules = { 'pkg_resources': @@ -293,15 +317,29 @@ def check_libraries(args): "or Install via pip.", pip="PyYAML"), } - if args.backend == 'webengine': + if backend == 'webengine': modules['PyQt5.QtWebEngineWidgets'] = _missing_str("QtWebEngine", webengine=True) else: + assert backend == 'webkit' modules['PyQt5.QtWebKit'] = _missing_str("PyQt5.QtWebKit") + modules['PyQt5.QtWebKitWidgets'] = _missing_str( + "PyQt5.QtWebKitWidgets") + + from qutebrowser.utils import log for name, text in modules.items(): try: - importlib.import_module(name) + # https://github.com/pallets/jinja/pull/628 + # https://bitbucket.org/birkenfeld/pygments-main/issues/1314/ + # https://github.com/pallets/jinja/issues/646 + # https://bitbucket.org/fdik/pypeg/commits/dd15ca462b532019c0a3be1d39b8ee2f3fa32f4e + messages = ['invalid escape sequence', + 'Flags not at the start of the expression'] + with log.ignore_py_warnings( + category=DeprecationWarning, + message=r'({})'.format('|'.join(messages))): + importlib.import_module(name) except ImportError as e: _die(text, e) @@ -334,6 +372,17 @@ def check_optimize_flag(): "unexpected behavior may occur.") +def set_backend(backend): + """Set the objects.backend global to the given backend (as string).""" + from qutebrowser.misc import objects + from qutebrowser.utils import usertypes + backends = { + 'webkit': usertypes.Backend.QtWebKit, + 'webengine': usertypes.Backend.QtWebEngine, + } + objects.backend = backends[backend] + + def earlyinit(args): """Do all needed early initialization. @@ -356,8 +405,10 @@ def earlyinit(args): fix_harfbuzz(args) # Now we can be sure QtCore is available, so we can print dialogs on # errors, so people only using the GUI notice them as well. - check_qt_version(args) + backend = get_backend(args) + check_qt_version(backend) remove_inputhook() - check_libraries(args) + check_libraries(backend) check_ssl_support() check_optimize_flag() + set_backend(backend) diff --git a/qutebrowser/misc/editor.py b/qutebrowser/misc/editor.py index 1705df09c..a6f2854d8 100644 --- a/qutebrowser/misc/editor.py +++ b/qutebrowser/misc/editor.py @@ -107,7 +107,7 @@ class ExternalEditor(QObject): # Close while the external process is running, as otherwise systems # with exclusive write access (e.g. Windows) may fail to update # the file from the external editor, see - # https://github.com/The-Compiler/qutebrowser/issues/1767 + # https://github.com/qutebrowser/qutebrowser/issues/1767 with tempfile.NamedTemporaryFile( mode='w', prefix='qutebrowser-editor-', encoding=encoding, delete=False) as fobj: diff --git a/qutebrowser/misc/ipc.py b/qutebrowser/misc/ipc.py index 6641a9ee8..58bea9ca7 100644 --- a/qutebrowser/misc/ipc.py +++ b/qutebrowser/misc/ipc.py @@ -27,7 +27,7 @@ import getpass import binascii import hashlib -from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, Qt +from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, Qt, QTimer from PyQt5.QtNetwork import QLocalSocket, QLocalServer, QAbstractSocket import qutebrowser @@ -37,7 +37,7 @@ from qutebrowser.utils import log, usertypes, error, objreg, standarddir CONNECT_TIMEOUT = 100 # timeout for connecting/disconnecting WRITE_TIMEOUT = 1000 READ_TIMEOUT = 5000 -ATIME_INTERVAL = 60 * 60 * 6 * 1000 # 6 hours +ATIME_INTERVAL = 60 * 60 * 3 * 1000 # 3 hours PROTOCOL_VERSION = 1 @@ -222,7 +222,7 @@ class IPCServer(QObject): try: os.chmod(self._server.fullServerName(), 0o700) except FileNotFoundError: - # https://github.com/The-Compiler/qutebrowser/issues/1530 + # https://github.com/qutebrowser/qutebrowser/issues/1530 # The server doesn't actually exist even if ok was reported as # True, so report this as an error. raise ListenError(self._server) @@ -281,7 +281,11 @@ class IPCServer(QObject): if self._socket is None: log.ipc.debug("In on_disconnected with None socket!") else: - self._socket.deleteLater() + # For some reason Qt can still get delayed canReadNotifications + # internally, so if we call deleteLater() right away and then call + # QApplication::processEvents() somewhere in the code, we can get a + # segfault. + QTimer.singleShot(500, self._socket.deleteLater) self._socket = None # Maybe another connection is waiting. self.handle_connection() @@ -363,7 +367,7 @@ class IPCServer(QObject): def on_timeout(self): """Cancel the current connection if it was idle for too long.""" if self._socket is None: # pragma: no cover - log.ipc.error("on_timeout got called with None socket!") + log.ipc.debug("on_timeout got called with None socket!") return log.ipc.error("IPC connection timed out " "(socket 0x{:x}).".format(id(self._socket))) diff --git a/qutebrowser/misc/miscwidgets.py b/qutebrowser/misc/miscwidgets.py index ba21e541a..0337269ea 100644 --- a/qutebrowser/misc/miscwidgets.py +++ b/qutebrowser/misc/miscwidgets.py @@ -19,13 +19,13 @@ """Misc. widgets used at different places.""" -from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QSize +from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QSize, QTimer from PyQt5.QtWidgets import (QLineEdit, QWidget, QHBoxLayout, QLabel, - QStyleOption, QStyle, QLayout) + QStyleOption, QStyle, QLayout, QApplication) from PyQt5.QtGui import QValidator, QPainter -from qutebrowser.utils import utils -from qutebrowser.misc import cmdhistory +from qutebrowser.utils import utils, objreg, qtutils, log, usertypes +from qutebrowser.misc import cmdhistory, objects class MinimalLineEditMixin: @@ -260,3 +260,56 @@ class WrapperLayout(QLayout): self._widget = widget container.setFocusProxy(widget) widget.setParent(container) + if (qtutils.version_check('5.8.0', exact=True) and + objects.backend == usertypes.Backend.QtWebEngine and + container.window() and + container.window().windowHandle() and + not container.window().windowHandle().isActive()): + log.misc.debug("Calling QApplication::sync...") + # WORKAROUND for: + # https://bugreports.qt.io/browse/QTBUG-56652 + # https://codereview.qt-project.org/#/c/176113/5//ALL,unified + QApplication.sync() + + def unwrap(self): + self._widget.setParent(None) + self._widget.deleteLater() + + +class FullscreenNotification(QLabel): + + """A label telling the user this page is now fullscreen.""" + + def __init__(self, parent=None): + super().__init__(parent) + self.setStyleSheet(""" + background-color: rgba(50, 50, 50, 80%); + color: white; + border-radius: 20px; + padding: 30px; + """) + + key_config = objreg.get('key-config') + all_bindings = key_config.get_reverse_bindings_for('normal') + bindings = all_bindings.get('fullscreen --leave') + if bindings: + key = bindings[0] + if utils.is_special_key(key): + key = key.strip('<>').capitalize() + self.setText("Press {} to exit fullscreen.".format(key)) + else: + self.setText("Page is now fullscreen.") + + self.resize(self.sizeHint()) + geom = QApplication.desktop().screenGeometry(self) + self.move((geom.width() - self.sizeHint().width()) / 2, 30) + + def set_timeout(self, timeout): + """Hide the widget after the given timeout.""" + QTimer.singleShot(timeout, self._on_timeout) + + @pyqtSlot() + def _on_timeout(self): + """Hide and delete the widget.""" + self.hide() + self.deleteLater() diff --git a/qutebrowser/misc/objects.py b/qutebrowser/misc/objects.py new file mode 100644 index 000000000..9af210498 --- /dev/null +++ b/qutebrowser/misc/objects.py @@ -0,0 +1,26 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2017 Florian Bruhin (The Compiler) +# +# 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 . + +"""Various global objects.""" + +# NOTE: We need to be careful with imports here, as this is imported from +# earlyinit. + +# A usertypes.Backend member +backend = None diff --git a/qutebrowser/misc/sessions.py b/qutebrowser/misc/sessions.py index cdda5739c..da7b9bf27 100644 --- a/qutebrowser/misc/sessions.py +++ b/qutebrowser/misc/sessions.py @@ -34,7 +34,6 @@ except ImportError: # pragma: no cover from qutebrowser.utils import (standarddir, objreg, qtutils, log, usertypes, message, utils) from qutebrowser.commands import cmdexc, cmdutils -from qutebrowser.mainwindow import mainwindow from qutebrowser.config import config @@ -167,7 +166,7 @@ class SessionManager(QObject): if item.title(): data['title'] = item.title() else: - # https://github.com/The-Compiler/qutebrowser/issues/879 + # https://github.com/qutebrowser/qutebrowser/issues/879 if tab.history.current_idx() == idx: data['title'] = tab.title() else: @@ -217,10 +216,15 @@ class SessionManager(QObject): data['history'].append(item_data) return data - def _save_all(self): + def _save_all(self, *, only_window=None): """Get a dict with data for all windows/tabs.""" data = {'windows': []} - for win_id in objreg.window_registry: + if only_window is not None: + winlist = [only_window] + else: + winlist = objreg.window_registry + + for win_id in winlist: tabbed_browser = objreg.get('tabbed-browser', scope='window', window=win_id) main_window = objreg.get('main-window', scope='window', @@ -258,7 +262,8 @@ class SessionManager(QObject): name = 'default' return name - def save(self, name, last_window=False, load_next_time=False): + def save(self, name, last_window=False, load_next_time=False, + only_window=None): """Save a named session. Args: @@ -267,6 +272,7 @@ class SessionManager(QObject): last_window: If set, saves the saved self._last_window_session instead of the currently open state. load_next_time: If set, prepares this session to be load next time. + only_window: If set, only tabs in the specified window is saved. Return: The name of the saved session. @@ -281,7 +287,7 @@ class SessionManager(QObject): log.sessions.error("last_window_session is None while saving!") return else: - data = self._save_all() + data = self._save_all(only_window=only_window) log.sessions.vdebug("Saving data: {}".format(data)) try: with qtutils.savefile_open(path) as f: @@ -296,6 +302,24 @@ class SessionManager(QObject): state_config['general']['session'] = name return name + def save_autosave(self): + """Save the autosave session.""" + try: + self.save('_autosave') + except SessionError as e: + log.sessions.error("Failed to save autosave session: {}".format(e)) + + def delete_autosave(self): + """Delete the autosave session.""" + try: + self.delete('_autosave') + except SessionNotFoundError: + # Exiting before the first load finished + pass + except SessionError as e: + log.sessions.error("Failed to delete autosave session: {}" + .format(e)) + def save_last_window_session(self): """Temporarily save the session for the last closed window.""" self._last_window_session = self._save_all() @@ -309,7 +333,7 @@ class SessionManager(QObject): if 'zoom' in data: # The zoom was accidentally stored in 'data' instead of per-tab # earlier. - # See https://github.com/The-Compiler/qutebrowser/issues/728 + # See https://github.com/qutebrowser/qutebrowser/issues/728 user_data['zoom'] = data['zoom'] elif 'zoom' in histentry: user_data['zoom'] = histentry['zoom'] @@ -317,7 +341,7 @@ class SessionManager(QObject): if 'scroll-pos' in data: # The scroll position was accidentally stored in 'data' instead # of per-tab earlier. - # See https://github.com/The-Compiler/qutebrowser/issues/728 + # See https://github.com/qutebrowser/qutebrowser/issues/728 pos = data['scroll-pos'] user_data['scroll-pos'] = QPoint(pos['x'], pos['y']) elif 'scroll-pos' in histentry: @@ -352,6 +376,7 @@ class SessionManager(QObject): name: The name of the session to load. temp: If given, don't set the current session. """ + from qutebrowser.mainwindow import mainwindow path = self._get_session_path(name, check_exists=True) try: with open(path, encoding='utf-8') as f: @@ -425,8 +450,9 @@ class SessionManager(QObject): @cmdutils.register(name=['session-save', 'w'], instance='session-manager') @cmdutils.argument('name', completion=usertypes.Completion.sessions) + @cmdutils.argument('win_id', win_id=True) def session_save(self, name: str=default, current=False, quiet=False, - force=False): + force=False, only_active_window=False, win_id=None): """Save a session. Args: @@ -435,6 +461,7 @@ class SessionManager(QObject): current: Save the current session instead of the default. quiet: Don't show confirmation message. force: Force saving internal sessions (starting with an underline). + only_active_window: Saves only tabs of the currently active window. """ if (name is not default and name.startswith('_') and # pylint: disable=no-member @@ -447,7 +474,10 @@ class SessionManager(QObject): name = self._current assert not name.startswith('_') try: - name = self.save(name) + if only_active_window: + name = self.save(name, only_window=win_id) + else: + name = self.save(name) except SessionError as e: raise cmdexc.CommandError("Error while saving session: {}" .format(e)) diff --git a/qutebrowser/misc/utilcmds.py b/qutebrowser/misc/utilcmds.py index affcb6f1d..e617c1af2 100644 --- a/qutebrowser/misc/utilcmds.py +++ b/qutebrowser/misc/utilcmds.py @@ -115,13 +115,16 @@ def message_error(text): @cmdutils.register(hide=True) -def message_info(text): +@cmdutils.argument('count', count=True) +def message_info(text, count=1): """Show an info message in the statusbar. Args: text: The text to show. + count: How many times to show the message """ - message.info(text) + for _ in range(count): + message.info(text) @cmdutils.register(hide=True) @@ -134,6 +137,12 @@ def message_warning(text): message.warning(text) +@cmdutils.register(hide=True) +def clear_messages(): + """Clear all message notifications.""" + message.global_bridge.clear_messages.emit() + + @cmdutils.register(debug=True) @cmdutils.argument('typ', choices=['exception', 'segfault']) def debug_crash(typ='exception'): diff --git a/qutebrowser/qutebrowser.py b/qutebrowser/qutebrowser.py index 8665d4592..8321fb04b 100644 --- a/qutebrowser/qutebrowser.py +++ b/qutebrowser/qutebrowser.py @@ -65,7 +65,13 @@ def get_argparser(): "qutebrowser instance running.") parser.add_argument('--backend', choices=['webkit', 'webengine'], help="Which backend to use (webengine backend is " - "EXPERIMENTAL!).", default='webkit') + "EXPERIMENTAL!).") + parser.add_argument('--enable-webengine-inspector', action='store_true', + help="Enable the web inspector for QtWebEngine. Note " + "that this is a SECURITY RISK and you should not " + "visit untrusted websites with the inspector turned " + "on. See https://bugreports.qt.io/browse/QTBUG-50725 " + "for more details.") parser.add_argument('--json-args', help=argparse.SUPPRESS) parser.add_argument('--temp-basedir-restarted', help=argparse.SUPPRESS) @@ -108,9 +114,10 @@ def get_argparser(): debug.add_argument('--qt-arg', help="Pass an argument with a value to Qt. " "For example, you can do " "`--qt-arg geometry 650x555+200+300` to set the window " - "geometry.", nargs=2, metavar=('NAME', 'VALUE')) + "geometry.", nargs=2, metavar=('NAME', 'VALUE'), + action='append') debug.add_argument('--qt-flag', help="Pass an argument to Qt as flag.", - nargs=1) + nargs=1, action='append') parser.add_argument('command', nargs='*', help="Commands to execute on " "startup.", metavar=':command') # URLs will actually be in command diff --git a/qutebrowser/utils/debug.py b/qutebrowser/utils/debug.py index 194540f34..89ae62faf 100644 --- a/qutebrowser/utils/debug.py +++ b/qutebrowser/utils/debug.py @@ -70,7 +70,11 @@ def log_signals(obj): name = bytes(meta_method.name()).decode('ascii') if name != 'destroyed': signal = getattr(obj, name) - signal.connect(functools.partial(log_slot, obj, signal)) + try: + signal.connect(functools.partial( + log_slot, obj, signal)) + except TypeError: # pragma: no cover + pass if inspect.isclass(obj): old_init = obj.__init__ @@ -133,7 +137,7 @@ def qflags_key(base, value, add_base=False, klass=None): Note: Passing a combined value (such as Qt.AlignCenter) will get the names for the individual bits (e.g. Qt.AlignVCenter | Qt.AlignHCenter). FIXME - https://github.com/The-Compiler/qutebrowser/issues/42 + https://github.com/qutebrowser/qutebrowser/issues/42 Args: base: The object the flags are in, e.g. QtCore.Qt diff --git a/qutebrowser/utils/jinja.py b/qutebrowser/utils/jinja.py index e60032ff4..2ad2be448 100644 --- a/qutebrowser/utils/jinja.py +++ b/qutebrowser/utils/jinja.py @@ -23,6 +23,7 @@ import os import os.path import traceback import mimetypes +import html import jinja2 import jinja2.exceptions @@ -31,6 +32,24 @@ from qutebrowser.utils import utils, urlutils, log from PyQt5.QtCore import QUrl +html_fallback = """ + + + + + Error while loading template + + +

+ The %FILE% template could not be found!
+ Please check your qutebrowser installation +

+ %ERROR% +

+ + +""" + class Loader(jinja2.BaseLoader): @@ -47,8 +66,11 @@ class Loader(jinja2.BaseLoader): path = os.path.join(self._subdir, template) try: source = utils.read_file(path) - except OSError: - raise jinja2.TemplateNotFound(template) + except OSError as e: + source = html_fallback.replace("%ERROR%", html.escape(str(e))) + source = source.replace("%FILE%", html.escape(template)) + log.misc.exception("The {} template could not be loaded from {}" + .format(template, path)) # Currently we don't implement auto-reloading, so we always return True # for up-to-date. return source, path, lambda: True diff --git a/qutebrowser/utils/log.py b/qutebrowser/utils/log.py index 1a3db19be..7c96d4072 100644 --- a/qutebrowser/utils/log.py +++ b/qutebrowser/utils/log.py @@ -94,7 +94,7 @@ LOGGER_NAMES = [ 'commands', 'signals', 'downloads', 'js', 'qt', 'rfc6266', 'ipc', 'shlexer', 'save', 'message', 'config', 'sessions', - 'webelem', 'prompt' + 'webelem', 'prompt', 'network' ] @@ -140,6 +140,7 @@ config = logging.getLogger('config') sessions = logging.getLogger('sessions') webelem = logging.getLogger('webelem') prompt = logging.getLogger('prompt') +network = logging.getLogger('network') ram_handler = None @@ -400,16 +401,18 @@ def qt_message_handler(msg_type, context, msg): "setting the environment variable QTWEBKIT_IMAGEFORMAT_WHITELIST=", # Installing Qt from the installer may cause it looking for SSL3 which # may not be available on the system + "QSslSocket: cannot resolve SSLv2_client_method", + "QSslSocket: cannot resolve SSLv2_server_method", "QSslSocket: cannot resolve SSLv3_client_method", "QSslSocket: cannot resolve SSLv3_server_method", # When enabling debugging with QtWebEngine "Remote debugging server started successfully. Try pointing a " "Chromium-based browser to ", - # https://github.com/The-Compiler/qutebrowser/issues/1287 + # https://github.com/qutebrowser/qutebrowser/issues/1287 "QXcbClipboard: SelectionRequest too old", - # https://github.com/The-Compiler/qutebrowser/issues/2071 + # https://github.com/qutebrowser/qutebrowser/issues/2071 'QXcbWindow: Unhandled client message: ""', - # No idea where this comes from... + # https://codereview.qt-project.org/176831 "QObject::disconnect: Unexpected null parameter", ] if sys.platform == 'darwin': @@ -550,7 +553,7 @@ class RAMHandler(logging.Handler): FIXME: We should do all the HTML formatter via jinja2. (probably obsolete when moving to a widget for logging, - https://github.com/The-Compiler/qutebrowser/issues/34 + https://github.com/qutebrowser/qutebrowser/issues/34 """ minlevel = LOG_LEVELS.get(level.upper(), VDEBUG_LEVEL) lines = [] diff --git a/qutebrowser/utils/message.py b/qutebrowser/utils/message.py index 368bb8289..bb758d78a 100644 --- a/qutebrowser/utils/message.py +++ b/qutebrowser/utils/message.py @@ -46,12 +46,13 @@ def _log_stack(typ, stack): log.message.debug("Stack for {} message:\n{}".format(typ, stack_text)) -def error(message, *, stack=None): +def error(message, *, stack=None, replace=False): """Convenience function to display an error message in the statusbar. Args: message: The message to show stack: The stack trace to show. + replace: Replace existing messages with replace=True """ if stack is None: stack = traceback.format_stack() @@ -60,20 +61,30 @@ def error(message, *, stack=None): typ = 'error (from exception)' _log_stack(typ, stack) log.message.error(message) - global_bridge.show_message.emit(usertypes.MessageLevel.error, message) + global_bridge.show(usertypes.MessageLevel.error, message, replace) -def warning(message): - """Convenience function to display a warning message in the statusbar.""" +def warning(message, *, replace=False): + """Convenience function to display a warning message in the statusbar. + + Args: + message: The message to show + replace: Replace existing messages with replace=True + """ _log_stack('warning', traceback.format_stack()) log.message.warning(message) - global_bridge.show_message.emit(usertypes.MessageLevel.warning, message) + global_bridge.show(usertypes.MessageLevel.warning, message, replace) -def info(message): - """Convenience function to display an info message in the statusbar.""" +def info(message, *, replace=False): + """Convenience function to display an info message in the statusbar. + + Args: + message: The message to show + replace: Replace existing messages with replace=True + """ log.message.info(message) - global_bridge.show_message.emit(usertypes.MessageLevel.info, message) + global_bridge.show(usertypes.MessageLevel.info, message, replace) def _build_question(title, text=None, *, mode, default=None, abort_on=()): @@ -159,10 +170,16 @@ class GlobalMessageBridge(QObject): """Global (not per-window) message bridge for errors/infos/warnings. + Attributes: + _connected: Whether a slot is connected and we can show messages. + _cache: Messages shown while we were not connected. + Signals: show_message: Show a message arg 0: A MessageLevel member arg 1: The text to show + arg 2: Whether to replace other messages with + replace=True. prompt_done: Emitted when a prompt was answered somewhere. ask_question: Ask a question to the user. arg 0: The Question object to ask. @@ -173,10 +190,16 @@ class GlobalMessageBridge(QObject): mode_left: Emitted when a keymode was left in any window. """ - show_message = pyqtSignal(usertypes.MessageLevel, str) + show_message = pyqtSignal(usertypes.MessageLevel, str, bool) prompt_done = pyqtSignal(usertypes.KeyMode) ask_question = pyqtSignal(usertypes.Question, bool) mode_left = pyqtSignal(usertypes.KeyMode) + clear_messages = pyqtSignal() + + def __init__(self, parent=None): + super().__init__(parent) + self._connected = False + self._cache = [] def ask(self, question, blocking, *, log_stack=False): """Ask a question to the user. @@ -192,6 +215,23 @@ class GlobalMessageBridge(QObject): """ self.ask_question.emit(question, blocking) + def show(self, level, text, replace=False): + if self._connected: + self.show_message.emit(level, text, replace) + else: + self._cache.append((level, text, replace)) + + def flush(self): + """Flush messages which accumulated while no handler was connected. + + This is so we don't miss messages shown during some early init phase. + It needs to be called once the show_message signal is connected. + """ + self._connected = True + for args in self._cache: + self.show(*args) + self._cache = [] + class MessageBridge(QObject): diff --git a/qutebrowser/utils/objreg.py b/qutebrowser/utils/objreg.py index d05424fc3..e54502ef8 100644 --- a/qutebrowser/utils/objreg.py +++ b/qutebrowser/utils/objreg.py @@ -111,7 +111,7 @@ class ObjectRegistry(collections.UserDict): # # With older PyQt-versions (5.2.1) we'll get a "TypeError: # pyqtSignal must be bound to a QObject" instead: - # https://github.com/The-Compiler/qutebrowser/issues/257 + # https://github.com/qutebrowser/qutebrowser/issues/257 pass del partial_objs[name] diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py index f8509ae96..e36fd0ffb 100644 --- a/qutebrowser/utils/qtutils.py +++ b/qutebrowser/utils/qtutils.py @@ -34,7 +34,7 @@ import operator import contextlib from PyQt5.QtCore import (qVersion, QEventLoop, QDataStream, QByteArray, - QIODevice, QSaveFile) + QIODevice, QSaveFile, QT_VERSION_STR) from PyQt5.QtWidgets import QApplication from qutebrowser.utils import log @@ -80,15 +80,34 @@ class QtOSError(OSError): self.qt_errno = None -def version_check(version, op=operator.ge): +def version_check(version, exact=False, strict=False): """Check if the Qt runtime version is the version supplied or newer. Args: version: The version to check against. - op: The operator to use for the check. + exact: if given, check with == instead of >= + strict: If given, also check the compiled Qt version. """ - return op(pkg_resources.parse_version(qVersion()), - pkg_resources.parse_version(version)) + # Catch code using the old API for this + assert exact not in [operator.gt, operator.lt, operator.ge, operator.le, + operator.eq], exact + parsed = pkg_resources.parse_version(version) + op = operator.eq if exact else operator.ge + result = op(pkg_resources.parse_version(qVersion()), parsed) + if strict and result: + # v1 ==/>= parsed, now check if v2 ==/>= parsed too. + result = op(pkg_resources.parse_version(QT_VERSION_STR), parsed) + return result + + +def is_qtwebkit_ng(version): + """Check if the given version is QtWebKit-NG. + + This is typically used as is_webkit_ng(qWebKitVersion) but we don't want to + have QtWebKit imports in here. + """ + return (pkg_resources.parse_version(version) > + pkg_resources.parse_version('538.1')) def check_overflow(arg, ctype, fatal=True): @@ -129,18 +148,24 @@ def get_args(namespace): The argv list to be passed to Qt. """ argv = [sys.argv[0]] + if namespace.qt_flag is not None: - argv.append('-' + namespace.qt_flag[0]) + argv += ['--' + flag[0] for flag in namespace.qt_flag] + if namespace.qt_arg is not None: - argv.append('-' + namespace.qt_arg[0]) - argv.append(namespace.qt_arg[1]) + for name, value in namespace.qt_arg: + argv += ['--' + name, value] + return argv def check_print_compat(): """Check if printing should work in the given Qt version.""" # WORKAROUND (remove this when we bump the requirements to 5.3.0) - return not (os.name == 'nt' and version_check('5.3.0', operator.lt)) + if os.name == 'nt': + return version_check('5.3') + else: + return True def ensure_valid(obj): diff --git a/qutebrowser/utils/standarddir.py b/qutebrowser/utils/standarddir.py index 05fe7ce27..7c756b805 100644 --- a/qutebrowser/utils/standarddir.py +++ b/qutebrowser/utils/standarddir.py @@ -33,6 +33,11 @@ from qutebrowser.utils import log, qtutils, debug _args = None +class EmptyValueError(Exception): + + """Error raised when QStandardPaths returns an empty value.""" + + def config(): """Get a location for configs.""" typ = QStandardPaths.ConfigLocation @@ -104,9 +109,18 @@ def runtime(): else: # pragma: no cover # RuntimeLocation is a weird path on OS X and Windows. typ = QStandardPaths.TempLocation + overridden, path = _from_args(typ, _args) + if not overridden: - path = _writable_location(typ) + try: + path = _writable_location(typ) + except EmptyValueError: + # Fall back to TempLocation when RuntimeLocation is misconfigured + if typ == QStandardPaths.TempLocation: + raise + path = _writable_location(QStandardPaths.TempLocation) + # This is generic, but per-user. # # For TempLocation: @@ -128,7 +142,7 @@ def _writable_location(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!") + raise EmptyValueError("QStandardPaths returned an empty value!") # Qt seems to use '/' as path separator even on Windows... path = path.replace('/', os.sep) return path diff --git a/qutebrowser/utils/urlutils.py b/qutebrowser/utils/urlutils.py index 6a24ceb82..1beebbe92 100644 --- a/qutebrowser/utils/urlutils.py +++ b/qutebrowser/utils/urlutils.py @@ -27,15 +27,16 @@ import posixpath import urllib.parse from PyQt5.QtCore import QUrl -from PyQt5.QtNetwork import QHostInfo, QHostAddress +from PyQt5.QtNetwork import QHostInfo, QHostAddress, QNetworkProxy from qutebrowser.config import config, configexc from qutebrowser.utils import log, qtutils, message, utils from qutebrowser.commands import cmdexc +from qutebrowser.browser.network import pac # FIXME: we probably could raise some exceptions on invalid URLs -# https://github.com/The-Compiler/qutebrowser/issues/108 +# https://github.com/qutebrowser/qutebrowser/issues/108 class InvalidUrlError(ValueError): @@ -296,7 +297,7 @@ def qurl_from_user_input(urlstr): WORKAROUND - https://bugreports.qt.io/browse/QTBUG-41089 FIXME - Maybe https://codereview.qt-project.org/#/c/93851/ has a better way to solve this? - https://github.com/The-Compiler/qutebrowser/issues/109 + https://github.com/qutebrowser/qutebrowser/issues/109 Args: urlstr: The URL as string. @@ -589,3 +590,47 @@ def data_url(mimetype, data): url = QUrl('data:{};base64,{}'.format(mimetype, b64)) qtutils.ensure_valid(url) return url + + +class InvalidProxyTypeError(Exception): + + """Error raised when proxy_from_url gets an unknown proxy type.""" + + def __init__(self, typ): + super().__init__("Invalid proxy type {}!".format(typ)) + + +def proxy_from_url(url): + """Create a QNetworkProxy from QUrl and a proxy type. + + Args: + url: URL of a proxy (possibly with credentials). + + Return: + New QNetworkProxy. + """ + if not url.isValid(): + raise InvalidUrlError(url) + + scheme = url.scheme() + if scheme in ['pac+http', 'pac+https', 'pac+file']: + return pac.PACFetcher(url) + + types = { + 'http': QNetworkProxy.HttpProxy, + 'socks': QNetworkProxy.Socks5Proxy, + 'socks5': QNetworkProxy.Socks5Proxy, + 'direct': QNetworkProxy.NoProxy, + } + if scheme not in types: + raise InvalidProxyTypeError(scheme) + + proxy = QNetworkProxy(types[scheme], url.host()) + + if url.port() != -1: + proxy.setPort(url.port()) + if url.userName(): + proxy.setUser(url.userName()) + if url.password(): + proxy.setPassword(url.password()) + return proxy diff --git a/qutebrowser/utils/usertypes.py b/qutebrowser/utils/usertypes.py index 7ad89b3a2..34148f9af 100644 --- a/qutebrowser/utils/usertypes.py +++ b/qutebrowser/utils/usertypes.py @@ -255,10 +255,6 @@ LoadStatus = enum('LoadStatus', ['none', 'success', 'success_https', 'error', # Backend of a tab Backend = enum('Backend', ['QtWebKit', 'QtWebEngine']) -arg2backend = { - 'webkit': Backend.QtWebKit, - 'webengine': Backend.QtWebEngine, -} # JS world for QtWebEngine diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py index 2e5142511..fd4466e2a 100644 --- a/qutebrowser/utils/utils.py +++ b/qutebrowser/utils/utils.py @@ -29,9 +29,10 @@ import functools import contextlib import itertools import socket +import shlex -from PyQt5.QtCore import Qt -from PyQt5.QtGui import QKeySequence, QColor, QClipboard +from PyQt5.QtCore import Qt, QUrl +from PyQt5.QtGui import QKeySequence, QColor, QClipboard, QDesktopServices from PyQt5.QtWidgets import QApplication import pkg_resources @@ -157,37 +158,6 @@ def resource_filename(filename): return pkg_resources.resource_filename(qutebrowser.__name__, filename) -def actute_warning(): - """Display a warning about the dead_actute issue if needed.""" - # WORKAROUND (remove this when we bump the requirements to 5.3.0) - # Non Linux OS' aren't affected - if not sys.platform.startswith('linux'): - return - # If no compose file exists for some reason, we're not affected - if not os.path.exists('/usr/share/X11/locale/en_US.UTF-8/Compose'): - return - # Qt >= 5.3 doesn't seem to be affected - try: - if qtutils.version_check('5.3.0'): - return - except ValueError: # pragma: no cover - pass - try: - with open('/usr/share/X11/locale/en_US.UTF-8/Compose', 'r', - encoding='utf-8') as f: - for line in f: - if '' in line: - if sys.stdout is not None: - sys.stdout.flush() - print("Note: If you got a 'dead_actute' warning above, " - "that is not a bug in qutebrowser! See " - "https://bugs.freedesktop.org/show_bug.cgi?id=69476 " - "for details.") - break - except OSError: - log.init.exception("Failed to read Compose file") - - def _get_color_percentage(a_c1, a_c2, a_c3, b_c1, b_c2, b_c3, percent): """Get a color which is percent% interpolated between start and end. @@ -399,7 +369,7 @@ def keyevent_to_string(e): if sys.platform == 'darwin': # Qt swaps Ctrl/Meta on OS X, so we switch it back here so the user can # use it in the config as expected. See: - # https://github.com/The-Compiler/qutebrowser/issues/110 + # https://github.com/qutebrowser/qutebrowser/issues/110 # http://doc.qt.io/qt-5.4/osx-issues.html#special-keys modmask2str = collections.OrderedDict([ (Qt.MetaModifier, 'Ctrl'), @@ -825,3 +795,53 @@ def random_port(): port = sock.getsockname()[1] sock.close() return port + + +def open_file(filename, cmdline=None): + """Open the given file. + + If cmdline is not given, general->default-open-dispatcher is used. + If default-open-dispatcher is unset, the system's default application is + used. + + Args: + filename: The filename to open. + cmdline: The command to use as string. A `{}` is expanded to the + filename. None means to use the system's default application + or `default-open-dispatcher` if set. If no `{}` is found, the + filename is appended to the cmdline. + """ + # Import late to avoid circular imports: + # utils -> config -> configdata -> configtypes -> cmdutils -> command -> + # utils + from qutebrowser.misc import guiprocess + from qutebrowser.config import config + # the default program to open downloads with - will be empty string + # if we want to use the default + override = config.get('general', 'default-open-dispatcher') + + # precedence order: cmdline > default-open-dispatcher > openUrl + + if cmdline is None and not override: + log.misc.debug("Opening {} with the system application" + .format(filename)) + url = QUrl.fromLocalFile(filename) + QDesktopServices.openUrl(url) + return + + if cmdline is None and override: + cmdline = override + + cmd, *args = shlex.split(cmdline) + args = [arg.replace('{}', filename) for arg in args] + if '{}' not in cmdline: + args.append(filename) + log.misc.debug("Opening {} with {}" + .format(filename, [cmd] + args)) + proc = guiprocess.GUIProcess(what='open-file') + proc.start_detached(cmd, args) + + +def unused(_arg): + """Function which does nothing to avoid pylint complaining.""" + pass diff --git a/qutebrowser/utils/version.py b/qutebrowser/utils/version.py index 3f6b9c8d0..e6a011f44 100644 --- a/qutebrowser/utils/version.py +++ b/qutebrowser/utils/version.py @@ -28,7 +28,8 @@ import subprocess import importlib import collections -from PyQt5.QtCore import QT_VERSION_STR, PYQT_VERSION_STR, qVersion +from PyQt5.QtCore import (QT_VERSION_STR, PYQT_VERSION_STR, qVersion, + QLibraryInfo) from PyQt5.QtNetwork import QSslSocket from PyQt5.QtWidgets import QApplication @@ -37,8 +38,14 @@ try: except ImportError: # pragma: no cover qWebKitVersion = None +try: + from PyQt5.QtWebEngineWidgets import QWebEngineProfile +except ImportError: # pragma: no cover + QWebEngineProfile = None + import qutebrowser -from qutebrowser.utils import log, utils, standarddir +from qutebrowser.utils import log, utils, standarddir, usertypes, qtutils +from qutebrowser.misc import objects from qutebrowser.browser import pdfjs @@ -132,6 +139,7 @@ def _module_versions(): ('cssutils', ['__version__']), ('typing', []), ('PyQt5.QtWebEngineWidgets', []), + ('PyQt5.QtWebKitWidgets', []), ]) for name, attributes in modules.items(): try: @@ -210,7 +218,8 @@ def _pdfjs_version(): else: pdfjs_file = pdfjs_file.decode('utf-8') version_re = re.compile( - r"^(PDFJS\.version|var pdfjsVersion) = '([^']+)';$", re.MULTILINE) + r"^ *(PDFJS\.version|var pdfjsVersion) = '([^']+)';$", + re.MULTILINE) match = version_re.search(pdfjs_file) if not match: @@ -222,6 +231,40 @@ def _pdfjs_version(): return '{} ({})'.format(pdfjs_version, file_path) +def qt_version(): + """Get a Qt version string based on the runtime/compiled versions.""" + if qVersion() != QT_VERSION_STR: + return '{} (compiled {})'.format(qVersion(), QT_VERSION_STR) + else: + return qVersion() + + +def _chromium_version(): + """Get the Chromium version for QtWebEngine.""" + if QWebEngineProfile is None: + # This should never happen + return 'unavailable' + profile = QWebEngineProfile() + ua = profile.httpUserAgent() + match = re.search(r' Chrome/([^ ]*) ', ua) + if not match: + log.misc.error("Could not get Chromium version from: {}".format(ua)) + return 'unknown' + return match.group(1) + + +def _backend(): + """Get the backend line with relevant information.""" + if objects.backend == usertypes.Backend.QtWebKit: + return 'QtWebKit{} (WebKit {})'.format( + '-NG' if qtutils.is_qtwebkit_ng(qWebKitVersion()) else '', + qWebKitVersion()) + else: + webengine = usertypes.Backend.QtWebEngine + assert objects.backend == webengine, objects.backend + return 'QtWebEngine (Chromium {})'.format(_chromium_version()) + + def version(): """Return a string with various version informations.""" lines = ["qutebrowser v{}".format(qutebrowser.__version__)] @@ -229,16 +272,13 @@ def version(): if gitver is not None: lines.append("Git commit: {}".format(gitver)) - if qVersion() != QT_VERSION_STR: - qt_version = 'Qt: {} (compiled {})'.format(qVersion(), QT_VERSION_STR) - else: - qt_version = 'Qt: {}'.format(qVersion()) + lines.append("Backend: {}".format(_backend())) lines += [ '', '{}: {}'.format(platform.python_implementation(), platform.python_version()), - qt_version, + 'Qt: {}'.format(qt_version()), 'PyQt: {}'.format(PYQT_VERSION_STR), '', ] @@ -247,11 +287,6 @@ def version(): lines += ['pdf.js: {}'.format(_pdfjs_version())] - if qWebKitVersion is None: - lines.append('Webkit: no') - else: - lines.append('Webkit: {}'.format(qWebKitVersion())) - lines += [ 'SSL: {}'.format(QSslSocket.sslLibraryVersionString()), '', @@ -269,6 +304,10 @@ def version(): platform.architecture()[0]), 'Frozen: {}'.format(hasattr(sys, 'frozen')), "Imported from {}".format(importpath), + "Qt library executable path: {}, data path: {}".format( + QLibraryInfo.location(QLibraryInfo.LibraryExecutablesPath), + QLibraryInfo.location(QLibraryInfo.DataPath) + ) ] lines += _os_info() diff --git a/requirements.txt b/requirements.txt index 3d65de342..a62285a08 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,9 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py colorama==0.3.7 -cssutils==1.0.1 -Jinja2==2.8 -MarkupSafe==0.23 -Pygments==2.1.3 +cssutils==1.0.2 +Jinja2==2.9.5 +MarkupSafe==1.0 +Pygments==2.2.0 pyPEG2==2.15.2 PyYAML==3.12 diff --git a/scripts/asciidoc2html.py b/scripts/asciidoc2html.py index d2234cc51..613f622a8 100755 --- a/scripts/asciidoc2html.py +++ b/scripts/asciidoc2html.py @@ -261,7 +261,6 @@ class AsciiDoc: env = os.environ.copy() env['HOME'] = self._homedir subprocess.check_call(cmdline, env=env) - self._failed = True except (subprocess.CalledProcessError, OSError) as e: self._failed = True utils.print_col(str(e), 'red') diff --git a/scripts/dev/build_release.py b/scripts/dev/build_release.py index 95241e5ac..fb8fe000d 100755 --- a/scripts/dev/build_release.py +++ b/scripts/dev/build_release.py @@ -23,6 +23,7 @@ import os import sys +import glob import os.path import shutil import subprocess @@ -90,12 +91,44 @@ def smoke_test(executable): '--temp-basedir', 'about:blank', ':later 500 quit']) +def patch_osx_app(): + """Patch .app to copy missing data and link some libs. + + See https://github.com/pyinstaller/pyinstaller/issues/2276 + """ + app_path = os.path.join('dist', 'qutebrowser.app') + qtwe_core_dir = os.path.join('.tox', 'pyinstaller', 'lib', 'python3.6', + 'site-packages', 'PyQt5', 'Qt', 'lib', + 'QtWebengineCore.framework') + # Copy QtWebEngineProcess.app + proc_app = 'QtWebEngineProcess.app' + shutil.copytree(os.path.join(qtwe_core_dir, 'Helpers', proc_app), + os.path.join(app_path, 'Contents', 'MacOS', proc_app)) + # Copy resources + for f in glob.glob(os.path.join(qtwe_core_dir, 'Resources', '*')): + dest = os.path.join(app_path, 'Contents', 'Resources') + if os.path.isdir(f): + shutil.copytree(f, os.path.join(dest, f)) + else: + shutil.copy(f, dest) + # Link dependencies + for lib in ['QtCore', 'QtWebEngineCore', 'QtQuick', 'QtQml', 'QtNetwork', + 'QtGui', 'QtWebChannel', 'QtPositioning']: + dest = os.path.join(app_path, lib + '.framework', 'Versions', '5') + os.makedirs(dest) + os.symlink(os.path.join(os.pardir, os.pardir, os.pardir, 'Contents', + 'MacOS', lib), + os.path.join(dest, lib)) + + def build_osx(): """Build OS X .dmg/.app.""" utils.print_title("Updating 3rdparty content") update_3rdparty.update_pdfjs() utils.print_title("Building .app via pyinstaller") call_tox('pyinstaller', '-r') + utils.print_title("Patching .app") + patch_osx_app() utils.print_title("Building .dmg") subprocess.check_call(['make', '-f', 'scripts/dev/Makefile-dmg']) utils.print_title("Cleaning up...") diff --git a/scripts/dev/check_coverage.py b/scripts/dev/check_coverage.py index 895993bae..c876acd1a 100644 --- a/scripts/dev/check_coverage.py +++ b/scripts/dev/check_coverage.py @@ -55,16 +55,14 @@ PERFECT_FILES = [ 'qutebrowser/browser/history.py'), ('tests/unit/browser/webkit/test_history.py', 'qutebrowser/browser/webkit/webkithistory.py'), - ('tests/unit/browser/webkit/test_tabhistory.py', - 'qutebrowser/browser/webkit/tabhistory.py'), ('tests/unit/browser/webkit/http/test_http.py', 'qutebrowser/browser/webkit/http.py'), ('tests/unit/browser/webkit/http/test_content_disposition.py', 'qutebrowser/browser/webkit/rfc6266.py'), - ('tests/unit/browser/webkit/test_webkitelem.py', - 'qutebrowser/browser/webkit/webkitelem.py'), - ('tests/unit/browser/webkit/test_webkitelem.py', - 'qutebrowser/browser/webelem.py'), + # ('tests/unit/browser/webkit/test_webkitelem.py', + # 'qutebrowser/browser/webkit/webkitelem.py'), + # ('tests/unit/browser/webkit/test_webkitelem.py', + # 'qutebrowser/browser/webelem.py'), ('tests/unit/browser/webkit/network/test_schemehandler.py', 'qutebrowser/browser/webkit/network/schemehandler.py'), ('tests/unit/browser/webkit/network/test_filescheme.py', @@ -104,6 +102,8 @@ PERFECT_FILES = [ 'qutebrowser/misc/keyhintwidget.py'), ('tests/unit/misc/test_pastebin.py', 'qutebrowser/misc/pastebin.py'), + (None, + 'qutebrowser/misc/objects.py'), (None, 'qutebrowser/mainwindow/statusbar/keystring.py'), @@ -166,6 +166,7 @@ PERFECT_FILES = [ WHITELISTED_FILES = [ 'qutebrowser/browser/webkit/webkitinspector.py', 'qutebrowser/keyinput/macros.py', + 'qutebrowser/browser/webkit/webkitelem.py', ] @@ -267,7 +268,7 @@ def main_check(): print() print("To debug this, run 'tox -e py35-cov' (or py34-cov) locally and " "check htmlcov/index.html") - print("or check https://codecov.io/github/The-Compiler/qutebrowser") + print("or check https://codecov.io/github/qutebrowser/qutebrowser") print() if 'CI' in os.environ: diff --git a/scripts/dev/ci/travis_backtrace.sh b/scripts/dev/ci/travis_backtrace.sh new file mode 100644 index 000000000..764c36116 --- /dev/null +++ b/scripts/dev/ci/travis_backtrace.sh @@ -0,0 +1,22 @@ +#!/bin/bash +# +# Find all possible core files under current directory. Attempt +# to determine exe using file(1) and dump stack trace with gdb. +# + +case $TESTENV in + py34-cov) + exe=/usr/bin/python3.4 + full=full + ;; + py3*-pyqt*) + exe=$(readlink -f .tox/$TESTENV/bin/python) + full= + ;; + *) + echo "Skipping coredump analysis in testenv $TESTENV!" + exit 0 + ;; +esac + +find . -name *.core -o -name core -exec gdb --batch --quiet -ex "thread apply all bt $full" "$exe" {} \; diff --git a/scripts/dev/ci/travis_install.sh b/scripts/dev/ci/travis_install.sh index f5d75eed5..e752685f5 100644 --- a/scripts/dev/ci/travis_install.sh +++ b/scripts/dev/ci/travis_install.sh @@ -102,7 +102,7 @@ elif [[ $TRAVIS_OS_NAME == osx ]]; then exit 0 fi -pyqt_pkgs="python3-pyqt5 python3-pyqt5.qtwebkit" +pyqt_pkgs="python3-pyqt5 python3-pyqt5.qtquick python3-pyqt5.qtwebkit" pip_install pip pip_install -r misc/requirements/requirements-tox.txt @@ -113,9 +113,12 @@ tox --version case $TESTENV in py34-cov) pip_install -r misc/requirements/requirements-codecov.txt - apt_install xvfb $pyqt_pkgs libpython3.4-dev + apt_install xvfb $pyqt_pkgs libpython3.4-dev gdb apport libqt5webkit5-dbg python3-pyqt5-dbg python3-pyqt5.qtquick-dbg python3-pyqt5.qtwebkit-dbg python3-dbg check_pyqt ;; + py3*-pyqt*) + apt_install xvfb geoclue gdb apport + ;; pylint|vulture) apt_install $pyqt_pkgs libpython3.4-dev check_pyqt diff --git a/scripts/dev/ci/travis_run.sh b/scripts/dev/ci/travis_run.sh index 86c2b11a8..6e148c088 100644 --- a/scripts/dev/ci/travis_run.sh +++ b/scripts/dev/ci/travis_run.sh @@ -1,11 +1,11 @@ #!/bin/bash if [[ $DOCKER ]]; then - docker run --privileged -v $PWD:/outside -e QUTE_BDD_WEBENGINE=$QUTE_BDD_WEBENGINE qutebrowser/travis:$DOCKER + docker run --privileged -v $PWD:/outside -e QUTE_BDD_WEBENGINE=$QUTE_BDD_WEBENGINE -e DOCKER=$DOCKER qutebrowser/travis:$DOCKER else args=() [[ $TESTENV == docs ]] && args=('--no-authors') - [[ $TRAVIS_OS_NAME == osx ]] && args=('--qute-bdd-webengine') + [[ $TRAVIS_OS_NAME == osx ]] && args=('--qute-bdd-webengine' '--no-xvfb') tox -e $TESTENV -- "${args[@]}" fi diff --git a/scripts/dev/download_release.sh b/scripts/dev/download_release.sh index 80b68d2f0..7ec4d9159 100644 --- a/scripts/dev/download_release.sh +++ b/scripts/dev/download_release.sh @@ -14,7 +14,7 @@ fi cd "$tmpdir" mkdir windows -base="https://github.com/The-Compiler/qutebrowser/releases/download/v$1" +base="https://github.com/qutebrowser/qutebrowser/releases/download/v$1" wget "$base/qutebrowser-$1.tar.gz" || exit 1 wget "$base/qutebrowser-$1.tar.gz.asc" || exit 1 diff --git a/scripts/dev/freeze.py b/scripts/dev/freeze.py index 67bda081a..f254f4d90 100755 --- a/scripts/dev/freeze.py +++ b/scripts/dev/freeze.py @@ -98,10 +98,7 @@ def get_build_exe_options(skip_html=False): 'include_msvcr': True, 'includes': [], 'excludes': ['tkinter'], - 'packages': ['pygments', 'pkg_resources._vendor.packaging', - 'pkg_resources._vendor.pyparsing', - 'pkg_resources._vendor.six', - 'pkg_resources._vendor.appdirs'], + 'packages': ['pygments'], } diff --git a/scripts/dev/freeze_tests.py b/scripts/dev/freeze_tests.py index 9c57b10e1..9f9e2bbd2 100755 --- a/scripts/dev/freeze_tests.py +++ b/scripts/dev/freeze_tests.py @@ -55,8 +55,7 @@ def get_build_exe_options(): opts = freeze.get_build_exe_options(skip_html=True) opts['includes'] += pytest.freeze_includes() opts['includes'] += ['unittest.mock', 'PyQt5.QtTest', 'hypothesis', 'bs4', - 'httpbin', 'jinja2.ext', 'cherrypy.wsgiserver', - 'pstats'] + 'httpbin', 'jinja2.ext', 'cheroot', 'pstats', 'queue'] httpbin_dir = os.path.dirname(httpbin.__file__) opts['include_files'] += [ diff --git a/scripts/dev/get_coredumpctl_traces.py b/scripts/dev/get_coredumpctl_traces.py index 49620a96a..d1e962834 100644 --- a/scripts/dev/get_coredumpctl_traces.py +++ b/scripts/dev/get_coredumpctl_traces.py @@ -89,7 +89,7 @@ def get_info(pid): def is_qutebrowser_dump(parsed): """Check if the given Line is a qutebrowser dump.""" basename = os.path.basename(parsed.exe) - if basename in ['python', 'python3', 'python3.4', 'python3.5']: + if basename == 'python' or basename.startswith('python3'): info = get_info(parsed.pid) try: cmdline = info['Command Line'] diff --git a/scripts/dev/recompile_requirements.py b/scripts/dev/recompile_requirements.py index 65c6ad5c8..1e3181344 100644 --- a/scripts/dev/recompile_requirements.py +++ b/scripts/dev/recompile_requirements.py @@ -60,6 +60,20 @@ def convert_line(line, comments): return line +def get_requirements(requirements_file, exclude=()): + """Get the requirements after freezing with the given file.""" + with tempfile.TemporaryDirectory() as tmpdir: + pip_bin = os.path.join(tmpdir, 'bin', 'pip') + subprocess.check_call(['virtualenv', tmpdir]) + if requirements_file is not None: + subprocess.check_call([pip_bin, 'install', '-r', + requirements_file]) + out = subprocess.check_output([pip_bin, 'freeze', '--all'], + universal_newlines=True) + + return [line for line in out.splitlines() if line not in exclude] + + def read_comments(fobj): """Find special comments in the config. @@ -98,36 +112,52 @@ def get_all_names(): """Get all requirement names based on filenames.""" for filename in glob.glob(os.path.join(REQ_DIR, 'requirements-*.txt-raw')): basename = os.path.basename(filename) - yield basename[len('requirements-'):-len('.txt-raw')] + name = basename[len('requirements-'):-len('.txt-raw')] + if name == 'cxfreeze' and sys.hexversion >= 0x030600: + print("Warning: Skipping cxfreeze") + else: + yield name + yield 'pip' def main(): """Re-compile the given (or all) requirement files.""" - names = sys.argv[1:] if len(sys.argv) > 1 else get_all_names() + names = sys.argv[1:] if len(sys.argv) > 1 else sorted(get_all_names()) + + utils.print_title('pip') + pip_requirements = get_requirements(None) for name in names: utils.print_title(name) - filename = os.path.join(REQ_DIR, - 'requirements-{}.txt-raw'.format(name)) + if name == 'qutebrowser': outfile = os.path.join(REPO_DIR, 'requirements.txt') else: outfile = os.path.join(REQ_DIR, 'requirements-{}.txt'.format(name)) - with tempfile.TemporaryDirectory() as tmpdir: - pip_bin = os.path.join(tmpdir, 'bin', 'pip') - subprocess.check_call(['virtualenv', tmpdir]) - subprocess.check_call([pip_bin, 'install', '-r', filename]) - reqs = subprocess.check_output([pip_bin, 'freeze']).decode('utf-8') + if name == 'pip': + requirements = [req for req in pip_requirements + if not req.startswith('pip==')] + comments = { + 'filter': {}, + 'comment': {}, + 'ignore': [], + 'replace': {}, + } + else: + filename = os.path.join(REQ_DIR, + 'requirements-{}.txt-raw'.format(name)) + requirements = get_requirements(filename, exclude=pip_requirements) - with open(filename, 'r', encoding='utf-8') as f: - comments = read_comments(f) + with open(filename, 'r', encoding='utf-8') as f: + comments = read_comments(f) with open(outfile, 'w', encoding='utf-8') as f: f.write("# This file is automatically generated by " "scripts/dev/recompile_requirements.py\n\n") - for line in reqs.splitlines(): - f.write(convert_line(line, comments) + '\n') + for line in requirements: + converted = convert_line(line, comments) + f.write(converted + '\n') if __name__ == '__main__': diff --git a/scripts/dev/run_frozen_tests.py b/scripts/dev/run_frozen_tests.py index 8afa5b634..e64325417 100644 --- a/scripts/dev/run_frozen_tests.py +++ b/scripts/dev/run_frozen_tests.py @@ -31,8 +31,10 @@ import pytest_faulthandler import pytest_xvfb import pytest_rerunfailures import pytest_warnings +import pytest_benchmark.plugin sys.exit(pytest.main(plugins=[pytestqt.plugin, pytest_mock, pytest_catchlog, pytest_instafail, pytest_faulthandler, pytest_xvfb, - pytest_rerunfailures, pytest_warnings])) + pytest_rerunfailures, pytest_warnings, + pytest_benchmark.plugin])) diff --git a/scripts/dev/run_pytest.py b/scripts/dev/run_pytest.py deleted file mode 100644 index eddd9ed42..000000000 --- a/scripts/dev/run_pytest.py +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env python3 -# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: - -# Copyright 2016 Florian Bruhin (The Compiler) -# -# 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 . - -"""Wrapper around pytest to ignore segfaults on exit.""" - -import os -import sys -import subprocess -import signal - - -if __name__ == '__main__': - script_path = os.path.abspath(os.path.dirname(__file__)) - pytest_status_file = os.path.join(script_path, '..', '..', '.cache', - 'pytest_status') - - try: - os.remove(pytest_status_file) - except FileNotFoundError: - pass - - try: - subprocess.check_call([sys.executable, '-m', 'pytest'] + sys.argv[1:]) - except subprocess.CalledProcessError as exc: - is_segfault = exc.returncode in [128 + signal.SIGSEGV, -signal.SIGSEGV] - if is_segfault and os.path.exists(pytest_status_file): - print("Ignoring segfault on exit!") - with open(pytest_status_file, 'r', encoding='ascii') as f: - exit_status = int(f.read()) - else: - exit_status = exc.returncode - else: - exit_status = 0 - - try: - os.remove(pytest_status_file) - except FileNotFoundError: - pass - - sys.exit(exit_status) diff --git a/scripts/dev/run_vulture.py b/scripts/dev/run_vulture.py index ade05ac94..55f4d9c86 100755 --- a/scripts/dev/run_vulture.py +++ b/scripts/dev/run_vulture.py @@ -65,7 +65,6 @@ def whitelist_generator(): yield 'qutebrowser.utils.debug.qflags_key' yield 'qutebrowser.utils.qtutils.QtOSError.qt_errno' yield 'scripts.utils.bg_colors' - yield 'qutebrowser.browser.webelem.AbstractWebElement.style_property' yield 'qutebrowser.config.configtypes.Float' # Qt attributes @@ -87,6 +86,7 @@ def whitelist_generator(): yield 'qutebrowser.browser.pdfjs.is_available' yield 'QEvent.posted' yield 'log_stack' # from message.py + yield 'propagate' # logging.getLogger('...).propagate = False # vulture doesn't notice the hasattr() and thus thinks netrc_used is unused # in NetworkManager.on_authentication_required yield 'PyQt5.QtNetwork.QNetworkReply.netrc_used' diff --git a/scripts/dev/src2asciidoc.py b/scripts/dev/src2asciidoc.py index 2628ba282..a8f019d9d 100755 --- a/scripts/dev/src2asciidoc.py +++ b/scripts/dev/src2asciidoc.py @@ -365,6 +365,8 @@ def generate_commands(filename): def _generate_setting_section(f, sectname, sect): """Generate documentation for a single section.""" + version_dependent_options = [('network', 'proxy'), + ('general', 'print-element-backgrounds')] for optname, option in sect.items(): f.write("\n") f.write('[[{}-{}]]'.format(sectname, optname) + "\n") @@ -390,7 +392,8 @@ def _generate_setting_section(f, sectname, sect): else: f.write("Default: empty\n") - if option.backends is None: + if (option.backends is None or + (sectname, optname) in version_dependent_options): pass elif option.backends == [usertypes.Backend.QtWebKit]: f.write("\nThis setting is only available with the QtWebKit " @@ -433,10 +436,13 @@ def _get_authors(): 'Corentin Jule': 'Corentin Julé', 'Claire C.C': 'Claire Cavanaugh', 'Rahid': 'Maciej Wołczyk', + 'Fritz V155 Reichwald': 'Fritz Reichwald', } + ignored = ['pyup-bot'] commits = subprocess.check_output(['git', 'log', '--format=%aN']) authors = [corrections.get(author, author) - for author in commits.decode('utf-8').splitlines()] + for author in commits.decode('utf-8').splitlines() + if author not in ignored] cnt = collections.Counter(authors) return sorted(cnt, key=lambda k: (cnt[k], k), reverse=True) diff --git a/tests/conftest.py b/tests/conftest.py index 7fa94ffc3..bcb67924b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -35,6 +35,7 @@ from helpers import logfail from helpers.logfail import fail_on_logging from helpers.messagemock import message_mock from helpers.fixtures import * # pylint: disable=wildcard-import +from qutebrowser.utils import qtutils # Set hypothesis settings @@ -115,8 +116,6 @@ def pytest_collection_modifyitems(config, items): 'test_conftest.py'] if module_root_dir == 'end2end': item.add_marker(pytest.mark.end2end) - elif os.environ.get('QUTE_BDD_WEBENGINE', ''): - deselected = True _apply_platform_markers(item) if item.get_marker('xfail_norun'): @@ -193,21 +192,3 @@ def pytest_runtest_makereport(item, call): outcome = yield rep = outcome.get_result() setattr(item, "rep_" + rep.when, rep) - - -@pytest.hookimpl(hookwrapper=True) -def pytest_sessionfinish(exitstatus): - """Create a file to tell run_pytest.py how pytest exited.""" - outcome = yield - outcome.get_result() - - cache_dir = os.path.join(os.path.abspath(os.path.dirname(__file__)), - '..', '.cache') - try: - os.mkdir(cache_dir) - except FileExistsError: - pass - - status_file = os.path.join(cache_dir, 'pytest_status') - with open(status_file, 'w', encoding='ascii') as f: - f.write(str(exitstatus)) diff --git a/tests/end2end/conftest.py b/tests/end2end/conftest.py index d2512457e..5dcff3fb5 100644 --- a/tests/end2end/conftest.py +++ b/tests/end2end/conftest.py @@ -68,31 +68,32 @@ def _get_version_tag(tag): """ version_re = re.compile(r""" (?Pqt|pyqt) - (?P==|>|>=|<|<=|!=) - (?P\d+\.\d+\.\d+) + (?P==|>=|!=) + (?P\d+\.\d+(\.\d+)?) """, re.VERBOSE) match = version_re.match(tag) if not match: return None - operators = { - '==': operator.eq, - '>': operator.gt, - '<': operator.lt, - '>=': operator.ge, - '<=': operator.le, - '!=': operator.ne, - } - package = match.group('package') - op = operators[match.group('operator')] version = match.group('version') if package == 'qt': - return pytest.mark.skipif(qtutils.version_check(version, op), - reason='Needs ' + tag) + op = match.group('operator') + do_skip = { + '==': not qtutils.version_check(version, exact=True), + '>=': not qtutils.version_check(version), + '!=': qtutils.version_check(version, exact=True), + } + return pytest.mark.skipif(do_skip[op], reason='Needs ' + tag) elif package == 'pyqt': + operators = { + '==': operator.eq, + '>=': operator.ge, + '!=': operator.ne, + } + op = operators[match.group('operator')] major, minor, patch = [int(e) for e in version.split('.')] hex_version = (major << 16) | (minor << 8) | patch return pytest.mark.skipif(not op(PYQT_VERSION, hex_version), @@ -106,7 +107,9 @@ def _get_backend_tag(tag): pytest_marks = { 'qtwebengine_todo': pytest.mark.qtwebengine_todo, 'qtwebengine_skip': pytest.mark.qtwebengine_skip, - 'qtwebkit_skip': pytest.mark.qtwebkit_skip + 'qtwebkit_skip': pytest.mark.qtwebkit_skip, + 'qtwebkit_ng_xfail': pytest.mark.qtwebkit_ng_xfail, + 'qtwebkit_ng_skip': pytest.mark.qtwebkit_ng_skip, } if not any(tag.startswith(t + ':') for t in pytest_marks): return None @@ -132,6 +135,16 @@ if not getattr(sys, 'frozen', False): def pytest_collection_modifyitems(config, items): """Apply @qtwebengine_* markers; skip unittests with QUTE_BDD_WEBENGINE.""" + if config.webengine: + qtwebkit_ng_used = False + else: + try: + from PyQt5.QtWebKit import qWebKitVersion + except ImportError: + qtwebkit_ng_used = False + else: + qtwebkit_ng_used = qtutils.is_qtwebkit_ng(qWebKitVersion()) + markers = [ ('qtwebengine_todo', 'QtWebEngine TODO', pytest.mark.xfail, config.webengine), @@ -139,6 +152,10 @@ def pytest_collection_modifyitems(config, items): config.webengine), ('qtwebkit_skip', 'Skipped with QtWebKit', pytest.mark.skipif, not config.webengine), + ('qtwebkit_ng_xfail', 'Failing with QtWebKit-NG', pytest.mark.xfail, + qtwebkit_ng_used), + ('qtwebkit_ng_skip', 'Skipped with QtWebKit-NG', pytest.mark.skipif, + qtwebkit_ng_used), ('qtwebengine_flaky', 'Flaky with QtWebEngine', pytest.mark.skipif, config.webengine), ('qtwebengine_osx_xfail', 'Fails on OS X with QtWebEngine', diff --git a/tests/end2end/data/downloads/download with spaces.bin b/tests/end2end/data/downloads/download with spaces.bin new file mode 100644 index 000000000..f76dd238a Binary files /dev/null and b/tests/end2end/data/downloads/download with spaces.bin differ diff --git a/tests/end2end/data/downloads/issue1535.html b/tests/end2end/data/downloads/issue1535.html index aec88420b..2c147bff6 100644 --- a/tests/end2end/data/downloads/issue1535.html +++ b/tests/end2end/data/downloads/issue1535.html @@ -4,7 +4,7 @@

Cancelling a download that belongs to an mhtml download.

-

See also GitHub

+

See also GitHub

@@ -80,7 +80,7 @@

...and how?

-

See +

See here for more information.

More useless trivia!

diff --git a/tests/end2end/data/downloads/mhtml/complex/complex.mht b/tests/end2end/data/downloads/mhtml/complex/complex.mht index d7988cb63..0cbcc4607 100644 --- a/tests/end2end/data/downloads/mhtml/complex/complex.mht +++ b/tests/end2end/data/downloads/mhtml/complex/complex.mht @@ -8,143 +8,125 @@ Content-Type: text/html; charset="UTF-8" Content-Transfer-Encoding: quoted-printable -=20=20=20=20=20=20=20=20 -=20=20=20=20=20=20=20=20qutebrowser=20mhtml=20test -=20=20=20=20 -=20=20=20=20=20=20=20=20 -=20=20=20=20=20=20=20=20 -=20=20=20=20=20=20=20=20 -=20=20=20=20=20=20=20=20 -=20=20=20=20=20=20=20=20 -=20=20=20=20=20=20=20=20 -=20=20=20=20=20=20=20=20 -=20=20=20=20=20=20=20=20 -=20=20=20=20=20=20=20=20@import=20"actually-it's-css"; -=20=20=20=20=20=20=20=20 -=20=20=20=20=20=20=20=20 -=20=20=20=20=20=20=20=20 -=20=20=20=20=20=20=20=20 -=20=20=20=20=20=20=20=20 -=20=20=20=20=20=20=20=20 -=20=20=20=20=20=20=20=20 -=20=20=20=20=20=20=20=20 -=20=20=20=20=20=20=20=20 -=20=20=20=20=20=20=20=20 -=20=20=20=20=20=20=20=20 -=20=20=20=20=20=20=20=20=20 -=20=20=20=20=20=20=20=20 -=20=20=20=20 -=20=20=20=20 -=20=20=20=20=20=20=20=20 -=20=20=20=20=20=20=20=20 -=20=20=20=20=20=20=20=20 -=20=20=20=20=20=20=20=20

Welcome=20to=20the=20qutebrowser=20mhtml=20test= -=20page

-=20=20=20=20=20=20=20=20 -=20=20=20=20=20=20=20=20 -=20=20=20=20=20=20=20=20...that=20the=20word=20qutebrowser=20is=20= -a=20word=20play=20on=20Qt,=20the -=20=20=20=20=20=20=20=20framework=20the=20browser=20is=20built=20with? -=20=20=20=20=20=20=20=20 -=20=20=20=20=20=20=20=20 -=20=20=20=20=20=20=20=20

What=20is=20this=20page?

-=20=20=20=20=20=20=20=20 -=20=20=20=20=20=20=20=20

This=20page=20is=20a=20test-case=20for=20the=20m= -html=20download=20feature=20of -=20=20=20=20=20=20=20=20qutebrowser.=20Under=20normal=20circumstances,=20yo= -u=20won't=20see=20this=20page,=20except -=20=20=20=20=20=20=20=20if=20you're=20a=20qutebrowser=20developer=20or<= -/em>=20you're=20attending=20one=20of -=20=20=20=20=20=20=20=20The-Compiler's=20pytest=20demos.

-=20=20=20=20=20=20=20=20 -=20=20=20=20=20=20=20=20 -=20=20=20=20=20=20=20=20...that=20this=20page=20was=20once=20a=20monstrosit= -y=20with=20"this=20weird=20pixelated -=20=20=20=20=20=20=20=20globe=20with=20the=20geocities-like=20background"?=20You=20can=20find=20the=20old -=20=20=20=20=20=20=20=20page=20in=20the=20old=20commits=20and=20indeed,=20i= -t=20was=20quite=20atrocious.=20But=20hey, -=20=20=20=20=20=20=20=20every=20browser=20needs=20a=20globe... -=20=20=20=20=20=20=20=20 -=20=20=20=20=20=20=20=20 -=20=20=20=20=20=20=20=20

This=20page=20references=20other=20assets=20and= -=20when=20the=20page=20is=20downloaded, -=20=20=20=20=20=20=20=20qutebrowser=20checks=20if=20each=20asset=20was=20do= -wnloaded.=20If=20some=20assets=20are -=20=20=20=20=20=20=20=20missing,=20the=20test=20fails=20and=20the=20poor=20= -developers=20have=20to=20search=20for=20the -=20=20=20=20=20=20=20=20error.

-=20=20=20=20=20=20=20=20 -=20=20=20=20=20=20=20=20

Can=20I=20contribute=20to=20qutebrowser?

-=20=20=20=20=20=20=20=20 -=20=20=20=20=20=20=20=20

Yes!

-=20=20=20=20=20=20=20=20 -=20=20=20=20=20=20=20=20 -=20=20=20=20=20=20=20=20...that=20qutebrowser=20is=20free=20software?=20Fre= -e=20as=20in=20free=20beer=20and -=20=20=20=20=20=20=20=20free=20speech!=20Isn't=20that=20great? -=20=20=20=20=20=20=20=20 -=20=20=20=20=20=20=20=20 -=20=20=20=20=20=20=20=20

...and=20how?

-=20=20=20=20=20=20=20=20 -=20=20=20=20=20=20=20=20

See=20 -=20=20=20=20=20=20=20=20here=20for=20more=20information.

-=20=20=20=20=20=20=20=20 -=20=20=20=20=20=20=20=20

More=20useless=20trivia!

-=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20 -=20=20=20=20=20=20=20=20 -=20=20=20=20=20=20=20=20...that=20the=20font=20in=20the=20header=20is=20Com= -ic=20Sans? -=20=20=20=20=20=20=20=20 -=20=20=20=20=20=20=20=20 -=20=20=20=20=20=20=20=20 -=20=20=20=20=20=20=20=20...the=20IRC=20channel=20for=20qutebrowser=20is=20<= -code>#qutebrowser=20on -=20=20=20=20=20=20=20=20irc.freenode.net -=20=20=20=20=20=20=20=20 -=20=20=20=20=20=20=20=20 -=20=20=20=20=20=20=20=20 -=20=20=20=20=20=20=20=20...the=20area=20of=20a=20circle=20is=20=CF=80*r2? -=20=20=20=20=20=20=20=20 -=20=20=20=20=20=20=20=20 -=20=20=20=20=20=20=20=20

To=20make=20this=20page=20a=20bit=20useful,=20I'= -ve=20included=20a=20chessboard,=20so=20you -=20=20=20=20=20=20=20=20can=20play=20chess.=20Just=20turn=20your=20screen= -=2090=20degrees,=20such=20that=20it=20forms=20a -=20=20=20=20=20=20=20=20flat,=20horizontal=20surface=20(you=20can=20skip=20= -this=20step=20if=20you're=20using=20a -=20=20=20=20=20=20=20=20tablet).=20Next,=20zoom=20the=20page=20until=20it= -=20fits=20your=20needs.=20Enjoy=20your=20round -=20=20=20=20=20=20=20=20of=20chess!

-=20=20=20=20=20=20=20=20 -=20=20=20=20=20=20=20=20 -=20=20=20=20 + + qutebrowser mhtml test + =20 + + + =20 + + + =20 + + + =20 + + + =20 + + + =20 + + + =20 + + + + + + + =20 +

Welcome to the qutebrowser mhtml test page

+ =20 +
+ ...that the word qutebrowser is a word play on Qt, the + framework the browser is built with? +
+ =20 +

What is this page?

+ =20 +

This page is a test-case for the mhtml download feature of + qutebrowser. Under normal circumstances, you won't see this page, e= +xcept + if you're a qutebrowser developer or you're attending one = +of + The-Compiler's pytest demos.

+ =20 +
+ ...that this page was once a monstrosity with "this weird pixel= +ated + globe with the geocities-like background"? You can find the old + page in the old commits and indeed, it was quite atrocious. But hey, + every browser needs a globe... +
+ =20 +

This page references other assets and when the page is downloade= +d, + qutebrowser checks if each asset was downloaded. If some assets are + missing, the test fails and the poor developers have to search for = +the + error.

+ =20 +

Can I contribute to qutebrowser?

+ =20 +

Yes!

+ =20 +
+ ...that qutebrowser is free software? Free as in free beer= + and + free speech! Isn't that great? +
+ =20 +

...and how?

+ =20 +

See + here for more information.

+ =20 +

More useless trivia!

+ =20 +
+ ...that the font in the header is Comic Sans? +
+ =20 +
+ ...the IRC channel for qutebrowser is #qutebrowser on + irc.freenode.net +
+ =20 +
+ ...the area of a circle is =CF=80*r2? +
+ =20 +

To make this page a bit useful, I've included a chessboard, so y= +ou + can play chess. Just turn your screen 90 degrees, such that it form= +s a + flat, horizontal surface (you can skip this step if you're using a + tablet). Next, zoom the page until it fits your needs. Enjoy your r= +ound + of chess!

+ +
+ =20 -----=_qute-5314618b-e51d-46e1-9598-103536e86b59 @@ -709,35 +691,33 @@ MIME-Version: 1.0 Content-Type: text/css; charset=utf-8 Content-Transfer-Encoding: quoted-printable -@import=20'external-in-extern.css'; -/*=20We=20want=20to=20make=20sure=20that=20assets=20referenced=20in=20exter= -nal=20css=20files=20are -=20*=20properly=20included -=20*/ -div.dyk=20{ -=20=20=20=20/*=20Did=20you=20know?=20*/ -=20=20=20=20background-image:=20url('DYK.png'); -=20=20=20=20background-repeat:=20no-repeat; -=20=20=20=20/*=20Image=20is=20128px=20wide=20*/ -=20=20=20=20min-height:=20128px; -=20=20=20=20padding-left:=20148px; -=20=20=20=20margin-top:=2010px; -=20=20=20=20margin-bottom:=2010px; -=20=20=20=20border:=202px=20solid=20#474747; -=20=20=20=20border-radius:=2064px; +@import 'external-in-extern.css'; +/* We want to make sure that assets referenced in external css files are + * properly included + */ +div.dyk { + /* Did you know? */ + background-image: url('DYK.png'); + background-repeat: no-repeat; + /* Image is 128px wide */ + min-height: 128px; + padding-left: 148px; + margin-top: 10px; + margin-bottom: 10px; + border: 2px solid #474747; + border-radius: 64px; } -=20=20=20=20 + =20 -----=_qute-5314618b-e51d-46e1-9598-103536e86b59 Content-Location: http://localhost:1337/data/downloads/mhtml/complex/external-in-extern.css MIME-Version: 1.0 Content-Type: text/css; charset=utf-8 Content-Transfer-Encoding: quoted-printable -/*=20Just=20making=20sure=20that=20more=20than=20one=20level=20of=20externa= -l=20css=20is=20included=20*/ -h1,=20h2,=20h3,=20h4,=20h5,=20h6=20{ -=20=20=20=20color:=20#0A396E; -=20=20=20=20border-bottom:=201px=20dotted=20#474747; +/* Just making sure that more than one level of external css is included */ +h1, h2, h3, h4, h5, h6 { + color: #0A396E; + border-bottom: 1px dotted #474747; } -----=_qute-5314618b-e51d-46e1-9598-103536e86b59 Content-Location: http://localhost:1337/data/downloads/mhtml/complex/favicon.png diff --git a/tests/end2end/data/downloads/mhtml/complex/not-css.qss b/tests/end2end/data/downloads/mhtml/complex/not-css.qss new file mode 100644 index 000000000..e69de29bb diff --git a/tests/end2end/data/downloads/mhtml/simple/simple-webengine.mht b/tests/end2end/data/downloads/mhtml/simple/simple-webengine.mht new file mode 100644 index 000000000..d8bfdee70 --- /dev/null +++ b/tests/end2end/data/downloads/mhtml/simple/simple-webengine.mht @@ -0,0 +1,26 @@ +From: +Subject: Simple MHTML test +Date: today +MIME-Version: 1.0 +Content-Type: multipart/related; + type="text/html"; + boundary="---=_qute-UUID" + +-----=_qute-UUID +Content-Type: text/html +Content-ID: 42 +Content-Transfer-Encoding: quoted-printable +Content-Location: http://localhost:(port)/data/downloads/mhtml/simple/simple.html + + + =20 + Simple MHTML test + + + normal link to another page + =20 + + +-----=_qute-UUID diff --git a/tests/end2end/data/downloads/mhtml/simple/simple.mht b/tests/end2end/data/downloads/mhtml/simple/simple.mht index d0b7a7c48..ef4431362 100644 --- a/tests/end2end/data/downloads/mhtml/simple/simple.mht +++ b/tests/end2end/data/downloads/mhtml/simple/simple.mht @@ -7,14 +7,13 @@ MIME-Version: 1.0 Content-Type: text/html; charset="UTF-8" Content-Transfer-Encoding: quoted-printable - -=20=20=20=20=20=20=20=20 -=20=20=20=20=20=20=20=20Simple=20MHTML=20test -=20=20=20=20 -=20=20=20=20 -=20=20=20=20=20=20=20=20normal=20link=20to=20another=20page= - -=20=20=20=20 + + + Simple MHTML test + + + normal link to another page + =20 -----=_qute-6d584056-b1e4-4882-91e6-d4a6d23adb67-- diff --git a/tests/end2end/data/email_address.html b/tests/end2end/data/email_address.html new file mode 100644 index 000000000..3c86446ad --- /dev/null +++ b/tests/end2end/data/email_address.html @@ -0,0 +1,11 @@ + + + + + + Email address + + + Email address + + diff --git a/tests/end2end/data/hints/ace/ace.js b/tests/end2end/data/hints/ace/ace.js index eaa57239b..4ee0b7b8d 100644 --- a/tests/end2end/data/hints/ace/ace.js +++ b/tests/end2end/data/hints/ace/ace.js @@ -2283,6 +2283,9 @@ var TextInput = function(parentNode, host) { if (e.type == "compositionend" && c.range) { host.selection.setRange(c.range); } + if (useragent.isChrome && useragent.isChrome >= 53) { + onInput(); + } }; @@ -6047,7 +6050,7 @@ var Mode = function() { }; (function() { - this.$behaviour = new CstyleBehaviour(); + this.$defaultBehaviour = new CstyleBehaviour(); this.tokenRe = new RegExp("^[" + unicode.packages.L @@ -10533,7 +10536,7 @@ var Search = function() { needle = lang.escapeRegExp(needle); if (options.wholeWord) - needle = "\\b" + needle + "\\b"; + needle = addWordBoundary(needle, options); var modifier = options.caseSensitive ? "gm" : "gmi"; @@ -10622,6 +10625,15 @@ var Search = function() { }).call(Search.prototype); +function addWordBoundary(needle, options) { + function wordBoundary(c) { + if (/\w/.test(c) || options.regExp) return "\\b"; + return ""; + } + return wordBoundary(needle[0]) + needle + + wordBoundary(needle[needle.length - 1]); +} + exports.Search = Search; }); @@ -10976,7 +10988,7 @@ exports.commands = [{ readOnly: true }, { name: "goToNextError", - bindKey: bindKey("Alt-E", "Ctrl-E"), + bindKey: bindKey("Alt-E", "F4"), exec: function(editor) { config.loadModule("ace/ext/error_marker", function(module) { module.showErrorMarker(editor, 1); @@ -10986,7 +10998,7 @@ exports.commands = [{ readOnly: true }, { name: "goToPreviousError", - bindKey: bindKey("Alt-Shift-E", "Ctrl-Shift-E"), + bindKey: bindKey("Alt-Shift-E", "Shift-F4"), exec: function(editor) { config.loadModule("ace/ext/error_marker", function(module) { module.showErrorMarker(editor, -1); @@ -11111,7 +11123,7 @@ exports.commands = [{ readOnly: true }, { name: "selecttostart", - bindKey: bindKey("Ctrl-Shift-Home", "Command-Shift-Up"), + bindKey: bindKey("Ctrl-Shift-Home", "Command-Shift-Home|Command-Shift-Up"), exec: function(editor) { editor.getSelection().selectFileStart(); }, multiSelectAction: "forEach", readOnly: true, @@ -11127,7 +11139,7 @@ exports.commands = [{ aceCommandGroup: "fileJump" }, { name: "selectup", - bindKey: bindKey("Shift-Up", "Shift-Up"), + bindKey: bindKey("Shift-Up", "Shift-Up|Ctrl-Shift-P"), exec: function(editor) { editor.getSelection().selectUp(); }, multiSelectAction: "forEach", scrollIntoView: "cursor", @@ -11141,7 +11153,7 @@ exports.commands = [{ readOnly: true }, { name: "selecttoend", - bindKey: bindKey("Ctrl-Shift-End", "Command-Shift-Down"), + bindKey: bindKey("Ctrl-Shift-End", "Command-Shift-End|Command-Shift-Down"), exec: function(editor) { editor.getSelection().selectFileEnd(); }, multiSelectAction: "forEach", readOnly: true, @@ -11157,7 +11169,7 @@ exports.commands = [{ aceCommandGroup: "fileJump" }, { name: "selectdown", - bindKey: bindKey("Shift-Down", "Shift-Down"), + bindKey: bindKey("Shift-Down", "Shift-Down|Ctrl-Shift-N"), exec: function(editor) { editor.getSelection().selectDown(); }, multiSelectAction: "forEach", scrollIntoView: "cursor", @@ -11185,7 +11197,7 @@ exports.commands = [{ readOnly: true }, { name: "selecttolinestart", - bindKey: bindKey("Alt-Shift-Left", "Command-Shift-Left"), + bindKey: bindKey("Alt-Shift-Left", "Command-Shift-Left|Ctrl-Shift-A"), exec: function(editor) { editor.getSelection().selectLineStart(); }, multiSelectAction: "forEach", scrollIntoView: "cursor", @@ -11199,7 +11211,7 @@ exports.commands = [{ readOnly: true }, { name: "selectleft", - bindKey: bindKey("Shift-Left", "Shift-Left"), + bindKey: bindKey("Shift-Left", "Shift-Left|Ctrl-Shift-B"), exec: function(editor) { editor.getSelection().selectLeft(); }, multiSelectAction: "forEach", scrollIntoView: "cursor", @@ -11227,7 +11239,7 @@ exports.commands = [{ readOnly: true }, { name: "selecttolineend", - bindKey: bindKey("Alt-Shift-Right", "Command-Shift-Right"), + bindKey: bindKey("Alt-Shift-Right", "Command-Shift-Right|Shift-End|Ctrl-Shift-E"), exec: function(editor) { editor.getSelection().selectLineEnd(); }, multiSelectAction: "forEach", scrollIntoView: "cursor", @@ -12091,7 +12103,8 @@ var Editor = function(renderer, session) { var row = iterator.getCurrentTokenRow(); var column = iterator.getCurrentTokenColumn(); var range = new Range(row, column, row, column+token.value.length); - if (session.$tagHighlight && range.compareRange(session.$backMarkers[session.$tagHighlight].range)!==0) { + var sbm = session.$backMarkers[session.$tagHighlight]; + if (session.$tagHighlight && sbm != undefined && range.compareRange(sbm.range) !== 0) { session.removeMarker(session.$tagHighlight); session.$tagHighlight = null; } @@ -19039,7 +19052,7 @@ exports.createEditSession = function(text, mode) { } exports.EditSession = EditSession; exports.UndoManager = UndoManager; -exports.version = "1.2.5"; +exports.version = "1.2.6"; }); (function() { window.require(["ace/ace"], function(a) { diff --git a/tests/end2end/data/hints/html/target_blank_js.html b/tests/end2end/data/hints/html/target_blank_js.html new file mode 100644 index 000000000..277d17f66 --- /dev/null +++ b/tests/end2end/data/hints/html/target_blank_js.html @@ -0,0 +1,28 @@ + + + + + + + + + + Link where we insert target=_blank via JS + + + + Follow me! + + diff --git a/tests/end2end/data/hints/input.html b/tests/end2end/data/hints/input.html index 9c93da15e..1e027ab1c 100644 --- a/tests/end2end/data/hints/input.html +++ b/tests/end2end/data/hints/input.html @@ -4,8 +4,21 @@ Simple input + - +
+ With padding: +
+ With existing text (logs to JS):: +
diff --git a/tests/end2end/data/hints/invisible.html b/tests/end2end/data/hints/invisible.html new file mode 100644 index 000000000..b0bfa9dd9 --- /dev/null +++ b/tests/end2end/data/hints/invisible.html @@ -0,0 +1,14 @@ + + + + + + Invisible links + + +

None of those invisible links should get a hint.

+ visibility: hidden + display: none + opacity: 0 + + diff --git a/tests/end2end/data/misc/jseval_file.js b/tests/end2end/data/misc/jseval_file.js new file mode 100644 index 000000000..5f6464ef3 --- /dev/null +++ b/tests/end2end/data/misc/jseval_file.js @@ -0,0 +1,2 @@ +console.log("Hello from JS!") +console.log("Hello again from JS!") diff --git a/tests/end2end/data/navigate/rel.html b/tests/end2end/data/navigate/rel.html new file mode 100644 index 000000000..48a5895b8 --- /dev/null +++ b/tests/end2end/data/navigate/rel.html @@ -0,0 +1,12 @@ + + + + + Navigate + + +

Index page

+ + + + diff --git a/tests/end2end/data/navigate/rel_nofollow.html b/tests/end2end/data/navigate/rel_nofollow.html new file mode 100644 index 000000000..d6f7fd3e8 --- /dev/null +++ b/tests/end2end/data/navigate/rel_nofollow.html @@ -0,0 +1,12 @@ + + + + + Navigate + + +

Index page

+ bla + blub + + diff --git a/tests/end2end/data/scroll/simple.html b/tests/end2end/data/scroll/simple.html index 2b1412ea4..7da9df101 100644 --- a/tests/end2end/data/scroll/simple.html +++ b/tests/end2end/data/scroll/simple.html @@ -6,6 +6,7 @@ Just a link +
 0
 1
diff --git a/tests/end2end/features/backforward.feature b/tests/end2end/features/backforward.feature
index 7c81af050..8f970837b 100644
--- a/tests/end2end/features/backforward.feature
+++ b/tests/end2end/features/backforward.feature
@@ -19,7 +19,7 @@ Feature: Going back and forward.
                 - active: true
                   url: http://localhost:*/data/backforward/2.txt
 
-    # https://travis-ci.org/The-Compiler/qutebrowser/jobs/157941720
+    # https://travis-ci.org/qutebrowser/qutebrowser/jobs/157941720
     @qtwebengine_flaky
     Scenario: Going back in a new tab
         Given I open data/backforward/1.txt
diff --git a/tests/end2end/features/completion.feature b/tests/end2end/features/completion.feature
index 4fec2d81b..87db94e8d 100644
--- a/tests/end2end/features/completion.feature
+++ b/tests/end2end/features/completion.feature
@@ -1,4 +1,4 @@
-Feature: Command bar completion
+Feature: Using completion
 
     Scenario: No warnings when completing with one entry (#1600)
         Given I open about:blank
@@ -27,3 +27,90 @@ Feature: Command bar completion
         Given I open about:blank
         When I run :set-cmd-text -s :🌀
         Then no crash should happen
+
+    Scenario: Using command completion
+        When I run :set-cmd-text :
+        Then the completion model should be CommandCompletionModel
+
+    Scenario: Using help completion
+        When I run :set-cmd-text -s :help
+        Then the completion model should be HelpCompletionModel
+
+    Scenario: Using quickmark completion
+        When I run :set-cmd-text -s :quickmark-load
+        Then the completion model should be QuickmarkCompletionModel
+
+    Scenario: Using bookmark completion
+        When I run :set-cmd-text -s :bookmark-load
+        Then the completion model should be BookmarkCompletionModel
+
+    Scenario: Using bind completion
+        When I run :set-cmd-text -s :bind X
+        Then the completion model should be BindCompletionModel
+
+    Scenario: Using session completion
+        Given I open data/hello.txt
+        And I run :session-save hello
+        When I run :set-cmd-text -s :session-load
+        And I run :completion-item-focus next
+        And I run :completion-item-focus next
+        And I run :session-delete hello
+        And I run :command-accept
+        Then the error "Session hello not found!" should be shown
+
+    Scenario: Using option completion
+        When I run :set-cmd-text -s :set colors
+        Then the completion model should be SettingOptionCompletionModel
+
+    Scenario: Using value completion
+        When I run :set-cmd-text -s :set colors statusbar.bg
+        Then the completion model should be SettingValueCompletionModel
+
+    Scenario: Updating the completion in realtime
+        Given I have a fresh instance
+        And I set completion -> quick-complete to false
+        When I open data/hello.txt
+        And I run :set-cmd-text -s :buffer
+        And I run :completion-item-focus next
+        And I open data/hello2.txt in a new background tab
+        And I run :completion-item-focus next
+        And I open data/hello3.txt in a new background tab
+        And I run :completion-item-focus next
+        And I run :command-accept
+        Then the following tabs should be open:
+            - data/hello.txt
+            - data/hello2.txt
+            - data/hello3.txt (active)
+
+    Scenario: Updating the value completion in realtime
+        Given I set colors -> statusbar.bg to green
+        When I run :set-cmd-text -s :set colors statusbar.bg
+        And I set colors -> statusbar.bg to yellow
+        And I run :completion-item-focus next
+        And I run :completion-item-focus next
+        And I set colors -> statusbar.bg to red
+        And I run :command-accept
+        Then colors -> statusbar.bg should be yellow
+
+    Scenario: Deleting an open tab via the completion
+        Given I have a fresh instance
+        When I open data/hello.txt
+        And I open data/hello2.txt in a new tab
+        And I run :set-cmd-text -s :buffer
+        And I run :completion-item-focus next
+        And I run :completion-item-focus next
+        And I run :completion-item-del
+        Then the following tabs should be open:
+            - data/hello.txt (active)
+
+    Scenario: Go to tab after moving a tab
+        Given I have a fresh instance
+        When I open data/hello.txt
+        And I open data/hello2.txt in a new tab
+        # Tricking completer into not updating tabs
+        And I run :set-cmd-text -s :buffer
+        And I run :tab-move 1
+        And I run :buffer hello2.txt
+        Then the following tabs should be open:
+            - data/hello2.txt (active)
+            - data/hello.txt
diff --git a/tests/end2end/features/conftest.py b/tests/end2end/features/conftest.py
index 517d77144..f93ac901f 100644
--- a/tests/end2end/features/conftest.py
+++ b/tests/end2end/features/conftest.py
@@ -181,11 +181,13 @@ def open_path(quteproc, path):
     "... as a URL", it's opened according to new-instance-open-target.
     """
     new_tab = False
+    new_bg_tab = False
     new_window = False
     as_url = False
     wait = True
 
     new_tab_suffix = ' in a new tab'
+    new_bg_tab_suffix = ' in a new background tab'
     new_window_suffix = ' in a new window'
     do_not_wait_suffix = ' without waiting'
     as_url_suffix = ' as a URL'
@@ -194,6 +196,9 @@ def open_path(quteproc, path):
         if path.endswith(new_tab_suffix):
             path = path[:-len(new_tab_suffix)]
             new_tab = True
+        elif path.endswith(new_bg_tab_suffix):
+            path = path[:-len(new_bg_tab_suffix)]
+            new_bg_tab = True
         elif path.endswith(new_window_suffix):
             path = path[:-len(new_window_suffix)]
             new_window = True
@@ -206,8 +211,8 @@ def open_path(quteproc, path):
         else:
             break
 
-    quteproc.open_path(path, new_tab=new_tab, new_window=new_window,
-                       as_url=as_url, wait=wait)
+    quteproc.open_path(path, new_tab=new_tab, new_bg_tab=new_bg_tab,
+                       new_window=new_window, as_url=as_url, wait=wait)
 
 
 @bdd.when(bdd.parsers.parse("I set {sect} -> {opt} to {value}"))
@@ -531,6 +536,9 @@ def check_open_tabs(quteproc, request, tabs):
     assert len(session['windows']) == 1
     assert len(session['windows'][0]['tabs']) == len(tabs)
 
+    # If we don't have (active) anywhere, don't check it
+    has_active = any(line.endswith(active_suffix) for line in tabs)
+
     for i, line in enumerate(tabs):
         line = line.strip()
         assert line.startswith('- ')
@@ -546,7 +554,7 @@ def check_open_tabs(quteproc, request, tabs):
         assert session_tab['history'][-1]['url'] == quteproc.path_to_url(path)
         if active:
             assert session_tab['active']
-        else:
+        elif has_active:
             assert 'active' not in session_tab
 
 
@@ -595,3 +603,9 @@ def check_not_scrolled(request, quteproc):
     x, y = _get_scroll_values(quteproc)
     assert x == 0
     assert y == 0
+
+
+@bdd.then(bdd.parsers.parse("{section} -> {option} should be {value}"))
+def check_option(quteproc, section, option, value):
+    actual_value = quteproc.get_setting(section, option)
+    assert actual_value == value
diff --git a/tests/end2end/features/downloads.feature b/tests/end2end/features/downloads.feature
index 14757c87a..b55673344 100644
--- a/tests/end2end/features/downloads.feature
+++ b/tests/end2end/features/downloads.feature
@@ -100,7 +100,7 @@ Feature: Downloading things from a website.
         And I run :close
         Then qutebrowser should quit
 
-    # https://github.com/The-Compiler/qutebrowser/issues/2134
+    # https://github.com/qutebrowser/qutebrowser/issues/2134
     @qtwebengine_skip
     Scenario: Downloading, then closing a tab
         When I set storage -> prompt-download-directory to false
@@ -111,7 +111,20 @@ Feature: Downloading things from a website.
         And I wait for "fetch: * -> drip" in the log
         And I run :tab-close
         And I wait for "Download drip finished" in the log
-        Then the downloaded file drip should contain 128 bytes
+        Then the downloaded file drip should be 128 bytes big
+
+    Scenario: Downloading a file with spaces
+        When I open data/downloads/download with spaces.bin without waiting
+        And I wait until the download is finished
+        Then the downloaded file download with spaces.bin should exist
+
+    @qtwebkit_skip
+    Scenario: Downloading a file with evil content-disposition header
+        # Content-Disposition: download; filename=..%2Ffoo
+        When I open response-headers?Content-Disposition=download;%20filename%3D..%252Ffoo without waiting
+        And I wait until the download is finished
+        Then the downloaded file ../foo should not exist
+        And the downloaded file foo should exist
 
     ## :download-retry
 
@@ -179,40 +192,47 @@ Feature: Downloading things from a website.
         Then the error "Can only download the current page as mhtml." should be shown
 
     Scenario: :download with a directory which doesn't exist
-        When I run :download --dest (tmpdir)/somedir/filename http://localhost:(port)/
+        When I run :download --dest (tmpdir)/downloads/somedir/filename http://localhost:(port)/
         Then the error "Download error: No such file or directory" should be shown
 
     ## mhtml downloads
 
-    @qtwebengine_todo: :download --mhtml is not implemented yet
     Scenario: Downloading as mhtml is available
-        When I open html
+        When I open data/title.html
         And I run :download --mhtml
         And I wait for "File successfully written." in the log
-        Then no crash should happen
+        Then the downloaded file Test title.mhtml should exist
 
-    @qtwebengine_todo: :download --mhtml is not implemented yet
+    @qtwebengine_skip: QtWebEngine refuses to load this
     Scenario: Downloading as mhtml with non-ASCII headers
         When I open response-headers?Content-Type=text%2Fpl%C3%A4in
-        And I run :download --mhtml --dest mhtml-response-headers.mht
+        And I run :download --mhtml --dest mhtml-response-headers.mhtml
         And I wait for "File successfully written." in the log
-        Then no crash should happen
+        Then the downloaded file mhtml-response-headers.mhtml should exist
 
-    @qtwebengine_todo: :download --mhtml is not implemented yet
+    @qtwebengine_skip: https://github.com/qutebrowser/qutebrowser/issues/2288
     Scenario: Overwriting existing mhtml file
         When I set storage -> prompt-download-directory to true
-        And I open html
+        And I open data/title.html
         And I run :download --mhtml
-        And I wait for "Asking question  text='Please enter a location for http://localhost:*/html' title='Save file to:'>, *" in the log
+        And I wait for "Asking question  text='Please enter a location for http://localhost:*/data/title.html' title='Save file to:'>, *" in the log
         And I run :prompt-accept
         And I wait for "File successfully written." in the log
         And I run :download --mhtml
-        And I wait for "Asking question  text='Please enter a location for http://localhost:*/html' title='Save file to:'>, *" in the log
+        And I wait for "Asking question  text='Please enter a location for http://localhost:*/data/title.html' title='Save file to:'>, *" in the log
         And I run :prompt-accept
         And I wait for "Asking question  text='* already exists. Overwrite?' title='Overwrite existing file?'>, *" in the log
         And I run :prompt-accept yes
         And I wait for "File successfully written." in the log
-        Then no crash should happen
+        Then the downloaded file Test title.mhtml should exist
+
+    Scenario: Opening a mhtml download directly
+        When I set storage -> prompt-download-directory to true
+        And I open html
+        And I run :download --mhtml
+        And I wait for the download prompt for "*"
+        And I directly open the download
+        Then "Opening *.mhtml* with [*python*]" should be logged
 
     ## :download-cancel
 
@@ -248,7 +268,7 @@ Feature: Downloading things from a website.
         Then "cancelled" should be logged
         And "cancelled" should be logged
 
-    # https://github.com/The-Compiler/qutebrowser/issues/1535
+    # https://github.com/qutebrowser/qutebrowser/issues/1535
     @qtwebengine_todo: :download --mhtml is not implemented yet
     Scenario: Cancelling an MHTML download (issue 1535)
         When I open data/downloads/issue1535.html
@@ -332,6 +352,20 @@ Feature: Downloading things from a website.
         And I open the download with a placeholder
         Then "Opening *download.bin* with [*python*]" should be logged
 
+    Scenario: Opening a download with default-open-dispatcher set
+        When I set a test python default-open-dispatcher
+        And I open data/downloads/download.bin without waiting
+        And I wait until the download is finished
+        And I run :download-open
+        Then "Opening *download.bin* with [*python*]" should be logged
+
+    Scenario: Opening a download with default-open-dispatcher set and override
+        When I set general -> default-open-dispatcher to cat
+        And I open data/downloads/download.bin without waiting
+        And I wait until the download is finished
+        And I open the download
+        Then "Opening *download.bin* with [*python*]" should be logged
+
     Scenario: Opening a download which does not exist
         When I run :download-open with count 42
         Then the error "There's no download 42!" should be shown
@@ -356,7 +390,7 @@ Feature: Downloading things from a website.
         And I wait until the download is finished
         Then "Opening *download.bin* with [*python*]" should be logged
 
-    # https://github.com/The-Compiler/qutebrowser/issues/1728
+    # https://github.com/qutebrowser/qutebrowser/issues/1728
 
     Scenario: Cancelling a download that should be opened
         When I set storage -> prompt-download-directory to true
@@ -366,7 +400,7 @@ Feature: Downloading things from a website.
         And I run :download-cancel
         Then "* finished but not successful, not opening!" should be logged
 
-    # https://github.com/The-Compiler/qutebrowser/issues/1725
+    # https://github.com/qutebrowser/qutebrowser/issues/1725
 
     Scenario: Directly open a download with a very long filename
         When I set storage -> prompt-download-directory to true
@@ -383,7 +417,7 @@ Feature: Downloading things from a website.
         When I set storage -> prompt-download-directory to true
         And I set completion -> download-path-suggestion to path
         And I open data/downloads/download.bin without waiting
-        Then the download prompt should be shown with "(tmpdir)/"
+        Then the download prompt should be shown with "(tmpdir)/downloads/"
 
     Scenario: completion -> download-path-suggestion = filename
         When I set storage -> prompt-download-directory to true
@@ -395,7 +429,7 @@ Feature: Downloading things from a website.
         When I set storage -> prompt-download-directory to true
         And I set completion -> download-path-suggestion to both
         And I open data/downloads/download.bin without waiting
-        Then the download prompt should be shown with "(tmpdir)/download.bin"
+        Then the download prompt should be shown with "(tmpdir)/downloads/download.bin"
 
     ## storage -> remember-download-directory
 
@@ -405,19 +439,34 @@ Feature: Downloading things from a website.
         And I set storage -> remember-download-directory to true
         And I open data/downloads/download.bin without waiting
         And I wait for the download prompt for "*/download.bin"
-        And I run :prompt-accept (tmpdir)(dirsep)subdir
+        And I run :prompt-accept (tmpdir)(dirsep)downloads(dirsep)subdir
         And I open data/downloads/download2.bin without waiting
-        Then the download prompt should be shown with "(tmpdir)/subdir/download2.bin"
+        Then the download prompt should be shown with "(tmpdir)/downloads/subdir/download2.bin"
 
     Scenario: Not remembering the last download directory
         When I set storage -> prompt-download-directory to true
         And I set completion -> download-path-suggestion to both
         And I set storage -> remember-download-directory to false
         And I open data/downloads/download.bin without waiting
-        And I wait for the download prompt for "(tmpdir)/download.bin"
-        And I run :prompt-accept (tmpdir)(dirsep)subdir
+        And I wait for the download prompt for "(tmpdir)/downloads/download.bin"
+        And I run :prompt-accept (tmpdir)(dirsep)downloads(dirsep)subdir
         And I open data/downloads/download2.bin without waiting
-        Then the download prompt should be shown with "(tmpdir)/download2.bin"
+        Then the download prompt should be shown with "(tmpdir)/downloads/download2.bin"
+
+    # https://github.com/qutebrowser/qutebrowser/issues/2173
+
+    Scenario: Remembering the temporary download directory (issue 2173)
+        When I set storage -> prompt-download-directory to true
+        And I set completion -> download-path-suggestion to both
+        And I set storage -> remember-download-directory to true
+        And I open data/downloads/download.bin without waiting
+        And I wait for the download prompt for "*"
+        And I run :prompt-accept (tmpdir)(dirsep)downloads
+        And I open data/downloads/download.bin without waiting
+        And I wait for the download prompt for "*"
+        And I directly open the download
+        And I open data/downloads/download.bin without waiting
+        Then the download prompt should be shown with "(tmpdir)/downloads/download.bin"
 
     # Overwriting files
 
@@ -428,7 +477,7 @@ Feature: Downloading things from a website.
         And I run :download http://localhost:(port)/data/downloads/download2.bin --dest download.bin
         And I wait for "Entering mode KeyMode.yesno *" in the log
         And I run :prompt-accept no
-        Then the downloaded file download.bin should contain 1 bytes
+        Then the downloaded file download.bin should be 1 bytes big
 
     Scenario: Overwriting an existing file
         When I set storage -> prompt-download-directory to false
@@ -438,7 +487,7 @@ Feature: Downloading things from a website.
         And I wait for "Entering mode KeyMode.yesno *" in the log
         And I run :prompt-accept yes
         And I wait until the download is finished
-        Then the downloaded file download.bin should contain 2 bytes
+        Then the downloaded file download.bin should be 2 bytes big
 
     @linux
     Scenario: Not overwriting a special file
@@ -486,14 +535,14 @@ Feature: Downloading things from a website.
     @posix
     Scenario: Downloading to unwritable destination
         When I set storage -> prompt-download-directory to false
-        And I run :download http://localhost:(port)/data/downloads/download.bin --dest (tmpdir)/unwritable
+        And I run :download http://localhost:(port)/data/downloads/download.bin --dest (tmpdir)/downloads/unwritable
         Then the error "Download error: Permission denied" should be shown
 
     Scenario: Downloading 20MB file
         When I set storage -> prompt-download-directory to false
         And I run :download http://localhost:(port)/custom/twenty-mb
         And I wait until the download is finished
-        Then the downloaded file twenty-mb should contain 20971520 bytes
+        Then the downloaded file twenty-mb should be 20971520 bytes big
 
     Scenario: Downloading 20MB file with late prompt confirmation
         When I set storage -> prompt-download-directory to true
@@ -501,7 +550,7 @@ Feature: Downloading things from a website.
         And I wait 1s
         And I run :prompt-accept
         And I wait until the download is finished
-        Then the downloaded file twenty-mb should contain 20971520 bytes
+        Then the downloaded file twenty-mb should be 20971520 bytes big
 
     Scenario: Downloading invalid URL
         When I set storage -> prompt-download-directory to false
@@ -509,7 +558,7 @@ Feature: Downloading things from a website.
         And I run :download foo!
         Then the error "Invalid URL" should be shown
 
-    @qtwebengine_todo: pdfjs is not implemented yet
+    @qtwebengine_todo: pdfjs is not implemented yet @qtwebkit_ng_xfail: https://github.com/annulen/webkit/issues/428
     Scenario: Downloading via pdfjs
         Given pdfjs is available
         When I set storage -> prompt-download-directory to false
@@ -537,3 +586,20 @@ Feature: Downloading things from a website.
        And I open stream-bytes/1024 without waiting
        And I wait until the download is finished
        Then the downloaded file 1024 should exist
+
+    @qtwebengine_skip: We can't get the UA from the page there
+    Scenario: user-agent when using :download
+        When I open user-agent
+        And I run :download
+        And I wait until the download is finished
+        Then the downloaded file user-agent should contain Safari/
+
+    @qtwebengine_skip: We can't get the UA from the page there
+    Scenario: user-agent when using hints
+        When I set hints -> mode to number
+        And I open /
+        And I run :hint links download
+        And I press the keys "us"  # user-agent
+        And I run :follow-hint 0
+        And I wait until the download is finished
+        Then the downloaded file user-agent should contain Safari/
diff --git a/tests/end2end/features/editor.feature b/tests/end2end/features/editor.feature
index 861c71710..947dba0b0 100644
--- a/tests/end2end/features/editor.feature
+++ b/tests/end2end/features/editor.feature
@@ -62,7 +62,7 @@ Feature: Opening external editors
         When I set up a fake editor returning "foobar"
         And I open data/editor.html
         And I run :click-element id qute-textarea
-        And I wait for "Clicked editable element!" in the log
+        And I wait for "Entering mode KeyMode.insert (reason: clicking input)" in the log
         And I run :open-editor
         And I wait for "Read back: foobar" in the log
         And I run :click-element id qute-button
@@ -72,7 +72,7 @@ Feature: Opening external editors
         When I set up a fake editor returning "foobar"
         And I open data/editor.html
         And I run :click-element id qute-textarea
-        And I wait for "Clicked editable element!" in the log
+        And I wait for "Entering mode KeyMode.insert (reason: clicking input)" in the log
         And I run :leave-mode
         And I wait for "Leaving mode KeyMode.insert (reason: leave current)" in the log
         And I run :open-editor
@@ -80,11 +80,12 @@ Feature: Opening external editors
         And I run :click-element id qute-button
         Then the javascript message "text: foobar" should be logged
 
+    @qtwebengine_todo: Caret mode is not implemented yet
     Scenario: Spawning an editor in caret mode
         When I set up a fake editor returning "foobar"
         And I open data/editor.html
         And I run :click-element id qute-textarea
-        And I wait for "Clicked editable element!" in the log
+        And I wait for "Entering mode KeyMode.insert (reason: clicking input)" in the log
         And I run :leave-mode
         And I wait for "Leaving mode KeyMode.insert (reason: leave current)" in the log
         And I run :enter-mode caret
@@ -98,7 +99,7 @@ Feature: Opening external editors
         When I set up a fake editor replacing "foo" by "bar"
         And I open data/editor.html
         And I run :click-element id qute-textarea
-        And I wait for "Clicked editable element!" in the log
+        And I wait for "Entering mode KeyMode.insert (reason: clicking input)" in the log
         And I run :insert-text foo
         And I wait for "Inserting text into element *" in the log
         And I run :open-editor
diff --git a/tests/end2end/features/hints.feature b/tests/end2end/features/hints.feature
index b6db8b47a..5fac84402 100644
--- a/tests/end2end/features/hints.feature
+++ b/tests/end2end/features/hints.feature
@@ -1,5 +1,9 @@
 Feature: Using hints
 
+    # https://bugreports.qt.io/browse/QTBUG-58381
+    Background:
+        Given I clean up open tabs
+
     Scenario: Using :follow-hint outside of hint mode (issue 1105)
         When I run :follow-hint
         Then the error "follow-hint: This command is only allowed in hint mode, not normal." should be shown
@@ -9,17 +13,6 @@ Feature: Using hints
         And I hint with args "links normal" and follow xyz
         Then the error "No hint xyz!" should be shown
 
-    # https://travis-ci.org/The-Compiler/qutebrowser/jobs/159412291
-    @qtwebengine_flaky
-    Scenario: Following a link after scrolling down
-        When I open data/scroll/simple.html
-        And I run :hint links normal
-        And I wait for "hints: *" in the log
-        And I run :scroll-page 0 1
-        And I wait until the scroll position changed
-        And I run :follow-hint a
-        Then the error "Element position is out of view!" should be shown
-
     ### Opening in current or new tab
 
     Scenario: Following a hint and force to open in current tab.
@@ -29,18 +22,16 @@ Feature: Using hints
         Then the following tabs should be open:
             - data/hello.txt (active)
 
-    @qtwebengine_skip: Opens in background
     Scenario: Following a hint and allow to open in new tab.
         When I open data/hints/link_blank.html
         And I hint with args "links normal" and follow a
         And I wait until data/hello.txt is loaded
         Then the following tabs should be open:
             - data/hints/link_blank.html
-            - data/hello.txt (active)
+            - data/hello.txt
 
     Scenario: Following a hint to link with sub-element and force to open in current tab.
         When I open data/hints/link_span.html
-        And I run :tab-close
         And I hint with args "links current" and follow a
         And I wait until data/hello.txt is loaded
         Then the following tabs should be open:
@@ -90,6 +81,11 @@ Feature: Using hints
         And I hint with args "all userscript (testdata)/userscripts/echo_hint_text" and follow a
         Then the message "Follow me!" should be shown
 
+    Scenario: Using :hint userscript with a script which doesn't exist
+        When I open data/hints/html/simple.html
+        And I hint with args "all userscript (testdata)/does_not_exist" and follow a
+        Then the error "Userscript '*' not found" should be shown
+
     Scenario: Yanking to clipboard
         When I run :debug-set-fake-clipboard
         And I open data/hints/html/simple.html
@@ -110,6 +106,12 @@ Feature: Using hints
         And I hint with args "links yank-primary" and follow a
         Then the clipboard should contain "http://localhost:(port)/data/hello.txt"
 
+    Scenario: Yanking email address to clipboard
+        When I run :debug-set-fake-clipboard
+        And I open data/email_address.html
+        And I hint with args "links yank" and follow a
+        Then the clipboard should contain "nobody"
+
     Scenario: Rapid hinting
         When I open data/hints/rapid.html in a new tab
         And I run :tab-only
@@ -151,10 +153,24 @@ Feature: Using hints
         And I hint with args "all" and follow a
         Then the error "Invalid link clicked - *" should be shown
 
+    Scenario: Clicking an invalid link opening in a new tab
+        When I open data/invalid_link.html
+        And I hint with args "all tab" and follow a
+        Then the error "Invalid link clicked - *" should be shown
+
     Scenario: Hinting inputs without type
         When I open data/hints/input.html
         And I hint with args "inputs" and follow a
-        And I wait for "Entering mode KeyMode.insert (reason: click)" in the log
+        And I wait for "Entering mode KeyMode.insert (reason: clicking input)" in the log
+        And I run :leave-mode
+        # The actual check is already done above
+        Then no crash should happen
+
+    # https://github.com/qutebrowser/qutebrowser/issues/1613
+    Scenario: Hinting inputs with padding
+        When I open data/hints/input.html
+        And I hint with args "inputs" and follow s
+        And I wait for "Entering mode KeyMode.insert (reason: clicking input)" in the log
         And I run :leave-mode
         # The actual check is already done above
         Then no crash should happen
@@ -162,11 +178,24 @@ Feature: Using hints
     Scenario: Hinting with ACE editor
         When I open data/hints/ace/ace.html
         And I hint with args "inputs" and follow a
-        And I wait for "Entering mode KeyMode.insert (reason: click)" in the log
+        And I wait for "Entering mode KeyMode.insert (reason: clicking input)" in the log
         And I run :leave-mode
         # The actual check is already done above
         Then no crash should happen
 
+    Scenario: Hinting invisible elements
+        When I open data/hints/invisible.html
+        And I run :hint
+        Then the error "No elements found." should be shown
+
+    Scenario: Clicking input with existing text
+        When I set general -> log-javascript-console to info
+        And I open data/hints/input.html
+        And I run :click-element id qute-input-existing
+        And I wait for "Entering mode KeyMode.insert *" in the log
+        And I run :fake-key new
+        Then the javascript message "contents: existingnew" should be logged
+
     ### iframes
 
     @qtwebengine_todo: Hinting in iframes is not implemented yet
@@ -175,7 +204,7 @@ Feature: Using hints
         And I hint with args "links normal" and follow a
         Then "navigation request: url http://localhost:*/data/hello.txt, type NavigationTypeLinkClicked, *" should be logged
 
-    ### FIXME currenly skipped, see https://github.com/The-Compiler/qutebrowser/issues/1525
+    ### FIXME currenly skipped, see https://github.com/qutebrowser/qutebrowser/issues/1525
     @xfail_norun
     Scenario: Using :follow-hint inside a scrolled iframe
         When I open data/hints/iframe_scroll.html
@@ -253,7 +282,7 @@ Feature: Using hints
 
     ### Number hint mode
 
-    # https://github.com/The-Compiler/qutebrowser/issues/308
+    # https://github.com/qutebrowser/qutebrowser/issues/308
     Scenario: Renumbering hints when filtering
         When I open data/hints/number.html
         And I set hints -> mode to number
@@ -262,7 +291,7 @@ Feature: Using hints
         And I run :follow-hint 1
         Then data/numbers/7.txt should be loaded
 
-    # https://github.com/The-Compiler/qutebrowser/issues/576
+    # https://github.com/qutebrowser/qutebrowser/issues/576
     @qtwebengine_flaky
     Scenario: Keeping hint filter in rapid mode
         When I open data/hints/number.html
@@ -274,7 +303,7 @@ Feature: Using hints
         Then data/numbers/2.txt should be loaded
         And data/numbers/3.txt should be loaded
 
-    # https://github.com/The-Compiler/qutebrowser/issues/1186
+    # https://github.com/qutebrowser/qutebrowser/issues/1186
     Scenario: Keeping hints filter when using backspace
         When I open data/hints/issue1186.html
         And I set hints -> mode to number
@@ -285,7 +314,7 @@ Feature: Using hints
         And I run :follow-hint 11
         Then the error "No hint 11!" should be shown
 
-    # https://github.com/The-Compiler/qutebrowser/issues/674#issuecomment-165096744
+    # https://github.com/qutebrowser/qutebrowser/issues/674#issuecomment-165096744
     Scenario: Multi-word matching
         When I open data/hints/number.html
         And I set hints -> mode to number
@@ -302,7 +331,7 @@ Feature: Using hints
         And I hint with args "all" and follow 00
         Then data/numbers/1.txt should be loaded
 
-    # https://github.com/The-Compiler/qutebrowser/issues/1559
+    # https://github.com/qutebrowser/qutebrowser/issues/1559
     Scenario: Filtering all hints in number mode
         When I open data/hints/number.html
         And I set hints -> mode to number
@@ -311,7 +340,7 @@ Feature: Using hints
         And I wait for "Leaving mode KeyMode.hint (reason: all filtered)" in the log
         Then no crash should happen
 
-    # https://github.com/The-Compiler/qutebrowser/issues/1657
+    # https://github.com/qutebrowser/qutebrowser/issues/1657
     Scenario: Using rapid number hinting twice
         When I open data/hints/number.html
         And I set hints -> mode to number
diff --git a/tests/end2end/features/history.feature b/tests/end2end/features/history.feature
index f4f57dcb7..bc7f537e6 100644
--- a/tests/end2end/features/history.feature
+++ b/tests/end2end/features/history.feature
@@ -3,7 +3,8 @@ Feature: Page history
     Make sure the global page history is saved correctly.
 
     Background:
-        Given I run :history-clear
+        Given I open about:blank
+        And I run :history-clear --force
 
     Scenario: Simple history saving
         When I open data/numbers/1.txt
@@ -49,13 +50,21 @@ Feature: Page history
             http://localhost:(port)/status/404 Error loading page: http://localhost:(port)/status/404
 
     Scenario: History with invalid URL
-        When I open data/javascript/window_open.html
+        When I run :tab-only
+        And I open data/javascript/window_open.html
         And I run :click-element id open-invalid
         Then "Changing title for idx 1 to 'about:blank'" should be logged
 
     Scenario: Clearing history
+        When I open data/title.html
+        And I run :history-clear --force
+        Then the history file should be empty
+
+    Scenario: Clearing history with confirmation
         When I open data/title.html
         And I run :history-clear
+        And I wait for "Asking question <* title='Clear all browsing history?'>, *" in the log
+        And I run :prompt-accept yes
         Then the history file should be empty
 
     Scenario: History with yanked URL and 'add to history' flag
@@ -65,9 +74,16 @@ Feature: Page history
             http://localhost:(port)/data/hints/html/simple.html Simple link
             http://localhost:(port)/data/hello.txt
 
+    Scenario: Listing history
+        When I open data/numbers/3.txt
+        And I open data/numbers/4.txt
+        And I open qute:history
+        Then the page should contain the plaintext "3.txt"
+        Then the page should contain the plaintext "4.txt"
+
     ## Bugs
 
-    @qtwebengine_skip
+    @qtwebengine_skip @qtwebkit_ng_skip
     Scenario: Opening a valid URL which turns out invalid
         When I set general -> auto-search to true
         And I run :open http://foo%40bar@baz
diff --git a/tests/end2end/features/javascript.feature b/tests/end2end/features/javascript.feature
index 338101f73..13ab0d96a 100644
--- a/tests/end2end/features/javascript.feature
+++ b/tests/end2end/features/javascript.feature
@@ -16,6 +16,7 @@ Feature: Javascript stuff
         And I run :click-element id close-normal
         Then "Focus object changed: *" should be logged
 
+    @qtwebkit_ng_skip
     Scenario: Opening/closing a modal window via JS
         When I open data/javascript/window_open.html
         And I run :tab-only
@@ -27,7 +28,7 @@ Feature: Javascript stuff
         # WebModalDialog with QtWebKit, WebDialog with QtWebEngine
         And "Web*Dialog requested, but we don't support that!" should be logged
 
-    # https://github.com/The-Compiler/qutebrowser/issues/906
+    # https://github.com/qutebrowser/qutebrowser/issues/906
 
     @qtwebengine_skip
     Scenario: Closing a JS window twice (issue 906) - qtwebkit
diff --git a/tests/end2end/features/keyinput.feature b/tests/end2end/features/keyinput.feature
index 6b671825d..6777056e8 100644
--- a/tests/end2end/features/keyinput.feature
+++ b/tests/end2end/features/keyinput.feature
@@ -255,3 +255,24 @@ Feature: Keyboard input
         And I press the key "a"
         And I wait for "hints: *" in the log
         Then no crash should happen
+
+    Scenario: Cancelling key input
+        When I run :record-macro
+        And I press the key ""
+        Then "Leaving mode KeyMode.record_macro (reason: leave current)" should be logged
+
+    Scenario: Ignoring non-register keys
+        Given I open data/scroll/simple.html
+        And I run :tab-only
+        When I run :scroll down with count 2
+        And I wait until the scroll position changed
+        And I run :record-macro
+        And I press the key ""
+        And I press the key "c"
+        And I run :scroll up
+        And I wait until the scroll position changed
+        And I run :record-macro
+        And I run :run-macro
+        And I press the key "c"
+        And I wait until the scroll position changed to 0/0
+        Then the page should not be scrolled
diff --git a/tests/end2end/features/marks.feature b/tests/end2end/features/marks.feature
index 82ded0746..e2738e23f 100644
--- a/tests/end2end/features/marks.feature
+++ b/tests/end2end/features/marks.feature
@@ -70,7 +70,7 @@ Feature: Setting positional marks
         And I run :jump-mark b
         Then the error "Mark b is not set" should be shown
 
-    @qtwebengine_todo: Does not emit loaded signal for fragments?
+    @qtwebengine_todo: Does not emit loaded signal for fragments? @flaky
     Scenario: Jumping to a local mark after changing fragments
         When I open data/marks.html#top
         And I run :scroll 'top'
diff --git a/tests/end2end/features/misc.feature b/tests/end2end/features/misc.feature
index 6d3862713..5e13e1feb 100644
--- a/tests/end2end/features/misc.feature
+++ b/tests/end2end/features/misc.feature
@@ -80,7 +80,7 @@ Feature: Various utility commands.
         And I wait for the javascript message "Hello from JS!"
         Then "Ignoring world ID 1" should be logged
 
-    @qtwebkit_skip @pyqt>=5.7.0
+    @qtwebkit_skip
     Scenario: :jseval uses separate world without --world
         When I set general -> log-javascript-console to info
         And I open data/misc/jseval.html
@@ -88,20 +88,31 @@ Feature: Various utility commands.
         Then the javascript message "Hello from the page!" should not be logged
         And the javascript message "Uncaught ReferenceError: do_log is not defined" should be logged
 
-    @qtwebkit_skip @pyqt>=5.7.0
+    @qtwebkit_skip
     Scenario: :jseval using the main world
         When I set general -> log-javascript-console to info
         And I open data/misc/jseval.html
         And I run :jseval --world 0 do_log()
         Then the javascript message "Hello from the page!" should be logged
 
-    @qtwebkit_skip @pyqt>=5.7.0
+    @qtwebkit_skip
     Scenario: :jseval using the main world as name
         When I set general -> log-javascript-console to info
         And I open data/misc/jseval.html
         And I run :jseval --world main do_log()
         Then the javascript message "Hello from the page!" should be logged
 
+    Scenario: :jseval --file using a file that exists as js-code
+        When I set general -> log-javascript-console to info
+        And I run :jseval --file (testdata)/misc/jseval_file.js
+        Then the javascript message "Hello from JS!" should be logged
+        And the javascript message "Hello again from JS!" should be logged
+
+    Scenario: :jseval --file using a file that doesn't exist as js-code
+        When I run :jseval --file nonexistentfile
+        Then the error "[Errno 2] No such file or directory: 'nonexistentfile'" should be shown
+        And "No output or error" should not be logged
+
     # :debug-webaction
 
     Scenario: :debug-webaction with valid value
@@ -130,11 +141,17 @@ Feature: Various utility commands.
 
     # :inspect
 
+    @qtwebengine_skip
     Scenario: Inspector without developer extras
         When I set general -> developer-extras to false
         And I run :inspector
         Then the error "Please enable developer-extras before using the webinspector!" should be shown
 
+    @qtwebkit_skip
+    Scenario: Inspector without --enable-webengine-inspector
+        When I run :inspector
+        Then the error "Debugging is not enabled. See 'qutebrowser --help' for details." should be shown
+
     @no_xvfb @posix @qtwebengine_skip
     Scenario: Inspector smoke test
         When I set general -> developer-extras to true
@@ -145,6 +162,7 @@ Feature: Various utility commands.
         Then no crash should happen
 
     # Different code path as an inspector got created now
+    @qtwebengine_skip
     Scenario: Inspector without developer extras (after smoke)
         When I set general -> developer-extras to false
         And I run :inspector
@@ -283,6 +301,24 @@ Feature: Various utility commands.
             - about:blank
             - qute://help/index.html (active)
 
+    # :history
+
+    Scenario: :history without arguments
+        When I run :tab-only
+        And I run :history
+        And I wait until qute://history/ is loaded
+        Then the following tabs should be open:
+            - qute://history/ (active)
+
+    Scenario: :history with -t
+        When I open about:blank
+        And I run :tab-only
+        And I run :history -t
+        And I wait until qute://history/ is loaded
+        Then the following tabs should be open:
+            - about:blank
+            - qute://history/ (active)
+
     # :home
 
     Scenario: :home with single page
@@ -297,7 +333,7 @@ Feature: Various utility commands.
 
     # pdfjs support
 
-    @qtwebengine_todo: pdfjs is not implemented yet
+    @qtwebengine_skip: pdfjs is not implemented yet
     Scenario: pdfjs is used for pdf files
         Given pdfjs is available
         When I set content -> enable-pdfjs to true
@@ -311,7 +347,7 @@ Feature: Various utility commands.
         And I open data/misc/test.pdf
         Then "Download test.pdf finished" should be logged
 
-    @qtwebengine_todo: pdfjs is not implemented yet
+    @qtwebengine_skip: pdfjs is not implemented yet @qtwebkit_ng_xfail: https://github.com/annulen/webkit/issues/428
     Scenario: Downloading a pdf via pdf.js button (issue 1214)
         Given pdfjs is available
         # WORKAROUND to prevent the "Painter ended with 2 saved states" warning
@@ -374,7 +410,7 @@ Feature: Various utility commands.
         When I run :debug-pyeval --quiet 1+1
         Then "pyeval output: 2" should be logged
 
-    ## https://github.com/The-Compiler/qutebrowser/issues/504
+    ## https://github.com/qutebrowser/qutebrowser/issues/504
 
     Scenario: Focusing download widget via Tab
         When I open about:blank
@@ -389,6 +425,7 @@ Feature: Various utility commands.
         And I wait for "Entering mode KeyMode.prompt *" in the log
         And I press the key ""
         And I press the key ""
+        And I run :leave-mode
         Then no crash should happen
 
     ## Custom headers
@@ -482,13 +519,13 @@ Feature: Various utility commands.
         Then qute://log?level=error should be loaded
         And the page should contain the plaintext "No messages to show."
 
-    ## https://github.com/The-Compiler/qutebrowser/issues/1523
+    ## https://github.com/qutebrowser/qutebrowser/issues/1523
 
     Scenario: Completing a single option argument
         When I run :set-cmd-text -s :--
         Then no crash should happen
 
-    ## https://github.com/The-Compiler/qutebrowser/issues/1386
+    ## https://github.com/qutebrowser/qutebrowser/issues/1386
 
     Scenario: Partial commandline matching with startup command
         When I run :message-i "Hello World" (invalid command)
@@ -502,9 +539,9 @@ Feature: Various utility commands.
         And I run :command-accept
         Then the message "Hello World" should be shown
 
-    ## https://github.com/The-Compiler/qutebrowser/issues/1219
+    ## https://github.com/qutebrowser/qutebrowser/issues/1219
 
-    @qtwebengine_todo: private browsing is not implemented yet
+    @qtwebengine_todo: private browsing is not implemented yet @qtwebkit_ng_skip: private browsing is not implemented yet
     Scenario: Sharing cookies with private browsing
         When I set general -> private-browsing to true
         And I open cookies/set?qute-test=42 without waiting
@@ -513,19 +550,15 @@ Feature: Various utility commands.
         And I set general -> private-browsing to false
         Then the cookie qute-test should be set to 42
 
-    ## https://github.com/The-Compiler/qutebrowser/issues/1742
+    ## https://github.com/qutebrowser/qutebrowser/issues/1742
 
-    @qtwebengine_todo: private browsing is not implemented yet
+    @qtwebengine_todo: private browsing is not implemented yet @qtwebkit_ng_xfail: private browsing is not implemented yet
     Scenario: Private browsing is activated in QtWebKit without restart
         When I set general -> private-browsing to true
         And I open data/javascript/localstorage.html
         And I set general -> private-browsing to false
         Then the page should contain the plaintext "Local storage status: not working"
 
-    Scenario: Using 0 as count
-        When I run :scroll down with count 0
-        Then the error "scroll: A zero count is not allowed for this command!" should be shown
-
     @no_xvfb
     Scenario: :window-only
         Given I run :tab-only
@@ -571,7 +604,7 @@ Feature: Various utility commands.
     Scenario: Clicking an element by ID
         When I open data/click_element.html
         And I run :click-element id qute-input
-        Then "Clicked editable element!" should be logged
+        Then "Entering mode KeyMode.insert (reason: clicking input)" should be logged
 
     Scenario: Clicking an element with tab target
         When I open data/click_element.html
@@ -582,14 +615,6 @@ Feature: Various utility commands.
             - data/click_element.html
             - data/hello.txt (active)
 
-    @qtwebengine_flaky
-    Scenario: Clicking an element which is out of view
-        When I open data/scroll/simple.html
-        And I run :scroll-page 0 1
-        And I wait until the scroll position changed
-        And I run :click-element id link
-        Then the error "Element position is out of view!" should be shown
-
     ## :command-history-{prev,next}
 
     Scenario: Calling previous command
@@ -626,7 +651,7 @@ Feature: Various utility commands.
         And I run :command-accept
         Then the error "No command given" should be shown
 
-    @qtwebengine_todo: private browsing is not implemented yet
+    @qtwebengine_todo: private browsing is not implemented yet @qtwebkit_ng_skip: private browsing is not implemented yet
     Scenario: Calling previous command with private-browsing mode
         When I run :set-cmd-text :message-info blah
         And I run :command-accept
@@ -639,3 +664,34 @@ Feature: Various utility commands.
         And I run :command-accept
         And I set general -> private-browsing to false
         Then the message "blah" should be shown
+
+    ## Modes blacklisted for :enter-mode
+
+    Scenario: Trying to enter command mode with :enter-mode
+        When I run :enter-mode command
+        Then the error "Mode command can't be entered manually!" should be shown
+
+    ## Renderer crashes
+
+    @qtwebkit_skip @no_invalid_lines
+    Scenario: Renderer crash
+        When I run :open -t chrome://crash
+        Then the error "Renderer process crashed" should be shown
+
+    @qtwebkit_skip @no_invalid_lines
+    Scenario: Renderer kill
+        When I run :open -t chrome://kill
+        Then the error "Renderer process was killed" should be shown
+
+    # https://github.com/qutebrowser/qutebrowser/issues/2290
+    @qtwebkit_skip @no_invalid_lines
+    Scenario: Navigating to URL after renderer process is gone
+        When I run :tab-only
+        And I open data/numbers/1.txt
+        And I open data/numbers/2.txt in a new tab
+        And I run :open chrome://kill
+        And I wait for "Renderer process was killed" in the log
+        And I open data/numbers/3.txt
+        Then no crash should happen
+        And the following tabs should be open:
+            - data/numbers/3.txt (active)
diff --git a/tests/end2end/features/navigate.feature b/tests/end2end/features/navigate.feature
index 956229f09..5153400a4 100644
--- a/tests/end2end/features/navigate.feature
+++ b/tests/end2end/features/navigate.feature
@@ -43,6 +43,26 @@ Feature: Using :navigate
         And I run :navigate next
         Then data/navigate/next.html should be loaded
 
+    Scenario: Navigating to previous page with rel
+        When I open data/navigate/rel.html
+        And I run :navigate prev
+        Then data/navigate/prev.html should be loaded
+
+    Scenario: Navigating to next page with rel
+        When I open data/navigate/rel.html
+        And I run :navigate next
+        Then data/navigate/next.html should be loaded
+
+    Scenario: Navigating to previous page with rel nofollow
+        When I open data/navigate/rel_nofollow.html
+        And I run :navigate prev
+        Then data/navigate/prev.html should be loaded
+
+    Scenario: Navigating to next page with rel nofollow
+        When I open data/navigate/rel_nofollow.html
+        And I run :navigate next
+        Then data/navigate/next.html should be loaded
+
     # increment/decrement
 
     Scenario: Incrementing number in URL
diff --git a/tests/end2end/features/open.feature b/tests/end2end/features/open.feature
index 3f6715e6a..89d6c9aa2 100644
--- a/tests/end2end/features/open.feature
+++ b/tests/end2end/features/open.feature
@@ -14,9 +14,10 @@ Feature: Opening pages
                 - active: true
                   url: http://localhost:*/data/numbers/1.txt
 
-    Scenario: :open without URL and no -t/-b/-w
-        When I run :open
-        Then the error "No URL given, but -t/-b/-w is not set!" should be shown
+    Scenario: :open without URL
+        When I set general -> default-page to http://localhost:(port)/data/numbers/11.txt
+        And I run :open
+        Then data/numbers/11.txt should be loaded
 
     Scenario: :open without URL and -t
         When I set general -> default-page to http://localhost:(port)/data/numbers/2.txt
diff --git a/tests/end2end/features/prompts.feature b/tests/end2end/features/prompts.feature
index 00cb93dd9..8b687abcc 100644
--- a/tests/end2end/features/prompts.feature
+++ b/tests/end2end/features/prompts.feature
@@ -213,14 +213,14 @@ Feature: Prompts
         And I run :click-element id button
         Then the javascript message "geolocation permission denied" should be logged
 
-    @ci @not_osx
+    @ci @not_osx @qt!=5.8
     Scenario: Always accepting geolocation
         When I set content -> geolocation to true
         And I open data/prompt/geolocation.html in a new tab
         And I run :click-element id button
         Then the javascript message "geolocation permission denied" should not be logged
 
-    @ci @not_osx
+    @ci @not_osx @qt!=5.8
     Scenario: geolocation with ask -> true
         When I set content -> geolocation to ask
         And I open data/prompt/geolocation.html in a new tab
@@ -353,6 +353,13 @@ Feature: Prompts
               "user": "user4"
             }
 
+    @qtwebengine_skip
+    Scenario: Cancellling webpage authentification with QtWebKit
+        When I open basic-auth/user6/password6 without waiting
+        And I wait for a prompt
+        And I run :leave-mode
+        Then basic-auth/user6/password6 should be loaded
+
     # :prompt-accept with value argument
 
     Scenario: Javascript alert with value
@@ -444,7 +451,7 @@ Feature: Prompts
         And I run :prompt-accept prompt-in-command-mode
         Then "Added quickmark prompt-in-command-mode for *" should be logged
 
-    # https://github.com/The-Compiler/qutebrowser/issues/1093
+    # https://github.com/qutebrowser/qutebrowser/issues/1093
     @qtwebengine_skip: QtWebEngine doesn't open the second page/prompt
     Scenario: Keyboard focus with multiple auth prompts
         When I open basic-auth/user5/password5 without waiting
@@ -470,8 +477,8 @@ Feature: Prompts
               "user": "user6"
             }
 
-    # https://github.com/The-Compiler/qutebrowser/issues/1249#issuecomment-175205531
-    # https://github.com/The-Compiler/qutebrowser/pull/2054#issuecomment-258285544
+    # https://github.com/qutebrowser/qutebrowser/issues/1249#issuecomment-175205531
+    # https://github.com/qutebrowser/qutebrowser/pull/2054#issuecomment-258285544
     @qtwebengine_todo: Notifications are not implemented in QtWebEngine
     Scenario: Interrupting SSL prompt during a notification prompt
         When I set content -> notifications to ask
diff --git a/tests/end2end/features/scroll.feature b/tests/end2end/features/scroll.feature
index 1de3ce30b..05f9c22e8 100644
--- a/tests/end2end/features/scroll.feature
+++ b/tests/end2end/features/scroll.feature
@@ -142,7 +142,6 @@ Feature: Scrolling
         And I wait until the scroll position changed to 0/0
         Then the page should not be scrolled
 
-    @qtwebengine_skip: Causes memory leak...
     Scenario: Scrolling down with a very big count
         When I run :scroll down with count 99999999999
         And I wait until the scroll position changed
@@ -231,7 +230,7 @@ Feature: Scrolling
         When I run :scroll-perc 0 with count 50
         Then the page should be scrolled vertically
         
-    # https://github.com/The-Compiler/qutebrowser/issues/1821
+    # https://github.com/qutebrowser/qutebrowser/issues/1821
     Scenario: :scroll-perc without doctype
         When I open data/scroll/no_doctype.html
         And I run :scroll-perc 100
diff --git a/tests/end2end/features/sessions.feature b/tests/end2end/features/sessions.feature
index af3e43f11..34a545841 100644
--- a/tests/end2end/features/sessions.feature
+++ b/tests/end2end/features/sessions.feature
@@ -194,7 +194,7 @@ Feature: Saving and loading sessions
             url: http://localhost:*/data/numbers/3.txt
             zoom: 1.0
 
-  # https://github.com/The-Compiler/qutebrowser/issues/879
+  # https://github.com/qutebrowser/qutebrowser/issues/879
 
   Scenario: Saving a session with a page using history.replaceState()
     When I open data/sessions/history_replace_state.html without waiting
@@ -263,8 +263,10 @@ Feature: Saving and loading sessions
     Then the error "No session loaded currently!" should be shown
 
   Scenario: Saving current session after one is loaded
+    When I open data/numbers/1.txt
     When I run :session-save current_session
     And I run :session-load current_session
+    And I wait until data/numbers/1.txt is loaded
     And I run :session-save --current
     Then the message "Saved session current_session." should be shown
 
@@ -278,6 +280,32 @@ Feature: Saving and loading sessions
     Then "Saved session quiet_session." should not be logged
     And the session quiet_session should exist
 
+  Scenario: Saving session with --only-active-window
+    When I open data/numbers/1.txt
+    And I open data/numbers/2.txt in a new tab
+    And I open data/numbers/3.txt in a new window
+    And I open data/numbers/4.txt in a new tab
+    And I open data/numbers/5.txt in a new tab
+    And I run :session-save --only-active-window window_session_name
+    And I run :window-only
+    And I run :tab-only
+    And I run :session-load window_session_name
+    And I wait until data/numbers/5.txt is loaded
+    Then the session should look like:
+      windows:
+        - tabs:
+            - history:
+              - active: true
+                url: http://localhost:*/data/numbers/5.txt
+        - tabs:
+            - history:
+                - url: http://localhost:*/data/numbers/3.txt
+            - history:
+                - url: http://localhost:*/data/numbers/4.txt
+            - history:
+                - active: true
+                  url: http://localhost:*/data/numbers/5.txt
+
   # :session-delete
 
   Scenario: Deleting a directory
diff --git a/tests/end2end/features/set.feature b/tests/end2end/features/set.feature
index 1b1185a5c..769605c3e 100644
--- a/tests/end2end/features/set.feature
+++ b/tests/end2end/features/set.feature
@@ -32,6 +32,26 @@ Feature: Setting settings.
         When I run :set colors statusbar.bg!
         Then the error "set: Attempted inversion of non-boolean value." should be shown
 
+    Scenario: Cycling an option
+        When I run :set colors statusbar.bg magenta
+        And I run :set colors statusbar.bg green magenta blue yellow
+        Then colors -> statusbar.bg should be blue
+
+    Scenario: Cycling an option through the end of the list
+        When I run :set colors statusbar.bg yellow
+        And I run :set colors statusbar.bg green magenta blue yellow
+        Then colors -> statusbar.bg should be green
+
+    Scenario: Cycling an option that's not on the list
+        When I run :set colors statusbar.bg red
+        And I run :set colors statusbar.bg green magenta blue yellow
+        Then colors -> statusbar.bg should be green
+
+    Scenario: Cycling through a single option
+        When I run :set colors statusbar.bg red
+        And I run :set colors statusbar.bg red
+        Then colors -> statusbar.bg should be red
+
     Scenario: Getting an option
         When I run :set colors statusbar.bg magenta
         And I run :set colors statusbar.bg?
diff --git a/tests/end2end/features/spawn.feature b/tests/end2end/features/spawn.feature
index dc12f4078..445920924 100644
--- a/tests/end2end/features/spawn.feature
+++ b/tests/end2end/features/spawn.feature
@@ -16,7 +16,7 @@ Feature: :spawn
         When I run :spawn -u /this_does_not_exist
         Then the error "Userscript '/this_does_not_exist' not found" should be shown
 
-    # https://github.com/The-Compiler/qutebrowser/issues/1614
+    # https://github.com/qutebrowser/qutebrowser/issues/1614
     @posix
     Scenario: Running :spawn with invalid quoting
         When I run :spawn ""'""
diff --git a/tests/end2end/features/tabs.feature b/tests/end2end/features/tabs.feature
index 5a7967b3a..b7150489e 100644
--- a/tests/end2end/features/tabs.feature
+++ b/tests/end2end/features/tabs.feature
@@ -255,17 +255,6 @@ Feature: Tab management
             - data/numbers/2.txt (active)
             - data/numbers/3.txt
 
-    Scenario: :tab-focus with count 0
-        When I open data/numbers/1.txt
-        And I open data/numbers/2.txt in a new tab
-        And I open data/numbers/3.txt in a new tab
-        And I run :tab-focus with count 1
-        And I run :tab-focus with count 0
-        Then the following tabs should be open:
-            - data/numbers/1.txt
-            - data/numbers/2.txt
-            - data/numbers/3.txt (active)
-
     Scenario: :tab-focus with invalid negative index
         When I open data/numbers/1.txt
         And I open data/numbers/2.txt in a new tab
@@ -615,6 +604,13 @@ Feature: Tab management
                 - url: http://localhost:*/data/title.html
                   title: Test title
 
+    # https://github.com/qutebrowser/qutebrowser/issues/2289
+    @qtwebkit_skip @qt>=5.8
+    Scenario: Cloning a tab with a special URL
+        When I open chrome://gpu
+        And I run :tab-clone
+        Then the error "Can't serialize special URL!" should be shown
+
     # :tab-detach
 
     Scenario: Detaching a tab
@@ -770,6 +766,18 @@ Feature: Tab management
             - data/numbers/2.txt
             - data/numbers/3.txt
 
+    # https://github.com/qutebrowser/qutebrowser/issues/2289
+    @qtwebkit_skip @qt>=5.8
+    Scenario: Undoing a tab with a special URL
+        Given I have a fresh instance
+        When I open data/numbers/1.txt
+        And I open chrome://gpu in a new tab
+        And I run :tab-close
+        And I run :undo
+        Then the error "Nothing to undo!" should be shown
+        And the following tabs should be open:
+            - data/numbers/1.txt (active)
+
     # last-close
 
     # FIXME:qtwebengine
@@ -928,6 +936,7 @@ Feature: Tab management
         And I run :buffer "99/1"
         Then the error "There's no window with id 99!" should be shown
 
+    @qtwebengine_flaky
     Scenario: :buffer with matching window index
         Given I have a fresh instance
         When I open data/title.html
@@ -984,6 +993,8 @@ Feature: Tab management
         And I run :buffer "1/2/3"
         Then the error "No matching tab for: 1/2/3" should be shown
 
+    # Other
+
     Scenario: Using :tab-next after closing last tab (#1448)
         When I set tabs -> last-close to close
         And I run :tab-only
@@ -998,6 +1009,21 @@ Feature: Tab management
         Then qutebrowser should quit
         And no crash should happen
 
+    Scenario: Opening link with tabs-are-windows set (#2162)
+        When I set tabs -> tabs-are-windows to true
+        And I open data/hints/html/simple.html
+        And I hint with args "all tab-fg" and follow a
+        And I wait until data/hello.txt is loaded
+        Then the session should look like:
+            windows:
+            - tabs:
+              - history:
+                - url: about:blank
+                - url: http://localhost:*/data/hints/html/simple.html
+            - tabs:
+              - history:
+                - url: http://localhost:*/data/hello.txt
+
     # :tab-pin
 
     Scenario: :tab-pin command
diff --git a/tests/end2end/features/test_completion_bdd.py b/tests/end2end/features/test_completion_bdd.py
index 4c2b43c8f..030a16ffc 100644
--- a/tests/end2end/features/test_completion_bdd.py
+++ b/tests/end2end/features/test_completion_bdd.py
@@ -1,6 +1,6 @@
 # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
 
-# Copyright 2016 Ryan Roden-Corrent (rcorre) 
+# Copyright 2015-2016 Florian Bruhin (The Compiler) 
 #
 # This file is part of qutebrowser.
 #
@@ -19,3 +19,10 @@
 
 import pytest_bdd as bdd
 bdd.scenarios('completion.feature')
+
+
+@bdd.then(bdd.parsers.parse("the completion model should be {model}"))
+def check_model(quteproc, model):
+    """Make sure the completion model was set to something."""
+    pattern = "Setting completion model to {} with pattern *".format(model)
+    quteproc.wait_for(message=pattern)
diff --git a/tests/end2end/features/test_downloads_bdd.py b/tests/end2end/features/test_downloads_bdd.py
index 65ccf1f3e..cb0388ea4 100644
--- a/tests/end2end/features/test_downloads_bdd.py
+++ b/tests/end2end/features/test_downloads_bdd.py
@@ -32,15 +32,17 @@ PROMPT_MSG = ("Asking question  {option} should be {value}"))
-def check_option(quteproc, section, option, value):
-    actual_value = quteproc.get_setting(section, option)
-    assert actual_value == value
diff --git a/tests/end2end/features/utilcmds.feature b/tests/end2end/features/utilcmds.feature
index 22acabc0b..5804dd8cd 100644
--- a/tests/end2end/features/utilcmds.feature
+++ b/tests/end2end/features/utilcmds.feature
@@ -30,24 +30,28 @@ Feature: Miscellaneous utility commands exposed to the user.
     ## :repeat
 
     Scenario: :repeat simple
-        When I run :repeat 5 scroll-px 10 0
-        And I wait until the scroll position changed to 50/0
-        # Then already covered by above And
+        When I run :repeat 2 message-info repeat-test
+        Then the message "repeat-test" should be shown
+        And the message "repeat-test" should be shown
 
     Scenario: :repeat zero times
-        When I run :repeat 0 scroll-px 10 0
-        And I wait 0.01s
-        Then the page should not be scrolled
+        When I run :repeat 0 message-error "repeat-test 2"
+        # If we have an error, the test will fail
+        Then no crash should happen
 
     ## :run-with-count
 
     Scenario: :run-with-count
-        When I run :run-with-count 2 scroll down
-        Then "command called: scroll ['down'] (count=2)" should be logged
+        When I run :run-with-count 2 message-info "run-with-count test"
+        Then the message "run-with-count test" should be shown
+        And the message "run-with-count test" should be shown
 
     Scenario: :run-with-count with count
-        When I run :run-with-count 2 scroll down with count 3
-        Then "command called: scroll ['down'] (count=6)" should be logged
+        When I run :run-with-count 2 message-info "run-with-count test 2" with count 2
+        Then the message "run-with-count test 2" should be shown
+        And the message "run-with-count test 2" should be shown
+        And the message "run-with-count test 2" should be shown
+        And the message "run-with-count test 2" should be shown
 
     ## :message-*
 
@@ -99,29 +103,24 @@ Feature: Miscellaneous utility commands exposed to the user.
     ## :repeat-command
 
     Scenario: :repeat-command
-        When I run :scroll down
+        When I run :message-info test1
         And I run :repeat-command
-        And I run :scroll up
-        Then the page should be scrolled vertically
+        Then the message "test1" should be shown
+        And the message "test1" should be shown
 
     Scenario: :repeat-command with count
-        When I run :scroll down with count 3
-        And I wait until the scroll position changed
-        And I run :scroll up
-        And I wait until the scroll position changed
+        When I run :message-info test2
         And I run :repeat-command with count 2
-        And I wait until the scroll position changed to 0/0
-        Then the page should not be scrolled
+        Then the message "test2" should be shown
+        And the message "test2" should be shown
+        And the message "test2" should be shown
 
     Scenario: :repeat-command with not-normal command inbetween
-        When I run :scroll down with count 3
-        And I wait until the scroll position changed
-        And I run :scroll up
-        And I wait until the scroll position changed
+        When I run :message-info test3
         And I run :prompt-accept
-        And I run :repeat-command with count 2
-        And I wait until the scroll position changed to 0/0
-        Then the page should not be scrolled
+        And I run :repeat-command
+        Then the message "test3" should be shown
+        And the message "test3" should be shown
         And the error "prompt-accept: This command is only allowed in prompt/yesno mode, not normal." should be shown
 
     Scenario: :repeat-command with mode-switching command
diff --git a/tests/end2end/features/yankpaste.feature b/tests/end2end/features/yankpaste.feature
index 9a4bf53a8..52c11ed22 100644
--- a/tests/end2end/features/yankpaste.feature
+++ b/tests/end2end/features/yankpaste.feature
@@ -143,7 +143,7 @@ Feature: Yanking and pasting.
         And I run :open {clipboard}
         Then the error "Invalid URL" should be shown
 
-    # https://travis-ci.org/The-Compiler/qutebrowser/jobs/157941726
+    # https://travis-ci.org/qutebrowser/qutebrowser/jobs/157941726
     @qtwebengine_flaky
     Scenario: Pasting multiple urls in a new tab
         When I put the following lines into the clipboard:
@@ -186,7 +186,7 @@ Feature: Yanking and pasting.
             - about:blank
             - data/hello.txt?q=text%3A%0Ashould%20open%0Aas%20search (active)
 
-    # https://travis-ci.org/The-Compiler/qutebrowser/jobs/157941726
+    # https://travis-ci.org/qutebrowser/qutebrowser/jobs/157941726
     @qtwebengine_flaky
     Scenario: Pasting multiple urls in a background tab
         When I put the following lines into the clipboard:
@@ -251,7 +251,7 @@ Feature: Yanking and pasting.
         When I set general -> log-javascript-console to info
         And I open data/paste_primary.html
         And I run :click-element id qute-textarea
-        And I wait for "Clicked editable element!" in the log
+        And I wait for "Entering mode KeyMode.insert (reason: clicking input)" in the log
         And I run :insert-text Hello world
         # Compare
         Then the javascript message "textarea contents: Hello world" should be logged
@@ -261,7 +261,7 @@ Feature: Yanking and pasting.
         And I set content -> allow-javascript to false
         And I open data/paste_primary.html
         And I run :click-element id qute-textarea
-        And I wait for "Clicked editable element!" in the log
+        And I wait for "Entering mode KeyMode.insert (reason: clicking input)" in the log
         And I run :insert-text Hello world
         And I wait for "Inserting text into element *" in the log
         And I run :jseval console.log("textarea contents: " + document.getElementById('qute-textarea').value);
@@ -275,7 +275,7 @@ Feature: Yanking and pasting.
         And I open data/paste_primary.html
         And I set the text field to "one two three four"
         And I run :click-element id qute-textarea
-        And I wait for "Clicked editable element!" in the log
+        And I wait for "Entering mode KeyMode.insert (reason: clicking input)" in the log
         # Move to the beginning and two characters to the right
         And I press the keys ""
         And I press the key ""
@@ -289,7 +289,7 @@ Feature: Yanking and pasting.
         When I set general -> log-javascript-console to info
         And I open data/paste_primary.html
         And I run :click-element id qute-textarea
-        And I wait for "Clicked editable element!" in the log
+        And I wait for "Entering mode KeyMode.insert (reason: clicking input)" in the log
         # Paste and undo
         And I run :insert-text This text should be undone
         And I wait for the javascript message "textarea contents: This text should be undone"
diff --git a/tests/end2end/features/zoom.feature b/tests/end2end/features/zoom.feature
index 8d8e9ed0a..3aa39df8b 100644
--- a/tests/end2end/features/zoom.feature
+++ b/tests/end2end/features/zoom.feature
@@ -20,19 +20,19 @@ Feature: Zooming in and out
         Then the message "Zoom level: 120%" should be shown
         And the zoom should be 120%
 
-    # https://github.com/The-Compiler/qutebrowser/issues/1118
+    # https://github.com/qutebrowser/qutebrowser/issues/1118
     Scenario: Zooming in with very big count
         When I run :zoom-in with count 99999999999
         Then the message "Zoom level: 120%" should be shown
         And the zoom should be 120%
 
-    # https://github.com/The-Compiler/qutebrowser/issues/1118
+    # https://github.com/qutebrowser/qutebrowser/issues/1118
     Scenario: Zooming out with very big count
         When I run :zoom-out with count 99999999999
         Then the message "Zoom level: 50%" should be shown
         And the zoom should be 50%
 
-    # https://github.com/The-Compiler/qutebrowser/issues/1118
+    # https://github.com/qutebrowser/qutebrowser/issues/1118
     Scenario: Zooming in with very big count and snapping in
         When I run :zoom-in with count 99999999999
         And I run :zoom-out
@@ -85,3 +85,11 @@ Feature: Zooming in and out
         And I run :zoom-in
         Then the message "Zoom level: 120%" should be shown
         And the zoom should be 120%
+
+    # https://github.com/qutebrowser/qutebrowser/issues/2183
+    @qtwebengine_flaky
+    Scenario: Setting a default zoom
+        When I set ui -> default-zoom to 200%
+        And I open data/hello.txt in a new tab
+        And I run :tab-only
+        Then the zoom should be 200%
diff --git a/tests/end2end/fixtures/quteprocess.py b/tests/end2end/fixtures/quteprocess.py
index 53f3bee88..f57c3c6d5 100644
--- a/tests/end2end/fixtures/quteprocess.py
+++ b/tests/end2end/fixtures/quteprocess.py
@@ -58,11 +58,23 @@ def is_ignored_lowlevel_message(message):
     if 'Running without the SUID sandbox!' in message:
         return True
     elif message.startswith('Xlib: sequence lost'):
-        # https://travis-ci.org/The-Compiler/qutebrowser/jobs/157941720
+        # https://travis-ci.org/qutebrowser/qutebrowser/jobs/157941720
         # ???
         return True
     elif 'CERT_PKIXVerifyCert for localhost failed' in message:
         return True
+    elif 'Invalid node channel message' in message:
+        # Started appearing in sessions.feature with Qt 5.8...
+        return True
+    elif ("_dl_allocate_tls_init: Assertion `listp->slotinfo[cnt].gen <= "
+          "GL(dl_tls_generation)' failed!" in message):
+        # Started appearing with Qt 5.8...
+        # http://patchwork.sourceware.org/patch/10255/
+        return True
+    elif ("CreatePlatformSocket() returned an error, errno=97: Address family "
+          "not supported by protocol" in message):
+        # Makes tests fail on Quantumcross' machine
+        return True
     return False
 
 
@@ -196,13 +208,24 @@ class QuteProc(testprocess.Process):
         start_okay_message_load = (
             "load status for : LoadStatus.success")
-        start_okay_message_focus = (
+        start_okay_messages_focus = [
+            ## QtWebKit
             "Focus object changed: "
-            "")
-        # With QtWebEngine the QOpenGLWidget has the actual focus
-        start_okay_message_focus_qtwe = (
-            "Focus object changed: "
-        )
+            "",
+            # when calling QApplication::sync
+            "Focus object changed: "
+            "",
+
+            ## QtWebEngine
+            "Focus object changed: "
+            "",
+            # with Qt >= 5.8
+            "Focus object changed: "
+            "",
+            # when calling QApplication::sync
+            "Focus object changed: "
+            "",
+        ]
 
         if (log_line.category == 'ipc' and
                 log_line.message.startswith("Listening as ")):
@@ -213,13 +236,9 @@ class QuteProc(testprocess.Process):
             if not self._load_ready:
                 log_line.waited_for = True
             self._is_ready('load')
-        elif (log_line.category == 'misc' and
-              testutils.pattern_match(pattern=start_okay_message_focus,
-                                      value=log_line.message)):
-            self._is_ready('focus')
-        elif (log_line.category == 'misc' and
-              testutils.pattern_match(pattern=start_okay_message_focus_qtwe,
-                                      value=log_line.message)):
+        elif log_line.category == 'misc' and any(testutils.pattern_match(
+                pattern=pattern, value=log_line.message) for pattern in
+                start_okay_messages_focus):
             self._is_ready('focus')
         elif (log_line.category == 'init' and
               log_line.module == 'standarddir' and
@@ -236,7 +255,9 @@ class QuteProc(testprocess.Process):
             if not line.strip():
                 return None
             elif (is_ignored_qt_message(line) or
-                  is_ignored_lowlevel_message(line)):
+                  is_ignored_lowlevel_message(line) or
+                  self.request.node.get_marker('no_invalid_lines')):
+                self._log("IGNORED: {}".format(line))
                 return None
             else:
                 raise
@@ -287,7 +308,8 @@ class QuteProc(testprocess.Process):
         URLs like about:... and qute:... are handled specially and returned
         verbatim.
         """
-        if path.startswith('about:') or path.startswith('qute:'):
+        special_schemes = ['about:', 'qute:', 'chrome:']
+        if any(path.startswith(scheme) for scheme in special_schemes):
             return path
         else:
             httpbin = self.request.getfixturevalue('httpbin')
@@ -319,13 +341,6 @@ class QuteProc(testprocess.Process):
         if (x is None and y is not None) or (y is None and x is not None):
             raise ValueError("Either both x/y or neither must be given!")
 
-        if self.request.config.webengine:
-            # pylint: disable=no-name-in-module,useless-suppression
-            from PyQt5.QtWebEngineWidgets import QWebEnginePage
-            # pylint: enable=no-name-in-module,useless-suppression
-            if not hasattr(QWebEnginePage, 'scrollPositionChanged'):
-                # Qt < 5.7
-                pytest.skip("QWebEnginePage.scrollPositionChanged missing")
         if x is None and y is None:
             point = 'PyQt5.QtCore.QPoint(*, *)'  # not counting 0/0 here
         elif x == '0' and y == '0':
@@ -358,8 +373,7 @@ class QuteProc(testprocess.Process):
             pattern="load status for <* tab_id=* url='*duckduckgo*'>: *",
             value=msg.message)
 
-        is_log_error = (msg.loglevel > logging.INFO and
-                        not msg.message.startswith('STUB:'))
+        is_log_error = msg.loglevel > logging.INFO
         return is_log_error or is_js_error or is_ddg_load
 
     def _maybe_skip(self):
@@ -475,15 +489,16 @@ class QuteProc(testprocess.Process):
         yield
         self.set_setting(sect, opt, old_value)
 
-    def open_path(self, path, *, new_tab=False, new_window=False, as_url=False,
-                  port=None, https=False, wait=True):
+    def open_path(self, path, *, new_tab=False, new_bg_tab=False,
+                  new_window=False, as_url=False, port=None, https=False,
+                  wait=True):
         """Open the given path on the local webserver in qutebrowser."""
         url = self.path_to_url(path, port=port, https=https)
-        self.open_url(url, new_tab=new_tab, new_window=new_window,
-                      as_url=as_url, wait=wait)
+        self.open_url(url, new_tab=new_tab, new_bg_tab=new_bg_tab,
+                      new_window=new_window, as_url=as_url, wait=wait)
 
-    def open_url(self, url, *, new_tab=False, new_window=False, as_url=False,
-                 wait=True):
+    def open_url(self, url, *, new_tab=False, new_bg_tab=False,
+                 new_window=False, as_url=False, wait=True):
         """Open the given url in qutebrowser."""
         if new_tab and new_window:
             raise ValueError("new_tab and new_window given!")
@@ -492,6 +507,8 @@ class QuteProc(testprocess.Process):
             self.send_cmd(url, invalid=True)
         elif new_tab:
             self.send_cmd(':open -t ' + url)
+        elif new_bg_tab:
+            self.send_cmd(':open -b ' + url)
         elif new_window:
             self.send_cmd(':open -w ' + url)
         else:
diff --git a/tests/end2end/fixtures/webserver_sub.py b/tests/end2end/fixtures/webserver_sub.py
index 0fa1d7139..df84d26d6 100644
--- a/tests/end2end/fixtures/webserver_sub.py
+++ b/tests/end2end/fixtures/webserver_sub.py
@@ -31,7 +31,7 @@ import threading
 
 from httpbin.core import app
 from httpbin.structures import CaseInsensitiveDict
-import cherrypy.wsgiserver
+import cheroot.wsgi
 import flask
 
 
@@ -135,7 +135,7 @@ def log_request(response):
     return response
 
 
-class WSGIServer(cherrypy.wsgiserver.CherryPyWSGIServer):
+class WSGIServer(cheroot.wsgi.Server):
 
     """A custom WSGIServer that prints a line on stderr when it's ready.
 
diff --git a/tests/end2end/test_insert_mode.py b/tests/end2end/test_insert_mode.py
index 6939dae0e..8b9ed3aca 100644
--- a/tests/end2end/test_insert_mode.py
+++ b/tests/end2end/test_insert_mode.py
@@ -19,9 +19,6 @@
 
 """Test insert mode settings on html files."""
 
-import logging
-import json
-
 import pytest
 
 
@@ -35,48 +32,29 @@ import pytest
     ('autofocus.html', 'qute-input-autofocus', 'keypress', 'cutebrowser',
      'true'),
 ])
-def test_insert_mode(file_name, elem_id, source, input_text, auto_insert,
+@pytest.mark.parametrize('zoom', [100, 125, 250])
+def test_insert_mode(file_name, elem_id, source, input_text, auto_insert, zoom,
                      quteproc, request):
     url_path = 'data/insert_mode_settings/html/{}'.format(file_name)
     quteproc.open_path(url_path)
 
     quteproc.set_setting('input', 'auto-insert-mode', auto_insert)
-    quteproc.send_cmd(':click-element id {}'.format(elem_id))
-    quteproc.wait_for(message='Clicked editable element!')
+    quteproc.send_cmd(':zoom {}'.format(zoom))
+
+    quteproc.send_cmd(':click-element --force-event id {}'.format(elem_id))
+    quteproc.wait_for(message='Entering mode KeyMode.insert (reason: *)')
     quteproc.send_cmd(':debug-set-fake-clipboard')
 
     if source == 'keypress':
         quteproc.press_keys(input_text)
     elif source == 'clipboard':
-        if request.config.webengine:
-            pytest.xfail(reason="QtWebEngine TODO: caret mode is not "
-                         "implemented")
-            # Note we actually run the keypress tests with QtWebEngine, as for
-            # some reason it selects all the text when clicking the field the
-            # second time.
         quteproc.send_cmd(':debug-set-fake-clipboard "{}"'.format(input_text))
         quteproc.send_cmd(':insert-text {clipboard}')
     else:
         raise ValueError("Invalid source {!r}".format(source))
 
     quteproc.wait_for_js('contents: {}'.format(input_text))
-
     quteproc.send_cmd(':leave-mode')
-    quteproc.send_cmd(':hint all')
-    quteproc.wait_for(message='hints: *')
-    quteproc.send_cmd(':follow-hint a')
-    quteproc.wait_for(message='Clicked editable element!')
-    quteproc.send_cmd(':enter-mode caret')
-    quteproc.send_cmd(':toggle-selection')
-    quteproc.send_cmd(':move-to-prev-word')
-    quteproc.send_cmd(':yank selection')
-
-    expected_message = '{} chars yanked to clipboard'.format(len(input_text))
-    quteproc.mark_expected(category='message',
-                           loglevel=logging.INFO,
-                           message=expected_message)
-    quteproc.wait_for(
-        message='Setting fake clipboard: {}'.format(json.dumps(input_text)))
 
 
 def test_auto_leave_insert_mode(quteproc):
@@ -84,6 +62,7 @@ def test_auto_leave_insert_mode(quteproc):
     quteproc.open_path(url_path)
 
     quteproc.set_setting('input', 'auto-leave-insert-mode', 'true')
+    quteproc.send_cmd(':zoom 100')
 
     quteproc.press_keys('abcd')
 
@@ -93,11 +72,3 @@ def test_auto_leave_insert_mode(quteproc):
     # Select the disabled input box to leave insert mode
     quteproc.send_cmd(':follow-hint s')
     quteproc.wait_for(message='Clicked non-editable element!')
-    quteproc.send_cmd(':enter-mode caret')
-    quteproc.send_cmd(':paste-primary')
-
-    expected_message = ('paste-primary: This command is only allowed in '
-                        'insert mode, not caret.')
-    quteproc.mark_expected(category='message',
-                           loglevel=logging.ERROR,
-                           message=expected_message)
diff --git a/tests/end2end/test_invocations.py b/tests/end2end/test_invocations.py
index d85e2a467..bcb8b04e6 100644
--- a/tests/end2end/test_invocations.py
+++ b/tests/end2end/test_invocations.py
@@ -19,6 +19,7 @@
 
 """Test starting qutebrowser with special arguments/environments."""
 
+import socket
 import sys
 import logging
 import re
@@ -28,6 +29,7 @@ import pytest
 from PyQt5.QtCore import QProcess
 
 from end2end.fixtures import quteprocess, testprocess
+from qutebrowser.utils import qtutils
 
 
 def _base_args(config):
@@ -72,8 +74,8 @@ def temp_basedir_env(tmpdir, short_tmpdir):
 def test_ascii_locale(request, httpbin, tmpdir, quteproc_new):
     """Test downloads with LC_ALL=C set.
 
-    https://github.com/The-Compiler/qutebrowser/issues/908
-    https://github.com/The-Compiler/qutebrowser/issues/1726
+    https://github.com/qutebrowser/qutebrowser/issues/908
+    https://github.com/qutebrowser/qutebrowser/issues/1726
     """
     if request.config.webengine:
         pytest.skip("Downloads are not implemented with QtWebEngine yet")
@@ -96,7 +98,7 @@ def test_ascii_locale(request, httpbin, tmpdir, quteproc_new):
                           .format(sys.executable))
     quteproc_new.wait_for(category='downloads',
                           message='Download ä-issue908.bin finished')
-    quteproc_new.wait_for(category='downloads',
+    quteproc_new.wait_for(category='misc',
                           message='Opening * with [*python*]')
 
     assert len(tmpdir.listdir()) == 1
@@ -108,8 +110,8 @@ def test_misconfigured_user_dirs(request, httpbin, temp_basedir_env,
                                  tmpdir, quteproc_new):
     """Test downloads with a misconfigured XDG_DOWNLOAD_DIR.
 
-    https://github.com/The-Compiler/qutebrowser/issues/866
-    https://github.com/The-Compiler/qutebrowser/issues/1269
+    https://github.com/qutebrowser/qutebrowser/issues/866
+    https://github.com/qutebrowser/qutebrowser/issues/1269
     """
     if request.config.webengine:
         pytest.skip("Downloads are not implemented with QtWebEngine yet")
@@ -185,3 +187,77 @@ def test_version(request):
     output = bytes(proc.proc.readAllStandardOutput()).decode('utf-8')
 
     assert re.search(r'^qutebrowser\s+v\d+(\.\d+)', output) is not None
+
+
+@pytest.mark.skipif(not qtutils.version_check('5.3'),
+                    reason="Does not work on Qt 5.2")
+def test_qt_arg(request, quteproc_new, tmpdir):
+    """Test --qt-arg."""
+    args = (['--temp-basedir', '--qt-arg', 'stylesheet',
+             str(tmpdir / 'does-not-exist')] + _base_args(request.config))
+    quteproc_new.start(args)
+
+    msg = 'QCss::Parser - Failed to load file  "*does-not-exist"'
+    line = quteproc_new.wait_for(message=msg)
+    line.expected = True
+
+    quteproc_new.send_cmd(':quit')
+    quteproc_new.wait_for_quit()
+
+
+def test_webengine_inspector(request, quteproc_new):
+    if not request.config.webengine:
+        pytest.skip()
+    args = (['--temp-basedir', '--enable-webengine-inspector'] +
+            _base_args(request.config))
+    quteproc_new.start(args)
+    line = quteproc_new.wait_for(
+        message='Remote debugging server started successfully. Try pointing a '
+                'Chromium-based browser to http://127.0.0.1:*')
+    port = int(line.message.split(':')[-1])
+
+    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+    s.connect(('127.0.0.1', port))
+    s.close()
+
+
+@pytest.mark.linux
+def test_webengine_download_suffix(request, quteproc_new, tmpdir):
+    """Make sure QtWebEngine does not add a suffix to downloads."""
+    if not request.config.webengine:
+        pytest.skip()
+
+    download_dir = tmpdir / 'downloads'
+    download_dir.ensure(dir=True)
+
+    (tmpdir / 'user-dirs.dirs').write(
+        'XDG_DOWNLOAD_DIR={}'.format(download_dir))
+    env = {'XDG_CONFIG_HOME': str(tmpdir)}
+    args = (['--temp-basedir'] + _base_args(request.config))
+    quteproc_new.start(args, env=env)
+
+    quteproc_new.set_setting('storage', 'prompt-download-directory', 'false')
+    quteproc_new.set_setting('storage', 'download-directory',
+                             str(download_dir))
+    quteproc_new.open_path('data/downloads/download.bin', wait=False)
+    quteproc_new.wait_for(category='downloads', message='Download * finished')
+    quteproc_new.open_path('data/downloads/download.bin', wait=False)
+    quteproc_new.wait_for(message='Entering mode KeyMode.yesno *')
+    quteproc_new.send_cmd(':prompt-accept yes')
+    quteproc_new.wait_for(category='downloads', message='Download * finished')
+
+    files = download_dir.listdir()
+    assert len(files) == 1
+    assert files[0].basename == 'download.bin'
+
+
+def test_command_on_start(request, quteproc_new):
+    """Make sure passing a command on start works.
+
+    See https://github.com/qutebrowser/qutebrowser/issues/2408
+    """
+    args = (['--temp-basedir'] + _base_args(request.config) +
+            [':quickmark-add https://www.example.com/ example'])
+    quteproc_new.start(args)
+    quteproc_new.send_cmd(':quit')
+    quteproc_new.wait_for_quit()
diff --git a/tests/end2end/test_mhtml_e2e.py b/tests/end2end/test_mhtml_e2e.py
index 91ed693f6..46ece8f0e 100644
--- a/tests/end2end/test_mhtml_e2e.py
+++ b/tests/end2end/test_mhtml_e2e.py
@@ -27,10 +27,6 @@ import collections
 import pytest
 
 
-pytestmark = pytest.mark.qtwebengine_todo("mhtml downloads are not "
-                                          "implemented")
-
-
 def collect_tests():
     basedir = os.path.dirname(__file__)
     datadir = os.path.join(basedir, 'data', 'downloads', 'mhtml')
@@ -40,10 +36,15 @@ def collect_tests():
 
 def normalize_line(line):
     line = line.rstrip('\n')
-    line = re.sub('boundary="---=_qute-[0-9a-f-]+"',
+    line = re.sub('boundary="-+(=_qute|MultipartBoundary)-[0-9a-zA-Z-]+"',
                   'boundary="---=_qute-UUID"', line)
-    line = re.sub('^-----=_qute-[0-9a-f-]+$', '-----=_qute-UUID', line)
+    line = re.sub('^-+(=_qute|MultipartBoundary)-[0-9a-zA-Z-]+$',
+                  '-----=_qute-UUID', line)
     line = re.sub(r'localhost:\d{1,5}', 'localhost:(port)', line)
+    if line.startswith('Date: '):
+        line = 'Date: today'
+    if line.startswith('Content-ID: '):
+        line = 'Content-ID: 42'
 
     # Depending on Python's mimetypes module/the system's mime files, .js
     # files could be either identified as x-javascript or just javascript
@@ -68,6 +69,9 @@ class DownloadDir:
         with open(str(files[0]), 'r', encoding='utf-8') as f:
             return f.readlines()
 
+    def sanity_check_mhtml(self):
+        assert 'Content-Type: multipart/related' in '\n'.join(self.read_file())
+
     def compare_mhtml(self, filename):
         with open(filename, 'r', encoding='utf-8') as f:
             expected_data = [normalize_line(line) for line in f]
@@ -81,8 +85,25 @@ def download_dir(tmpdir):
     return DownloadDir(tmpdir)
 
 
+def _test_mhtml_requests(test_dir, test_path, httpbin):
+    with open(os.path.join(test_dir, 'requests'), encoding='utf-8') as f:
+        expected_requests = []
+        for line in f:
+            if line.startswith('#'):
+                continue
+            path = '/{}/{}'.format(test_path, line.strip())
+            expected_requests.append(httpbin.ExpectedRequest('GET', path))
+
+    actual_requests = httpbin.get_requests()
+    # Requests are not hashable, we need to convert to ExpectedRequests
+    actual_requests = [httpbin.ExpectedRequest.from_request(req)
+                       for req in actual_requests]
+    assert (collections.Counter(actual_requests) ==
+            collections.Counter(expected_requests))
+
+
 @pytest.mark.parametrize('test_name', collect_tests())
-def test_mhtml(test_name, download_dir, quteproc, httpbin):
+def test_mhtml(request, test_name, download_dir, quteproc, httpbin):
     quteproc.set_setting('storage', 'download-directory',
                          download_dir.location)
     quteproc.set_setting('storage', 'prompt-download-directory', 'false')
@@ -104,24 +125,16 @@ def test_mhtml(test_name, download_dir, quteproc, httpbin):
     # Discard all requests that were necessary to display the page
     httpbin.clear_data()
     quteproc.send_cmd(':download --mhtml --dest "{}"'.format(download_dest))
-    quteproc.wait_for(category='downloads', module='mhtml',
-                      function='_finish_file',
+    quteproc.wait_for(category='downloads',
                       message='File successfully written.')
 
-    expected_file = os.path.join(test_dir, '{}.mht'.format(test_name))
-    download_dir.compare_mhtml(expected_file)
+    suffix = '-webengine' if request.config.webengine else ''
+    filename = '{}{}.mht'.format(test_name, suffix)
+    expected_file = os.path.join(test_dir, filename)
+    if os.path.exists(expected_file):
+        download_dir.compare_mhtml(expected_file)
+    else:
+        download_dir.sanity_check_mhtml()
 
-    with open(os.path.join(test_dir, 'requests'), encoding='utf-8') as f:
-        expected_requests = []
-        for line in f:
-            if line.startswith('#'):
-                continue
-            path = '/{}/{}'.format(test_path, line.strip())
-            expected_requests.append(httpbin.ExpectedRequest('GET', path))
-
-    actual_requests = httpbin.get_requests()
-    # Requests are not hashable, we need to convert to ExpectedRequests
-    actual_requests = [httpbin.ExpectedRequest.from_request(req)
-                       for req in actual_requests]
-    assert (collections.Counter(actual_requests) ==
-            collections.Counter(expected_requests))
+    if not request.config.webengine:
+        _test_mhtml_requests(test_dir, test_path, httpbin)
diff --git a/tests/helpers/fixtures.py b/tests/helpers/fixtures.py
index f494943a7..f9e829e40 100644
--- a/tests/helpers/fixtures.py
+++ b/tests/helpers/fixtures.py
@@ -39,7 +39,7 @@ import py.path  # pylint: disable=no-name-in-module
 
 import helpers.stubs as stubsmod
 from qutebrowser.config import config
-from qutebrowser.utils import objreg
+from qutebrowser.utils import objreg, standarddir
 from qutebrowser.browser.webkit import cookies
 from qutebrowser.misc import savemanager
 from qutebrowser.keyinput import modeman
@@ -155,11 +155,10 @@ def tab_registry(win_registry):
 
 
 @pytest.fixture
-def fake_web_tab(stubs, tab_registry, mode_manager, qapp, fake_args):
+def fake_web_tab(stubs, tab_registry, mode_manager, qapp):
     """Fixture providing the FakeWebTab *class*."""
     if PYQT_VERSION < 0x050600:
         pytest.skip('Causes segfaults, see #1638')
-    fake_args.backend = 'webengine'
     return stubs.FakeWebTab
 
 
@@ -447,7 +446,7 @@ def config_tmpdir(monkeypatch, tmpdir):
     confdir = tmpdir / 'config'
     path = str(confdir)
     os.mkdir(path)
-    monkeypatch.setattr('qutebrowser.utils.standarddir.config', lambda: path)
+    monkeypatch.setattr(standarddir, 'config', lambda: path)
     return confdir
 
 
@@ -460,7 +459,7 @@ def data_tmpdir(monkeypatch, tmpdir):
     datadir = tmpdir / 'data'
     path = str(datadir)
     os.mkdir(path)
-    monkeypatch.setattr('qutebrowser.utils.standarddir.data', lambda: path)
+    monkeypatch.setattr(standarddir, 'data', lambda: path)
     return datadir
 
 
diff --git a/tests/helpers/messagemock.py b/tests/helpers/messagemock.py
index 38fdf3e97..7854aabcc 100644
--- a/tests/helpers/messagemock.py
+++ b/tests/helpers/messagemock.py
@@ -70,6 +70,7 @@ class MessageMock:
     def patch(self):
         """Start recording messages."""
         message.global_bridge.show_message.connect(self._record_message)
+        message.global_bridge._connected = True
 
     def unpatch(self):
         """Stop recording messages."""
diff --git a/tests/helpers/stubs.py b/tests/helpers/stubs.py
index 50f4e545e..dfbcc550d 100644
--- a/tests/helpers/stubs.py
+++ b/tests/helpers/stubs.py
@@ -27,7 +27,7 @@ from unittest import mock
 from PyQt5.QtCore import pyqtSignal, QPoint, QProcess, QObject
 from PyQt5.QtNetwork import (QNetworkRequest, QAbstractNetworkCache,
                              QNetworkCacheMetaData)
-from PyQt5.QtWidgets import QCommonStyle, QLineEdit, QWidget
+from PyQt5.QtWidgets import QCommonStyle, QLineEdit, QWidget, QTabBar
 
 from qutebrowser.browser import browsertab, history
 from qutebrowser.config import configexc
@@ -571,6 +571,7 @@ class TabbedBrowserStub(QObject):
         super().__init__(parent)
         self.tabs = []
         self.shutting_down = False
+        self._qtabbar = QTabBar()
 
     def count(self):
         return len(self.tabs)
@@ -584,6 +585,9 @@ class TabbedBrowserStub(QObject):
     def on_tab_close_requested(self, idx):
         del self.tabs[idx]
 
+    def tabBar(self):
+        return self._qtabbar
+
 
 class ApplicationStub(QObject):
 
diff --git a/tests/manual/hints/hide_unmatched_rapid_hints.html b/tests/manual/hints/hide_unmatched_rapid_hints.html
index d9535733f..bee1411d9 100644
--- a/tests/manual/hints/hide_unmatched_rapid_hints.html
+++ b/tests/manual/hints/hide_unmatched_rapid_hints.html
@@ -5,7 +5,7 @@
         Hide unmatched rapid hints
     
     
-        

When hints -> hide-unmatched-rapid-hints is set to true (default), rapid hints behave like normal hints, i.e. unmatched hints will be hidden as you type. Setting the option to false will disable hiding in rapid mode, which is sometimes useful (see #1799).

+

When hints -> hide-unmatched-rapid-hints is set to true (default), rapid hints behave like normal hints, i.e. unmatched hints will be hidden as you type. Setting the option to false will disable hiding in rapid mode, which is sometimes useful (see #1799).

Note that when hinting in number mode, the hints -> hide-unmatched-rapid-hints option affects typing the hint string (number), but not the filter (letters).

Here is couple of invalid links to test the behaviour:

one

diff --git a/tests/manual/hints/issue824.html b/tests/manual/hints/issue824.html index 881e1b263..7e2b364a5 100644 --- a/tests/manual/hints/issue824.html +++ b/tests/manual/hints/issue824.html @@ -7,7 +7,7 @@

When using hints (f) on this page, the hint should be drawn over the link.

-

See #824.

-

This was fixed by #1433.

+

See #824.

+

This was fixed by #1433.

diff --git a/tests/manual/hints/issue925.html b/tests/manual/hints/issue925.html index 8064d3b64..b3bf96e1a 100644 --- a/tests/manual/hints/issue925.html +++ b/tests/manual/hints/issue925.html @@ -13,7 +13,7 @@

When using hints (f) on this page, the hint should have a normal size.

- See #925. + See #925.

diff --git a/tests/manual/hints/other.html b/tests/manual/hints/other.html index fe403d78d..7fb6c26c9 100644 --- a/tests/manual/hints/other.html +++ b/tests/manual/hints/other.html @@ -8,11 +8,11 @@

Hint issues without minimal reproducers yet:

  • - Links should be correctly positioned on metafilter.com - see #824 (comment).
    + Links should be correctly positioned on metafilter.com - see #824 (comment).
    Current state: good
  • - Links should be correctly positioned on xkcd.org - see #824.
    + Links should be correctly positioned on xkcd.org - see #824.
    Current state: good - fixed in #1433
  • @@ -20,15 +20,15 @@ current state: bad
  • - links should be correctly positioned on this ctl.io page - see #824 (comment).
    + links should be correctly positioned on this ctl.io page - see #824 (comment).
    Current state: good - fixed in #1433
  • - When clicking titles under the images on etsy, the correct item should be selected (sometimes the one on the right is selected instead) - see #1005.
    + When clicking titles under the images on etsy, the correct item should be selected (sometimes the one on the right is selected instead) - see #1005.
    Current state: good - fixed in #1433
  • - When clicking titles on Geizhals, the correct item should be selected (one of the sub-titles is selected instead) - see #1514.
    + When clicking titles on Geizhals, the correct item should be selected (one of the sub-titles is selected instead) - see #1514.
    Current state: good - fixed in #1433
diff --git a/tests/test_conftest.py b/tests/test_conftest.py index 4dff7ddce..24fb67097 100644 --- a/tests/test_conftest.py +++ b/tests/test_conftest.py @@ -44,7 +44,7 @@ def test_fail_on_warnings(): warnings.warn('test', PendingDeprecationWarning) -@pytest.mark.xfail(reason="https://github.com/The-Compiler/qutebrowser/issues/1070", +@pytest.mark.xfail(reason="https://github.com/qutebrowser/qutebrowser/issues/1070", strict=False) def test_installed_package(): """Make sure the tests are running against the installed package.""" diff --git a/tests/unit/browser/test_adblock.py b/tests/unit/browser/test_adblock.py index 3caf6e526..59fb14365 100644 --- a/tests/unit/browser/test_adblock.py +++ b/tests/unit/browser/test_adblock.py @@ -312,6 +312,68 @@ def test_failed_dl_update(config_stub, basedir, download_stub, assert_urls(host_blocker, whitelisted=[]) +@pytest.mark.parametrize('location', ['content', 'comment']) +def test_invalid_utf8(config_stub, download_stub, tmpdir, caplog, location): + """Make sure invalid UTF-8 is handled correctly. + + See https://github.com/qutebrowser/qutebrowser/issues/2301 + """ + blocklist = tmpdir / 'blocklist' + if location == 'comment': + blocklist.write_binary(b'# nbsp: \xa0\n') + else: + assert location == 'content' + blocklist.write_binary(b'https://www.example.org/\xa0') + for url in BLOCKLIST_HOSTS: + blocklist.write(url + '\n', mode='a') + + config_stub.data = { + 'content': { + 'host-block-lists': [QUrl(str(blocklist))], + 'host-blocking-enabled': True, + 'host-blocking-whitelist': None, + } + } + host_blocker = adblock.HostBlocker() + host_blocker.adblock_update() + finished_signal = host_blocker._in_progress[0].finished + + if location == 'content': + with caplog.at_level(logging.ERROR): + finished_signal.emit() + expected = (r"Failed to decode: " + r"b'https://www.example.org/\xa0localhost") + assert caplog.records[-2].message.startswith(expected) + else: + finished_signal.emit() + + host_blocker.read_hosts() + assert_urls(host_blocker, whitelisted=[]) + + +def test_invalid_utf8_compiled(config_stub, tmpdir, monkeypatch, caplog): + """Make sure invalid UTF-8 in the compiled file is handled.""" + data_dir = tmpdir / 'data' + config_dir = tmpdir / 'config' + monkeypatch.setattr(adblock.standarddir, 'data', lambda: str(data_dir)) + monkeypatch.setattr(adblock.standarddir, 'config', lambda: str(config_dir)) + + config_stub.data = { + 'content': { + 'host-block-lists': [], + } + } + + (config_dir / 'blocked-hosts').write_binary( + b'https://www.example.org/\xa0') + (data_dir / 'blocked-hosts').ensure() + + host_blocker = adblock.HostBlocker() + with caplog.at_level(logging.ERROR): + host_blocker.read_hosts() + assert caplog.records[-1].message == "Failed to read host blocklist!" + + def test_blocking_with_whitelist(config_stub, basedir, download_stub, data_tmpdir, tmpdir): """Ensure hosts in host-blocking-whitelist are never blocked.""" diff --git a/tests/unit/browser/test_qutescheme.py b/tests/unit/browser/test_qutescheme.py new file mode 100644 index 000000000..5ceecb7f8 --- /dev/null +++ b/tests/unit/browser/test_qutescheme.py @@ -0,0 +1,139 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2017 Imran Sobir +# +# 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 . + +import datetime +import collections + +from PyQt5.QtCore import QUrl +import pytest + +from qutebrowser.browser import history, qutescheme +from qutebrowser.utils import objreg + + +Dates = collections.namedtuple('Dates', ['yesterday', 'today', 'tomorrow']) + + +class TestHistoryHandler: + + """Test the qute://history endpoint.""" + + @pytest.fixture + def dates(self): + one_day = datetime.timedelta(days=1) + today = datetime.datetime.today() + tomorrow = today + one_day + yesterday = today - one_day + return Dates(yesterday, today, tomorrow) + + @pytest.fixture + def entries(self, dates): + today = history.Entry(atime=str(dates.today.timestamp()), + url=QUrl('www.today.com'), title='today') + tomorrow = history.Entry(atime=str(dates.tomorrow.timestamp()), + url=QUrl('www.tomorrow.com'), title='tomorrow') + yesterday = history.Entry(atime=str(dates.yesterday.timestamp()), + url=QUrl('www.yesterday.com'), title='yesterday') + return Dates(yesterday, today, tomorrow) + + @pytest.fixture + def fake_web_history(self, fake_save_manager, tmpdir): + """Create a fake web-history and register it into objreg.""" + web_history = history.WebHistory(tmpdir.dirname, 'fake-history') + objreg.register('web-history', web_history) + yield web_history + objreg.delete('web-history') + + @pytest.fixture(autouse=True) + def fake_history(self, fake_web_history, entries): + """Create fake history for three different days.""" + fake_web_history._add_entry(entries.yesterday) + fake_web_history._add_entry(entries.today) + fake_web_history._add_entry(entries.tomorrow) + fake_web_history.save() + + def test_history_without_query(self): + """Ensure qute://history shows today's history without any query.""" + _mimetype, data = qutescheme.qute_history(QUrl("qute://history")) + key = "{}".format( + datetime.date.today().strftime("%a, %d %B %Y")) + assert key in data + + def test_history_with_bad_query(self): + """Ensure qute://history shows today's history with bad query.""" + url = QUrl("qute://history?date=204-blaah") + _mimetype, data = qutescheme.qute_history(url) + key = "{}".format( + datetime.date.today().strftime("%a, %d %B %Y")) + assert key in data + + def test_history_today(self): + """Ensure qute://history shows history for today.""" + url = QUrl("qute://history") + _mimetype, data = qutescheme.qute_history(url) + assert "today" in data + assert "tomorrow" not in data + assert "yesterday" not in data + + def test_history_yesterday(self, dates): + """Ensure qute://history shows history for yesterday.""" + url = QUrl("qute://history?date=" + + dates.yesterday.strftime("%Y-%m-%d")) + _mimetype, data = qutescheme.qute_history(url) + assert "today" not in data + assert "tomorrow" not in data + assert "yesterday" in data + + def test_history_tomorrow(self, dates): + """Ensure qute://history shows history for tomorrow.""" + url = QUrl("qute://history?date=" + + dates.tomorrow.strftime("%Y-%m-%d")) + _mimetype, data = qutescheme.qute_history(url) + assert "today" not in data + assert "tomorrow" in data + assert "yesterday" not in data + + def test_no_next_link_to_future(self, dates): + """Ensure there's no next link pointing to the future.""" + url = QUrl("qute://history") + _mimetype, data = qutescheme.qute_history(url) + assert "Next" not in data + + url = QUrl("qute://history?date=" + + dates.tomorrow.strftime("%Y-%m-%d")) + _mimetype, data = qutescheme.qute_history(url) + assert "Next" not in data + + def test_qute_history_benchmark(self, dates, entries, fake_web_history, + benchmark): + for i in range(100000): + entry = history.Entry( + atime=str(dates.yesterday.timestamp()), + url=QUrl('www.yesterday.com/{}'.format(i)), + title='yesterday') + fake_web_history._add_entry(entry) + fake_web_history._add_entry(entries.today) + fake_web_history._add_entry(entries.tomorrow) + + url = QUrl("qute://history") + _mimetype, data = benchmark(qutescheme.qute_history, url) + + assert "today" in data + assert "tomorrow" not in data + assert "yesterday" not in data diff --git a/tests/unit/browser/test_tab.py b/tests/unit/browser/test_tab.py index ef35dfafa..7ff1edbde 100644 --- a/tests/unit/browser/test_tab.py +++ b/tests/unit/browser/test_tab.py @@ -85,6 +85,15 @@ def tab(request, default_config, qtbot, tab_registry, cookiejar_and_cache): objreg.delete('mode-manager', scope='window', window=0) +class Zoom(browsertab.AbstractZoom): + + def _set_factor_internal(self, _factor): + pass + + def factor(self): + assert False + + class Tab(browsertab.AbstractTab): # pylint: disable=abstract-method @@ -97,17 +106,17 @@ class Tab(browsertab.AbstractTab): self.caret = browsertab.AbstractCaret(win_id=self.win_id, mode_manager=mode_manager, tab=self, parent=self) - self.zoom = browsertab.AbstractZoom(win_id=self.win_id) + self.zoom = Zoom(win_id=self.win_id) self.search = browsertab.AbstractSearch(parent=self) self.printing = browsertab.AbstractPrinting() self.elements = browsertab.AbstractElements(self) + self.action = browsertab.AbstractAction() def _install_event_filter(self): pass -@pytest.mark.skipif(PYQT_VERSION < 0x050600, - reason='Causes segfaults, see #1638') +@pytest.mark.xfail(run=False, reason='Causes segfaults, see #1638') def test_tab(qtbot, view, config_stub, tab_registry, mode_manager): tab_w = Tab(win_id=0, mode_manager=mode_manager) qtbot.add_widget(tab_w) diff --git a/tests/unit/browser/webengine/test_webenginedownloads.py b/tests/unit/browser/webengine/test_webenginedownloads.py new file mode 100644 index 000000000..ee5b8e22a --- /dev/null +++ b/tests/unit/browser/webengine/test_webenginedownloads.py @@ -0,0 +1,38 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2016 Florian Bruhin (The Compiler) +# +# 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 . + +import os.path + +import pytest + +pytest.importorskip('PyQt5.QtWebEngineWidgets') + +from qutebrowser.browser.webengine import webenginedownloads + + +@pytest.mark.parametrize('path, expected', [ + (os.path.join('subfolder', 'foo'), 'foo'), + ('foo(1)', 'foo'), + ('foo(a)', 'foo(a)'), + ('foo1', 'foo1'), + ('foo%20bar', 'foo bar'), + ('foo%2Fbar', 'bar'), +]) +def test_get_suggested_filename(path, expected): + assert webenginedownloads._get_suggested_filename(path) == expected diff --git a/tests/unit/browser/webkit/network/test_filescheme.py b/tests/unit/browser/webkit/network/test_filescheme.py index c18da6934..c3ef870d6 100644 --- a/tests/unit/browser/webkit/network/test_filescheme.py +++ b/tests/unit/browser/webkit/network/test_filescheme.py @@ -154,7 +154,7 @@ class TestDirbrowserHtml: def test_icons(self, monkeypatch): """Make sure icon paths are correct file:// URLs.""" - monkeypatch.setattr('qutebrowser.utils.jinja.utils.resource_filename', + monkeypatch.setattr(filescheme.jinja.utils, 'resource_filename', lambda name: '/test path/foo.svg') html = filescheme.dirbrowser_html(os.getcwd()).decode('utf-8') diff --git a/tests/unit/browser/webkit/network/test_pac.py b/tests/unit/browser/webkit/network/test_pac.py new file mode 100644 index 000000000..2ad63a496 --- /dev/null +++ b/tests/unit/browser/webkit/network/test_pac.py @@ -0,0 +1,254 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2016 Florian Bruhin (The Compiler) +# +# 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 . + +import http.server +import threading +import logging +import sys +import pytest + +from PyQt5.QtCore import QUrl, QT_VERSION_STR +from PyQt5.QtNetwork import (QNetworkProxy, QNetworkProxyQuery, QHostInfo, + QHostAddress) + +from qutebrowser.browser.network import pac + + +pytestmark = pytest.mark.usefixtures('qapp') + + +def _pac_common_test(test_str): + fun_str_f = """ + function FindProxyForURL(domain, host) {{ + {} + return "DIRECT; PROXY 127.0.0.1:8080; SOCKS 192.168.1.1:4444"; + }} + """ + + fun_str = fun_str_f.format(test_str) + res = pac.PACResolver(fun_str) + proxies = res.resolve(QNetworkProxyQuery(QUrl("https://example.com/test"))) + assert len(proxies) == 3 + assert proxies[0].type() == QNetworkProxy.NoProxy + assert proxies[1].type() == QNetworkProxy.HttpProxy + assert proxies[1].hostName() == "127.0.0.1" + assert proxies[1].port() == 8080 + assert proxies[2].type() == QNetworkProxy.Socks5Proxy + assert proxies[2].hostName() == "192.168.1.1" + assert proxies[2].port() == 4444 + + +def _pac_equality_test(call, expected): + test_str_f = """ + var res = ({0}); + var expected = ({1}); + if(res !== expected) {{ + throw new Error("failed test {0}: got '" + res + "', expected '" + expected + "'"); + }} + """ + _pac_common_test(test_str_f.format(call, expected)) + + +def _pac_except_test(caplog, call): + test_str_f = """ + var thrown = false; + try {{ + var res = ({0}); + }} catch(e) {{ + thrown = true; + }} + if(!thrown) {{ + throw new Error("failed test {0}: got '" + res + "', expected exception"); + }} + """ + with caplog.at_level(logging.ERROR): + _pac_common_test(test_str_f.format(call)) + + +def _pac_noexcept_test(call): + test_str_f = """ + var res = ({0}); + """ + _pac_common_test(test_str_f.format(call)) + + +# pylint: disable=line-too-long, invalid-name + + +@pytest.mark.parametrize("domain, expected", [ + ("known.domain", "'1.2.3.4'"), + ("bogus.domain.foobar", "null") +]) +def test_dnsResolve(monkeypatch, domain, expected): + def mock_fromName(host): + info = QHostInfo() + if host == "known.domain": + info.setAddresses([QHostAddress("1.2.3.4")]) + return info + monkeypatch.setattr(QHostInfo, 'fromName', mock_fromName) + _pac_equality_test("dnsResolve('{}')".format(domain), expected) + + +def test_myIpAddress(): + _pac_equality_test("isResolvable(myIpAddress())", "true") + + +@pytest.mark.parametrize("host, expected", [ + ("example", "true"), + ("example.com", "false"), + ("www.example.com", "false"), +]) +def test_isPlainHostName(host, expected): + _pac_equality_test("isPlainHostName('{}')".format(host), expected) + + +def test_proxyBindings(): + _pac_equality_test("JSON.stringify(ProxyConfig.bindings)", "'{}'") + + +def test_invalid_port(): + test_str = """ + function FindProxyForURL(domain, host) { + return "PROXY 127.0.0.1:FOO"; + } + """ + + res = pac.PACResolver(test_str) + with pytest.raises(pac.ParseProxyError): + res.resolve(QNetworkProxyQuery(QUrl("https://example.com/test"))) + + +@pytest.mark.parametrize('string', ["", "{"]) +def test_wrong_pac_string(string): + with pytest.raises(pac.EvalProxyError): + pac.PACResolver(string) + + +@pytest.mark.parametrize("value", [ + "", + "DIRECT FOO", + "PROXY", + "SOCKS", + "FOOBAR", +]) +def test_fail_parse(value): + test_str_f = """ + function FindProxyForURL(domain, host) {{ + return "{}"; + }} + """ + + res = pac.PACResolver(test_str_f.format(value)) + with pytest.raises(pac.ParseProxyError): + res.resolve(QNetworkProxyQuery(QUrl("https://example.com/test"))) + + +def test_fail_return(): + test_str = """ + function FindProxyForURL(domain, host) { + return null; + } + """ + + res = pac.PACResolver(test_str) + with pytest.raises(pac.EvalProxyError): + res.resolve(QNetworkProxyQuery(QUrl("https://example.com/test"))) + + +@pytest.mark.parametrize('url, has_secret', [ + ('http://example.com/secret', True), # path passed with HTTP + ('http://example.com?secret=yes', True), # query passed with HTTP + ('http://secret@example.com', False), # user stripped with HTTP + ('http://user:secret@example.com', False), # password stripped with HTTP + + ('https://example.com/secret', False), # path stripped with HTTPS + ('https://example.com?secret=yes', False), # query stripped with HTTPS + ('https://secret@example.com', False), # user stripped with HTTPS + ('https://user:secret@example.com', False), # password stripped with HTTPS +]) +@pytest.mark.parametrize('from_file', [True, False]) +def test_secret_url(url, has_secret, from_file): + """Make sure secret parts in an URL are stripped correctly. + + The following parts are considered secret: + - If the PAC info is loaded from a local file, nothing. + - If the URL to resolve is a HTTP URL, the username/password. + - If the URL to resolve is a HTTPS URL, the username/password, query + and path. + """ + test_str = """ + function FindProxyForURL(domain, host) {{ + has_secret = domain.indexOf("secret") !== -1; + expected_secret = {}; + if (has_secret !== expected_secret) {{ + throw new Error("Expected secret: " + expected_secret + ", found: " + has_secret + " in " + domain); + }} + return "DIRECT"; + }} + """.format('true' if (has_secret or from_file) else 'false') + res = pac.PACResolver(test_str) + res.resolve(QNetworkProxyQuery(QUrl(url)), from_file=from_file) + + +# See https://github.com/qutebrowser/qutebrowser/pull/1891#issuecomment-259222615 + +try: + from PyQt5 import QtWebEngineWidgets +except ImportError: + QtWebEngineWidgets = None + + +@pytest.mark.skipif(QT_VERSION_STR.startswith('5.7') and + QtWebEngineWidgets is not None and + sys.platform == "linux", + reason="Segfaults when run with QtWebEngine tests on Linux") +def test_fetch(): + test_str = """ + function FindProxyForURL(domain, host) { + return "DIRECT; PROXY 127.0.0.1:8080; SOCKS 192.168.1.1:4444"; + } + """ + + class PACHandler(http.server.BaseHTTPRequestHandler): + def do_GET(self): + self.send_response(200) + + self.send_header('Content-type', 'application/x-ns-proxy-autoconfig') + self.end_headers() + + self.wfile.write(test_str.encode("ascii")) + + ready_event = threading.Event() + + def serve(): + httpd = http.server.HTTPServer(("127.0.0.1", 8081), PACHandler) + ready_event.set() + httpd.handle_request() + httpd.server_close() + + serve_thread = threading.Thread(target=serve, daemon=True) + serve_thread.start() + try: + ready_event.wait() + res = pac.PACFetcher(QUrl("pac+http://127.0.0.1:8081")) + assert res.fetch_error() is None + finally: + serve_thread.join() + proxies = res.resolve(QNetworkProxyQuery(QUrl("https://example.com/test"))) + assert len(proxies) == 3 diff --git a/tests/unit/browser/webkit/network/test_webkitqutescheme.py b/tests/unit/browser/webkit/network/test_webkitqutescheme.py index 224d06b06..f941abb6d 100644 --- a/tests/unit/browser/webkit/network/test_webkitqutescheme.py +++ b/tests/unit/browser/webkit/network/test_webkitqutescheme.py @@ -22,6 +22,7 @@ import logging from PyQt5.QtCore import QUrl +from qutebrowser.utils import usertypes from qutebrowser.browser import pdfjs, qutescheme # pylint: disable=unused-import from qutebrowser.browser.webkit.network import webkitqutescheme @@ -38,12 +39,12 @@ class TestPDFJSHandler: return b'foobar' raise pdfjs.PDFJSNotFound(path) - monkeypatch.setattr('qutebrowser.browser.pdfjs.get_pdfjs_res', - get_pdfjs_res) + monkeypatch.setattr(pdfjs, 'get_pdfjs_res', get_pdfjs_res) @pytest.fixture(autouse=True) - def patch_args(self, fake_args): - fake_args.backend = 'webkit' + def patch_backend(self, monkeypatch): + monkeypatch.setattr(qutescheme.objects, 'backend', + usertypes.Backend.QtWebKit) def test_existing_resource(self): """Test with a resource that exists.""" diff --git a/tests/unit/browser/webkit/test_history.py b/tests/unit/browser/webkit/test_history.py index 94169e40e..cc35dc4e4 100644 --- a/tests/unit/browser/webkit/test_history.py +++ b/tests/unit/browser/webkit/test_history.py @@ -27,7 +27,7 @@ from hypothesis import strategies from PyQt5.QtCore import QUrl from qutebrowser.browser import history -from qutebrowser.utils import objreg, urlutils +from qutebrowser.utils import objreg, urlutils, usertypes class FakeWebHistory: @@ -220,7 +220,7 @@ def test_clear(qtbot, hist, tmpdir): hist.add_url(QUrl('http://www.qutebrowser.org/')) with qtbot.waitSignal(hist.cleared): - hist.clear() + hist._do_clear() assert not hist_file.read() assert not hist.history_dict @@ -380,14 +380,16 @@ def test_history_interface(qtbot, webview, hist_interface): webview.load(url) -@pytest.mark.parametrize('backend', ['webengine', 'webkit']) -def test_init(backend, qapp, tmpdir, monkeypatch, fake_save_manager, - fake_args): - if backend == 'webkit': +@pytest.mark.parametrize('backend', [usertypes.Backend.QtWebEngine, + usertypes.Backend.QtWebKit]) +def test_init(backend, qapp, tmpdir, monkeypatch, fake_save_manager): + if backend == usertypes.Backend.QtWebKit: pytest.importorskip('PyQt5.QtWebKitWidgets') + else: + assert backend == usertypes.Backend.QtWebEngine - fake_args.backend = backend monkeypatch.setattr(history.standarddir, 'data', lambda: str(tmpdir)) + monkeypatch.setattr(history.objects, 'backend', backend) history.init(qapp) hist = objreg.get('web-history') assert hist.parent() is qapp @@ -397,11 +399,11 @@ def test_init(backend, qapp, tmpdir, monkeypatch, fake_save_manager, except ImportError: QWebHistoryInterface = None - if backend == 'webkit': + if backend == usertypes.Backend.QtWebKit: default_interface = QWebHistoryInterface.defaultInterface() assert default_interface._history is hist else: - assert backend == 'webengine' + assert backend == usertypes.Backend.QtWebEngine if QWebHistoryInterface is None: default_interface = None else: diff --git a/tests/unit/browser/webkit/test_mhtml.py b/tests/unit/browser/webkit/test_mhtml.py index 3b33857e9..e5fdf4ffd 100644 --- a/tests/unit/browser/webkit/test_mhtml.py +++ b/tests/unit/browser/webkit/test_mhtml.py @@ -87,7 +87,7 @@ def test_quoted_printable_umlauts(checker): Content-Type: text/plain Content-Transfer-Encoding: quoted-printable - Die=20s=FC=DFe=20H=FCndin=20l=E4uft=20in=20die=20H=F6hle=20des=20B=E4ren + Die s=FC=DFe H=FCndin l=E4uft in die H=F6hle des B=E4ren -----=_qute-UUID-- """) @@ -128,7 +128,7 @@ def test_file_encoded_as_base64(checker): Content-Type: text/plain Content-Transfer-Encoding: quoted-printable - Image=20file=20attached + Image file attached -----=_qute-UUID Content-Location: http://a.example.com/image.png MIME-Version: 1.0 @@ -175,56 +175,56 @@ def test_files_appear_sorted(checker): Content-Type: text/plain Content-Transfer-Encoding: quoted-printable - root=20file + root file -----=_qute-UUID Content-Location: http://a.example.com/ MIME-Version: 1.0 Content-Type: text/plain Content-Transfer-Encoding: quoted-printable - file=20a + file a -----=_qute-UUID Content-Location: http://b.example.com/ MIME-Version: 1.0 Content-Type: text/plain Content-Transfer-Encoding: quoted-printable - file=20b + file b -----=_qute-UUID Content-Location: http://g.example.com/ MIME-Version: 1.0 Content-Type: text/plain Content-Transfer-Encoding: quoted-printable - file=20g + file g -----=_qute-UUID Content-Location: http://h.example.com/ MIME-Version: 1.0 Content-Type: text/plain Content-Transfer-Encoding: quoted-printable - file=20h + file h -----=_qute-UUID Content-Location: http://i.example.com/ MIME-Version: 1.0 Content-Type: text/plain Content-Transfer-Encoding: quoted-printable - file=20i + file i -----=_qute-UUID Content-Location: http://t.example.com/ MIME-Version: 1.0 Content-Type: text/plain Content-Transfer-Encoding: quoted-printable - file=20t + file t -----=_qute-UUID Content-Location: http://z.example.com/ MIME-Version: 1.0 Content-Type: text/plain Content-Transfer-Encoding: quoted-printable - file=20z + file z -----=_qute-UUID-- """) @@ -251,7 +251,7 @@ def test_empty_content_type(checker): Content-Location: http://example.com/file Content-Transfer-Encoding: quoted-printable - file=20content + file content -----=_qute-UUID-- """) @@ -283,6 +283,28 @@ def test_css_url_scanner(monkeypatch, has_cssutils, inline, style, assert urls == expected_urls +def test_quoted_printable_spaces(checker): + content = b' ' * 100 + writer = mhtml.MHTMLWriter(root_content=content, + content_location='localhost', + content_type='text/plain') + writer.write_to(checker.fp) + checker.expect(""" + Content-Type: multipart/related; boundary="---=_qute-UUID" + MIME-Version: 1.0 + + -----=_qute-UUID + Content-Location: localhost + MIME-Version: 1.0 + Content-Type: text/plain + Content-Transfer-Encoding: quoted-printable + + {}= + {}=20 + -----=_qute-UUID-- + """.format(' ' * 75, ' ' * 24)) + + class TestNoCloseBytesIO: def test_fake_close(self): diff --git a/tests/unit/browser/webkit/test_tabhistory.py b/tests/unit/browser/webkit/test_tabhistory.py index 168621558..b99d8376d 100644 --- a/tests/unit/browser/webkit/test_tabhistory.py +++ b/tests/unit/browser/webkit/test_tabhistory.py @@ -24,7 +24,7 @@ import collections from PyQt5.QtCore import QUrl, QPoint import pytest -from qutebrowser.browser.webkit import tabhistory +tabhistory = pytest.importorskip('qutebrowser.browser.webkit.tabhistory') from qutebrowser.misc.sessions import TabHistoryItem as Item from qutebrowser.utils import qtutils diff --git a/tests/unit/browser/webkit/test_webkitelem.py b/tests/unit/browser/webkit/test_webkitelem.py index eaff07f9e..c4384fa61 100644 --- a/tests/unit/browser/webkit/test_webkitelem.py +++ b/tests/unit/browser/webkit/test_webkitelem.py @@ -108,7 +108,12 @@ def get_webelem(geometry=None, frame=None, *, null=False, style=None, else: elem.classes.return_value = [] - style_dict = {'visibility': '', 'display': '', 'foo': 'bar'} + style_dict = { + 'visibility': '', + 'display': '', + 'foo': 'bar', + 'opacity': '100' + } if style is not None: style_dict.update(style) @@ -257,7 +262,6 @@ class TestWebKitElement: len, lambda e: e.has_frame(), lambda e: e.geometry(), - lambda e: e.style_property('visibility', strategy='computed'), lambda e: e.value(), lambda e: e.set_value('foo'), lambda e: e.insert_text('foo'), @@ -271,8 +275,8 @@ class TestWebKitElement: lambda e: e.rect_on_view(), lambda e: e._is_visible(None), ], ids=['str', 'getitem', 'setitem', 'delitem', 'contains', 'iter', 'len', - 'frame', 'geometry', 'style_property', 'value', 'set_value', - 'insert_text', 'is_writable', 'is_content_editable', 'is_editable', + 'frame', 'geometry', 'value', 'set_value', 'insert_text', + 'is_writable', 'is_content_editable', 'is_editable', 'is_text_input', 'remove_blank_target', 'outer_xml', 'tag_name', 'rect_on_view', 'is_visible']) def test_vanished(self, elem, code): @@ -408,9 +412,6 @@ class TestWebKitElement: elem._elem.tagName.return_value = 'SPAN' assert elem.tag_name() == 'span' - def test_style_property(self, elem): - assert elem.style_property('foo', strategy='computed') == 'bar' - def test_value(self, elem): elem._elem.evaluateJavaScript.return_value = 'js' assert elem.value() == 'js' @@ -667,8 +668,7 @@ class TestRectOnView: This is needed for all the tests calling rect_on_view or is_visible. """ config_stub.data = {'ui': {'zoom-text-only': 'true'}} - monkeypatch.setattr('qutebrowser.browser.webkit.webkitelem.config', - config_stub) + monkeypatch.setattr(webkitelem, 'config', config_stub) return config_stub @pytest.mark.parametrize('js_rect', [ @@ -795,8 +795,7 @@ class TestIsEditable: def stubbed_config(self, config_stub, monkeypatch): """Fixture to create a config stub with an input section.""" config_stub.data = {'input': {}} - monkeypatch.setattr('qutebrowser.browser.webkit.webkitelem.config', - config_stub) + monkeypatch.setattr(webkitelem, 'config', config_stub) return config_stub @pytest.mark.parametrize('tagname, attributes, editable', [ diff --git a/tests/unit/commands/test_cmdutils.py b/tests/unit/commands/test_cmdutils.py index aaf63014e..577a85540 100644 --- a/tests/unit/commands/test_cmdutils.py +++ b/tests/unit/commands/test_cmdutils.py @@ -307,7 +307,7 @@ class TestRegister: fun(*args, **kwargs) def test_choices_no_annotation(self): - # https://github.com/The-Compiler/qutebrowser/issues/1871 + # https://github.com/qutebrowser/qutebrowser/issues/1871 @cmdutils.register() @cmdutils.argument('arg', choices=['foo', 'bar']) def fun(arg): @@ -321,7 +321,7 @@ class TestRegister: cmd._get_call_args(win_id=0) def test_choices_no_annotation_kwonly(self): - # https://github.com/The-Compiler/qutebrowser/issues/1871 + # https://github.com/qutebrowser/qutebrowser/issues/1871 @cmdutils.register() @cmdutils.argument('arg', choices=['foo', 'bar']) def fun(*, arg='foo'): @@ -350,7 +350,7 @@ class TestRegister: cmd.get_pos_arg_info(2) def test_keyword_only_without_default(self): - # https://github.com/The-Compiler/qutebrowser/issues/1872 + # https://github.com/qutebrowser/qutebrowser/issues/1872 def fun(*, target): """Blah.""" pass @@ -363,7 +363,7 @@ class TestRegister: assert str(excinfo.value) == expected def test_typed_keyword_only_without_default(self): - # https://github.com/The-Compiler/qutebrowser/issues/1872 + # https://github.com/qutebrowser/qutebrowser/issues/1872 def fun(*, target: int): """Blah.""" pass @@ -423,16 +423,6 @@ class TestArgument: assert str(excinfo.value) == "Argument marked as both count/win_id!" - def test_count_and_zero_count_arg(self): - with pytest.raises(TypeError) as excinfo: - @cmdutils.argument('arg', count=False, zero_count=True) - def fun(arg=0): - """Blah.""" - pass - - expected = "zero_count argument cannot exist without count!" - assert str(excinfo.value) == expected - def test_no_docstring(self, caplog): with caplog.at_level(logging.WARNING): @cmdutils.register() @@ -456,19 +446,20 @@ class TestArgument: class TestRun: @pytest.fixture(autouse=True) - def patching(self, mode_manager, fake_args): - fake_args.backend = 'webkit' + def patch_backend(self, mode_manager, monkeypatch): + monkeypatch.setattr(command.objects, 'backend', + usertypes.Backend.QtWebKit) @pytest.mark.parametrize('backend, used, ok', [ - (usertypes.Backend.QtWebEngine, 'webengine', True), - (usertypes.Backend.QtWebEngine, 'webkit', False), - (usertypes.Backend.QtWebKit, 'webengine', False), - (usertypes.Backend.QtWebKit, 'webkit', True), - (None, 'webengine', True), - (None, 'webkit', True), + (usertypes.Backend.QtWebEngine, usertypes.Backend.QtWebEngine, True), + (usertypes.Backend.QtWebEngine, usertypes.Backend.QtWebKit, False), + (usertypes.Backend.QtWebKit, usertypes.Backend.QtWebEngine, False), + (usertypes.Backend.QtWebKit, usertypes.Backend.QtWebKit, True), + (None, usertypes.Backend.QtWebEngine, True), + (None, usertypes.Backend.QtWebKit, True), ]) - def test_backend(self, fake_args, backend, used, ok): - fake_args.backend = used + def test_backend(self, monkeypatch, backend, used, ok): + monkeypatch.setattr(command.objects, 'backend', used) cmd = _get_cmd(backend=backend) if ok: cmd.run(win_id=0) @@ -481,7 +472,7 @@ class TestRun: cmd = _get_cmd() cmd.run(win_id=0) - def test_instance_unavailable_with_backend(self, fake_args): + def test_instance_unavailable_with_backend(self, monkeypatch): """Test what happens when a backend doesn't have an objreg object. For example, QtWebEngine doesn't have 'hintmanager' registered. We make @@ -494,7 +485,8 @@ class TestRun: """Blah.""" pass - fake_args.backend = 'webkit' + monkeypatch.setattr(command.objects, 'backend', + usertypes.Backend.QtWebKit) cmd = cmdutils.cmd_dict['fun'] with pytest.raises(cmdexc.PrerequisitesError) as excinfo: cmd.run(win_id=0) diff --git a/tests/unit/commands/test_runners.py b/tests/unit/commands/test_runners.py index 77c9d8f72..a7370081b 100644 --- a/tests/unit/commands/test_runners.py +++ b/tests/unit/commands/test_runners.py @@ -31,7 +31,7 @@ class TestCommandRunner: def test_parse_all(self, cmdline_test): """Test parsing of commands. - See https://github.com/The-Compiler/qutebrowser/issues/615 + See https://github.com/qutebrowser/qutebrowser/issues/615 Args: cmdline_test: A pytest fixture which provides testcases. @@ -57,8 +57,8 @@ class TestCommandRunner: def test_parse_empty_with_alias(self, command): """An empty command should not crash. - See https://github.com/The-Compiler/qutebrowser/issues/1690 - and https://github.com/The-Compiler/qutebrowser/issues/1773 + See https://github.com/qutebrowser/qutebrowser/issues/1690 + and https://github.com/qutebrowser/qutebrowser/issues/1773 """ cr = runners.CommandRunner(0) with pytest.raises(cmdexc.NoSuchCommandError): diff --git a/tests/unit/commands/test_userscripts.py b/tests/unit/commands/test_userscripts.py index 778270551..5212b0d32 100644 --- a/tests/unit/commands/test_userscripts.py +++ b/tests/unit/commands/test_userscripts.py @@ -227,6 +227,24 @@ def test_temporary_files_failed_cleanup(caplog, qtbot, py_proc, runner): assert caplog.records[0].message.startswith(expected) +def test_unicode_error(caplog, qtbot, py_proc, runner): + cmd, args = py_proc(r""" + import os + with open(os.environ['QUTE_FIFO'], 'wb') as f: + f.write(b'\x80') + """) + with caplog.at_level(logging.ERROR): + with qtbot.waitSignal(runner.finished, timeout=10000): + runner.prepare_run(cmd, *args) + runner.store_text('') + runner.store_html('') + + assert len(caplog.records) == 1 + expected = ("Invalid unicode in userscript output: 'utf-8' codec can't " + "decode byte 0x80 in position 0: invalid start byte") + assert caplog.records[0].message == expected + + def test_unsupported(monkeypatch, tabbed_browser_stubs): monkeypatch.setattr(userscripts.os, 'name', 'toaster') with pytest.raises(userscripts.UnsupportedError) as excinfo: diff --git a/tests/unit/completion/test_completer.py b/tests/unit/completion/test_completer.py index 2c02efa44..a2b3bc7f0 100644 --- a/tests/unit/completion/test_completer.py +++ b/tests/unit/completion/test_completer.py @@ -64,8 +64,7 @@ def completion_widget_stub(): def completer_obj(qtbot, status_command_stub, config_stub, monkeypatch, stubs, completion_widget_stub): """Create the completer used for testing.""" - monkeypatch.setattr('qutebrowser.completion.completer.QTimer', - stubs.InstaTimer) + monkeypatch.setattr(completer, 'QTimer', stubs.InstaTimer) config_stub.data = {'completion': {'show': 'auto'}} return completer.Completer(status_command_stub, 0, completion_widget_stub) @@ -85,8 +84,7 @@ def instances(monkeypatch): 'editor': FakeCompletionModel(usertypes.Completion.value), } } - monkeypatch.setattr('qutebrowser.completion.completer.instances', - instances) + monkeypatch.setattr(completer, 'instances', instances) @pytest.fixture(autouse=True) @@ -129,7 +127,7 @@ def cmdutils_patch(monkeypatch, stubs): 'bind': command.Command(name='bind', handler=bind), 'tab-detach': command.Command(name='tab-detach', handler=tab_detach), }) - monkeypatch.setattr('qutebrowser.completion.completer.cmdutils', cmd_utils) + monkeypatch.setattr(completer, 'cmdutils', cmd_utils) def _set_cmd_prompt(cmd, txt): diff --git a/tests/unit/config/old_configs/qutebrowser-v0.10.0.conf b/tests/unit/config/old_configs/qutebrowser-v0.10.0.conf new file mode 100644 index 000000000..139954247 --- /dev/null +++ b/tests/unit/config/old_configs/qutebrowser-v0.10.0.conf @@ -0,0 +1,263 @@ + + +[general] +ignore-case = smart +startpage = https://start.duckduckgo.com +yank-ignored-url-parameters = ref,utm_source,utm_medium,utm_campaign,utm_term,utm_content +default-open-dispatcher = +default-page = ${startpage} +auto-search = naive +auto-save-config = true +auto-save-interval = 15000 +editor = gvim -f "{}" +editor-encoding = utf-8 +private-browsing = false +developer-extras = false +print-element-backgrounds = true +xss-auditing = false +site-specific-quirks = true +default-encoding = +new-instance-open-target = tab +new-instance-open-target.window = last-focused +log-javascript-console = debug +save-session = false +session-default-name = +url-incdec-segments = path,query + +[ui] +zoom-levels = 25%,33%,50%,67%,75%,90%,100%,110%,125%,150%,175%,200%,250%,300%,400%,500% +default-zoom = 100% +downloads-position = top +status-position = bottom +message-timeout = 2000 +message-unfocused = false +confirm-quit = never +zoom-text-only = false +frame-flattening = false +user-stylesheet = +hide-scrollbar = true +css-media-type = +smooth-scrolling = false +remove-finished-downloads = -1 +hide-statusbar = false +statusbar-padding = 1,1,0,0 +window-title-format = {perc}{title}{title_sep}qutebrowser +modal-js-dialog = false +hide-wayland-decoration = false +keyhint-blacklist = +prompt-radius = 8 +prompt-filebrowser = true + +[network] +do-not-track = true +accept-language = en-US,en +referer-header = same-domain +user-agent = +proxy = system +proxy-dns-requests = true +ssl-strict = ask +dns-prefetch = true +custom-headers = +netrc-file = + +[completion] +show = always +download-path-suggestion = path +timestamp-format = %Y-%m-%d +height = 50% +cmd-history-max-items = 100 +web-history-max-items = 1000 +quick-complete = true +shrink = false +scrollbar-width = 12 +scrollbar-padding = 2 + +[input] +timeout = 500 +partial-timeout = 5000 +insert-mode-on-plugins = false +auto-leave-insert-mode = true +auto-insert-mode = false +forward-unbound-keys = auto +spatial-navigation = false +links-included-in-focus-chain = true +rocker-gestures = false +mouse-zoom-divider = 512 + +[tabs] +background-tabs = false +select-on-remove = next +new-tab-position = next +new-tab-position-explicit = last +last-close = ignore +show = always +show-switching-delay = 800 +wrap = true +movable = true +close-mouse-button = middle +position = top +show-favicons = true +width = 20% +indicator-width = 3 +tabs-are-windows = false +title-format = {index}: {title} +title-alignment = left +mousewheel-tab-switching = true +padding = 0,0,5,5 +indicator-padding = 2,2,0,4 + +[storage] +download-directory = +prompt-download-directory = true +remember-download-directory = true +maximum-pages-in-cache = +object-cache-capacities = +offline-storage-default-quota = +offline-web-application-cache-quota = +offline-storage-database = true +offline-web-application-storage = true +local-storage = true +cache-size = + +[content] +allow-images = true +allow-javascript = true +allow-plugins = false +webgl = true +css-regions = true +hyperlink-auditing = false +geolocation = ask +notifications = ask +media-capture = ask +javascript-can-open-windows-automatically = false +javascript-can-close-windows = false +javascript-can-access-clipboard = false +ignore-javascript-prompt = false +ignore-javascript-alert = false +local-content-can-access-remote-urls = false +local-content-can-access-file-urls = true +cookies-accept = no-3rdparty +cookies-store = true +host-block-lists = https://www.malwaredomainlist.com/hostslist/hosts.txt,http://someonewhocares.org/hosts/hosts,http://winhelp2002.mvps.org/hosts.zip,http://malwaredomains.lehigh.edu/files/justdomains.zip,https://pgl.yoyo.org/adservers/serverlist.php?hostformat=hosts&mimetype=plaintext +host-blocking-enabled = true +host-blocking-whitelist = piwik.org +enable-pdfjs = false + +[hints] +border = 1px solid #E3BE23 +mode = letter +chars = asdfghjkl +min-chars = 1 +scatter = true +uppercase = false +dictionary = /usr/share/dict/words +auto-follow = unique-match +auto-follow-timeout = 0 +next-regexes = \bnext\b,\bmore\b,\bnewer\b,\b[>→≫]\b,\b(>>|»)\b,\bcontinue\b +prev-regexes = \bprev(ious)?\b,\bback\b,\bolder\b,\b[<←≪]\b,\b(<<|«)\b +find-implementation = python +hide-unmatched-rapid-hints = true + +[searchengines] +DEFAULT = https://duckduckgo.com/?q={} + +[aliases] + +[colors] +completion.fg = white +completion.bg = #333333 +completion.alternate-bg = #444444 +completion.category.fg = white +completion.category.bg = qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #888888, stop:1 #505050) +completion.category.border.top = black +completion.category.border.bottom = ${completion.category.border.top} +completion.item.selected.fg = black +completion.item.selected.bg = #e8c000 +completion.item.selected.border.top = #bbbb00 +completion.item.selected.border.bottom = ${completion.item.selected.border.top} +completion.match.fg = #ff4444 +completion.scrollbar.fg = ${completion.fg} +completion.scrollbar.bg = ${completion.bg} +statusbar.fg = white +statusbar.bg = black +statusbar.fg.insert = ${statusbar.fg} +statusbar.bg.insert = darkgreen +statusbar.fg.command = ${statusbar.fg} +statusbar.bg.command = ${statusbar.bg} +statusbar.fg.caret = ${statusbar.fg} +statusbar.bg.caret = purple +statusbar.fg.caret-selection = ${statusbar.fg} +statusbar.bg.caret-selection = #a12dff +statusbar.progress.bg = white +statusbar.url.fg = ${statusbar.fg} +statusbar.url.fg.success = white +statusbar.url.fg.success.https = lime +statusbar.url.fg.error = orange +statusbar.url.fg.warn = yellow +statusbar.url.fg.hover = aqua +tabs.fg.odd = white +tabs.bg.odd = grey +tabs.fg.even = white +tabs.bg.even = darkgrey +tabs.fg.selected.odd = white +tabs.bg.selected.odd = black +tabs.fg.selected.even = ${tabs.fg.selected.odd} +tabs.bg.selected.even = ${tabs.bg.selected.odd} +tabs.bg.bar = #555555 +tabs.indicator.start = #0000aa +tabs.indicator.stop = #00aa00 +tabs.indicator.error = #ff0000 +tabs.indicator.system = rgb +hints.fg = black +hints.bg = qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 rgba(255, 247, 133, 0.8), stop:1 rgba(255, 197, 66, 0.8)) +hints.fg.match = green +downloads.bg.bar = black +downloads.fg.start = white +downloads.bg.start = #0000aa +downloads.fg.stop = ${downloads.fg.start} +downloads.bg.stop = #00aa00 +downloads.fg.system = rgb +downloads.bg.system = rgb +downloads.fg.error = white +downloads.bg.error = red +webpage.bg = white +keyhint.fg = #FFFFFF +keyhint.fg.suffix = #FFFF00 +keyhint.bg = rgba(0, 0, 0, 80%) +messages.fg.error = white +messages.bg.error = red +messages.border.error = #bb0000 +messages.fg.warning = white +messages.bg.warning = darkorange +messages.border.warning = #d47300 +messages.fg.info = white +messages.bg.info = black +messages.border.info = #333333 +prompts.fg = white +prompts.bg = darkblue +prompts.selected.bg = #308cc6 + +[fonts] +_monospace = Terminus, Monospace, "DejaVu Sans Mono", Monaco, "Bitstream Vera Sans Mono", "Andale Mono", "Courier New", Courier, "Liberation Mono", monospace, Fixed, Consolas, Terminal +completion = 8pt ${_monospace} +completion.category = bold ${completion} +tabbar = 8pt ${_monospace} +statusbar = 8pt ${_monospace} +downloads = 8pt ${_monospace} +hints = bold 13px ${_monospace} +debug-console = 8pt ${_monospace} +web-family-standard = +web-family-fixed = +web-family-serif = +web-family-sans-serif = +web-family-cursive = +web-family-fantasy = +web-size-minimum = +web-size-minimum-logical = +web-size-default = +web-size-default-fixed = +keyhint = 8pt ${_monospace} +messages.error = 8pt ${_monospace} +messages.warning = 8pt ${_monospace} +messages.info = 8pt ${_monospace} +prompts = 8pt sans-serif diff --git a/tests/unit/config/old_configs/qutebrowser-v0.9.0.conf b/tests/unit/config/old_configs/qutebrowser-v0.9.0.conf new file mode 100644 index 000000000..8949fd417 --- /dev/null +++ b/tests/unit/config/old_configs/qutebrowser-v0.9.0.conf @@ -0,0 +1,263 @@ + + +[general] +ignore-case = smart +startpage = https://start.duckduckgo.com +yank-ignored-url-parameters = ref,utm_source,utm_medium,utm_campaign,utm_term,utm_content +default-open-dispatcher = +default-page = ${startpage} +auto-search = naive +auto-save-config = true +auto-save-interval = 15000 +editor = gvim -f "{}" +editor-encoding = utf-8 +private-browsing = false +developer-extras = false +print-element-backgrounds = true +xss-auditing = false +site-specific-quirks = true +default-encoding = +new-instance-open-target = tab +new-instance-open-target.window = last-focused +log-javascript-console = debug +save-session = false +session-default-name = +url-incdec-segments = path,query + +[ui] +zoom-levels = 25%,33%,50%,67%,75%,90%,100%,110%,125%,150%,175%,200%,250%,300%,400%,500% +default-zoom = 100% +downloads-position = top +status-position = bottom +message-timeout = 2000 +message-unfocused = false +confirm-quit = never +zoom-text-only = false +frame-flattening = false +user-stylesheet = +hide-scrollbar = true +css-media-type = +smooth-scrolling = false +remove-finished-downloads = -1 +hide-statusbar = false +statusbar-padding = 1,1,0,0 +window-title-format = {perc}{title}{title_sep}qutebrowser +modal-js-dialog = false +hide-wayland-decoration = false +keyhint-blacklist = +prompt-radius = 8 +prompt-filebrowser = true + +[network] +do-not-track = true +accept-language = en-US,en +referer-header = same-domain +user-agent = +proxy = system +proxy-dns-requests = true +ssl-strict = ask +dns-prefetch = true +custom-headers = +netrc-file = + +[completion] +show = always +download-path-suggestion = path +timestamp-format = %Y-%m-%d +height = 50% +cmd-history-max-items = 100 +web-history-max-items = 1000 +quick-complete = true +shrink = false +scrollbar-width = 12 +scrollbar-padding = 2 + +[input] +timeout = 500 +partial-timeout = 5000 +insert-mode-on-plugins = false +auto-leave-insert-mode = true +auto-insert-mode = false +forward-unbound-keys = auto +spatial-navigation = false +links-included-in-focus-chain = true +rocker-gestures = false +mouse-zoom-divider = 512 + +[tabs] +background-tabs = false +select-on-remove = next +new-tab-position = next +new-tab-position-explicit = last +last-close = ignore +show = always +show-switching-delay = 800 +wrap = true +movable = true +close-mouse-button = middle +position = top +show-favicons = true +width = 20% +indicator-width = 3 +tabs-are-windows = false +title-format = {index}: {title} +title-alignment = left +mousewheel-tab-switching = true +padding = 0,0,5,5 +indicator-padding = 2,2,0,4 + +[storage] +download-directory = +prompt-download-directory = true +remember-download-directory = true +maximum-pages-in-cache = +object-cache-capacities = +offline-storage-default-quota = +offline-web-application-cache-quota = +offline-storage-database = true +offline-web-application-storage = true +local-storage = true +cache-size = 52428800 + +[content] +allow-images = true +allow-javascript = true +allow-plugins = false +webgl = false +css-regions = true +hyperlink-auditing = false +geolocation = ask +notifications = ask +media-capture = ask +javascript-can-open-windows-automatically = false +javascript-can-close-windows = false +javascript-can-access-clipboard = false +ignore-javascript-prompt = false +ignore-javascript-alert = false +local-content-can-access-remote-urls = false +local-content-can-access-file-urls = true +cookies-accept = no-3rdparty +cookies-store = true +host-block-lists = https://www.malwaredomainlist.com/hostslist/hosts.txt,http://someonewhocares.org/hosts/hosts,http://winhelp2002.mvps.org/hosts.zip,http://malwaredomains.lehigh.edu/files/justdomains.zip,https://pgl.yoyo.org/adservers/serverlist.php?hostformat=hosts&mimetype=plaintext +host-blocking-enabled = true +host-blocking-whitelist = piwik.org +enable-pdfjs = false + +[hints] +border = 1px solid #E3BE23 +mode = letter +chars = asdfghjkl +min-chars = 1 +scatter = true +uppercase = false +dictionary = /usr/share/dict/words +auto-follow = unique-match +auto-follow-timeout = 0 +next-regexes = \bnext\b,\bmore\b,\bnewer\b,\b[>→≫]\b,\b(>>|»)\b,\bcontinue\b +prev-regexes = \bprev(ious)?\b,\bback\b,\bolder\b,\b[<←≪]\b,\b(<<|«)\b +find-implementation = python +hide-unmatched-rapid-hints = true + +[searchengines] +DEFAULT = https://duckduckgo.com/?q={} + +[aliases] + +[colors] +completion.fg = white +completion.bg = #333333 +completion.alternate-bg = #444444 +completion.category.fg = white +completion.category.bg = qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #888888, stop:1 #505050) +completion.category.border.top = black +completion.category.border.bottom = ${completion.category.border.top} +completion.item.selected.fg = black +completion.item.selected.bg = #e8c000 +completion.item.selected.border.top = #bbbb00 +completion.item.selected.border.bottom = ${completion.item.selected.border.top} +completion.match.fg = #ff4444 +completion.scrollbar.fg = ${completion.fg} +completion.scrollbar.bg = ${completion.bg} +statusbar.fg = white +statusbar.bg = black +statusbar.fg.insert = ${statusbar.fg} +statusbar.bg.insert = darkgreen +statusbar.fg.command = ${statusbar.fg} +statusbar.bg.command = ${statusbar.bg} +statusbar.fg.caret = ${statusbar.fg} +statusbar.bg.caret = purple +statusbar.fg.caret-selection = ${statusbar.fg} +statusbar.bg.caret-selection = #a12dff +statusbar.progress.bg = white +statusbar.url.fg = ${statusbar.fg} +statusbar.url.fg.success = white +statusbar.url.fg.success.https = lime +statusbar.url.fg.error = orange +statusbar.url.fg.warn = yellow +statusbar.url.fg.hover = aqua +tabs.fg.odd = white +tabs.bg.odd = grey +tabs.fg.even = white +tabs.bg.even = darkgrey +tabs.fg.selected.odd = white +tabs.bg.selected.odd = black +tabs.fg.selected.even = ${tabs.fg.selected.odd} +tabs.bg.selected.even = ${tabs.bg.selected.odd} +tabs.bg.bar = #555555 +tabs.indicator.start = #0000aa +tabs.indicator.stop = #00aa00 +tabs.indicator.error = #ff0000 +tabs.indicator.system = rgb +hints.fg = black +hints.bg = qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 rgba(255, 247, 133, 0.8), stop:1 rgba(255, 197, 66, 0.8)) +hints.fg.match = green +downloads.bg.bar = black +downloads.fg.start = white +downloads.bg.start = #0000aa +downloads.fg.stop = ${downloads.fg.start} +downloads.bg.stop = #00aa00 +downloads.fg.system = rgb +downloads.bg.system = rgb +downloads.fg.error = white +downloads.bg.error = red +webpage.bg = white +keyhint.fg = #FFFFFF +keyhint.fg.suffix = #FFFF00 +keyhint.bg = rgba(0, 0, 0, 80%) +messages.fg.error = white +messages.bg.error = red +messages.border.error = #bb0000 +messages.fg.warning = white +messages.bg.warning = darkorange +messages.border.warning = #d47300 +messages.fg.info = white +messages.bg.info = black +messages.border.info = #333333 +prompts.fg = white +prompts.bg = darkblue +prompts.selected.bg = #308cc6 + +[fonts] +_monospace = Terminus, Monospace, "DejaVu Sans Mono", Monaco, "Bitstream Vera Sans Mono", "Andale Mono", "Courier New", Courier, "Liberation Mono", monospace, Fixed, Consolas, Terminal +completion = 8pt ${_monospace} +completion.category = bold ${completion} +tabbar = 8pt ${_monospace} +statusbar = 8pt ${_monospace} +downloads = 8pt ${_monospace} +hints = bold 13px ${_monospace} +debug-console = 8pt ${_monospace} +web-family-standard = +web-family-fixed = +web-family-serif = +web-family-sans-serif = +web-family-cursive = +web-family-fantasy = +web-size-minimum = +web-size-minimum-logical = +web-size-default = +web-size-default-fixed = +keyhint = 8pt ${_monospace} +messages.error = 8pt ${_monospace} +messages.warning = 8pt ${_monospace} +messages.info = 8pt ${_monospace} +prompts = 8pt sans-serif diff --git a/tests/unit/config/test_config.py b/tests/unit/config/test_config.py index 3b6f21f7d..e98302604 100644 --- a/tests/unit/config/test_config.py +++ b/tests/unit/config/test_config.py @@ -136,7 +136,7 @@ class TestConfigParser: This is done during interpolation in later Python 3.4 versions. - See https://github.com/The-Compiler/qutebrowser/issues/968 + See https://github.com/qutebrowser/qutebrowser/issues/968 """ assert objects.cfg.get('general', 'blabla', fallback='blub') == 'blub' @@ -247,7 +247,7 @@ class TestKeyConfigParser: def test_cmd_binding(self, cmdline_test, config_stub, tmpdir): """Test various command bindings. - See https://github.com/The-Compiler/qutebrowser/issues/615 + See https://github.com/qutebrowser/qutebrowser/issues/615 Args: cmdline_test: A pytest fixture which provides testcases. @@ -281,9 +281,9 @@ class TestKeyConfigParser: ('download-page', 'download'), ('cancel-download', 'download-cancel'), - ('search ""', 'clear-keychain ;; search'), - ("search ''", 'clear-keychain ;; search'), - ("search", 'clear-keychain ;; search'), + ('search ""', 'clear-keychain ;; search ;; fullscreen --leave'), + ("search ''", 'clear-keychain ;; search ;; fullscreen --leave'), + ("search", 'clear-keychain ;; search ;; fullscreen --leave'), ("search ;; foobar", None), ('search "foo"', None), @@ -305,11 +305,16 @@ class TestKeyConfigParser: ('scroll 0 0', 'scroll-px 0 0'), ('scroll 23 42', 'scroll-px 23 42'), - ('search ;; clear-keychain', 'clear-keychain ;; search'), - ('search;;clear-keychain', 'clear-keychain ;; search'), + ('search ;; clear-keychain', + 'clear-keychain ;; search ;; fullscreen --leave'), + ('search;;clear-keychain', + 'clear-keychain ;; search ;; fullscreen --leave'), ('search;;foo', None), - ('clear-keychain ;; leave-mode', 'leave-mode'), + ('clear-keychain ;; search', + 'clear-keychain ;; search ;; fullscreen --leave'), ('leave-mode ;; foo', None), + ('search ;; clear-keychain', + 'clear-keychain ;; search ;; fullscreen --leave'), ('download-remove --all', 'download-clear'), @@ -403,7 +408,7 @@ class TestDefaultConfig: If it did change, place a new qutebrowser-vx.y.z.conf in old_configs and then increment the version. """ - assert qutebrowser.__version__ == '0.8.4' + assert qutebrowser.__version__ == '0.10.1' @pytest.mark.parametrize('filename', os.listdir(os.path.join(os.path.dirname(__file__), 'old_configs')), diff --git a/tests/unit/config/test_configtypes.py b/tests/unit/config/test_configtypes.py index 77cfdfdee..d44303278 100644 --- a/tests/unit/config/test_configtypes.py +++ b/tests/unit/config/test_configtypes.py @@ -59,16 +59,6 @@ class Font(QFont): return f -class NetworkProxy(QNetworkProxy): - - """A QNetworkProxy with a nicer repr().""" - - def __repr__(self): - return utils.get_repr(self, type=self.type(), hostName=self.hostName(), - port=self.port(), user=self.user(), - password=self.password()) - - class RegexEq: """A class to compare regex objects.""" @@ -806,8 +796,7 @@ class TestCommand: cmd_utils = stubs.FakeCmdUtils({ 'cmd1': stubs.FakeCommand(desc="desc 1"), 'cmd2': stubs.FakeCommand(desc="desc 2")}) - monkeypatch.setattr('qutebrowser.config.configtypes.cmdutils', - cmd_utils) + monkeypatch.setattr(configtypes, 'cmdutils', cmd_utils) @pytest.fixture def klass(self): @@ -890,19 +879,16 @@ class ColorTests: ('#12', []), ('foobar', []), ('42', []), - ('rgb(1, 2, 3, 4)', []), ('foo(1, 2, 3)', []), + ('rgb(1, 2, 3', []), ('rgb(0, 0, 0)', [configtypes.QssColor]), ('rgb(0,0,0)', [configtypes.QssColor]), ('-foobar(42)', [configtypes.CssColor]), - ('rgba(255, 255, 255, 255)', [configtypes.QssColor]), - ('rgba(255,255,255,255)', [configtypes.QssColor]), - ('hsv(359, 255, 255)', [configtypes.QssColor]), - ('hsva(359, 255, 255, 255)', [configtypes.QssColor]), - ('hsv(10%, 10%, 10%)', [configtypes.QssColor]), + ('rgba(255, 255, 255, 1.0)', [configtypes.QssColor]), ('hsv(10%,10%,10%)', [configtypes.QssColor]), + ('qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 white, ' 'stop: 0.4 gray, stop:1 green)', [configtypes.QssColor]), ('qconicalgradient(cx:0.5, cy:0.5, angle:30, stop:0 white, ' @@ -988,6 +974,9 @@ class TestFont: 'inconsolatazi4': FontDesc(QFont.StyleNormal, QFont.Normal, -1, -1, 'inconsolatazi4'), + 'Terminus (TTF)': + FontDesc(QFont.StyleNormal, QFont.Normal, -1, -1, + 'Terminus (TTF)'), '10pt "Foobar Neue"': FontDesc(QFont.StyleNormal, QFont.Normal, 10, None, 'Foobar Neue'), '10PT "Foobar Neue"': @@ -1052,7 +1041,7 @@ class TestFont: font_xfail('green'), font_xfail('10pt'), font_xfail('10pt ""'), - '%', + '', ]) def test_validate_invalid(self, klass, val): with pytest.raises(configexc.ValidationError): @@ -1112,7 +1101,6 @@ class TestFontFamily: 'normal bold 10pt "Foobar Neue"', 'bold italic 10pt "Foobar Neue"', '', # with none_ok=False - '%', ] @pytest.fixture @@ -1490,8 +1478,8 @@ class TestProxy: 'system', 'none', 'http://user:pass@example.com:2323/', - 'socks://user:pass@example.com:2323/', - 'socks5://user:pass@example.com:2323/', + 'pac+http://example.com/proxy.pac', + 'pac+file:///tmp/proxy.pac' ]) def test_validate_valid(self, klass, val): klass(none_ok=True).validate(val) @@ -1517,27 +1505,18 @@ class TestProxy: @pytest.mark.parametrize('val, expected', [ ('', None), ('system', configtypes.SYSTEM_PROXY), - ('none', NetworkProxy(QNetworkProxy.NoProxy)), + ('none', QNetworkProxy(QNetworkProxy.NoProxy)), ('socks://example.com/', - NetworkProxy(QNetworkProxy.Socks5Proxy, 'example.com')), - ('socks5://example.com', - NetworkProxy(QNetworkProxy.Socks5Proxy, 'example.com')), - ('socks5://example.com:2342', - NetworkProxy(QNetworkProxy.Socks5Proxy, 'example.com', 2342)), - ('socks5://foo@example.com', - NetworkProxy(QNetworkProxy.Socks5Proxy, 'example.com', 0, 'foo')), - ('socks5://foo:bar@example.com', - NetworkProxy(QNetworkProxy.Socks5Proxy, 'example.com', 0, 'foo', - 'bar')), + QNetworkProxy(QNetworkProxy.Socks5Proxy, 'example.com')), ('socks5://foo:bar@example.com:2323', - NetworkProxy(QNetworkProxy.Socks5Proxy, 'example.com', 2323, 'foo', - 'bar')), + QNetworkProxy(QNetworkProxy.Socks5Proxy, 'example.com', 2323, + 'foo', 'bar')), ]) def test_transform(self, klass, val, expected): """Test transform with an empty value.""" actual = klass().transform(val) if isinstance(actual, QNetworkProxy): - actual = NetworkProxy(actual) + actual = QNetworkProxy(actual) assert actual == expected diff --git a/tests/unit/keyinput/test_basekeyparser.py b/tests/unit/keyinput/test_basekeyparser.py index 3e55d0366..da1ecfbdf 100644 --- a/tests/unit/keyinput/test_basekeyparser.py +++ b/tests/unit/keyinput/test_basekeyparser.py @@ -245,6 +245,12 @@ class TestKeyChain: 'ba', keyparser.Type.chain, None) assert keyparser._keystring == '' + def test_0_press(self, handle_text, keyparser): + handle_text((Qt.Key_0, '0')) + keyparser.execute.assert_called_once_with( + '0', keyparser.Type.chain, None) + assert keyparser._keystring == '' + def test_ambiguous_keychain(self, qapp, handle_text, config_stub, keyparser): config_stub.data = CONFIG @@ -308,8 +314,9 @@ class TestCount: def test_count_0(self, handle_text, keyparser): handle_text((Qt.Key_0, '0'), (Qt.Key_B, 'b'), (Qt.Key_A, 'a')) - keyparser.execute.assert_called_once_with( - 'ba', keyparser.Type.chain, 0) + calls = [mock.call('0', keyparser.Type.chain, None), + mock.call('ba', keyparser.Type.chain, None)] + keyparser.execute.assert_has_calls(calls) assert keyparser._keystring == '' def test_count_42(self, handle_text, keyparser): diff --git a/tests/unit/keyinput/test_modeman.py b/tests/unit/keyinput/test_modeman.py index b9f2db572..aa3a2753a 100644 --- a/tests/unit/keyinput/test_modeman.py +++ b/tests/unit/keyinput/test_modeman.py @@ -47,7 +47,7 @@ def modeman(mode_manager): @pytest.mark.parametrize('key, modifiers, text, filtered', [ (Qt.Key_A, Qt.NoModifier, 'a', True), (Qt.Key_Up, Qt.NoModifier, '', False), - # https://github.com/The-Compiler/qutebrowser/issues/1207 + # https://github.com/qutebrowser/qutebrowser/issues/1207 (Qt.Key_A, Qt.ShiftModifier, 'A', True), (Qt.Key_A, Qt.ShiftModifier | Qt.ControlModifier, 'x', False), ]) diff --git a/tests/unit/keyinput/test_modeparsers.py b/tests/unit/keyinput/test_modeparsers.py index 09697c408..c01e0e5ba 100644 --- a/tests/unit/keyinput/test_modeparsers.py +++ b/tests/unit/keyinput/test_modeparsers.py @@ -46,8 +46,7 @@ class TestsNormalKeyParser: 'qutebrowser.keyinput.basekeyparser.usertypes.Timer', stubs.FakeTimer) config_stub.data = CONFIG - monkeypatch.setattr('qutebrowser.keyinput.modeparsers.config', - config_stub) + monkeypatch.setattr(modeparsers, 'config', config_stub) @pytest.fixture def keyparser(self): diff --git a/tests/unit/mainwindow/statusbar/test_progress.py b/tests/unit/mainwindow/statusbar/test_progress.py index 5696b269c..70865e104 100644 --- a/tests/unit/mainwindow/statusbar/test_progress.py +++ b/tests/unit/mainwindow/statusbar/test_progress.py @@ -77,8 +77,8 @@ def test_tab_changed(fake_web_tab, progress_widget, progress, load_status, def test_progress_affecting_statusbar_height(fake_statusbar, progress_widget): """Make sure the statusbar stays the same height when progress is shown. - https://github.com/The-Compiler/qutebrowser/issues/886 - https://github.com/The-Compiler/qutebrowser/pull/890 + https://github.com/qutebrowser/qutebrowser/issues/886 + https://github.com/qutebrowser/qutebrowser/pull/890 """ expected_height = fake_statusbar.fontMetrics().height() assert fake_statusbar.height() == expected_height @@ -92,7 +92,7 @@ def test_progress_affecting_statusbar_height(fake_statusbar, progress_widget): def test_progress_big_statusbar(qtbot, fake_statusbar, progress_widget): """Make sure the progress bar is small with a big statusbar. - https://github.com/The-Compiler/qutebrowser/commit/46d1760798b730852e2207e2cdc05a9308e44f80 + https://github.com/qutebrowser/qutebrowser/commit/46d1760798b730852e2207e2cdc05a9308e44f80 """ fake_statusbar.hbox.addWidget(progress_widget) progress_widget.show() diff --git a/tests/unit/mainwindow/test_messageview.py b/tests/unit/mainwindow/test_messageview.py index 310ea7180..740c090b6 100644 --- a/tests/unit/mainwindow/test_messageview.py +++ b/tests/unit/mainwindow/test_messageview.py @@ -101,3 +101,16 @@ def test_changing_timer_with_messages_shown(qtbot, view, config_stub): view.show_message(usertypes.MessageLevel.info, 'test') with qtbot.waitSignal(view._clear_timer.timeout): config_stub.set('ui', 'message-timeout', 100) + + +@pytest.mark.parametrize('replace1, replace2, length', [ + (False, False, 2), # Two stacked messages + (True, True, 1), # Two replaceable messages + (False, True, 2), # Stacked and replaceable + (True, False, 2), # Replaceable and stacked +]) +def test_replaced_messages(view, replace1, replace2, length): + """Show two stack=False messages which should replace each other.""" + view.show_message(usertypes.MessageLevel.info, 'test', replace=replace1) + view.show_message(usertypes.MessageLevel.info, 'test 2', replace=replace2) + assert len(view._messages) == length diff --git a/tests/unit/mainwindow/test_prompt.py b/tests/unit/mainwindow/test_prompt.py index 1288ccd3f..dcc5461a5 100644 --- a/tests/unit/mainwindow/test_prompt.py +++ b/tests/unit/mainwindow/test_prompt.py @@ -34,7 +34,7 @@ def setup(qapp, key_config_stub): class TestFileCompletion: @pytest.fixture - def get_prompt(self, qtbot): + def get_prompt(self, qtbot, config_stub): """Get a function to display a prompt with a path.""" def _get_prompt_func(path): question = usertypes.Question() @@ -48,6 +48,7 @@ class TestFileCompletion: assert prompt._lineedit.text() == path return prompt + config_stub.data = {'ui': {'prompt-filebrowser': 'true'}} return _get_prompt_func @pytest.mark.parametrize('steps, where, subfolder', [ diff --git a/tests/unit/mainwindow/test_tabwidget.py b/tests/unit/mainwindow/test_tabwidget.py index 4329f12db..b9cfb5d89 100644 --- a/tests/unit/mainwindow/test_tabwidget.py +++ b/tests/unit/mainwindow/test_tabwidget.py @@ -21,11 +21,13 @@ import pytest -from qutebrowser.mainwindow import tabwidget -from qutebrowser.config import configtypes from PyQt5.QtGui import QIcon, QPixmap, QFont, QColor from PyQt5.QtCore import Qt +from qutebrowser.mainwindow import tabwidget +from qutebrowser.config import configtypes +from qutebrowser.utils import usertypes + class TestTabWidget: @@ -58,10 +60,12 @@ class TestTabWidget: } @pytest.fixture - def widget(self, qtbot, config_stub): + def widget(self, qtbot, monkeypatch, config_stub): config_stub.data = self.CONFIG w = tabwidget.TabWidget(0) qtbot.addWidget(w) + monkeypatch.setattr(tabwidget.objects, 'backend', + usertypes.Backend.QtWebKit) return w def test_small_icon_doesnt_crash(self, widget, qtbot, fake_web_tab): diff --git a/tests/unit/misc/test_checkpyver.py b/tests/unit/misc/test_checkpyver.py index c0b107c4e..4c2cf3b7d 100644 --- a/tests/unit/misc/test_checkpyver.py +++ b/tests/unit/misc/test_checkpyver.py @@ -58,12 +58,10 @@ def test_normal(capfd): def test_patched_no_errwindow(capfd, monkeypatch): """Test with a patched sys.hexversion and --no-err-windows.""" - monkeypatch.setattr('qutebrowser.misc.checkpyver.sys.argv', + monkeypatch.setattr(checkpyver.sys, 'argv', [sys.argv[0], '--no-err-windows']) - monkeypatch.setattr('qutebrowser.misc.checkpyver.sys.hexversion', - 0x03000000) - monkeypatch.setattr('qutebrowser.misc.checkpyver.sys.exit', - lambda status: None) + monkeypatch.setattr(checkpyver.sys, 'hexversion', 0x03000000) + monkeypatch.setattr(checkpyver.sys, 'exit', lambda status: None) checkpyver.check_python_version() stdout, stderr = capfd.readouterr() assert not stdout @@ -72,10 +70,8 @@ def test_patched_no_errwindow(capfd, monkeypatch): def test_patched_errwindow(capfd, mocker, monkeypatch): """Test with a patched sys.hexversion and a fake Tk.""" - monkeypatch.setattr('qutebrowser.misc.checkpyver.sys.hexversion', - 0x03000000) - monkeypatch.setattr('qutebrowser.misc.checkpyver.sys.exit', - lambda status: None) + monkeypatch.setattr(checkpyver.sys, 'hexversion', 0x03000000) + monkeypatch.setattr(checkpyver.sys, 'exit', lambda status: None) try: import tkinter # pylint: disable=unused-variable diff --git a/tests/unit/misc/test_editor.py b/tests/unit/misc/test_editor.py index bae8bd035..193f5fa30 100644 --- a/tests/unit/misc/test_editor.py +++ b/tests/unit/misc/test_editor.py @@ -33,13 +33,13 @@ from qutebrowser.utils import usertypes @pytest.fixture(autouse=True) def patch_things(config_stub, monkeypatch, stubs): - monkeypatch.setattr('qutebrowser.misc.editor.guiprocess.QProcess', + monkeypatch.setattr(editormod.guiprocess, 'QProcess', stubs.fake_qprocess()) config_stub.data = { 'general': {'editor': [''], 'editor-encoding': 'utf-8'}, 'input': {}, } - monkeypatch.setattr('qutebrowser.misc.editor.config', config_stub) + monkeypatch.setattr(editormod, 'config', config_stub) @pytest.fixture @@ -141,8 +141,7 @@ class TestFileHandling: caplog): """Test file handling when the initial file is not writable.""" tmpdir.chmod(0) - monkeypatch.setattr('qutebrowser.misc.editor.tempfile.tempdir', - str(tmpdir)) + monkeypatch.setattr(editormod.tempfile, 'tempdir', str(tmpdir)) with caplog.at_level(logging.ERROR): editor.edit("") diff --git a/tests/unit/misc/test_guiprocess.py b/tests/unit/misc/test_guiprocess.py index 4341284df..865e91a84 100644 --- a/tests/unit/misc/test_guiprocess.py +++ b/tests/unit/misc/test_guiprocess.py @@ -205,8 +205,7 @@ def test_exit_unsuccessful_output(qtbot, proc, caplog, py_proc, stream): print("test", file=sys.{}) sys.exit(1) """.format(stream))) - assert len(caplog.records) == 2 - assert caplog.records[1].msg == 'Process {}:\ntest'.format(stream) + assert caplog.records[-1].msg == 'Process {}:\ntest'.format(stream) @pytest.mark.parametrize('stream', ['stdout', 'stderr']) diff --git a/tests/unit/misc/test_ipc.py b/tests/unit/misc/test_ipc.py index 9b185f764..a942229bd 100644 --- a/tests/unit/misc/test_ipc.py +++ b/tests/unit/misc/test_ipc.py @@ -101,7 +101,6 @@ class FakeSocket(QObject): _error_val: The value returned for error(). _state_val: The value returned for state(). _connect_successful: The value returned for waitForConnected(). - deleted: Set to True if deleteLater() was called. """ readyRead = pyqtSignal() @@ -115,7 +114,6 @@ class FakeSocket(QObject): self._data = data self._connect_successful = connect_successful self.error = stubs.FakeSignal('error', func=self._error) - self.deleted = False def _error(self): return self._error_val @@ -131,9 +129,6 @@ class FakeSocket(QObject): self._data = rest return firstline + mid - def deleteLater(self): - self.deleted = True - def errorString(self): return "Error string" @@ -199,8 +194,7 @@ class TestSocketName: @pytest.fixture(autouse=True) def patch_user(self, monkeypatch): - monkeypatch.setattr('qutebrowser.misc.ipc.getpass.getuser', - lambda: 'testusername') + monkeypatch.setattr(ipc.getpass, 'getuser', lambda: 'testusername') @pytest.mark.parametrize('basedir, expected', LEGACY_TESTS) def test_legacy(self, basedir, expected): @@ -281,7 +275,7 @@ class TestListen: def test_error(self, ipc_server, monkeypatch): """Simulate an error while listening.""" - monkeypatch.setattr('qutebrowser.misc.ipc.QLocalServer.removeServer', + monkeypatch.setattr(ipc.QLocalServer, 'removeServer', lambda self: True) monkeypatch.setattr(ipc_server, '_socketname', None) with pytest.raises(ipc.ListenError): @@ -289,7 +283,7 @@ class TestListen: @pytest.mark.posix def test_in_use(self, qlocalserver, ipc_server, monkeypatch): - monkeypatch.setattr('qutebrowser.misc.ipc.QLocalServer.removeServer', + monkeypatch.setattr(ipc.QLocalServer, 'removeServer', lambda self: True) qlocalserver.listen('qute-test') with pytest.raises(ipc.AddressInUseError): @@ -443,7 +437,7 @@ class TestHandleConnection: def connected_socket(qtbot, qlocalsocket, ipc_server): if sys.platform == 'darwin': pytest.skip("Skipping connected_socket test - " - "https://github.com/The-Compiler/qutebrowser/issues/1045") + "https://github.com/qutebrowser/qutebrowser/issues/1045") ipc_server.listen() with qtbot.waitSignal(ipc_server._server.newConnection): qlocalsocket.connectToServer('qute-test') @@ -577,7 +571,7 @@ class TestSendToRunningInstance: assert str(excinfo.value) == msg -@pytest.mark.not_osx(reason="https://github.com/The-Compiler/qutebrowser/" +@pytest.mark.not_osx(reason="https://github.com/qutebrowser/qutebrowser/" "issues/975") def test_timeout(qtbot, caplog, qlocalsocket, ipc_server): ipc_server._timer.setInterval(100) @@ -831,11 +825,10 @@ class TestSendOrListen: @pytest.mark.windows @pytest.mark.osx def test_long_username(monkeypatch): - """See https://github.com/The-Compiler/qutebrowser/issues/888.""" + """See https://github.com/qutebrowser/qutebrowser/issues/888.""" username = 'alexandercogneau' basedir = '/this_is_a_long_basedir' - monkeypatch.setattr('qutebrowser.misc.ipc.standarddir.getpass.getuser', - lambda: username) + monkeypatch.setattr('getpass.getuser', lambda: username) name = ipc._get_socketname(basedir=basedir) server = ipc.IPCServer(name) expected_md5 = md5('{}-{}'.format(username, basedir)) diff --git a/tests/unit/misc/test_miscwidgets.py b/tests/unit/misc/test_miscwidgets.py index 3e25d0080..dd17ac254 100644 --- a/tests/unit/misc/test_miscwidgets.py +++ b/tests/unit/misc/test_miscwidgets.py @@ -100,3 +100,26 @@ class TestWrapperLayout: def test_wrapped(self, container): assert container.wrapped.parent() is container assert container.focusProxy() is container.wrapped + + +class TestFullscreenNotification: + + @pytest.mark.parametrize('bindings, text', [ + ({'': 'fullscreen --leave'}, + "Press Escape to exit fullscreen."), + ({'': 'fullscreen'}, "Page is now fullscreen."), + ({'a': 'fullscreen --leave'}, "Press a to exit fullscreen."), + ({}, "Page is now fullscreen."), + ]) + def test_text(self, qtbot, key_config_stub, bindings, text): + key_config_stub.set_bindings_for('normal', bindings) + w = miscwidgets.FullscreenNotification() + qtbot.add_widget(w) + assert w.text() == text + + def test_timeout(self, qtbot, key_config_stub): + key_config_stub.set_bindings_for('normal', {}) + w = miscwidgets.FullscreenNotification() + qtbot.add_widget(w) + with qtbot.waitSignal(w.destroyed): + w.set_timeout(1) diff --git a/tests/unit/misc/test_readline.py b/tests/unit/misc/test_readline.py index 70bd887e4..b0c1403a1 100644 --- a/tests/unit/misc/test_readline.py +++ b/tests/unit/misc/test_readline.py @@ -29,7 +29,7 @@ from qutebrowser.misc import readline # Some functions aren't 100% readline compatible: -# https://github.com/The-Compiler/qutebrowser/issues/678 +# https://github.com/qutebrowser/qutebrowser/issues/678 # Those are marked with fixme and have another value marked with '# wrong' # which marks the current behavior. diff --git a/tests/unit/misc/test_sessions.py b/tests/unit/misc/test_sessions.py index f1810c203..4bef9b9a4 100644 --- a/tests/unit/misc/test_sessions.py +++ b/tests/unit/misc/test_sessions.py @@ -54,8 +54,7 @@ class TestInit: @pytest.mark.parametrize('create_dir', [True, False]) def test_with_standarddir(self, tmpdir, monkeypatch, create_dir): - monkeypatch.setattr('qutebrowser.misc.sessions.standarddir.data', - lambda: str(tmpdir)) + monkeypatch.setattr(sessions.standarddir, 'data', lambda: str(tmpdir)) session_dir = tmpdir / 'sessions' if create_dir: session_dir.ensure(dir=True) @@ -181,7 +180,7 @@ class TestSaveAll: def test_no_active_window(self, sess_man, fake_window, stubs, monkeypatch): qapp = stubs.FakeQApplication(active_window=None) - monkeypatch.setattr('qutebrowser.misc.sessions.QApplication', qapp) + monkeypatch.setattr(sessions, 'QApplication', qapp) sess_man._save_all() @@ -216,7 +215,7 @@ class TestSave: objreg.register('tabbed-browser', browser, scope='window', window=0) qapp = stubs.FakeQApplication(active_window=win) - monkeypatch.setattr('qutebrowser.misc.sessions.QApplication', qapp) + monkeypatch.setattr(sessions, 'QApplication', qapp) def set_data(items): history = browser.widgets()[0].page().history() @@ -353,7 +352,7 @@ class TestLoadTab: if in_main_data: # This information got saved in the main data instead of saving it # per item - make sure the old format can still be read - # https://github.com/The-Compiler/qutebrowser/issues/728 + # https://github.com/qutebrowser/qutebrowser/issues/728 d = {'history': [item], key: val} else: item[key] = val diff --git a/tests/unit/scripts/test_check_coverage.py b/tests/unit/scripts/test_check_coverage.py index 50406a569..70f429aa2 100644 --- a/tests/unit/scripts/test_check_coverage.py +++ b/tests/unit/scripts/test_check_coverage.py @@ -63,7 +63,7 @@ class CovtestHelper: perfect_files = [(None, 'module.py')] argv = [sys.argv[0]] - self._monkeypatch.setattr('scripts.dev.check_coverage.sys.argv', argv) + self._monkeypatch.setattr(check_coverage.sys, 'argv', argv) with self._testdir.tmpdir.as_cwd(): with coverage_file.open(encoding='utf-8') as f: @@ -72,7 +72,7 @@ class CovtestHelper: def check_skipped(self, args, reason): """Run check_coverage.py and make sure it's skipped.""" argv = [sys.argv[0]] + list(args) - self._monkeypatch.setattr('scripts.dev.check_coverage.sys.argv', argv) + self._monkeypatch.setattr(check_coverage.sys, 'argv', argv) with pytest.raises(check_coverage.Skipped) as excinfo: return check_coverage.check(None, perfect_files=[]) assert excinfo.value.reason == reason @@ -179,7 +179,7 @@ def test_skipped_args(covtest, args, reason): def test_skipped_windows(covtest, monkeypatch): - monkeypatch.setattr('scripts.dev.check_coverage.sys.platform', 'toaster') + monkeypatch.setattr(check_coverage.sys, 'platform', 'toaster') covtest.check_skipped([], "on non-Linux system.") diff --git a/tests/unit/utils/test_debug.py b/tests/unit/utils/test_debug.py index 7b00502f7..04a2fa436 100644 --- a/tests/unit/utils/test_debug.py +++ b/tests/unit/utils/test_debug.py @@ -157,7 +157,7 @@ class TestQFlagsKey: """Tests for qutebrowser.utils.debug.qflags_key. - https://github.com/The-Compiler/qutebrowser/issues/42 + https://github.com/qutebrowser/qutebrowser/issues/42 """ fixme = pytest.mark.xfail(reason="See issue #42", raises=AssertionError) diff --git a/tests/unit/utils/test_error.py b/tests/unit/utils/test_error.py index c8440819b..abcc72c44 100644 --- a/tests/unit/utils/test_error.py +++ b/tests/unit/utils/test_error.py @@ -67,7 +67,7 @@ def test_no_err_windows(caplog, exc, name, exc_text, fake_args): # This happens on Xvfb for some reason -# See https://github.com/The-Compiler/qutebrowser/issues/984 +# See https://github.com/qutebrowser/qutebrowser/issues/984 @pytest.mark.qt_log_ignore(r'^QXcbConnection: XCB error: 8 \(BadMatch\), ' r'sequence: \d+, resource id: \d+, major code: 42 ' r'\(SetInputFocus\), minor code: 0$', diff --git a/tests/unit/utils/test_jinja.py b/tests/unit/utils/test_jinja.py index e951e3994..289b2c26b 100644 --- a/tests/unit/utils/test_jinja.py +++ b/tests/unit/utils/test_jinja.py @@ -24,7 +24,6 @@ import os.path import pytest import logging -import jinja2 from PyQt5.QtCore import QUrl from qutebrowser.utils import utils, jinja @@ -67,9 +66,8 @@ def patch_read_file(monkeypatch): else: raise IOError("Invalid path {}!".format(path)) - monkeypatch.setattr('qutebrowser.utils.jinja.utils.read_file', _read_file) - monkeypatch.setattr('qutebrowser.utils.jinja.utils.resource_filename', - _resource_filename) + monkeypatch.setattr(jinja.utils, 'read_file', _read_file) + monkeypatch.setattr(jinja.utils, 'resource_filename', _resource_filename) def test_simple_template(): @@ -105,11 +103,14 @@ def test_data_url(): assert data == 'data:text/plain;base64,Zm9v' # 'foo' -def test_not_found(): +def test_not_found(caplog): """Test with a template which does not exist.""" - with pytest.raises(jinja2.TemplateNotFound) as excinfo: - jinja.render('does_not_exist.html') - assert str(excinfo.value) == 'does_not_exist.html' + with caplog.at_level(logging.ERROR): + data = jinja.render('does_not_exist.html') + assert "The does_not_exist.html template could not be found!" in data + + assert caplog.records[0].msg.startswith("The does_not_exist.html template" + " could not be loaded from") def test_utf8(): @@ -118,7 +119,7 @@ def test_utf8(): This was an attempt to get a failing test case for #127 but it seems the issue is elsewhere. - https://github.com/The-Compiler/qutebrowser/issues/127 + https://github.com/qutebrowser/qutebrowser/issues/127 """ data = jinja.render('test.html', var='\u2603') assert data == "Hello \u2603" diff --git a/tests/unit/utils/test_log.py b/tests/unit/utils/test_log.py index a96e13402..3c1e82fc0 100644 --- a/tests/unit/utils/test_log.py +++ b/tests/unit/utils/test_log.py @@ -68,7 +68,7 @@ def restore_loggers(): root_logger.setLevel(original_logging_level) for h in root_handlers: if not isinstance(h, pytest_catchlog.LogCaptureHandler): - # https://github.com/The-Compiler/qutebrowser/issues/856 + # https://github.com/qutebrowser/qutebrowser/issues/856 root_logger.addHandler(h) logging._acquireLock() try: diff --git a/tests/unit/utils/test_qtutils.py b/tests/unit/utils/test_qtutils.py index 8ac783505..b2de7b58d 100644 --- a/tests/unit/utils/test_qtutils.py +++ b/tests/unit/utils/test_qtutils.py @@ -22,7 +22,6 @@ import io import os import sys -import operator import os.path try: # pylint: disable=no-name-in-module,useless-suppression @@ -42,26 +41,55 @@ from qutebrowser.utils import qtutils import overflow_test_cases -@pytest.mark.parametrize('qversion, version, op, expected', [ - ('5.4.0', '5.4.0', operator.ge, True), - ('5.4.0', '5.4.0', operator.eq, True), - ('5.4.0', '5.4', operator.eq, True), - ('5.4.1', '5.4', operator.ge, True), - ('5.3.2', '5.4', operator.ge, False), - ('5.3.0', '5.3.2', operator.ge, False), +@pytest.mark.parametrize('qversion, compiled, version, exact, expected', [ + # equal versions + ('5.4.0', None, '5.4.0', False, True), + ('5.4.0', None, '5.4.0', True, True), # exact=True + ('5.4.0', None, '5.4', True, True), # without trailing 0 + # newer version installed + ('5.4.1', None, '5.4', False, True), + ('5.4.1', None, '5.4', True, False), # exact=True + # older version installed + ('5.3.2', None, '5.4', False, False), + ('5.3.0', None, '5.3.2', False, False), + ('5.3.0', None, '5.3.2', True, False), # exact=True + # strict + ('5.4.0', '5.3.0', '5.4.0', False, False), + ('5.4.0', '5.4.0', '5.4.0', False, True), + # strict and exact=True + ('5.4.0', '5.5.0', '5.4.0', True, False), + ('5.5.0', '5.4.0', '5.4.0', True, False), + ('5.4.0', '5.4.0', '5.4.0', True, True), ]) -def test_version_check(monkeypatch, qversion, version, op, expected): +def test_version_check(monkeypatch, qversion, compiled, version, exact, + expected): """Test for version_check(). Args: monkeypatch: The pytest monkeypatch fixture. qversion: The version to set as fake qVersion(). + compiled: The value for QT_VERSION_STR (set strict=True) version: The version to compare with. - op: The operator to use when comparing. + exact: Use exact comparing (==) expected: The expected result. """ - monkeypatch.setattr('qutebrowser.utils.qtutils.qVersion', lambda: qversion) - assert qtutils.version_check(version, op) == expected + monkeypatch.setattr(qtutils, 'qVersion', lambda: qversion) + if compiled is not None: + monkeypatch.setattr(qtutils, 'QT_VERSION_STR', compiled) + strict = True + else: + strict = False + + assert qtutils.version_check(version, exact, strict=strict) == expected + + +@pytest.mark.parametrize('version, ng', [ + ('537.21', False), # QtWebKit 5.1 + ('538.1', False), # Qt 5.8 + ('602.1', True) # QtWebKit-NG TP5 +]) +def test_is_qtwebkit_ng(version, ng): + assert qtutils.is_qtwebkit_ng(version) == ng class TestCheckOverflow: @@ -108,10 +136,16 @@ class TestGetQtArgs: # No Qt arguments (['--debug'], [sys.argv[0]]), # Qt flag - (['--debug', '--qt-flag', 'reverse'], [sys.argv[0], '-reverse']), + (['--debug', '--qt-flag', 'reverse'], [sys.argv[0], '--reverse']), # Qt argument with value (['--qt-arg', 'stylesheet', 'foo'], - [sys.argv[0], '-stylesheet', 'foo']), + [sys.argv[0], '--stylesheet', 'foo']), + # --qt-arg given twice + (['--qt-arg', 'stylesheet', 'foo', '--qt-arg', 'geometry', 'bar'], + [sys.argv[0], '--stylesheet', 'foo', '--geometry', 'bar']), + # --qt-flag given twice + (['--qt-flag', 'foo', '--qt-flag', 'bar'], + [sys.argv[0], '--foo', '--bar']), ]) def test_qt_args(self, args, expected, parser): """Test commandline with no Qt arguments given.""" @@ -124,8 +158,8 @@ class TestGetQtArgs: '--qt-flag', 'reverse']) qt_args = qtutils.get_args(args) assert qt_args[0] == sys.argv[0] - assert '-reverse' in qt_args - assert '-stylesheet' in qt_args + assert '--reverse' in qt_args + assert '--stylesheet' in qt_args assert 'foobar' in qt_args @@ -144,8 +178,8 @@ def test_check_print_compat(os_name, qversion, expected, monkeypatch): qversion: The fake qVersion() to set. expected: The expected return value. """ - monkeypatch.setattr('qutebrowser.utils.qtutils.os.name', os_name) - monkeypatch.setattr('qutebrowser.utils.qtutils.qVersion', lambda: qversion) + monkeypatch.setattr(qtutils.os, 'name', os_name) + monkeypatch.setattr(qtutils, 'qVersion', lambda: qversion) assert qtutils.check_print_compat() == expected @@ -506,7 +540,7 @@ class TestSavefileOpen: def test_line_endings(self, tmpdir): """Make sure line endings are translated correctly. - See https://github.com/The-Compiler/qutebrowser/issues/309 + See https://github.com/qutebrowser/qutebrowser/issues/309 """ filename = tmpdir / 'foo' with qtutils.savefile_open(str(filename)) as f: diff --git a/tests/unit/utils/test_standarddir.py b/tests/unit/utils/test_standarddir.py index eb348132b..69acd2884 100644 --- a/tests/unit/utils/test_standarddir.py +++ b/tests/unit/utils/test_standarddir.py @@ -48,8 +48,7 @@ def change_qapp_name(qapp): @pytest.fixture def no_cachedir_tag(monkeypatch): """Fixture to prevent writing a CACHEDIR.TAG.""" - monkeypatch.setattr('qutebrowser.utils.standarddir._init_cachedir_tag', - lambda: None) + monkeypatch.setattr(standarddir, '_init_cachedir_tag', lambda: None) @pytest.fixture @@ -72,10 +71,9 @@ def test_get_fake_windows_equal_dir(data_subdir, config_subdir, expected, QStandardPaths.DataLocation: str(tmpdir / data_subdir), QStandardPaths.ConfigLocation: str(tmpdir / config_subdir), } - monkeypatch.setattr('qutebrowser.utils.standarddir.os.name', 'nt') - monkeypatch.setattr( - 'qutebrowser.utils.standarddir.QStandardPaths.writableLocation', - locations.get) + monkeypatch.setattr(standarddir.os, 'name', 'nt') + monkeypatch.setattr(standarddir.QStandardPaths, 'writableLocation', + locations.get) expected = str(tmpdir / expected) assert standarddir.data() == expected @@ -90,12 +88,12 @@ class TestWritableLocation: monkeypatch.setattr( 'qutebrowser.utils.standarddir.QStandardPaths.writableLocation', lambda typ: '') - with pytest.raises(ValueError): + with pytest.raises(standarddir.EmptyValueError): standarddir._writable_location(QStandardPaths.DataLocation) def test_sep(self, monkeypatch): """Make sure the right kind of separator is used.""" - monkeypatch.setattr('qutebrowser.utils.standarddir.os.sep', '\\') + monkeypatch.setattr(standarddir.os, 'sep', '\\') loc = standarddir._writable_location(QStandardPaths.DataLocation) assert '/' not in loc assert '\\' in loc @@ -137,6 +135,22 @@ class TestStandardDir: monkeypatch.delenv('XDG_{}_HOME'.format(var), raising=False) assert func() == str(tmpdir.join(*subdirs)) + @pytest.mark.linux + @pytest.mark.qt_log_ignore(r'^QStandardPaths: ') + def test_linux_invalid_runtimedir(self, monkeypatch, tmpdir): + """With invalid XDG_RUNTIME_DIR, fall back to TempLocation.""" + monkeypatch.setenv('XDG_RUNTIME_DIR', str(tmpdir / 'does-not-exist')) + monkeypatch.setenv('TMPDIR', str(tmpdir / 'temp')) + assert standarddir.runtime() == str(tmpdir / 'temp' / 'qute_test') + + def test_runtimedir_empty_tempdir(self, monkeypatch, tmpdir): + """With an empty tempdir on non-Linux, we should raise.""" + monkeypatch.setattr(standarddir.sys, 'platform', 'nt') + monkeypatch.setattr(standarddir.QStandardPaths, 'writableLocation', + lambda typ: '') + with pytest.raises(standarddir.EmptyValueError): + standarddir.runtime() + @pytest.mark.parametrize('func, elems, expected', [ (standarddir.data, 2, ['qute_test', 'data']), (standarddir.config, 1, ['qute_test']), @@ -192,8 +206,7 @@ class TestInitCacheDirTag: def test_existent_cache_dir_tag(self, tmpdir, mocker, monkeypatch): """Test with an existent CACHEDIR.TAG.""" - monkeypatch.setattr('qutebrowser.utils.standarddir.cache', - lambda: str(tmpdir)) + monkeypatch.setattr(standarddir, 'cache', lambda: str(tmpdir)) mocker.patch('builtins.open', side_effect=AssertionError) m = mocker.patch('qutebrowser.utils.standarddir.os') m.path.join.side_effect = os.path.join @@ -204,8 +217,7 @@ class TestInitCacheDirTag: def test_new_cache_dir_tag(self, tmpdir, mocker, monkeypatch): """Test creating a new CACHEDIR.TAG.""" - monkeypatch.setattr('qutebrowser.utils.standarddir.cache', - lambda: str(tmpdir)) + monkeypatch.setattr(standarddir, 'cache', lambda: str(tmpdir)) standarddir._init_cachedir_tag() assert tmpdir.listdir() == [(tmpdir / 'CACHEDIR.TAG')] data = (tmpdir / 'CACHEDIR.TAG').read_text('utf-8') @@ -218,8 +230,7 @@ class TestInitCacheDirTag: def test_open_oserror(self, caplog, tmpdir, mocker, monkeypatch): """Test creating a new CACHEDIR.TAG.""" - monkeypatch.setattr('qutebrowser.utils.standarddir.cache', - lambda: str(tmpdir)) + monkeypatch.setattr(standarddir, 'cache', lambda: str(tmpdir)) mocker.patch('builtins.open', side_effect=OSError) with caplog.at_level(logging.ERROR, 'init'): standarddir._init_cachedir_tag() @@ -255,7 +266,7 @@ class TestCreatingDir: def test_exists_race_condition(self, mocker, tmpdir, typ): """Make sure there can't be a TOCTOU issue when creating the file. - See https://github.com/The-Compiler/qutebrowser/issues/942. + See https://github.com/qutebrowser/qutebrowser/issues/942. """ (tmpdir / typ).ensure(dir=True) diff --git a/tests/unit/utils/test_urlutils.py b/tests/unit/utils/test_urlutils.py index 984971b3c..f944f95b2 100644 --- a/tests/unit/utils/test_urlutils.py +++ b/tests/unit/utils/test_urlutils.py @@ -24,9 +24,11 @@ import collections import logging from PyQt5.QtCore import QUrl +from PyQt5.QtNetwork import QNetworkProxy import pytest from qutebrowser.commands import cmdexc +from qutebrowser.browser.network import pac from qutebrowser.utils import utils, urlutils, qtutils, usertypes @@ -82,8 +84,7 @@ def fake_dns(monkeypatch): fromname_mock will be called without answer being set. """ dns = FakeDNS() - monkeypatch.setattr('qutebrowser.utils.urlutils.QHostInfo.fromName', - dns.fromname_mock) + monkeypatch.setattr(urlutils.QHostInfo, 'fromName', dns.fromname_mock) return dns @@ -103,7 +104,7 @@ def urlutils_config_stub(config_stub, monkeypatch): 'DEFAULT': 'http://www.example.com/?q={}', }, } - monkeypatch.setattr('qutebrowser.utils.urlutils.config', config_stub) + monkeypatch.setattr(urlutils, 'config', config_stub) return config_stub @@ -223,7 +224,7 @@ class TestFuzzyUrl: caplog): """Test with an invalid URL.""" is_url_mock.return_value = True - monkeypatch.setattr('qutebrowser.utils.urlutils.qurl_from_user_input', + monkeypatch.setattr(urlutils, 'qurl_from_user_input', lambda url: QUrl()) with pytest.raises(exception): with caplog.at_level(logging.ERROR): @@ -736,3 +737,42 @@ def test_file_url(): def test_data_url(): url = urlutils.data_url('text/plain', b'foo') assert url == QUrl('data:text/plain;base64,Zm9v') + + +class TestProxyFromUrl: + + @pytest.mark.parametrize('url, expected', [ + ('socks://example.com/', + QNetworkProxy(QNetworkProxy.Socks5Proxy, 'example.com')), + ('socks5://example.com', + QNetworkProxy(QNetworkProxy.Socks5Proxy, 'example.com')), + ('socks5://example.com:2342', + QNetworkProxy(QNetworkProxy.Socks5Proxy, 'example.com', 2342)), + ('socks5://foo@example.com', + QNetworkProxy(QNetworkProxy.Socks5Proxy, 'example.com', 0, 'foo')), + ('socks5://foo:bar@example.com', + QNetworkProxy(QNetworkProxy.Socks5Proxy, 'example.com', 0, 'foo', + 'bar')), + ('socks5://foo:bar@example.com:2323', + QNetworkProxy(QNetworkProxy.Socks5Proxy, 'example.com', 2323, + 'foo', 'bar')), + ('direct://', QNetworkProxy(QNetworkProxy.NoProxy)), + ]) + def test_proxy_from_url_valid(self, url, expected): + assert urlutils.proxy_from_url(QUrl(url)) == expected + + @pytest.mark.parametrize('scheme', ['pac+http', 'pac+https']) + def test_proxy_from_url_pac(self, scheme): + fetcher = urlutils.proxy_from_url(QUrl('{}://foo'.format(scheme))) + assert isinstance(fetcher, pac.PACFetcher) + + @pytest.mark.parametrize('url, exception', [ + ('blah', urlutils.InvalidProxyTypeError), + (':', urlutils.InvalidUrlError), # invalid URL + # Invalid/unsupported scheme + ('ftp://example.com/', urlutils.InvalidProxyTypeError), + ('socks4://example.com/', urlutils.InvalidProxyTypeError), + ]) + def test_invalid(self, url, exception): + with pytest.raises(exception): + urlutils.proxy_from_url(QUrl(url)) diff --git a/tests/unit/utils/test_utils.py b/tests/unit/utils/test_utils.py index 73252ddc4..12330aa73 100644 --- a/tests/unit/utils/test_utils.py +++ b/tests/unit/utils/test_utils.py @@ -27,8 +27,10 @@ import logging import functools import collections import socket +import re +import shlex -from PyQt5.QtCore import Qt +from PyQt5.QtCore import Qt, QUrl from PyQt5.QtGui import QColor, QClipboard import pytest @@ -115,7 +117,7 @@ class TestElidingFilenames: def freezer(request, monkeypatch): if request.param and not getattr(sys, 'frozen', False): monkeypatch.setattr(sys, 'frozen', True, raising=False) - monkeypatch.setattr('sys.executable', qutebrowser.__file__) + monkeypatch.setattr(sys, 'executable', qutebrowser.__file__) elif not request.param and getattr(sys, 'frozen', False): # Want to test unfrozen tests, but we are frozen pytest.skip("Can't run with sys.frozen = True!") @@ -146,129 +148,6 @@ def test_resource_filename(): assert f.read().splitlines()[0] == "Hello World!" -class Patcher: - - """Helper for TestActuteWarning. - - Attributes: - monkeypatch: The pytest monkeypatch fixture. - """ - - def __init__(self, monkeypatch): - self.monkeypatch = monkeypatch - - def patch_platform(self, platform='linux'): - """Patch sys.platform.""" - self.monkeypatch.setattr('sys.platform', platform) - - def patch_exists(self, exists=True): - """Patch os.path.exists.""" - self.monkeypatch.setattr('qutebrowser.utils.utils.os.path.exists', - lambda path: exists) - - def patch_version(self, version='5.2.0'): - """Patch Qt version.""" - self.monkeypatch.setattr('qutebrowser.utils.utils.qtutils.qVersion', - lambda: version) - - def patch_file(self, data): - """Patch open() to return the given data.""" - fake_file = io.StringIO(data) - self.monkeypatch.setattr(utils, 'open', - lambda filename, mode, encoding: fake_file, - raising=False) - - def patch_all(self, data): - """Patch everything so the issue would exist.""" - self.patch_platform() - self.patch_exists() - self.patch_version() - self.patch_file(data) - - -class TestActuteWarning: - - """Test actute_warning.""" - - @pytest.fixture - def patcher(self, monkeypatch): - """Fixture providing a Patcher helper.""" - return Patcher(monkeypatch) - - def test_non_linux(self, patcher, capsys): - """Test with a non-Linux OS.""" - patcher.patch_platform('toaster') - utils.actute_warning() - out, err = capsys.readouterr() - assert not out - assert not err - - def test_no_compose(self, patcher, capsys): - """Test with no compose file.""" - patcher.patch_platform() - patcher.patch_exists(False) - utils.actute_warning() - out, err = capsys.readouterr() - assert not out - assert not err - - def test_newer_qt(self, patcher, capsys): - """Test with compose file but newer Qt version.""" - patcher.patch_platform() - patcher.patch_exists() - patcher.patch_version('5.4') - utils.actute_warning() - out, err = capsys.readouterr() - assert not out - assert not err - - def test_no_match(self, patcher, capsys): - """Test with compose file and affected Qt but no match.""" - patcher.patch_all('foobar') - utils.actute_warning() - out, err = capsys.readouterr() - assert not out - assert not err - - def test_empty(self, patcher, capsys): - """Test with empty compose file.""" - patcher.patch_all(None) - utils.actute_warning() - out, err = capsys.readouterr() - assert not out - assert not err - - def test_match(self, patcher, capsys): - """Test with compose file and affected Qt and a match.""" - patcher.patch_all('foobar\n\nbaz') - utils.actute_warning() - out, err = capsys.readouterr() - assert out.startswith('Note: If you got a') - assert not err - - def test_match_stdout_none(self, monkeypatch, patcher, capsys): - """Test with a match and stdout being None.""" - patcher.patch_all('foobar\n\nbaz') - monkeypatch.setattr('sys.stdout', None) - utils.actute_warning() - - def test_unreadable(self, mocker, patcher, capsys, caplog): - """Test with an unreadable compose file.""" - patcher.patch_platform() - patcher.patch_exists() - patcher.patch_version() - mocker.patch('qutebrowser.utils.utils.open', side_effect=OSError, - create=True) - - with caplog.at_level(logging.ERROR, 'init'): - utils.actute_warning() - - assert len(caplog.records) == 1 - assert caplog.records[0].message == 'Failed to read Compose file' - out, _err = capsys.readouterr() - assert not out - - class TestInterpolateColor: """Tests for interpolate_color. @@ -432,7 +311,7 @@ class TestKeyToString: def test_missing(self, monkeypatch): """Test with a missing key.""" - monkeypatch.delattr('qutebrowser.utils.utils.Qt.Key_Blue') + monkeypatch.delattr(utils.Qt, 'Key_Blue') # We don't want to test the key which is actually missing - we only # want to know if the mapping still behaves properly. assert utils.key_to_string(Qt.Key_A) == 'A' @@ -484,7 +363,7 @@ class TestKeyEventToString: def test_mac(self, monkeypatch, fake_keyevent_factory): """Test with a simulated mac.""" - monkeypatch.setattr('sys.platform', 'darwin') + monkeypatch.setattr(sys, 'platform', 'darwin') evt = fake_keyevent_factory(key=Qt.Key_A, modifiers=Qt.ControlModifier) assert utils.keyevent_to_string(evt) == 'Meta+A' @@ -946,6 +825,12 @@ class TestGetSetClipboard: def test_get(self): assert utils.get_clipboard() == 'mocked clipboard text' + @pytest.mark.parametrize('selection', [True, False]) + def test_get_empty(self, clipboard_mock, selection): + clipboard_mock.text.return_value = '' + with pytest.raises(utils.ClipboardEmptyError): + utils.get_clipboard(selection=selection) + def test_get_unsupported_selection(self, clipboard_mock): clipboard_mock.supportsSelection.return_value = False with pytest.raises(utils.SelectionUnsupportedError): @@ -984,3 +869,50 @@ def test_random_port(): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.bind(('localhost', port)) sock.close() + + +class TestOpenFile: + + @pytest.mark.not_frozen + def test_cmdline_without_argument(self, caplog, config_stub): + config_stub.data = {'general': {'default-open-dispatcher': ''}} + executable = shlex.quote(sys.executable) + cmdline = '{} -c pass'.format(executable) + utils.open_file('/foo/bar', cmdline) + result = caplog.records[0].message + assert re.match( + r'Opening /foo/bar with \[.*python.*/foo/bar.*\]', result) + + @pytest.mark.not_frozen + def test_cmdline_with_argument(self, caplog, config_stub): + config_stub.data = {'general': {'default-open-dispatcher': ''}} + executable = shlex.quote(sys.executable) + cmdline = '{} -c pass {{}} raboof'.format(executable) + utils.open_file('/foo/bar', cmdline) + result = caplog.records[0].message + assert re.match( + r"Opening /foo/bar with \[.*python.*/foo/bar.*'raboof'\]", result) + + @pytest.mark.not_frozen + def test_setting_override(self, caplog, config_stub): + executable = shlex.quote(sys.executable) + cmdline = '{} -c pass'.format(executable) + config_stub.data = {'general': {'default-open-dispatcher': cmdline}} + utils.open_file('/foo/bar') + result = caplog.records[0].message + assert re.match( + r"Opening /foo/bar with \[.*python.*/foo/bar.*\]", result) + + def test_system_default_application(self, caplog, config_stub, mocker): + config_stub.data = {'general': {'default-open-dispatcher': ''}} + m = mocker.patch('PyQt5.QtGui.QDesktopServices.openUrl', spec={}, + new_callable=mocker.Mock) + utils.open_file('/foo/bar') + result = caplog.records[0].message + assert re.match( + r"Opening /foo/bar with the system application", result) + m.assert_called_with(QUrl('file:///foo/bar')) + + +def test_unused(): + utils.unused(None) diff --git a/tests/unit/utils/test_version.py b/tests/unit/utils/test_version.py index 39ae7cea1..c6e629ef1 100644 --- a/tests/unit/utils/test_version.py +++ b/tests/unit/utils/test_version.py @@ -33,7 +33,7 @@ import textwrap import pytest import qutebrowser -from qutebrowser.utils import version +from qutebrowser.utils import version, usertypes from qutebrowser.browser import pdfjs @@ -93,21 +93,18 @@ class TestGitStr: mocker.patch('qutebrowser.utils.version.subprocess', side_effect=AssertionError) fake = GitStrSubprocessFake() - monkeypatch.setattr('qutebrowser.utils.version._git_str_subprocess', - fake.func) + monkeypatch.setattr(version, '_git_str_subprocess', fake.func) return fake def test_frozen_ok(self, commit_file_mock, monkeypatch): """Test with sys.frozen=True and a successful git-commit-id read.""" - monkeypatch.setattr(qutebrowser.utils.version.sys, 'frozen', True, - raising=False) + monkeypatch.setattr(version.sys, 'frozen', True, raising=False) commit_file_mock.return_value = 'deadbeef' assert version._git_str() == 'deadbeef' def test_frozen_oserror(self, caplog, commit_file_mock, monkeypatch): """Test with sys.frozen=True and OSError when reading git-commit-id.""" - monkeypatch.setattr(qutebrowser.utils.version.sys, 'frozen', True, - raising=False) + monkeypatch.setattr(version.sys, 'frozen', True, raising=False) commit_file_mock.side_effect = OSError with caplog.at_level(logging.ERROR, 'misc'): assert version._git_str() is None @@ -144,7 +141,7 @@ class TestGitStr: def test_normal_path_nofile(self, monkeypatch, caplog, git_str_subprocess_fake, commit_file_mock): """Test with undefined __file__ but available git-commit-id.""" - monkeypatch.delattr('qutebrowser.utils.version.__file__') + monkeypatch.delattr(version, '__file__') commit_file_mock.return_value = '0deadcode' with caplog.at_level(logging.ERROR, 'misc'): assert version._git_str() == '0deadcode' @@ -304,7 +301,7 @@ def test_release_info(files, expected, caplog, monkeypatch): expected: The expected _release_info output. """ fake = ReleaseInfoFake(files) - monkeypatch.setattr('qutebrowser.utils.version.glob.glob', fake.glob_fake) + monkeypatch.setattr(version.glob, 'glob', fake.glob_fake) monkeypatch.setattr(version, 'open', fake.open_fake, raising=False) with caplog.at_level(logging.ERROR, 'misc'): assert version._release_info() == expected @@ -325,7 +322,7 @@ def test_path_info(monkeypatch): } for attr, val in patches.items(): - monkeypatch.setattr('qutebrowser.utils.standarddir.' + attr, val) + monkeypatch.setattr(version.standarddir, attr, val) pathinfo = version._path_info() @@ -362,6 +359,7 @@ class ImportFake: 'cssutils': True, 'typing': True, 'PyQt5.QtWebEngineWidgets': True, + 'PyQt5.QtWebKitWidgets': True, } self.version_attribute = '__version__' self.version = '1.2.3' @@ -407,7 +405,7 @@ def import_fake(monkeypatch): """Fixture to patch imports using ImportFake.""" fake = ImportFake() monkeypatch.setattr('builtins.__import__', fake.fake_import) - monkeypatch.setattr('qutebrowser.utils.version.importlib.import_module', + monkeypatch.setattr(version.importlib, 'import_module', fake.fake_importlib_import) return fake @@ -422,7 +420,8 @@ class TestModuleVersions: expected = ['sip: yes', 'colorama: 1.2.3', 'pypeg2: 1.2.3', 'jinja2: 1.2.3', 'pygments: 1.2.3', 'yaml: 1.2.3', 'cssutils: 1.2.3', 'typing: yes', - 'PyQt5.QtWebEngineWidgets: yes'] + 'PyQt5.QtWebEngineWidgets: yes', + 'PyQt5.QtWebKitWidgets: yes'] assert version._module_versions() == expected @pytest.mark.parametrize('module, idx, expected', [ @@ -445,14 +444,17 @@ class TestModuleVersions: ('VERSION', ['sip: yes', 'colorama: 1.2.3', 'pypeg2: yes', 'jinja2: yes', 'pygments: yes', 'yaml: yes', 'cssutils: yes', 'typing: yes', - 'PyQt5.QtWebEngineWidgets: yes']), + 'PyQt5.QtWebEngineWidgets: yes', + 'PyQt5.QtWebKitWidgets: yes']), ('SIP_VERSION_STR', ['sip: 1.2.3', 'colorama: yes', 'pypeg2: yes', 'jinja2: yes', 'pygments: yes', 'yaml: yes', 'cssutils: yes', 'typing: yes', - 'PyQt5.QtWebEngineWidgets: yes']), + 'PyQt5.QtWebEngineWidgets: yes', + 'PyQt5.QtWebKitWidgets: yes']), (None, ['sip: yes', 'colorama: yes', 'pypeg2: yes', 'jinja2: yes', 'pygments: yes', 'yaml: yes', 'cssutils: yes', 'typing: yes', - 'PyQt5.QtWebEngineWidgets: yes']), + 'PyQt5.QtWebEngineWidgets: yes', + 'PyQt5.QtWebKitWidgets: yes']), ]) def test_version_attribute(self, value, expected, import_fake): """Test with a different version attribute. @@ -508,8 +510,8 @@ class TestOsInfo: No args because osver is set to '' if the OS is linux. """ - monkeypatch.setattr('qutebrowser.utils.version.sys.platform', 'linux') - monkeypatch.setattr('qutebrowser.utils.version._release_info', + monkeypatch.setattr(version.sys, 'platform', 'linux') + monkeypatch.setattr(version, '_release_info', lambda: [('releaseinfo', 'Hello World')]) ret = version._os_info() expected = ['OS Version: ', '', @@ -518,8 +520,8 @@ class TestOsInfo: def test_windows_fake(self, monkeypatch): """Test with a fake Windows.""" - monkeypatch.setattr('qutebrowser.utils.version.sys.platform', 'win32') - monkeypatch.setattr('qutebrowser.utils.version.platform.win32_ver', + monkeypatch.setattr(version.sys, 'platform', 'win32') + monkeypatch.setattr(version.platform, 'win32_ver', lambda: ('eggs', 'bacon', 'ham', 'spam')) ret = version._os_info() expected = ['OS Version: eggs, bacon, ham, spam'] @@ -537,17 +539,15 @@ class TestOsInfo: mac_ver: The tuple to set platform.mac_ver() to. mac_ver_str: The expected Mac version string in version._os_info(). """ - monkeypatch.setattr('qutebrowser.utils.version.sys.platform', 'darwin') - monkeypatch.setattr('qutebrowser.utils.version.platform.mac_ver', - lambda: mac_ver) + monkeypatch.setattr(version.sys, 'platform', 'darwin') + monkeypatch.setattr(version.platform, 'mac_ver', lambda: mac_ver) ret = version._os_info() expected = ['OS Version: {}'.format(mac_ver_str)] assert ret == expected def test_unknown_fake(self, monkeypatch): """Test with a fake unknown sys.platform.""" - monkeypatch.setattr('qutebrowser.utils.version.sys.platform', - 'toaster') + monkeypatch.setattr(version.sys, 'platform', 'toaster') ret = version._os_info() expected = ['OS Version: ?'] assert ret == expected @@ -638,18 +638,58 @@ class FakeQSslSocket: return self._version -@pytest.mark.parametrize(['git_commit', 'frozen', 'style', - 'equal_qt', 'with_webkit'], [ - (True, False, True, True, True), # normal - (False, False, True, True, True), # no git commit - (True, True, True, True, True), # frozen - (True, True, False, True, True), # no style - (True, False, True, False, True), # different Qt - (True, False, True, True, False), # no webkit +@pytest.mark.parametrize('same', [True, False]) +def test_qt_version(monkeypatch, same): + if same: + qt_version_str = '5.4.0' + expected = '5.4.0' + else: + qt_version_str = '5.3.0' + expected = '5.4.0 (compiled 5.3.0)' + monkeypatch.setattr(version, 'qVersion', lambda: '5.4.0') + monkeypatch.setattr(version, 'QT_VERSION_STR', qt_version_str) + assert version.qt_version() == expected + + +@pytest.mark.parametrize('ua, expected', [ + (None, 'unavailable'), # No QWebEngineProfile + ('Mozilla/5.0', 'unknown'), + ('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) ' + 'QtWebEngine/5.8.0 Chrome/53.0.2785.148 Safari/537.36', '53.0.2785.148'), ]) -def test_version_output(git_commit, frozen, style, equal_qt, with_webkit, - stubs, monkeypatch): +def test_chromium_version(monkeypatch, caplog, ua, expected): + if ua is None: + monkeypatch.setattr(version, 'QWebEngineProfile', None) + else: + class FakeWebEngineProfile: + def httpUserAgent(self): + return ua + monkeypatch.setattr(version, 'QWebEngineProfile', FakeWebEngineProfile) + + with caplog.at_level(logging.ERROR): + assert version._chromium_version() == expected + + +def test_chromium_version_unpatched(qapp): + pytest.importorskip('PyQt5.QtWebEngineWidgets') + assert version._chromium_version() not in ['', 'unknown', 'unavailable'] + + +@pytest.mark.parametrize(['git_commit', 'frozen', 'style', 'with_webkit'], [ + (True, False, True, True), # normal + (False, False, True, True), # no git commit + (True, True, True, True), # frozen + (True, True, False, True), # no style + (True, False, True, False), # no webkit + (True, False, True, 'ng'), # QtWebKit-NG +]) +def test_version_output(git_commit, frozen, style, with_webkit, stubs, + monkeypatch): """Test version.version().""" + class FakeWebEngineProfile: + def httpUserAgent(self): + return 'Toaster/4.0.4 Chrome/CHROMIUMVERSION Teapot/4.1.8' + import_path = os.path.abspath('/IMPORTPATH') patches = { 'qutebrowser.__file__': os.path.join(import_path, '__init__.py'), @@ -659,11 +699,9 @@ def test_version_output(git_commit, frozen, style, equal_qt, with_webkit, 'platform.python_version': lambda: 'PYTHON VERSION', 'PYQT_VERSION_STR': 'PYQT VERSION', 'QT_VERSION_STR': 'QT VERSION', - 'qVersion': (lambda: - 'QT VERSION' if equal_qt else 'QT RUNTIME VERSION'), + 'qVersion': lambda: 'QT VERSION', '_module_versions': lambda: ['MODULE VERSION 1', 'MODULE VERSION 2'], '_pdfjs_version': lambda: 'PDFJS VERSION', - 'qWebKitVersion': (lambda: 'WEBKIT VERSION') if with_webkit else None, 'QSslSocket': FakeQSslSocket('SSL VERSION'), 'platform.platform': lambda: 'PLATFORM', 'platform.architecture': lambda: ('ARCHITECTURE', ''), @@ -671,8 +709,33 @@ def test_version_output(git_commit, frozen, style, equal_qt, with_webkit, '_path_info': lambda: {'PATH DESC': 'PATH NAME'}, 'QApplication': (stubs.FakeQApplication(style='STYLE') if style else stubs.FakeQApplication(instance=None)), + 'QLibraryInfo.location': (lambda _loc: 'QT PATH') } + substitutions = { + 'git_commit': '\nGit commit: GIT COMMIT' if git_commit else '', + 'style': '\nStyle: STYLE' if style else '', + 'qt': 'QT VERSION', + 'frozen': str(frozen), + 'import_path': import_path, + } + + if with_webkit: + patches['qWebKitVersion'] = lambda: 'WEBKIT VERSION' + patches['objects.backend'] = usertypes.Backend.QtWebKit + patches['QWebEngineProfile'] = None + if with_webkit == 'ng': + patches['qtutils.is_qtwebkit_ng'] = lambda v: True + substitutions['backend'] = 'QtWebKit-NG (WebKit WEBKIT VERSION)' + else: + patches['qtutils.is_qtwebkit_ng'] = lambda v: False + substitutions['backend'] = 'QtWebKit (WebKit WEBKIT VERSION)' + else: + patches['qWebKitVersion'] = None + patches['objects.backend'] = usertypes.Backend.QtWebEngine + patches['QWebEngineProfile'] = FakeWebEngineProfile + substitutions['backend'] = 'QtWebEngine (Chromium CHROMIUMVERSION)' + for attr, val in patches.items(): monkeypatch.setattr('qutebrowser.utils.version.' + attr, val) @@ -682,8 +745,9 @@ def test_version_output(git_commit, frozen, style, equal_qt, with_webkit, monkeypatch.delattr(sys, 'frozen', raising=False) template = textwrap.dedent(""" - qutebrowser vVERSION - {git_commit} + qutebrowser vVERSION{git_commit} + Backend: {backend} + PYTHON IMPLEMENTATION: PYTHON VERSION Qt: {qt} PyQt: PYQT VERSION @@ -691,12 +755,12 @@ def test_version_output(git_commit, frozen, style, equal_qt, with_webkit, MODULE VERSION 1 MODULE VERSION 2 pdf.js: PDFJS VERSION - Webkit: {webkit} SSL: SSL VERSION {style} Platform: PLATFORM, ARCHITECTURE Frozen: {frozen} Imported from {import_path} + Qt library executable path: QT PATH, data path: QT PATH OS INFO 1 OS INFO 2 @@ -704,15 +768,5 @@ def test_version_output(git_commit, frozen, style, equal_qt, with_webkit, PATH DESC: PATH NAME """.lstrip('\n')) - substitutions = { - 'git_commit': 'Git commit: GIT COMMIT\n' if git_commit else '', - 'style': '\nStyle: STYLE' if style else '', - 'qt': ('QT VERSION' if equal_qt else - 'QT RUNTIME VERSION (compiled QT VERSION)'), - 'frozen': str(frozen), - 'import_path': import_path, - 'webkit': 'WEBKIT VERSION' if with_webkit else 'no' - } - expected = template.rstrip('\n').format(**substitutions) assert version.version() == expected diff --git a/tox.ini b/tox.ini index 22e584667..731a07831 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ # and then run "tox" from this directory. [tox] -envlist = py34,py35-cov,misc,vulture,flake8,pylint,pyroma,check-manifest +envlist = py34,py35,py36-cov,misc,vulture,flake8,pylint,pyroma,check-manifest distshare = {toxworkdir} skipsdist = true @@ -13,13 +13,26 @@ skipsdist = true setenv = QT_QPA_PLATFORM_PLUGIN_PATH={envdir}/Lib/site-packages/PyQt5/plugins/platforms PYTEST_QT_API=pyqt5 -passenv = PYTHON DISPLAY XAUTHORITY HOME USERNAME USER CI TRAVIS XDG_* QUTE_* +passenv = PYTHON DISPLAY XAUTHORITY HOME USERNAME USER CI TRAVIS XDG_* QUTE_* DOCKER deps = -r{toxinidir}/requirements.txt -r{toxinidir}/misc/requirements/requirements-tests.txt commands = {envpython} scripts/link_pyqt.py --tox {envdir} - {envpython} scripts/dev/run_pytest.py {posargs:tests} + {envpython} -bb -m pytest {posargs:tests} + + +# test envs with coverage + +[testenv:py36-cov] +basepython = python3.6 +setenv = {[testenv]setenv} +passenv = {[testenv]passenv} +deps = {[testenv]deps} +commands = + {envpython} scripts/link_pyqt.py --tox {envdir} + {envpython} -bb -m pytest --cov --cov-report xml --cov-report=html --cov-report= {posargs:tests} + {envpython} scripts/dev/check_coverage.py {posargs} [testenv:py35-cov] basepython = python3.5 @@ -28,7 +41,7 @@ passenv = {[testenv]passenv} deps = {[testenv]deps} commands = {envpython} scripts/link_pyqt.py --tox {envdir} - {envpython} scripts/dev/run_pytest.py --cov --cov-report xml --cov-report=html --cov-report= {posargs:tests} + {envpython} -bb -m pytest --cov --cov-report xml --cov-report=html --cov-report= {posargs:tests} {envpython} scripts/dev/check_coverage.py {posargs} [testenv:py34-cov] @@ -38,9 +51,68 @@ passenv = {[testenv]passenv} deps = {[testenv]deps} commands = {envpython} scripts/link_pyqt.py --tox {envdir} - {envpython} scripts/dev/run_pytest.py --cov --cov-report xml --cov-report=html --cov-report= {posargs:tests} + {envpython} -bb -m pytest --cov --cov-report xml --cov-report=html --cov-report= {posargs:tests} {envpython} scripts/dev/check_coverage.py {posargs} +# test envs with PyQt5 from PyPI + +[testenv:py35-pyqt56] +basepython = python3.5 +setenv = + {[testenv]setenv} + QUTE_BDD_WEBENGINE=true +passenv = {[testenv]passenv} +deps = + {[testenv]deps} + PyQt5==5.6 +commands = {envpython} -bb -m pytest {posargs:tests} + +[testenv:py35-pyqt571] +basepython = python3.5 +setenv = + {[testenv]setenv} + QUTE_BDD_WEBENGINE=true +passenv = {[testenv]passenv} +deps = + {[testenv]deps} + PyQt5==5.7.1 +commands = {envpython} -bb -m pytest {posargs:tests} + +[testenv:py36-pyqt571] +basepython = python3.6 +setenv = + {[testenv]setenv} + QUTE_BDD_WEBENGINE=true +passenv = {[testenv]passenv} +deps = + {[testenv]deps} + PyQt5==5.7.1 +commands = {envpython} -bb -m pytest {posargs:tests} + +[testenv:py35-pyqt58] +basepython = python3.5 +setenv = + {[testenv]setenv} + QUTE_BDD_WEBENGINE=true +passenv = {[testenv]passenv} +deps = + {[testenv]deps} + PyQt5==5.8.1.1 +commands = {envpython} -bb -m pytest {posargs:tests} + +[testenv:py36-pyqt58] +basepython = python3.6 +setenv = + {[testenv]setenv} + QUTE_BDD_WEBENGINE=true +passenv = {[testenv]passenv} +deps = + {[testenv]deps} + PyQt5==5.8.1.1 +commands = {envpython} -bb -m pytest {posargs:tests} + +# other envs + [testenv:mkvenv] basepython = python3 commands = {envpython} scripts/link_pyqt.py --tox {envdir} @@ -66,6 +138,16 @@ envdir = {[testenv:mkvenv]envdir} usedevelop = {[testenv:mkvenv]usedevelop} deps = {[testenv:mkvenv]deps} +# Virtualenv with PyQt5 from PyPI +[testenv:mkvenv-pypi] +basepython = python3 +envdir = {toxinidir}/.venv +commands = {envpython} -c "" +usedevelop = true +deps = + -r{toxinidir}/requirements.txt + -r{toxinidir}/misc/requirements/requirements-pyqt.txt + [testenv:unittests-frozen] # cx_Freeze doesn't support Python 3.5 yet basepython = python3.4 @@ -176,11 +258,17 @@ basepython = python3 deps = -r{toxinidir}/requirements.txt -r{toxinidir}/misc/requirements/requirements-pyinstaller.txt + -r{toxinidir}/misc/requirements/requirements-pyqt.txt +whitelist_externals = + patch + wget commands = - {envpython} scripts/link_pyqt.py --tox {envdir} + wget https://patch-diff.githubusercontent.com/raw/pyinstaller/pyinstaller/pull/2403.patch -O {envtmpdir}/pyqt.patch -o/dev/null + patch -f -p1 -d {envdir}/src/pyinstaller -i {envtmpdir}/pyqt.patch {envbindir}/pyinstaller --noconfirm misc/qutebrowser.spec [testenv:eslint] deps = whitelist_externals = eslint -commands = eslint --color qutebrowser/javascript +changedir = {toxinidir}/qutebrowser/javascript +commands = eslint --color . diff --git a/www/header.asciidoc b/www/header.asciidoc index f2095bd71..dc80a5d00 100644 --- a/www/header.asciidoc +++ b/www/header.asciidoc @@ -13,8 +13,8 @@ Install Changelog Contributing - GitHub - Releases + GitHub + Releases Blog +++ diff --git a/www/qute.css b/www/qute.css index d8367603b..2de633b18 100644 --- a/www/qute.css +++ b/www/qute.css @@ -216,6 +216,6 @@ table td { - - + +