diff --git a/.appveyor.yml b/.appveyor.yml index d3a790aaa..45a221d57 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -5,16 +5,15 @@ cache: build: off environment: PYTHONUNBUFFERED: 1 + PYTHON: C:\Python36\python.exe matrix: - - TESTENV: py34 - TESTENV: py36-pyqt58 - PYTHON: C:\Python36\python.exe - - TESTENV: unittests-frozen - TESTENV: pylint install: - - C:\Python27\python -u scripts\dev\ci\appveyor_install.py - - set PATH=%PATH%;C:\Python36 + - '%PYTHON% -m pip install -U pip' + - '%PYTHON% -m pip install -r misc\requirements\requirements-tox.txt' + - 'set PATH=%PATH%;C:\Python36' test_script: - - C:\Python34\Scripts\tox -e %TESTENV% + - '%PYTHON% -m tox -e %TESTENV%' diff --git a/.flake8 b/.flake8 index 7bfc34c0a..eada2c86d 100644 --- a/.flake8 +++ b/.flake8 @@ -11,6 +11,7 @@ exclude = .*,__pycache__,resources.py # (for pytest's __tracebackhide__) # F401: Unused import # N802: function name should be lowercase +# N806: variable in function should be lowercase # P101: format string does contain unindexed parameters # P102: docstring does contain unindexed parameters # P103: other string does contain unindexed parameters @@ -35,10 +36,10 @@ max-complexity = 12 putty-auto-ignore = True putty-ignore = /# pylint: disable=invalid-name/ : +N801,N806 - /# pylint: disable=wildcard-import/ : +F403 /# pragma: no mccabe/ : +C901 tests/*/test_*.py : +D100,D101,D401 - tests/unit/browser/webkit/test_history.py : +N806 + tests/conftest.py : +F403 + tests/unit/browser/test_history.py : +N806 tests/helpers/fixtures.py : +N806 tests/unit/browser/webkit/http/test_content_disposition.py : +D400 scripts/dev/ci/appveyor_install.py : +FI53 diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..2b8c12de9 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,8 @@ +qutebrowser/browser/history.py @rcorre +qutebrowser/completion/* @rcorre +qutebrowser/misc/sql.py @rcorre +tests/end2end/features/completion.feature @rcorre +tests/end2end/features/test_completion_bdd.py @rcorre +tests/unit/browser/test_history.py @rcorre +tests/unit/completion/* @rcorre +tests/unit/misc/test_sql.py @rcorre diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 9ea69b642..b9bf8d399 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,2 +1,2 @@ +`:open qute:version` or `qutebrowser --version` --> diff --git a/.pylintrc b/.pylintrc index c94058297..04d689ffd 100644 --- a/.pylintrc +++ b/.pylintrc @@ -30,11 +30,7 @@ disable=no-self-use, broad-except, bare-except, eval-used, - exec-used, - file-ignored, - wrong-import-order, ungrouped-imports, - redefined-variable-type, suppressed-message, too-many-return-statements, duplicate-code, @@ -53,12 +49,9 @@ no-docstring-rgx=(^_|^main$) [FORMAT] max-line-length=79 -ignore-long-lines=( keyhint-delay` setting to configure the delay until - the keyhint overlay pops up. -- New `-s` option for `:open` to force a HTTPS scheme. +- New back/forward indicator in the statusbar Changed ~~~~~~~ -- When using QtWebEngine, the underlying Chromium version is now shown in the - version info. -- Improved `qute:history` page with lazy loading -- Messages are now hidden when clicked -- Paths like `C:Downloads` are now treated as absolute paths on Windows. -- PAC on QtWebKit now supports SOCK5 as type. +- Upgrading qutebrowser with a version older than v0.4.0 still running now won't + work properly anymore. +- Using `:download` now uses the page's title as filename. +- Using `:back` or `:forward` with a count now skips intermediate pages. +- When there are multiple messages shown, the timeout is increased. +- `:search` now only clears the search if one was displayed before, so pressing + `` doesn't un-focus inputs anymore. + +Fixes +~~~~~ + +- Exiting fullscreen via `:fullscreen` or buttons on a page now + restores the correct previous window state (maximized/fullscreen). + +v0.11.1 (unreleased) +-------------------- + +Fixes +~~~~~ + +- Fixed empty space being shown after tabs in the tabbar in some cases. +- Fixed `:restart` in private browsing mode. +- Fixed printing on macOS. +- Closing a pinned tab via mouse now also prompts for confirmation. +- The "try again" button on error pages works correctly again. +- :spawn -u -d is now disallowed. +- :spawn -d shows error messages correctly now. + +v0.11.0 +------- + +New dependencies +~~~~~~~~~~~~~~~~ + +- New dependency on `PyQt5.QtOpenGL` if QtWebEngine is used. QtWebEngine depends + on QtOpenGL already, but on distributions packaging split PyQt5 wrappers, the + wrappers for QtOpenGL are now required. +- New dependency on `PyOpenGL` if QtWebEngine is used. + +Added +~~~~~ + +- Private browsing is now implemented for QtWebEngine, *and changed its + behavior*: The `general -> private-browsing` setting now only applies to newly + opened windows, and you can use the `-p` flag to `:open` to open a private + window. +- New "pinned tabs" feature, with a new `:tab-pin` command (bound + to `` by default). +- (QtWebEngine) Implemented `:follow-selected`. +- New `:clear-messages` command to clear shown messages. +- New `ui -> keyhint-delay` setting to configure the delay until + the keyhint overlay pops up. +- New `-s` option for `:open` to force a HTTPS scheme. +- `:debug-log-filter` now accepts `none` as an argument to clear any log + filters. +- New `--debug-flag` argument which replaces `--debug-exit` and + `--pdb-postmortem`. +- New `tabs -> favicon-scale` option to scale up/down favicons. +- `colors -> statusbar.bg/fg.private` and `.command.private` to + customize statusbar colors for private windows. +- New `{private}` field displaying `[Private Mode]` for + `ui -> window-title-format` and `tabs -> title-format`. +- (QtWebEngine) Proxy support with Qt 5.7.1 (already was supported for 5.8 and + newer) + +Changed +~~~~~~~ + +- To prevent elaborate phishing attacks, the Punycode version (`xn--*`) is now + shown in addition to the decoded version for international domain names + (IDN). +- Starting with legacy QtWebKit now shows a warning message. + *With the next release, support for it will be removed.* +- The Windows releases are redone from scratch, which means: + - They now use the new QtWebEngine backend + - The bundled Qt is updated from 5.5 to 5.9 + - The bundled Python is updated from 3.4 to 3.6 + - They are now generated with PyInstaller instead of cx_Freeze + - The installer is now generated using NSIS instead of being a MSI +- Improved `qute://history` page (with lazy loading) +- Crash reports are not public anymore. +- Paths like `C:` are now treated as absolute paths on Windows for downloads, + and invalid paths are handled properly. +- Comments in the config file are now placed before the individual options + instead of being before sections. +- Messages are now hidden when clicked. +- stdin is now closed immediately for processes spawned from qutebrowser. +- When `ui -> message-timeout` is set to 0, messages are now never cleared. +- Middle/right-clicking the blank parts of the tab bar (when vertical) now + closes the current tab. +- The adblocker now also blocks non-GET requests (e.g. POST). +- `javascript:` links can now be hinted. +- `:view-source`, `:tab-clone` and `:navigate --tab` now don't open the tab as + "explicit" anymore, i.e. (with the default settings) open it next to the + active tab. +- `qute:*` pages now use `qute://*` instead (e.g. `qute://version` instead of + `qute:version`), but the old versions are automatically redirected. +- Texts in prompts are now selectable. +- The default level for `:messages` is now `info`, not `error` +- Trying to focus the currently focused tab with `:tab-focus` now focuses the + last viewed tab. +- (QtWebEngine) With Qt 5.9, `content -> cookies-store` can now be set without + a restart. +- (QtWebEngine) With Qt 5.9, better error messages are now shown for failed + downloads. +- (QtWebEngine) The underlying Chromium version is now shown in the version + info. +- (QtWebKit) Renderer process crashes now show an error page on Qt 5.9 or newer. +- (QtWebKit) storage -> offline-web-application-storage` got renamed to `...-cache` +- (QtWebKit) PAC now supports SOCKS5 as type. Fixed ~~~~~ -- Added a workaround for a black screen with QtWebEngine with some setups - (the workaround requires PyOpenGL to be installed, but it's optional) -- Crash when trying to retry downloads with QtWebEngine -- Crash when cloning page without history -- Continuing a search after clearing it -- Crash when downloading a download resulting in a HTTP error -- Various rare crashes +- The macOS .dmg is now built against Qt 5.9 which fixes various + important issues (such as not being able to type dead keys). +- Fixed crash with `:download` on PyQt 5.9. +- Cloning a page without history doesn't crash anymore. +- When a download results in a HTTP error, it now shows the error correctly + instead of crashing. +- Pressing ctrl-c while a config error is shown works as intended now. +- When the key config isn't writable, we now show an error instead of crashing. +- Fixed crash when unbinding an unbound key in the key config. +- Fixed crash when using `:debug-log-filter` when `--filter` wasn't given on startup. +- Fixed crash with some invalid setting values. +- Continuing a search after clearing it now works correctly. +- The tabbar and completion should now be more consistently and correctly + styled with various system styles. +- Applying styiles in `qt5ct` now shouldn't crash anymore. +- The validation for colors in stylesheets is now less strict, + allowing for all valid Qt values. +- `data:` URLs now aren't added to the history anymore. +- Accidentally starting with Python 2 now shows a proper error message again. +- For some people, running some userscripts crashed - this should now be fixed. +- Various other rare crashes should now be fixed. +- The settings documentation was truncated with v0.10.1 which should now be + fixed. +- Scrolling to an anchor in a background tab now works correctly, and javascript + gets the correct window size for background tabs. +- (QtWebEngine) Added a workaround for a black screen with some setups +- (QtWebEngine) Starting with Nouveau graphics now shows an error message + instead of crashing in Qt. +- (QtWebEngine) Retrying downloads now shows an error instead of crashing. +- (QtWebEngine) Cloning a view-source tab now doesn't crash anymore. +- (QtWebEngine) `window.navigator.userAgent` is now set correctly when + customizing the user agent. +- (QtWebEngine) HTML fullscreen is now tracked for each tab separately, which + means it's not possible anymore to accidentally get stuck in fullscreen state + by closing a tab with a fullscreen video. +- (QtWebEngine) `:scroll-page` with `--bottom-navigate` now works correctly. +- (QtWebKit) The HTTP cache is disabled on Qt 5.7.1 and 5.8 now as it leads to + frequent crashes due to a Qt bug. +- (QtWebKit) Fixed Crash when a PAC file returns an invalid value. v0.10.1 ------- @@ -89,7 +241,7 @@ Added - 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 +- macOS builds are back, and built with QtWebEngine Changed ~~~~~~~ @@ -105,6 +257,18 @@ Changed - `network -> proxy` can also be set to `pac+file://...` now to use a local proxy autoconfig file (on QtWebKit) +Removed +~~~~~~~ + +- (QtWebKit) Various rarely customized settings were removed: + - `ui -> css-media-type` (defaults to desktop) + - `general -> site-specific-quirks` (now always turned on) + - `storage -> offline-storage-default-quota` (defaults to 5MB) + - `storage -> offline-web-application-cache-quota` (defaults to no quota) + - `storage -> object-cache-capacities` (default depends on disk space) + - `content -> css-regions` (now always turned off) + - `storage -> offline-storage-database` (merged into `storage -> local-storage`) + Fixed ~~~~~ @@ -379,7 +543,7 @@ Fixed - Fix crash when pressing enter without a command - Adjust error message to point out QtWebEngine is unsupported with the OS X .app currently. -- Hide Harfbuzz warning with the OS X .app +- Hide Harfbuzz warning with the macOS .app v0.8.0 ------ @@ -742,7 +906,7 @@ Fixed - Fixed scrolling to the very left/right with `:scroll-perc`. - Using an external editor should now work correctly with some funny chars (U+2028/U+2029/BOM). -- Movements in caret mode now should work correctly on OS X and Windows. +- Movements in caret mode now should work correctly on macOS and Windows. - Fixed upgrade from earlier config versions. - Fixed crash when killing a running userscript. - Fixed characters being passed through when shifted with @@ -817,7 +981,7 @@ Changed - The completion widget doesn't show a border anymore. - The tabbar doesn't display ugly arrows anymore if there isn't enough space for all tabs. -- Some insignificant Qt warnings which were printed on OS X are now hidden. +- Some insignificant Qt warnings which were printed on macOS are now hidden. - Better support for Qt 5.5 and Python 3.5. Fixed @@ -928,7 +1092,7 @@ Fixed - Fixed AssertionError when closing many windows quickly. - Various fixes for deprecated key bindings and auto-migrations. - Workaround for qutebrowser not starting when there are NUL-bytes in the history (because of a currently unknown bug). -- Fixed handling of keybindings containing Ctrl/Meta on OS X. +- Fixed handling of keybindings containing Ctrl/Meta on macOS. - Fixed crash when downloading a URL without filename (e.g. magnet links) via "Save as...". - Fixed exception when starting qutebrowser with `:set` as argument. - Fixed horrible completion performance when the `shrink` option was set. @@ -1026,7 +1190,7 @@ Changed - Add a `:search` command in addition to `/foo` so it's more visible and can be used from scripts. - Various improvements to documentation, logging, and the crash reporter. - Expand `~` to the users home directory with `:run-userscript`. -- Improve the userscript runner on Linux/OS X by using `QSocketNotifier`. +- Improve the userscript runner on Linux/macOS by using `QSocketNotifier`. - Add luakit-like `gt`/`gT` keybindings to cycle through tabs. - Show default value for config values in the completion. - Clone tab icon, tab text and zoom level when cloning tabs. @@ -1046,7 +1210,7 @@ Changed * `init_venv.py` and `run_checks.py` have been replaced by http://tox.readthedocs.org/[tox]. Install tox and run `tox -e mkvenv` instead. * The tests now use http://pytest.org/[pytest] * Many new tests added - * Mac Mini buildbot to run the tests on OS X. + * Mac Mini buildbot to run the tests on macOS. * Coverage recording via http://nedbatchelder.com/code/coverage/[coverage.py]. * New `--pdb-postmortem argument` to drop into the pdb debugger on exceptions. * Use https://github.com/ionelmc/python-hunter[hunter] for line tracing instead of a selfmade solution. @@ -1182,7 +1346,7 @@ Fixed * Fix rare exception when a key is pressed shortly after opening a window * Fix exception with certain invalid URLs like `http:foo:0` -* Work around Qt bug which renders checkboxes on OS X unusable +* Work around Qt bug which renders checkboxes on macOS unusable * Fix exception when a local files can't be read in `:adblock-update` * Hide 2 more Qt warnings. * Add `!important` to hint CSS so websites don't override the hint look @@ -1218,7 +1382,7 @@ Changes * Set zoom to default instead of 100% with `:zoom`/`=`. * Adjust page zoom if default zoom changed. * Force tabs to be focused on `:undo`. -* Replace manual installation instructions on OS X with homebrew/macports. +* Replace manual installation instructions on macOS with homebrew/macports. * Allow min-/maximizing of print preview on Windows. * Various documentation improvements. * Various other small improvements and cleanups. diff --git a/CONTRIBUTING.asciidoc b/CONTRIBUTING.asciidoc index 12d52fee7..fc039ad03 100644 --- a/CONTRIBUTING.asciidoc +++ b/CONTRIBUTING.asciidoc @@ -5,6 +5,12 @@ The Compiler :data-uri: :toc: +IMPORTANT: I'm currently (July 2017) more busy than usual until September, +because of exams coming up. In addition to that, a new config system is coming +which will conflict with many non-trivial contributions. Because of that, please +refrain from contributing new features until then. If you're reading this note +after mid-September, please open an issue. + I `<3` footnote:[Of course, that says `<3` in HTML.] contributors! This document contains guidelines for contributing to qutebrowser, as well as @@ -42,6 +48,12 @@ be easy to solve] * https://github.com/qutebrowser/qutebrowser/labels/not%20code[Issues which require little/no coding] +If you prefer C++ or Javascript to Python, see the relevant issues which involve +work in those languages: + +* https://github.com/qutebrowser/qutebrowser/issues?utf8=%E2%9C%93&q=is%3Aopen%20is%3Aissue%20label%3Ac%2B%2B[C++] (mostly work on Qt, the library behind qutebrowser) +* https://github.com/qutebrowser/qutebrowser/issues?q=is%3Aopen+is%3Aissue+label%3Ajavascript[JavaScript] + There are also some things to do if you don't want to write code: * Help the community, e.g., on the mailinglist and the IRC channel. @@ -214,7 +226,7 @@ Documentation of used Python libraries: * http://pygments.org/docs/[pygments] * http://fdik.org/pyPEG/index.html[pyPEG2] * http://pythonhosted.org/setuptools/[setuptools] -* http://cx-freeze.readthedocs.org/en/latest/overview.html[cx_Freeze] +* http://www.pyinstaller.org/[PyInstaller] * https://pypi.python.org/pypi/colorama[colorama] Related RFCs and standards: @@ -546,6 +558,28 @@ Rebuilding the website If you want to rebuild the website, run `./scripts/asciidoc2html.py --website `. +Chrome URLs +~~~~~~~~~~~ + +With the QtWebEngine backend, qutebrowser supports several chrome:// urls which +can be useful for debugging: + +- chrome://appcache-internals/ +- chrome://blob-internals/ +- chrome://gpu/ +- chrome://histograms/ +- chrome://indexeddb-internals/ +- chrome://media-internals/ +- chrome://network-errors/ +- chrome://serviceworker-internals/ +- chrome://webrtc-internals/ +- chrome://crash/ (crashes the current renderer process!) +- chrome://kill/ (kills the current renderer process!) +- chrome://gpucrash/ (crashes qutebrowser!) +- chrome://gpuhang/ (hangs qutebrowser!) +- chrome://gpuclean/ (crashes the current renderer process!) +- chrome://ppapiflashcrash/ +- chrome://ppapiflashhang/ Style conventions ----------------- @@ -654,8 +688,9 @@ qutebrowser release * Add newest config to `tests/unit/config/old_configs` and update `test_upgrade_version` - `python -m qutebrowser --basedir conf :quit` - - `sed '/^#/d' conf/config/qutebrowser.conf > tests/unit/config/old_configs/qutebrowser-v0.x.y.conf` + - `sed '/^#/d' conf/config/qutebrowser.conf > tests/unit/config/old_configs/qutebrowser-v0.$x.$y.conf` - `rm -r conf` + - git add - commit * Adjust `__version_info__` in `qutebrowser/__init__.py`. * Update changelog (remove *(unreleased)*) @@ -670,8 +705,8 @@ qutebrowser release as closed. * Linux: Run `python3 scripts/dev/build_release.py --upload v0.$x.$y` -* Windows: Run `C:\Python34_x32\python scripts\dev\build_release.py --asciidoc C:\Python27\python C:\asciidoc-8.6.9\asciidoc.py --upload v0.X.Y` (replace X/Y by hand) -* OS X: Run `python3 scripts/dev/build_release.py --upload v0.X.Y` (replace X/Y by hand) +* Windows: Run `C:\Python36-32\python scripts\dev\build_release.py --asciidoc C:\Python27\python C:\asciidoc-8.6.9\asciidoc.py --upload v0.X.Y` (replace X/Y by hand) +* macOS: Run `python3 scripts/dev/build_release.py --upload v0.X.Y` (replace X/Y by hand) * On server: Run `python3 scripts/dev/download_release.py v0.X.Y` (replace X/Y by hand) * Update `qutebrowser-git` PKGBUILD if dependencies/install changed * Announce to qutebrowser and qutebrowser-announce mailinglist diff --git a/FAQ.asciidoc b/FAQ.asciidoc index 71d5f2834..9a540160a 100644 --- a/FAQ.asciidoc +++ b/FAQ.asciidoc @@ -72,8 +72,8 @@ Is there an adblocker?:: http://www.reddit.com/r/programming/comments/25j41u/adblock_pluss_effect_on_firefoxs_memory_usage/chhpomw[big impact] on browsing speed and https://blog.mozilla.org/nnethercote/2014/05/14/adblock-pluss-effect-on-firefoxs-memory-usage/[RAM - usage], so implementing it properly might take some time and won't be done - for v0.1 if at all. + usage], so implementing support for AdBlockPlus-like lists is currently not + a priority. How do I play Youtube videos with mpv?:: You can easily add a key binding to play youtube videos inside a real video @@ -124,6 +124,67 @@ When using quickmark, you can give them all names, like `:open foodrecipes`, you will see a list of all the food recipe sites, without having to remember the exact website title or address. +How do I use spell checking?:: + Qutebrowser's support for spell checking is somewhat limited at the moment + (see https://github.com/qutebrowser/qutebrowser/issues/700[#700]), but it + can be done. ++ +For QtWebKit: + +. Install https://github.com/QupZilla/qtwebkit-plugins[qtwebkit-plugins]. + . Note: with QtWebKit reloaded you may experience some issues. See + https://github.com/QupZilla/qtwebkit-plugins/issues/10[#10]. +. The dictionary to use is taken from the `DICTIONARY` environment variable. + The default is `en_US`. For example to use Dutch spell check set `DICTIONARY` + to `nl_NL`; you can't use multiple dictionaries or change them at runtime at + the moment. + (also see the README file for `qtwebkit-plugins`). +. Remember to install the hunspell dictionaries if you don't have them already + (most distros should have packages for this). + ++ +For QtWebEngine: + +. Not yet supported unfortunately :-( + + Adding it shouldn't be too hard though, since QtWebEngine 5.8 added an API for + this (see + https://github.com/qutebrowser/qutebrowser/issues/700#issuecomment-290780706[this + comment for a basic example]), so what are you waiting for and why aren't you + hacking qutebrowser yet? + +How do I use Tor with qutebrowser?:: + Start tor on your machine, and do `:set network proxy socks://localhost:9050/` + in qutebrowser. Note this won't give you the same amount of fingerprinting + protection that the Tor Browser does, but it's useful to be able to access + `.onion` sites. + +Why does J move to the next (right) tab, and K to the previous (left) one?:: + One reason is because https://bitbucket.org/portix/dwb[dwb] did it that way, + and qutebrowser's keybindings are designed to be compatible with dwb's. + The rationale behind it is that J is "down" in vim, and K is "up", which + corresponds nicely to "next"/"previous". It also makes much more sense with + vertical tabs (e.g. `:set tabs position left`). + +What's the difference between insert and passthrough mode?:: + They are quite similar, but insert mode has some bindings (like `Ctrl-e` to + open an editor) while passthrough mode only has escape bound. It might also + be useful to rebind escape to something else in passthrough mode only, to be + able to send an escape keypress to the website. + +Why takes it longer to open an URL in qutebrowser than in chromium?:: + When opening an URL in an existing instance the normal qutebrowser + Python script is started and a few PyQt libraries need to be + loaded until it is detected that there is an instance running + where the URL is then passed to. This takes some time. + One workaround is to use this + https://github.com/qutebrowser/qutebrowser/blob/master/scripts/open_url_in_instance.sh[script] + and place it in your $PATH with the name "qutebrowser". This + script passes the URL via an unix socket to qutebrowser (if its + running already) using socat which is much faster and starts a new + qutebrowser if it is not running already. Also check if you want + to use webengine as backend in line 17 and change it to your + needs. + == Troubleshooting Configuration not saved after modifying config.:: diff --git a/INSTALL.asciidoc b/INSTALL.asciidoc index 933859346..0cfc3d651 100644 --- a/INSTALL.asciidoc +++ b/INSTALL.asciidoc @@ -1,6 +1,8 @@ Installing qutebrowser ====================== +toc::[] + On Debian / Ubuntu ------------------ @@ -15,15 +17,25 @@ still relatively easy! You can use packages that are built for every release or build it yourself from git. +On Ubuntu 16.04 and 16.10 it's recommended to <> +instead in order to be able to use the new QtWebEngine backend. Newer versions +have a QtWebEngine package in the repositories. + Using the packages ~~~~~~~~~~~~~~~~~~ Install the dependencies via apt-get: ---- -# apt-get install python3-lxml python-tox python3-pyqt5 python3-pyqt5.qtwebkit python3-pyqt5.qtquick python3-sip python3-jinja2 python3-pygments python3-yaml +# apt-get install python3-lxml python-tox python3-pyqt5 python3-pyqt5.qtwebkit python3-pyqt5.qtquick python3-sip python3-jinja2 python3-pygments python3-yaml python3-pyqt5.qtsql libqt5sql5-sqlite ---- +On Debian Stretch or Ubuntu 17.04 or later, it's also recommended to use the +newer QtWebEngine backend. + +To do so, install `python3-pyqt5.qtwebengine` and `python3-pyqt5.qtopengl`, then +start qutebrowser with `--backend webengine`. + Get the qutebrowser package from the https://github.com/qutebrowser/qutebrowser/releases[release page] and download the https://qutebrowser.org/python3-pypeg2_2.15.2-1_all.deb[PyPEG2 package]. @@ -40,42 +52,13 @@ Build it from git Install the dependencies via apt-get: -[NOTE] -========================== -On Debian, it's recommended to install the Qt packages from the -https://wiki.debian.org/DebianExperimental[experimental] repository as those -are a much newer version of Qt which is more stable. - -Add the following line to your `/etc/apt/sources.list`: - ---- -deb http://ftp.debian.org/debian experimental main +# apt-get install python3-pyqt5 python3-pyqt5.qtwebkit python3-pyqt5.qtquick python-tox python3-sip python3-dev python3-pyqt5.qtsql libqt5sql5-sqlite ---- -Then install the packages like this: - ----- -# apt-get update -# apt-get install -t experimental python3-pyqt5 python3-pyqt5.qtwebkit python3-pyqt5.qtquick python3-sip python3-dev -# apt-get install python-tox ----- - -It's also recommended to pin those packages to receive updates by creating a -file `/etc/apt/preferences.d/qutebrowser` with the following contents: - ----- -Package: python3-pyqt5* libqt5* -Pin: release a=experimental -Pin-Priority: 800 ----- -========================== - -For distributions other than Debian or if you prefer to not use the -experimental repo: - ----- -# apt-get install python3-pyqt5 python3-pyqt5.qtwebkit python3-pyqt5.qtquick python-tox python3-sip python3-dev ----- +On Debian Stretch or Ubuntu 17.04 or later, it's also recommended to install +`python3-pyqt5.qtwebengine` and start qutebrowser with `--backend webengine` in +order to use the new backend. To generate the documentation for the `:help` command, when using the git repository (rather than a release): @@ -102,6 +85,9 @@ qutebrowser is available in the official repositories for Fedora 22 and newer. # dnf install qutebrowser ---- +It's also recommended to install `qt5-qtwebengine` and start with `--backend +webengine` to use the new backend. + On Archlinux ------------ @@ -111,6 +97,10 @@ qutebrowser is available in the official [community] repository. # pacman -S qutebrowser ---- +Archlinux packages an updated `qt5-webkit` package by default. If you want to +use the QtWebEngine backend instead, install `qt5-webengine` and start with +`--backend webengine`. + There is also a -git version available in the AUR: https://aur.archlinux.org/packages/qutebrowser-git/[qutebrowser-git]. @@ -175,6 +165,10 @@ Make sure you have `python3_4` in your `PYTHON_TARGETS` (`/etc/portage/make.conf`) and rebuild your system (`emerge -uDNav @world`) if necessary. +It's also recommended to install QtWebKit-NG via +https://gist.github.com/annulen/309569fb61e5d64a703c055c1e726f71[this ebuild], +or install Qt >= 5.7.1 with QtWebEngine in order to use an up-to-date backend. + If video or sound don't seem to work, try installing the gstreamer plugins: ---- @@ -192,6 +186,10 @@ with: # xbps-install qutebrowser ---- +It's currently recommended to install `python3-PyQt5-webengine` and +`python3-PyQt5-opengl`, then start with `--backend webengine` to use the new +backend. + On NixOS -------- @@ -202,6 +200,9 @@ it with: $ nix-env -i qutebrowser ---- +It's recommended to install `qt5.qtwebengine` and start with +`--backend webengine` to use the new backend. + On openSUSE ----------- @@ -222,19 +223,19 @@ On OpenBSD qutebrowser is in http://cvsweb.openbsd.org/cgi-bin/cvsweb/ports/www/qutebrowser/[OpenBSD ports]. -Manual install: +Install the package: + +---- +# pkg_add qutebrowser +---- + +Or alternatively, use the ports system : ---- # cd /usr/ports/www/qutebrowser # make install ---- -Or alternatively if you're using `-current` (or OpenBSD 6.1 once it's been released): - ----- -# pkg_add qutebrowser ----- - On Windows ---------- @@ -243,7 +244,7 @@ There are different ways to install qutebrowser on Windows: Prebuilt binaries ~~~~~~~~~~~~~~~~~ -Prebuilt standalone packages and MSI installers +Prebuilt standalone packages and installers https://github.com/qutebrowser/qutebrowser/releases[are built] for every release. @@ -276,13 +277,13 @@ $ pip install tox Then <>. -On OS X -------- +On macOS +-------- Prebuilt binary ~~~~~~~~~~~~~~~ -The easiest way to install qutebrowser on OS X is to use the prebuilt `.app` +The easiest way to install qutebrowser on macOS is to use the prebuilt `.app` files from the https://github.com/qutebrowser/qutebrowser/releases[release page]. @@ -344,17 +345,23 @@ Then run tox inside the qutebrowser repository to set up a https://docs.python.org/3/library/venv.html[virtual environment]: ---- -$ tox -e mkvenv +$ tox -e mkvenv-pypi ---- -On Windows, run tox with the 'mkvenv-win' option, however make sure that ONLY Python3 is in your PATH before running tox. +If your distribution uses OpenSSL 1.1 (like Debian Stretch or Archlinux), you'll +need to set `LD_LIBRARY_PATH` to the OpenSSL 1.0 directory +(`export LD_LIBRARY_PATH=/usr/lib/openssl-1.0` on Archlinux) before starting +qutebrowser. ----- -$ tox -e mkvenv-win ----- +Alternatively, you can use `tox -e mkvenv` (without `-pypi`) to symlink your +local Qt install instead of installing PyQt in the virtualenv. However, unless +you have QtWebKit-NG or QtWebEngine available, qutebrowser will use the legacy +QtWebKit backend. -This installs all needed Python dependencies in a `.venv` subfolder. The -system-wide Qt5/PyQt5 installations are symlinked into the virtual environment. +On Windows, run `tox -e 'mkvenv-win' instead, however make sure that ONLY +Python3 is in your PATH before running tox. + +This installs all needed Python dependencies in a `.venv` subfolder. You can then create a simple wrapper script to start qutebrowser somewhere in your `$PATH` (e.g. `/usr/local/bin/qutebrowser` or `~/bin/qutebrowser`): @@ -364,14 +371,6 @@ your `$PATH` (e.g. `/usr/local/bin/qutebrowser` or `~/bin/qutebrowser`): ~/path/to/qutebrowser/.venv/bin/python3 -m qutebrowser "$@" ---- -If you are developing on qutebrowser, you may want to redirect it to a local -config: - ----- -#!/bin/bash -~/path/to/qutebrowser/.venv/bin/python3 -m qutebrowser -c .qutebrowser-local "$@" ----- - Updating ~~~~~~~~ @@ -382,5 +381,5 @@ virtualenv. Thus it's recommended to run the following command to recreate the virtualenv: ---- -$ tox -r -e mkvenv +$ tox -r -e mkvenv-pypi ---- diff --git a/MANIFEST.in b/MANIFEST.in index 822cc58f0..ec3e90473 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -8,7 +8,7 @@ graft icons graft doc/img graft misc/apparmor graft misc/userscripts -recursive-include scripts *.py +recursive-include scripts *.py *.sh include qutebrowser/utils/testfile include qutebrowser/git-commit-id include COPYING doc/* README.asciidoc CONTRIBUTING.asciidoc FAQ.asciidoc INSTALL.asciidoc CHANGELOG.asciidoc @@ -30,20 +30,15 @@ prune tests prune qutebrowser/3rdparty prune misc/requirements prune misc/docker -exclude .editorconfig exclude pytest.ini exclude qutebrowser.rcc -exclude .coveragerc -exclude .pylintrc exclude qutebrowser/javascript/.eslintrc.yaml exclude qutebrowser/javascript/.eslintignore exclude doc/help -exclude .appveyor.yml -exclude .travis.yml +exclude .* exclude codecov.yml -exclude .pydocstylerc exclude misc/appveyor_install.py exclude misc/qutebrowser.spec -exclude .flake8 +exclude misc/qutebrowser.nsi global-exclude __pycache__ *.pyc *.pyo diff --git a/README.asciidoc b/README.asciidoc index 99062bd99..f7ad0f64d 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -36,11 +36,8 @@ Downloads --------- 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). - -See link:INSTALL.asciidoc[INSTALL] for detailed instructions on how to get -qutebrowser running for various platforms. +page] for available downloads and the link:INSTALL.asciidoc[INSTALL] file for +detailed instructions on how to get qutebrowser running on various platforms. Documentation ------------- @@ -71,7 +68,11 @@ https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser[mailinglist] at mailto:qutebrowser@lists.qutebrowser.org[]. There's also a https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser-announce[announce-only mailinglist] -at mailto:qutebrowser-announce@lists.qutebrowser.org[]. +at mailto:qutebrowser-announce@lists.qutebrowser.org[] (the announcements also +get sent to the general qutebrowser@ list). + +If you're a reddit user, there's a +https://www.reddit.com/r/qutebrowser/[/r/qutebrowser] subreddit there. Contributions / Bugs -------------------- @@ -97,26 +98,36 @@ Requirements 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 (old or link:https://github.com/annulen/webkit/wiki[reloaded]/NG) or QtWebEngine +* http://www.python.org/[Python] 3.4 or newer (3.6 recommended) - note that + support for Python 3.4 + https://github.com/qutebrowser/qutebrowser/issues/2742[will be dropped soon]. +* http://qt.io/[Qt] 5.2.0 or newer (5.9 recommended - note that support for Qt + < 5.7.1 will be dropped soon) with the following modules: + - QtCore / qtbase + - QtQuick (part of qtbase in some distributions) + - QtSQL (part of qtbase in some distributions) + - QtWebEngine, or + - QtWebKit (old or link:https://github.com/annulen/webkit/wiki[reloaded]/NG). + Note that support for legacy QtWebKit (before 5.212) will be + dropped soon. * http://www.riverbankcomputing.com/software/pyqt/intro[PyQt] 5.2.0 or newer -(5.5.1 recommended) for Python 3 + (5.9 recommended) for Python 3. Note that support for PyQt < 5.7 will be + dropped soon. * https://pypi.python.org/pypi/setuptools/[pkg_resources/setuptools] * http://fdik.org/pyPEG/[pyPEG2] * http://jinja.pocoo.org/[jinja2] * http://pygments.org/[pygments] * http://pyyaml.org/wiki/PyYAML[PyYAML] +* http://pyopengl.sourceforge.net/[PyOpenGL] when using QtWebEngine -The following libraries are optional and provide a better user experience: +The following libraries are optional: -* http://cthedot.de/cssutils/[cssutils] - -To generate the documentation for the `:help` command, when using the git -repository (rather than a release), http://asciidoc.org/[asciidoc] is needed. - -On Windows, https://pypi.python.org/pypi/colorama/[colorama] is needed to -display colored log output. +* http://cthedot.de/cssutils/[cssutils] (for an improved `:download --mhtml` + with QtWebKit) +* On Windows, https://pypi.python.org/pypi/colorama/[colorama] for colored log + output. +* http://asciidoc.org/[asciidoc] to generate the documentation for the `:help` + command, when using the git repository (rather than a release). See link:INSTALL.asciidoc[INSTALL] for directions on how to install qutebrowser and its dependencies. @@ -140,209 +151,59 @@ get in touch! Authors ------- -Contributors, sorted by the number of commits in descending order: +qutebrowser's primary author is Florian Bruhin (The Compiler), but qutebrowser +wouldn't be what it is without the help of +https://github.com/qutebrowser/qutebrowser/graphs/contributors[hundreds of contributors]! -// QUTE_AUTHORS_START -* Florian Bruhin -* Daniel Schadt -* Ryan Roden-Corrent -* Jan Verbeek -* Jakub Klinkovský -* Antoni Boucher -* Lamar Pavel -* Marshall Lochbaum -* Bruno Oliveira -* Alexander Cogneau -* Felix Van der Jeugt -* Daniel Karbach -* Imran Sobir -* Martin Tournoij -* Kevin Velghe -* Raphael Pierzina -* Joel Torstensson -* Patric Schmitz -* Tarcisio Fedrizzi -* Claude -* Corentin Julé -* meles5 -* Philipp Hansch -* Panagiotis Ktistakis -* Artur Shaik -* Nathan Isom -* Thorsten Wißmann -* Austin Anderson -* Fritz Reichwald -* Jimmy -* Niklas Haas -* Maciej Wołczyk -* Spreadyy -* Alexey "Averrin" Nabrodov -* pkill9 -* nanjekyejoannah -* avk -* ZDarian -* Milan Svoboda -* John ShaggyTwoDope Jenkins -* Clayton Craft -* Peter Vilim -* knaggita -* Oliver Caldwell -* Julian Weigt -* Tomasz Kramkowski -* Sebastian Frysztak -* Nikolay Amiantov -* Julie Engel -* Jonas Schürmann -* error800 -* Michael Hoang -* Liam BEGUIN -* Daniel Fiser -* skinnay -* Zach-Button -* Samuel Walladge -* Peter Rice -* Ismail S -* Halfwit -* David Vogt -* Claire Cavanaugh -* rikn00 -* kanikaa1234 -* haitaka -* Nick Ginther -* Michał Góral -* Michael Ilsaas -* Martin Zimmermann -* Jussi Timperi -* 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 -* Edgar Hipp -* Daryl Finlay -* arza -* adam -* Samir Benmendil -* Regina Hug -* Mathias Fussenegger -* Marcelo Santos -* Joel Bradshaw -* Jean-Louis Fuchs -* Franz Fellner -* Eric Drechsel -* zwarag -* xd1le -* rmortens -* oniondreams -* issue -* haxwithaxe -* evan -* dylan araps -* caveman -* addictedtoflames -* Xitian9 -* Vasilij Schneidermann -* Tomas Orsava -* Tom Janson -* Tobias Werth -* Tim Harder -* Thiago Barroso Perrotta -* Sorokin Alexei -* Simon Désaulniers -* Rok Mandeljc -* Noah Huesser -* Moez Bouhlel -* Matthias Lisin -* Marcel Schilling -* Lazlow Carmichael -* Kevin Wang -* Ján Kobezda -* Johannes Martinsson -* Jean-Christophe Petkovich -* Jay Kamat -* Helen Sherwood-Taylor -* HalosGhost -* Gregor Pohl -* Eivind Uggedal -* Dietrich Daroch -* Derek Sivers -* Daniel Lu -* Arseniy Seroka -* Andy Balaam -* Andreas Fischer -* Akselmo -// QUTE_AUTHORS_END - -The following people have contributed graphics: +Additionally, the following people have contributed graphics: * Jad/link:http://yelostudio.com[yelo] (new icon) * WOFall (original icon) * regines (key binding cheatsheet) -Thanks / Similar projects -------------------------- +Also, thanks to everyone who contributed to one of qutebrowser's +link:doc/backers.asciidoc[crowdfunding campaigns]! -Many projects with a similar goal as qutebrowser exist: - -* http://portix.bitbucket.org/dwb/[dwb] (C, GTK+ with WebKit1, currently -http://www.reddit.com/r/linux/comments/2huqbc/dwb_abandoned/[unmaintained] - -main inspiration for qutebrowser) -* https://github.com/fanglingsu/vimb[vimb] (C, GTK+ with WebKit1, active) -* http://sourceforge.net/p/vimprobable/wiki/Home/[vimprobable] (C, GTK+ with -WebKit1, dead) -* http://surf.suckless.org/[surf] (C, GTK+ with WebKit1, active) -* https://mason-larobina.github.io/luakit/[luakit] (C/Lua, GTK+ with -WebKit1, not very active) -* http://pwmt.org/projects/jumanji/[jumanji] (C, GTK+ with WebKit1, not very -active) -* http://www.uzbl.org/[uzbl] (C, GTK+ with WebKit1/WebKit2, active) -* http://conkeror.org/[conkeror] (Javascript, Emacs-like, XULRunner/Gecko, -active) -* https://github.com/AeroNotix/lispkit[lispkit] (quite new, lisp, GTK+ with -WebKit, active) -* http://www.vimperator.org/[Vimperator] (Firefox addon) -* http://5digits.org/pentadactyl/[Pentadactyl] (Firefox addon) -* https://github.com/akhodakivskiy/VimFx[VimFx] (Firefox addon) -* https://github.com/1995eaton/chromium-vim[cVim] (Chrome/Chromium addon) -* http://vimium.github.io/[vimium] (Chrome/Chromium addon) -* https://chrome.google.com/webstore/detail/vichrome/gghkfhpblkcmlkmpcpgaajbbiikbhpdi?hl=en[ViChrome] (Chrome/Chromium addon) -* https://github.com/jinzhu/vrome[Vrome] (Chrome/Chromium addon) +Similar projects +---------------- +Many projects with a similar goal as qutebrowser exist. Most of them were inspirations for qutebrowser in some way, thanks for that! -Thanks as well to the following projects and people for helping me with -problems and helpful hints: +Active +~~~~~~ -* http://eric-ide.python-projects.org/[eric5] / Detlev Offenbach -* https://code.google.com/p/devicenzo/[devicenzo] -* portix -* seir -* nitroxleecher +* https://fanglingsu.github.io/vimb/[vimb] (C, GTK+ with WebKit2) +* https://luakit.github.io/luakit/[luakit] (C/Lua, GTK+ with WebKit2) +* http://surf.suckless.org/[surf] (C, GTK+ with WebKit1/WebKit2) +* http://www.uzbl.org/[uzbl] (C, GTK+ with WebKit1/WebKit2) +* Chrome/Chromium addons: + https://github.com/1995eaton/chromium-vim[cVim], + http://vimium.github.io/[Vimium], + https://github.com/brookhong/Surfingkeys[Surfingkeys], + http://saka-key.lusakasa.com/[Saka Key] +* Firefox addons (based on WebExtensions): + https://addons.mozilla.org/en-GB/firefox/addon/vimium-ff/[Vimium-FF] (experimental), + http://saka-key.lusakasa.com/[Saka Key] -Also, thanks to: +Inactive +~~~~~~~~ -* Everyone contributing to the link:doc/backers.asciidoc[crowdfunding]. -* Everyone who had the patience to test qutebrowser before v0.1. -* Everyone triaging/fixing my bugs in the -https://bugreports.qt.io/secure/Dashboard.jspa[Qt bugtracker] -* Everyone answering my questions on http://stackoverflow.com/[Stack Overflow] -and in IRC. -* All the projects which were a great help while developing qutebrowser. +* https://bitbucket.org/portix/dwb[dwb] (C, GTK+ with WebKit1, +https://bitbucket.org/portix/dwb/pull-requests/22/several-cleanups-to-increase-portability/diff[unmaintained] - +main inspiration for qutebrowser) +* http://sourceforge.net/p/vimprobable/wiki/Home/[vimprobable] (C, GTK+ with + WebKit1) +* http://pwmt.org/projects/jumanji/[jumanji] (C, GTK+ with WebKit1) +* http://conkeror.org/[conkeror] (Javascript, Emacs-like, XULRunner/Gecko) +* Firefox addons (not based on WebExtensions or no recent activity): + http://www.vimperator.org/[Vimperator], + http://5digits.org/pentadactyl/[Pentadactyl], + https://github.com/akhodakivskiy/VimFx[VimFx], + https://github.com/shinglyu/QuantumVim[QuantumVim] +* Chrome/Chromium addons: + https://chrome.google.com/webstore/detail/vichrome/gghkfhpblkcmlkmpcpgaajbbiikbhpdi?hl=en[ViChrome], + https://github.com/jinzhu/vrome[Vrome] License ------- diff --git a/codecov.yml b/codecov.yml index 6d4f1abce..47e3c919c 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,9 +1,7 @@ -status: - project: - enabled: no - patch: - enabled: no - changes: - enabled: no +coverage: + status: + project: off + patch: off + changes: off comment: off diff --git a/doc/backers.asciidoc b/doc/backers.asciidoc index 4c9ef85c1..811cde75e 100644 --- a/doc/backers.asciidoc +++ b/doc/backers.asciidoc @@ -1,13 +1,135 @@ Crowdfunding backers ==================== +2017 +---- + +Mid-2017, qutebrowser had its +https://www.kickstarter.com/projects/the-compiler/qutebrowser-v10-with-per-domain-settings[second crowdfunding] +with the goal of implementing the new config system and releasing v1.0. + +Thanks a lot to the following people who contributed to it: + +Gold sponsors +~~~~~~~~~~~~~ + +TODO + +Silver sponsors +~~~~~~~~~~~~~~~ + +TODO + +Other sponsors +~~~~~~~~~~~~~~ + +TODO: people with t-shirts or higher pledge levels + +- 7scan +- Alex Suykov +- Alexey Zhikhartsev +- Allan Nordhøy +- Anirudh Sanjeev +- Anssi Puustinen +- Benedikt Steindorf +- Bernardo Kuri +- Blaise Duszynski +- Bostan +- Bruno Oliveira +- Colin Jacobs +- Daniel Andersson +- Danilo +- David Beley +- David Hollings +- David Parrish +- Derin Yarsuvat +- Dmytro Kostiuchenko +- Frederik Thorøe +- G4v4g4i +- Gyula Teleki +- H +- Hosaka +- Iordanis Grigoriou +- Isaac Sandaljian +- Jakub Podeszwik +- Jamie Anderson +- Jasper Woudenberg +- Jens Højgaard +- Johannes +- John Baber-Lucero +- Jonas Schürmann +- Kenichiro Ito +- Kenny Low +- Lars Ivar Igesund +- Lucas Aride Moulin +- Ludovic Chabant +- Lukas Gierth +- Marulkan +- Matthew Chun-Lum +- Matthew Cronen +- Matthew Quigley +- Michael Schönwälder +- Mika Kutila +- Mitchell Stokes +- Nathan Howell +- Nathan Schlehlein +- Noël Zindel +- Obri +- Patrik Peng +- Peter DiMarco +- Peter Rice +- Philipp Middendorf +- Pkill9 +- Prescott +- Robotichead +- Roshless +- Ryan Ellis +- Ryan P Deslandes +- Sam Doshi +- Sam Stone +- Sean Herman +- Sebastian Frysztak +- Shelby Cruver +- SirCmpwn +- Soham Pal +- Stewart Webb +- Sven Reinecke +- Tom Bass +- Tomas Slusny +- Tomasz Kramkowski +- Tommy Thomas +- Vasilij Schneidermann +- Vlaaaaaaad +- beanieuptop +- demure +- evenorbert +- fishss +- gsnewmark +- guillermohs9 +- hubcaps +- lobachevsky +- neodarz +- nihlaeth +- notbenh +- patrick suwanvithaya +- pyratebeard +- randm_dave +- sabreman +- toml +- vimja +- wiz +- 43 Anonymous + +2016 +---- + Mid-2016, qutebrowser did run a http://igg.me/at/qutebrowser[crowdfunding] for QtWebEngine support in qutebrowser. Thanks a lot to the following people who contributed to it: Gold sponsors -------------- +~~~~~~~~~~~~~ - Chris Salzberg - Clayton Craft @@ -16,7 +138,7 @@ Gold sponsors - 1 Anonymous Day sponsors ------------- +~~~~~~~~~~~~ - Agent 42 - Iggy Jackson @@ -28,7 +150,7 @@ Day sponsors - 4 Anonymous Other sponsors --------------- +~~~~~~~~~~~~~~ - AP M - Alessandro Balzano diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc index 3e10b9d81..4fca0a272 100644 --- a/doc/help/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -1,5 +1,5 @@ // DO NOT EDIT THIS FILE DIRECTLY! -// It is autogenerated from docstrings by running: +// It is autogenerated by running: // $ python3 scripts/dev/src2asciidoc.py = Commands @@ -82,9 +82,10 @@ It is possible to run or bind multiple commands by separating them with `;;`. |<>|Move the current tab according to the argument and [count]. |<>|Switch to the next tab, or switch [count] tabs forward. |<>|Close all tabs except for the current one. +|<>|Pin/Unpin the current/[count]th tab. |<>|Switch to the previous tab, or switch [count] tabs back. |<>|Unbind a keychain. -|<>|Re-open a closed tab (optionally skipping [count] closed tabs). +|<>|Re-open a closed tab. |<>|Show the source of the current page in a new tab. |<>|Close all windows except for the current one. |<>|Save open pages and quit. @@ -544,7 +545,8 @@ For `increment` and `decrement`, the number to change the URL by. For `up`, the [[open]] === open -Syntax: +:open [*--implicit*] [*--bg*] [*--tab*] [*--window*] [*--secure*] ['url']+ +Syntax: +:open [*--implicit*] [*--bg*] [*--tab*] [*--window*] [*--secure*] [*--private*] + ['url']+ Open a URL in the current/[count]th tab. @@ -560,6 +562,7 @@ If the URL contains newlines, each line gets opened in its own tab. * +*-t*+, +*--tab*+: Open in a new tab. * +*-w*+, +*--window*+: Open in a new window. * +*-s*+, +*--secure*+: Force HTTPS. +* +*-p*+, +*--private*+: Open a new window in private browsing mode. ==== count The tab index to open the URL in. @@ -743,6 +746,7 @@ Load a session. [[session-save]] === session-save Syntax: +:session-save [*--current*] [*--quiet*] [*--force*] [*--only-active-window*] + [*--with-private*] ['name']+ Save a session. @@ -756,6 +760,7 @@ Save a session. * +*-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. +* +*-p*+, +*--with-private*+: Include private windows. [[set]] === set @@ -831,7 +836,7 @@ Duplicate the current tab. [[tab-close]] === tab-close -Syntax: +:tab-close [*--prev*] [*--next*] [*--opposite*]+ +Syntax: +:tab-close [*--prev*] [*--next*] [*--opposite*] [*--force*]+ Close the current/[count]th tab. @@ -840,6 +845,7 @@ Close the current/[count]th tab. * +*-n*+, +*--next*+: Force selecting the tab after the current tab. * +*-o*+, +*--opposite*+: Force selecting the tab in the opposite direction of what's configured in 'tabs->select-on-remove'. +* +*-f*+, +*--force*+: Avoid confirmation for pinned tabs. ==== count The tab index to close @@ -892,13 +898,23 @@ How many tabs to switch forward. [[tab-only]] === tab-only -Syntax: +:tab-only [*--prev*] [*--next*]+ +Syntax: +:tab-only [*--prev*] [*--next*] [*--force*]+ Close all tabs except for the current one. ==== optional arguments * +*-p*+, +*--prev*+: Keep tabs before the current. * +*-n*+, +*--next*+: Keep tabs after the current. +* +*-f*+, +*--force*+: Avoid confirmation for pinned tabs. + +[[tab-pin]] +=== tab-pin +Pin/Unpin the current/[count]th tab. + +Pinning a tab shrinks it to tabs->pinned-width size. Attempting to close a pinned tab will cause a confirmation, unless --force is passed. + +==== count +The tab index to pin or unpin [[tab-prev]] === tab-prev @@ -920,7 +936,7 @@ Unbind a keychain. [[undo]] === undo -Re-open a closed tab (optionally skipping [count] closed tabs). +Re-open a closed tab. [[view-source]] === view-source @@ -1537,6 +1553,7 @@ These commands are mainly intended for debugging. They are hidden if qutebrowser |<>|Clear remembered SSL error answers. |<>|Show the debugging console. |<>|Crash for debugging purposes. +|<>|Dump the history to a file in the old pre-SQL format. |<>|Dump the current page's content to a file. |<>|Change the number of log lines to be stored in RAM. |<>|Change the log filter for console logging. @@ -1571,6 +1588,15 @@ Crash for debugging purposes. ==== positional arguments * +'typ'+: either 'exception' or 'segfault'. +[[debug-dump-history]] +=== debug-dump-history +Syntax: +:debug-dump-history 'dest'+ + +Dump the history to a file in the old pre-SQL format. + +==== positional arguments +* +'dest'+: Where to write the file to. + [[debug-dump-page]] === debug-dump-page Syntax: +:debug-dump-page [*--plain*] 'dest'+ @@ -1599,7 +1625,8 @@ Syntax: +:debug-log-filter 'filters'+ Change the log filter for console logging. ==== positional arguments -* +'filters'+: A comma separated list of logger names. +* +'filters'+: A comma separated list of logger names. Can also be "none" to clear any existing filters. + [[debug-log-level]] === debug-log-level @@ -1654,7 +1681,7 @@ Syntax: +:debug-webaction 'action'+ Execute a webaction. -See http://doc.qt.io/qt-5/qwebpage.html#WebAction-enum for the available actions. +Available actions: http://doc.qt.io/archives/qt-5.5/qwebpage.html#WebAction-enum (WebKit) http://doc.qt.io/qt-5/qwebenginepage.html#WebAction-enum (WebEngine) ==== positional arguments * +'action'+: The action to execute, e.g. MoveToNextChar. diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index a2df65ca4..92bc918e1 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -1,5 +1,5 @@ // DO NOT EDIT THIS FILE DIRECTLY! -// It is autogenerated from docstrings by running: +// It is autogenerated by running: // $ python3 scripts/dev/src2asciidoc.py = Settings @@ -18,11 +18,10 @@ |<>|How often (in milliseconds) to auto-save config/cookies/etc. |<>|The editor (and arguments) to use for the `open-editor` command. |<>|Encoding to use for editor. -|<>|Do not record visited pages in the history or store web page icons. +|<>|Open new windows in private browsing mode which does not record visited pages. |<>|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 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. @@ -48,7 +47,6 @@ |<>|Whether to expand each subframe to its contents. |<>|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 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. @@ -126,10 +124,13 @@ |<>|On which mouse button to close tabs. |<>|The position of the tab bar. |<>|Whether to show favicons in the tab bar. +|<>|Scale for favicons in the tab bar. The tab size is unchanged, so big favicons also require extra `tabs->padding`. |<>|The width of the tab bar if it's vertical, in px or as percentage of the window. +|<>|The width for pinned tabs with a horizontal tabbar, in px. |<>|Width of the progress indicator (0 to disable). |<>|Whether to open windows instead of tabs. |<>|The format to use for the tab title. The following placeholders are defined: +|<>|The format to use for the tab title for pinned tabs. The same placeholders like for title-format are defined. |<>|Alignment of the text inside of tabs |<>|Switch between tabs using the mouse wheel. |<>|Padding for tabs (top, bottom, left, right). @@ -144,12 +145,8 @@ |<>|Whether to prompt the user for the download location. |<>|Whether to remember the last used download directory. |<>|The maximum number of pages to hold in the global memory page cache. -|<>|The capacities for the global memory cache for dead objects such as stylesheets or scripts. Syntax: cacheMinDeadCapacity, cacheMaxDead, totalCapacity. -|<>|Default quota for new offline storage databases. -|<>|Quota for the offline web application cache. -|<>|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. +|<>|Whether support for the HTML 5 web application cache feature is enabled. +|<>|Whether support for HTML 5 local storage and Web SQL is enabled. |<>|Size of the HTTP network cache. Empty to use the default value. |============== @@ -161,7 +158,6 @@ |<>|Enables or disables the running of JavaScript programs. |<>|Enables or disables plugins in Web pages. |<>|Enables or disables WebGL. -|<>|Enable or disable support for CSS regions. |<>|Enable or disable hyperlink auditing (). |<>|Allow websites to request geolocations. |<>|Allow websites to show notifications. @@ -174,7 +170,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. Note this option needs a restart with QtWebEngine. +|<>|Whether to store cookies. Note this option needs a restart with QtWebEngine on Qt < 5.9. |<>|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. @@ -220,10 +216,14 @@ |<>|Color of the scrollbar in completion view |<>|Foreground color of the statusbar. |<>|Background color of the statusbar. +|<>|Foreground color of the statusbar in private browsing mode. +|<>|Background color of the statusbar in private browsing mode. |<>|Foreground color of the statusbar in insert mode. |<>|Background color of the statusbar in insert mode. |<>|Foreground color of the statusbar in command mode. |<>|Background color of the statusbar in command mode. +|<>|Foreground color of the statusbar in private browsing + command mode. +|<>|Background color of the statusbar in private browsing + command mode. |<>|Foreground color of the statusbar in caret mode. |<>|Background color of the statusbar in caret mode. |<>|Foreground color of the statusbar in caret mode with a selection @@ -394,7 +394,7 @@ Default: +pass:[utf-8]+ [[general-private-browsing]] === private-browsing -Do not record visited pages in the history or store web page icons. +Open new windows in private browsing mode which does not record visited pages. Valid values: @@ -403,8 +403,6 @@ Valid values: Default: +pass:[false]+ -This setting is only available with the QtWebKit backend. - [[general-developer-extras]] === developer-extras Enable extra tools for Web developers. @@ -445,26 +443,13 @@ Valid values: Default: +pass:[false]+ -[[general-site-specific-quirks]] -=== site-specific-quirks -Enable QtWebKit workarounds for broken sites. - -Valid values: - - * +true+ - * +false+ - -Default: +pass:[true]+ - -This setting is only available with the QtWebKit backend. - [[general-default-encoding]] === default-encoding Default encoding to use for websites. -The encoding must be a string describing an encoding such as _utf-8_, _iso-8859-1_, etc. If left empty a default value will be used. +The encoding must be a string describing an encoding such as _utf-8_, _iso-8859-1_, etc. -Default: empty +Default: +pass:[iso-8859-1]+ [[general-new-instance-open-target]] === new-instance-open-target @@ -581,6 +566,7 @@ Default: +pass:[bottom]+ [[ui-message-timeout]] === message-timeout Time (in ms) to show messages in the statusbar for. +Set to 0 to never clear messages. Default: +pass:[2000]+ @@ -653,14 +639,6 @@ Valid values: Default: +pass:[true]+ -[[ui-css-media-type]] -=== css-media-type -Set the CSS media type. - -Default: empty - -This setting is only available with the QtWebKit backend. - [[ui-smooth-scrolling]] === smooth-scrolling Whether to enable smooth scrolling for web pages. Note smooth scrolling does not work with the :scroll-px command. @@ -707,6 +685,8 @@ The format to use for the window title. The following placeholders are defined: * `{scroll_pos}`: The page scroll position. * `{host}`: The host of the current web page. * `{backend}`: Either 'webkit' or 'webengine' +* `{private}` : Indicates when private mode is enabled. + Default: +pass:[{perc}{title}{title_sep}qutebrowser]+ @@ -809,8 +789,6 @@ 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. @@ -921,7 +899,7 @@ How many URLs to show in the web history. 0: no history / -1: unlimited -Default: +pass:[1000]+ +Default: +pass:[-1]+ [[completion-quick-complete]] === quick-complete @@ -1205,12 +1183,24 @@ Valid values: Default: +pass:[true]+ +[[tabs-favicon-scale]] +=== favicon-scale +Scale for favicons in the tab bar. The tab size is unchanged, so big favicons also require extra `tabs->padding`. + +Default: +pass:[1.0]+ + [[tabs-width]] === width The width of the tab bar if it's vertical, in px or as percentage of the window. Default: +pass:[20%]+ +[[tabs-pinned-width]] +=== pinned-width +The width for pinned tabs with a horizontal tabbar, in px. + +Default: +pass:[43]+ + [[tabs-indicator-width]] === indicator-width Width of the progress indicator (0 to disable). @@ -1241,9 +1231,17 @@ The format to use for the tab title. The following placeholders are defined: * `{scroll_pos}`: The page scroll position. * `{host}`: The host of the current web page. * `{backend}`: Either 'webkit' or 'webengine' +* `{private}` : Indicates when private mode is enabled. + Default: +pass:[{index}: {title}]+ +[[tabs-title-format-pinned]] +=== title-format-pinned +The format to use for the tab title for pinned tabs. The same placeholders like for title-format are defined. + +Default: +pass:[{index}]+ + [[tabs-title-alignment]] === title-alignment Alignment of the text inside of tabs @@ -1319,55 +1317,12 @@ The Page Cache allows for a nicer user experience when navigating forth or back For more information about the feature, please refer to: http://webkit.org/blog/427/webkit-page-cache-i-the-basics/ -Default: empty +Default: +pass:[0]+ This setting is only available with the QtWebKit backend. -[[storage-object-cache-capacities]] -=== object-cache-capacities -The capacities for the global memory cache for dead objects such as stylesheets or scripts. Syntax: cacheMinDeadCapacity, cacheMaxDead, totalCapacity. - -The _cacheMinDeadCapacity_ specifies the minimum number of bytes that dead objects should consume when the cache is under pressure. - -_cacheMaxDead_ is the maximum number of bytes that dead objects should consume when the cache is *not* under pressure. - -_totalCapacity_ specifies the maximum number of bytes that the cache should consume *overall*. - -Default: empty - -This setting is only available with the QtWebKit backend. - -[[storage-offline-storage-default-quota]] -=== offline-storage-default-quota -Default quota for new offline storage databases. - -Default: empty - -This setting is only available with the QtWebKit backend. - -[[storage-offline-web-application-cache-quota]] -=== offline-web-application-cache-quota -Quota for the offline web application cache. - -Default: empty - -This setting is only available with the QtWebKit backend. - -[[storage-offline-storage-database]] -=== offline-storage-database -Whether support for the HTML 5 offline storage feature is enabled. - -Valid values: - - * +true+ - * +false+ - -Default: +pass:[true]+ - -This setting is only available with the QtWebKit backend. - -[[storage-offline-web-application-storage]] -=== offline-web-application-storage +[[storage-offline-web-application-cache]] +=== offline-web-application-cache Whether support for the HTML 5 web application cache feature is enabled. An application cache acts like an HTTP cache in some sense. For documents that use the application cache via JavaScript, the loader engine will first ask the application cache for the contents, before hitting the network. @@ -1385,7 +1340,7 @@ This setting is only available with the QtWebKit backend. [[storage-local-storage]] === local-storage -Whether support for the HTML 5 local storage feature is enabled. +Whether support for HTML 5 local storage and Web SQL is enabled. Valid values: @@ -1449,19 +1404,6 @@ Valid values: Default: +pass:[true]+ -[[content-css-regions]] -=== css-regions -Enable or disable support for CSS regions. - -Valid values: - - * +true+ - * +false+ - -Default: +pass:[true]+ - -This setting is only available with the QtWebKit backend. - [[content-hyperlink-auditing]] === hyperlink-auditing Enable or disable hyperlink auditing (). @@ -1608,7 +1550,7 @@ This setting is only available with the QtWebKit backend. [[content-cookies-store]] === cookies-store -Whether to store cookies. Note this option needs a restart with QtWebEngine. +Whether to store cookies. Note this option needs a restart with QtWebEngine on Qt < 5.9. Valid values: @@ -1898,6 +1840,18 @@ Background color of the statusbar. Default: +pass:[black]+ +[[colors-statusbar.fg.private]] +=== statusbar.fg.private +Foreground color of the statusbar in private browsing mode. + +Default: +pass:[${statusbar.fg}]+ + +[[colors-statusbar.bg.private]] +=== statusbar.bg.private +Background color of the statusbar in private browsing mode. + +Default: +pass:[#666666]+ + [[colors-statusbar.fg.insert]] === statusbar.fg.insert Foreground color of the statusbar in insert mode. @@ -1922,6 +1876,18 @@ Background color of the statusbar in command mode. Default: +pass:[${statusbar.bg}]+ +[[colors-statusbar.fg.command.private]] +=== statusbar.fg.command.private +Foreground color of the statusbar in private browsing + command mode. + +Default: +pass:[${statusbar.fg.private}]+ + +[[colors-statusbar.bg.command.private]] +=== statusbar.bg.command.private +Background color of the statusbar in private browsing + command mode. + +Default: +pass:[${statusbar.bg.private}]+ + [[colors-statusbar.fg.caret]] === statusbar.fg.caret Foreground color of the statusbar in caret mode. @@ -2266,7 +2232,7 @@ Fonts used for the UI, with optional style/weight/size. === _monospace Default monospace fonts. -Default: +pass:[Terminus, Monospace, "DejaVu Sans Mono", Monaco, "Bitstream Vera Sans Mono", "Andale Mono", "Courier New", Courier, "Liberation Mono", monospace, Fixed, Consolas, Terminal]+ +Default: +pass:[xos4 Terminus, Terminus, Monospace, "DejaVu Sans Mono", Monaco, "Bitstream Vera Sans Mono", "Andale Mono", "Courier New", Courier, "Liberation Mono", monospace, Fixed, Consolas, Terminal]+ [[fonts-completion]] === completion @@ -2350,25 +2316,25 @@ Default: empty === web-size-minimum The hard minimum font size. -Default: empty +Default: +pass:[0]+ [[fonts-web-size-minimum-logical]] === web-size-minimum-logical The minimum logical font size that is applied when zooming out. -Default: empty +Default: +pass:[6]+ [[fonts-web-size-default]] === web-size-default The default font size for regular text. -Default: empty +Default: +pass:[16]+ [[fonts-web-size-default-fixed]] === web-size-default-fixed The default font size for fixed-pitch text. -Default: empty +Default: +pass:[13]+ [[fonts-keyhint]] === keyhint diff --git a/doc/img/completion.png b/doc/img/completion.png index 9ba30877e..84a49828f 100644 Binary files a/doc/img/completion.png and b/doc/img/completion.png differ diff --git a/doc/img/downloads.png b/doc/img/downloads.png index 530210c7f..cf2e6951d 100644 Binary files a/doc/img/downloads.png and b/doc/img/downloads.png differ diff --git a/doc/img/hints.png b/doc/img/hints.png index 812af3581..3407ddf47 100644 Binary files a/doc/img/hints.png and b/doc/img/hints.png differ diff --git a/doc/img/main.png b/doc/img/main.png index b371c5fba..85da51a2f 100644 Binary files a/doc/img/main.png and b/doc/img/main.png differ diff --git a/doc/notes b/doc/notes deleted file mode 100644 index 2261e3228..000000000 --- a/doc/notes +++ /dev/null @@ -1,196 +0,0 @@ -henk's thoughts -=============== - -1. Power to the user! Protect privacy! -Things the browser should only do with explicit consent from the user, if -applicable the user should be able to choose which protocol/host/port triplets -to white/blacklist: - -- load/run executable code, like js, flash, java applets, ... (think NoScript) -- requests to other domains, ports or using a different protocol than what the - user requested (think RequestPolicy) -- accept cookies -- storing/saving/caching things, e.g. open tabs ("session"), cookies, page - contents, browsing/download history, form data, ... -- send referrer -- disclose any (presence, type, version, settings, capabilities, etc.) - information about OS, browser, installed fonts, plugins, addons, etc. - -2. Be efficient! -I tend to leave a lot of tabs open and nobody can deny that some websites -simply suck, so the browser should, unless told otherwise by the user: - -- load tabs only when needed -- run code in tabs only when needed, i.e. when the tab is currently being - used/viewed (background tabs doing some JS magic even when they are not being - used can create a lot of unnecessary load on the machine) -- finish requests to the domain the user requested (e.g. www.example.org) - before doing any requests to other subdomains (e.g. images.example.org) and - finish those before doing requests to thirdparty domains (e.g. example.com) - -3. Be stable! -- one site should not make the complete browser crash, only that site's tab - - -Upstream Bugs -============= - -- Web inspector is blank unless .hide()/.show() is called. - Asked on SO: http://stackoverflow.com/q/23499159/2085149 - TODO: Report to PyQt/Qt - -- Report some other crashes - - -/u/angelic_sedition's thoughts -============================== - -Well support for greasemonkey scripts and bookmarklets/js (which was mentioned -in the arch forum post) would be a big addition. What I've usually missed when -using other vim-like browsers is things that allow for different settings and -key bindings for different contexts. With that implemented I think I could -switch to a lightweight browser (and believe me, I'd like to) for the most part -and only use firefox when I needed downthemall or something. - -For example, I have different bindings based on tab position that are reloaded -with a pentadactyl autocmd so that will take me to tab -1-10 if I'm in that range or 2-20 if I'm in that range. I have an autocmd that -will run on completed downloads that passes the file path to a script that will -open ranger in a floating window with that file cut (this is basically like -using ranger to save files instead of the crappy gui popup). - -I also have a few bindings based on tabgroups. Tabgroups are a firefox feature, -but I find them very useful for sorting things by topic so that only the tabs -I'm interested at the moment are visible. - -Pentadactyl has a feature it calls groups. You can create a group that will -activate for sites/urls that match a pattern with some regex support. This -allows me, for example, to set up different (more convenient) bindings for -zooming only on images. I'll never need use the equivalent of vim n (next text -search match), so I can bind that to zoom. This allows setting up custom -quickmarks/gotos using the same keys for different websites. For example, on -reddit I have different g(some key) bindings to go to different subreddits. -This can also be used to pass certain keys directly to the site (e.g. for use -with RES). For sites that don't have modifiable bindings, I can use this with -pentadactyl's feedkeys or xdotool to create my own custom bindings. I even have -a binding that will call out to bash script with different arguments depending -on the site to download an image or an image gallery depending on the site (in -some cases passing the url to some cli program). - -I've also noticed the lack of completion. For example, on "o" pentadactyl will -show sites (e.g. from history) that can be completed. I think I've been spoiled -by pentadactyl having completion for just about everything. - - -suckless surf ML post -===================== - -From: Ben Woolley -Date: Wed, 7 Jan 2015 18:29:25 -0800 - -Hi all, - -This patch is a bit of a beast for surf. It is intended to be applied after -the disk cache patch. It breaks some internal interfaces, so it could -conflict with other patches. - -I have been wanting a browser to implement a complete same-origin policy, -and have been investigating how to do this in various browsers for many -months. When I saw how surf opened new windows in a separate process, and -was so simple, I knew I could do it quickly. Over the last two weeks, I -have been developing this implementation on surf. - -The basic idea is to prevent browser-based tracking as you browse from site -to site, or origin to origin. By "origin" domain, I mean the "first-party" -domain, the domain normally in the location bar (of the typical browser -interface). Each origin domain effectively gets its own browser profile, -and a browser process only ever deals with one origin domain at a time. -This isolates origins vertically, preventing cookies, disk cache, memory -cache, and window.name vulnerabilities. Basically, all known -vulnerabilities that google and Mozilla cite as counter-examples when they -explain why they haven't disabled third-party cookies yet. - -When you are on msnbc.com, the tracking pixels will be stored in a cookie -file for msnbc.com. When you go to cnn.com, the tracking pixels will be -stored in a cookie file for cnn.com. You will not be tracked between them. -However, third-party cookies, and the caching of third party resources will -still work, but they will be isolated between origin domains. Instead of -blocking cookies and cache entries, they are "double-keyed", or *also* -keyed by origin. - -There is a unidirectional communication channel, however, from one origin -to the next, through navigation from one origin to the next. That is, the -query string is passed from one origin to the next, and may embed -identifiers. One example is an affiliate link that identifies where the -lead came from. I have implemented what I call "horizontal isolation", in -the form of an "Origin Crossing Gate". - -Whenever you follow a link to a new domain, or even are just redirected to -a new domain, a new window/tab is opened, and passed the referring origin -via -R. The page passed to -O, for example -O originprompt.html, is an HTML -page that is loaded in the new origin's context. That page tells you the -origin you were on, the new origin, and the full link, and you can decide -to go just to the new origin, or go to the full URL, after reviewing it for -tracking data. - -Also, you may click links that store your trust of that relationship with -various expiration times, the same way you would trust geolocation requests -for a particular origin for a period of time. The database used is actually -the new origin's cookie file. Since the origin prompt is loaded in the new -origin's context, I can set a cookie on behalf of the new origin. The -expiration time of the trust is the expiration time of the cookie. The -cookie implementation in webkit automatically expires the trust as part of -how cookies work. Each time you cross an origin, the origin crossing page -checks the cookie to see if trust is still established. If so, it will use -window.location.replace() to continue on automatically. The initial page -renders blank until the trust is invalidated, in which case the content of -the gate is made visible. - -However, the new origin is technically able to mess with those cookies, so -a website could set trust for an origin crossing. I have addressed that by -hashing the key with a salt, and setting the real expiration time as the -value, along with an HMAC to verify the contents of the value. If the -cookie is messed with in any way, the trust will be disabled, and the -prompt will appear again. So it has a fail-safe function. - -I know it seems a bit convoluted, but it just started out as a nice little -rabbit hole, and I just wanted to get something workable. At first I -thought using the cookie expiration time was convenient, but then when I -realized that I needed to protect the cookie, things got a bit hairy. But -it works. - -Each profile is, by default, stored in ~/.surf/origins/$origin/ -The interesting side effect is that if there is a problem where a website -relies on the cross-site cookie vulnerability to make a connection, you can -simply make a symbolic link from one origin folder to another, and they -will share the same profile. And if you want to delete cookies and/or cache -for a particular origin, you just rm -rf the origin's profile folder, and -don't have to interfere with your other sites that are working just fine. - -One thing I don't handle are cross-origins POSTs. They just end up as GET -requests right now. I intend to do something about that, but I haven't -figured that out yet. - -I have only been using this functionality for a few days myself, so I have -absolutely no feedback yet. I wanted to provide the first implementation of -the management of identity as a system resource the same way that things -like geolocation, camera, and microphone resources are managed in browsers -and mobile apps. - -Currently, Mozilla and Tor have are working on third-party tracking issues -in Firefox. -https://blog.mozilla.org/privacy/2014/11/10/introducing-polaris-privacy-initiative-to-accelerate-user-focused-privacy-online/ - -Up to this point, Tor has provided a patch that double-keys cookies with -the origin domain, but no other progress is visible. I have seen no -discussion of how horizontal isolation is supposed to happen, and I wanted -to show people that it can be done, and this is one way it can be done, and -to compel the other browser makers to catch up, and hopefully the community -can work toward a standard *without* the tracking loopholes, by showing -people what a *complete* solution looks like. - -Thank you, - -Ben Woolley - -Patch: http://lists.suckless.org/dev/att-25070/0005-same-origin-policy.patch diff --git a/doc/quickstart.asciidoc b/doc/quickstart.asciidoc index 7d597ed2e..4881cca62 100644 --- a/doc/quickstart.asciidoc +++ b/doc/quickstart.asciidoc @@ -31,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. (Currently not available with the QtWebEngine backend and on the OS X build - use the `:set` command instead) +* 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 macOS 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 36530bffe..1b8345b0c 100644 --- a/doc/qutebrowser.1.asciidoc +++ b/doc/qutebrowser.1.asciidoc @@ -57,7 +57,7 @@ show it. How URLs should be opened if there is already a qutebrowser instance running. *--backend* '{webkit,webengine}':: - Which backend to use (webengine backend is EXPERIMENTAL!). + Which backend to use. *--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. @@ -93,12 +93,6 @@ show it. *--nowindow*:: Don't show the main window. -*--debug-exit*:: - Turn on debugging of late exit. - -*--pdb-postmortem*:: - Drop into pdb on exceptions. - *--temp-basedir*:: Use a temporary basedir. @@ -110,6 +104,9 @@ show it. *--qt-flag* 'QT_FLAG':: Pass an argument to Qt as flag. + +*--debug-flag* 'DEBUG_FLAGS':: + Pass name of debugging feature to be turned on. // QUTE_OPTIONS_END == FILES diff --git a/doc/userscripts.asciidoc b/doc/userscripts.asciidoc index b44a6b8ff..85811266d 100644 --- a/doc/userscripts.asciidoc +++ b/doc/userscripts.asciidoc @@ -60,7 +60,7 @@ Sending commands Normal qutebrowser commands can be written to `$QUTE_FIFO` and will be executed. -On Unix/OS X, this is a named pipe and commands written to it will get executed +On Unix/macOS, this is a named pipe and commands written to it will get executed immediately. On Windows, this is a regular file, and the commands in it will be executed as diff --git a/misc/qutebrowser.nsi b/misc/qutebrowser.nsi new file mode 100644 index 000000000..8f815c391 --- /dev/null +++ b/misc/qutebrowser.nsi @@ -0,0 +1,77 @@ +Name "qutebrowser" + +Unicode true +RequestExecutionLevel admin +SetCompressor /solid lzma + +!ifdef X64 + OutFile "..\dist\qutebrowser-${VERSION}-amd64.exe" + InstallDir "$ProgramFiles64\qutebrowser" +!else + OutFile "..\dist\qutebrowser-${VERSION}-win32.exe" + InstallDir "$ProgramFiles\qutebrowser" +!endif + +;Default installation folder + +!include "MUI2.nsh" +;!include "MultiUser.nsh" + +!define MUI_ABORTWARNING +;!define MULTIUSER_MUI +;!define MULTIUSER_INSTALLMODE_COMMANDLINE +!define MUI_ICON "../icons/qutebrowser.ico" +!define MUI_UNICON "../icons/qutebrowser.ico" + +!insertmacro MUI_PAGE_LICENSE "..\COPYING" +!insertmacro MUI_PAGE_DIRECTORY +!insertmacro MUI_PAGE_INSTFILES +!insertmacro MUI_UNPAGE_CONFIRM +!insertmacro MUI_UNPAGE_INSTFILES + +!insertmacro MUI_LANGUAGE "English" + +; depends on admin status +;SetShellVarContext current + + +Section "Install" + + ; Uninstall old versions + ExecWait 'MsiExec.exe /quiet /qn /norestart /X{633F41F9-FE9B-42D1-9CC4-718CBD01EE11}' + ExecWait 'MsiExec.exe /quiet /qn /norestart /X{9331D947-AC86-4542-A755-A833429C6E69}' + + SetOutPath "$INSTDIR" + + !ifdef X64 + file /r "..\dist\qutebrowser-${VERSION}-x64\*.*" + !else + file /r "..\dist\qutebrowser-${VERSION}-x86\*.*" + !endif + + SetShellVarContext all + CreateShortCut "$SMPROGRAMS\qutebrowser.lnk" "$INSTDIR\qutebrowser.exe" + + ;Create uninstaller + WriteUninstaller "$INSTDIR\uninst.exe" + + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\qutebrowser" "DisplayName" "qutebrowser" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\qutebrowser" "UninstallString" '"$INSTDIR\uninst.exe"' + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\qutebrowser" "QuietUninstallString" '"$INSTDIR\uninst.exe" /S' + +SectionEnd + +;-------------------------------- +;Uninstaller Section + +Section "Uninstall" + + SetShellVarContext all + Delete "$SMPROGRAMS\qutebrowser.lnk" + + RMDir /r "$INSTDIR\*.*" + RMDir "$INSTDIR" + + DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\qutebrowser" + +SectionEnd diff --git a/misc/qutebrowser.spec b/misc/qutebrowser.spec index fe58891ac..cd0ce3883 100644 --- a/misc/qutebrowser.spec +++ b/misc/qutebrowser.spec @@ -18,10 +18,10 @@ def get_data_files(): ('../qutebrowser/git-commit-id', '') ] - if os.path.exists(os.path.join('qutebrowser', '3rdparty', 'pdfjs')): - data_files.append(('../qutebrowser/3rdparty/pdfjs', '3rdparty/pdfjs')) - else: - print("Warning: excluding pdfjs as it's not present!") + # if os.path.exists(os.path.join('qutebrowser', '3rdparty', 'pdfjs')): + # data_files.append(('../qutebrowser/3rdparty/pdfjs', '3rdparty/pdfjs')) + # else: + # print("Warning: excluding pdfjs as it's not present!") return data_files @@ -41,10 +41,10 @@ a = Analysis(['../qutebrowser/__main__.py'], pathex=['misc'], binaries=None, datas=get_data_files(), - hiddenimports=[], + hiddenimports=['PyQt5.QtOpenGL', 'PyQt5._QOpenGLFunctions_2_0'], hookspath=[], runtime_hooks=[], - excludes=[], + excludes=['tkinter'], win_no_prefer_redirects=False, win_private_assemblies=False, cipher=block_cipher) diff --git a/misc/requirements/requirements-codecov.txt b/misc/requirements/requirements-codecov.txt index 4f020b5cd..86f78562d 100644 --- a/misc/requirements/requirements-codecov.txt +++ b/misc/requirements/requirements-codecov.txt @@ -1,5 +1,9 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -codecov==2.0.5 -coverage==4.3.4 -requests==2.13.0 +certifi==2017.4.17 +chardet==3.0.4 +codecov==2.0.9 +coverage==4.4.1 +idna==2.5 +requests==2.18.1 +urllib3==1.21.1 diff --git a/misc/requirements/requirements-cxfreeze.txt b/misc/requirements/requirements-cxfreeze.txt deleted file mode 100644 index 58f14266e..000000000 --- a/misc/requirements/requirements-cxfreeze.txt +++ /dev/null @@ -1,3 +0,0 @@ -# This file is automatically generated by scripts/dev/recompile_requirements.py - -cx-Freeze==4.3.4 # rq.filter: < 5.0.0 diff --git a/misc/requirements/requirements-cxfreeze.txt-raw b/misc/requirements/requirements-cxfreeze.txt-raw deleted file mode 100644 index 2ae8920ca..000000000 --- a/misc/requirements/requirements-cxfreeze.txt-raw +++ /dev/null @@ -1,5 +0,0 @@ -cx-Freeze < 5.0.0 - -# We'll probably switch to PyInstaller soon, and 5.x doesn't install without a -# compiler? -#@ filter: cx-Freeze < 5.0.0 diff --git a/misc/requirements/requirements-flake8.txt b/misc/requirements/requirements-flake8.txt index 9c0aad110..f68bcb227 100644 --- a/misc/requirements/requirements-flake8.txt +++ b/misc/requirements/requirements-flake8.txt @@ -3,18 +3,21 @@ 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.3 +flake8-deprecated==1.2 +flake8-docstrings==1.0.3 # rq.filter: < 1.1.0 flake8-future-import==0.4.3 flake8-mock==0.3 -flake8-pep3101==1.0 +flake8-pep3101==1.0 # rq.filter: < 1.1 flake8-polyfill==1.0.1 flake8-putty==0.4.0 flake8-string-format==0.2.3 -flake8-tidy-imports==1.0.6 -flake8-tuple==0.2.12 +flake8-tidy-imports==1.1.0 +flake8-tuple==0.2.13 mccabe==0.6.1 +packaging==16.8 pep8-naming==0.4.1 pycodestyle==2.3.1 -pydocstyle==1.1.1 +pydocstyle==1.1.1 # rq.filter: < 2.0.0 pyflakes==1.5.0 +pyparsing==2.2.0 +six==1.10.0 diff --git a/misc/requirements/requirements-flake8.txt-raw b/misc/requirements/requirements-flake8.txt-raw index cf660a8ce..d9e40e51b 100644 --- a/misc/requirements/requirements-flake8.txt-raw +++ b/misc/requirements/requirements-flake8.txt-raw @@ -2,16 +2,16 @@ flake8<3.0.0 flake8-copyright flake8-debugger!=2.0.0 flake8-deprecated -flake8-docstrings +flake8-docstrings<1.1.0 flake8-future-import flake8-mock -flake8-pep3101 +flake8-pep3101<1.1 flake8-putty flake8-string-format flake8-tidy-imports flake8-tuple pep8-naming -pydocstyle +pydocstyle<2.0.0 pyflakes # Pinned to 2.0.0 otherwise @@ -21,6 +21,9 @@ mccabe==0.6.1 # Waiting until flake8-putty updated #@ filter: flake8 < 3.0.0 +#@ filter: pydocstyle < 2.0.0 +#@ filter: flake8-docstrings < 1.1.0 +#@ filter: flake8-pep3101 < 1.1 # https://github.com/JBKahn/flake8-debugger/issues/5 #@ filter: flake8-debugger != 2.0.0 diff --git a/misc/requirements/requirements-pip.txt b/misc/requirements/requirements-pip.txt index 54c6747af..55c1f873e 100644 --- a/misc/requirements/requirements-pip.txt +++ b/misc/requirements/requirements-pip.txt @@ -3,6 +3,6 @@ appdirs==1.4.3 packaging==16.8 pyparsing==2.2.0 -setuptools==34.3.3 +setuptools==36.2.0 six==1.10.0 wheel==0.29.0 diff --git a/misc/requirements/requirements-pyinstaller.txt b/misc/requirements/requirements-pyinstaller.txt index 9b16d9413..fc019e5a9 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/pyinstaller/pyinstaller.git@develop#egg=PyInstaller +-e git+https://github.com/xoviat/pyinstaller.git@qtweb#egg=PyInstaller diff --git a/misc/requirements/requirements-pyinstaller.txt-raw b/misc/requirements/requirements-pyinstaller.txt-raw index 5ccd44ec7..522ef7df2 100644 --- a/misc/requirements/requirements-pyinstaller.txt-raw +++ b/misc/requirements/requirements-pyinstaller.txt-raw @@ -1,4 +1,4 @@ --e git+https://github.com/pyinstaller/pyinstaller.git@develop#egg=PyInstaller +-e git+https://github.com/xoviat/pyinstaller.git@qtweb#egg=PyInstaller # remove @commit-id for scm installs -#@ replace: @.*# @develop# \ No newline at end of file +#@ replace: @.*# @qtweb# \ No newline at end of file diff --git a/misc/requirements/requirements-pylint-master.txt b/misc/requirements/requirements-pylint-master.txt index e438f86ed..37e705b7a 100644 --- a/misc/requirements/requirements-pylint-master.txt +++ b/misc/requirements/requirements-pylint-master.txt @@ -1,14 +1,18 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -e git+https://github.com/PyCQA/astroid.git#egg=astroid -editdistance==0.3.1 +certifi==2017.4.17 +chardet==3.0.4 github3.py==0.9.6 -isort==4.2.5 -lazy-object-proxy==1.2.2 +idna==2.5 +isort==4.2.15 +lazy-object-proxy==1.3.1 mccabe==0.6.1 -e git+https://github.com/PyCQA/pylint.git#egg=pylint ./scripts/dev/pylint_checkers -requests==2.13.0 +requests==2.18.1 +six==1.10.0 uritemplate==3.0.0 uritemplate.py==3.0.2 +urllib3==1.21.1 wrapt==1.10.10 diff --git a/misc/requirements/requirements-pylint.txt b/misc/requirements/requirements-pylint.txt index 12d94c8eb..a76d0dbf4 100644 --- a/misc/requirements/requirements-pylint.txt +++ b/misc/requirements/requirements-pylint.txt @@ -1,13 +1,18 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -astroid==1.4.9 +astroid==1.5.3 +certifi==2017.4.17 +chardet==3.0.4 github3.py==0.9.6 -isort==4.2.5 -lazy-object-proxy==1.2.2 +idna==2.5 +isort==4.2.15 +lazy-object-proxy==1.3.1 mccabe==0.6.1 -pylint==1.6.5 +pylint==1.7.2 ./scripts/dev/pylint_checkers -requests==2.13.0 +requests==2.18.1 +six==1.10.0 uritemplate==3.0.0 uritemplate.py==3.0.2 +urllib3==1.21.1 wrapt==1.10.10 diff --git a/misc/requirements/requirements-pyqt.txt b/misc/requirements/requirements-pyqt.txt index da611589a..fffa133ab 100644 --- a/misc/requirements/requirements-pyqt.txt +++ b/misc/requirements/requirements-pyqt.txt @@ -1,4 +1,4 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -PyQt5==5.8.2 -sip==4.19.2 +PyQt5==5.9 +sip==4.19.3 diff --git a/misc/requirements/requirements-tests-git.txt b/misc/requirements/requirements-tests-git.txt index 68f93bc7a..5a4450293 100644 --- a/misc/requirements/requirements-tests-git.txt +++ b/misc/requirements/requirements-tests-git.txt @@ -27,7 +27,6 @@ git+https://github.com/pytest-dev/pytest-qt.git git+https://github.com/pytest-dev/pytest-repeat.git git+https://github.com/pytest-dev/pytest-rerunfailures.git git+https://github.com/abusalimov/pytest-travis-fold.git -git+https://github.com/fschulze/pytest-warnings.git git+https://github.com/The-Compiler/pytest-xvfb.git hg+https://bitbucket.org/gutworth/six hg+https://bitbucket.org/jendrikseipp/vulture diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt index e1123a1d7..91cecb798 100644 --- a/misc/requirements/requirements-tests.txt +++ b/misc/requirements/requirements-tests.txt @@ -1,36 +1,39 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -beautifulsoup4==4.5.3 -cheroot==5.4.0 +beautifulsoup4==4.6.0 +cheroot==5.7.0 click==6.7 -coverage==4.3.4 -decorator==4.0.11 +# colorama==0.3.9 +coverage==4.4.1 +decorator==4.1.1 EasyProcess==0.2.3 -Flask==0.12 +fields==5.0.0 +Flask==0.12.2 glob2==0.5 httpbin==0.5.0 -hypothesis==3.7.0 +hunter==1.4.1 +hypothesis==3.13.0 itsdangerous==0.24 -# Jinja2==2.9.5 -Mako==1.0.6 +# Jinja2==2.9.6 +Mako==1.0.7 # MarkupSafe==1.0 -parse==1.8.0 +parse==1.8.2 parse-type==0.3.4 -py==1.4.33 -pytest==3.0.7 -pytest-bdd==2.18.1 +py==1.4.34 +pytest==3.1.3 +pytest-bdd==2.18.2 pytest-benchmark==3.0.0 pytest-catchlog==1.2.2 -pytest-cov==2.4.0 +pytest-cov==2.5.1 pytest-faulthandler==1.3.1 pytest-instafail==0.3.0 pytest-mock==1.6.0 -pytest-qt==2.1.0 +pytest-qt==2.1.2 pytest-repeat==0.4.1 -pytest-rerunfailures==2.1.0 +pytest-rerunfailures==2.2 pytest-travis-fold==1.2.0 -pytest-warnings==0.2.0 pytest-xvfb==1.0.0 PyVirtualDisplay==0.2.1 -vulture==0.13 -Werkzeug==0.12.1 +six==1.10.0 +vulture==0.16 +Werkzeug==0.12.2 diff --git a/misc/requirements/requirements-tests.txt-raw b/misc/requirements/requirements-tests.txt-raw index d0f3bec52..3ca6de6bd 100644 --- a/misc/requirements/requirements-tests.txt-raw +++ b/misc/requirements/requirements-tests.txt-raw @@ -2,6 +2,7 @@ beautifulsoup4 cheroot coverage Flask +hunter httpbin hypothesis pytest @@ -16,8 +17,7 @@ pytest-qt pytest-repeat pytest-rerunfailures pytest-travis-fold -pytest-warnings pytest-xvfb vulture -#@ ignore: Jinja2, MarkupSafe +#@ ignore: Jinja2, MarkupSafe, colorama diff --git a/misc/requirements/requirements-tox.txt b/misc/requirements/requirements-tox.txt index df38c7fca..2fedd0bc3 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.33 -tox==2.6.0 +py==1.4.34 +tox==2.7.0 virtualenv==15.1.0 diff --git a/misc/requirements/requirements-vulture.txt b/misc/requirements/requirements-vulture.txt index 26ee55a6e..f2ba7dcbf 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.13 +vulture==0.16 diff --git a/misc/userscripts/dmenu_qutebrowser b/misc/userscripts/dmenu_qutebrowser index dbc7c05bf..6917dae98 100755 --- a/misc/userscripts/dmenu_qutebrowser +++ b/misc/userscripts/dmenu_qutebrowser @@ -1,6 +1,7 @@ #!/usr/bin/env bash # Copyright 2015 Zach-Button +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/misc/userscripts/openfeeds b/misc/userscripts/openfeeds index 085bdbe67..8bc4c2d33 100755 --- a/misc/userscripts/openfeeds +++ b/misc/userscripts/openfeeds @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # Copyright 2015 jnphilipp +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/misc/userscripts/password_fill b/misc/userscripts/password_fill index 5d709512c..327a55690 100755 --- a/misc/userscripts/password_fill +++ b/misc/userscripts/password_fill @@ -9,7 +9,7 @@ directly ask me via IRC (nickname thorsten\`) in #qutebrowser on freenode. $blink!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!$reset WARNING: the passwords are stored in qutebrowser's - debug log reachable via the url qute:log + debug log reachable via the url qute://log $blink!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!$reset Usage: run as a userscript form qutebrowser, e.g.: diff --git a/misc/userscripts/qutebrowser_viewsource b/misc/userscripts/qutebrowser_viewsource index b528c41e8..a8ad71de3 100755 --- a/misc/userscripts/qutebrowser_viewsource +++ b/misc/userscripts/qutebrowser_viewsource @@ -1,6 +1,7 @@ #!/usr/bin/env bash # Copyright 2015 Zach-Button +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/misc/userscripts/readability b/misc/userscripts/readability index 2de4be5ab..639e3a111 100755 --- a/misc/userscripts/readability +++ b/misc/userscripts/readability @@ -2,20 +2,32 @@ # # Executes python-readability on current page and opens the summary as new tab. # +# Depends on the python-readability package, or its fork: +# +# - https://github.com/buriy/python-readability +# - https://github.com/bookieio/breadability +# # Usage: # :spawn --userscript readability # from __future__ import absolute_import import codecs, os -from readability.readability import Document tmpfile=os.path.expanduser('~/.local/share/qutebrowser/userscripts/readability.html') if not os.path.exists(os.path.dirname(tmpfile)): os.makedirs(os.path.dirname(tmpfile)) with codecs.open(os.environ['QUTE_HTML'], 'r', 'utf-8') as source: - doc = Document(source.read()) - content = doc.summary().replace('', '%s' % doc.title()) + data = source.read() + + try: + from breadability.readable import Article as reader + doc = reader(data) + content = doc.readable + except ImportError: + from readability import Document + doc = Document(data) + content = doc.summary().replace('', '%s' % doc.title()) with codecs.open(tmpfile, 'w', 'utf-8') as target: target.write('') diff --git a/pytest.ini b/pytest.ini index 905bca52c..225d64d9a 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,12 +1,13 @@ [pytest] addopts = --strict -rfEw --faulthandler-timeout=70 --instafail --pythonwarnings error +testpaths = tests markers = gui: Tests using the GUI (e.g. spawning widgets) posix: Tests which only can run on a POSIX OS. windows: Tests which only can run on Windows. linux: Tests which only can run on Linux. - osx: Tests which only can run on OS X. - not_osx: Tests which can not run on OS X. + mac: Tests which only can run on macOS. + not_mac: Tests which can not run on macOS. not_frozen: Tests which can't be run if sys.frozen is True. no_xvfb: Tests which can't be run with Xvfb. frozen: Tests which can only be run if sys.frozen is True. @@ -20,7 +21,7 @@ markers = 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 + qtwebengine_mac_xfail: Tests which fail on macOS 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 @@ -45,6 +46,7 @@ qt_log_ignore = ^QStandardPaths: XDG_RUNTIME_DIR not set, defaulting to .* ^QGeoclueMaster error creating GeoclueMasterClient\. ^Geoclue error: Process org\.freedesktop\.Geoclue\.Master exited with status 127 + ^QDBusConnection: name 'org.freedesktop.Geoclue.Master' had owner '' but we thought it was ':1.1' ^QObject::connect: Cannot connect \(null\)::stateChanged\(QNetworkSession::State\) to QNetworkReplyHttpImpl::_q_networkSessionStateChanged\(QNetworkSession::State\) ^QXcbClipboard: Cannot transfer data, no data available ^load glyph failed @@ -53,4 +55,5 @@ qt_log_ignore = ^QPainter::end: Painter ended with \d+ saved states ^QSslSocket: cannot resolve SSLv[23]_(client|server)_method ^QQuickWidget::invalidateRenderControl could not make context current + ^libpng warning: iCCP: known incorrect sRGB profile xfail_strict = true diff --git a/qutebrowser/__init__.py b/qutebrowser/__init__.py index 348cf407a..cca2bf1b8 100644 --- a/qutebrowser/__init__.py +++ b/qutebrowser/__init__.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -22,11 +22,11 @@ import os.path __author__ = "Florian Bruhin" -__copyright__ = "Copyright 2014-2016 Florian Bruhin (The Compiler)" +__copyright__ = "Copyright 2014-2017 Florian Bruhin (The Compiler)" __license__ = "GPL" __maintainer__ = __author__ __email__ = "mail@qutebrowser.org" -__version_info__ = (0, 10, 1) +__version_info__ = (0, 11, 0) __version__ = '.'.join(str(e) for e in __version_info__) __description__ = "A keyboard-driven, vim-like browser based on PyQt5." diff --git a/qutebrowser/__main__.py b/qutebrowser/__main__.py index 4f977d2d2..506039890 100644 --- a/qutebrowser/__main__.py +++ b/qutebrowser/__main__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 15a60ec4e..51cacfac7 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -42,9 +42,10 @@ except ImportError: import qutebrowser import qutebrowser.resources -from qutebrowser.completion.models import instances as completionmodels +from qutebrowser.completion.models import miscmodels from qutebrowser.commands import cmdutils, runners, cmdexc from qutebrowser.config import style, config, websettings, configexc +from qutebrowser.config.parsers import keyconf from qutebrowser.browser import (urlmarks, adblock, history, browsertab, downloads) from qutebrowser.browser.network import proxy @@ -53,10 +54,10 @@ from qutebrowser.browser.webkit.network import networkmanager from qutebrowser.keyinput import macros from qutebrowser.mainwindow import mainwindow, prompt from qutebrowser.misc import (readline, ipc, savemanager, sessions, - crashsignal, earlyinit) + crashsignal, earlyinit, objects, sql) from qutebrowser.misc import utilcmds # pylint: disable=unused-import from qutebrowser.utils import (log, version, message, utils, qtutils, urlutils, - objreg, usertypes, standarddir, error, debug) + objreg, usertypes, standarddir, error) # We import utilcmds to run the cmdutils.register decorators. @@ -136,7 +137,7 @@ def init(args, crash_handler): try: _init_modules(args, crash_handler) - except (OSError, UnicodeDecodeError) as e: + except (OSError, UnicodeDecodeError, browsertab.WebTabError) as e: error.handle_fatal_exc(e, args, "Error while initializing!", pre_text="Error while initializing") sys.exit(usertypes.Exit.err_init) @@ -157,7 +158,7 @@ def init(args, crash_handler): QDesktopServices.setUrlHandler('https', open_desktopservices_url) QDesktopServices.setUrlHandler('qute', open_desktopservices_url) - QTimer.singleShot(10, functools.partial(_init_late_modules, args)) + objreg.get('web-history').import_txt() log.init.debug("Init done!") crash_handler.raise_crashdlg() @@ -170,12 +171,15 @@ def _init_icon(): for size in [16, 24, 32, 48, 64, 96, 128, 256, 512]: filename = ':/icons/qutebrowser-{}x{}.png'.format(size, size) pixmap = QPixmap(filename) - qtutils.ensure_not_null(pixmap) - fallback_icon.addPixmap(pixmap) - qtutils.ensure_not_null(fallback_icon) + if pixmap.isNull(): + log.init.warning("Failed to load {}".format(filename)) + else: + fallback_icon.addPixmap(pixmap) icon = QIcon.fromTheme('qutebrowser', fallback_icon) - qtutils.ensure_not_null(icon) - qApp.setWindowIcon(icon) + if icon.isNull(): + log.init.warning("Failed to load icon") + else: + qApp.setWindowIcon(icon) def _process_args(args): @@ -192,14 +196,14 @@ def _process_args(args): session_manager = objreg.get('session-manager') if not session_manager.did_load: log.init.debug("Initializing main window...") - window = mainwindow.MainWindow() + window = mainwindow.MainWindow(private=None) if not args.nowindow: window.show() qApp.setActiveWindow(window) process_pos_args(args.command) _open_startpage() - _open_quickstart(args) + _open_special_pages(args) delta = datetime.datetime.now() - earlyinit.START_TIME log.init.debug("Init finished after {}s".format(delta.total_seconds())) @@ -316,23 +320,40 @@ def _open_startpage(win_id=None): tabbed_browser.tabopen(url) -def _open_quickstart(args): - """Open quickstart if it's the first start. +def _open_special_pages(args): + """Open special notification pages which are only shown once. + + Currently this is: + - Quickstart page if it's the first start. + - Legacy QtWebKit warning if needed. Args: args: The argparse namespace. """ if args.basedir is not None: - # With --basedir given, don't open quickstart. + # With --basedir given, don't open anything. return + state_config = objreg.get('state-config') - try: - quickstart_done = state_config['general']['quickstart-done'] == '1' - except KeyError: - quickstart_done = False + tabbed_browser = objreg.get('tabbed-browser', scope='window', + window='last-focused') + + # Legacy QtWebKit warning + + needs_warning = (objects.backend == usertypes.Backend.QtWebKit and + not qtutils.is_qtwebkit_ng()) + warning_shown = state_config['general'].get('backend-warning-shown') == '1' + + if not warning_shown and needs_warning: + tabbed_browser.tabopen(QUrl('qute://backend-warning'), + background=False) + state_config['general']['backend-warning-shown'] = '1' + + # Quickstart page + + quickstart_done = state_config['general'].get('quickstart-done') == '1' + if not quickstart_done: - tabbed_browser = objreg.get('tabbed-browser', scope='window', - window='last-focused') tabbed_browser.tabopen( QUrl('https://www.qutebrowser.org/quickstart.html')) state_config['general']['quickstart-done'] = '1' @@ -340,8 +361,9 @@ def _open_quickstart(args): def _save_version(): """Save the current version to the state config.""" - state_config = objreg.get('state-config') - state_config['general']['version'] = qutebrowser.__version__ + state_config = objreg.get('state-config', None) + if state_config is not None: + state_config['general']['version'] = qutebrowser.__version__ def on_focus_changed(_old, new): @@ -389,10 +411,8 @@ 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 proxy...") + proxy.init() log.init.debug("Initializing readline-bridge...") readline_bridge = readline.ReadlineBridge() @@ -402,6 +422,17 @@ def _init_modules(args, crash_handler): config.init(qApp) save_manager.init_autosave() + log.init.debug("Initializing keys...") + keyconf.init(qApp) + + log.init.debug("Initializing sql...") + try: + sql.init(os.path.join(standarddir.data(), 'history.sqlite')) + except sql.SqlException as e: + error.handle_fatal_exc(e, args, 'Error initializing SQL', + pre_text='Error initializing SQL') + sys.exit(usertypes.Exit.err_init) + log.init.debug("Initializing web history...") history.init(qApp) @@ -438,9 +469,6 @@ def _init_modules(args, crash_handler): diskcache = cache.DiskCache(standarddir.cache(), parent=qApp) objreg.register('cache', diskcache) - log.init.debug("Initializing completions...") - completionmodels.init() - log.init.debug("Misc initialization...") if config.get('ui', 'hide-wayland-decoration'): os.environ['QT_WAYLAND_DISABLE_WINDOWDECORATION'] = '1' @@ -451,23 +479,6 @@ def _init_modules(args, crash_handler): browsertab.init() -def _init_late_modules(args): - """Initialize modules which can be inited after the window is shown.""" - log.init.debug("Reading web history...") - reader = objreg.get('web-history').async_read() - with debug.log_time(log.init, 'Reading history'): - while True: - QApplication.processEvents() - try: - next(reader) - except StopIteration: - break - except (OSError, UnicodeDecodeError) as e: - error.handle_fatal_exc(e, args, "Error while initializing!", - pre_text="Error while initializing") - sys.exit(usertypes.Exit.err_init) - - class Quitter: """Utility class to quit/restart the QApplication. @@ -615,7 +626,7 @@ class Quitter: # Save the session if one is given. if session is not None: session_manager = objreg.get('session-manager') - session_manager.save(session) + session_manager.save(session, with_private=True) # Open a new process and immediately shutdown the existing one try: args, cwd = self._get_restart_args(pages, session) @@ -647,14 +658,14 @@ class Quitter: self._shutting_down = True log.destroy.debug("Shutting down with status {}, session {}...".format( status, session)) - - session_manager = objreg.get('session-manager') - if session is not None: - session_manager.save(session, last_window=last_window, - load_next_time=True) - elif config.get('general', 'save-session'): - session_manager.save(sessions.default, last_window=last_window, - load_next_time=True) + session_manager = objreg.get('session-manager', None) + if session_manager is not None: + if session is not None: + session_manager.save(session, last_window=last_window, + load_next_time=True) + elif config.get('general', 'save-session'): + session_manager.save(sessions.default, last_window=last_window, + load_next_time=True) if prompt.prompt_queue.shutdown(): # If shutdown was called while we were asking a question, we're in @@ -671,7 +682,7 @@ class Quitter: # event loop, so we can shut down immediately. self._shutdown(status, restart=restart) - def _shutdown(self, status, restart): + def _shutdown(self, status, restart): # noqa """Second stage of shutdown.""" log.destroy.debug("Stage 2 of shutting down...") if qApp is None: @@ -680,7 +691,9 @@ class Quitter: # Remove eventfilter try: log.destroy.debug("Removing eventfilter...") - qApp.removeEventFilter(objreg.get('event-filter')) + event_filter = objreg.get('event-filter', None) + if event_filter is not None: + qApp.removeEventFilter(event_filter) except AttributeError: pass # Close all windows @@ -722,13 +735,15 @@ 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() + session_manager = objreg.get('session-manager', None) + if session_manager is not None: + 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)) @cmdutils.register(instance='quitter', name='wq') - @cmdutils.argument('name', completion=usertypes.Completion.sessions) + @cmdutils.argument('name', completion=miscmodels.session) def save_and_quit(self, name=sessions.default): """Save open pages and quit. @@ -784,7 +799,7 @@ class Application(QApplication): def exit(self, status): """Extend QApplication::exit to log the event.""" log.destroy.debug("Now calling QApplication::exit.") - if self._args.debug_exit: + if 'debug-exit' in self._args.debug_flags: if hunter is None: print("Not logging late shutdown because hunter could not be " "imported!", file=sys.stderr) diff --git a/qutebrowser/browser/__init__.py b/qutebrowser/browser/__init__.py index dbc790589..c5d5e6c92 100644 --- a/qutebrowser/browser/__init__.py +++ b/qutebrowser/browser/__init__.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/adblock.py b/qutebrowser/browser/adblock.py index a7d0d43bb..276dec08b 100644 --- a/qutebrowser/browser/adblock.py +++ b/qutebrowser/browser/adblock.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index ac1f73a63..b94172118 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -21,7 +21,7 @@ import itertools -from PyQt5.QtCore import pyqtSignal, pyqtSlot, QUrl, QObject, QSizeF +from PyQt5.QtCore import pyqtSignal, pyqtSlot, QUrl, QObject, QSizeF, Qt from PyQt5.QtGui import QIcon from PyQt5.QtWidgets import QWidget, QApplication @@ -35,11 +35,12 @@ from qutebrowser.browser import mouse, hints tab_id_gen = itertools.count(0) -def create(win_id, parent=None): +def create(win_id, private, parent=None): """Get a QtWebKit/QtWebEngine tab object. Args: win_id: The window ID where the tab will be shown. + private: Whether the tab is a private/off the record tab. parent: The Qt parent to set. """ # Importing modules here so we don't depend on QtWebEngine without the @@ -51,7 +52,8 @@ def create(win_id, parent=None): else: from qutebrowser.browser.webkit import webkittab tab_class = webkittab.WebKitTab - return tab_class(win_id=win_id, mode_manager=mode_manager, parent=parent) + return tab_class(win_id=win_id, mode_manager=mode_manager, private=private, + parent=parent) def init(): @@ -94,6 +96,8 @@ class TabData: viewing_source: Set if we're currently showing a source view. override_target: Override for open_target for fake clicks (like hints). Only used for QtWebKit. + pinned: Flag to pin the tab. + fullscreen: Whether the tab has a video shown fullscreen currently. """ def __init__(self): @@ -101,11 +105,21 @@ class TabData: self.viewing_source = False self.inspector = None self.override_target = None + self.pinned = False + self.fullscreen = False class AbstractAction: - """Attribute of AbstractTab for Qt WebActions.""" + """Attribute of AbstractTab for Qt WebActions. + + Class attributes (overridden by subclasses): + action_class: The class actions are defined on (QWeb{Engine,}Page) + action_base: The type of the actions (QWeb{Engine,}Page.WebAction) + """ + + action_class = None + action_base = None def __init__(self): self._widget = None @@ -118,6 +132,13 @@ class AbstractAction: """Save the current page.""" raise NotImplementedError + def run_string(self, name): + """Run a webaction based on its name.""" + member = getattr(self.action_class, name, None) + if not isinstance(member, self.action_base): + raise WebTabError("{} is not a valid web action!".format(name)) + self._widget.triggerPageAction(member) + class AbstractPrinting: @@ -155,6 +176,8 @@ class AbstractSearch(QObject): Attributes: text: The last thing this view was searched for. + search_displayed: Whether we're currently displaying search results in + this view. _flags: The flags of the last search (needs to be set by subclasses). _widget: The underlying WebView widget. """ @@ -163,6 +186,7 @@ class AbstractSearch(QObject): super().__init__(parent) self._widget = None self.text = None + self.search_displayed = False def search(self, text, *, ignore_case=False, reverse=False, result_cb=None): @@ -441,11 +465,21 @@ class AbstractHistory: def current_idx(self): raise NotImplementedError - def back(self): - raise NotImplementedError + def back(self, count=1): + idx = self.current_idx() - count + if idx >= 0: + self._go_to_item(self._item_at(idx)) + else: + self._go_to_item(self._item_at(0)) + raise WebTabError("At beginning of history.") - def forward(self): - raise NotImplementedError + def forward(self, count=1): + idx = self.current_idx() + count + if idx < len(self): + self._go_to_item(self._item_at(idx)) + else: + self._go_to_item(self._item_at(len(self) - 1)) + raise WebTabError("At end of history.") def can_go_back(self): raise NotImplementedError @@ -453,6 +487,12 @@ class AbstractHistory: def can_go_forward(self): raise NotImplementedError + def _item_at(self, i): + raise NotImplementedError + + def _go_to_item(self, item): + raise NotImplementedError + def serialize(self): """Serialize into an opaque format understood by self.deserialize.""" raise NotImplementedError @@ -524,6 +564,7 @@ class AbstractTab(QWidget): Attributes: history: The AbstractHistory for the current tab. registry: The ObjectRegistry associated with this tab. + private: Whether private browsing is turned on for this tab. _load_status: loading status of this page Accessible via load_status() method. @@ -563,7 +604,8 @@ class AbstractTab(QWidget): fullscreen_requested = pyqtSignal(bool) renderer_process_terminated = pyqtSignal(TerminationStatus, int) - def __init__(self, win_id, mode_manager, parent=None): + def __init__(self, *, win_id, mode_manager, private, parent=None): + self.private = private self.win_id = win_id self.tab_id = next(tab_id_gen) super().__init__(parent) @@ -740,6 +782,10 @@ class AbstractTab(QWidget): def clear_ssl_errors(self): raise NotImplementedError + def key_press(self, key, modifier=Qt.NoModifier): + """Send a fake key event to this tab.""" + raise NotImplementedError + def dump_async(self, callback, *, plain=False): """Dump the current page to a file ascync. @@ -771,7 +817,7 @@ class AbstractTab(QWidget): def icon(self): raise NotImplementedError - def set_html(self, html, base_url): + def set_html(self, html, base_url=QUrl()): raise NotImplementedError def networkaccessmanager(self): diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 12f58d610..f3f71d910 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -20,22 +20,15 @@ """Command dispatcher for TabbedBrowser.""" import os +import sys import os.path import shlex import functools -from PyQt5.QtWidgets import QApplication, QTabBar +from PyQt5.QtWidgets import QApplication, QTabBar, QDialog from PyQt5.QtCore import Qt, QUrl, QEvent, QUrlQuery from PyQt5.QtGui import QKeyEvent from PyQt5.QtPrintSupport import QPrintDialog, QPrintPreviewDialog -try: - from PyQt5.QtWebKitWidgets import QWebPage -except ImportError: - QWebPage = None -try: - from PyQt5.QtWebEngineWidgets import QWebEnginePage -except ImportError: - QWebEnginePage = None import pygments import pygments.lexers import pygments.formatters @@ -46,10 +39,10 @@ from qutebrowser.browser import (urlmarks, browsertab, inspector, navigate, webelem, downloads) from qutebrowser.keyinput import modeman from qutebrowser.utils import (message, usertypes, log, qtutils, urlutils, - objreg, utils, typing) + objreg, utils, typing, debug) from qutebrowser.utils.usertypes import KeyMode from qutebrowser.misc import editor, guiprocess -from qutebrowser.completion.models import instances, sortfilter +from qutebrowser.completion.models import urlmodel, miscmodels class CommandDispatcher: @@ -75,10 +68,10 @@ class CommandDispatcher: def __repr__(self): return utils.get_repr(self) - def _new_tabbed_browser(self): + def _new_tabbed_browser(self, private): """Get a tabbed-browser from a new window.""" from qutebrowser.mainwindow import mainwindow - new_window = mainwindow.MainWindow() + new_window = mainwindow.MainWindow(private=private) new_window.show() return new_window.tabbed_browser @@ -118,7 +111,7 @@ class CommandDispatcher: return widget def _open(self, url, tab=False, background=False, window=False, - explicit=True): + explicit=True, private=None): """Helper function to open a page. Args: @@ -126,12 +119,17 @@ class CommandDispatcher: tab: Whether to open in a new tab. background: Whether to open in the background. window: Whether to open in a new window + private: If opening a new window, open it in private browsing mode. + If not given, inherit the current window's mode. """ urlutils.raise_cmdexc_if_invalid(url) tabbed_browser = self._tabbed_browser - cmdutils.check_exclusive((tab, background, window), 'tbw') - if window: - tabbed_browser = self._new_tabbed_browser() + cmdutils.check_exclusive((tab, background, window, private), 'tbwp') + if window and private is None: + private = self._tabbed_browser.private + + if window or private: + tabbed_browser = self._new_tabbed_browser(private) tabbed_browser.tabopen(url) elif tab: tabbed_browser.tabopen(url, background=False, explicit=explicit) @@ -160,12 +158,14 @@ class CommandDispatcher: else: return None - def _tab_focus_last(self): + def _tab_focus_last(self, *, show_error=True): """Select the tab which was last focused.""" try: tab = objreg.get('last-focused-tab', scope='window', window=self._win_id) except KeyError: + if not show_error: + return raise cmdexc.CommandError("No last focused tab!") idx = self._tabbed_browser.indexOf(tab) if idx == -1: @@ -205,24 +205,21 @@ class CommandDispatcher: "{!r}!".format(conf_selection)) return None - @cmdutils.register(instance='command-dispatcher', scope='window') - @cmdutils.argument('count', count=True) - def tab_close(self, prev=False, next_=False, opposite=False, count=None): - """Close the current/[count]th tab. + def _tab_close(self, tab, prev=False, next_=False, opposite=False): + """Helper function for tab_close be able to handle message.async. Args: + tab: Tab object to select be closed. prev: Force selecting the tab before the current tab. next_: Force selecting the tab after the current tab. opposite: Force selecting the tab in the opposite direction of what's configured in 'tabs->select-on-remove'. count: The tab index to close, or None """ - tab = self._cntwidget(count) - if tab is None: - return tabbar = self._tabbed_browser.tabBar() selection_override = self._get_selection_override(prev, next_, opposite) + if selection_override is None: self._tabbed_browser.close_tab(tab) else: @@ -231,12 +228,55 @@ class CommandDispatcher: self._tabbed_browser.close_tab(tab) tabbar.setSelectionBehaviorOnRemove(old_selection_behavior) + @cmdutils.register(instance='command-dispatcher', scope='window') + @cmdutils.argument('count', count=True) + def tab_close(self, prev=False, next_=False, opposite=False, + force=False, count=None): + """Close the current/[count]th tab. + + Args: + prev: Force selecting the tab before the current tab. + next_: Force selecting the tab after the current tab. + opposite: Force selecting the tab in the opposite direction of + what's configured in 'tabs->select-on-remove'. + force: Avoid confirmation for pinned tabs. + count: The tab index to close, or None + """ + tab = self._cntwidget(count) + if tab is None: + return + close = functools.partial(self._tab_close, tab, prev, + next_, opposite) + + self._tabbed_browser.tab_close_prompt_if_pinned(tab, force, close) + + @cmdutils.register(instance='command-dispatcher', scope='window', + name='tab-pin') + @cmdutils.argument('count', count=True) + def tab_pin(self, count=None): + """Pin/Unpin the current/[count]th tab. + + Pinning a tab shrinks it to tabs->pinned-width size. + Attempting to close a pinned tab will cause a confirmation, + unless --force is passed. + + Args: + count: The tab index to pin or unpin, or None + """ + tab = self._cntwidget(count) + if tab is None: + return + + to_pin = not tab.data.pinned + self._tabbed_browser.set_tab_pinned(tab, to_pin) + @cmdutils.register(instance='command-dispatcher', name='open', maxsplit=0, scope='window') - @cmdutils.argument('url', completion=usertypes.Completion.url) + @cmdutils.argument('url', completion=urlmodel.url) @cmdutils.argument('count', count=True) def openurl(self, url=None, implicit=False, - bg=False, tab=False, window=False, count=None, secure=False): + bg=False, tab=False, window=False, count=None, secure=False, + private=False): """Open a URL in the current/[count]th tab. If the URL contains newlines, each line gets opened in its own tab. @@ -250,6 +290,7 @@ class CommandDispatcher: clicking on a link). count: The tab index to open the URL in, or None. secure: Force HTTPS. + private: Open a new window in private browsing mode. """ if url is None: urls = [config.get('general', 'default-page')] @@ -262,8 +303,10 @@ class CommandDispatcher: if not window and i > 0: tab = False bg = True - if tab or bg or window: - self._open(cur_url, tab, bg, window, not implicit) + + if tab or bg or window or private: + self._open(cur_url, tab, bg, window, explicit=not implicit, + private=private) else: curtab = self._cntwidget(count) if curtab is None: @@ -274,6 +317,8 @@ class CommandDispatcher: else: # Explicit count with a tab that doesn't exist. return + elif curtab.data.pinned: + message.info("Tab is pinned!") else: curtab.openurl(cur_url) @@ -379,9 +424,18 @@ class CommandDispatcher: message.error("Printing failed!") diag.deleteLater() + def do_print(): + """Called when the dialog was closed.""" + tab.printing.to_printer(diag.printer(), print_callback) + diag = QPrintDialog(tab) - diag.open(lambda: tab.printing.to_printer(diag.printer(), - print_callback)) + if sys.platform == 'darwin': + # For some reason we get a segfault when using open() on macOS + ret = diag.exec_() + if ret == QDialog.Accepted: + do_print() + else: + diag.open(do_print) @cmdutils.register(instance='command-dispatcher', name='print', scope='window') @@ -438,10 +492,11 @@ class CommandDispatcher: # The new tab could be in a new tabbed_browser (e.g. because of # tabs-are-windows being set) if window: - new_tabbed_browser = self._new_tabbed_browser() + new_tabbed_browser = self._new_tabbed_browser( + private=self._tabbed_browser.private) else: new_tabbed_browser = self._tabbed_browser - newtab = new_tabbed_browser.tabopen(background=bg, explicit=True) + newtab = new_tabbed_browser.tabopen(background=bg) new_tabbed_browser = objreg.get('tabbed-browser', scope='window', window=newtab.win_id) idx = new_tabbed_browser.indexOf(newtab) @@ -455,6 +510,7 @@ class CommandDispatcher: newtab.data.keep_icon = True newtab.history.deserialize(history) newtab.zoom.set_factor(curtab.zoom.factor()) + new_tabbed_browser.set_tab_pinned(newtab, curtab.data.pinned) return newtab @cmdutils.register(instance='command-dispatcher', scope='window') @@ -481,15 +537,13 @@ class CommandDispatcher: else: widget = self._current_widget() - for _ in range(count): + try: if forward: - if not widget.history.can_go_forward(): - raise cmdexc.CommandError("At end of history.") - widget.history.forward() + widget.history.forward(count) else: - if not widget.history.can_go_back(): - raise cmdexc.CommandError("At beginning of history.") - widget.history.back() + widget.history.back(count) + except browsertab.WebTabError as e: + raise cmdexc.CommandError(e) @cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.argument('count', count=True) @@ -568,7 +622,7 @@ class CommandDispatcher: tab=tab, background=bg, window=window) elif where in ['up', 'increment', 'decrement']: new_url = handlers[where](url, count) - self._open(new_url, tab, bg, window) + self._open(new_url, tab, bg, window, explicit=False) else: # pragma: no cover raise ValueError("Got called with invalid value {} for " "`where'.".format(where)) @@ -634,7 +688,7 @@ class CommandDispatcher: scope='window') @cmdutils.argument('count', count=True) @cmdutils.argument('horizontal', flag='x') - def scroll_perc(self, perc: float=None, horizontal=False, count=None): + def scroll_perc(self, perc: float = None, horizontal=False, count=None): """Scroll to a specific percentage of the page. The percentage can be given either as argument or as count. @@ -670,7 +724,7 @@ class CommandDispatcher: @cmdutils.argument('bottom_navigate', metavar='ACTION', choices=('next', 'increment')) def scroll_page(self, x: float, y: float, *, - top_navigate: str=None, bottom_navigate: str=None, + top_navigate: str = None, bottom_navigate: str = None, count=1): """Scroll the frame page-wise. @@ -807,7 +861,7 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.argument('count', count=True) - def zoom(self, zoom: int=None, count=None): + def zoom(self, zoom=None, count=None): """Set the zoom level for the current tab. The zoom can be given as argument or as [count]. If neither is @@ -818,6 +872,13 @@ class CommandDispatcher: zoom: The zoom percentage to set. count: The zoom percentage to set. """ + if zoom is not None: + try: + zoom = int(zoom.rstrip('%')) + except ValueError: + raise cmdexc.CommandError("zoom: Invalid int value {}" + .format(zoom)) + level = count if count is not None else zoom if level is None: level = config.get('ui', 'default-zoom') @@ -830,27 +891,42 @@ class CommandDispatcher: message.info("Zoom level: {}%".format(level), replace=True) @cmdutils.register(instance='command-dispatcher', scope='window') - def tab_only(self, prev=False, next_=False): + def tab_only(self, prev=False, next_=False, force=False): """Close all tabs except for the current one. Args: prev: Keep tabs before the current. next_: Keep tabs after the current. + force: Avoid confirmation for pinned tabs. """ cmdutils.check_exclusive((prev, next_), 'pn') cur_idx = self._tabbed_browser.currentIndex() assert cur_idx != -1 + def _to_close(i): + """Helper method to check if a tab should be closed or not.""" + return not (i == cur_idx or + (prev and i < cur_idx) or + (next_ and i > cur_idx)) + + # Check to see if we are closing any pinned tabs + if not force: + for i, tab in enumerate(self._tabbed_browser.widgets()): + if _to_close(i) and tab.data.pinned: + self._tabbed_browser.tab_close_prompt_if_pinned( + tab, + force, + lambda: self.tab_only( + prev=prev, next_=next_, force=True)) + return + for i, tab in enumerate(self._tabbed_browser.widgets()): - if (i == cur_idx or (prev and i < cur_idx) or - (next_ and i > cur_idx)): - continue - else: + if _to_close(i): self._tabbed_browser.close_tab(tab) @cmdutils.register(instance='command-dispatcher', scope='window') def undo(self): - """Re-open a closed tab (optionally skipping [count] closed tabs).""" + """Re-open a closed tab.""" try: self._tabbed_browser.undo() except IndexError: @@ -934,7 +1010,7 @@ class CommandDispatcher: self._open(url, tab, bg, window) @cmdutils.register(instance='command-dispatcher', scope='window') - @cmdutils.argument('index', completion=usertypes.Completion.tab) + @cmdutils.argument('index', completion=miscmodels.buffer) def buffer(self, index): """Select tab by index or url/title best match. @@ -950,11 +1026,10 @@ class CommandDispatcher: for part in index_parts: int(part) except ValueError: - model = instances.get(usertypes.Completion.tab) - sf = sortfilter.CompletionFilterModel(source=model) - sf.set_pattern(index) - if sf.count() > 0: - index = sf.data(sf.first_item()) + model = miscmodels.buffer() + model.set_pattern(index) + if model.count() > 0: + index = model.data(model.first_item()) index_parts = index.split('/', 1) else: raise cmdexc.CommandError( @@ -1003,12 +1078,15 @@ class CommandDispatcher: last tab. count: The tab index to focus, starting with 1. """ + index = count if count is not None else index + if index == 'last': self._tab_focus_last() return - index = count if count is not None else index - - if index is None: + elif index == self._current_index() + 1: + self._tab_focus_last(show_error=False) + return + elif index is None: self.tab_next() return @@ -1082,6 +1160,7 @@ class CommandDispatcher: detach: Whether the command should be detached from qutebrowser. cmdline: The commandline to execute. """ + cmdutils.check_exclusive((userscript, detach), 'ud') try: cmd, *args = shlex.split(cmdline) except ValueError as e: @@ -1156,8 +1235,7 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', scope='window', maxsplit=0) - @cmdutils.argument('name', - completion=usertypes.Completion.quickmark_by_name) + @cmdutils.argument('name', completion=miscmodels.quickmark) def quickmark_load(self, name, tab=False, bg=False, window=False): """Load a quickmark. @@ -1175,8 +1253,7 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', scope='window', maxsplit=0) - @cmdutils.argument('name', - completion=usertypes.Completion.quickmark_by_name) + @cmdutils.argument('name', completion=miscmodels.quickmark) def quickmark_del(self, name=None): """Delete a quickmark. @@ -1233,12 +1310,12 @@ class CommandDispatcher: except urlmarks.Error as e: raise cmdexc.CommandError(str(e)) else: - msg = "Bookmarked {}!" if was_added else "Removed bookmark {}!" + msg = "Bookmarked {}" if was_added else "Removed bookmark {}" message.info(msg.format(url.toDisplayString())) @cmdutils.register(instance='command-dispatcher', scope='window', maxsplit=0) - @cmdutils.argument('url', completion=usertypes.Completion.bookmark_by_url) + @cmdutils.argument('url', completion=miscmodels.bookmark) def bookmark_load(self, url, tab=False, bg=False, window=False, delete=False): """Load a bookmark. @@ -1260,7 +1337,7 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', scope='window', maxsplit=0) - @cmdutils.argument('url', completion=usertypes.Completion.bookmark_by_url) + @cmdutils.argument('url', completion=miscmodels.bookmark) def bookmark_del(self, url=None): """Delete a bookmark. @@ -1365,8 +1442,18 @@ class CommandDispatcher: download_manager.get_mhtml(tab, target) else: qnam = tab.networkaccessmanager() - download_manager.get(self._current_url(), user_agent=user_agent, - qnam=qnam, target=target) + + suggested_fn = downloads.suggested_fn_from_title( + self._current_url().path(), tab.title() + ) + + download_manager.get( + self._current_url(), + user_agent=user_agent, + qnam=qnam, + target=target, + suggested_fn=suggested_fn + ) @cmdutils.register(instance='command-dispatcher', scope='window') def view_source(self): @@ -1377,19 +1464,22 @@ class CommandDispatcher: if tab.data.viewing_source: raise cmdexc.CommandError("Already viewing source!") + try: + current_url = self._current_url() + except cmdexc.CommandError as e: + message.error(str(e)) + return + def show_source_cb(source): """Show source as soon as it's ready.""" lexer = pygments.lexers.HtmlLexer() - formatter = pygments.formatters.HtmlFormatter(full=True, - linenos='table') + formatter = pygments.formatters.HtmlFormatter( + full=True, linenos='table', + title='Source for {}'.format(current_url.toDisplayString())) highlighted = pygments.highlight(source, lexer, formatter) - try: - current_url = self._current_url() - except cmdexc.CommandError as e: - message.error(str(e)) - return - new_tab = self._tabbed_browser.tabopen(explicit=True) - new_tab.set_html(highlighted, current_url) + + new_tab = self._tabbed_browser.tabopen() + new_tab.set_html(highlighted) new_tab.data.viewing_source = True tab.dump_async(show_source_cb) @@ -1431,7 +1521,7 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', name='help', scope='window') - @cmdutils.argument('topic', completion=usertypes.Completion.helptopic) + @cmdutils.argument('topic', completion=miscmodels.helptopic) def show_help(self, tab=False, bg=False, window=False, topic=None): r"""Show help about a command or setting. @@ -1472,7 +1562,7 @@ class CommandDispatcher: self._open(url, tab, bg, window) @cmdutils.register(instance='command-dispatcher', scope='window') - def messages(self, level='error', plain=False, tab=False, bg=False, + def messages(self, level='info', plain=False, tab=False, bg=False, window=False): """Show a log of past messages. @@ -1505,6 +1595,7 @@ class CommandDispatcher: if text is None: message.error("Could not get text from the focused element.") return + assert isinstance(text, str), text ed = editor.ExternalEditor(self._tabbed_browser) ed.editing_finished.connect(functools.partial( @@ -1651,7 +1742,8 @@ class CommandDispatcher: """ self.set_mark("'") tab = self._current_widget() - tab.search.clear() + if tab.search.search_displayed: + tab.search.clear() if not text: return @@ -1904,33 +1996,20 @@ class CommandDispatcher: def debug_webaction(self, action, count=1): """Execute a webaction. - See http://doc.qt.io/qt-5/qwebpage.html#WebAction-enum for the - available actions. + Available actions: + http://doc.qt.io/archives/qt-5.5/qwebpage.html#WebAction-enum (WebKit) + http://doc.qt.io/qt-5/qwebenginepage.html#WebAction-enum (WebEngine) Args: action: The action to execute, e.g. MoveToNextChar. count: How many times to repeat the action. """ tab = self._current_widget() - - if tab.backend == usertypes.Backend.QtWebKit: - assert QWebPage is not None - member = getattr(QWebPage, action, None) - base = QWebPage.WebAction - elif tab.backend == usertypes.Backend.QtWebEngine: - assert QWebEnginePage is not None - member = getattr(QWebEnginePage, action, None) - base = QWebEnginePage.WebAction - - if not isinstance(member, base): - raise cmdexc.CommandError("{} is not a valid web action!".format( - action)) - for _ in range(count): - # This whole command is backend-specific anyways, so it makes no - # sense to introduce some API for this. - # pylint: disable=protected-access - tab._widget.triggerPageAction(member) + try: + tab.action.run_string(action) + except browsertab.WebTabError as e: + raise cmdexc.CommandError(str(e)) @cmdutils.register(instance='command-dispatcher', scope='window', maxsplit=0, no_cmd_split=True) @@ -2095,6 +2174,10 @@ class CommandDispatcher: window = self._tabbed_browser.window() if window.isFullScreen(): - window.showNormal() + window.setWindowState( + window.state_before_fullscreen & ~Qt.WindowFullScreen) else: + window.state_before_fullscreen = window.windowState() window.showFullScreen() + log.misc.debug('state before fullscreen: {}'.format( + debug.qflags_key(Qt, window.state_before_fullscreen))) diff --git a/qutebrowser/browser/downloads.py b/qutebrowser/browser/downloads.py index f9b246d3a..438dfc528 100644 --- a/qutebrowser/browser/downloads.py +++ b/qutebrowser/browser/downloads.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -182,6 +182,28 @@ def transform_path(path): return path +def suggested_fn_from_title(url_path, title=None): + """Suggest a filename depending on the URL extension and page title. + + Args: + url_path: a string with the URL path + title: the page title string + + Return: + The download filename based on the title, or None if the extension is + not found in the whitelist (or if there is no page title). + """ + ext_whitelist = [".html", ".htm", ".php", ""] + _, ext = os.path.splitext(url_path) + if ext.lower() in ext_whitelist and title: + suggested_fn = utils.sanitize_filename(title) + if not suggested_fn.lower().endswith((".html", ".htm")): + suggested_fn += ".html" + else: + suggested_fn = None + return suggested_fn + + class NoFilenameError(Exception): """Raised when we can't find out a filename in DownloadTarget.""" @@ -952,7 +974,7 @@ class DownloadModel(QAbstractListModel): @cmdutils.register(instance='download-model', scope='window', maxsplit=0) @cmdutils.argument('count', count=True) - def download_open(self, cmdline: str=None, count=0): + def download_open(self, cmdline: str = None, count=0): """Open the last/[count]th download. If no specific command is given, this will use the system's default diff --git a/qutebrowser/browser/downloadview.py b/qutebrowser/browser/downloadview.py index 633fd8700..199f4d1d8 100644 --- a/qutebrowser/browser/downloadview.py +++ b/qutebrowser/browser/downloadview.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -23,7 +23,7 @@ import functools import sip from PyQt5.QtCore import pyqtSlot, QSize, Qt, QTimer -from PyQt5.QtWidgets import QListView, QSizePolicy, QMenu +from PyQt5.QtWidgets import QListView, QSizePolicy, QMenu, QStyleFactory from qutebrowser.browser import downloads from qutebrowser.config import style @@ -75,6 +75,7 @@ class DownloadView(QListView): def __init__(self, win_id, parent=None): super().__init__(parent) + self.setStyle(QStyleFactory.create('Fusion')) style.set_register_stylesheet(self) self.setResizeMode(QListView.Adjust) self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py index 144b13f76..3cc70f434 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -579,12 +579,10 @@ class HintManager(QObject): if elems is None: message.error("There was an error while getting hint elements") return - - filterfunc = webelem.FILTERS.get(self._context.group, lambda e: True) - elems = [e for e in elems if filterfunc(e)] if not elems: message.error("No elements found.") return + strings = self._hint_strings(elems) log.hints.debug("hints: {}".format(', '.join(strings))) diff --git a/qutebrowser/browser/history.py b/qutebrowser/browser/history.py index 2615fd393..b9e791207 100644 --- a/qutebrowser/browser/history.py +++ b/qutebrowser/browser/history.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -19,215 +19,82 @@ """Simple history which gets written to disk.""" +import os import time -import collections -from PyQt5.QtCore import pyqtSignal, pyqtSlot, QUrl, QObject +from PyQt5.QtCore import pyqtSlot, QUrl, QTimer -from qutebrowser.commands import cmdutils -from qutebrowser.utils import (utils, objreg, standarddir, log, qtutils, - usertypes, message) -from qutebrowser.config import config -from qutebrowser.misc import lineparser, objects +from qutebrowser.commands import cmdutils, cmdexc +from qutebrowser.utils import (utils, objreg, log, usertypes, message, + debug, standarddir) +from qutebrowser.misc import objects, sql -class Entry: +class CompletionHistory(sql.SqlTable): - """A single entry in the web history. + """History which only has the newest entry for each URL.""" - Attributes: - atime: The time the page was accessed. - url: The URL which was accessed as QUrl. - redirect: If True, don't save this entry to disk - """ - - def __init__(self, atime, url, title, redirect=False): - self.atime = float(atime) - self.url = url - self.title = title - self.redirect = redirect - qtutils.ensure_valid(url) - - def __repr__(self): - return utils.get_repr(self, constructor=True, atime=self.atime, - url=self.url_str(), title=self.title, - redirect=self.redirect) - - def __str__(self): - atime = str(int(self.atime)) - if self.redirect: - atime += '-r' # redirect flag - elems = [atime, self.url_str()] - if self.title: - elems.append(self.title) - return ' '.join(elems) - - def __eq__(self, other): - return (self.atime == other.atime and - self.title == other.title and - self.url == other.url and - self.redirect == other.redirect) - - def url_str(self): - """Get the URL as a lossless string.""" - return self.url.toString(QUrl.FullyEncoded | QUrl.RemovePassword) - - @classmethod - def from_str(cls, line): - """Parse a history line like '12345 http://example.com title'.""" - data = line.split(maxsplit=2) - if len(data) == 2: - atime, url = data - title = "" - elif len(data) == 3: - atime, url, title = data - else: - raise ValueError("2 or 3 fields expected") - - url = QUrl(url) - if not url.isValid(): - raise ValueError("Invalid URL: {}".format(url.errorString())) - - # https://github.com/qutebrowser/qutebrowser/issues/670 - atime = atime.lstrip('\0') - - if '-' in atime: - atime, flags = atime.split('-') - else: - flags = '' - - if not set(flags).issubset('r'): - raise ValueError("Invalid flags {!r}".format(flags)) - - redirect = 'r' in flags - - return cls(atime, url, title, redirect=redirect) + def __init__(self, parent=None): + super().__init__("CompletionHistory", ['url', 'title', 'last_atime'], + constraints={'url': 'PRIMARY KEY'}, parent=parent) + self.create_index('CompletionHistoryAtimeIndex', 'last_atime') -class WebHistory(QObject): +class WebHistory(sql.SqlTable): - """The global history of visited pages. + """The global history of visited pages.""" - This is a little more complex as you'd expect so the history can be read - from disk async while new history is already arriving. + def __init__(self, parent=None): + super().__init__("History", ['url', 'title', 'atime', 'redirect'], + parent=parent) + self.completion = CompletionHistory(parent=self) + self.create_index('HistoryIndex', 'url') + self.create_index('HistoryAtimeIndex', 'atime') + self._contains_query = self.contains_query('url') + self._between_query = sql.Query('SELECT * FROM History ' + 'where not redirect ' + 'and not url like "qute://%" ' + 'and atime > :earliest ' + 'and atime <= :latest ' + 'ORDER BY atime desc') - self.history_dict is the main place where the history is stored, in an - OrderedDict (sorted by time) of URL strings mapped to Entry objects. - - While reading from disk is still ongoing, the history is saved in - self._temp_history instead, and then appended to self.history_dict once - that's fully populated. - - All history which is new in this session (rather than read from disk from a - previous browsing session) is also stored in self._new_history. - self._saved_count tracks how many of those entries were already written to - disk, so we can always append to the existing data. - - Attributes: - history_dict: An OrderedDict of URLs read from the on-disk history. - _lineparser: The AppendLineParser used to save the history. - _new_history: A list of Entry items of the current session. - _saved_count: How many HistoryEntries have been written to disk. - _initial_read_started: Whether async_read was called. - _initial_read_done: Whether async_read has completed. - _temp_history: OrderedDict of temporary history entries before - async_read was called. - - Signals: - add_completion_item: Emitted before a new Entry is added. - Used to sync with the completion. - arg: The new Entry. - item_added: Emitted after a new Entry is added. - Used to tell the savemanager that the history is dirty. - arg: The new Entry. - cleared: Emitted after the history is cleared. - """ - - add_completion_item = pyqtSignal(Entry) - item_added = pyqtSignal(Entry) - cleared = pyqtSignal() - async_read_done = pyqtSignal() - - def __init__(self, hist_dir, hist_name, parent=None): - super().__init__(parent) - self._initial_read_started = False - self._initial_read_done = False - self._lineparser = lineparser.AppendLineParser(hist_dir, hist_name, - parent=self) - self.history_dict = collections.OrderedDict() - self._temp_history = collections.OrderedDict() - self._new_history = [] - self._saved_count = 0 - objreg.get('save-manager').add_saveable( - 'history', self.save, self.item_added) + self._before_query = sql.Query('SELECT * FROM History ' + 'where not redirect ' + 'and not url like "qute://%" ' + 'and atime <= :latest ' + 'ORDER BY atime desc ' + 'limit :limit offset :offset') def __repr__(self): return utils.get_repr(self, length=len(self)) - def __iter__(self): - return iter(self.history_dict.values()) - - def __len__(self): - return len(self.history_dict) - - def async_read(self): - """Read the initial history.""" - if self._initial_read_started: - log.init.debug("Ignoring async_read() because reading is started.") - return - self._initial_read_started = True - - with self._lineparser.open(): - for line in self._lineparser: - yield - - line = line.rstrip() - if not line: - continue - - try: - entry = Entry.from_str(line) - except ValueError as e: - log.init.warning("Invalid history entry {!r}: {}!".format( - line, e)) - continue - - # This de-duplicates history entries; only the latest - # entry for each URL is kept. If you want to keep - # information about previous hits change the items in - # old_urls to be lists or change Entry to have a - # list of atimes. - self._add_entry(entry) - - self._initial_read_done = True - self.async_read_done.emit() - - for entry in self._temp_history.values(): - self._add_entry(entry) - self._new_history.append(entry) - if not entry.redirect: - self.add_completion_item.emit(entry) - self._temp_history.clear() - - def _add_entry(self, entry, target=None): - """Add an entry to self.history_dict or another given OrderedDict.""" - if target is None: - target = self.history_dict - url_str = entry.url_str() - target[url_str] = entry - target.move_to_end(url_str) + def __contains__(self, url): + return self._contains_query.run(val=url).value() def get_recent(self): """Get the most recent history entries.""" - old = self._lineparser.get_recent() - return old + [str(e) for e in self._new_history] + return self.select(sort_by='atime', sort_order='desc', limit=100) - def save(self): - """Save the history to disk.""" - new = (str(e) for e in self._new_history[self._saved_count:]) - self._lineparser.new_data = new - self._lineparser.save() - self._saved_count = len(self._new_history) + def entries_between(self, earliest, latest): + """Iterate non-redirect, non-qute entries between two timestamps. + + Args: + earliest: Omit timestamps earlier than this. + latest: Omit timestamps later than this. + """ + self._between_query.run(earliest=earliest, latest=latest) + return iter(self._between_query) + + def entries_before(self, latest, limit, offset): + """Iterate non-redirect, non-qute entries occurring before a timestamp. + + Args: + latest: Omit timestamps more recent than this. + limit: Max number of entries to include. + offset: Number of entries to skip. + """ + self._before_query.run(latest=latest, limit=limit, offset=offset) + return iter(self._before_query) @cmdutils.register(name='history-clear', instance='web-history') def clear(self, force=False): @@ -247,16 +114,27 @@ class WebHistory(QObject): "history?") def _do_clear(self): - self._lineparser.clear() - self.history_dict.clear() - self._temp_history.clear() - self._new_history.clear() - self._saved_count = 0 - self.cleared.emit() + self.delete_all() + self.completion.delete_all() + + def delete_url(self, url): + """Remove all history entries with the given url. + + Args: + url: URL string to delete. + """ + self.delete('url', url) + self.completion.delete('url', url) @pyqtSlot(QUrl, QUrl, str) def add_from_tab(self, url, requested_url, title): """Add a new history entry as slot, called from a BrowserTab.""" + if url.scheme() == 'data' or requested_url.scheme() == 'data': + return + if url.isEmpty(): + # things set via setHtml + return + no_formatting = QUrl.UrlFormattingOption(0) if (requested_url.isValid() and not requested_url.matches(url, no_formatting)): @@ -274,23 +152,135 @@ class WebHistory(QObject): (hidden in completion) atime: Override the atime used to add the entry """ - if config.get('general', 'private-browsing'): - return - if not url.isValid(): + if not url.isValid(): # pragma: no cover + # the no cover pragma is a WORKAROUND for this not being covered in + # old Qt versions. log.misc.warning("Ignoring invalid URL being added to history") return - if atime is None: - atime = time.time() - entry = Entry(atime, url, title, redirect=redirect) - if self._initial_read_done: - self._add_entry(entry) - self._new_history.append(entry) - self.item_added.emit(entry) - if not entry.redirect: - self.add_completion_item.emit(entry) + atime = int(atime) if (atime is not None) else int(time.time()) + url_str = url.toString(QUrl.FullyEncoded | QUrl.RemovePassword) + self.insert({'url': url_str, + 'title': title, + 'atime': atime, + 'redirect': redirect}) + if not redirect: + self.completion.insert({'url': url_str, + 'title': title, + 'last_atime': atime}, + replace=True) + + def _parse_entry(self, line): + """Parse a history line like '12345 http://example.com title'.""" + if not line or line.startswith('#'): + return None + data = line.split(maxsplit=2) + if len(data) == 2: + atime, url = data + title = "" + elif len(data) == 3: + atime, url, title = data else: - self._add_entry(entry, target=self._temp_history) + raise ValueError("2 or 3 fields expected") + + # http://xn--pple-43d.com/ with + # https://bugreports.qt.io/browse/QTBUG-60364 + if url in ['http://.com/', 'https://www..com/']: + return None + + url = QUrl(url) + if not url.isValid(): + raise ValueError("Invalid URL: {}".format(url.errorString())) + + # https://github.com/qutebrowser/qutebrowser/issues/2646 + if url.scheme() == 'data': + return None + + # https://github.com/qutebrowser/qutebrowser/issues/670 + atime = atime.lstrip('\0') + + if '-' in atime: + atime, flags = atime.split('-') + else: + flags = '' + + if not set(flags).issubset('r'): + raise ValueError("Invalid flags {!r}".format(flags)) + + redirect = 'r' in flags + return (url, title, int(atime), redirect) + + def import_txt(self): + """Import a history text file into sqlite if it exists. + + In older versions of qutebrowser, history was stored in a text format. + This converts that file into the new sqlite format and moves it to a + backup location. + """ + path = os.path.join(standarddir.data(), 'history') + if not os.path.isfile(path): + return + + def action(): + with debug.log_time(log.init, 'Import old history file to sqlite'): + try: + self._read(path) + except ValueError as ex: + message.error('Failed to import history: {}'.format(ex)) + else: + bakpath = path + '.bak' + message.info('History import complete. Moving {} to {}' + .format(path, bakpath)) + os.rename(path, bakpath) + + # delay to give message time to appear before locking down for import + message.info('Converting {} to sqlite...'.format(path)) + QTimer.singleShot(100, action) + + def _read(self, path): + """Import a text file into the sql database.""" + with open(path, 'r', encoding='utf-8') as f: + data = {'url': [], 'title': [], 'atime': [], 'redirect': []} + completion_data = {'url': [], 'title': [], 'last_atime': []} + for (i, line) in enumerate(f): + try: + parsed = self._parse_entry(line.strip()) + if parsed is None: + continue + url, title, atime, redirect = parsed + data['url'].append(url) + data['title'].append(title) + data['atime'].append(atime) + data['redirect'].append(redirect) + if not redirect: + completion_data['url'].append(url) + completion_data['title'].append(title) + completion_data['last_atime'].append(atime) + except ValueError as ex: + raise ValueError('Failed to parse line #{} of {}: "{}"' + .format(i, path, ex)) + self.insert_batch(data) + self.completion.insert_batch(completion_data, replace=True) + + @cmdutils.register(instance='web-history', debug=True) + def debug_dump_history(self, dest): + """Dump the history to a file in the old pre-SQL format. + + Args: + dest: Where to write the file to. + """ + dest = os.path.expanduser(dest) + + lines = ('{}{} {} {}' + .format(int(x.atime), '-r' * x.redirect, x.url, x.title) + for x in self.select(sort_by='atime', sort_order='asc')) + + try: + with open(dest, 'w', encoding='utf-8') as f: + f.write('\n'.join(lines)) + message.info("Dumped history to {}".format(dest)) + except OSError as e: + raise cmdexc.CommandError('Could not write history: {}', e) def init(parent=None): @@ -299,8 +289,7 @@ def init(parent=None): Args: parent: The parent to use for WebHistory. """ - history = WebHistory(hist_dir=standarddir.data(), hist_name='history', - parent=parent) + history = WebHistory(parent=parent) objreg.register('web-history', history) if objects.backend == usertypes.Backend.QtWebKit: diff --git a/qutebrowser/browser/inspector.py b/qutebrowser/browser/inspector.py index a18d1ecf2..e225c31da 100644 --- a/qutebrowser/browser/inspector.py +++ b/qutebrowser/browser/inspector.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/mouse.py b/qutebrowser/browser/mouse.py index 79b6816bb..d1cd889d3 100644 --- a/qutebrowser/browser/mouse.py +++ b/qutebrowser/browser/mouse.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/navigate.py b/qutebrowser/browser/navigate.py index aacec9d3c..4443c6a47 100644 --- a/qutebrowser/browser/navigate.py +++ b/qutebrowser/browser/navigate.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -79,8 +79,7 @@ def _find_prevnext(prev, elems): return e # Then check for regular links/buttons. - filterfunc = webelem.FILTERS[webelem.Group.prevnext] - elems = [e for e in elems if e.tag_name() != 'link' and filterfunc(e)] + elems = [e for e in elems if e.tag_name() != 'link'] option = 'prev-regexes' if prev else 'next-regexes' if not elems: return None @@ -128,20 +127,21 @@ def prevnext(*, browsertab, win_id, baseurl, prev=False, return qtutils.ensure_valid(url) + cur_tabbed_browser = objreg.get('tabbed-browser', scope='window', + window=win_id) + if window: from qutebrowser.mainwindow import mainwindow - new_window = mainwindow.MainWindow() + new_window = mainwindow.MainWindow( + private=cur_tabbed_browser.private) new_window.show() tabbed_browser = objreg.get('tabbed-browser', scope='window', window=new_window.win_id) tabbed_browser.tabopen(url, background=False) elif tab: - tabbed_browser = objreg.get('tabbed-browser', scope='window', - window=win_id) - tabbed_browser.tabopen(url, background=background) + cur_tabbed_browser.tabopen(url, background=background) else: browsertab.openurl(url) - selector = ', '.join([webelem.SELECTORS[webelem.Group.links], - webelem.SELECTORS[webelem.Group.prevnext]]) - browsertab.elements.find_css(selector, _prevnext_cb) + browsertab.elements.find_css(webelem.SELECTORS[webelem.Group.links], + _prevnext_cb) diff --git a/qutebrowser/browser/network/pac.py b/qutebrowser/browser/network/pac.py index 63220d9b5..74a6c4296 100644 --- a/qutebrowser/browser/network/pac.py +++ b/qutebrowser/browser/network/pac.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -312,4 +312,4 @@ class PACFetcher(QObject): # 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) + return [QNetworkProxy(QNetworkProxy.HttpProxy, error_host, 9)] diff --git a/qutebrowser/browser/network/proxy.py b/qutebrowser/browser/network/proxy.py index 719c33178..1bdbc7b0b 100644 --- a/qutebrowser/browser/network/proxy.py +++ b/qutebrowser/browser/network/proxy.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/pdfjs.py b/qutebrowser/browser/pdfjs.py index d11cf3098..d003cefb1 100644 --- a/qutebrowser/browser/pdfjs.py +++ b/qutebrowser/browser/pdfjs.py @@ -1,6 +1,7 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: # Copyright 2015 Daniel Schadt +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/qtnetworkdownloads.py b/qutebrowser/browser/qtnetworkdownloads.py index 920673d4b..512991bbc 100644 --- a/qutebrowser/browser/qtnetworkdownloads.py +++ b/qutebrowser/browser/qtnetworkdownloads.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -27,6 +27,7 @@ import collections from PyQt5.QtCore import pyqtSlot, pyqtSignal, QTimer from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply +from qutebrowser.config import config from qutebrowser.utils import message, usertypes, log, urlutils, utils from qutebrowser.browser import downloads from qutebrowser.browser.webkit import http @@ -273,7 +274,7 @@ class DownloadItem(downloads.AbstractDownloadItem): if self.fileobj is None or self._reply is None: # No filename has been set yet (so we don't empty the buffer) or we # got a readyRead after the reply was finished (which happens on - # qute:log for example). + # qute://log for example). return if not self._reply.isOpen(): raise OSError("Reply is closed!") @@ -366,7 +367,8 @@ class DownloadManager(downloads.AbstractDownloadManager): def __init__(self, win_id, parent=None): super().__init__(parent) self._networkmanager = networkmanager.NetworkManager( - win_id, None, self) + win_id=win_id, tab_id=None, + private=config.get('general', 'private-browsing'), parent=self) @pyqtSlot('QUrl') def get(self, url, *, user_agent=None, **kwargs): @@ -410,7 +412,8 @@ class DownloadManager(downloads.AbstractDownloadManager): mhtml.start_download_checked, tab=tab)) message.global_bridge.ask(question, blocking=False) - def get_request(self, request, *, target=None, **kwargs): + def get_request(self, request, *, target=None, + suggested_fn=None, **kwargs): """Start a download with a QNetworkRequest. Args: @@ -426,7 +429,9 @@ class DownloadManager(downloads.AbstractDownloadManager): request.setAttribute(QNetworkRequest.CacheLoadControlAttribute, QNetworkRequest.AlwaysNetwork) - if request.url().scheme().lower() != 'data': + if suggested_fn is not None: + pass + elif request.url().scheme().lower() != 'data': suggested_fn = urlutils.filename_from_url(request.url()) else: # We might be downloading a binary blob embedded on a page or even diff --git a/qutebrowser/browser/qutescheme.py b/qutebrowser/browser/qutescheme.py index 45ce71c27..0f86d0980 100644 --- a/qutebrowser/browser/qutescheme.py +++ b/qutebrowser/browser/qutescheme.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -17,7 +17,7 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -"""Backend-independent qute:* code. +"""Backend-independent qute://* code. Module attributes: pyeval_output: The output of the last :pyeval command. @@ -26,16 +26,17 @@ Module attributes: import json import os -import sys import time import urllib.parse +import datetime +import pkg_resources -from PyQt5.QtCore import QUrlQuery +from PyQt5.QtCore import QUrlQuery, QUrl import qutebrowser from qutebrowser.config import config from qutebrowser.utils import (version, utils, jinja, log, message, docutils, - objreg) + objreg, usertypes, qtutils) from qutebrowser.misc import objects @@ -77,12 +78,25 @@ class QuteSchemeError(Exception): super().__init__(errorstring) -class add_handler: # pylint: disable=invalid-name +class Redirect(Exception): - """Decorator to register a qute:* URL handler. + """Exception to signal a redirect should happen. Attributes: - _name: The 'foo' part of qute:foo + url: The URL to redirect to, as a QUrl. + """ + + def __init__(self, url): + super().__init__(url.toDisplayString()) + self.url = url + + +class add_handler: # pylint: disable=invalid-name + + """Decorator to register a qute://* URL handler. + + Attributes: + _name: The 'foo' part of qute://foo backend: Limit which backends the handler can run with. """ @@ -105,7 +119,7 @@ class add_handler: # pylint: disable=invalid-name def wrong_backend_handler(self, url): """Show an error page about using the invalid backend.""" html = jinja.render('error.html', - title="Error while opening qute:url", + title="Error while opening qute://url", url=url.toDisplayString(), error='{} is not available with this ' 'backend'.format(url.toDisplayString()), @@ -127,13 +141,19 @@ def data_for_url(url): # A url like "qute:foo" is split as "scheme:path", not "scheme:host". log.misc.debug("url: {}, path: {}, host {}".format( url.toDisplayString(), path, host)) + if path and not host: + new_url = QUrl() + new_url.setScheme('qute') + new_url.setHost(path) + new_url.setPath('/') + if new_url.host(): # path was a valid host + raise Redirect(new_url) + try: - handler = _HANDLERS[path] + handler = _HANDLERS[host] except KeyError: - try: - handler = _HANDLERS[host] - except KeyError: - raise NoHandlerFound(url) + raise NoHandlerFound(url) + try: mimetype, data = handler(url) except OSError as e: @@ -152,7 +172,7 @@ def data_for_url(url): @add_handler('bookmarks') def qute_bookmarks(_url): - """Handler for qute:bookmarks. Display all quickmarks / bookmarks.""" + """Handler for qute://bookmarks. Display all quickmarks / bookmarks.""" bookmarks = sorted(objreg.get('bookmark-manager').marks.items(), key=lambda x: x[1]) # Sort by title quickmarks = sorted(objreg.get('quickmark-manager').marks.items(), @@ -165,68 +185,36 @@ def qute_bookmarks(_url): return 'text/html', html -@add_handler('history') # noqa +def history_data(start_time, offset=None): + """Return history data. + + Arguments: + start_time: select history starting from this timestamp. + offset: number of items to skip + """ + # history atimes are stored as ints, ensure start_time is not a float + start_time = int(start_time) + hist = objreg.get('web-history') + if offset is not None: + entries = hist.entries_before(start_time, limit=1000, offset=offset) + else: + # end is 24hrs earlier than start + end_time = start_time - 24*60*60 + entries = hist.entries_between(end_time, start_time) + + return [{"url": e.url, "title": e.title or e.url, "time": e.atime} + for e in entries] + + +@add_handler('history') def qute_history(url): - """Handler for qute:history. Display and serve history.""" - def history_iter(start_time, reverse=False): - """Iterate through the history and get items we're interested. - - Arguments: - reverse -- whether to reverse the history_dict before iterating. - start_time -- select history starting from this timestamp. - """ - history = objreg.get('web-history').history_dict.values() - if reverse: - history = reversed(history) - - end_time = start_time - 24*60*60 # end is 24hrs earlier than start - - # when history_dict is not reversed, we need to keep track of last item - # so that we can yield its atime - last_item = None - - for item in history: - # Skip redirects - # Skip qute:// links - if item.redirect or item.url.scheme() == 'qute': - continue - - # Skip items out of time window - item_newer = item.atime > start_time - item_older = item.atime <= end_time - if reverse: - # history_dict is reversed, we are going back in history. - # so: - # abort if item is older than start_time+24hr - # skip if item is newer than start - if item_older: - yield {"next": int(item.atime)} - return - if item_newer: - continue - else: - # history_dict isn't reversed, we are going forward in history. - # so: - # abort if item is newer than start_time - # skip if item is older than start_time+24hrs - if item_older: - last_item = item - continue - if item_newer: - yield {"next": int(last_item.atime if last_item else -1)} - return - - # 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 - item_time = int(item.atime * 1000) - - yield {"url": item_url, "title": item_title, "time": item_time} - - # if we reached here, we had reached the end of history - yield {"next": int(last_item.atime if last_item else -1)} - + """Handler for qute://history. Display and serve history.""" if url.path() == '/data': + try: + offset = QUrlQuery(url).queryItemValue("offset") + offset = int(offset) if offset else None + except ValueError as e: + raise QuteSchemeError("Query parameter offset is invalid", e) # Use start_time in query or current time. try: start_time = QUrlQuery(url).queryItemValue("start_time") @@ -234,26 +222,57 @@ def qute_history(url): except ValueError as e: raise QuteSchemeError("Query parameter start_time is invalid", e) - 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 = history_iter(start_time, 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(start_time, reverse=False))) - - return 'text/html', json.dumps(list(history)) + return 'text/html', json.dumps(history_data(start_time, offset)) else: - return 'text/html', jinja.render('history.html', title='History', - session_interval=config.get('ui', 'history-session-interval')) + if ( + config.get('content', 'allow-javascript') and + (objects.backend == usertypes.Backend.QtWebEngine or + qtutils.is_qtwebkit_ng()) + ): + return 'text/html', jinja.render( + 'history.html', + title='History', + session_interval=config.get('ui', 'history-session-interval') + ) + else: + # Get current date from query parameter, if not given choose today. + curr_date = datetime.date.today() + try: + query_date = QUrlQuery(url).queryItemValue("date") + if query_date: + curr_date = datetime.datetime.strptime(query_date, + "%Y-%m-%d").date() + except ValueError: + log.misc.debug("Invalid date passed to qute:history: " + + query_date) + + one_day = datetime.timedelta(days=1) + next_date = curr_date + one_day + prev_date = curr_date - one_day + + # start_time is the last second of curr_date + start_time = time.mktime(next_date.timetuple()) - 1 + history = [ + (i["url"], i["title"], + datetime.datetime.fromtimestamp(i["time"]), + QUrl(i["url"]).host()) + for i in history_data(start_time) + ] + + return 'text/html', jinja.render( + 'history_nojs.html', + title='History', + history=history, + curr_date=curr_date, + next_date=next_date, + prev_date=prev_date, + today=datetime.date.today(), + ) @add_handler('javascript') def qute_javascript(url): - """Handler for qute:javascript. + """Handler for qute://javascript. Return content of file given as query parameter. """ @@ -267,7 +286,7 @@ def qute_javascript(url): @add_handler('pyeval') def qute_pyeval(_url): - """Handler for qute:pyeval.""" + """Handler for qute://pyeval.""" html = jinja.render('pre.html', title='pyeval', content=pyeval_output) return 'text/html', html @@ -275,7 +294,7 @@ def qute_pyeval(_url): @add_handler('version') @add_handler('verizon') def qute_version(_url): - """Handler for qute:version.""" + """Handler for qute://version.""" html = jinja.render('version.html', title='Version info', version=version.version(), copyright=qutebrowser.__copyright__) @@ -284,7 +303,7 @@ def qute_version(_url): @add_handler('plainlog') def qute_plainlog(url): - """Handler for qute:plainlog. + """Handler for qute://plainlog. An optional query parameter specifies the minimum log level to print. For example, qute://log?level=warning prints warnings and errors. @@ -304,7 +323,7 @@ def qute_plainlog(url): @add_handler('log') def qute_log(url): - """Handler for qute:log. + """Handler for qute://log. An optional query parameter specifies the minimum log level to print. For example, qute://log?level=warning prints warnings and errors. @@ -325,13 +344,13 @@ def qute_log(url): @add_handler('gpl') def qute_gpl(_url): - """Handler for qute:gpl. Return HTML content as string.""" + """Handler for qute://gpl. Return HTML content as string.""" return 'text/html', utils.read_file('html/COPYING.html') @add_handler('help') def qute_help(url): - """Handler for qute:help.""" + """Handler for qute://help.""" try: utils.read_file('html/doc/index.html') except OSError: @@ -360,3 +379,14 @@ def qute_help(url): else: data = utils.read_file(path) return 'text/html', data + + +@add_handler('backend-warning') +def qute_backend_warning(_url): + """Handler for qute://backend-warning.""" + html = jinja.render('backend-warning.html', + distribution=version.distribution(), + Distribution=version.Distribution, + version=pkg_resources.parse_version, + title="Legacy backend warning") + return 'text/html', html diff --git a/qutebrowser/browser/shared.py b/qutebrowser/browser/shared.py index 885e2809d..d400387a9 100644 --- a/qutebrowser/browser/shared.py +++ b/qutebrowser/browser/shared.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -216,8 +216,10 @@ def get_tab(win_id, target): win_id = win_id bg_tab = True elif target == usertypes.ClickTarget.window: + tabbed_browser = objreg.get('tabbed-browser', scope='window', + window=win_id) from qutebrowser.mainwindow import mainwindow - window = mainwindow.MainWindow() + window = mainwindow.MainWindow(private=tabbed_browser.private) window.show() win_id = window.win_id bg_tab = False diff --git a/qutebrowser/browser/signalfilter.py b/qutebrowser/browser/signalfilter.py index 78aa18b42..90b85c586 100644 --- a/qutebrowser/browser/signalfilter.py +++ b/qutebrowser/browser/signalfilter.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/urlmarks.py b/qutebrowser/browser/urlmarks.py index 991ba7fc5..013de408c 100644 --- a/qutebrowser/browser/urlmarks.py +++ b/qutebrowser/browser/urlmarks.py @@ -1,7 +1,7 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) -# Copyright 2015-2016 Antoni Boucher +# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Antoni Boucher # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/webelem.py b/qutebrowser/browser/webelem.py index 5dd263da3..149300111 100644 --- a/qutebrowser/browser/webelem.py +++ b/qutebrowser/browser/webelem.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -22,9 +22,6 @@ Module attributes: Group: Enum for different kinds of groups. SELECTORS: CSS selectors for different groups of elements. - FILTERS: A dictionary of filter functions for the modes. - The filter for "links" filters javascript:-links and a-tags - without "href". """ import collections.abc @@ -37,18 +34,16 @@ from qutebrowser.keyinput import modeman from qutebrowser.utils import log, usertypes, utils, qtutils, objreg -Group = usertypes.enum('Group', ['all', 'links', 'images', 'url', 'prevnext', - 'inputs']) +Group = usertypes.enum('Group', ['all', 'links', 'images', 'url', 'inputs']) SELECTORS = { Group.all: ('a, area, textarea, select, input:not([type=hidden]), button, ' 'frame, iframe, link, [onclick], [onmousedown], [role=link], ' '[role=option], [role=button], img'), - Group.links: 'a, area, link, [role=link]', + Group.links: 'a[href], area[href], link[href], [role=link][href]', Group.images: 'img', Group.url: '[src], [href]', - Group.prevnext: 'a, area, button, link, [role=button]', Group.inputs: ('input[type=text], input[type=email], input[type=url], ' 'input[type=tel], input[type=number], ' 'input[type=password], input[type=search], ' @@ -56,16 +51,6 @@ SELECTORS = { } -def filter_links(elem): - return 'href' in elem and QUrl(elem['href']).scheme() != 'javascript' - - -FILTERS = { - Group.links: filter_links, - Group.prevnext: filter_links, -} - - class Error(Exception): """Base class for WebElement errors.""" @@ -306,6 +291,11 @@ class AbstractWebElement(collections.abc.MutableMapping): qtutils.ensure_valid(url) return url + def is_link(self): + """Return True if this AbstractWebElement is a link.""" + href_tags = ['a', 'area', 'link'] + return self.tag_name() in href_tags and 'href' in self + def _mouse_pos(self): """Get the position to click/hover.""" # Click the center of the largest square fitting into the top/left @@ -374,15 +364,16 @@ class AbstractWebElement(collections.abc.MutableMapping): self._click_fake_event(click_target) return + tabbed_browser = objreg.get('tabbed-browser', scope='window', + window=self._tab.win_id) + 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 = mainwindow.MainWindow(private=tabbed_browser.private) window.show() window.tabbed_browser.tabopen(url) else: @@ -403,9 +394,8 @@ class AbstractWebElement(collections.abc.MutableMapping): 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: + if self.is_link(): log.webelem.debug("Clicking via JS click()") self._click_js(click_target) elif self.is_editable(strict=True): @@ -418,7 +408,7 @@ class AbstractWebElement(collections.abc.MutableMapping): elif click_target in [usertypes.ClickTarget.tab, usertypes.ClickTarget.tab_bg, usertypes.ClickTarget.window]: - if self.tag_name() in href_tags: + if self.is_link(): self._click_href(click_target) else: self._click_fake_event(click_target) diff --git a/qutebrowser/browser/webengine/__init__.py b/qutebrowser/browser/webengine/__init__.py index d7c910b36..60d140540 100644 --- a/qutebrowser/browser/webengine/__init__.py +++ b/qutebrowser/browser/webengine/__init__.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/webengine/certificateerror.py b/qutebrowser/browser/webengine/certificateerror.py index 19b59c522..c97e51b81 100644 --- a/qutebrowser/browser/webengine/certificateerror.py +++ b/qutebrowser/browser/webengine/certificateerror.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -19,9 +19,7 @@ """Wrapper over a QWebEngineCertificateError.""" -# pylint: disable=no-name-in-module,import-error,useless-suppression from PyQt5.QtWebEngineWidgets import QWebEngineCertificateError -# pylint: enable=no-name-in-module,import-error,useless-suppression from qutebrowser.utils import usertypes, utils, debug diff --git a/qutebrowser/browser/webengine/interceptor.py b/qutebrowser/browser/webengine/interceptor.py index 350378147..0cb2d446e 100644 --- a/qutebrowser/browser/webengine/interceptor.py +++ b/qutebrowser/browser/webengine/interceptor.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -19,9 +19,7 @@ """A request interceptor taking care of adblocking and custom headers.""" -# pylint: disable=no-name-in-module,import-error,useless-suppression from PyQt5.QtWebEngineCore import QWebEngineUrlRequestInterceptor -# pylint: enable=no-name-in-module,import-error,useless-suppression from qutebrowser.config import config from qutebrowser.browser import shared @@ -57,8 +55,7 @@ class RequestInterceptor(QWebEngineUrlRequestInterceptor): info: QWebEngineUrlRequestInfo &info """ # FIXME:qtwebengine only block ads for NavigationTypeOther? - if (bytes(info.requestMethod()) == b'GET' and - self._host_blocker.is_blocked(info.requestUrl())): + if self._host_blocker.is_blocked(info.requestUrl()): log.webview.info("Request to {} blocked by host blocker.".format( info.requestUrl().host())) info.block(True) diff --git a/qutebrowser/browser/webengine/tabhistory.py b/qutebrowser/browser/webengine/tabhistory.py index ec43aa07b..5db6faeb1 100644 --- a/qutebrowser/browser/webengine/tabhistory.py +++ b/qutebrowser/browser/webengine/tabhistory.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/webengine/webenginedownloads.py b/qutebrowser/browser/webengine/webenginedownloads.py index 53cbb82c1..4a558471f 100644 --- a/qutebrowser/browser/webengine/webenginedownloads.py +++ b/qutebrowser/browser/webengine/webenginedownloads.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -25,9 +25,7 @@ import urllib import functools from PyQt5.QtCore import pyqtSlot, Qt -# pylint: disable=no-name-in-module,import-error,useless-suppression 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, qtutils @@ -79,7 +77,12 @@ class DownloadItem(downloads.AbstractDownloadItem): elif state == QWebEngineDownloadItem.DownloadInterrupted: self.successful = False # https://bugreports.qt.io/browse/QTBUG-56839 - self._die("Download failed") + try: + reason = self._qt_item.interruptReasonString() + except AttributeError: + # Qt < 5.9 + reason = "Download failed" + self._die(reason) else: raise ValueError("_on_state_changed was called with unknown state " "{}".format(state_name)) @@ -100,7 +103,8 @@ class DownloadItem(downloads.AbstractDownloadItem): def _get_open_filename(self): return self._filename - def _set_fileobj(self, fileobj): + def _set_fileobj(self, fileobj, *, + autoclose=True): # pylint: disable=unused-argument raise downloads.UnsupportedOperationError def _set_tempfile(self, fileobj): @@ -146,7 +150,7 @@ def _get_suggested_filename(path): """ filename = os.path.basename(path) filename = re.sub(r'\([0-9]+\)(?=\.|$)', '', filename) - if not qtutils.version_check('5.8.1'): + if not qtutils.version_check('5.9'): # https://bugreports.qt.io/browse/QTBUG-58155 filename = urllib.parse.unquote(filename) # Doing basename a *second* time because there could be a %2F in diff --git a/qutebrowser/browser/webengine/webengineelem.py b/qutebrowser/browser/webengine/webengineelem.py index 3e145468b..12744cedb 100644 --- a/qutebrowser/browser/webengine/webengineelem.py +++ b/qutebrowser/browser/webengine/webengineelem.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -18,16 +18,14 @@ # along with qutebrowser. If not, see . # FIXME:qtwebengine remove this once the stubs are gone -# pylint: disable=unused-variable +# pylint: disable=unused-argument """QtWebEngine specific part of the web element API.""" 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 @@ -39,6 +37,38 @@ class WebEngineElement(webelem.AbstractWebElement): def __init__(self, js_dict, tab): super().__init__(tab) + # Do some sanity checks on the data we get from JS + js_dict_types = { + 'id': int, + 'text': str, + 'value': (str, int, float), + 'tag_name': str, + 'outer_xml': str, + 'class_name': str, + 'rects': list, + 'attributes': dict, + } + assert set(js_dict.keys()).issubset(js_dict_types.keys()) + for name, typ in js_dict_types.items(): + if name in js_dict and not isinstance(js_dict[name], typ): + raise TypeError("Got {} for {} from JS but expected {}: " + "{}".format(type(js_dict[name]), name, typ, + js_dict)) + for name, value in js_dict['attributes'].items(): + if not isinstance(name, str): + raise TypeError("Got {} ({}) for attribute name from JS: " + "{}".format(name, type(name), js_dict)) + if not isinstance(value, str): + raise TypeError("Got {} ({}) for attribute {} from JS: " + "{}".format(value, type(value), name, js_dict)) + for rect in js_dict['rects']: + assert set(rect.keys()) == {'top', 'right', 'bottom', 'left', + 'height', 'width'}, rect.keys() + for value in rect.values(): + if not isinstance(value, (int, float)): + raise TypeError("Got {} ({}) for rect from JS: " + "{}".format(value, type(value), js_dict)) + self._id = js_dict['id'] self._js_dict = js_dict @@ -88,7 +118,9 @@ class WebEngineElement(webelem.AbstractWebElement): The returned name will always be lower-case. """ - return self._js_dict['tag_name'].lower() + tag = self._js_dict['tag_name'] + assert isinstance(tag, str), tag + return tag.lower() def outer_xml(self): """Get the full HTML representation of this element.""" @@ -158,21 +190,19 @@ class WebEngineElement(webelem.AbstractWebElement): 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() + # FIXME:qtwebengine Have a proper API for this + # pylint: disable=protected-access + settings = self._tab._widget.settings() + # pylint: enable=protected-access attribute = QWebEngineSettings.JavascriptCanOpenWindows could_open_windows = settings.testAttribute(attribute) settings.setAttribute(attribute, True) diff --git a/qutebrowser/browser/webengine/webengineinspector.py b/qutebrowser/browser/webengine/webengineinspector.py index b822bd253..8fa8bcb2d 100644 --- a/qutebrowser/browser/webengine/webengineinspector.py +++ b/qutebrowser/browser/webengine/webengineinspector.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -22,9 +22,7 @@ import os from PyQt5.QtCore import QUrl -# pylint: disable=no-name-in-module,import-error,useless-suppression from PyQt5.QtWebEngineWidgets import QWebEngineView -# pylint: enable=no-name-in-module,import-error,useless-suppression from qutebrowser.browser import inspector diff --git a/qutebrowser/browser/webengine/webenginequtescheme.py b/qutebrowser/browser/webengine/webenginequtescheme.py index 6bc31f9be..2e9aedd3e 100644 --- a/qutebrowser/browser/webengine/webenginequtescheme.py +++ b/qutebrowser/browser/webengine/webenginequtescheme.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -17,24 +17,22 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -"""QtWebEngine specific qute:* handlers and glue code.""" +"""QtWebEngine specific qute://* handlers and glue code.""" from PyQt5.QtCore import QBuffer, QIODevice -# pylint: disable=no-name-in-module,import-error,useless-suppression from PyQt5.QtWebEngineCore import (QWebEngineUrlSchemeHandler, QWebEngineUrlRequestJob) -# pylint: enable=no-name-in-module,import-error,useless-suppression from qutebrowser.browser import qutescheme -from qutebrowser.utils import log +from qutebrowser.utils import log, qtutils class QuteSchemeHandler(QWebEngineUrlSchemeHandler): - """Handle qute:* requests on QtWebEngine.""" + """Handle qute://* requests on QtWebEngine.""" def install(self, profile): - """Install the handler for qute: URLs on the given profile.""" + """Install the handler for qute:// URLs on the given profile.""" profile.installUrlSchemeHandler(b'qute', self) def requestStarted(self, job): @@ -58,12 +56,15 @@ class QuteSchemeHandler(QWebEngineUrlSchemeHandler): job.fail(QWebEngineUrlRequestJob.UrlNotFound) except qutescheme.QuteSchemeOSError: # FIXME:qtwebengine how do we show a better error here? - log.misc.exception("OSError while handling qute:* URL") + log.misc.exception("OSError while handling qute://* URL") job.fail(QWebEngineUrlRequestJob.UrlNotFound) except qutescheme.QuteSchemeError: # FIXME:qtwebengine how do we show a better error here? - log.misc.exception("Error while handling qute:* URL") + log.misc.exception("Error while handling qute://* URL") job.fail(QWebEngineUrlRequestJob.RequestFailed) + except qutescheme.Redirect as e: + qtutils.ensure_valid(e.url) + job.redirect(e.url) else: log.misc.debug("Returning {} data".format(mimetype)) diff --git a/qutebrowser/browser/webengine/webenginesettings.py b/qutebrowser/browser/webengine/webenginesettings.py index b029b4fd5..27ef60cb6 100644 --- a/qutebrowser/browser/webengine/webenginesettings.py +++ b/qutebrowser/browser/webengine/webenginesettings.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -17,6 +17,9 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . +# We get various "abstract but not overridden" warnings +# pylint: disable=abstract-method + """Bridge from QWebEngineSettings to our own settings. Module attributes: @@ -25,81 +28,96 @@ Module attributes: """ import os -import logging -# pylint: disable=no-name-in-module,import-error,useless-suppression +from PyQt5.QtGui import QFont from PyQt5.QtWebEngineWidgets import (QWebEngineSettings, QWebEngineProfile, QWebEngineScript) -# pylint: enable=no-name-in-module,import-error,useless-suppression from qutebrowser.browser import shared from qutebrowser.config import config, websettings -from qutebrowser.utils import objreg, utils, standarddir, javascript, log +from qutebrowser.utils import objreg, utils, standarddir, javascript, qtutils -class Attribute(websettings.Attribute): +# The default QWebEngineProfile +default_profile = None +# The QWebEngineProfile used for private (off-the-record) windows +private_profile = None + + +class Base(websettings.Base): + + """Base settings class with appropriate _get_global_settings.""" + + def _get_global_settings(self): + return [default_profile.settings(), private_profile.settings()] + + +class Attribute(Base, websettings.Attribute): """A setting set via QWebEngineSettings::setAttribute.""" - GLOBAL_SETTINGS = QWebEngineSettings.globalSettings ENUM_BASE = QWebEngineSettings -class Setter(websettings.Setter): +class Setter(Base, websettings.Setter): - """A setting set via QWebEngineSettings getter/setter methods.""" + """A setting set via a QWebEngineSettings setter method.""" - GLOBAL_SETTINGS = QWebEngineSettings.globalSettings + pass -class NullStringSetter(websettings.NullStringSetter): +class FontFamilySetter(Base, websettings.FontFamilySetter): - """A setter for settings requiring a null QString as default.""" + """A setter for a font family. - GLOBAL_SETTINGS = QWebEngineSettings.globalSettings + Gets the default value from QFont. + """ + + def __init__(self, font): + # Mapping from WebEngineSettings::initDefaults in + # qtwebengine/src/core/web_engine_settings.cpp + font_to_qfont = { + QWebEngineSettings.StandardFont: QFont.Serif, + QWebEngineSettings.FixedFont: QFont.Monospace, + QWebEngineSettings.SerifFont: QFont.Serif, + QWebEngineSettings.SansSerifFont: QFont.SansSerif, + QWebEngineSettings.CursiveFont: QFont.Cursive, + QWebEngineSettings.FantasyFont: QFont.Fantasy, + } + super().__init__(setter=QWebEngineSettings.setFontFamily, font=font, + qfont=font_to_qfont[font]) -class StaticSetter(websettings.StaticSetter): - - """A setting set via static QWebEngineSettings getter/setter methods.""" - - GLOBAL_SETTINGS = QWebEngineSettings.globalSettings - - -class ProfileSetter(websettings.Base): +class DefaultProfileSetter(websettings.Base): """A setting set on the QWebEngineProfile.""" - def __init__(self, getter, setter): - super().__init__() - self._getter = getter + def __init__(self, setter, default=websettings.UNSET): + super().__init__(default) self._setter = setter - def get(self, settings=None): - utils.unused(settings) - getter = getattr(QWebEngineProfile.defaultProfile(), self._getter) - return getter() + def __repr__(self): + return utils.get_repr(self, setter=self._setter, constructor=True) def _set(self, value, settings=None): - utils.unused(settings) - setter = getattr(QWebEngineProfile.defaultProfile(), self._setter) + if settings is not None: + raise ValueError("'settings' may not be set with " + "DefaultProfileSetters!") + setter = getattr(default_profile, self._setter) setter(value) -class PersistentCookiePolicy(ProfileSetter): +class PersistentCookiePolicy(DefaultProfileSetter): """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') + super().__init__('setPersistentCookiesPolicy') def _set(self, value, settings=None): - utils.unused(settings) + if settings is not None: + raise ValueError("'settings' may not be set with " + "PersistentCookiePolicy!") setter = getattr(QWebEngineProfile.defaultProfile(), self._setter) setter( QWebEngineProfile.AllowPersistentCookies if value else @@ -113,9 +131,6 @@ def _init_stylesheet(profile): Mostly inspired by QupZilla: https://github.com/QupZilla/qupzilla/blob/v2.0/src/lib/app/mainapplication.cpp#L1063-L1101 https://github.com/QupZilla/qupzilla/blob/v2.0/src/lib/tools/scripts.cpp#L119-L132 - - FIXME:qtwebengine Use QWebEngineStyleSheet once that's available - https://codereview.qt-project.org/#/c/148671/ """ old_script = profile.scripts().findScript('_qute_stylesheet') if not old_script.isNull(): @@ -140,19 +155,43 @@ 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 _set_user_agent(profile): + """Set the user agent for the given profile. + + We override this per request in the URL interceptor (to allow for + per-domain user agents), but this one still gets used for things like + window.navigator.userAgent in JS. + """ + user_agent = config.get('network', 'user-agent') + profile.setHttpUserAgent(user_agent) def update_settings(section, option): """Update global settings when qwebsettings changed.""" websettings.update_mappings(MAPPINGS, section, option) - profile = QWebEngineProfile.defaultProfile() if section == 'ui' and option in ['hide-scrollbar', 'user-stylesheet']: - _init_stylesheet(profile) + _init_stylesheet(default_profile) + _init_stylesheet(private_profile) + elif section == 'network' and option == 'user-agent': + _set_user_agent(default_profile) + _set_user_agent(private_profile) + + +def _init_profiles(): + """Init the two used QWebEngineProfiles.""" + global default_profile, private_profile + default_profile = QWebEngineProfile.defaultProfile() + default_profile.setCachePath( + os.path.join(standarddir.cache(), 'webengine')) + default_profile.setPersistentStoragePath( + os.path.join(standarddir.data(), 'webengine')) + _init_stylesheet(default_profile) + _set_user_agent(default_profile) + + private_profile = QWebEngineProfile() + assert private_profile.isOffTheRecord() + _init_stylesheet(private_profile) + _set_user_agent(private_profile) def init(args): @@ -160,25 +199,12 @@ def init(args): 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") + _init_profiles() - profile = QWebEngineProfile.defaultProfile() - _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')) - + if not qtutils.version_check('5.9'): + PersistentCookiePolicy().set(config.get('content', 'cookies-store')) Attribute(QWebEngineSettings.FullScreenSupportEnabled).set(True) websettings.init_mappings(MAPPINGS) @@ -199,7 +225,6 @@ def shutdown(): # - AllowRunningInsecureContent (5.8) # # Missing QtWebEngine fonts: -# - FantasyFont # - PictographFont @@ -221,9 +246,6 @@ 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), }, @@ -235,44 +257,28 @@ MAPPINGS = { }, 'fonts': { 'web-family-standard': - Setter(getter=QWebEngineSettings.fontFamily, - setter=QWebEngineSettings.setFontFamily, - args=[QWebEngineSettings.StandardFont]), + FontFamilySetter(QWebEngineSettings.StandardFont), 'web-family-fixed': - Setter(getter=QWebEngineSettings.fontFamily, - setter=QWebEngineSettings.setFontFamily, - args=[QWebEngineSettings.FixedFont]), + FontFamilySetter(QWebEngineSettings.FixedFont), 'web-family-serif': - Setter(getter=QWebEngineSettings.fontFamily, - setter=QWebEngineSettings.setFontFamily, - args=[QWebEngineSettings.SerifFont]), + FontFamilySetter(QWebEngineSettings.SerifFont), 'web-family-sans-serif': - Setter(getter=QWebEngineSettings.fontFamily, - setter=QWebEngineSettings.setFontFamily, - args=[QWebEngineSettings.SansSerifFont]), + FontFamilySetter(QWebEngineSettings.SansSerifFont), 'web-family-cursive': - Setter(getter=QWebEngineSettings.fontFamily, - setter=QWebEngineSettings.setFontFamily, - args=[QWebEngineSettings.CursiveFont]), + FontFamilySetter(QWebEngineSettings.CursiveFont), 'web-family-fantasy': - Setter(getter=QWebEngineSettings.fontFamily, - setter=QWebEngineSettings.setFontFamily, - args=[QWebEngineSettings.FantasyFont]), + FontFamilySetter(QWebEngineSettings.FantasyFont), 'web-size-minimum': - Setter(getter=QWebEngineSettings.fontSize, - setter=QWebEngineSettings.setFontSize, + Setter(QWebEngineSettings.setFontSize, args=[QWebEngineSettings.MinimumFontSize]), 'web-size-minimum-logical': - Setter(getter=QWebEngineSettings.fontSize, - setter=QWebEngineSettings.setFontSize, + Setter(QWebEngineSettings.setFontSize, args=[QWebEngineSettings.MinimumLogicalFontSize]), 'web-size-default': - Setter(getter=QWebEngineSettings.fontSize, - setter=QWebEngineSettings.setFontSize, + Setter(QWebEngineSettings.setFontSize, args=[QWebEngineSettings.DefaultFontSize]), 'web-size-default-fixed': - Setter(getter=QWebEngineSettings.fontSize, - setter=QWebEngineSettings.setFontSize, + Setter(QWebEngineSettings.setFontSize, args=[QWebEngineSettings.DefaultFixedFontSize]), }, 'ui': { @@ -283,15 +289,14 @@ MAPPINGS = { 'local-storage': Attribute(QWebEngineSettings.LocalStorageEnabled), 'cache-size': - ProfileSetter(getter='httpCacheMaximumSize', - setter='setHttpCacheMaximumSize') + # 0: automatically managed by QtWebEngine + DefaultProfileSetter('setHttpCacheMaximumSize', default=0), }, 'general': { 'xss-auditing': Attribute(QWebEngineSettings.XSSAuditingEnabled), 'default-encoding': - Setter(getter=QWebEngineSettings.defaultTextEncoding, - setter=QWebEngineSettings.setDefaultTextEncoding), + Setter(QWebEngineSettings.setDefaultTextEncoding), } } @@ -301,3 +306,8 @@ try: except AttributeError: # Added in Qt 5.8 pass + + +if qtutils.version_check('5.9'): + # https://bugreports.qt.io/browse/QTBUG-58650 + MAPPINGS['content']['cookies-store'] = PersistentCookiePolicy() diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index 76a526670..8d328f5e8 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -17,30 +17,27 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -# FIXME:qtwebengine remove this once the stubs are gone -# pylint: disable=unused-variable - """Wrapper over a QWebEngineView.""" +import os +import math import functools import sip from PyQt5.QtCore import pyqtSlot, Qt, QEvent, QPoint, QUrl, QTimer from PyQt5.QtGui import QKeyEvent from PyQt5.QtNetwork import QAuthenticator -# pylint: disable=no-name-in-module,import-error,useless-suppression from PyQt5.QtWidgets import QApplication -from PyQt5.QtWebEngineWidgets import (QWebEnginePage, QWebEngineScript, - QWebEngineProfile) -# pylint: enable=no-name-in-module,import-error,useless-suppression +from PyQt5.QtWebEngineWidgets import QWebEnginePage, QWebEngineScript from qutebrowser.browser import browsertab, mouse, shared from qutebrowser.browser.webengine import (webview, webengineelem, tabhistory, interceptor, webenginequtescheme, - webenginedownloads) + webenginedownloads, + webenginesettings) from qutebrowser.misc import miscwidgets from qutebrowser.utils import (usertypes, qtutils, log, javascript, utils, - objreg, jinja) + objreg, jinja, debug, version) _qute_scheme_handler = None @@ -53,21 +50,31 @@ def init(): # https://www.riverbankcomputing.com/pipermail/pyqt/2016-September/038075.html global _qute_scheme_handler app = QApplication.instance() - profile = QWebEngineProfile.defaultProfile() - log.init.debug("Initializing qute:* handler...") + software_rendering = (os.environ.get('LIBGL_ALWAYS_SOFTWARE') == '1' or + 'QT_XCB_FORCE_SOFTWARE_OPENGL' in os.environ) + if version.opengl_vendor() == 'nouveau' and not software_rendering: + # FIXME:qtwebengine display something more sophisticated here + raise browsertab.WebTabError( + "QtWebEngine is not supported with Nouveau graphics (unless " + "QT_XCB_FORCE_SOFTWARE_OPENGL is set as environment variable).") + + log.init.debug("Initializing qute://* handler...") _qute_scheme_handler = webenginequtescheme.QuteSchemeHandler(parent=app) - _qute_scheme_handler.install(profile) + _qute_scheme_handler.install(webenginesettings.default_profile) + _qute_scheme_handler.install(webenginesettings.private_profile) log.init.debug("Initializing request interceptor...") host_blocker = objreg.get('host-blocker') req_interceptor = interceptor.RequestInterceptor( host_blocker, parent=app) - req_interceptor.install(profile) + req_interceptor.install(webenginesettings.default_profile) + req_interceptor.install(webenginesettings.private_profile) log.init.debug("Initializing QtWebEngine downloads...") download_manager = webenginedownloads.DownloadManager(parent=app) - download_manager.install(profile) + download_manager.install(webenginesettings.default_profile) + download_manager.install(webenginesettings.private_profile) objreg.register('webengine-download-manager', download_manager) @@ -82,17 +89,17 @@ _JS_WORLD_MAP = { class WebEngineAction(browsertab.AbstractAction): - """QtWebKit implementations related to web actions.""" + """QtWebEngine implementations related to web actions.""" - def _action(self, action): - self._widget.triggerPageAction(action) + action_class = QWebEnginePage + action_base = QWebEnginePage.WebAction def exit_fullscreen(self): - self._action(QWebEnginePage.ExitFullScreen) + self._widget.triggerPageAction(QWebEnginePage.ExitFullScreen) def save_page(self): """Save the current page.""" - self._action(QWebEnginePage.SavePage) + self._widget.triggerPageAction(QWebEnginePage.SavePage) class WebEnginePrinting(browsertab.AbstractPrinting): @@ -128,12 +135,23 @@ class WebEngineSearch(browsertab.AbstractSearch): super().__init__(parent) self._flags = QWebEnginePage.FindFlags(0) - def _find(self, text, flags, cb=None): - """Call findText on the widget with optional callback.""" - if cb is None: - self._widget.findText(text, flags) - else: - self._widget.findText(text, flags, cb) + def _find(self, text, flags, callback, caller): + """Call findText on the widget.""" + self.search_displayed = True + + def wrapped_callback(found): + """Wrap the callback to do debug logging.""" + found_text = 'found' if found else "didn't find" + if flags: + flag_text = 'with flags {}'.format(debug.qflags_key( + QWebEnginePage, flags, klass=QWebEnginePage.FindFlag)) + else: + flag_text = '' + log.webview.debug(' '.join([caller, found_text, text, flag_text]) + .strip()) + if callback is not None: + callback(found) + self._widget.findText(text, flags, wrapped_callback) def search(self, text, *, ignore_case=False, reverse=False, result_cb=None): @@ -148,9 +166,10 @@ class WebEngineSearch(browsertab.AbstractSearch): self.text = text self._flags = flags - self._find(text, flags, result_cb) + self._find(text, flags, result_cb, 'search') def clear(self): + self.search_displayed = False self._widget.findText('') def prev_result(self, *, result_cb=None): @@ -160,10 +179,10 @@ class WebEngineSearch(browsertab.AbstractSearch): flags &= ~QWebEnginePage.FindBackward else: flags |= QWebEnginePage.FindBackward - self._find(self.text, flags, result_cb) + self._find(self.text, flags, result_cb, 'prev_result') def next_result(self, *, result_cb=None): - self._find(self.text, self._flags, result_cb) + self._find(self.text, self._flags, result_cb, 'next_result') class WebEngineCaret(browsertab.AbstractCaret): @@ -237,8 +256,47 @@ class WebEngineCaret(browsertab.AbstractCaret): raise browsertab.UnsupportedOperationError return self._widget.selectedText() + def _follow_selected_cb(self, js_elem, tab=False): + """Callback for javascript which clicks the selected element. + + Args: + js_elem: The element serialized from javascript. + tab: Open in a new tab. + """ + if js_elem is None: + return + assert isinstance(js_elem, dict), js_elem + elem = webengineelem.WebEngineElement(js_elem, tab=self._tab) + if tab: + click_type = usertypes.ClickTarget.tab + else: + click_type = usertypes.ClickTarget.normal + + # Only click if we see a link + if elem.is_link(): + log.webview.debug("Found link in selection, clicking. ClickTarget " + "{}, elem {}".format(click_type, elem)) + elem.click(click_type) + def follow_selected(self, *, tab=False): - log.stub() + if self._tab.search.search_displayed: + # We are currently in search mode. + # let's click the link via a fake-click + # https://bugreports.qt.io/browse/QTBUG-60673 + self._tab.search.clear() + + log.webview.debug("Clicking a searched link via fake key press.") + # send a fake enter, clicking the orange selection box + if tab: + self._tab.key_press(Qt.Key_Enter, modifier=Qt.ControlModifier) + else: + self._tab.key_press(Qt.Key_Enter) + + else: + # click an existing blue selection + js_code = javascript.assemble('webelem', 'find_selected_link') + self._tab.run_js_async(js_code, lambda jsret: + self._follow_selected_cb(jsret, tab)) class WebEngineScroller(browsertab.AbstractScroller): @@ -256,13 +314,10 @@ class WebEngineScroller(browsertab.AbstractScroller): page = widget.page() page.scrollPositionChanged.connect(self._update_pos) - def _key_press(self, key, count=1): + def _repeated_key_press(self, key, count=1, modifier=Qt.NoModifier): + """Send count fake key presses to this scroller's WebEngineTab.""" 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) - self._tab.send_event(press_evt) - self._tab.send_event(release_evt) + self._tab.key_press(key, modifier) @pyqtSlot() def _update_pos(self): @@ -289,7 +344,7 @@ class WebEngineScroller(browsertab.AbstractScroller): else: perc_y = min(100, round(100 / dy * jsret['px']['y'])) - self._at_bottom = dy >= jsret['px']['y'] + self._at_bottom = math.ceil(jsret['px']['y']) >= dy self._pos_perc = perc_x, perc_y self.perc_changed.emit(*self._pos_perc) @@ -319,28 +374,28 @@ class WebEngineScroller(browsertab.AbstractScroller): self._tab.run_js_async(js_code) def up(self, count=1): - self._key_press(Qt.Key_Up, count) + self._repeated_key_press(Qt.Key_Up, count) def down(self, count=1): - self._key_press(Qt.Key_Down, count) + self._repeated_key_press(Qt.Key_Down, count) def left(self, count=1): - self._key_press(Qt.Key_Left, count) + self._repeated_key_press(Qt.Key_Left, count) def right(self, count=1): - self._key_press(Qt.Key_Right, count) + self._repeated_key_press(Qt.Key_Right, count) def top(self): - self._key_press(Qt.Key_Home) + self._tab.key_press(Qt.Key_Home) def bottom(self): - self._key_press(Qt.Key_End) + self._tab.key_press(Qt.Key_End) def page_up(self, count=1): - self._key_press(Qt.Key_PageUp, count) + self._repeated_key_press(Qt.Key_PageUp, count) def page_down(self, count=1): - self._key_press(Qt.Key_PageDown, count) + self._repeated_key_press(Qt.Key_PageDown, count) def at_top(self): return self.pos_px().y() == 0 @@ -356,29 +411,28 @@ class WebEngineHistory(browsertab.AbstractHistory): def current_idx(self): return self._history.currentItemIndex() - def back(self): - self._history.back() - - def forward(self): - self._history.forward() - def can_go_back(self): return self._history.canGoBack() def can_go_forward(self): return self._history.canGoForward() + def _item_at(self, i): + return self._history.itemAt(i) + + def _go_to_item(self, item): + return self._history.goToItem(item) + def serialize(self): - # WORKAROUND (remove this when we bump the requirements to 5.9) - # https://bugreports.qt.io/browse/QTBUG-59599 - if self._history.count() == 0: - raise browsertab.WebTabError("Can't serialize page without " - "history!") - # WORKAROUND (FIXME: remove this when we bump the requirements to 5.9?) - # https://github.com/qutebrowser/qutebrowser/issues/2289 - scheme = self._history.currentItem().url().scheme() - if scheme in ['view-source', 'chrome']: - raise browsertab.WebTabError("Can't serialize special URL!") + if not qtutils.version_check('5.9'): + # WORKAROUND for + # https://github.com/qutebrowser/qutebrowser/issues/2289 + # Don't use the history's currentItem here, because of + # https://bugreports.qt.io/browse/QTBUG-59599 and because it doesn't + # contain view-source. + scheme = self._tab.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): @@ -448,18 +502,18 @@ class WebEngineElements(browsertab.AbstractElements): callback(elem) def find_css(self, selector, callback, *, only_visible=False): - js_code = javascript.assemble('webelem', 'find_all', selector, + js_code = javascript.assemble('webelem', 'find_css', selector, only_visible) js_cb = functools.partial(self._js_cb_multiple, callback) self._tab.run_js_async(js_code, js_cb) def find_id(self, elem_id, callback): - js_code = javascript.assemble('webelem', 'element_by_id', elem_id) + js_code = javascript.assemble('webelem', 'find_id', elem_id) js_cb = functools.partial(self._js_cb_single, callback) self._tab.run_js_async(js_code, js_cb) def find_focused(self, callback): - js_code = javascript.assemble('webelem', 'focus_element') + js_code = javascript.assemble('webelem', 'find_focused') js_cb = functools.partial(self._js_cb_single, callback) self._tab.run_js_async(js_code, js_cb) @@ -467,7 +521,7 @@ class WebEngineElements(browsertab.AbstractElements): assert pos.x() >= 0 assert pos.y() >= 0 pos /= self._tab.zoom.factor() - js_code = javascript.assemble('webelem', 'element_at_pos', + js_code = javascript.assemble('webelem', 'find_at_pos', pos.x(), pos.y()) js_cb = functools.partial(self._js_cb_single, callback) self._tab.run_js_async(js_code, js_cb) @@ -477,10 +531,11 @@ class WebEngineTab(browsertab.AbstractTab): """A QtWebEngine tab in the browser.""" - def __init__(self, win_id, mode_manager, parent=None): + def __init__(self, *, win_id, mode_manager, private, parent=None): super().__init__(win_id=win_id, mode_manager=mode_manager, - parent=parent) - widget = webview.WebEngineView(tabdata=self.data, win_id=win_id) + private=private, parent=parent) + widget = webview.WebEngineView(tabdata=self.data, win_id=win_id, + private=private) self.history = WebEngineHistory(self) self.scroller = WebEngineScroller(self, parent=self) self.caret = WebEngineCaret(win_id=win_id, mode_manager=mode_manager, @@ -561,9 +616,11 @@ class WebEngineTab(browsertab.AbstractTab): def shutdown(self): self.shutting_down.emit() - # WORKAROUND for - # https://bugreports.qt.io/browse/QTBUG-58563 - self.search.clear() + self.action.exit_fullscreen() + if qtutils.version_check('5.8', exact=True): + # WORKAROUND for + # https://bugreports.qt.io/browse/QTBUG-58563 + self.search.clear() self._widget.shutdown() def reload(self, *, force=False): @@ -582,14 +639,12 @@ class WebEngineTab(browsertab.AbstractTab): def icon(self): return self._widget.icon() - def set_html(self, html, base_url=None): + def set_html(self, html, base_url=QUrl()): # 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): @@ -601,6 +656,13 @@ class WebEngineTab(browsertab.AbstractTab): def clear_ssl_errors(self): raise browsertab.UnsupportedOperationError + def key_press(self, key, modifier=Qt.NoModifier): + press_evt = QKeyEvent(QEvent.KeyPress, key, modifier, 0, 0, 0) + release_evt = QKeyEvent(QEvent.KeyRelease, key, modifier, + 0, 0, 0) + self.send_event(press_evt) + self.send_event(release_evt) + @pyqtSlot() def _on_history_trigger(self): url = self.url() @@ -644,12 +706,24 @@ class WebEngineTab(browsertab.AbstractTab): def _on_fullscreen_requested(self, request): request.accept() on = request.toggleOn() + + self.data.fullscreen = on self.fullscreen_requested.emit(on) if on: notification = miscwidgets.FullscreenNotification(self) notification.show() notification.set_timeout(3000) + @pyqtSlot() + def _on_load_started(self): + """Clear search when a new load is started if needed.""" + if (qtutils.version_check('5.9') and + not qtutils.version_check('5.9.2')): + # WORKAROUND for + # https://bugreports.qt.io/browse/QTBUG-61506 + self.search.clear() + super()._on_load_started() + @pyqtSlot(QWebEnginePage.RenderProcessTerminationStatus, int) def _on_render_process_terminated(self, status, exitcode): """Show an error when the renderer process terminated.""" diff --git a/qutebrowser/browser/webengine/webview.py b/qutebrowser/browser/webengine/webview.py index 67e5fc259..fd6fc99cb 100644 --- a/qutebrowser/browser/webengine/webview.py +++ b/qutebrowser/browser/webengine/webview.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -23,12 +23,10 @@ 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 from qutebrowser.browser import shared -from qutebrowser.browser.webengine import certificateerror +from qutebrowser.browser.webengine import certificateerror, webenginesettings from qutebrowser.config import config from qutebrowser.utils import (log, debug, usertypes, jinja, urlutils, message, objreg) @@ -38,13 +36,19 @@ class WebEngineView(QWebEngineView): """Custom QWebEngineView subclass with qutebrowser-specific features.""" - def __init__(self, tabdata, win_id, parent=None): + def __init__(self, *, tabdata, win_id, private, parent=None): super().__init__(parent) self._win_id = win_id self._tabdata = tabdata theme_color = self.style().standardPalette().color(QPalette.Base) - page = WebEnginePage(theme_color=theme_color, parent=self) + if private: + profile = webenginesettings.private_profile + assert profile.isOffTheRecord() + else: + profile = webenginesettings.default_profile + page = WebEnginePage(theme_color=theme_color, profile=profile, + parent=self) self.setPage(page) def shutdown(self): @@ -124,8 +128,8 @@ class WebEnginePage(QWebEnginePage): certificate_error = pyqtSignal() shutting_down = pyqtSignal() - def __init__(self, theme_color, parent=None): - super().__init__(parent) + def __init__(self, *, theme_color, profile, parent=None): + super().__init__(profile, parent) self._is_shutting_down = False self.featurePermissionRequested.connect( self._on_feature_permission_requested) diff --git a/qutebrowser/browser/webkit/__init__.py b/qutebrowser/browser/webkit/__init__.py index b67442be7..93b53cdba 100644 --- a/qutebrowser/browser/webkit/__init__.py +++ b/qutebrowser/browser/webkit/__init__.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/webkit/cache.py b/qutebrowser/browser/webkit/cache.py index 860a532b0..ae717c956 100644 --- a/qutebrowser/browser/webkit/cache.py +++ b/qutebrowser/browser/webkit/cache.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -21,161 +21,35 @@ import os.path -from PyQt5.QtCore import pyqtSlot -from PyQt5.QtNetwork import QNetworkDiskCache, QNetworkCacheMetaData +from PyQt5.QtNetwork import QNetworkDiskCache from qutebrowser.config import config -from qutebrowser.utils import utils, objreg +from qutebrowser.utils import utils, objreg, qtutils class DiskCache(QNetworkDiskCache): - """Disk cache which sets correct cache dir and size. - - Attributes: - _activated: Whether the cache should be used. - _cache_dir: The base directory for cache files (standarddir.cache()) - """ + """Disk cache which sets correct cache dir and size.""" def __init__(self, cache_dir, parent=None): super().__init__(parent) - self._cache_dir = cache_dir - self._maybe_activate() - objreg.get('config').changed.connect(self.on_config_changed) + self.setCacheDirectory(os.path.join(cache_dir, 'http')) + self._set_cache_size() + objreg.get('config').changed.connect(self._set_cache_size) def __repr__(self): return utils.get_repr(self, size=self.cacheSize(), maxsize=self.maximumCacheSize(), path=self.cacheDirectory()) + @config.change_filter('storage', 'cache-size') 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 + # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-59909 + if (qtutils.version_check('5.7.1') and + not qtutils.version_check('5.9')): # pragma: no cover + size = 0 self.setMaximumCacheSize(size) - - def _maybe_activate(self): - """Activate/deactivate the cache based on the config.""" - if config.get('general', 'private-browsing'): - self._activated = False - else: - self._activated = True - self.setCacheDirectory(os.path.join(self._cache_dir, 'http')) - 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._set_cache_size() - elif (section, option) == ('general', # pragma: no branch - 'private-browsing'): - self._maybe_activate() - - def cacheSize(self): - """Return the current size taken up by the cache. - - Return: - An int. - """ - if self._activated: - return super().cacheSize() - else: - return 0 - - def fileMetaData(self, filename): - """Return the QNetworkCacheMetaData for the cache file filename. - - Args: - filename: The file name as a string. - - Return: - A QNetworkCacheMetaData object. - """ - if self._activated: - return super().fileMetaData(filename) - else: - return QNetworkCacheMetaData() - - def data(self, url): - """Return the data associated with url. - - Args: - url: A QUrl. - - return: - A QIODevice or None. - """ - if self._activated: - return super().data(url) - else: - return None - - def insert(self, device): - """Insert the data in device and the prepared meta data into the cache. - - Args: - device: A QIODevice. - """ - if self._activated: - super().insert(device) - else: - return None - - def metaData(self, url): - """Return the meta data for the url url. - - Args: - url: A QUrl. - - Return: - A QNetworkCacheMetaData object. - """ - if self._activated: - return super().metaData(url) - else: - return QNetworkCacheMetaData() - - def prepare(self, meta_data): - """Return the device that should be populated with the data. - - Args: - meta_data: A QNetworkCacheMetaData object. - - Return: - A QIODevice or None. - """ - if self._activated: - return super().prepare(meta_data) - else: - return None - - def remove(self, url): - """Remove the cache entry for url. - - Return: - True on success, False otherwise. - """ - if self._activated: - return super().remove(url) - else: - return False - - def updateMetaData(self, meta_data): - """Update the cache meta date for the meta_data's url to meta_data. - - Args: - meta_data: A QNetworkCacheMetaData object. - """ - if self._activated: - super().updateMetaData(meta_data) - else: - return - - def clear(self): - """Remove all items from the cache.""" - if self._activated: - super().clear() - else: - return diff --git a/qutebrowser/browser/webkit/certificateerror.py b/qutebrowser/browser/webkit/certificateerror.py index 41cf2866f..d02ded76c 100644 --- a/qutebrowser/browser/webkit/certificateerror.py +++ b/qutebrowser/browser/webkit/certificateerror.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -41,7 +41,7 @@ class CertificateErrorWrapper(usertypes.AbstractCertificateErrorWrapper): try: # Qt >= 5.4 return hash(self._error) - except TypeError: + except TypeError: # pragma: no cover return hash((self._error.certificate().toDer(), self._error.error())) diff --git a/qutebrowser/browser/webkit/cookies.py b/qutebrowser/browser/webkit/cookies.py index 113eb661a..79f7a67fa 100644 --- a/qutebrowser/browser/webkit/cookies.py +++ b/qutebrowser/browser/webkit/cookies.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/webkit/http.py b/qutebrowser/browser/webkit/http.py index f55542c67..08cad7a44 100644 --- a/qutebrowser/browser/webkit/http.py +++ b/qutebrowser/browser/webkit/http.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/webkit/mhtml.py b/qutebrowser/browser/webkit/mhtml.py index bc9ea2695..ed357f2bd 100644 --- a/qutebrowser/browser/webkit/mhtml.py +++ b/qutebrowser/browser/webkit/mhtml.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Daniel Schadt +# Copyright 2015-2017 Daniel Schadt # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/webkit/network/filescheme.py b/qutebrowser/browser/webkit/network/filescheme.py index cd0a6d489..2f3a3ff18 100644 --- a/qutebrowser/browser/webkit/network/filescheme.py +++ b/qutebrowser/browser/webkit/network/filescheme.py @@ -1,7 +1,7 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) -# Copyright 2015-2016 Antoni Boucher (antoyo) +# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Antoni Boucher (antoyo) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/webkit/network/networkmanager.py b/qutebrowser/browser/webkit/network/networkmanager.py index 779d778bc..9d4c1b317 100644 --- a/qutebrowser/browser/webkit/network/networkmanager.py +++ b/qutebrowser/browser/webkit/network/networkmanager.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -121,13 +121,13 @@ class NetworkManager(QNetworkAccessManager): reparented to the DownloadManager. This counts the still running downloads, so the QNAM can clean itself up when this reaches zero again. - _requests: Pending requests. _scheme_handlers: A dictionary (scheme -> handler) of supported custom schemes. _win_id: The window ID this NetworkManager is associated with. _tab_id: The tab ID this NetworkManager is associated with. _rejected_ssl_errors: A {QUrl: [SslError]} dict of rejected errors. _accepted_ssl_errors: A {QUrl: [SslError]} dict of accepted errors. + _private: Whether we're in private browsing mode. Signals: shutting_down: Emitted when the QNAM is shutting down. @@ -135,7 +135,7 @@ class NetworkManager(QNetworkAccessManager): shutting_down = pyqtSignal() - def __init__(self, win_id, tab_id, parent=None): + def __init__(self, *, win_id, tab_id, private, parent=None): log.init.debug("Initializing NetworkManager") with log.disable_qt_msghandler(): # WORKAROUND for a hang when a message is printed - See: @@ -145,12 +145,12 @@ class NetworkManager(QNetworkAccessManager): self.adopted_downloads = 0 self._win_id = win_id self._tab_id = tab_id - self._requests = [] + self._private = private self._scheme_handlers = { 'qute': webkitqutescheme.QuteSchemeHandler(win_id), 'file': filescheme.FileSchemeHandler(win_id), } - self._set_cookiejar(private=config.get('general', 'private-browsing')) + self._set_cookiejar() self._set_cache() self.sslErrors.connect(self.on_ssl_errors) self._rejected_ssl_errors = collections.defaultdict(list) @@ -158,15 +158,10 @@ class NetworkManager(QNetworkAccessManager): self.authenticationRequired.connect(self.on_authentication_required) self.proxyAuthenticationRequired.connect( self.on_proxy_authentication_required) - objreg.get('config').changed.connect(self.on_config_changed) - def _set_cookiejar(self, private=False): - """Set the cookie jar of the NetworkManager correctly. - - Args: - private: Whether we're currently in private browsing mode. - """ - if private: + def _set_cookiejar(self): + """Set the cookie jar of the NetworkManager correctly.""" + if self._private: cookie_jar = objreg.get('ram-cookie-jar') else: cookie_jar = objreg.get('cookie-jar') @@ -178,11 +173,9 @@ class NetworkManager(QNetworkAccessManager): cookie_jar.setParent(app) def _set_cache(self): - """Set the cache of the NetworkManager correctly. - - We can't switch the whole cache in private mode because QNAM would - delete the old cache. - """ + """Set the cache of the NetworkManager correctly.""" + if self._private: + return # We have a shared cache - we restore its parent so we don't take # ownership of it. app = QCoreApplication.instance() @@ -206,9 +199,6 @@ class NetworkManager(QNetworkAccessManager): def shutdown(self): """Abort all running requests.""" self.setNetworkAccessible(QNetworkAccessManager.NotAccessible) - for request in self._requests: - request.abort() - request.deleteLater() self.shutting_down.emit() # No @pyqtSlot here, see @@ -324,17 +314,6 @@ class NetworkManager(QNetworkAccessManager): authenticator.setPassword(answer.password) _proxy_auth_cache[proxy_id] = answer - @config.change_filter('general', 'private-browsing') - def on_config_changed(self): - """Set cookie jar when entering/leaving private browsing mode.""" - private_browsing = config.get('general', 'private-browsing') - if private_browsing: - # switched from normal mode to private mode - self._set_cookiejar(private=True) - else: - # switched from private mode to normal mode - self._set_cookiejar() - @pyqtSlot() def on_adopted_download_destroyed(self): """Check if we can clean up if an adopted download was destroyed. @@ -386,9 +365,6 @@ class NetworkManager(QNetworkAccessManager): def createRequest(self, op, req, outgoing_data): """Return a new QNetworkReply object. - Extend QNetworkAccessManager::createRequest to save requests in - self._requests and handle custom schemes. - Args: op: Operation op req: const QNetworkRequest & req @@ -416,8 +392,7 @@ class NetworkManager(QNetworkAccessManager): req.setRawHeader(header, value) host_blocker = objreg.get('host-blocker') - if (op == QNetworkAccessManager.GetOperation and - host_blocker.is_blocked(req.url())): + if host_blocker.is_blocked(req.url()): log.webview.info("Request to {} blocked by host blocker.".format( req.url().host())) return networkreply.ErrorNetworkReply( @@ -454,6 +429,4 @@ class NetworkManager(QNetworkAccessManager): reply = super().createRequest(op, req, outgoing_data) else: reply = super().createRequest(op, req, outgoing_data) - self._requests.append(reply) - reply.destroyed.connect(self._requests.remove) return reply diff --git a/qutebrowser/browser/webkit/network/networkreply.py b/qutebrowser/browser/webkit/network/networkreply.py index 2cc5727be..a4a4f59ca 100644 --- a/qutebrowser/browser/webkit/network/networkreply.py +++ b/qutebrowser/browser/webkit/network/networkreply.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # Based on the Eric5 helpviewer, # Copyright (c) 2009 - 2014 Detlev Offenbach @@ -19,6 +19,10 @@ # # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . +# +# For some reason, a segfault will be triggered if the unnecessary lambdas in +# this file aren't there. +# pylint: disable=unnecessary-lambda """Special network replies..""" @@ -114,9 +118,6 @@ class ErrorNetworkReply(QNetworkReply): # the device to avoid getting a warning. self.setOpenMode(QIODevice.ReadOnly) self.setError(error, errorstring) - # For some reason, a segfault will be triggered if these lambdas aren't - # there. - # pylint: disable=unnecessary-lambda QTimer.singleShot(0, lambda: self.error.emit(error)) QTimer.singleShot(0, lambda: self.finished.emit()) @@ -137,3 +138,20 @@ class ErrorNetworkReply(QNetworkReply): def isRunning(self): return False + + +class RedirectNetworkReply(QNetworkReply): + + """A reply which redirects to the given URL.""" + + def __init__(self, new_url, parent=None): + super().__init__(parent) + self.setAttribute(QNetworkRequest.RedirectionTargetAttribute, new_url) + QTimer.singleShot(0, lambda: self.finished.emit()) + + def abort(self): + """Called when there's e.g. a redirection limit.""" + pass + + def readData(self, _maxlen): + return bytes() diff --git a/qutebrowser/browser/webkit/network/schemehandler.py b/qutebrowser/browser/webkit/network/schemehandler.py index 9975db121..c6337efa3 100644 --- a/qutebrowser/browser/webkit/network/schemehandler.py +++ b/qutebrowser/browser/webkit/network/schemehandler.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # Based on the Eric5 helpviewer, # Copyright (c) 2009 - 2014 Detlev Offenbach diff --git a/qutebrowser/browser/webkit/network/webkitqutescheme.py b/qutebrowser/browser/webkit/network/webkitqutescheme.py index 61ef760bc..6e83e60a0 100644 --- a/qutebrowser/browser/webkit/network/webkitqutescheme.py +++ b/qutebrowser/browser/webkit/network/webkitqutescheme.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -17,7 +17,7 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -"""QtWebKit specific qute:* handlers and glue code.""" +"""QtWebKit specific qute://* handlers and glue code.""" import mimetypes import functools @@ -28,13 +28,13 @@ from PyQt5.QtNetwork import QNetworkReply from qutebrowser.browser import pdfjs, qutescheme from qutebrowser.browser.webkit.network import schemehandler, networkreply -from qutebrowser.utils import jinja, log, message, objreg, usertypes +from qutebrowser.utils import jinja, log, message, objreg, usertypes, qtutils from qutebrowser.config import configexc, configdata class QuteSchemeHandler(schemehandler.SchemeHandler): - """Scheme handler for qute: URLs.""" + """Scheme handler for qute:// URLs.""" def createRequest(self, _op, request, _outgoing_data): """Create a new request. @@ -62,6 +62,9 @@ class QuteSchemeHandler(schemehandler.SchemeHandler): except qutescheme.QuteSchemeError as e: return networkreply.ErrorNetworkReply(request, e.errorstring, e.error, self.parent()) + except qutescheme.Redirect as e: + qtutils.ensure_valid(e.url) + return networkreply.RedirectNetworkReply(e.url, self.parent()) return networkreply.FixedDataNetworkReply(request, data, mimetype, self.parent()) @@ -69,15 +72,15 @@ class QuteSchemeHandler(schemehandler.SchemeHandler): class JSBridge(QObject): - """Javascript-bridge for special qute:... pages.""" + """Javascript-bridge for special qute://... pages.""" @pyqtSlot(str, str, str) def set(self, sectname, optname, value): - """Slot to set a setting from qute:settings.""" + """Slot to set a setting from qute://settings.""" # https://github.com/qutebrowser/qutebrowser/issues/727 if ((sectname, optname) == ('content', 'allow-javascript') and value == 'false'): - message.error("Refusing to disable javascript via qute:settings " + message.error("Refusing to disable javascript via qute://settings " "as it needs javascript support.") return try: @@ -88,7 +91,7 @@ class JSBridge(QObject): @qutescheme.add_handler('settings', backend=usertypes.Backend.QtWebKit) def qute_settings(_url): - """Handler for qute:settings. View/change qute configuration.""" + """Handler for qute://settings. View/change qute configuration.""" config_getter = functools.partial(objreg.get('config').get, raw=True) html = jinja.render('settings.html', title='settings', config=configdata, confget=config_getter) diff --git a/qutebrowser/browser/webkit/rfc6266.py b/qutebrowser/browser/webkit/rfc6266.py index 89f4c78c6..c3c8f7a4b 100644 --- a/qutebrowser/browser/webkit/rfc6266.py +++ b/qutebrowser/browser/webkit/rfc6266.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -286,9 +286,6 @@ def normalize_ws(text): def parse_headers(content_disposition): """Build a _ContentDisposition from header values.""" - # WORKAROUND for https://bitbucket.org/logilab/pylint/issue/492/ - # pylint: disable=no-member - # We allow non-ascii here (it will only be parsed inside of qdtext, and # rejected by the grammar if it appears in other places), although parsing # it can be ambiguous. Parsing it ensures that a non-ambiguous filename* diff --git a/qutebrowser/browser/webkit/tabhistory.py b/qutebrowser/browser/webkit/tabhistory.py index d2efca257..19e4ef15c 100644 --- a/qutebrowser/browser/webkit/tabhistory.py +++ b/qutebrowser/browser/webkit/tabhistory.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -21,7 +21,6 @@ from PyQt5.QtCore import QByteArray, QDataStream, QIODevice, QUrl -from PyQt5.QtWebKit import qWebKitVersion from qutebrowser.utils import qtutils @@ -181,7 +180,7 @@ def serialize(items): else: current_idx = 0 - if qtutils.is_qtwebkit_ng(qWebKitVersion()): + if qtutils.is_qtwebkit_ng(): _serialize_ng(items, current_idx, stream) else: _serialize_old(items, current_idx, stream) diff --git a/qutebrowser/browser/webkit/webkitelem.py b/qutebrowser/browser/webkit/webkitelem.py index 34209c290..b184f9905 100644 --- a/qutebrowser/browser/webkit/webkitelem.py +++ b/qutebrowser/browser/webkit/webkitelem.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -112,7 +112,9 @@ class WebKitElement(webelem.AbstractWebElement): def value(self): self._check_vanished() - return self._elem.evaluateJavaScript('this.value') + val = self._elem.evaluateJavaScript('this.value') + assert isinstance(val, (int, float, str, type(None))), val + return val def set_value(self, value): self._check_vanished() @@ -283,8 +285,7 @@ class WebKitElement(webelem.AbstractWebElement): for _ in range(5): if elem is None: break - tag = elem.tag_name() - if tag == 'a' or tag == 'area': + if elem.is_link(): if elem.get('target', None) == '_blank': elem['target'] = '_top' break diff --git a/qutebrowser/browser/webkit/webkithistory.py b/qutebrowser/browser/webkit/webkithistory.py index abd80eb39..0edbb3fa3 100644 --- a/qutebrowser/browser/webkit/webkithistory.py +++ b/qutebrowser/browser/webkit/webkithistory.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -19,9 +19,12 @@ """QtWebKit specific part of history.""" +import functools from PyQt5.QtWebKit import QWebHistoryInterface +from qutebrowser.utils import debug + class WebHistoryInterface(QWebHistoryInterface): @@ -34,11 +37,13 @@ class WebHistoryInterface(QWebHistoryInterface): def __init__(self, webhistory, parent=None): super().__init__(parent) self._history = webhistory + self._history.changed.connect(self.historyContains.cache_clear) def addHistoryEntry(self, url_string): """Required for a QWebHistoryInterface impl, obsoleted by add_url.""" pass + @functools.lru_cache(maxsize=32768) def historyContains(self, url_string): """Called by WebKit to determine if a URL is contained in the history. @@ -48,7 +53,8 @@ class WebHistoryInterface(QWebHistoryInterface): Return: True if the url is in the history, False otherwise. """ - return url_string in self._history.history_dict + with debug.log_time('sql', 'historyContains'): + return url_string in self._history def init(history): diff --git a/qutebrowser/browser/webkit/webkitinspector.py b/qutebrowser/browser/webkit/webkitinspector.py index 9e612b28e..9a056a896 100644 --- a/qutebrowser/browser/webkit/webkitinspector.py +++ b/qutebrowser/browser/webkit/webkitinspector.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/webkit/webkitsettings.py b/qutebrowser/browser/webkit/webkitsettings.py index caeef296f..dd251949f 100644 --- a/qutebrowser/browser/webkit/webkitsettings.py +++ b/qutebrowser/browser/webkit/webkitsettings.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -17,6 +17,9 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . +# We get various "abstract but not overridden" warnings +# pylint: disable=abstract-method + """Bridge from QWebSettings to our own settings. Module attributes: @@ -26,43 +29,66 @@ Module attributes: import os.path -from PyQt5.QtWebKit import QWebSettings, qWebKitVersion +from PyQt5.QtGui import QFont +from PyQt5.QtWebKit import QWebSettings from qutebrowser.config import config, websettings -from qutebrowser.utils import standarddir, objreg, urlutils, qtutils, message +from qutebrowser.utils import standarddir, objreg, urlutils, qtutils from qutebrowser.browser import shared -class Attribute(websettings.Attribute): +class Base(websettings.Base): + + """Base settings class with appropriate _get_global_settings.""" + + def _get_global_settings(self): + return [QWebSettings.globalSettings()] + + +class Attribute(Base, websettings.Attribute): """A setting set via QWebSettings::setAttribute.""" - GLOBAL_SETTINGS = QWebSettings.globalSettings ENUM_BASE = QWebSettings -class Setter(websettings.Setter): +class Setter(Base, websettings.Setter): - """A setting set via QWebSettings getter/setter methods.""" + """A setting set via a QWebSettings setter method.""" - GLOBAL_SETTINGS = QWebSettings.globalSettings + pass -class NullStringSetter(websettings.NullStringSetter): +class StaticSetter(Base, websettings.StaticSetter): - """A setter for settings requiring a null QString as default.""" + """A setting set via a static QWebSettings setter method.""" - GLOBAL_SETTINGS = QWebSettings.globalSettings + pass -class StaticSetter(websettings.StaticSetter): +class FontFamilySetter(Base, websettings.FontFamilySetter): - """A setting set via static QWebSettings getter/setter methods.""" + """A setter for a font family. - GLOBAL_SETTINGS = QWebSettings.globalSettings + Gets the default value from QFont. + """ + + def __init__(self, font): + # Mapping from QWebSettings::QWebSettings() in + # qtwebkit/Source/WebKit/qt/Api/qwebsettings.cpp + font_to_qfont = { + QWebSettings.StandardFont: QFont.Serif, + QWebSettings.FixedFont: QFont.Monospace, + QWebSettings.SerifFont: QFont.Serif, + QWebSettings.SansSerifFont: QFont.SansSerif, + QWebSettings.CursiveFont: QFont.Cursive, + QWebSettings.FantasyFont: QFont.Fantasy, + } + super().__init__(setter=QWebSettings.setFontFamily, font=font, + qfont=font_to_qfont[font]) -class CookiePolicy(websettings.Base): +class CookiePolicy(Base): """The ThirdPartyCookiePolicy setting is different from other settings.""" @@ -73,12 +99,9 @@ class CookiePolicy(websettings.Base): 'no-unknown-3rdparty': QWebSettings.AllowThirdPartyWithExistingCookies, } - def get(self, settings=None): - return config.get('content', 'cookies-accept') - def _set(self, value, settings=None): - QWebSettings.globalSettings().setThirdPartyCookiePolicy( - self.MAPPING[value]) + for obj in self._get_settings(settings): + obj.setThirdPartyCookiePolicy(self.MAPPING[value]) def _set_user_stylesheet(): @@ -88,21 +111,9 @@ 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'): - _init_private_browsing() - elif section == 'ui' and option in ['hide-scrollbar', 'user-stylesheet']: + if section == 'ui' and option in ['hide-scrollbar', 'user-stylesheet']: _set_user_stylesheet() websettings.update_mappings(MAPPINGS, section, option) @@ -113,8 +124,7 @@ def init(_args): cache_path = standarddir.cache() data_path = standarddir.data() - _init_private_browsing() - + QWebSettings.setIconDatabasePath(standarddir.cache()) QWebSettings.setOfflineWebApplicationCachePath( os.path.join(cache_path, 'application-cache')) QWebSettings.globalSettings().setLocalStoragePath( @@ -122,6 +132,13 @@ def init(_args): QWebSettings.setOfflineStoragePath( os.path.join(data_path, 'offline-storage')) + if (config.get('general', 'private-browsing') and + not qtutils.version_check('5.4.2')): + # WORKAROUND for https://codereview.qt-project.org/#/c/108936/ + # Won't work when private browsing is not enabled globally, but that's + # the best we can do... + QWebSettings.setIconDatabasePath('') + websettings.init_mappings(MAPPINGS) _set_user_stylesheet() objreg.get('config').changed.connect(update_settings) @@ -146,14 +163,10 @@ MAPPINGS = { Attribute(QWebSettings.JavascriptCanCloseWindows), 'javascript-can-access-clipboard': Attribute(QWebSettings.JavascriptCanAccessClipboard), - #'allow-java': - # Attribute(QWebSettings.JavaEnabled), 'allow-plugins': Attribute(QWebSettings.PluginsEnabled), 'webgl': Attribute(QWebSettings.WebGLEnabled), - 'css-regions': - Attribute(QWebSettings.CSSRegionsEnabled), 'hyperlink-auditing': Attribute(QWebSettings.HyperlinkAuditingEnabled), 'local-content-can-access-remote-urls': @@ -175,44 +188,28 @@ MAPPINGS = { }, 'fonts': { 'web-family-standard': - Setter(getter=QWebSettings.fontFamily, - setter=QWebSettings.setFontFamily, - args=[QWebSettings.StandardFont]), + FontFamilySetter(QWebSettings.StandardFont), 'web-family-fixed': - Setter(getter=QWebSettings.fontFamily, - setter=QWebSettings.setFontFamily, - args=[QWebSettings.FixedFont]), + FontFamilySetter(QWebSettings.FixedFont), 'web-family-serif': - Setter(getter=QWebSettings.fontFamily, - setter=QWebSettings.setFontFamily, - args=[QWebSettings.SerifFont]), + FontFamilySetter(QWebSettings.SerifFont), 'web-family-sans-serif': - Setter(getter=QWebSettings.fontFamily, - setter=QWebSettings.setFontFamily, - args=[QWebSettings.SansSerifFont]), + FontFamilySetter(QWebSettings.SansSerifFont), 'web-family-cursive': - Setter(getter=QWebSettings.fontFamily, - setter=QWebSettings.setFontFamily, - args=[QWebSettings.CursiveFont]), + FontFamilySetter(QWebSettings.CursiveFont), 'web-family-fantasy': - Setter(getter=QWebSettings.fontFamily, - setter=QWebSettings.setFontFamily, - args=[QWebSettings.FantasyFont]), + FontFamilySetter(QWebSettings.FantasyFont), 'web-size-minimum': - Setter(getter=QWebSettings.fontSize, - setter=QWebSettings.setFontSize, + Setter(QWebSettings.setFontSize, args=[QWebSettings.MinimumFontSize]), 'web-size-minimum-logical': - Setter(getter=QWebSettings.fontSize, - setter=QWebSettings.setFontSize, + Setter(QWebSettings.setFontSize, args=[QWebSettings.MinimumLogicalFontSize]), 'web-size-default': - Setter(getter=QWebSettings.fontSize, - setter=QWebSettings.setFontSize, + Setter(QWebSettings.setFontSize, args=[QWebSettings.DefaultFontSize]), 'web-size-default-fixed': - Setter(getter=QWebSettings.fontSize, - setter=QWebSettings.setFontSize, + Setter(QWebSettings.setFontSize, args=[QWebSettings.DefaultFixedFontSize]), }, 'ui': { @@ -221,9 +218,6 @@ MAPPINGS = { 'frame-flattening': Attribute(QWebSettings.FrameFlatteningEnabled), # user-stylesheet is handled separately - 'css-media-type': - NullStringSetter(getter=QWebSettings.cssMediaType, - setter=QWebSettings.setCSSMediaType), 'smooth-scrolling': Attribute(QWebSettings.ScrollAnimatorEnabled), #'accelerated-compositing': @@ -232,40 +226,22 @@ MAPPINGS = { # Attribute(QWebSettings.TiledBackingStoreEnabled), }, 'storage': { - 'offline-storage-database': - Attribute(QWebSettings.OfflineStorageDatabaseEnabled), - 'offline-web-application-storage': + 'offline-web-application-cache': Attribute(QWebSettings.OfflineWebApplicationCacheEnabled), 'local-storage': - Attribute(QWebSettings.LocalStorageEnabled), + Attribute(QWebSettings.LocalStorageEnabled, + QWebSettings.OfflineStorageDatabaseEnabled), 'maximum-pages-in-cache': - StaticSetter(getter=QWebSettings.maximumPagesInCache, - setter=QWebSettings.setMaximumPagesInCache), - 'object-cache-capacities': - StaticSetter(getter=None, - setter=QWebSettings.setObjectCacheCapacities, - unpack=True), - 'offline-storage-default-quota': - StaticSetter(getter=QWebSettings.offlineStorageDefaultQuota, - setter=QWebSettings.setOfflineStorageDefaultQuota), - 'offline-web-application-cache-quota': - StaticSetter( - getter=QWebSettings.offlineWebApplicationCacheQuota, - setter=QWebSettings.setOfflineWebApplicationCacheQuota), + StaticSetter(QWebSettings.setMaximumPagesInCache), }, 'general': { - 'private-browsing': - Attribute(QWebSettings.PrivateBrowsingEnabled), 'developer-extras': Attribute(QWebSettings.DeveloperExtrasEnabled), 'print-element-backgrounds': Attribute(QWebSettings.PrintElementBackgrounds), 'xss-auditing': Attribute(QWebSettings.XSSAuditingEnabled), - 'site-specific-quirks': - Attribute(QWebSettings.SiteSpecificQuirksEnabled), 'default-encoding': - Setter(getter=QWebSettings.defaultTextEncoding, - setter=QWebSettings.setDefaultTextEncoding), + Setter(QWebSettings.setDefaultTextEncoding), } } diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py index ef427c3be..348d2c628 100644 --- a/qutebrowser/browser/webkit/webkittab.py +++ b/qutebrowser/browser/webkit/webkittab.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -33,21 +33,14 @@ 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 webkitqutescheme -from qutebrowser.utils import qtutils, objreg, usertypes, utils, log +from qutebrowser.utils import qtutils, objreg, usertypes, utils, log, debug def init(): """Initialize QtWebKit-specific modules.""" qapp = QApplication.instance() - - 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) @@ -57,6 +50,9 @@ class WebKitAction(browsertab.AbstractAction): """QtWebKit implementations related to web actions.""" + action_class = QWebPage + action_base = QWebPage.WebAction + def exit_fullscreen(self): raise browsertab.UnsupportedOperationError @@ -104,7 +100,7 @@ class WebKitSearch(browsertab.AbstractSearch): super().__init__(parent) self._flags = QWebPage.FindFlags(0) - def _call_cb(self, callback, found): + def _call_cb(self, callback, found, text, flags, caller): """Call the given callback if it's non-None. Delays the call via a QTimer so the website is re-rendered in between. @@ -112,17 +108,34 @@ class WebKitSearch(browsertab.AbstractSearch): Args: callback: What to call found: If the text was found + text: The text searched for + flags: The flags searched with + caller: Name of the caller. """ + found_text = 'found' if found else "didn't find" + # Removing FindWrapsAroundDocument to get the same logging as with + # QtWebEngine + debug_flags = debug.qflags_key( + QWebPage, flags & ~QWebPage.FindWrapsAroundDocument, + klass=QWebPage.FindFlag) + if debug_flags != '0x0000': + flag_text = 'with flags {}'.format(debug_flags) + else: + flag_text = '' + log.webview.debug(' '.join([caller, found_text, text, flag_text]) + .strip()) if callback is not None: QTimer.singleShot(0, functools.partial(callback, found)) def clear(self): + self.search_displayed = False # We first clear the marked text, then the highlights self._widget.findText('') self._widget.findText('', QWebPage.HighlightAllOccurrences) def search(self, text, *, ignore_case=False, reverse=False, result_cb=None): + self.search_displayed = True flags = QWebPage.FindWrapsAroundDocument if ignore_case == 'smart': if not text.islower(): @@ -137,13 +150,15 @@ class WebKitSearch(browsertab.AbstractSearch): self._widget.findText(text, flags | QWebPage.HighlightAllOccurrences) self.text = text self._flags = flags - self._call_cb(result_cb, found) + self._call_cb(result_cb, found, text, flags, 'search') def next_result(self, *, result_cb=None): + self.search_displayed = True found = self._widget.findText(self.text, self._flags) - self._call_cb(result_cb, found) + self._call_cb(result_cb, found, self.text, self._flags, 'next_result') def prev_result(self, *, result_cb=None): + self.search_displayed = True # The int() here makes sure we get a copy of the flags. flags = QWebPage.FindFlags(int(self._flags)) if flags & QWebPage.FindBackward: @@ -151,7 +166,7 @@ class WebKitSearch(browsertab.AbstractSearch): else: flags |= QWebPage.FindBackward found = self._widget.findText(self.text, flags) - self._call_cb(result_cb, found) + self._call_cb(result_cb, found, self.text, flags, 'prev_result') class WebKitCaret(browsertab.AbstractCaret): @@ -445,15 +460,11 @@ class WebKitScroller(browsertab.AbstractScroller): # self._widget.setFocus() 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) # Abort scrolling if the minimum/maximum was reached. if (getter is not None and frame.scrollBarValue(direction) == getter(direction)): return - self._widget.keyPressEvent(press_evt) - self._widget.keyReleaseEvent(release_evt) + self._tab.key_press(key) def up(self, count=1): self._key_press(Qt.Key_Up, count, 'scrollBarMinimum', Qt.Vertical) @@ -495,18 +506,18 @@ class WebKitHistory(browsertab.AbstractHistory): def current_idx(self): return self._history.currentItemIndex() - def back(self): - self._history.back() - - def forward(self): - self._history.forward() - def can_go_back(self): return self._history.canGoBack() def can_go_forward(self): return self._history.canGoForward() + def _item_at(self, i): + return self._history.itemAt(i) + + def _go_to_item(self, item): + return self._history.goToItem(item) + def serialize(self): return qtutils.serialize(self._history) @@ -611,10 +622,13 @@ class WebKitTab(browsertab.AbstractTab): """A QtWebKit tab in the browser.""" - def __init__(self, win_id, mode_manager, parent=None): + def __init__(self, *, win_id, mode_manager, private, parent=None): super().__init__(win_id=win_id, mode_manager=mode_manager, - parent=parent) - widget = webview.WebView(win_id, self.tab_id, tab=self) + private=private, parent=parent) + widget = webview.WebView(win_id=win_id, tab_id=self.tab_id, + private=private, tab=self) + if private: + self._make_private(widget) self.history = WebKitHistory(self) self.scroller = WebKitScroller(self, parent=self) self.caret = WebKitCaret(win_id=win_id, mode_manager=mode_manager, @@ -631,6 +645,10 @@ class WebKitTab(browsertab.AbstractTab): def _install_event_filter(self): self._widget.installEventFilter(self._mouse_event_filter) + def _make_private(self, widget): + settings = widget.settings() + settings.setAttribute(QWebSettings.PrivateBrowsingEnabled, True) + def openurl(self, url): self._openurl_prepare(url) self._widget.openurl(url) @@ -679,13 +697,20 @@ class WebKitTab(browsertab.AbstractTab): def clear_ssl_errors(self): self.networkaccessmanager().clear_all_ssl_errors() + def key_press(self, key, modifier=Qt.NoModifier): + press_evt = QKeyEvent(QEvent.KeyPress, key, modifier, 0, 0, 0) + release_evt = QKeyEvent(QEvent.KeyRelease, key, modifier, + 0, 0, 0) + self.send_event(press_evt) + self.send_event(release_evt) + @pyqtSlot() def _on_history_trigger(self): url = self.url() requested_url = self.url(requested=True) self.add_history_item.emit(url, requested_url, self.title()) - def set_html(self, html, base_url): + def set_html(self, html, base_url=QUrl()): self._widget.setHtml(html, base_url) def networkaccessmanager(self): diff --git a/qutebrowser/browser/webkit/webpage.py b/qutebrowser/browser/webkit/webpage.py index 7fc891c4c..acebf5cd3 100644 --- a/qutebrowser/browser/webkit/webpage.py +++ b/qutebrowser/browser/webkit/webpage.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -59,7 +59,7 @@ class BrowserPage(QWebPage): shutting_down = pyqtSignal() reloading = pyqtSignal(QUrl) - def __init__(self, win_id, tab_id, tabdata, parent=None): + def __init__(self, win_id, tab_id, tabdata, private, parent=None): super().__init__(parent) self._win_id = win_id self._tabdata = tabdata @@ -72,7 +72,7 @@ class BrowserPage(QWebPage): self.error_occurred = False self.open_target = usertypes.ClickTarget.normal self._networkmanager = networkmanager.NetworkManager( - win_id, tab_id, self) + win_id=win_id, tab_id=tab_id, private=private, parent=self) self.setNetworkAccessManager(self._networkmanager) self.setForwardUnsupportedContent(True) self.reloading.connect(self._networkmanager.clear_rejected_ssl_errors) diff --git a/qutebrowser/browser/webkit/webview.py b/qutebrowser/browser/webkit/webview.py index 12670be4f..99980b4b1 100644 --- a/qutebrowser/browser/webkit/webview.py +++ b/qutebrowser/browser/webkit/webview.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -55,7 +55,7 @@ class WebView(QWebView): scroll_pos_changed = pyqtSignal(int, int) shutting_down = pyqtSignal() - def __init__(self, win_id, tab_id, tab, parent=None): + def __init__(self, *, win_id, tab_id, tab, private, parent=None): super().__init__(parent) if sys.platform == 'darwin' and qtutils.version_check('5.4'): # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-42948 @@ -71,7 +71,8 @@ class WebView(QWebView): self._set_bg_color() self._tab_id = tab_id - page = webpage.BrowserPage(self.win_id, self._tab_id, tab.data, + page = webpage.BrowserPage(win_id=self.win_id, tab_id=self._tab_id, + tabdata=tab.data, private=private, parent=self) try: @@ -140,7 +141,7 @@ class WebView(QWebView): @pyqtSlot() def add_js_bridge(self): - """Add the javascript bridge for qute:... pages.""" + """Add the javascript bridge for qute://... pages.""" frame = self.sender() if not isinstance(frame, QWebFrame): log.webview.error("Got non-QWebFrame {!r} in " diff --git a/qutebrowser/commands/__init__.py b/qutebrowser/commands/__init__.py index 70f058d82..7bc59ae40 100644 --- a/qutebrowser/commands/__init__.py +++ b/qutebrowser/commands/__init__.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/commands/argparser.py b/qutebrowser/commands/argparser.py index e4c6378bd..9dfe841ce 100644 --- a/qutebrowser/commands/argparser.py +++ b/qutebrowser/commands/argparser.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -76,11 +76,11 @@ class ArgumentParser(argparse.ArgumentParser): self.name = name super().__init__(*args, add_help=False, prog=name, **kwargs) - def exit(self, status=0, msg=None): - raise ArgumentParserExit(status, msg) + def exit(self, status=0, message=None): + raise ArgumentParserExit(status, message) - def error(self, msg): - raise ArgumentParserError(msg.capitalize()) + def error(self, message): + raise ArgumentParserError(message.capitalize()) def arg_name(name): diff --git a/qutebrowser/commands/cmdexc.py b/qutebrowser/commands/cmdexc.py index 766d20620..51e20fec7 100644 --- a/qutebrowser/commands/cmdexc.py +++ b/qutebrowser/commands/cmdexc.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/commands/cmdutils.py b/qutebrowser/commands/cmdutils.py index 3641c3cb9..9b1539b71 100644 --- a/qutebrowser/commands/cmdutils.py +++ b/qutebrowser/commands/cmdutils.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/commands/command.py b/qutebrowser/commands/command.py index d46cc5c77..b40f7ab16 100644 --- a/qutebrowser/commands/command.py +++ b/qutebrowser/commands/command.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/commands/runners.py b/qutebrowser/commands/runners.py index f6a8e07e2..cc967ce86 100644 --- a/qutebrowser/commands/runners.py +++ b/qutebrowser/commands/runners.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -133,7 +133,8 @@ class CommandRunner(QObject): Yields: ParseResult tuples. """ - if not text.strip(): + text = text.strip().lstrip(':').strip() + if not text: raise cmdexc.NoSuchCommandError("No command given") if aliases: diff --git a/qutebrowser/commands/userscripts.py b/qutebrowser/commands/userscripts.py index 4533e86b1..69a36f400 100644 --- a/qutebrowser/commands/userscripts.py +++ b/qutebrowser/commands/userscripts.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -64,14 +64,19 @@ class _QtFIFOReader(QObject): def read_line(self): """(Try to) read a line from the FIFO.""" log.procs.debug("QSocketNotifier triggered!") - self._notifier.setEnabled(False) 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)) + self._notifier.setEnabled(False) + 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)) + except RuntimeError as e: + # For unknown reasons, read_line can still get called after the + # QSocketNotifier was already deleted... + log.procs.debug("While reading userscript output: {}".format(e)) def cleanup(self): """Clean up so the FIFO can be closed.""" diff --git a/qutebrowser/completion/__init__.py b/qutebrowser/completion/__init__.py index d3caf8703..8b8b9d88d 100644 --- a/qutebrowser/completion/__init__.py +++ b/qutebrowser/completion/__init__.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/completion/completer.py b/qutebrowser/completion/completer.py index 74c759c0d..ae72add20 100644 --- a/qutebrowser/completion/completer.py +++ b/qutebrowser/completion/completer.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -23,8 +23,8 @@ from PyQt5.QtCore import pyqtSlot, QObject, QTimer from qutebrowser.config import config from qutebrowser.commands import cmdutils, runners -from qutebrowser.utils import usertypes, log, utils -from qutebrowser.completion.models import instances, sortfilter +from qutebrowser.utils import log, utils, debug +from qutebrowser.completion.models import miscmodels class Completer(QObject): @@ -39,6 +39,7 @@ class Completer(QObject): _last_cursor_pos: The old cursor position so we avoid double completion updates. _last_text: The old command text so we avoid double completion updates. + _last_completion_func: The completion function used for the last text. """ def __init__(self, cmd, win_id, parent=None): @@ -52,6 +53,7 @@ class Completer(QObject): self._timer.timeout.connect(self._update_completion) self._last_cursor_pos = None self._last_text = None + self._last_completion_func = None self._cmd.update_completion.connect(self.schedule_completion_update) def __repr__(self): @@ -62,37 +64,8 @@ class Completer(QObject): completion = self.parent() return completion.model() - def _get_completion_model(self, completion, pos_args): - """Get a completion model based on an enum member. - - Args: - completion: A usertypes.Completion member. - pos_args: The positional args entered before the cursor. - - Return: - A completion model or None. - """ - if completion == usertypes.Completion.option: - section = pos_args[0] - model = instances.get(completion).get(section) - elif completion == usertypes.Completion.value: - section = pos_args[0] - option = pos_args[1] - try: - model = instances.get(completion)[section][option] - except KeyError: - # No completion model for this section/option. - model = None - else: - model = instances.get(completion) - - if model is None: - return None - else: - return sortfilter.CompletionFilterModel(source=model, parent=self) - def _get_new_completion(self, before_cursor, under_cursor): - """Get a new completion. + """Get the completion function based on the current command text. Args: before_cursor: The command chunks before the cursor. @@ -109,8 +82,8 @@ class Completer(QObject): log.completion.debug("After removing flags: {}".format(before_cursor)) if not before_cursor: # '|' or 'set|' - model = instances.get(usertypes.Completion.command) - return sortfilter.CompletionFilterModel(source=model, parent=self) + log.completion.debug('Starting command completion') + return miscmodels.command try: cmd = cmdutils.cmd_dict[before_cursor[0]] except KeyError: @@ -119,14 +92,11 @@ class Completer(QObject): return None argpos = len(before_cursor) - 1 try: - completion = cmd.get_pos_arg_info(argpos).completion + func = cmd.get_pos_arg_info(argpos).completion except IndexError: log.completion.debug("No completion in position {}".format(argpos)) return None - if completion is None: - return None - model = self._get_completion_model(completion, before_cursor[1:]) - return model + return func def _quote(self, s): """Quote s if it needs quoting for the commandline. @@ -241,6 +211,7 @@ class Completer(QObject): # FIXME complete searches # https://github.com/qutebrowser/qutebrowser/issues/32 completion.set_model(None) + self._last_completion_func = None return before_cursor, pattern, after_cursor = self._partition() @@ -249,13 +220,24 @@ class Completer(QObject): before_cursor, pattern, after_cursor)) pattern = pattern.strip("'\"") - model = self._get_new_completion(before_cursor, pattern) + func = self._get_new_completion(before_cursor, pattern) - log.completion.debug("Setting completion model to {} with pattern '{}'" - .format(model.srcmodel.__class__.__name__ if model else 'None', - pattern)) + if func is None: + log.completion.debug('Clearing completion') + completion.set_model(None) + self._last_completion_func = None + return - completion.set_model(model, pattern) + if func != self._last_completion_func: + self._last_completion_func = func + args = (x for x in before_cursor[1:] if not x.startswith('-')) + with debug.log_time(log.completion, + 'Starting {} completion'.format(func.__name__)): + model = func(*args) + with debug.log_time(log.completion, 'Set completion model'): + completion.set_model(model) + + completion.set_pattern(pattern) def _change_completed_part(self, newtext, before, after, immediate=False): """Change the part we're currently completing in the commandline. diff --git a/qutebrowser/completion/completiondelegate.py b/qutebrowser/completion/completiondelegate.py index dfb479b3f..b2a933cef 100644 --- a/qutebrowser/completion/completiondelegate.py +++ b/qutebrowser/completion/completiondelegate.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -196,8 +196,9 @@ class CompletionItemDelegate(QStyledItemDelegate): self._doc.setDocumentMargin(2) if index.parent().isValid(): - pattern = index.model().pattern - columns_to_filter = index.model().srcmodel.columns_to_filter + view = self.parent() + pattern = view.pattern + columns_to_filter = index.model().columns_to_filter(index) if index.column() in columns_to_filter and pattern: repl = r'\g<0>' text = re.sub(re.escape(pattern).replace(r'\ ', r'|'), diff --git a/qutebrowser/completion/completionwidget.py b/qutebrowser/completion/completionwidget.py index bd6b8c5be..65f1de76c 100644 --- a/qutebrowser/completion/completionwidget.py +++ b/qutebrowser/completion/completionwidget.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -23,13 +23,12 @@ Defines a CompletionView which uses CompletionFiterModel and CompletionModel subclasses to provide completions. """ -from PyQt5.QtWidgets import QStyle, QTreeView, QSizePolicy +from PyQt5.QtWidgets import QStyle, QTreeView, QSizePolicy, QStyleFactory from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QItemSelectionModel, QSize from qutebrowser.config import config, style from qutebrowser.completion import completiondelegate -from qutebrowser.completion.models import base -from qutebrowser.utils import utils, usertypes, objreg +from qutebrowser.utils import utils, usertypes, objreg, debug, log from qutebrowser.commands import cmdexc, cmdutils @@ -41,6 +40,7 @@ class CompletionView(QTreeView): headers, and children show as flat list. Attributes: + pattern: Current filter pattern, used for highlighting. _win_id: The ID of the window this CompletionView is associated with. _height: The height to use for the CompletionView. _height_perc: Either None or a percentage if height should be relative. @@ -107,16 +107,15 @@ class CompletionView(QTreeView): def __init__(self, win_id, parent=None): super().__init__(parent) + self.pattern = '' self._win_id = win_id - # FIXME handle new aliases. - # objreg.get('config').changed.connect(self.init_command_completion) objreg.get('config').changed.connect(self._on_config_changed) - self._column_widths = base.BaseCompletionModel.COLUMN_WIDTHS self._active = False self._delegate = completiondelegate.CompletionItemDelegate(self) self.setItemDelegate(self._delegate) + self.setStyle(QStyleFactory.create('Fusion')) style.set_register_stylesheet(self) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.setHeaderHidden(True) @@ -124,6 +123,7 @@ class CompletionView(QTreeView): self.setIndentation(0) self.setItemsExpandable(False) self.setExpandsOnDoubleClick(False) + self.setAnimated(False) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) # WORKAROUND # This is a workaround for weird race conditions with invalid @@ -149,7 +149,8 @@ class CompletionView(QTreeView): def _resize_columns(self): """Resize the completion columns based on column_widths.""" width = self.size().width() - pixel_widths = [(width * perc // 100) for perc in self._column_widths] + column_widths = self.model().column_widths + pixel_widths = [(width * perc // 100) for perc in column_widths] if self.verticalScrollBar().isVisible(): delta = self.style().pixelMetric(QStyle.PM_ScrollBarExtent) + 5 @@ -260,47 +261,50 @@ class CompletionView(QTreeView): elif config.get('completion', 'show') == 'auto': self.show() - def set_model(self, model, pattern=None): + def set_model(self, model): """Switch completion to a new model. Called from on_update_completion(). Args: model: The model to use. - pattern: The filter pattern to set (what the user entered). """ + if self.model() is not None and model is not self.model(): + self.model().deleteLater() + self.selectionModel().deleteLater() + + self.setModel(model) + if model is None: self._active = False self.hide() return - old_model = self.model() - if model is not old_model: - sel_model = self.selectionModel() - - self.setModel(model) - self._active = True - - if sel_model is not None: - sel_model.deleteLater() - if old_model is not None: - old_model.deleteLater() - - if (config.get('completion', 'show') == 'always' and - model.count() > 0): - self.show() - else: - self.hide() + model.setParent(self) + self._active = True + self._maybe_show() + self._resize_columns() for i in range(model.rowCount()): self.expand(model.index(i, 0)) - if pattern is not None: - model.set_pattern(pattern) + def set_pattern(self, pattern): + """Set the pattern on the underlying model.""" + if not self.model(): + return + self.pattern = pattern + with debug.log_time(log.completion, 'Set pattern {}'.format(pattern)): + self.model().set_pattern(pattern) + self.selectionModel().clear() + self._maybe_update_geometry() + self._maybe_show() - self._column_widths = model.srcmodel.COLUMN_WIDTHS - self._resize_columns() - self._maybe_update_geometry() + def _maybe_show(self): + if (config.get('completion', 'show') == 'always' and + self.model().count() > 0): + self.show() + else: + self.hide() def _maybe_update_geometry(self): """Emit the update_geometry signal if the config says so.""" @@ -345,7 +349,7 @@ class CompletionView(QTreeView): indexes = selected.indexes() if not indexes: return - data = self.model().data(indexes[0]) + data = str(self.model().data(indexes[0])) self.selection_changed.emit(data) def resizeEvent(self, e): @@ -365,9 +369,7 @@ class CompletionView(QTreeView): modes=[usertypes.KeyMode.command], scope='window') def completion_item_del(self): """Delete the current completion item.""" - if not self.currentIndex().isValid(): + index = self.currentIndex() + if not index.isValid(): raise cmdexc.CommandError("No item selected!") - try: - self.model().srcmodel.delete_cur_item(self) - except NotImplementedError: - raise cmdexc.CommandError("Cannot delete this item.") + self.model().delete_cur_item(index) diff --git a/qutebrowser/completion/models/__init__.py b/qutebrowser/completion/models/__init__.py index 99f1954fe..5812545eb 100644 --- a/qutebrowser/completion/models/__init__.py +++ b/qutebrowser/completion/models/__init__.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/completion/models/base.py b/qutebrowser/completion/models/base.py deleted file mode 100644 index 88b06a4e0..000000000 --- a/qutebrowser/completion/models/base.py +++ /dev/null @@ -1,130 +0,0 @@ -# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: - -# Copyright 2014-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 . - -"""The base completion model for completion in the command line. - -Module attributes: - Role: An enum of user defined model roles. -""" - -from PyQt5.QtCore import Qt -from PyQt5.QtGui import QStandardItemModel, QStandardItem - -from qutebrowser.utils import usertypes - - -Role = usertypes.enum('Role', ['sort', 'userdata'], start=Qt.UserRole, - is_int=True) - - -class BaseCompletionModel(QStandardItemModel): - - """A simple QStandardItemModel adopted for completions. - - Used for showing completions later in the CompletionView. Supports setting - marks and adding new categories/items easily. - - Class Attributes: - COLUMN_WIDTHS: The width percentages of the columns used in the - completion view. - DUMB_SORT: the dumb sorting used by the model - """ - - COLUMN_WIDTHS = (30, 70, 0) - DUMB_SORT = None - - def __init__(self, parent=None): - super().__init__(parent) - self.setColumnCount(3) - self.columns_to_filter = [0] - - def new_category(self, name, sort=None): - """Add a new category to the model. - - Args: - name: The name of the category to add. - sort: The value to use for the sort role. - - Return: - The created QStandardItem. - """ - cat = QStandardItem(name) - if sort is not None: - cat.setData(sort, Role.sort) - self.appendRow(cat) - return cat - - def new_item(self, cat, name, desc='', misc=None, sort=None, - userdata=None): - """Add a new item to a category. - - Args: - cat: The parent category. - name: The name of the item. - desc: The description of the item. - misc: Misc text to display. - sort: Data for the sort role (int). - userdata: User data to be added for the first column. - - Return: - A (nameitem, descitem, miscitem) tuple. - """ - assert not isinstance(name, int) - assert not isinstance(desc, int) - assert not isinstance(misc, int) - - nameitem = QStandardItem(name) - descitem = QStandardItem(desc) - if misc is None: - miscitem = QStandardItem() - else: - miscitem = QStandardItem(misc) - - cat.appendRow([nameitem, descitem, miscitem]) - if sort is not None: - nameitem.setData(sort, Role.sort) - if userdata is not None: - nameitem.setData(userdata, Role.userdata) - return nameitem, descitem, miscitem - - def delete_cur_item(self, win_id): - """Delete the selected item.""" - raise NotImplementedError - - def flags(self, index): - """Return the item flags for index. - - Override QAbstractItemModel::flags. - - Args: - index: The QModelIndex to get item flags for. - - Return: - The item flags, or Qt.NoItemFlags on error. - """ - if not index.isValid(): - return - - if index.parent().isValid(): - # item - return (Qt.ItemIsEnabled | Qt.ItemIsSelectable | - Qt.ItemNeverHasChildren) - else: - # category - return Qt.NoItemFlags diff --git a/qutebrowser/completion/models/completionmodel.py b/qutebrowser/completion/models/completionmodel.py new file mode 100644 index 000000000..398673200 --- /dev/null +++ b/qutebrowser/completion/models/completionmodel.py @@ -0,0 +1,232 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2017 Ryan Roden-Corrent (rcorre) +# +# 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 . + +"""A model that proxies access to one or more completion categories.""" + +from PyQt5.QtCore import Qt, QModelIndex, QAbstractItemModel + +from qutebrowser.utils import log, qtutils +from qutebrowser.commands import cmdexc + + +class CompletionModel(QAbstractItemModel): + + """A model that proxies access to one or more completion categories. + + Top level indices represent categories. + Child indices represent rows of those tables. + + Attributes: + column_widths: The width percentages of the columns used in the + completion view. + _categories: The sub-categories. + """ + + def __init__(self, *, column_widths=(30, 70, 0), parent=None): + super().__init__(parent) + self.column_widths = column_widths + self._categories = [] + + def _cat_from_idx(self, index): + """Return the category pointed to by the given index. + + Args: + idx: A QModelIndex + Returns: + A category if the index points at one, else None + """ + # items hold an index to the parent category in their internalPointer + # categories have an empty internalPointer + if index.isValid() and not index.internalPointer(): + return self._categories[index.row()] + return None + + def add_category(self, cat): + """Add a completion category to the model.""" + self._categories.append(cat) + cat.layoutAboutToBeChanged.connect(self.layoutAboutToBeChanged) + cat.layoutChanged.connect(self.layoutChanged) + + def data(self, index, role=Qt.DisplayRole): + """Return the item data for index. + + Override QAbstractItemModel::data. + + Args: + index: The QModelIndex to get item flags for. + + Return: The item data, or None on an invalid index. + """ + if role != Qt.DisplayRole: + return None + cat = self._cat_from_idx(index) + if cat: + # category header + if index.column() == 0: + return self._categories[index.row()].name + return None + # item + cat = self._cat_from_idx(index.parent()) + if not cat: + return None + idx = cat.index(index.row(), index.column()) + return cat.data(idx) + + def flags(self, index): + """Return the item flags for index. + + Override QAbstractItemModel::flags. + + Return: The item flags, or Qt.NoItemFlags on error. + """ + if not index.isValid(): + return Qt.NoItemFlags + if index.parent().isValid(): + # item + return (Qt.ItemIsEnabled | Qt.ItemIsSelectable | + Qt.ItemNeverHasChildren) + else: + # category + return Qt.NoItemFlags + + def index(self, row, col, parent=QModelIndex()): + """Get an index into the model. + + Override QAbstractItemModel::index. + + Return: A QModelIndex. + """ + if (row < 0 or row >= self.rowCount(parent) or + col < 0 or col >= self.columnCount(parent)): + return QModelIndex() + if parent.isValid(): + if parent.column() != 0: + return QModelIndex() + # store a pointer to the parent category in internalPointer + return self.createIndex(row, col, self._categories[parent.row()]) + return self.createIndex(row, col, None) + + def parent(self, index): + """Get an index to the parent of the given index. + + Override QAbstractItemModel::parent. + + Args: + index: The QModelIndex to get the parent index for. + """ + parent_cat = index.internalPointer() + if not parent_cat: + # categories have no parent + return QModelIndex() + row = self._categories.index(parent_cat) + return self.createIndex(row, 0, None) + + def rowCount(self, parent=QModelIndex()): + """Override QAbstractItemModel::rowCount.""" + if not parent.isValid(): + # top-level + return len(self._categories) + cat = self._cat_from_idx(parent) + if not cat or parent.column() != 0: + # item or nonzero category column (only first col has children) + return 0 + else: + # category + return cat.rowCount() + + def columnCount(self, parent=QModelIndex()): + """Override QAbstractItemModel::columnCount.""" + # pylint: disable=unused-argument + return 3 + + def canFetchMore(self, parent): + """Override to forward the call to the categories.""" + cat = self._cat_from_idx(parent) + if cat: + return cat.canFetchMore(QModelIndex()) + return False + + def fetchMore(self, parent): + """Override to forward the call to the categories.""" + cat = self._cat_from_idx(parent) + if cat: + cat.fetchMore(QModelIndex()) + + def count(self): + """Return the count of non-category items.""" + return sum(t.rowCount() for t in self._categories) + + def set_pattern(self, pattern): + """Set the filter pattern for all categories. + + Args: + pattern: The filter pattern to set. + """ + log.completion.debug("Setting completion pattern '{}'".format(pattern)) + for cat in self._categories: + cat.set_pattern(pattern) + + def first_item(self): + """Return the index of the first child (non-category) in the model.""" + for row, cat in enumerate(self._categories): + if cat.rowCount() > 0: + parent = self.index(row, 0) + index = self.index(0, 0, parent) + qtutils.ensure_valid(index) + return index + return QModelIndex() + + def last_item(self): + """Return the index of the last child (non-category) in the model.""" + for row, cat in reversed(list(enumerate(self._categories))): + childcount = cat.rowCount() + if childcount > 0: + parent = self.index(row, 0) + index = self.index(childcount - 1, 0, parent) + qtutils.ensure_valid(index) + return index + return QModelIndex() + + def columns_to_filter(self, index): + """Return the column indices the filter pattern applies to. + + Args: + index: index of the item to check. + + Return: A list of integers. + """ + cat = self._cat_from_idx(index.parent()) + return cat.columns_to_filter if cat else [] + + def delete_cur_item(self, index): + """Delete the row at the given index.""" + qtutils.ensure_valid(index) + parent = index.parent() + cat = self._cat_from_idx(parent) + assert cat, "CompletionView sent invalid index for deletion" + if not cat.delete_func: + raise cmdexc.CommandError("Cannot delete this item.") + + data = [cat.data(cat.index(index.row(), i)) + for i in range(cat.columnCount())] + cat.delete_func(data) + + self.beginRemoveRows(parent, index.row(), index.row()) + cat.removeRow(index.row(), QModelIndex()) + self.endRemoveRows() diff --git a/qutebrowser/completion/models/configmodel.py b/qutebrowser/completion/models/configmodel.py index 4058a5f00..663a0b7f7 100644 --- a/qutebrowser/completion/models/configmodel.py +++ b/qutebrowser/completion/models/configmodel.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -17,142 +17,80 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -"""CompletionModels for the config.""" +"""Functions that return config-related completion models.""" -from PyQt5.QtCore import pyqtSlot, Qt - -from qutebrowser.config import config, configdata -from qutebrowser.utils import log, qtutils, objreg -from qutebrowser.completion.models import base +from qutebrowser.config import configdata, configexc +from qutebrowser.completion.models import completionmodel, listcategory +from qutebrowser.utils import objreg -class SettingSectionCompletionModel(base.BaseCompletionModel): - +def section(): """A CompletionModel filled with settings sections.""" - - # https://github.com/qutebrowser/qutebrowser/issues/545 - # pylint: disable=abstract-method - - COLUMN_WIDTHS = (20, 70, 10) - - def __init__(self, parent=None): - super().__init__(parent) - cat = self.new_category("Sections") - for name in configdata.DATA: - desc = configdata.SECTION_DESC[name].splitlines()[0].strip() - self.new_item(cat, name, desc) + model = completionmodel.CompletionModel(column_widths=(20, 70, 10)) + sections = ((name, configdata.SECTION_DESC[name].splitlines()[0].strip()) + for name in configdata.DATA) + model.add_category(listcategory.ListCategory("Sections", sections)) + return model -class SettingOptionCompletionModel(base.BaseCompletionModel): - +def option(sectname): """A CompletionModel filled with settings and their descriptions. - Attributes: - _misc_items: A dict of the misc. column items which will be set later. - _section: The config section this model shows. + Args: + sectname: The name of the config section this model shows. """ - - # https://github.com/qutebrowser/qutebrowser/issues/545 - # pylint: disable=abstract-method - - COLUMN_WIDTHS = (20, 70, 10) - - def __init__(self, section, parent=None): - super().__init__(parent) - cat = self.new_category(section) - sectdata = configdata.DATA[section] - self._misc_items = {} - self._section = section - objreg.get('config').changed.connect(self.update_misc_column) - for name in sectdata: - try: - desc = sectdata.descriptions[name] - except (KeyError, AttributeError): - # Some stuff (especially ValueList items) don't have a - # description. - desc = "" - else: - desc = desc.splitlines()[0] - value = config.get(section, name, raw=True) - _valitem, _descitem, miscitem = self.new_item(cat, name, desc, - value) - self._misc_items[name] = miscitem - - @pyqtSlot(str, str) - def update_misc_column(self, section, option): - """Update misc column when config changed.""" - if section != self._section: - return + model = completionmodel.CompletionModel(column_widths=(20, 70, 10)) + try: + sectdata = configdata.DATA[sectname] + except KeyError: + return None + options = [] + for name in sectdata: try: - item = self._misc_items[option] - except KeyError: - log.completion.debug("Couldn't get item {}.{} from model!".format( - section, option)) - # changed before init - return - val = config.get(section, option, raw=True) - idx = item.index() - qtutils.ensure_valid(idx) - ok = self.setData(idx, val, Qt.DisplayRole) - if not ok: - raise ValueError("Setting data failed! (section: {}, option: {}, " - "value: {})".format(section, option, val)) + desc = sectdata.descriptions[name] + except (KeyError, AttributeError): + # Some stuff (especially ValueList items) don't have a + # description. + desc = "" + else: + desc = desc.splitlines()[0] + config = objreg.get('config') + val = config.get(sectname, name, raw=True) + options.append((name, desc, val)) + model.add_category(listcategory.ListCategory(sectname, options)) + return model -class SettingValueCompletionModel(base.BaseCompletionModel): - +def value(sectname, optname): """A CompletionModel filled with setting values. - Attributes: - _section: The config section this model shows. - _option: The config option this model shows. + Args: + sectname: The name of the config section this model shows. + optname: The name of the config option this model shows. """ + model = completionmodel.CompletionModel(column_widths=(20, 70, 10)) + config = objreg.get('config') - # https://github.com/qutebrowser/qutebrowser/issues/545 - # pylint: disable=abstract-method + try: + current = config.get(sectname, optname, raw=True) or '""' + except (configexc.NoSectionError, configexc.NoOptionError): + return None - COLUMN_WIDTHS = (20, 70, 10) + default = configdata.DATA[sectname][optname].default() or '""' - def __init__(self, section, option, parent=None): - super().__init__(parent) - self._section = section - self._option = option - objreg.get('config').changed.connect(self.update_current_value) - cur_cat = self.new_category("Current/Default", sort=0) - value = config.get(section, option, raw=True) - if not value: - value = '""' - self.cur_item, _descitem, _miscitem = self.new_item(cur_cat, value, - "Current value") - default_value = configdata.DATA[section][option].default() - if not default_value: - default_value = '""' - self.new_item(cur_cat, default_value, "Default value") - if hasattr(configdata.DATA[section], 'valtype'): - # Same type for all values (ValueList) - vals = configdata.DATA[section].valtype.complete() - else: - if option is None: - raise ValueError("option may only be None for ValueList " - "sections, but {} is not!".format(section)) - # Different type for each value (KeyValue) - vals = configdata.DATA[section][option].typ.complete() - if vals is not None: - cat = self.new_category("Completions", sort=1) - for (val, desc) in vals: - self.new_item(cat, val, desc) + if hasattr(configdata.DATA[sectname], 'valtype'): + # Same type for all values (ValueList) + vals = configdata.DATA[sectname].valtype.complete() + else: + if optname is None: + raise ValueError("optname may only be None for ValueList " + "sections, but {} is not!".format(sectname)) + # Different type for each value (KeyValue) + vals = configdata.DATA[sectname][optname].typ.complete() - @pyqtSlot(str, str) - def update_current_value(self, section, option): - """Update current value when config changed.""" - if (section, option) != (self._section, self._option): - return - value = config.get(section, option, raw=True) - if not value: - value = '""' - idx = self.cur_item.index() - qtutils.ensure_valid(idx) - ok = self.setData(idx, value, Qt.DisplayRole) - if not ok: - raise ValueError("Setting data failed! (section: {}, option: {}, " - "value: {})".format(section, option, value)) + cur_cat = listcategory.ListCategory("Current/Default", + [(current, "Current value"), (default, "Default value")]) + model.add_category(cur_cat) + if vals is not None: + model.add_category(listcategory.ListCategory("Completions", vals)) + return model diff --git a/qutebrowser/completion/models/histcategory.py b/qutebrowser/completion/models/histcategory.py new file mode 100644 index 000000000..42301c0dd --- /dev/null +++ b/qutebrowser/completion/models/histcategory.py @@ -0,0 +1,102 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2017 Ryan Roden-Corrent (rcorre) +# +# 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 . + +"""A completion category that queries the SQL History store.""" + +import re + +from PyQt5.QtSql import QSqlQueryModel + +from qutebrowser.misc import sql +from qutebrowser.utils import debug +from qutebrowser.config import config + + +class HistoryCategory(QSqlQueryModel): + + """A completion category that queries the SQL History store.""" + + def __init__(self, *, delete_func=None, parent=None): + """Create a new History completion category.""" + super().__init__(parent=parent) + self.name = "History" + + # replace ' in timestamp-format to avoid breaking the query + timefmt = ("strftime('{}', last_atime, 'unixepoch', 'localtime')" + .format(config.get('completion', 'timestamp-format') + .replace("'", "`"))) + + self._query = sql.Query(' '.join([ + "SELECT url, title, {}".format(timefmt), + "FROM CompletionHistory", + # the incoming pattern will have literal % and _ escaped with '\' + # we need to tell sql to treat '\' as an escape character + "WHERE (url LIKE :pat escape '\\' or title LIKE :pat escape '\\')", + self._atime_expr(), + "ORDER BY last_atime DESC", + ]), forward_only=False) + + # advertise that this model filters by URL and title + self.columns_to_filter = [0, 1] + self.delete_func = delete_func + + def _atime_expr(self): + """If max_items is set, return an expression to limit the query.""" + max_items = config.get('completion', 'web-history-max-items') + # HistoryCategory should not be added to the completion in that case. + assert max_items != 0 + + if max_items < 0: + return '' + + min_atime = sql.Query(' '.join([ + 'SELECT min(last_atime) FROM', + '(SELECT last_atime FROM CompletionHistory', + 'ORDER BY last_atime DESC LIMIT :limit)', + ])).run(limit=max_items).value() + + if not min_atime: + # if there are no history items, min_atime may be '' (issue #2849) + return '' + + return "AND last_atime >= {}".format(min_atime) + + def set_pattern(self, pattern): + """Set the pattern used to filter results. + + Args: + pattern: string pattern to filter by. + """ + # escape to treat a user input % or _ as a literal, not a wildcard + pattern = pattern.replace('%', '\\%') + pattern = pattern.replace('_', '\\_') + # treat spaces as wildcards to match any of the typed words + pattern = re.sub(r' +', '%', pattern) + pattern = '%{}%'.format(pattern) + with debug.log_time('sql', 'Running completion query'): + self._query.run(pat=pattern) + self.setQuery(self._query) + + def removeRows(self, _row, _count, _parent=None): + """Override QAbstractItemModel::removeRows to re-run sql query.""" + # re-run query to reload updated table + with debug.log_time('sql', 'Re-running completion query post-delete'): + self._query.run() + self.setQuery(self._query) + return True diff --git a/qutebrowser/completion/models/instances.py b/qutebrowser/completion/models/instances.py deleted file mode 100644 index 6359d3771..000000000 --- a/qutebrowser/completion/models/instances.py +++ /dev/null @@ -1,196 +0,0 @@ -# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: - -# Copyright 2015-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 . - -"""Global instances of the completion models. - -Module attributes: - _instances: A dict of available completions. - INITIALIZERS: A {usertypes.Completion: callable} dict of functions to - initialize completions. -""" - -import functools - -from qutebrowser.completion.models import miscmodels, urlmodel, configmodel -from qutebrowser.utils import objreg, usertypes, log, debug -from qutebrowser.config import configdata, config - - -_instances = {} - - -def _init_command_completion(): - """Initialize the command completion model.""" - log.completion.debug("Initializing command completion.") - model = miscmodels.CommandCompletionModel() - _instances[usertypes.Completion.command] = model - - -def _init_helptopic_completion(): - """Initialize the helptopic completion model.""" - log.completion.debug("Initializing helptopic completion.") - model = miscmodels.HelpCompletionModel() - _instances[usertypes.Completion.helptopic] = model - - -def _init_url_completion(): - """Initialize the URL completion model.""" - log.completion.debug("Initializing URL completion.") - with debug.log_time(log.completion, 'URL completion init'): - model = urlmodel.UrlCompletionModel() - _instances[usertypes.Completion.url] = model - - -def _init_tab_completion(): - """Initialize the tab completion model.""" - log.completion.debug("Initializing tab completion.") - with debug.log_time(log.completion, 'tab completion init'): - model = miscmodels.TabCompletionModel() - _instances[usertypes.Completion.tab] = model - - -def _init_setting_completions(): - """Initialize setting completion models.""" - log.completion.debug("Initializing setting completion.") - _instances[usertypes.Completion.section] = ( - configmodel.SettingSectionCompletionModel()) - _instances[usertypes.Completion.option] = {} - _instances[usertypes.Completion.value] = {} - for sectname in configdata.DATA: - opt_model = configmodel.SettingOptionCompletionModel(sectname) - _instances[usertypes.Completion.option][sectname] = opt_model - _instances[usertypes.Completion.value][sectname] = {} - for opt in configdata.DATA[sectname]: - val_model = configmodel.SettingValueCompletionModel(sectname, opt) - _instances[usertypes.Completion.value][sectname][opt] = val_model - - -def init_quickmark_completions(): - """Initialize quickmark completion models.""" - log.completion.debug("Initializing quickmark completion.") - try: - _instances[usertypes.Completion.quickmark_by_name].deleteLater() - except KeyError: - pass - model = miscmodels.QuickmarkCompletionModel() - _instances[usertypes.Completion.quickmark_by_name] = model - - -def init_bookmark_completions(): - """Initialize bookmark completion models.""" - log.completion.debug("Initializing bookmark completion.") - try: - _instances[usertypes.Completion.bookmark_by_url].deleteLater() - except KeyError: - pass - model = miscmodels.BookmarkCompletionModel() - _instances[usertypes.Completion.bookmark_by_url] = model - - -def init_session_completion(): - """Initialize session completion model.""" - log.completion.debug("Initializing session completion.") - try: - _instances[usertypes.Completion.sessions].deleteLater() - except KeyError: - pass - model = miscmodels.SessionCompletionModel() - _instances[usertypes.Completion.sessions] = model - - -def _init_bind_completion(): - """Initialize the command completion model.""" - log.completion.debug("Initializing bind completion.") - model = miscmodels.BindCompletionModel() - _instances[usertypes.Completion.bind] = model - - -INITIALIZERS = { - usertypes.Completion.command: _init_command_completion, - usertypes.Completion.helptopic: _init_helptopic_completion, - usertypes.Completion.url: _init_url_completion, - usertypes.Completion.tab: _init_tab_completion, - usertypes.Completion.section: _init_setting_completions, - usertypes.Completion.option: _init_setting_completions, - usertypes.Completion.value: _init_setting_completions, - usertypes.Completion.quickmark_by_name: init_quickmark_completions, - usertypes.Completion.bookmark_by_url: init_bookmark_completions, - usertypes.Completion.sessions: init_session_completion, - usertypes.Completion.bind: _init_bind_completion, -} - - -def get(completion): - """Get a certain completion. Initializes the completion if needed.""" - try: - return _instances[completion] - except KeyError: - if completion in INITIALIZERS: - INITIALIZERS[completion]() - return _instances[completion] - else: - raise - - -def update(completions): - """Update an already existing completion. - - Args: - completions: An iterable of usertypes.Completions. - """ - did_run = [] - for completion in completions: - if completion in _instances: - func = INITIALIZERS[completion] - if func not in did_run: - func() - did_run.append(func) - - -@config.change_filter('aliases', function=True) -def _update_aliases(): - """Update completions that include command aliases.""" - update([usertypes.Completion.command]) - - -def init(): - """Initialize completions. Note this only connects signals.""" - quickmark_manager = objreg.get('quickmark-manager') - quickmark_manager.changed.connect( - functools.partial(update, [usertypes.Completion.quickmark_by_name])) - - bookmark_manager = objreg.get('bookmark-manager') - bookmark_manager.changed.connect( - functools.partial(update, [usertypes.Completion.bookmark_by_url])) - - session_manager = objreg.get('session-manager') - session_manager.update_completion.connect( - functools.partial(update, [usertypes.Completion.sessions])) - - history = objreg.get('web-history') - history.async_read_done.connect( - functools.partial(update, [usertypes.Completion.url])) - - keyconf = objreg.get('key-config') - keyconf.changed.connect( - functools.partial(update, [usertypes.Completion.command])) - keyconf.changed.connect( - functools.partial(update, [usertypes.Completion.bind])) - - objreg.get('config').changed.connect(_update_aliases) diff --git a/qutebrowser/completion/models/listcategory.py b/qutebrowser/completion/models/listcategory.py new file mode 100644 index 000000000..b1ad77bae --- /dev/null +++ b/qutebrowser/completion/models/listcategory.py @@ -0,0 +1,92 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2017 Ryan Roden-Corrent (rcorre) +# +# 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 . + +"""Completion category that uses a list of tuples as a data source.""" + +import re + +from PyQt5.QtCore import Qt, QSortFilterProxyModel, QRegExp +from PyQt5.QtGui import QStandardItem, QStandardItemModel + +from qutebrowser.utils import qtutils + + +class ListCategory(QSortFilterProxyModel): + + """Expose a list of items as a category for the CompletionModel.""" + + def __init__(self, name, items, delete_func=None, parent=None): + super().__init__(parent) + self.name = name + self.srcmodel = QStandardItemModel(parent=self) + self._pattern = '' + # ListCategory filters all columns + self.columns_to_filter = [0, 1, 2] + self.setFilterKeyColumn(-1) + for item in items: + self.srcmodel.appendRow([QStandardItem(x) for x in item]) + self.setSourceModel(self.srcmodel) + self.delete_func = delete_func + + def set_pattern(self, val): + """Setter for pattern. + + Args: + val: The value to set. + """ + self._pattern = val + val = re.sub(r' +', r' ', val) # See #1919 + val = re.escape(val) + val = val.replace(r'\ ', '.*') + rx = QRegExp(val, Qt.CaseInsensitive) + self.setFilterRegExp(rx) + self.invalidate() + sortcol = 0 + self.sort(sortcol) + + def lessThan(self, lindex, rindex): + """Custom sorting implementation. + + Prefers all items which start with self._pattern. Other than that, uses + normal Python string sorting. + + Args: + lindex: The QModelIndex of the left item (*left* < right) + rindex: The QModelIndex of the right item (left < *right*) + + Return: + True if left < right, else False + """ + qtutils.ensure_valid(lindex) + qtutils.ensure_valid(rindex) + + left = self.srcmodel.data(lindex) + right = self.srcmodel.data(rindex) + + leftstart = left.startswith(self._pattern) + rightstart = right.startswith(self._pattern) + + if leftstart and rightstart: + return left < right + elif leftstart: + return True + elif rightstart: + return False + else: + return left < right diff --git a/qutebrowser/completion/models/miscmodels.py b/qutebrowser/completion/models/miscmodels.py index 76c1a8997..167eccde8 100644 --- a/qutebrowser/completion/models/miscmodels.py +++ b/qutebrowser/completion/models/miscmodels.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -17,252 +17,125 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -"""Misc. CompletionModels.""" +"""Functions that return miscellaneous completion models.""" -from PyQt5.QtCore import Qt, QTimer, pyqtSlot - -from qutebrowser.browser import browsertab from qutebrowser.config import config, configdata -from qutebrowser.utils import objreg, log, qtutils +from qutebrowser.utils import objreg, log from qutebrowser.commands import cmdutils -from qutebrowser.completion.models import base +from qutebrowser.completion.models import completionmodel, listcategory -class CommandCompletionModel(base.BaseCompletionModel): - +def command(): """A CompletionModel filled with non-hidden commands and descriptions.""" - - # https://github.com/qutebrowser/qutebrowser/issues/545 - # pylint: disable=abstract-method - - COLUMN_WIDTHS = (20, 60, 20) - - def __init__(self, parent=None): - super().__init__(parent) - cmdlist = _get_cmd_completions(include_aliases=True, - include_hidden=False) - cat = self.new_category("Commands") - for (name, desc, misc) in cmdlist: - self.new_item(cat, name, desc, misc) + model = completionmodel.CompletionModel(column_widths=(20, 60, 20)) + cmdlist = _get_cmd_completions(include_aliases=True, include_hidden=False) + model.add_category(listcategory.ListCategory("Commands", cmdlist)) + return model -class HelpCompletionModel(base.BaseCompletionModel): - +def helptopic(): """A CompletionModel filled with help topics.""" + model = completionmodel.CompletionModel() - # https://github.com/qutebrowser/qutebrowser/issues/545 - # pylint: disable=abstract-method + cmdlist = _get_cmd_completions(include_aliases=False, include_hidden=True, + prefix=':') + settings = [] + for sectname, sectdata in configdata.DATA.items(): + for optname in sectdata: + try: + desc = sectdata.descriptions[optname] + except (KeyError, AttributeError): + # Some stuff (especially ValueList items) don't have a + # description. + desc = "" + else: + desc = desc.splitlines()[0] + name = '{}->{}'.format(sectname, optname) + settings.append((name, desc)) - COLUMN_WIDTHS = (20, 60, 20) - - def __init__(self, parent=None): - super().__init__(parent) - self._init_commands() - self._init_settings() - - def _init_commands(self): - """Fill completion with :command entries.""" - cmdlist = _get_cmd_completions(include_aliases=False, - include_hidden=True, prefix=':') - cat = self.new_category("Commands") - for (name, desc, misc) in cmdlist: - self.new_item(cat, name, desc, misc) - - def _init_settings(self): - """Fill completion with section->option entries.""" - cat = self.new_category("Settings") - for sectname, sectdata in configdata.DATA.items(): - for optname in sectdata: - try: - desc = sectdata.descriptions[optname] - except (KeyError, AttributeError): - # Some stuff (especially ValueList items) don't have a - # description. - desc = "" - else: - desc = desc.splitlines()[0] - name = '{}->{}'.format(sectname, optname) - self.new_item(cat, name, desc) + model.add_category(listcategory.ListCategory("Commands", cmdlist)) + model.add_category(listcategory.ListCategory("Settings", settings)) + return model -class QuickmarkCompletionModel(base.BaseCompletionModel): - +def quickmark(): """A CompletionModel filled with all quickmarks.""" - - # https://github.com/qutebrowser/qutebrowser/issues/545 - # pylint: disable=abstract-method - - def __init__(self, parent=None): - super().__init__(parent) - cat = self.new_category("Quickmarks") - quickmarks = objreg.get('quickmark-manager').marks.items() - for qm_name, qm_url in quickmarks: - self.new_item(cat, qm_name, qm_url) + model = completionmodel.CompletionModel(column_widths=(30, 70, 0)) + marks = objreg.get('quickmark-manager').marks.items() + model.add_category(listcategory.ListCategory('Quickmarks', marks)) + return model -class BookmarkCompletionModel(base.BaseCompletionModel): - +def bookmark(): """A CompletionModel filled with all bookmarks.""" - - # https://github.com/qutebrowser/qutebrowser/issues/545 - # pylint: disable=abstract-method - - def __init__(self, parent=None): - super().__init__(parent) - cat = self.new_category("Bookmarks") - bookmarks = objreg.get('bookmark-manager').marks.items() - for bm_url, bm_title in bookmarks: - self.new_item(cat, bm_url, bm_title) + model = completionmodel.CompletionModel(column_widths=(30, 70, 0)) + marks = objreg.get('bookmark-manager').marks.items() + model.add_category(listcategory.ListCategory('Bookmarks', marks)) + return model -class SessionCompletionModel(base.BaseCompletionModel): - +def session(): """A CompletionModel filled with session names.""" - - # https://github.com/qutebrowser/qutebrowser/issues/545 - # pylint: disable=abstract-method - - def __init__(self, parent=None): - super().__init__(parent) - cat = self.new_category("Sessions") - try: - for name in objreg.get('session-manager').list_sessions(): - if not name.startswith('_'): - self.new_item(cat, name) - except OSError: - log.completion.exception("Failed to list sessions!") + model = completionmodel.CompletionModel() + try: + manager = objreg.get('session-manager') + sessions = ((name,) for name in manager.list_sessions() + if not name.startswith('_')) + model.add_category(listcategory.ListCategory("Sessions", sessions)) + except OSError: + log.completion.exception("Failed to list sessions!") + return model -class TabCompletionModel(base.BaseCompletionModel): - +def buffer(): """A model to complete on open tabs across all windows. Used for switching the buffer command. """ - - IDX_COLUMN = 0 - URL_COLUMN = 1 - TEXT_COLUMN = 2 - - COLUMN_WIDTHS = (6, 40, 54) - DUMB_SORT = Qt.DescendingOrder - - def __init__(self, parent=None): - super().__init__(parent) - - self.columns_to_filter = [self.IDX_COLUMN, self.URL_COLUMN, - self.TEXT_COLUMN] - - for win_id in objreg.window_registry: - tabbed_browser = objreg.get('tabbed-browser', scope='window', - window=win_id) - for i in range(tabbed_browser.count()): - tab = tabbed_browser.widget(i) - tab.url_changed.connect(self.rebuild) - 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() - - def on_new_window(self, window): - """Add hooks to new windows.""" - window.tabbed_browser.new_tab.connect(self.on_new_tab) - - @pyqtSlot(browsertab.AbstractTab) - def on_new_tab(self, tab): - """Add hooks to new tabs.""" - tab.url_changed.connect(self.rebuild) - tab.title_changed.connect(self.rebuild) - tab.shutting_down.connect(self.delayed_rebuild) - self.rebuild() - - @pyqtSlot() - def delayed_rebuild(self): - """Fire a rebuild indirectly so widgets get a chance to update.""" - QTimer.singleShot(0, self.rebuild) - - @pyqtSlot() - def rebuild(self): - """Rebuild completion model from current tabs. - - Very lazy method of keeping the model up to date. We could connect to - signals for new tab, tab url/title changed, tab close, tab moved and - make sure we handled background loads too ... but iterating over a - few/few dozen/few hundred tabs doesn't take very long at all. - """ - window_count = 0 - for win_id in objreg.window_registry: - tabbed_browser = objreg.get('tabbed-browser', scope='window', - window=win_id) - if not tabbed_browser.shutting_down: - window_count += 1 - - if window_count < self.rowCount(): - self.removeRows(window_count, self.rowCount() - window_count) - - for i, win_id in enumerate(objreg.window_registry): - tabbed_browser = objreg.get('tabbed-browser', scope='window', - window=win_id) - if tabbed_browser.shutting_down: - continue - if i >= self.rowCount(): - c = self.new_category("{}".format(win_id)) - else: - c = self.item(i, 0) - c.setData("{}".format(win_id), Qt.DisplayRole) - if tabbed_browser.count() < c.rowCount(): - c.removeRows(tabbed_browser.count(), - c.rowCount() - tabbed_browser.count()) - for idx in range(tabbed_browser.count()): - tab = tabbed_browser.widget(idx) - if idx >= c.rowCount(): - self.new_item(c, "{}/{}".format(win_id, idx + 1), - tab.url().toDisplayString(), - tabbed_browser.page_title(idx)) - else: - c.child(idx, 0).setData("{}/{}".format(win_id, idx + 1), - Qt.DisplayRole) - c.child(idx, 1).setData(tab.url().toDisplayString(), - Qt.DisplayRole) - c.child(idx, 2).setData(tabbed_browser.page_title(idx), - Qt.DisplayRole) - - def delete_cur_item(self, completion): - """Delete the selected item. - - Args: - completion: The Completion object to use. - """ - index = completion.currentIndex() - qtutils.ensure_valid(index) - category = index.parent() - qtutils.ensure_valid(category) - index = category.child(index.row(), self.IDX_COLUMN) - win_id, tab_index = index.data().split('/') - + def delete_buffer(data): + """Close the selected tab.""" + win_id, tab_index = data[0].split('/') tabbed_browser = objreg.get('tabbed-browser', scope='window', window=int(win_id)) tabbed_browser.on_tab_close_requested(int(tab_index) - 1) + model = completionmodel.CompletionModel(column_widths=(6, 40, 54)) -class BindCompletionModel(base.BaseCompletionModel): + for win_id in objreg.window_registry: + tabbed_browser = objreg.get('tabbed-browser', scope='window', + window=win_id) + if tabbed_browser.shutting_down: + continue + tabs = [] + for idx in range(tabbed_browser.count()): + tab = tabbed_browser.widget(idx) + tabs.append(("{}/{}".format(win_id, idx + 1), + tab.url().toDisplayString(), + tabbed_browser.page_title(idx))) + cat = listcategory.ListCategory("{}".format(win_id), tabs, + delete_func=delete_buffer) + model.add_category(cat) - """A CompletionModel filled with all bindable commands and descriptions.""" + return model - # https://github.com/qutebrowser/qutebrowser/issues/545 - # pylint: disable=abstract-method - COLUMN_WIDTHS = (20, 60, 20) +def bind(key): + """A CompletionModel filled with all bindable commands and descriptions. - def __init__(self, parent=None): - super().__init__(parent) - cmdlist = _get_cmd_completions(include_hidden=True, - include_aliases=True) - cat = self.new_category("Commands") - for (name, desc, misc) in cmdlist: - self.new_item(cat, name, desc, misc) + Args: + key: the key being bound. + """ + model = completionmodel.CompletionModel(column_widths=(20, 60, 20)) + cmd_name = objreg.get('key-config').get_bindings_for('normal').get(key) + + if cmd_name: + cmd = cmdutils.cmd_dict.get(cmd_name) + data = [(cmd_name, cmd.desc, key)] + model.add_category(listcategory.ListCategory("Current", data)) + + cmdlist = _get_cmd_completions(include_hidden=True, include_aliases=True) + model.add_category(listcategory.ListCategory("Commands", cmdlist)) + return model def _get_cmd_completions(include_hidden, include_aliases, prefix=''): diff --git a/qutebrowser/completion/models/sortfilter.py b/qutebrowser/completion/models/sortfilter.py deleted file mode 100644 index 3df787539..000000000 --- a/qutebrowser/completion/models/sortfilter.py +++ /dev/null @@ -1,190 +0,0 @@ -# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: - -# Copyright 2014-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 . - -"""A filtering/sorting base model for completions. - -Contains: - CompletionFilterModel -- A QSortFilterProxyModel subclass for completions. -""" - -from PyQt5.QtCore import QSortFilterProxyModel, QModelIndex, Qt - -from qutebrowser.utils import log, qtutils, debug -from qutebrowser.completion.models import base as completion -import re - - -class CompletionFilterModel(QSortFilterProxyModel): - - """Subclass of QSortFilterProxyModel with custom sorting/filtering. - - Attributes: - pattern: The pattern to filter with. - srcmodel: The current source model. - Kept as attribute because calling `sourceModel` takes quite - a long time for some reason. - _sort_order: The order to use for sorting if using dumb_sort. - """ - - def __init__(self, source, parent=None): - super().__init__(parent) - super().setSourceModel(source) - self.srcmodel = source - self.pattern = '' - self.pattern_re = None - - dumb_sort = self.srcmodel.DUMB_SORT - if dumb_sort is None: - # pylint: disable=invalid-name - self.lessThan = self.intelligentLessThan - self._sort_order = Qt.AscendingOrder - else: - self.setSortRole(completion.Role.sort) - self._sort_order = dumb_sort - - def set_pattern(self, val): - """Setter for pattern. - - Invalidates the filter and re-sorts the model. - - Args: - val: The value to set. - """ - with debug.log_time(log.completion, 'Setting filter pattern'): - self.pattern = val - val = re.sub(r' +', r' ', val) # See #1919 - val = re.escape(val) - val = val.replace(r'\ ', '.*') - self.pattern_re = re.compile(val, re.IGNORECASE) - self.invalidate() - sortcol = 0 - self.sort(sortcol) - - def count(self): - """Get the count of non-toplevel items currently visible. - - Note this only iterates one level deep, as we only need root items - (categories) and children (items) in our model. - """ - count = 0 - for i in range(self.rowCount()): - cat = self.index(i, 0) - qtutils.ensure_valid(cat) - count += self.rowCount(cat) - return count - - def first_item(self): - """Return the first item in the model.""" - for i in range(self.rowCount()): - cat = self.index(i, 0) - qtutils.ensure_valid(cat) - if cat.model().hasChildren(cat): - index = self.index(0, 0, cat) - qtutils.ensure_valid(index) - return index - return QModelIndex() - - def last_item(self): - """Return the last item in the model.""" - for i in range(self.rowCount() - 1, -1, -1): - cat = self.index(i, 0) - qtutils.ensure_valid(cat) - if cat.model().hasChildren(cat): - index = self.index(self.rowCount(cat) - 1, 0, cat) - qtutils.ensure_valid(index) - return index - return QModelIndex() - - def setSourceModel(self, model): - """Override QSortFilterProxyModel's setSourceModel to clear pattern.""" - log.completion.debug("Setting source model: {}".format(model)) - self.set_pattern('') - super().setSourceModel(model) - self.srcmodel = model - - def filterAcceptsRow(self, row, parent): - """Custom filter implementation. - - Override QSortFilterProxyModel::filterAcceptsRow. - - Args: - row: The row of the item. - parent: The parent item QModelIndex. - - Return: - True if self.pattern is contained in item, or if it's a root item - (category). False in all other cases - """ - if parent == QModelIndex() or not self.pattern: - return True - - for col in self.srcmodel.columns_to_filter: - idx = self.srcmodel.index(row, col, parent) - if not idx.isValid(): # pragma: no cover - # this is a sanity check not hit by any test case - continue - data = self.srcmodel.data(idx) - if not data: - continue - elif self.pattern_re.search(data): - return True - return False - - def intelligentLessThan(self, lindex, rindex): - """Custom sorting implementation. - - Prefers all items which start with self.pattern. Other than that, uses - normal Python string sorting. - - Args: - lindex: The QModelIndex of the left item (*left* < right) - rindex: The QModelIndex of the right item (left < *right*) - - Return: - True if left < right, else False - """ - qtutils.ensure_valid(lindex) - qtutils.ensure_valid(rindex) - - left_sort = self.srcmodel.data(lindex, role=completion.Role.sort) - right_sort = self.srcmodel.data(rindex, role=completion.Role.sort) - - if left_sort is not None and right_sort is not None: - return left_sort < right_sort - - left = self.srcmodel.data(lindex) - right = self.srcmodel.data(rindex) - - leftstart = left.startswith(self.pattern) - rightstart = right.startswith(self.pattern) - - if leftstart and rightstart: - return left < right - elif leftstart: - return True - elif rightstart: - return False - else: - return left < right - - def sort(self, column, order=None): - """Extend sort to respect self._sort_order if no order was given.""" - if order is None: - order = self._sort_order - super().sort(column, order) diff --git a/qutebrowser/completion/models/urlmodel.py b/qutebrowser/completion/models/urlmodel.py index a222df4f6..fbb9661ec 100644 --- a/qutebrowser/completion/models/urlmodel.py +++ b/qutebrowser/completion/models/urlmodel.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -17,176 +17,56 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -"""CompletionModels for URLs.""" +"""Function to return the url completion model for the `open` command.""" -import datetime - -from PyQt5.QtCore import pyqtSlot, Qt - -from qutebrowser.utils import objreg, utils, qtutils, log -from qutebrowser.completion.models import base +from qutebrowser.completion.models import (completionmodel, listcategory, + histcategory) +from qutebrowser.utils import log, objreg from qutebrowser.config import config -class UrlCompletionModel(base.BaseCompletionModel): +_URLCOL = 0 +_TEXTCOL = 1 + +def _delete_history(data): + urlstr = data[_URLCOL] + log.completion.debug('Deleting history entry {}'.format(urlstr)) + hist = objreg.get('web-history') + hist.delete_url(urlstr) + + +def _delete_bookmark(data): + urlstr = data[_URLCOL] + log.completion.debug('Deleting bookmark {}'.format(urlstr)) + bookmark_manager = objreg.get('bookmark-manager') + bookmark_manager.delete(urlstr) + + +def _delete_quickmark(data): + name = data[_TEXTCOL] + quickmark_manager = objreg.get('quickmark-manager') + log.completion.debug('Deleting quickmark {}'.format(name)) + quickmark_manager.delete(name) + + +def url(): """A model which combines bookmarks, quickmarks and web history URLs. Used for the `open` command. """ + model = completionmodel.CompletionModel(column_widths=(40, 50, 10)) - URL_COLUMN = 0 - TEXT_COLUMN = 1 - TIME_COLUMN = 2 + quickmarks = ((url, name) for (name, url) + in objreg.get('quickmark-manager').marks.items()) + bookmarks = objreg.get('bookmark-manager').marks.items() - COLUMN_WIDTHS = (40, 50, 10) - DUMB_SORT = Qt.DescendingOrder + model.add_category(listcategory.ListCategory( + 'Quickmarks', quickmarks, delete_func=_delete_quickmark)) + model.add_category(listcategory.ListCategory( + 'Bookmarks', bookmarks, delete_func=_delete_bookmark)) - def __init__(self, parent=None): - super().__init__(parent) - - self.columns_to_filter = [self.URL_COLUMN, self.TEXT_COLUMN] - - self._quickmark_cat = self.new_category("Quickmarks") - self._bookmark_cat = self.new_category("Bookmarks") - self._history_cat = self.new_category("History") - - quickmark_manager = objreg.get('quickmark-manager') - quickmarks = quickmark_manager.marks.items() - for qm_name, qm_url in quickmarks: - self.new_item(self._quickmark_cat, qm_url, qm_name) - quickmark_manager.added.connect( - lambda name, url: self.new_item(self._quickmark_cat, url, name)) - quickmark_manager.removed.connect(self.on_quickmark_removed) - - bookmark_manager = objreg.get('bookmark-manager') - bookmarks = bookmark_manager.marks.items() - for bm_url, bm_title in bookmarks: - self.new_item(self._bookmark_cat, bm_url, bm_title) - bookmark_manager.added.connect( - lambda name, url: self.new_item(self._bookmark_cat, url, name)) - bookmark_manager.removed.connect(self.on_bookmark_removed) - - self._history = objreg.get('web-history') - self._max_history = config.get('completion', 'web-history-max-items') - history = utils.newest_slice(self._history, self._max_history) - for entry in history: - if not entry.redirect: - self._add_history_entry(entry) - self._history.add_completion_item.connect(self.on_history_item_added) - self._history.cleared.connect(self.on_history_cleared) - - objreg.get('config').changed.connect(self.reformat_timestamps) - - def _fmt_atime(self, atime): - """Format an atime to a human-readable string.""" - fmt = config.get('completion', 'timestamp-format') - if fmt is None: - return '' - try: - dt = datetime.datetime.fromtimestamp(atime) - except (ValueError, OSError, OverflowError): - # Different errors which can occur for too large values... - log.misc.error("Got invalid timestamp {}!".format(atime)) - return '(invalid)' - else: - return dt.strftime(fmt) - - def _remove_oldest_history(self): - """Remove the oldest history entry.""" - self._history_cat.removeRow(0) - - def _add_history_entry(self, entry): - """Add a new history entry to the completion.""" - self.new_item(self._history_cat, entry.url.toDisplayString(), - entry.title, - self._fmt_atime(entry.atime), sort=int(entry.atime), - userdata=entry.url) - - if (self._max_history != -1 and - self._history_cat.rowCount() > self._max_history): - self._remove_oldest_history() - - @config.change_filter('completion', 'timestamp-format') - def reformat_timestamps(self): - """Reformat the timestamps if the config option was changed.""" - for i in range(self._history_cat.rowCount()): - url_item = self._history_cat.child(i, self.URL_COLUMN) - atime_item = self._history_cat.child(i, self.TIME_COLUMN) - atime = url_item.data(base.Role.sort) - atime_item.setText(self._fmt_atime(atime)) - - @pyqtSlot(object) - def on_history_item_added(self, entry): - """Slot called when a new history item was added.""" - for i in range(self._history_cat.rowCount()): - url_item = self._history_cat.child(i, self.URL_COLUMN) - atime_item = self._history_cat.child(i, self.TIME_COLUMN) - title_item = self._history_cat.child(i, self.TEXT_COLUMN) - url = url_item.data(base.Role.userdata) - if url == entry.url: - atime_item.setText(self._fmt_atime(entry.atime)) - title_item.setText(entry.title) - url_item.setData(int(entry.atime), base.Role.sort) - break - else: - self._add_history_entry(entry) - - @pyqtSlot() - def on_history_cleared(self): - self._history_cat.removeRows(0, self._history_cat.rowCount()) - - def _remove_item(self, data, category, column): - """Helper function for on_quickmark_removed and on_bookmark_removed. - - Args: - data: The item to search for. - category: The category to search in. - column: The column to use for matching. - """ - for i in range(category.rowCount()): - item = category.child(i, column) - if item.data(Qt.DisplayRole) == data: - category.removeRow(i) - break - - @pyqtSlot(str) - def on_quickmark_removed(self, name): - """Called when a quickmark has been removed by the user. - - Args: - name: The name of the quickmark which has been removed. - """ - self._remove_item(name, self._quickmark_cat, self.TEXT_COLUMN) - - @pyqtSlot(str) - def on_bookmark_removed(self, url): - """Called when a bookmark has been removed by the user. - - Args: - url: The url of the bookmark which has been removed. - """ - self._remove_item(url, self._bookmark_cat, self.URL_COLUMN) - - def delete_cur_item(self, completion): - """Delete the selected item. - - Args: - completion: The Completion object to use. - """ - index = completion.currentIndex() - qtutils.ensure_valid(index) - category = index.parent() - index = category.child(index.row(), self.URL_COLUMN) - url = index.data() - qtutils.ensure_valid(category) - - if category.data() == 'Bookmarks': - bookmark_manager = objreg.get('bookmark-manager') - bookmark_manager.delete(url) - elif category.data() == 'Quickmarks': - quickmark_manager = objreg.get('quickmark-manager') - sibling = index.sibling(index.row(), self.TEXT_COLUMN) - qtutils.ensure_valid(sibling) - name = sibling.data() - quickmark_manager.delete(name) + if config.get('completion', 'web-history-max-items') != 0: + hist_cat = histcategory.HistoryCategory(delete_func=_delete_history) + model.add_category(hist_cat) + return model diff --git a/qutebrowser/config/__init__.py b/qutebrowser/config/__init__.py index e2a04ee47..bf0bce0ec 100644 --- a/qutebrowser/config/__init__.py +++ b/qutebrowser/config/__init__.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 05d71605f..8bae2bae0 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -38,13 +38,12 @@ from PyQt5.QtCore import pyqtSignal, QObject, QUrl, QSettings from PyQt5.QtGui import QColor from qutebrowser.config import configdata, configexc, textwrapper -from qutebrowser.config.parsers import keyconf 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 +from qutebrowser.completion.models import configmodel UNSET = object() @@ -175,36 +174,6 @@ def _init_main_config(parent=None): return -def _init_key_config(parent): - """Initialize the key config. - - Args: - parent: The parent to use for the KeyConfigParser. - """ - args = objreg.get('args') - try: - key_config = keyconf.KeyConfigParser(standarddir.config(), 'keys.conf', - args.relaxed_config, - parent=parent) - except (keyconf.KeyConfigError, UnicodeDecodeError) as e: - log.init.exception(e) - errstr = "Error while reading key config:\n" - if e.lineno is not None: - errstr += "In line {}: ".format(e.lineno) - error.handle_fatal_exc(e, args, "Error while reading key config!", - pre_text=errstr) - # We didn't really initialize much so far, so we just quit hard. - sys.exit(usertypes.Exit.err_key_config) - else: - objreg.register('key-config', key_config) - save_manager = objreg.get('save-manager') - filename = os.path.join(standarddir.config(), 'keys.conf') - save_manager.add_saveable( - 'key-config', key_config.save, key_config.config_dirty, - config_opt=('general', 'auto-save-config'), filename=filename, - dirty=key_config.is_dirty) - - def _init_misc(): """Initialize misc. config-related files.""" save_manager = objreg.get('save-manager') @@ -248,7 +217,6 @@ def init(parent=None): parent: The parent to pass to QObjects which get initialized. """ _init_main_config(parent) - _init_key_config(parent) _init_misc() @@ -389,6 +357,8 @@ class ConfigManager(QObject): ('colors', 'statusbar.bg.warning'): 'messages.bg.warning', ('colors', 'statusbar.fg.prompt'): 'prompts.fg', ('colors', 'statusbar.bg.prompt'): 'prompts.bg', + ('storage', 'offline-web-application-storage'): + 'offline-web-application-cache', } DELETED_OPTIONS = [ ('colors', 'tab.separator'), @@ -402,9 +372,16 @@ class ConfigManager(QObject): ('tabs', 'hide-always'), ('ui', 'display-statusbar-messages'), ('ui', 'hide-mouse-cursor'), + ('ui', 'css-media-type'), ('general', 'wrap-search'), + ('general', 'site-specific-quirks'), ('hints', 'opacity'), ('completion', 'auto-open'), + ('storage', 'object-cache-capacities'), + ('storage', 'offline-storage-database'), + ('storage', 'offline-storage-default-quota'), + ('storage', 'offline-web-application-cache-quota'), + ('content', 'css-regions'), ] CHANGED_OPTIONS = { ('content', 'cookies-accept'): @@ -443,7 +420,20 @@ class ConfigManager(QObject): 'html > ::-webkit-scrollbar { width: 0px; height: 0px; }': '', '::-webkit-scrollbar { width: 0px; height: 0px; }': '', }), - ('contents', 'cache-size'): _get_value_transformer({'52428800': ''}), + ('general', 'default-encoding'): + _get_value_transformer({'': 'iso-8859-1'}), + ('contents', 'cache-size'): + _get_value_transformer({'52428800': ''}), + ('storage', 'maximum-pages-in-cache'): + _get_value_transformer({'': '0'}), + ('fonts', 'web-size-minimum'): + _get_value_transformer({'': '0'}), + ('fonts', 'web-size-minimum-logical'): + _get_value_transformer({'': '6'}), + ('fonts', 'web-size-default'): + _get_value_transformer({'': '16'}), + ('fonts', 'web-size-default-fixed'): + _get_value_transformer({'': '13'}), } changed = pyqtSignal(str, str) @@ -471,10 +461,9 @@ class ConfigManager(QObject): """Get the whole config as a string.""" lines = configdata.FIRST_COMMENT.strip('\n').splitlines() for sectname, sect in self.sections.items(): - lines.append('\n[{}]'.format(sectname)) - lines += self._str_section_desc(sectname) - lines += self._str_option_desc(sectname, sect) - lines += self._str_items(sect) + lines += ['\n'] + self._str_section_desc(sectname) + lines.append('[{}]'.format(sectname)) + lines += self._str_items(sectname, sect) return '\n'.join(lines) + '\n' def _str_section_desc(self, sectname): @@ -489,42 +478,7 @@ class ConfigManager(QObject): lines += wrapper.wrap(secline) return lines - def _str_option_desc(self, sectname, sect): - """Get the option description strings for sect/sectname.""" - wrapper = textwrapper.TextWrapper(initial_indent='#' + ' ' * 5, - subsequent_indent='#' + ' ' * 5) - lines = [] - if not getattr(sect, 'descriptions', None): - return lines - - for optname, option in sect.items(): - - lines.append('#') - typestr = ' ({})'.format(option.typ.get_name()) - lines.append("# {}{}:".format(optname, typestr)) - - try: - desc = self.sections[sectname].descriptions[optname] - except KeyError: - log.config.exception("No description for {}.{}!".format( - sectname, optname)) - continue - for descline in desc.splitlines(): - lines += wrapper.wrap(descline) - valid_values = option.typ.get_valid_values() - if valid_values is not None: - if valid_values.descriptions: - for val in valid_values: - desc = valid_values.descriptions[val] - lines += wrapper.wrap(" {}: {}".format(val, desc)) - else: - lines += wrapper.wrap("Valid values: {}".format(', '.join( - valid_values))) - lines += wrapper.wrap("Default: {}".format( - option.values['default'])) - return lines - - def _str_items(self, sect): + def _str_items(self, sectname, sect): """Get the option items as string for sect.""" lines = [] for optname, option in sect.items(): @@ -535,9 +489,43 @@ class ConfigManager(QObject): # configparser can't handle = in keys :( optname = optname.replace('=', '') keyval = '{} = {}'.format(optname, value) + lines += self._str_option_desc(sectname, sect, optname, option) lines.append(keyval) return lines + def _str_option_desc(self, sectname, sect, optname, option): + """Get the option description strings for a single option.""" + wrapper = textwrapper.TextWrapper(initial_indent='#' + ' ' * 5, + subsequent_indent='#' + ' ' * 5) + lines = [] + if not getattr(sect, 'descriptions', None): + return lines + + lines.append('') + typestr = ' ({})'.format(option.typ.get_name()) + lines.append("# {}{}:".format(optname, typestr)) + + try: + desc = self.sections[sectname].descriptions[optname] + except KeyError: + log.config.exception("No description for {}.{}!".format( + sectname, optname)) + return [] + for descline in desc.splitlines(): + lines += wrapper.wrap(descline) + valid_values = option.typ.get_valid_values() + if valid_values is not None: + if valid_values.descriptions: + for val in valid_values: + desc = valid_values.descriptions[val] + lines += wrapper.wrap(" {}: {}".format(val, desc)) + else: + lines += wrapper.wrap("Valid values: {}".format(', '.join( + valid_values))) + lines += wrapper.wrap("Default: {}".format( + option.values['default'])) + return lines + def _get_real_sectname(self, cp, sectname): """Get an old or new section name based on a configparser. @@ -644,8 +632,7 @@ class ConfigManager(QObject): def _after_set(self, changed_sect, changed_opt): """Clean up caches and emit signals after an option has been set.""" - # WORKAROUND for https://bitbucket.org/logilab/pylint/issues/659/ - self.get.cache_clear() # pylint: disable=no-member + self.get.cache_clear() self._changed(changed_sect, changed_opt) # Options in the same section and ${optname} interpolation. for optname, option in self.sections[changed_sect].items(): @@ -716,8 +703,7 @@ class ConfigManager(QObject): existed = optname in sectdict if existed: sectdict.delete(optname) - # WORKAROUND for https://bitbucket.org/logilab/pylint/issues/659/ - self.get.cache_clear() # pylint: disable=no-member + self.get.cache_clear() return existed @functools.lru_cache() @@ -775,9 +761,9 @@ class ConfigManager(QObject): e.__class__.__name__, e)) @cmdutils.register(name='set', instance='config', star_args_optional=True) - @cmdutils.argument('section_', completion=Completion.section) - @cmdutils.argument('option', completion=Completion.option) - @cmdutils.argument('values', completion=Completion.value) + @cmdutils.argument('section_', completion=configmodel.section) + @cmdutils.argument('option', completion=configmodel.option) + @cmdutils.argument('values', completion=configmodel.value) @cmdutils.argument('win_id', win_id=True) def set_command(self, win_id, section_=None, option=None, *values, temp=False, print_=False): @@ -806,7 +792,7 @@ class ConfigManager(QObject): if section_ is None and option is None: tabbed_browser = objreg.get('tabbed-browser', scope='window', window=win_id) - tabbed_browser.openurl(QUrl('qute:settings'), newtab=False) + tabbed_browser.openurl(QUrl('qute://settings'), newtab=False) return if option.endswith('?') and option != '?': diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 1fbc22937..92878d931 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -185,10 +185,9 @@ def data(readonly=False): "Encoding to use for editor."), ('private-browsing', - SettingValue(typ.Bool(), 'false', - backends=[usertypes.Backend.QtWebKit]), - "Do not record visited pages in the history or store web page " - "icons."), + SettingValue(typ.Bool(), 'false'), + "Open new windows in private browsing mode which does not record " + "visited pages."), ('developer-extras', SettingValue(typ.Bool(), 'false', @@ -216,17 +215,11 @@ def data(readonly=False): "inspector's JavaScript console. Enabling this feature might " "have an impact on performance."), - ('site-specific-quirks', - SettingValue(typ.Bool(), 'true', - backends=[usertypes.Backend.QtWebKit]), - "Enable QtWebKit workarounds for broken sites."), - ('default-encoding', - SettingValue(typ.String(none_ok=True), ''), + SettingValue(typ.String(), 'iso-8859-1'), "Default encoding to use for websites.\n\n" "The encoding must be a string describing an encoding such as " - "_utf-8_, _iso-8859-1_, etc. If left empty a default value will " - "be used."), + "_utf-8_, _iso-8859-1_, etc."), ('new-instance-open-target', SettingValue(typ.String( @@ -317,8 +310,9 @@ def data(readonly=False): "The position of the status bar."), ('message-timeout', - SettingValue(typ.Int(), '2000'), - "Time (in ms) to show messages in the statusbar for."), + SettingValue(typ.Int(minval=0), '2000'), + "Time (in ms) to show messages in the statusbar for.\n" + "Set to 0 to never clear messages."), ('message-unfocused', SettingValue(typ.Bool(), 'false'), @@ -350,11 +344,6 @@ def data(readonly=False): SettingValue(typ.Bool(), 'true'), "Hide the main scrollbar."), - ('css-media-type', - SettingValue(typ.String(none_ok=True), '', - backends=[usertypes.Backend.QtWebKit]), - "Set the CSS media type."), - ('smooth-scrolling', SettingValue(typ.Bool(), 'false'), "Whether to enable smooth scrolling for web pages. Note smooth " @@ -377,7 +366,7 @@ def data(readonly=False): SettingValue(typ.FormatString(fields=['perc', 'perc_raw', 'title', 'title_sep', 'id', 'scroll_pos', 'host', - 'backend']), + 'backend', 'private']), '{perc}{title}{title_sep}qutebrowser'), "The format to use for the window title. The following " "placeholders are defined:\n\n" @@ -389,7 +378,8 @@ def data(readonly=False): "* `{id}`: The internal window ID of this window.\n" "* `{scroll_pos}`: The page scroll position.\n" "* `{host}`: The host of the current web page.\n" - "* `{backend}`: Either 'webkit' or 'webengine'"), + "* `{backend}`: Either 'webkit' or 'webengine'\n" + "* `{private}` : Indicates when private mode is enabled.\n"), ('modal-js-dialog', SettingValue(typ.Bool(), 'false'), @@ -447,14 +437,10 @@ def data(readonly=False): "User agent to send. Empty to send the default."), ('proxy', - SettingValue(typ.Proxy(), 'system', - backends=(None if qtutils.version_check('5.8') - else [usertypes.Backend.QtWebKit])), + SettingValue(typ.Proxy(), 'system'), "The proxy to use.\n\n" "In addition to the listed values, you can use a `socks://...` " - "or `http://...` URL.\n\n" - "This setting only works with Qt 5.8 or newer when using the " - "QtWebEngine backend."), + "or `http://...` URL."), ('proxy-dns-requests', SettingValue(typ.Bool(), 'true', @@ -517,7 +503,7 @@ def data(readonly=False): "0: no history / -1: unlimited"), ('web-history-max-items', - SettingValue(typ.Int(minval=-1), '1000'), + SettingValue(typ.Int(minval=-1, maxval=MAXVALS['int64']), '-1'), "How many URLs to show in the web history.\n\n" "0: no history / -1: unlimited"), @@ -679,12 +665,22 @@ def data(readonly=False): SettingValue(typ.Bool(), 'true'), "Whether to show favicons in the tab bar."), + ('favicon-scale', + SettingValue(typ.Float(minval=0.0), '1.0'), + "Scale for favicons in the tab bar. The tab size is unchanged, " + "so big favicons also require extra `tabs->padding`."), + ('width', SettingValue(typ.PercOrInt(minperc=0, maxperc=100, minint=1), '20%'), "The width of the tab bar if it's vertical, in px or as " "percentage of the window."), + ('pinned-width', + SettingValue(typ.Int(minval=10), + '43'), + "The width for pinned tabs with a horizontal tabbar, in px."), + ('indicator-width', SettingValue(typ.Int(minval=0), '3'), "Width of the progress indicator (0 to disable)."), @@ -696,7 +692,7 @@ def data(readonly=False): ('title-format', SettingValue(typ.FormatString( fields=['perc', 'perc_raw', 'title', 'title_sep', 'index', - 'id', 'scroll_pos', 'host'], none_ok=True), + 'id', 'scroll_pos', 'host', 'private'], none_ok=True), '{index}: {title}'), "The format to use for the tab title. The following placeholders " "are defined:\n\n" @@ -709,7 +705,16 @@ def data(readonly=False): "* `{id}`: The internal tab ID of this tab.\n" "* `{scroll_pos}`: The page scroll position.\n" "* `{host}`: The host of the current web page.\n" - "* `{backend}`: Either 'webkit' or 'webengine'"), + "* `{backend}`: Either 'webkit' or 'webengine'\n" + "* `{private}` : Indicates when private mode is enabled.\n"), + + ('title-format-pinned', + SettingValue(typ.FormatString( + fields=['perc', 'perc_raw', 'title', 'title_sep', 'index', + 'id', 'scroll_pos', 'host', 'private'], none_ok=True), + '{index}'), + "The format to use for the tab title for pinned tabs. " + "The same placeholders like for title-format are defined."), ('title-alignment', SettingValue(typ.TextAlignment(), 'left'), @@ -746,10 +751,12 @@ def data(readonly=False): SettingValue(typ.Bool(), 'true'), "Whether to remember the last used download directory."), + # Defaults from QWebSettings::QWebSettings() in + # qtwebkit/Source/WebKit/qt/Api/qwebsettings.cpp + ('maximum-pages-in-cache', - SettingValue( - typ.Int(none_ok=True, minval=0, maxval=MAXVALS['int']), '', - backends=[usertypes.Backend.QtWebKit]), + SettingValue(typ.Int(minval=0, maxval=MAXVALS['int']), '0', + backends=[usertypes.Backend.QtWebKit]), "The maximum number of pages to hold in the global memory page " "cache.\n\n" "The Page Cache allows for a nicer user experience when " @@ -758,41 +765,7 @@ def data(readonly=False): "For more information about the feature, please refer to: " "http://webkit.org/blog/427/webkit-page-cache-i-the-basics/"), - ('object-cache-capacities', - SettingValue( - typ.List(typ.WebKitBytes(maxsize=MAXVALS['int'], - none_ok=True), none_ok=True, length=3), '', - backends=[usertypes.Backend.QtWebKit]), - "The capacities for the global memory cache for dead objects " - "such as stylesheets or scripts. Syntax: cacheMinDeadCapacity, " - "cacheMaxDead, totalCapacity.\n\n" - "The _cacheMinDeadCapacity_ specifies the minimum number of " - "bytes that dead objects should consume when the cache is under " - "pressure.\n\n" - "_cacheMaxDead_ is the maximum number of bytes that dead objects " - "should consume when the cache is *not* under pressure.\n\n" - "_totalCapacity_ specifies the maximum number of bytes " - "that the cache should consume *overall*."), - - ('offline-storage-default-quota', - SettingValue(typ.WebKitBytes(maxsize=MAXVALS['int64'], - none_ok=True), '', - backends=[usertypes.Backend.QtWebKit]), - "Default quota for new offline storage databases."), - - ('offline-web-application-cache-quota', - SettingValue(typ.WebKitBytes(maxsize=MAXVALS['int64'], - none_ok=True), '', - backends=[usertypes.Backend.QtWebKit]), - "Quota for the offline web application cache."), - - ('offline-storage-database', - SettingValue(typ.Bool(), 'true', - backends=[usertypes.Backend.QtWebKit]), - "Whether support for the HTML 5 offline storage feature is " - "enabled."), - - ('offline-web-application-storage', + ('offline-web-application-cache', SettingValue(typ.Bool(), 'true', backends=[usertypes.Backend.QtWebKit]), "Whether support for the HTML 5 web application cache feature is " @@ -806,7 +779,7 @@ def data(readonly=False): ('local-storage', SettingValue(typ.Bool(), 'true'), - "Whether support for the HTML 5 local storage feature is " + "Whether support for HTML 5 local storage and Web SQL is " "enabled."), ('cache-size', @@ -837,11 +810,6 @@ def data(readonly=False): SettingValue(typ.Bool(), 'true'), "Enables or disables WebGL."), - ('css-regions', - SettingValue(typ.Bool(), 'true', - backends=[usertypes.Backend.QtWebKit]), - "Enable or disable support for CSS regions."), - ('hyperlink-auditing', SettingValue(typ.Bool(), 'false'), "Enable or disable hyperlink auditing ()."), @@ -909,7 +877,7 @@ def data(readonly=False): ('cookies-store', SettingValue(typ.Bool(), 'true'), "Whether to store cookies. Note this option needs a restart with " - "QtWebEngine."), + "QtWebEngine on Qt < 5.9."), ('host-block-lists', SettingValue( @@ -1119,6 +1087,14 @@ def data(readonly=False): SettingValue(typ.QssColor(), 'black'), "Background color of the statusbar."), + ('statusbar.fg.private', + SettingValue(typ.QssColor(), '${statusbar.fg}'), + "Foreground color of the statusbar in private browsing mode."), + + ('statusbar.bg.private', + SettingValue(typ.QssColor(), '#666666'), + "Background color of the statusbar in private browsing mode."), + ('statusbar.fg.insert', SettingValue(typ.QssColor(), '${statusbar.fg}'), "Foreground color of the statusbar in insert mode."), @@ -1135,6 +1111,16 @@ def data(readonly=False): SettingValue(typ.QssColor(), '${statusbar.bg}'), "Background color of the statusbar in command mode."), + ('statusbar.fg.command.private', + SettingValue(typ.QssColor(), '${statusbar.fg.private}'), + "Foreground color of the statusbar in private browsing + command " + "mode."), + + ('statusbar.bg.command.private', + SettingValue(typ.QssColor(), '${statusbar.bg.private}'), + "Background color of the statusbar in private browsing + command " + "mode."), + ('statusbar.fg.caret', SettingValue(typ.QssColor(), '${statusbar.fg}'), "Foreground color of the statusbar in caret mode."), @@ -1358,7 +1344,7 @@ def data(readonly=False): ('fonts', sect.KeyValue( ('_monospace', - SettingValue(typ.Font(), 'Terminus, Monospace, ' + SettingValue(typ.Font(), 'xos4 Terminus, Terminus, Monospace, ' '"DejaVu Sans Mono", Monaco, ' '"Bitstream Vera Sans Mono", "Andale Mono", ' '"Courier New", Courier, "Liberation Mono", ' @@ -1417,25 +1403,28 @@ def data(readonly=False): SettingValue(typ.FontFamily(none_ok=True), ''), "Font family for fantasy fonts."), + # Defaults for web-size-* from WebEngineSettings::initDefaults in + # qtwebengine/src/core/web_engine_settings.cpp and + # QWebSettings::QWebSettings() in + # qtwebkit/Source/WebKit/qt/Api/qwebsettings.cpp + ('web-size-minimum', - SettingValue( - typ.Int(none_ok=True, minval=1, maxval=MAXVALS['int']), ''), + SettingValue(typ.Int(minval=0, maxval=MAXVALS['int']), '0'), "The hard minimum font size."), + # This is 0 as default on QtWebKit, and 6 on QtWebEngine - so let's + # just go for 6 here. ('web-size-minimum-logical', - SettingValue( - typ.Int(none_ok=True, minval=1, maxval=MAXVALS['int']), ''), + SettingValue(typ.Int(minval=0, maxval=MAXVALS['int']), '6'), "The minimum logical font size that is applied when zooming " "out."), ('web-size-default', - SettingValue( - typ.Int(none_ok=True, minval=1, maxval=MAXVALS['int']), ''), + SettingValue(typ.Int(minval=1, maxval=MAXVALS['int']), '16'), "The default font size for regular text."), ('web-size-default-fixed', - SettingValue( - typ.Int(none_ok=True, minval=1, maxval=MAXVALS['int']), ''), + SettingValue(typ.Int(minval=1, maxval=MAXVALS['int']), '13'), "The default font size for fixed-pitch text."), ('keyhint', @@ -1669,7 +1658,7 @@ KEY_DATA = collections.OrderedDict([ ('download-clear', ['cd']), ('view-source', ['gf']), ('set-cmd-text -s :buffer', ['gt']), - ('tab-focus last', ['']), + ('tab-focus last', ['', '', '']), ('enter-mode passthrough', ['']), ('quit', ['', 'ZQ']), ('wq', ['ZZ']), @@ -1677,7 +1666,7 @@ KEY_DATA = collections.OrderedDict([ ('scroll-page 0 -1', ['']), ('scroll-page 0 0.5', ['']), ('scroll-page 0 -0.5', ['']), - ('tab-focus 1', ['']), + ('tab-focus 1', ['', 'g0', 'g^']), ('tab-focus 2', ['']), ('tab-focus 3', ['']), ('tab-focus 4', ['']), @@ -1685,14 +1674,15 @@ KEY_DATA = collections.OrderedDict([ ('tab-focus 6', ['']), ('tab-focus 7', ['']), ('tab-focus 8', ['']), - ('tab-focus 9', ['']), + ('tab-focus -1', ['', 'g$']), ('home', ['']), ('stop', ['']), ('print', ['']), - ('open qute:settings', ['Ss']), + ('open qute://settings', ['Ss']), ('follow-selected', RETURN_KEYS), ('follow-selected -t', ['', '']), ('repeat-command', ['.']), + ('tab-pin', ['']), ('record-macro', ['q']), ('run-macro', ['@']), ])), diff --git a/qutebrowser/config/configexc.py b/qutebrowser/config/configexc.py index a8fd0af2e..b19d45d7b 100644 --- a/qutebrowser/config/configexc.py +++ b/qutebrowser/config/configexc.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index 2dad85117..3d6e71426 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -915,60 +915,6 @@ class FormatString(BaseType): raise configexc.ValidationError(value, str(e)) -class WebKitBytes(BaseType): - - """A size with an optional suffix. - - Attributes: - maxsize: The maximum size to be used. - - Class attributes: - SUFFIXES: A mapping of size suffixes to multiplicators. - """ - - SUFFIXES = { - 'k': 1024 ** 1, - 'm': 1024 ** 2, - 'g': 1024 ** 3, - 't': 1024 ** 4, - 'p': 1024 ** 5, - 'e': 1024 ** 6, - 'z': 1024 ** 7, - 'y': 1024 ** 8, - } - - def __init__(self, maxsize=None, none_ok=False): - super().__init__(none_ok) - self.maxsize = maxsize - - def validate(self, value): - self._basic_validation(value) - if not value: - return - try: - val = self.transform(value) - except ValueError: - raise configexc.ValidationError(value, "must be a valid integer " - "with optional suffix!") - if self.maxsize is not None and val > self.maxsize: - raise configexc.ValidationError(value, "must be {} " - "maximum!".format(self.maxsize)) - if val < 0: - raise configexc.ValidationError(value, "must be 0 minimum!") - - def transform(self, value): - if not value: - return None - elif any(value.lower().endswith(c) for c in self.SUFFIXES): - suffix = value[-1].lower() - val = value[:-1] - multiplicator = self.SUFFIXES[suffix] - else: - val = value - multiplicator = 1 - return int(val) * multiplicator - - class ShellCommand(BaseType): """A shellcommand which is split via shlex. @@ -1452,7 +1398,11 @@ class UserAgent(BaseType): ('Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like ' 'Gecko', - "IE 11.0 for Desktop Win7 64-bit") + "IE 11.0 for Desktop Win7 64-bit"), + + ('Mozilla/5.0 (Linux; U; Android 7.1.2) AppleWebKit/534.30 ' + '(KHTML, like Gecko) Version/4.0 Mobile Safari/534.30', + "Mobile Generic Android") ] return out diff --git a/qutebrowser/config/parsers/__init__.py b/qutebrowser/config/parsers/__init__.py index 5e2183794..1c316078d 100644 --- a/qutebrowser/config/parsers/__init__.py +++ b/qutebrowser/config/parsers/__init__.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/config/parsers/ini.py b/qutebrowser/config/parsers/ini.py index 56640e299..0ae485f4b 100644 --- a/qutebrowser/config/parsers/ini.py +++ b/qutebrowser/config/parsers/ini.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/config/parsers/keyconf.py b/qutebrowser/config/parsers/keyconf.py index 6ca5f72b7..751eafb71 100644 --- a/qutebrowser/config/parsers/keyconf.py +++ b/qutebrowser/config/parsers/keyconf.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -22,12 +22,44 @@ import collections import os.path import itertools +import sys from PyQt5.QtCore import pyqtSignal, QObject from qutebrowser.config import configdata, textwrapper from qutebrowser.commands import cmdutils, cmdexc -from qutebrowser.utils import log, utils, qtutils, message, usertypes +from qutebrowser.utils import (log, utils, qtutils, message, usertypes, objreg, + standarddir, error) +from qutebrowser.completion.models import miscmodels + + +def init(parent=None): + """Read and save keybindings. + + Args: + parent: The parent to use for the KeyConfigParser. + """ + args = objreg.get('args') + try: + key_config = KeyConfigParser(standarddir.config(), 'keys.conf', + args.relaxed_config, parent=parent) + except (KeyConfigError, UnicodeDecodeError) as e: + log.init.exception(e) + errstr = "Error while reading key config:\n" + if e.lineno is not None: + errstr += "In line {}: ".format(e.lineno) + error.handle_fatal_exc(e, args, "Error while reading key config!", + pre_text=errstr) + # We didn't really initialize much so far, so we just quit hard. + sys.exit(usertypes.Exit.err_key_config) + else: + objreg.register('key-config', key_config) + save_manager = objreg.get('save-manager') + filename = os.path.join(standarddir.config(), 'keys.conf') + save_manager.add_saveable( + 'key-config', key_config.save, key_config.config_dirty, + config_opt=('general', 'auto-save-config'), filename=filename, + dirty=key_config.is_dirty) class KeyConfigError(Exception): @@ -142,13 +174,18 @@ class KeyConfigParser(QObject): def save(self): """Save the key config file.""" log.destroy.debug("Saving key config to {}".format(self._configfile)) - with qtutils.savefile_open(self._configfile, encoding='utf-8') as f: - data = str(self) - f.write(data) + + try: + with qtutils.savefile_open(self._configfile, + encoding='utf-8') as f: + data = str(self) + f.write(data) + except OSError as e: + message.error("Could not save key config: {}".format(e)) @cmdutils.register(instance='key-config', maxsplit=1, no_cmd_split=True, no_replace_variables=True) - @cmdutils.argument('command', completion=usertypes.Completion.bind) + @cmdutils.argument('command', completion=miscmodels.bind) def bind(self, key, command=None, *, mode='normal', force=False): """Bind a key to a command. @@ -252,6 +289,7 @@ class KeyConfigParser(QObject): """ # {'sectname': {'keychain1': 'command', 'keychain2': 'command'}, ...} bindings_to_add = collections.OrderedDict() + mark_dirty = False for sectname, sect in configdata.KEY_DATA.items(): sectname = self._normalize_sectname(sectname) @@ -261,6 +299,7 @@ class KeyConfigParser(QObject): if not only_new or self._is_new(sectname, command, e): assert e not in bindings_to_add[sectname] bindings_to_add[sectname][e] = command + mark_dirty = True for sectname, sect in bindings_to_add.items(): if not sect: @@ -271,7 +310,7 @@ class KeyConfigParser(QObject): self._add_binding(sectname, keychain, command) self.changed.emit(sectname) - if bindings_to_add: + if mark_dirty: self._mark_config_dirty() def _is_new(self, sectname, command, keychain): @@ -315,7 +354,7 @@ class KeyConfigParser(QObject): else: line = line.strip() self._read_command(line) - except KeyConfigError as e: + except (KeyConfigError, cmdexc.CommandError) as e: if relaxed: continue else: diff --git a/qutebrowser/config/sections.py b/qutebrowser/config/sections.py index 254348fe9..04a735647 100644 --- a/qutebrowser/config/sections.py +++ b/qutebrowser/config/sections.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/config/style.py b/qutebrowser/config/style.py index b2697daac..15215c398 100644 --- a/qutebrowser/config/style.py +++ b/qutebrowser/config/style.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/config/textwrapper.py b/qutebrowser/config/textwrapper.py index a36a73b6f..b5744f60b 100644 --- a/qutebrowser/config/textwrapper.py +++ b/qutebrowser/config/textwrapper.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/config/value.py b/qutebrowser/config/value.py index 388d8febc..b23674606 100644 --- a/qutebrowser/config/value.py +++ b/qutebrowser/config/value.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/config/websettings.py b/qutebrowser/config/websettings.py index b6e010499..d2ab52ccc 100644 --- a/qutebrowser/config/websettings.py +++ b/qutebrowser/config/websettings.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -17,8 +17,13 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . +# We get various "abstract but not overridden" warnings +# pylint: disable=abstract-method + """Bridge from QWeb(Engine)Settings to our own settings.""" +from PyQt5.QtGui import QFont + from qutebrowser.config import config from qutebrowser.utils import log, utils, debug, usertypes from qutebrowser.misc import objects @@ -28,86 +33,54 @@ UNSET = object() class Base: - """Base class for QWeb(Engine)Settings wrappers. + """Base class for QWeb(Engine)Settings wrappers.""" - Attributes: - _default: The default value of this setting. - """ + def __init__(self, default=UNSET): + self._default = default - # Needs to be overridden by subclasses in - # webkitsettings.py/webenginesettings.py - GLOBAL_SETTINGS = None - - def __init__(self): - self._default = UNSET + def _get_global_settings(self): + """Get a list of global QWeb(Engine)Settings to use.""" + raise NotImplementedError def _get_settings(self, settings): - """Get the QWeb(Engine)Settings object to use. - - Args: - settings: The QWeb(Engine)Settings instance to use, or None to use - the global instance. - """ - if settings is None: - return self.GLOBAL_SETTINGS() # pylint: disable=not-callable - else: - return settings - - def save_default(self, settings=None): - """Save the default value based on the currently set one. - - This does nothing if no getter is configured for this setting. + """Get a list of QWeb(Engine)Settings objects to use. Args: settings: The QWeb(Engine)Settings instance to use, or None to use the global instance. Return: - The saved default value. + A list of QWeb(Engine)Settings objects. The first one should be + used for reading. """ - try: - self._default = self.get(settings) - return self._default - except AttributeError: - return None - - def restore_default(self, settings=None): - """Restore the default value from the saved one. - - This does nothing if the default has never been set. - - Args: - settings: The QWeb(Engine)Settings instance to use, or None to use - the global instance. - """ - if self._default is not UNSET: - log.config.vdebug("Restoring default {!r}.".format(self._default)) - self._set(self._default, settings=settings) - - def get(self, settings=None): - """Get the value of this setting. - - Must be overridden by subclasses. - - Args: - settings: The QWeb(Engine)Settings instance to use, or None to use - the global instance. - """ - raise NotImplementedError + if settings is None: + return self._get_global_settings() + else: + return [settings] def set(self, value, settings=None): """Set the value of this setting. Args: - value: The value to set. + value: The value to set, or None to restore the default. settings: The QWeb(Engine)Settings instance to use, or None to use the global instance. """ if value is None: - self.restore_default(settings) + self.set_default(settings=settings) else: self._set(value, settings=settings) + def set_default(self, settings=None): + """Set the default value for this setting. + + Not implemented for most settings. + """ + if self._default is UNSET: + raise ValueError("No default set for {!r}".format(self)) + else: + self._set(self._default, settings=settings) + def _set(self, value, settings): """Inner function to set the value of this setting. @@ -126,109 +99,71 @@ class Attribute(Base): """A setting set via QWeb(Engine)Settings::setAttribute. Attributes: - self._attribute: A QWeb(Engine)Settings::WebAttribute instance. + self._attributes: A list of QWeb(Engine)Settings::WebAttribute members. """ ENUM_BASE = None - def __init__(self, attribute): - super().__init__() - self._attribute = attribute + def __init__(self, *attributes, default=UNSET): + super().__init__(default=default) + self._attributes = list(attributes) def __repr__(self): - return utils.get_repr( - self, attribute=debug.qenum_key(self.ENUM_BASE, self._attribute), - constructor=True) - - def get(self, settings=None): - return self._get_settings(settings).attribute(self._attribute) + attributes = [debug.qenum_key(self.ENUM_BASE, attr) + for attr in self._attributes] + return utils.get_repr(self, attributes=attributes, constructor=True) def _set(self, value, settings=None): - self._get_settings(settings).setAttribute(self._attribute, value) + for obj in self._get_settings(settings): + for attribute in self._attributes: + obj.setAttribute(attribute, value) class Setter(Base): - """A setting set via QWeb(Engine)Settings getter/setter methods. + """A setting set via a QWeb(Engine)Settings setter method. This will pass the QWeb(Engine)Settings instance ("self") as first argument - to the methods, so self._getter/self._setter are the *unbound* methods. + to the methods, so self._setter is the *unbound* method. Attributes: - _getter: The unbound QWeb(Engine)Settings method to get this value, or - None. _setter: The unbound QWeb(Engine)Settings method to set this value. - _args: An iterable of the arguments to pass to the setter/getter - (before the value, for the setter). + _args: An iterable of the arguments to pass to the setter (before the + value). _unpack: Whether to unpack args (True) or pass them directly (False). """ - def __init__(self, getter, setter, args=(), unpack=False): - super().__init__() - self._getter = getter + def __init__(self, setter, args=(), unpack=False, default=UNSET): + super().__init__(default=default) self._setter = setter self._args = args self._unpack = unpack def __repr__(self): - return utils.get_repr(self, getter=self._getter, setter=self._setter, - args=self._args, unpack=self._unpack, - constructor=True) - - def get(self, settings=None): - if self._getter is None: - raise AttributeError("No getter set!") - return self._getter(self._get_settings(settings), *self._args) + return utils.get_repr(self, setter=self._setter, args=self._args, + unpack=self._unpack, constructor=True) def _set(self, value, settings=None): - args = [self._get_settings(settings)] - args.extend(self._args) - if self._unpack: - args.extend(value) - else: - args.append(value) - self._setter(*args) - - -class NullStringSetter(Setter): - - """A setter for settings requiring a null QString as default. - - This overrides save_default so None is saved for an empty string. This is - needed for the CSS media type, because it returns an empty Python string - when getting the value, but setting it to the default requires passing None - (a null QString) instead of an empty string. - """ - - def save_default(self, settings=None): - try: - val = self.get(settings) - except AttributeError: - return None - if val == '': - self._set(None, settings=settings) - else: - self._set(val, settings=settings) - return val + for obj in self._get_settings(settings): + args = [obj] + args.extend(self._args) + if self._unpack: + args.extend(value) + else: + args.append(value) + self._setter(*args) class StaticSetter(Setter): - """A setting set via static QWeb(Engine)Settings getter/setter methods. + """A setting set via a static QWeb(Engine)Settings method. - self._getter/self._setter are the *bound* methods. + self._setter is the *bound* method. """ - def get(self, settings=None): - if settings is not None: - raise ValueError("'settings' may not be set with GlobalSetters!") - if self._getter is None: - raise AttributeError("No getter set!") - return self._getter(*self._args) - def _set(self, value, settings=None): if settings is not None: - raise ValueError("'settings' may not be set with GlobalSetters!") + raise ValueError("'settings' may not be set with StaticSetters!") args = list(self._args) if self._unpack: args.extend(value) @@ -237,13 +172,28 @@ class StaticSetter(Setter): self._setter(*args) +class FontFamilySetter(Setter): + + """A setter for a font family. + + Gets the default value from QFont. + """ + + def __init__(self, setter, font, qfont): + super().__init__(setter=setter, args=[font]) + self._qfont = qfont + + def set_default(self, settings=None): + font = QFont() + font.setStyleHint(self._qfont) + value = font.defaultFamily() + self._set(value, settings=settings) + + def init_mappings(mappings): """Initialize all settings based on a settings mapping.""" for sectname, section in mappings.items(): for optname, mapping in section.items(): - default = mapping.save_default() - log.config.vdebug("Saved default for {} -> {}: {!r}".format( - sectname, optname, default)) value = config.get(sectname, optname) log.config.vdebug("Setting {} -> {} to {!r}".format( sectname, optname, value)) diff --git a/qutebrowser/html/backend-warning.html b/qutebrowser/html/backend-warning.html new file mode 100644 index 000000000..2b631d6a5 --- /dev/null +++ b/qutebrowser/html/backend-warning.html @@ -0,0 +1,100 @@ +{% extends "styled.html" %} + +{% block style %} +{{super()}} +.note { + font-size: smaller; + color: grey; +} + +.mono { + font-family: monospace; +} +{% endblock %} + +{% block content %} +

Legacy QtWebKit backend

+ +Note this warning will only appear once. Use :open +qute://backend-warning to show it again at a later time. + +

+ You're using qutebrowser with the legacy QtWebKit backend. It's still the + default until a few remaining issues are sorted out. If you can, it's + strongly suggested to switch earlier, as legacy QtWebKit has known security + issues and also breaks things on various websites. +

+ +

Using QtWebEngine instead

+ +This is usually the better choice if you aren't using Nouveau graphics, and +don't need any features which are currently unavailable with QtWebEngine (like +the qute://settings page or caret browsing). + +{% macro install_webengine(package) -%} + You should be able to install {{ package }} and start qutebrowser with --backend webengine to use the new backend. +{%- endmacro %} + +{% macro please_open_issue() -%} + If you know more, please
open an issue! +{%- endmacro %} + +{% macro unknown_system() -%} + There's no information available for your system. {{ please_open_issue() }} +{%- endmacro %} + +

+{% if distribution.parsed == Distribution.ubuntu %} + {% if distribution.version == none %} + {{ unknown_system() }} + {% elif distribution.version >= version('17.04') %} + {{ install_webengine('python3-pyqt5.qtwebengine') }} + {% elif distribution.version >= version('16.04') %} + QtWebEngine is only available in Ubuntu's repositories since 17.04, but you can install qutebrowser via tox with tox -e mkvenv-pypi to use the new backend. + {% else %} + Unfortunately, no easy way is known to install QtWebEngine on Ubuntu < 16.04. {{ please_open_issue() }} + {% endif %} +{% elif distribution.parsed == Distribution.debian %} + {% if distribution.version == none %} + {{ unknown_system() }} + {% elif distribution.version >= version('9') %} + {{ install_webengine('python3-pyqt5.qtwebengine') }} + {% else %} + Unfortunately, no easy way is known to install QtWebEngine on Debian < 9. {{ please_open_issue() }} + {% endif %} +{% elif distribution.parsed in [Distribution.arch, Distribution.manjaro] %} + {{ install_webengine('qt5-webengine') }} +{% elif distribution.parsed == Distribution.void %} + {{ install_webengine('python-PyQt5-webengine') }} +{% elif distribution.parsed == Distribution.fedora %} + {{ install_webengine('qt5-qtwebengine') }} +{% elif distribution.parsed == Distribution.opensuse %} + {{ install_webengine('libqt5-qtwebengine') }} +{% elif distribution.parsed == Distribution.gentoo %} + {{ install_webengine('dev-qt/qtwebengine') }} +{% else %} + {{ unknown_system() }} +{% endif %} +

+ +

Using QtWebKit-NG instead

+ +This is a drop-in replacement for legacy QtWebKit. + +

+{% if distribution.parsed == Distribution.debian and distribution.version != none and distribution.version >= version('9') %} + There are unofficial QtWebKit-NG packages available. +{% elif distribution.parsed in [Distribution.ubuntu, Distribution.debian] %} + No easy way is known to install QtWebKit-NG on your system. + There are unofficial QtWebKit-NG packages available, but they are intended for Debian Unstable. + {{ please_open_issue() }} +{% elif distribution.parsed in [Distribution.arch, Distribution.manjaro] %} + With an updated qt5-webkit package, you should already get QtWebKit-NG. +{% elif distribution.parsed == Distribution.gentoo %} + There's an unofficial ebuild available. +{% else %} + {{ unknown_system() }} +{% endif %} +

+ +{% endblock %} diff --git a/qutebrowser/html/error.html b/qutebrowser/html/error.html index ef50682ef..615e4ba8b 100644 --- a/qutebrowser/html/error.html +++ b/qutebrowser/html/error.html @@ -61,10 +61,7 @@ li { {{ super() }} function tryagain() { - location.reload(); -} -function searchFor(uri) { - location.href = uri; + location.href = "{{ url }}"; } {% endblock %} @@ -77,7 +74,7 @@ function searchFor(uri) {

Unable to load page

- Error while opening {{ url }}:
+ Error while opening {{ url }}

{{ error }}



diff --git a/qutebrowser/html/history.html b/qutebrowser/html/history.html index 58e467135..2c6c0a2f1 100644 --- a/qutebrowser/html/history.html +++ b/qutebrowser/html/history.html @@ -46,23 +46,9 @@ table { height: 40px; text-align: center; } - -.error { - background-color: #ffbbbb; - border-radius: 5px; - font-weight: bold; - padding: 10px; - text-align: center; - width: 100%; - border: 1px solid #ff7777; -} - {% endblock %} {% block content %}

Browsing history

-
diff --git a/qutebrowser/html/history_nojs.html b/qutebrowser/html/history_nojs.html new file mode 100644 index 000000000..bcc5663c1 --- /dev/null +++ b/qutebrowser/html/history_nojs.html @@ -0,0 +1,61 @@ +{% 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; +} + +table { + margin-bottom: 30px; +} + +.date { + color: #555; + font-size: 12pt; + padding-bottom: 15px; + font-weight: bold; + text-align: left; +} + +.pagination-link { + color: #555; + font-weight: bold; + margn-bottom: 15px; + text-decoration: none; +} +{% endblock %} +{% block content %} + +

Browsing history

+ + + + + {% for url, title, time, host in history %} + + + + + {% endfor %} + +
{{curr_date.strftime("%a, %d %B %Y")}}
+ {{title}} + {{host}} + {{time.strftime("%X")}}
+ + +{% if today >= next_date %} + +{% endif %} +{% endblock %} diff --git a/qutebrowser/html/no_pdfjs.html b/qutebrowser/html/no_pdfjs.html index ddaa2c257..d6206e8b2 100644 --- a/qutebrowser/html/no_pdfjs.html +++ b/qutebrowser/html/no_pdfjs.html @@ -71,7 +71,7 @@ li {

No pdf.js installation found

-

Error while opening {{ url }}:
+

Error while opening {{ url }}

qutebrowser can't find a suitable pdf.js installation

It looks like you set content -> enable-pdfjs diff --git a/qutebrowser/html/styled.html b/qutebrowser/html/styled.html index e2a608538..f4d256422 100644 --- a/qutebrowser/html/styled.html +++ b/qutebrowser/html/styled.html @@ -38,4 +38,11 @@ td { padding: 2px 5px; text-align: left; } + +.hostname { + color: #858585; + font-size: 0.9em; + margin-left: 10px; + text-decoration: none; +} {% endblock %} diff --git a/qutebrowser/javascript/.eslintrc.yaml b/qutebrowser/javascript/.eslintrc.yaml index ef7ed00b5..7e787bae8 100644 --- a/qutebrowser/javascript/.eslintrc.yaml +++ b/qutebrowser/javascript/.eslintrc.yaml @@ -38,3 +38,8 @@ rules: max-len: ["error", {"ignoreUrls": true}] capitalized-comments: "off" prefer-destructuring: "off" + line-comment-position: "off" + no-inline-comments: "off" + array-bracket-newline: "off" + array-element-newline: "off" + no-multi-spaces: ["error", {"ignoreEOLComments": true}] diff --git a/qutebrowser/javascript/history.js b/qutebrowser/javascript/history.js index a2448a14f..26b4405e9 100644 --- a/qutebrowser/javascript/history.js +++ b/qutebrowser/javascript/history.js @@ -23,8 +23,12 @@ window.loadHistory = (function() { // Date of last seen item. var lastItemDate = null; - // The time to load next. + // Each request for new items includes the time of the last item and an + // offset. The offset is equal to the number of items from the previous + // request that had time=nextTime, and causes the next request to skip + // those items to avoid duplicates. var nextTime = null; + var nextOffset = 0; // The URL to fetch data from. var DATA_URL = "qute://history/data"; @@ -111,7 +115,11 @@ window.loadHistory = (function() { var link = document.createElement("a"); link.href = itemUrl; link.innerHTML = itemTitle; + var host = document.createElement("span"); + host.className = "hostname"; + host.innerHTML = link.hostname; title.appendChild(link); + title.appendChild(host); var time = document.createElement("td"); time.className = "time"; @@ -153,23 +161,28 @@ window.loadHistory = (function() { return; } - for (var i = 0, len = history.length - 1; i < len; i++) { - var item = history[i]; - var currentItemDate = new Date(item.time); - getSessionNode(currentItemDate).appendChild(makeHistoryRow( - item.url, item.title, currentItemDate.toLocaleTimeString() - )); - lastItemDate = currentItemDate; - } - - var next = history[history.length - 1].next; - if (next === -1) { + if (history.length === 0) { // Reached end of history window.onscroll = null; EOF_MESSAGE.style.display = "block"; LOAD_LINK.style.display = "none"; - } else { - nextTime = next; + return; + } + + nextTime = history[history.length - 1].time; + nextOffset = 0; + + for (var i = 0, len = history.length; i < len; i++) { + var item = history[i]; + // python's time.time returns seconds, but js Date expects ms + var currentItemDate = new Date(item.time * 1000); + getSessionNode(currentItemDate).appendChild(makeHistoryRow( + item.url, item.title, currentItemDate.toLocaleTimeString() + )); + lastItemDate = currentItemDate; + if (item.time === nextTime) { + nextOffset++; + } } } @@ -178,10 +191,11 @@ window.loadHistory = (function() { * @return {void} */ function loadHistory() { + var url = DATA_URL.concat("?offset=", nextOffset.toString()); if (nextTime === null) { - getJSON(DATA_URL, receiveHistory); + getJSON(url, receiveHistory); } else { - var url = DATA_URL.concat("?start_time=", nextTime.toString()); + url = url.concat("&start_time=", nextTime.toString()); getJSON(url, receiveHistory); } } diff --git a/qutebrowser/javascript/position_caret.js b/qutebrowser/javascript/position_caret.js index 09d2301ad..4f6c32380 100644 --- a/qutebrowser/javascript/position_caret.js +++ b/qutebrowser/javascript/position_caret.js @@ -1,6 +1,6 @@ /** * Copyright 2015 Artur Shaik -* Copyright 2015-2016 Florian Bruhin (The Compiler) +* Copyright 2015-2017 Florian Bruhin (The Compiler) * * This file is part of qutebrowser. * diff --git a/qutebrowser/javascript/scroll.js b/qutebrowser/javascript/scroll.js index ac19d175c..35f412783 100644 --- a/qutebrowser/javascript/scroll.js +++ b/qutebrowser/javascript/scroll.js @@ -1,5 +1,5 @@ /** - * Copyright 2016 Florian Bruhin (The Compiler) + * Copyright 2016-2017 Florian Bruhin (The Compiler) * * This file is part of qutebrowser. * diff --git a/qutebrowser/javascript/webelem.js b/qutebrowser/javascript/webelem.js index 206cdf129..ce348c46b 100644 --- a/qutebrowser/javascript/webelem.js +++ b/qutebrowser/javascript/webelem.js @@ -1,5 +1,5 @@ /** - * Copyright 2016 Florian Bruhin (The Compiler) + * Copyright 2016-2017 Florian Bruhin (The Compiler) * * This file is part of qutebrowser. * @@ -21,8 +21,8 @@ * 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). + * - Python calls into Javascript and invokes a function to find elements (one + * of the find_* functions). * - 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 @@ -50,14 +50,33 @@ window._qutebrowser.webelem = (function() { var out = { "id": id, - "text": elem.text, "value": elem.value, - "tag_name": elem.tagName, "outer_xml": elem.outerHTML, - "class_name": elem.className, "rects": [], // Gets filled up later }; + // https://github.com/qutebrowser/qutebrowser/issues/2569 + if (typeof elem.tagName === "string") { + out.tag_name = elem.tagName; + } else if (typeof elem.nodeName === "string") { + out.tag_name = elem.nodeName; + } else { + out.tag_name = ""; + } + + if (typeof elem.className === "string") { + out.class_name = elem.className; + } else { + // e.g. SVG elements + out.class_name = ""; + } + + if (typeof elem.textContent === "string") { + out.text = elem.textContent; + } else if (typeof elem.text === "string") { + out.text = elem.text; + } // else: don't add the text at all + var attributes = {}; for (var i = 0; i < elem.attributes.length; ++i) { var attr = elem.attributes[i]; @@ -125,7 +144,7 @@ window._qutebrowser.webelem = (function() { return true; } - funcs.find_all = function(selector, only_visible) { + funcs.find_css = function(selector, only_visible) { var elems = document.querySelectorAll(selector); var out = []; @@ -138,7 +157,12 @@ window._qutebrowser.webelem = (function() { return out; }; - funcs.focus_element = function() { + funcs.find_id = function(id) { + var elem = document.getElementById(id); + return serialize_elem(elem); + }; + + funcs.find_focused = function() { var elem = document.activeElement; if (!elem || elem === document.body) { @@ -150,17 +174,7 @@ window._qutebrowser.webelem = (function() { return serialize_elem(elem); }; - funcs.set_value = function(id, value) { - elements[id].value = value; - }; - - funcs.insert_text = function(id, text) { - var elem = elements[id]; - elem.focus(); - document.execCommand("insertText", false, text); - }; - - funcs.element_at_pos = function(x, y) { + funcs.find_at_pos = function(x, y) { // FIXME:qtwebengine // If the element at the specified point belongs to another document // (for example, an iframe's subdocument), the subdocument's parent @@ -170,9 +184,23 @@ window._qutebrowser.webelem = (function() { return serialize_elem(elem); }; - funcs.element_by_id = function(id) { - var elem = document.getElementById(id); - return serialize_elem(elem); + // Function for returning a selection to python (so we can click it) + funcs.find_selected_link = function() { + var elem = window.getSelection().anchorNode; + if (!elem) { + return null; + } + return serialize_elem(elem.parentNode); + }; + + funcs.set_value = function(id, value) { + elements[id].value = value; + }; + + funcs.insert_text = function(id, text) { + var elem = elements[id]; + elem.focus(); + document.execCommand("insertText", false, text); }; funcs.set_attribute = function(id, name, value) { diff --git a/qutebrowser/keyinput/__init__.py b/qutebrowser/keyinput/__init__.py index 6fb35e5d6..c6b95b8a9 100644 --- a/qutebrowser/keyinput/__init__.py +++ b/qutebrowser/keyinput/__init__.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/keyinput/basekeyparser.py b/qutebrowser/keyinput/basekeyparser.py index 7325223a3..670cde853 100644 --- a/qutebrowser/keyinput/basekeyparser.py +++ b/qutebrowser/keyinput/basekeyparser.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/keyinput/keyparser.py b/qutebrowser/keyinput/keyparser.py index 0b46dffc4..f9a64edca 100644 --- a/qutebrowser/keyinput/keyparser.py +++ b/qutebrowser/keyinput/keyparser.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/keyinput/macros.py b/qutebrowser/keyinput/macros.py index 8176e5652..9e3667590 100644 --- a/qutebrowser/keyinput/macros.py +++ b/qutebrowser/keyinput/macros.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Jan Verbeek (blyxxyz) +# Copyright 2016-2017 Jan Verbeek (blyxxyz) # # This file is part of qutebrowser. # diff --git a/qutebrowser/keyinput/modeman.py b/qutebrowser/keyinput/modeman.py index 542081719..5ce55e670 100644 --- a/qutebrowser/keyinput/modeman.py +++ b/qutebrowser/keyinput/modeman.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/keyinput/modeparsers.py b/qutebrowser/keyinput/modeparsers.py index db540b58e..4ef393b03 100644 --- a/qutebrowser/keyinput/modeparsers.py +++ b/qutebrowser/keyinput/modeparsers.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/mainwindow/__init__.py b/qutebrowser/mainwindow/__init__.py index 178413514..43eb563a9 100644 --- a/qutebrowser/mainwindow/__init__.py +++ b/qutebrowser/mainwindow/__init__.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py index fbc86d010..3a7d78e97 100644 --- a/qutebrowser/mainwindow/mainwindow.py +++ b/qutebrowser/mainwindow/mainwindow.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -30,7 +30,8 @@ from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication, QSizePolicy from qutebrowser.commands import runners, cmdutils from qutebrowser.config import config -from qutebrowser.utils import message, log, usertypes, qtutils, objreg, utils +from qutebrowser.utils import (message, log, usertypes, qtutils, objreg, utils, + debug) from qutebrowser.mainwindow import tabbedbrowser, messageview, prompt from qutebrowser.mainwindow.statusbar import bar from qutebrowser.completion import completionwidget, completer @@ -81,7 +82,7 @@ def get_window(via_ipc, force_window=False, force_tab=False, # Otherwise, or if no window was found, create a new one if window is None: - window = MainWindow() + window = MainWindow(private=None) window.show() raise_window = True @@ -123,17 +124,20 @@ class MainWindow(QWidget): Attributes: status: The StatusBar widget. tabbed_browser: The TabbedBrowser widget. + state_before_fullscreen: window state before activation of fullscreen. _downloadview: The DownloadView widget. _vbox: The main QVBoxLayout. _commandrunner: The main CommandRunner instance. _overlays: Widgets shown as overlay for the current webpage. + _private: Whether the window is in private browsing mode. """ - def __init__(self, geometry=None, parent=None): + def __init__(self, *, private, geometry=None, parent=None): """Create a new main window. Args: geometry: The geometry to load, as a bytes-object (or None). + private: Whether the window is in private browsing mode. parent: The parent the window should get. """ super().__init__(parent) @@ -161,7 +165,14 @@ class MainWindow(QWidget): self._init_downloadmanager() self._downloadview = downloadview.DownloadView(self.win_id) - self.tabbed_browser = tabbedbrowser.TabbedBrowser(self.win_id) + if config.get('general', 'private-browsing'): + # This setting always trumps what's passed in. + private = True + else: + private = bool(private) + self._private = private + self.tabbed_browser = tabbedbrowser.TabbedBrowser(win_id=self.win_id, + private=private) objreg.register('tabbed-browser', self.tabbed_browser, scope='window', window=self.win_id) self._init_command_dispatcher() @@ -169,7 +180,8 @@ class MainWindow(QWidget): # We need to set an explicit parent for StatusBar because it does some # show/hide magic immediately which would mean it'd show up as a # window. - self.status = bar.StatusBar(self.win_id, parent=self) + self.status = bar.StatusBar(win_id=self.win_id, private=private, + parent=self) self._add_widgets() self._downloadview.show() @@ -196,15 +208,7 @@ class MainWindow(QWidget): self._messageview = messageview.MessageView(parent=self) self._add_overlay(self._messageview, self._messageview.update_geometry) - if geometry is not None: - self._load_geometry(geometry) - elif self.win_id == 0: - self._load_state_geometry() - else: - self._set_default_geometry() - log.init.debug("Initial main window geometry: {}".format( - self.geometry())) - + self._init_geometry(geometry) self._connect_signals() # When we're here the statusbar might not even really exist yet, so @@ -215,6 +219,19 @@ class MainWindow(QWidget): objreg.get("app").new_window.emit(self) + self.state_before_fullscreen = self.windowState() + + def _init_geometry(self, geometry): + """Initialize the window geometry or load it from disk.""" + if geometry is not None: + self._load_geometry(geometry) + elif self.win_id == 0: + self._load_state_geometry() + else: + self._set_default_geometry() + log.init.debug("Initial main window geometry: {}".format( + self.geometry())) + def _add_overlay(self, widget, signal, *, centered=False, padding=0): self._overlays.append((widget, signal, centered, padding)) @@ -446,24 +463,22 @@ class MainWindow(QWidget): message_bridge.s_maybe_reset_text.connect(status.txt.maybe_reset_text) # statusbar - tabs.current_tab_changed.connect(status.prog.on_tab_changed) + tabs.current_tab_changed.connect(status.on_tab_changed) + tabs.cur_progress.connect(status.prog.setValue) tabs.cur_load_finished.connect(status.prog.hide) tabs.cur_load_started.connect(status.prog.on_load_started) - tabs.current_tab_changed.connect(status.percentage.on_tab_changed) tabs.cur_scroll_perc_changed.connect(status.percentage.set_perc) - tabs.tab_index_changed.connect(status.tabindex.on_tab_index_changed) - tabs.current_tab_changed.connect(status.url.on_tab_changed) tabs.cur_url_changed.connect(status.url.set_url) + tabs.cur_url_changed.connect(functools.partial( + status.backforward.on_tab_cur_url_changed, tabs=tabs)) 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) + tabs.cur_fullscreen_requested.connect(self._on_fullscreen_requested) + tabs.cur_fullscreen_requested.connect(status.maybe_hide) # command input / completion mode_manager.left.connect(tabs.on_mode_left) @@ -472,11 +487,14 @@ class MainWindow(QWidget): cmd.hide_completion.connect(completion_obj.hide) @pyqtSlot(bool) - def _on_page_fullscreen_requested(self, on): + def _on_fullscreen_requested(self, on): if on: + self.state_before_fullscreen = self.windowState() self.showFullScreen() - else: - self.showNormal() + elif self.isFullScreen(): + self.setWindowState(self.state_before_fullscreen) + log.misc.debug('on: {}, state before fullscreen: {}'.format( + on, debug.qflags_key(Qt, self.state_before_fullscreen))) @cmdutils.register(instance='main-window', scope='window') @pyqtSlot() diff --git a/qutebrowser/mainwindow/messageview.py b/qutebrowser/mainwindow/messageview.py index 5c407b78b..657835dba 100644 --- a/qutebrowser/mainwindow/messageview.py +++ b/qutebrowser/mainwindow/messageview.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -83,7 +83,6 @@ class MessageView(QWidget): self._clear_timer = QTimer() self._clear_timer.timeout.connect(self.clear_messages) - self._set_clear_timer_interval() objreg.get('config').changed.connect(self._set_clear_timer_interval) self._last_text = None @@ -98,7 +97,10 @@ class MessageView(QWidget): @config.change_filter('ui', 'message-timeout') def _set_clear_timer_interval(self): """Configure self._clear_timer according to the config.""" - self._clear_timer.setInterval(config.get('ui', 'message-timeout')) + interval = config.get('ui', 'message-timeout') + if interval > 0: + interval *= min(5, len(self._messages)) + self._clear_timer.setInterval(interval) @pyqtSlot() def clear_messages(self): @@ -125,11 +127,13 @@ class MessageView(QWidget): widget = Message(level, text, replace=replace, parent=self) self._vbox.addWidget(widget) widget.show() - self._clear_timer.start() self._messages.append(widget) self._last_text = text self.show() self.update_geometry.emit() + if config.get('ui', 'message-timeout') != 0: + self._set_clear_timer_interval() + self._clear_timer.start() def mousePressEvent(self, e): """Clear messages when they are clicked on.""" diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index 69feab920..c38a41caa 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -387,7 +387,7 @@ class PromptContainer(QWidget): @cmdutils.register(instance='prompt-container', hide=True, scope='window', modes=[usertypes.KeyMode.prompt], maxsplit=0) - def prompt_open_download(self, cmdline: str=None): + def prompt_open_download(self, cmdline: str = None): """Immediately open a download. If no specific command is given, this will use the system's default @@ -475,6 +475,7 @@ class _BasePrompt(QWidget): if question.text is not None: # Not doing any HTML escaping here as the text can be formatted text_label = QLabel(question.text) + text_label.setTextInteractionFlags(Qt.TextSelectableByMouse) self._vbox.addWidget(text_label) def _init_key_label(self): diff --git a/qutebrowser/mainwindow/statusbar/__init__.py b/qutebrowser/mainwindow/statusbar/__init__.py index c6a25fe0c..eb3ed7193 100644 --- a/qutebrowser/mainwindow/statusbar/__init__.py +++ b/qutebrowser/mainwindow/statusbar/__init__.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/mainwindow/statusbar/backforward.py b/qutebrowser/mainwindow/statusbar/backforward.py new file mode 100644 index 000000000..fe044e621 --- /dev/null +++ b/qutebrowser/mainwindow/statusbar/backforward.py @@ -0,0 +1,49 @@ +# 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 . + +"""Navigation (back/forward) indicator displayed in the statusbar.""" + +from qutebrowser.mainwindow.statusbar import textbase + + +class Backforward(textbase.TextBase): + + """Shows navigation indicator (if you can go backward and/or forward).""" + + def on_tab_cur_url_changed(self, tabs): + """Called on URL changes.""" + tab = tabs.currentWidget() + if tab is None: # pragma: no cover + # WORKAROUND: Doesn't get tested on older PyQt + self.setText('') + self.hide() + return + self.on_tab_changed(tab) + + def on_tab_changed(self, tab): + """Update the text based on the given tab.""" + text = '' + if tab.history.can_go_back(): + text += '<' + if tab.history.can_go_forward(): + text += '>' + if text: + text = '[' + text + ']' + self.setText(text) + self.setVisible(bool(text)) diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py index eaf8e6ffc..c6fafe14f 100644 --- a/qutebrowser/mainwindow/statusbar/bar.py +++ b/qutebrowser/mainwindow/statusbar/bar.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -22,14 +22,94 @@ from PyQt5.QtCore import pyqtSignal, pyqtSlot, pyqtProperty, Qt, QSize, QTimer from PyQt5.QtWidgets import QWidget, QHBoxLayout, QStackedLayout, QSizePolicy +from qutebrowser.browser import browsertab from qutebrowser.config import config, style from qutebrowser.utils import usertypes, log, objreg, utils -from qutebrowser.mainwindow.statusbar import (command, progress, keystring, - percentage, url, tabindex) +from qutebrowser.mainwindow.statusbar import (backforward, command, progress, + keystring, percentage, url, + tabindex) from qutebrowser.mainwindow.statusbar import text as textwidget -CaretMode = usertypes.enum('CaretMode', ['off', 'on', 'selection']) +class ColorFlags: + + """Flags which change the appearance of the statusbar. + + Attributes: + prompt: If we're currently in prompt-mode. + insert: If we're currently in insert mode. + command: If we're currently in command mode. + mode: The current caret mode (CaretMode.off/.on/.selection). + private: Whether this window is in private browsing mode. + """ + + CaretMode = usertypes.enum('CaretMode', ['off', 'on', 'selection']) + + def __init__(self): + self.prompt = False + self.insert = False + self.command = False + self.caret = self.CaretMode.off + self.private = False + + def to_stringlist(self): + """Get a string list of set flags used in the stylesheet. + + This also combines flags in ways they're used in the sheet. + """ + strings = [] + if self.prompt: + strings.append('prompt') + if self.insert: + strings.append('insert') + if self.command: + strings.append('command') + if self.private: + strings.append('private') + + if self.private and self.command: + strings.append('private-command') + + if self.caret == self.CaretMode.on: + strings.append('caret') + elif self.caret == self.CaretMode.selection: + strings.append('caret-selection') + else: + assert self.caret == self.CaretMode.off + + return strings + + +def _generate_stylesheet(): + flags = [ + ('private', 'statusbar.{}.private'), + ('caret', 'statusbar.{}.caret'), + ('caret-selection', 'statusbar.{}.caret-selection'), + ('prompt', 'prompts.{}'), + ('insert', 'statusbar.{}.insert'), + ('command', 'statusbar.{}.command'), + ('private-command', 'statusbar.{}.command.private'), + ] + stylesheet = """ + QWidget#StatusBar, + QWidget#StatusBar QLabel, + QWidget#StatusBar QLineEdit { + font: {{ font['statusbar'] }}; + background-color: {{ color['statusbar.bg'] }}; + color: {{ color['statusbar.fg'] }}; + } + """ + for flag, option in flags: + stylesheet += """ + QWidget#StatusBar[color_flags~="%s"], + QWidget#StatusBar[color_flags~="%s"] QLabel, + QWidget#StatusBar[color_flags~="%s"] QLineEdit { + color: {{ color['%s'] }}; + background-color: {{ color['%s'] }}; + } + """ % (flag, flag, flag, # flake8: disable=S001 + option.format('fg'), option.format('bg')) + return stylesheet class StatusBar(QWidget): @@ -46,29 +126,6 @@ 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. - - For some reason we need to have this as class attribute - so pyqtProperty works correctly. - - _insert_active: If we're currently in insert mode. - - For some reason we need to have this as class attribute - so pyqtProperty works correctly. - - _command_active: If we're currently in command mode. - - For some reason we need to have this as class - attribute so pyqtProperty works correctly. - - _caret_mode: The current caret mode (off/on/selection). - - For some reason we need to have this as class attribute - so pyqtProperty works correctly. Signals: resized: Emitted when the statusbar has resized, so the completion @@ -82,59 +139,11 @@ class StatusBar(QWidget): resized = pyqtSignal('QRect') moved = pyqtSignal('QPoint') _severity = None - _prompt_active = False - _insert_active = False - _command_active = False - _caret_mode = CaretMode.off + _color_flags = [] - STYLESHEET = """ + STYLESHEET = _generate_stylesheet() - QWidget#StatusBar, - QWidget#StatusBar QLabel, - QWidget#StatusBar QLineEdit { - font: {{ font['statusbar'] }}; - background-color: {{ color['statusbar.bg'] }}; - color: {{ color['statusbar.fg'] }}; - } - - QWidget#StatusBar[caret_mode="on"], - QWidget#StatusBar[caret_mode="on"] QLabel, - QWidget#StatusBar[caret_mode="on"] QLineEdit { - color: {{ color['statusbar.fg.caret'] }}; - background-color: {{ color['statusbar.bg.caret'] }}; - } - - QWidget#StatusBar[caret_mode="selection"], - QWidget#StatusBar[caret_mode="selection"] QLabel, - QWidget#StatusBar[caret_mode="selection"] QLineEdit { - color: {{ color['statusbar.fg.caret-selection'] }}; - background-color: {{ color['statusbar.bg.caret-selection'] }}; - } - - QWidget#StatusBar[prompt_active="true"], - QWidget#StatusBar[prompt_active="true"] QLabel, - QWidget#StatusBar[prompt_active="true"] QLineEdit { - color: {{ color['prompts.fg'] }}; - background-color: {{ color['prompts.bg'] }}; - } - - QWidget#StatusBar[insert_active="true"], - QWidget#StatusBar[insert_active="true"] QLabel, - QWidget#StatusBar[insert_active="true"] QLineEdit { - color: {{ color['statusbar.fg.insert'] }}; - background-color: {{ color['statusbar.bg.insert'] }}; - } - - QWidget#StatusBar[command_active="true"], - QWidget#StatusBar[command_active="true"] QLabel, - QWidget#StatusBar[command_active="true"] QLineEdit { - color: {{ color['statusbar.fg.command'] }}; - background-color: {{ color['statusbar.bg.command'] }}; - } - - """ - - def __init__(self, win_id, parent=None): + def __init__(self, *, win_id, private, parent=None): super().__init__(parent) objreg.register('statusbar', self, scope='window', window=win_id) self.setObjectName(self.__class__.__name__) @@ -144,19 +153,18 @@ class StatusBar(QWidget): self.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Fixed) self._win_id = win_id - self._option = None - self._page_fullscreen = False + self._color_flags = ColorFlags() + self._color_flags.private = private self._hbox = QHBoxLayout(self) - self.set_hbox_padding() - objreg.get('config').changed.connect(self.set_hbox_padding) + self._set_hbox_padding() self._hbox.setSpacing(5) self._stack = QStackedLayout() self._hbox.addLayout(self._stack) self._stack.setContentsMargins(0, 0, 0, 0) - self.cmd = command.Command(win_id) + self.cmd = command.Command(private=private, win_id=win_id) self._stack.addWidget(self.cmd) objreg.register('status-command', self.cmd, scope='window', window=win_id) @@ -177,6 +185,9 @@ class StatusBar(QWidget): self.percentage = percentage.Percentage() self._hbox.addWidget(self.percentage) + self.backforward = backforward.Backforward() + self._hbox.addWidget(self.backforward) + self.tabindex = tabindex.TabIndex() self._hbox.addWidget(self.tabindex) @@ -186,45 +197,45 @@ class StatusBar(QWidget): self.prog = progress.Progress(self) self._hbox.addWidget(self.prog) - objreg.get('config').changed.connect(self.maybe_hide) + objreg.get('config').changed.connect(self._on_config_changed) QTimer.singleShot(0, self.maybe_hide) def __repr__(self): return utils.get_repr(self) - @config.change_filter('ui', 'hide-statusbar') + @pyqtSlot(str, str) + def _on_config_changed(self, section, option): + if section != 'ui': + return + if option == 'hide-statusbar': + self.maybe_hide() + elif option == 'statusbar-pdading': + self._set_hbox_padding() + + @pyqtSlot() def maybe_hide(self): """Hide the statusbar if it's configured to do so.""" hide = config.get('ui', 'hide-statusbar') - if hide or self._page_fullscreen: + tab = self._current_tab() + if hide or (tab is not None and tab.data.fullscreen): self.hide() else: self.show() - @config.change_filter('ui', 'statusbar-padding') - def set_hbox_padding(self): + def _set_hbox_padding(self): padding = config.get('ui', 'statusbar-padding') self._hbox.setContentsMargins(padding.left, 0, padding.right, 0) - @pyqtProperty(bool) - def prompt_active(self): - """Getter for self.prompt_active, so it can be used as Qt property.""" - return self._prompt_active + @pyqtProperty('QStringList') + def color_flags(self): + """Getter for self.color_flags, so it can be used as Qt property.""" + return self._color_flags.to_stringlist() - @pyqtProperty(bool) - def command_active(self): - """Getter for self.command_active, so it can be used as Qt property.""" - return self._command_active - - @pyqtProperty(bool) - def insert_active(self): - """Getter for self.insert_active, so it can be used as Qt property.""" - return self._insert_active - - @pyqtProperty(str) - def caret_mode(self): - """Getter for self._caret_mode, so it can be used as Qt property.""" - return self._caret_mode.name + def _current_tab(self): + """Get the currently displayed tab.""" + window = objreg.get('tabbed-browser', scope='window', + window=self._win_id) + return window.currentWidget() def set_mode_active(self, mode, val): """Setter for self.{insert,command,caret}_active. @@ -233,28 +244,27 @@ class StatusBar(QWidget): updated by Qt properly. """ if mode == usertypes.KeyMode.insert: - log.statusbar.debug("Setting insert_active to {}".format(val)) - self._insert_active = val + log.statusbar.debug("Setting insert flag to {}".format(val)) + self._color_flags.insert = val if mode == usertypes.KeyMode.command: - log.statusbar.debug("Setting command_active to {}".format(val)) - self._command_active = val + log.statusbar.debug("Setting command flag to {}".format(val)) + self._color_flags.command = val elif mode in [usertypes.KeyMode.prompt, usertypes.KeyMode.yesno]: - log.statusbar.debug("Setting prompt_active to {}".format(val)) - self._prompt_active = val + log.statusbar.debug("Setting prompt flag to {}".format(val)) + self._color_flags.prompt = val elif mode == usertypes.KeyMode.caret: - tab = objreg.get('tabbed-browser', scope='window', - window=self._win_id).currentWidget() - log.statusbar.debug("Setting caret_mode - val {}, selection " + tab = self._current_tab() + log.statusbar.debug("Setting caret flag - val {}, selection " "{}".format(val, tab.caret.selection_enabled)) if val: if tab.caret.selection_enabled: self._set_mode_text("{} selection".format(mode.name)) - self._caret_mode = CaretMode.selection + self._color_flags.caret = ColorFlags.CaretMode.selection else: self._set_mode_text(mode.name) - self._caret_mode = CaretMode.on + self._color_flags.caret = ColorFlags.CaretMode.on else: - self._caret_mode = CaretMode.off + self._color_flags.caret = ColorFlags.CaretMode.off self.setStyleSheet(style.get_stylesheet(self.STYLESHEET)) def _set_mode_text(self, mode): @@ -309,10 +319,15 @@ 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 + @pyqtSlot(browsertab.AbstractTab) + def on_tab_changed(self, tab): + """Notify sub-widgets when the tab has been changed.""" + self.url.on_tab_changed(tab) + self.prog.on_tab_changed(tab) + self.percentage.on_tab_changed(tab) + self.backforward.on_tab_changed(tab) self.maybe_hide() + assert tab.private == self._color_flags.private def resizeEvent(self, e): """Extend resizeEvent of QWidget to emit a resized signal afterwards. diff --git a/qutebrowser/mainwindow/statusbar/command.py b/qutebrowser/mainwindow/statusbar/command.py index a5abaa290..3647d9859 100644 --- a/qutebrowser/mainwindow/statusbar/command.py +++ b/qutebrowser/mainwindow/statusbar/command.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -54,14 +54,14 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit): show_cmd = pyqtSignal() hide_cmd = pyqtSignal() - def __init__(self, win_id, parent=None): - misc.CommandLineEdit.__init__(self, parent) + def __init__(self, *, win_id, private, parent=None): + misc.CommandLineEdit.__init__(self, parent=parent) misc.MinimalLineEditMixin.__init__(self) self._win_id = win_id - command_history = objreg.get('command-history') - self.history.handle_private_mode = True - self.history.history = command_history.data - self.history.changed.connect(command_history.changed) + if not private: + command_history = objreg.get('command-history') + self.history.history = command_history.data + self.history.changed.connect(command_history.changed) self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Ignored) self.cursorPositionChanged.connect(self.update_completion) self.textChanged.connect(self.update_completion) diff --git a/qutebrowser/mainwindow/statusbar/keystring.py b/qutebrowser/mainwindow/statusbar/keystring.py index 0baa8137c..dd9825ab2 100644 --- a/qutebrowser/mainwindow/statusbar/keystring.py +++ b/qutebrowser/mainwindow/statusbar/keystring.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/mainwindow/statusbar/percentage.py b/qutebrowser/mainwindow/statusbar/percentage.py index 9c2d2dcd9..0d75e8163 100644 --- a/qutebrowser/mainwindow/statusbar/percentage.py +++ b/qutebrowser/mainwindow/statusbar/percentage.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -21,7 +21,6 @@ from PyQt5.QtCore import pyqtSlot -from qutebrowser.browser import browsertab from qutebrowser.mainwindow.statusbar import textbase @@ -51,7 +50,6 @@ class Percentage(textbase.TextBase): else: self.setText('[{:2}%]'.format(y)) - @pyqtSlot(browsertab.AbstractTab) def on_tab_changed(self, tab): """Update scroll position when tab changed.""" self.set_perc(*tab.scroller.pos_perc()) diff --git a/qutebrowser/mainwindow/statusbar/progress.py b/qutebrowser/mainwindow/statusbar/progress.py index 17892fe33..a2f192732 100644 --- a/qutebrowser/mainwindow/statusbar/progress.py +++ b/qutebrowser/mainwindow/statusbar/progress.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -22,7 +22,6 @@ from PyQt5.QtCore import pyqtSlot, QSize from PyQt5.QtWidgets import QProgressBar, QSizePolicy -from qutebrowser.browser import browsertab from qutebrowser.config import style from qutebrowser.utils import utils, usertypes @@ -60,7 +59,6 @@ class Progress(QProgressBar): self.setValue(0) self.show() - @pyqtSlot(browsertab.AbstractTab) def on_tab_changed(self, tab): """Set the correct value when the current tab changed.""" if self is None: # pragma: no branch diff --git a/qutebrowser/mainwindow/statusbar/tabindex.py b/qutebrowser/mainwindow/statusbar/tabindex.py index 7dda5f806..6a4cc987c 100644 --- a/qutebrowser/mainwindow/statusbar/tabindex.py +++ b/qutebrowser/mainwindow/statusbar/tabindex.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/mainwindow/statusbar/text.py b/qutebrowser/mainwindow/statusbar/text.py index 2791385b7..e99891ecd 100644 --- a/qutebrowser/mainwindow/statusbar/text.py +++ b/qutebrowser/mainwindow/statusbar/text.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/mainwindow/statusbar/textbase.py b/qutebrowser/mainwindow/statusbar/textbase.py index eb3064286..0ae271191 100644 --- a/qutebrowser/mainwindow/statusbar/textbase.py +++ b/qutebrowser/mainwindow/statusbar/textbase.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/mainwindow/statusbar/url.py b/qutebrowser/mainwindow/statusbar/url.py index bca1f0458..f83fdef1f 100644 --- a/qutebrowser/mainwindow/statusbar/url.py +++ b/qutebrowser/mainwindow/statusbar/url.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -21,10 +21,9 @@ from PyQt5.QtCore import pyqtSlot, pyqtProperty, Qt, QUrl -from qutebrowser.browser import browsertab from qutebrowser.mainwindow.statusbar import textbase from qutebrowser.config import style -from qutebrowser.utils import usertypes +from qutebrowser.utils import usertypes, urlutils # Note this has entries for success/error/warn from widgets.webview:LoadStatus @@ -138,8 +137,10 @@ class UrlText(textbase.TextBase): """ if url is None: self._normal_url = None + elif not url.isValid(): + self._normal_url = "Invalid URL!" else: - self._normal_url = url.toDisplayString() + self._normal_url = urlutils.safe_display_string(url) self._normal_url_type = UrlType.normal self._update_url() @@ -156,17 +157,19 @@ class UrlText(textbase.TextBase): if link: qurl = QUrl(link) if qurl.isValid(): - self._hover_url = qurl.toDisplayString() + self._hover_url = urlutils.safe_display_string(qurl) else: - self._hover_url = link + self._hover_url = '(invalid URL!) {}'.format(link) else: self._hover_url = None self._update_url() - @pyqtSlot(browsertab.AbstractTab) def on_tab_changed(self, tab): """Update URL if the tab changed.""" self._hover_url = None - self._normal_url = tab.url().toDisplayString() + if tab.url().isValid(): + self._normal_url = urlutils.safe_display_string(tab.url()) + else: + self._normal_url = '' self.on_load_status_changed(tab.load_status().name) self._update_url() diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py index f56bfc5a6..d74f74b4a 100644 --- a/qutebrowser/mainwindow/tabbedbrowser.py +++ b/qutebrowser/mainwindow/tabbedbrowser.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -31,10 +31,11 @@ from qutebrowser.keyinput import modeman from qutebrowser.mainwindow import tabwidget from qutebrowser.browser import signalfilter, browsertab from qutebrowser.utils import (log, usertypes, utils, qtutils, objreg, - urlutils, message) + urlutils, message, jinja) -UndoEntry = collections.namedtuple('UndoEntry', ['url', 'history', 'index']) +UndoEntry = collections.namedtuple('UndoEntry', + ['url', 'history', 'index', 'pinned']) class TabDeletedError(Exception): @@ -68,6 +69,7 @@ class TabbedBrowser(tabwidget.TabWidget): _local_marks: Jump markers local to each page _global_marks: Jump markers used across all pages default_window_icon: The qutebrowser window icon + private: Whether private browsing is on for this window. Signals: cur_progress: Progress of the current tab changed (load_progress). @@ -94,13 +96,13 @@ class TabbedBrowser(tabwidget.TabWidget): cur_link_hovered = pyqtSignal(str) cur_scroll_perc_changed = pyqtSignal(int, int) cur_load_status_changed = pyqtSignal(str) + cur_fullscreen_requested = pyqtSignal(bool) close_window = pyqtSignal() 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): + def __init__(self, *, win_id, private, parent=None): super().__init__(win_id, parent) self._win_id = win_id self._tab_insert_idx_left = 0 @@ -109,6 +111,7 @@ class TabbedBrowser(tabwidget.TabWidget): self.tabCloseRequested.connect(self.on_tab_close_requested) self.currentChanged.connect(self.on_current_changed) self.cur_load_started.connect(self.on_cur_load_started) + self.cur_fullscreen_requested.connect(self.tabBar().maybe_hide) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self._undo_stack = [] self._filter = signalfilter.SignalFilter(win_id, self) @@ -118,6 +121,7 @@ class TabbedBrowser(tabwidget.TabWidget): self._local_marks = {} self._global_marks = {} self.default_window_icon = self.window().windowIcon() + self.private = private objreg.get('config').changed.connect(self.update_favicons) objreg.get('config').changed.connect(self.update_window_title) objreg.get('config').changed.connect(self.update_tab_titles) @@ -182,14 +186,16 @@ class TabbedBrowser(tabwidget.TabWidget): self._filter.create(self.cur_load_started, tab)) tab.scroller.perc_changed.connect( self._filter.create(self.cur_scroll_perc_changed, tab)) - tab.scroller.perc_changed.connect(self.on_scroll_pos_changed) tab.url_changed.connect( self._filter.create(self.cur_url_changed, tab)) tab.load_status_changed.connect( self._filter.create(self.cur_load_status_changed, tab)) + tab.fullscreen_requested.connect( + self._filter.create(self.cur_fullscreen_requested, tab)) + # misc + tab.scroller.perc_changed.connect(self.on_scroll_pos_changed) tab.url_changed.connect( functools.partial(self.on_url_changed, tab)) - # misc tab.title_changed.connect( functools.partial(self.on_title_changed, tab)) tab.icon_changed.connect( @@ -205,10 +211,9 @@ class TabbedBrowser(tabwidget.TabWidget): 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) + if not self.private: + web_history = objreg.get('web-history') + tab.add_history_item.connect(web_history.add_from_tab) def current_url(self): """Get the URL of the current tab. @@ -227,6 +232,19 @@ class TabbedBrowser(tabwidget.TabWidget): for tab in self.widgets(): self._remove_tab(tab) + def tab_close_prompt_if_pinned(self, tab, force, yes_action): + """Helper method for tab_close. + + If tab is pinned, prompt. If everything is good, run yes_action. + """ + if tab.data.pinned and not force: + message.confirm_async( + title='Pinned Tab', + text="Are you sure you want to close a pinned tab?", + yes_action=yes_action, default=False) + else: + yes_action() + def close_tab(self, tab, *, add_undo=True): """Close a tab. @@ -240,6 +258,10 @@ class TabbedBrowser(tabwidget.TabWidget): if last_close == 'ignore' and count == 1: return + # If we are removing a pinned tab, decrease count + if tab.data.pinned: + self.tabBar().pinned_count -= 1 + self._remove_tab(tab, add_undo=add_undo) if count == 1: # We just closed the last tab above. @@ -264,6 +286,8 @@ class TabbedBrowser(tabwidget.TabWidget): """ idx = self.indexOf(tab) if idx == -1: + if crashed: + return raise TabDeletedError("tab {} is not contained in " "TabbedWidget!".format(tab)) if tab is self._now_focused: @@ -290,7 +314,8 @@ class TabbedBrowser(tabwidget.TabWidget): except browsertab.WebTabError: pass # special URL else: - entry = UndoEntry(tab.url(), history_data, idx) + entry = UndoEntry(tab.url(), history_data, idx, + tab.data.pinned) self._undo_stack.append(entry) tab.shutdown() @@ -321,7 +346,7 @@ class TabbedBrowser(tabwidget.TabWidget): use_current_tab = (only_one_tab_open and no_history and last_close_url_used) - url, history_data, idx = self._undo_stack.pop() + url, history_data, idx, pinned = self._undo_stack.pop() if use_current_tab: self.openurl(url, newtab=False) @@ -330,6 +355,7 @@ class TabbedBrowser(tabwidget.TabWidget): newtab = self.tabopen(url, background=False, idx=idx) newtab.history.deserialize(history_data) + self.set_tab_pinned(newtab, pinned) @pyqtSlot('QUrl', bool) def openurl(self, url, newtab): @@ -353,7 +379,8 @@ class TabbedBrowser(tabwidget.TabWidget): log.webview.debug("Got invalid tab {} for index {}!".format( tab, idx)) return - self.close_tab(tab) + self.tab_close_prompt_if_pinned( + tab, False, lambda: self.close_tab(tab)) @pyqtSlot(browsertab.AbstractTab) def on_window_close_requested(self, widget): @@ -399,13 +426,14 @@ class TabbedBrowser(tabwidget.TabWidget): 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 = mainwindow.MainWindow(private=self.private) window.show() tabbed_browser = objreg.get('tabbed-browser', scope='window', window=window.win_id) return tabbed_browser.tabopen(url, background, explicit) - tab = browsertab.create(win_id=self._win_id, parent=self) + tab = browsertab.create(win_id=self._win_id, private=self.private, + parent=self) self._connect_tab_signals(tab) if idx is None: @@ -414,12 +442,18 @@ class TabbedBrowser(tabwidget.TabWidget): if url is not None: tab.openurl(url) + if background is None: background = config.get('tabs', 'background-tabs') if background: + # Make sure the background tab has the correct initial size. + # With a foreground tab, it's going to be resized correctly by the + # layout anyways. + tab.resize(self.currentWidget().size()) self.tab_index_changed.emit(self.currentIndex(), self.count()) else: self.setCurrentWidget(tab) + tab.show() self.new_tab.emit(tab, idx) return tab @@ -660,24 +694,33 @@ class TabbedBrowser(tabwidget.TabWidget): 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)) + return - # 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')) + messages = { + browsertab.TerminationStatus.abnormal: + "Renderer process exited with status {}".format(code), + browsertab.TerminationStatus.crashed: + "Renderer process crashed", + browsertab.TerminationStatus.killed: + "Renderer process was killed", + browsertab.TerminationStatus.unknown: + "Renderer process did not start", + } + msg = messages[status] + + if qtutils.version_check('5.9'): + url_string = tab.url(requested=True).toDisplayString() + error_page = jinja.render( + 'error.html', title="Error loading {}".format(url_string), + url=url_string, error=msg, icon='') + QTimer.singleShot(0, lambda: tab.set_html(error_page)) + log.webview.error(msg) + else: + # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-58698 + message.error(msg) + 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 ce96607fa..c70d4f4ca 100644 --- a/qutebrowser/mainwindow/tabwidget.py +++ b/qutebrowser/mainwindow/tabwidget.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -22,9 +22,11 @@ import collections import functools -from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QSize, QRect, QTimer, QUrl +from PyQt5.QtCore import (pyqtSignal, pyqtSlot, Qt, QSize, QRect, QPoint, + QTimer, QUrl) from PyQt5.QtWidgets import (QTabWidget, QTabBar, QSizePolicy, QCommonStyle, - QStyle, QStylePainter, QStyleOptionTab) + QStyle, QStylePainter, QStyleOptionTab, + QStyleFactory, QWidget) from PyQt5.QtGui import QIcon, QPalette, QColor from qutebrowser.utils import qtutils, objreg, utils, usertypes, log @@ -50,13 +52,13 @@ class TabWidget(QTabWidget): def __init__(self, win_id, parent=None): super().__init__(parent) - bar = TabBar(win_id) - self.setStyle(TabBarStyle(self.style())) + bar = TabBar(win_id, self) + self.setStyle(TabBarStyle()) self.setTabBar(bar) bar.tabCloseRequested.connect(self.tabCloseRequested) bar.tabMoved.connect(functools.partial( QTimer.singleShot, 0, self.update_tab_titles)) - bar.currentChanged.connect(self.emit_tab_index_changed) + bar.currentChanged.connect(self._on_current_changed) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.setDocumentMode(True) self.setElideMode(Qt.ElideRight) @@ -92,6 +94,33 @@ class TabWidget(QTabWidget): bar.set_tab_data(idx, 'indicator-color', color) bar.update(bar.tabRect(idx)) + def set_tab_pinned(self, tab: QWidget, + pinned: bool, *, loading: bool = False) -> None: + """Set the tab status as pinned. + + Args: + tab: The tab to pin + pinned: Pinned tab state to set. + loading: Whether to ignore current data state when + counting pinned_count. + """ + bar = self.tabBar() + idx = self.indexOf(tab) + + # Only modify pinned_count if we had a change + # always modify pinned_count if we are loading + if tab.data.pinned != pinned or loading: + if pinned: + bar.pinned_count += 1 + elif not pinned: + bar.pinned_count -= 1 + + bar.set_tab_data(idx, 'pinned', pinned) + tab.data.pinned = pinned + self.update_tab_title(idx) + + bar.refresh() + def tab_indicator_color(self, idx): """Get the tab indicator color for the given index.""" return self.tabBar().tab_indicator_color(idx) @@ -107,12 +136,19 @@ class TabWidget(QTabWidget): def update_tab_title(self, idx): """Update the tab text for the given tab.""" + tab = self.widget(idx) fields = self.get_tab_fields(idx) fields['title'] = fields['title'].replace('&', '&&') fields['index'] = idx + 1 fmt = config.get('tabs', 'title-format') - title = '' if fmt is None else fmt.format(**fields) + fmt_pinned = config.get('tabs', 'title-format-pinned') + + if tab.data.pinned: + title = '' if fmt_pinned is None else fmt_pinned.format(**fields) + else: + title = '' if fmt is None else fmt.format(**fields) + self.tabBar().setTabText(idx, title) def get_tab_fields(self, idx): @@ -129,6 +165,7 @@ class TabWidget(QTabWidget): fields['title_sep'] = ' - ' if page_title else '' fields['perc_raw'] = tab.progress() fields['backend'] = objects.backend.name + fields['private'] = ' [Private Mode] ' if tab.private else '' if tab.load_status() == usertypes.LoadStatus.loading: fields['perc'] = '[{}%] '.format(tab.progress()) @@ -153,11 +190,12 @@ class TabWidget(QTabWidget): fields['scroll_pos'] = scroll_pos return fields - @config.change_filter('tabs', 'title-format') - def update_tab_titles(self): + def update_tab_titles(self, section='tabs', option='title-format'): """Update all texts.""" - for idx in range(self.count()): - self.update_tab_title(idx) + if section == 'tabs' and option in ['title-format', + 'title-format-pinned']: + for idx in range(self.count()): + self.update_tab_title(idx) def tabInserted(self, idx): """Update titles when a tab was inserted.""" @@ -228,9 +266,9 @@ class TabWidget(QTabWidget): return new_idx @pyqtSlot(int) - def emit_tab_index_changed(self, index): + def _on_current_changed(self, index): """Emit the tab_index_changed signal if the current tab changed.""" - self.tabBar().on_change() + self.tabBar().on_current_changed() self.tab_index_changed.emit(index, self.count()) def tab_url(self, idx): @@ -262,28 +300,27 @@ class TabBar(QTabBar): 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): super().__init__(parent) self._win_id = win_id - self.setStyle(TabBarStyle(self.style())) + self.setStyle(TabBarStyle()) self.set_font() config_obj = objreg.get('config') config_obj.changed.connect(self.set_font) + config_obj.changed.connect(self.set_icon_size) self.vertical = False - self._page_fullscreen = False self._auto_hide_timer = QTimer() self._auto_hide_timer.setSingleShot(True) self._auto_hide_timer.setInterval( config.get('tabs', 'show-switching-delay')) - self._auto_hide_timer.timeout.connect(self._tabhide) + self._auto_hide_timer.timeout.connect(self.maybe_hide) self.setAutoFillBackground(True) self.set_colors() + self.pinned_count = 0 config_obj.changed.connect(self.set_colors) - QTimer.singleShot(0, self._tabhide) + QTimer.singleShot(0, self.maybe_hide) config_obj.changed.connect(self.on_tab_colors_changed) config_obj.changed.connect(self.on_show_switching_delay_changed) config_obj.changed.connect(self.tabs_show) @@ -291,10 +328,14 @@ class TabBar(QTabBar): def __repr__(self): return utils.get_repr(self, count=self.count()) + def _current_tab(self): + """Get the current tab object.""" + return self.parent().currentWidget() + @config.change_filter('tabs', 'show') def tabs_show(self): """Hide or show tab bar if needed when tabs->show got changed.""" - self._tabhide() + self.maybe_hide() @config.change_filter('tabs', 'show-switching-delay') def on_show_switching_delay_changed(self): @@ -302,24 +343,22 @@ 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): + def on_current_changed(self): """Show tab bar when current tab got changed.""" + self.maybe_hide() # for fullscreen tabs show = config.get('tabs', 'show') - if show == 'switching' or self._page_fullscreen: + if show == 'switching': self.show() self._auto_hide_timer.start() - def _tabhide(self): + @pyqtSlot() + def maybe_hide(self): """Hide the tab bar if needed.""" show = config.get('tabs', 'show') + tab = self._current_tab() if (show in ['never', 'switching'] or (show == 'multiple' and self.count() == 1) or - self._page_fullscreen): + (tab and tab.data.fullscreen)): self.hide() else: self.show() @@ -374,7 +413,13 @@ class TabBar(QTabBar): def set_font(self): """Set the tab bar font.""" self.setFont(config.get('fonts', 'tabbar')) + self.set_icon_size() + + @config.change_filter('tabs', 'favicon-scale') + def set_icon_size(self): + """Set the tab bar favicon size.""" size = self.fontMetrics().height() - 2 + size *= config.get('tabs', 'favicon-scale') self.setIconSize(QSize(size, size)) @config.change_filter('colors', 'tabs.bg.bar') @@ -395,11 +440,12 @@ class TabBar(QTabBar): button = config.get('tabs', 'close-mouse-button') if (e.button() == Qt.RightButton and button == 'right' or e.button() == Qt.MiddleButton and button == 'middle'): + e.accept() idx = self.tabAt(e.pos()) - if idx != -1: - e.accept() - self.tabCloseRequested.emit(idx) - return + if idx == -1: + idx = self.currentIndex() + self.tabCloseRequested.emit(idx) + return super().mousePressEvent(e) def minimumTabSizeHint(self, index): @@ -453,22 +499,48 @@ class TabBar(QTabBar): width = int(confwidth) size = QSize(max(minimum_size.width(), width), height) elif self.count() == 0: - # This happens on startup on OS X. + # This happens on startup on macOS. # We return it directly rather than setting `size' because we don't # want to ensure it's valid in this special case. return QSize() - elif self.count() * minimum_size.width() > self.width(): + else: + tab_width_pinned_conf = config.get('tabs', 'pinned-width') + + try: + pinned = self.tab_data(index, 'pinned') + except KeyError: + pinned = False + + no_pinned_count = self.count() - self.pinned_count + pinned_width = tab_width_pinned_conf * self.pinned_count + no_pinned_width = self.width() - pinned_width + + if pinned: + width = tab_width_pinned_conf + else: + + # Tabs should attempt to occupy the whole window width. If + # there are pinned tabs their size will be subtracted from the + # total window width. During shutdown the self.count goes + # down, but the self.pinned_count not - this generates some odd + # behavior. To avoid this we compare self.count against + # self.pinned_count. If we end up having too little space, we + # set the minimum size below. + if self.pinned_count > 0 and no_pinned_count > 0: + width = no_pinned_width / no_pinned_count + else: + width = self.width() / self.count() + + # If no_pinned_width is not divisible by no_pinned_count, add a + # pixel to some tabs so that there is no ugly leftover space. + if (no_pinned_count > 0 and + index < no_pinned_width % no_pinned_count): + width += 1 + # If we don't have enough space, we return the minimum size so we # get scroll buttons as soon as needed. - size = minimum_size - else: - # If we *do* have enough space, tabs should occupy the whole window - # width. - width = self.width() / self.count() - # If width is not divisible by count, add a pixel to some tabs so - # that there is no ugly leftover space. - if index < self.width() % self.count(): - width += 1 + width = max(width, minimum_size.width()) + size = QSize(width, height) qtutils.ensure_valid(size) return size @@ -512,12 +584,12 @@ class TabBar(QTabBar): def tabInserted(self, idx): """Update visibility when a tab was inserted.""" super().tabInserted(idx) - self._tabhide() + self.maybe_hide() def tabRemoved(self, idx): """Update visibility when a tab was removed.""" super().tabRemoved(idx) - self._tabhide() + self.maybe_hide() def wheelEvent(self, e): """Override wheelEvent to make the action configurable. @@ -553,20 +625,14 @@ class TabBarStyle(QCommonStyle): http://stackoverflow.com/a/17294081 https://code.google.com/p/makehuman/source/browse/trunk/makehuman/lib/qtgui.py - - Attributes: - _style: The base/"parent" style. """ - def __init__(self, style): + def __init__(self): """Initialize all functions we're not overriding. This simply calls the corresponding function in self._style. - - Args: - style: The base/"parent" style. """ - self._style = style + self._style = QStyleFactory.create('Fusion') for method in ['drawComplexControl', 'drawItemPixmap', 'generatedIconPixmap', 'hitTestComplexControl', 'itemPixmapRect', 'itemTextRect', 'polish', 'styleHint', @@ -695,6 +761,17 @@ class TabBarStyle(QCommonStyle): rct = super().subElementRect(sr, opt, widget) return rct else: + try: + # We need this so the left scroll button is aligned properly. + # Otherwise, empty space will be shown after the last tab even + # though the button width is set to 0 + # + # QStyle.SE_TabBarScrollLeftButton was added in Qt 5.7 + if sr == QStyle.SE_TabBarScrollLeftButton: + return super().subElementRect(sr, opt, widget) + except AttributeError: + pass + return self._style.subElementRect(sr, opt, widget) def _tab_layout(self, opt): @@ -775,7 +852,7 @@ class TabBarStyle(QCommonStyle): tab_icon_size = QSize( min(actual_size.width(), icon_size.width()), min(actual_size.height(), icon_size.height())) - icon_rect = QRect(text_rect.left(), text_rect.top() + 1, - tab_icon_size.width(), tab_icon_size.height()) + icon_top = text_rect.center().y() + 1 - tab_icon_size.height() / 2 + icon_rect = QRect(QPoint(text_rect.left(), icon_top), tab_icon_size) icon_rect = self._style.visualRect(opt.direction, opt.rect, icon_rect) return icon_rect diff --git a/qutebrowser/misc/__init__.py b/qutebrowser/misc/__init__.py index 3dc51e6a9..03ad27aa8 100644 --- a/qutebrowser/misc/__init__.py +++ b/qutebrowser/misc/__init__.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/misc/autoupdate.py b/qutebrowser/misc/autoupdate.py index 860064672..15a3b6670 100644 --- a/qutebrowser/misc/autoupdate.py +++ b/qutebrowser/misc/autoupdate.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/misc/checkpyver.py b/qutebrowser/misc/checkpyver.py index 364b8bb57..34183041b 100644 --- a/qutebrowser/misc/checkpyver.py +++ b/qutebrowser/misc/checkpyver.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The-Compiler) +# Copyright 2014-2017 Florian Bruhin (The-Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/misc/cmdhistory.py b/qutebrowser/misc/cmdhistory.py index 7f3dfd52f..baf210a68 100644 --- a/qutebrowser/misc/cmdhistory.py +++ b/qutebrowser/misc/cmdhistory.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -21,7 +21,6 @@ from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject -from qutebrowser.config import config from qutebrowser.utils import usertypes, log @@ -44,7 +43,6 @@ class History(QObject): """Command history. Attributes: - handle_private_mode: Whether to ignore history in private mode. history: A list of executed commands, with newer commands at the end. _tmphist: Temporary history for history browsing (as NeighborList) @@ -54,14 +52,13 @@ class History(QObject): changed = pyqtSignal() - def __init__(self, history=None, parent=None): + def __init__(self, *, history=None, parent=None): """Constructor. Args: history: The initial history to set. """ super().__init__(parent) - self.handle_private_mode = False self._tmphist = None if history is None: self.history = [] @@ -129,9 +126,6 @@ class History(QObject): Args: text: The text to append. """ - if (self.handle_private_mode and - config.get('general', 'private-browsing')): - return if not self.history or text != self.history[-1]: self.history.append(text) self.changed.emit() diff --git a/qutebrowser/misc/consolewidget.py b/qutebrowser/misc/consolewidget.py index 485da0fe4..963076c21 100644 --- a/qutebrowser/misc/consolewidget.py +++ b/qutebrowser/misc/consolewidget.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -50,7 +50,7 @@ class ConsoleLineEdit(miscwidgets.CommandLineEdit): Args: _namespace: The local namespace of the interpreter. """ - super().__init__(parent) + super().__init__(parent=parent) self.update_font() objreg.get('config').changed.connect(self.update_font) self._history = cmdhistory.History(parent=self) diff --git a/qutebrowser/misc/crashdialog.py b/qutebrowser/misc/crashdialog.py index 0add7932a..b670f80ed 100644 --- a/qutebrowser/misc/crashdialog.py +++ b/qutebrowser/misc/crashdialog.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -230,8 +230,7 @@ class _CrashDialog(QDialog): def _init_info_text(self): """Add an info text encouraging the user to report crashes.""" info_label = QLabel("
Note that without your help, I can't fix " - "the bug you encountered.
Crash reports are " - "currently publicly accessible.
", + "the bug you encountered.", wordWrap=True) self._vbox.addWidget(info_label) diff --git a/qutebrowser/misc/crashsignal.py b/qutebrowser/misc/crashsignal.py index 35c39bf2e..e51855af2 100644 --- a/qutebrowser/misc/crashsignal.py +++ b/qutebrowser/misc/crashsignal.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -28,6 +28,11 @@ import functools import faulthandler import os.path import collections +try: + # WORKAROUND for segfaults when using pdb in pytest for some reason... + import readline # pylint: disable=unused-import +except ImportError: + pass from PyQt5.QtCore import (pyqtSlot, qInstallMessageHandler, QObject, QSocketNotifier, QTimer, QUrl) @@ -171,14 +176,14 @@ class CrashHandler(QObject): """ try: pages = self._recover_pages(forgiving=True) - except Exception: - log.destroy.exception("Error while recovering pages") + except Exception as e: + log.destroy.exception("Error while recovering pages: {}".format(e)) pages = [] try: cmd_history = objreg.get('command-history')[-5:] - except Exception: - log.destroy.exception("Error while getting history: {}") + except Exception as e: + log.destroy.exception("Error while getting history: {}".format(e)) cmd_history = [] try: @@ -207,10 +212,10 @@ class CrashHandler(QObject): is_ignored_exception = (exctype is bdb.BdbQuit or not issubclass(exctype, Exception)) - if self._args.pdb_postmortem: + if 'pdb-postmortem' in self._args.debug_flags: pdb.post_mortem(tb) - if is_ignored_exception or self._args.pdb_postmortem: + if is_ignored_exception or 'pdb-postmortem' in self._args.debug_flags: # pdb exit, KeyboardInterrupt, ... status = 0 if is_ignored_exception else 2 try: diff --git a/qutebrowser/misc/earlyinit.py b/qutebrowser/misc/earlyinit.py index 3622dc36a..e43cd8891 100644 --- a/qutebrowser/misc/earlyinit.py +++ b/qutebrowser/misc/earlyinit.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The-Compiler) +# Copyright 2014-2017 Florian Bruhin (The-Compiler) # # This file is part of qutebrowser. # @@ -35,12 +35,15 @@ import faulthandler import traceback import signal import importlib -import pkg_resources import datetime +import logging try: import tkinter except ImportError: tkinter = None + +import pkg_resources + # NOTE: No qutebrowser or PyQt import should be done here, as some early # initialization needs to take place before that! @@ -63,15 +66,7 @@ def _missing_str(name, *, windows=None, pip=None, webengine=False): 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 Ubuntu), so you need to start without --backend ' - 'webengine there.'), - ('QtWebEngine is currently unsupported with the OS X .app, see ' - 'https://github.com/qutebrowser/qutebrowser/issues/1692'), - ] - else: + if not webengine: lines = ['If you installed a qutebrowser package for your ' 'distribution, please report this as a bug.'] blocks.append('
'.join(lines)) @@ -259,14 +254,29 @@ def get_backend(args): return 'webengine' +def qt_version(qversion=None, qt_version_str=None): + """Get a Qt version string based on the runtime/compiled versions.""" + if qversion is None: + from PyQt5.QtCore import qVersion + qversion = qVersion() + if qt_version_str is None: + from PyQt5.QtCore import QT_VERSION_STR + qt_version_str = QT_VERSION_STR + + if qversion != qt_version_str: + return '{} (compiled {})'.format(qversion, qt_version_str) + else: + return qversion + + def check_qt_version(backend): """Check if the Qt version is recent enough.""" from PyQt5.QtCore import PYQT_VERSION, PYQT_VERSION_STR - from qutebrowser.utils import qtutils, version + from qutebrowser.utils import qtutils 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 {} is installed.".format(qt_version(), PYQT_VERSION_STR)) _die(text) elif (backend == 'webengine' and ( @@ -274,21 +284,32 @@ def check_qt_version(backend): 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)) + .format(qt_version(), PYQT_VERSION_STR)) _die(text) -def check_ssl_support(): +def check_ssl_support(backend): """Check if SSL support is available.""" + from qutebrowser.utils import log + try: from PyQt5.QtNetwork import QSslSocket except ImportError: - ok = False - else: - ok = QSslSocket.supportsSsl() - if not ok: - text = "Fatal error: Your Qt is built without SSL support." - _die(text) + _die("Fatal error: Your Qt is built without SSL support.") + + text = ("Could not initialize QtNetwork SSL support. If you use " + "OpenSSL 1.1 with a PyQt package from PyPI (e.g. on Archlinux " + "or Debian Stretch), you need to set LD_LIBRARY_PATH to the path " + "of OpenSSL 1.0.") + if backend == 'webengine': + text += " This only affects downloads." + + if not QSslSocket.supportsSsl(): + if backend == 'webkit': + _die("Could not initialize SSL support.") + else: + assert backend == 'webengine' + log.init.warning(text) def check_libraries(backend): @@ -316,10 +337,19 @@ def check_libraries(backend): "http://pyyaml.org/download/pyyaml/ (py3.4) " "or Install via pip.", pip="PyYAML"), + 'PyQt5.QtQml': _missing_str("PyQt5.QtQml"), + 'PyQt5.QtSql': _missing_str("PyQt5.QtSql"), } if backend == 'webengine': modules['PyQt5.QtWebEngineWidgets'] = _missing_str("QtWebEngine", webengine=True) + modules['PyQt5.QtOpenGL'] = _missing_str("PyQt5.QtOpenGL") + # 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 + modules['OpenGL.GL'] = _missing_str("PyOpenGL") else: assert backend == 'webkit' modules['PyQt5.QtWebKit'] = _missing_str("PyQt5.QtWebKit") @@ -409,6 +439,6 @@ def earlyinit(args): check_qt_version(backend) remove_inputhook() check_libraries(backend) - check_ssl_support() + check_ssl_support(backend) check_optimize_flag() set_backend(backend) diff --git a/qutebrowser/misc/editor.py b/qutebrowser/misc/editor.py index a6f2854d8..58a08daf1 100644 --- a/qutebrowser/misc/editor.py +++ b/qutebrowser/misc/editor.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/misc/guiprocess.py b/qutebrowser/misc/guiprocess.py index eb0a036e5..95bfac79e 100644 --- a/qutebrowser/misc/guiprocess.py +++ b/qutebrowser/misc/guiprocess.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -142,6 +142,7 @@ class GUIProcess(QObject): self._proc.start(cmd, args) else: self._proc.start(cmd, args, mode) + self._proc.closeWriteChannel() def start_detached(self, cmd, args, cwd=None): """Convenience wrapper around QProcess::startDetached.""" @@ -153,8 +154,8 @@ class GUIProcess(QObject): log.procs.debug("Process started.") self._started = True else: - message.error("Error while spawning {}: {}.".format( - self._what, self._proc.error())) + message.error("Error while spawning {}: {}".format( + self._what, ERROR_STRINGS[self._proc.error()])) def exit_status(self): return self._proc.exitStatus() diff --git a/qutebrowser/misc/httpclient.py b/qutebrowser/misc/httpclient.py index fc6f94f59..4d33d487e 100644 --- a/qutebrowser/misc/httpclient.py +++ b/qutebrowser/misc/httpclient.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/misc/ipc.py b/qutebrowser/misc/ipc.py index f6567a11f..562cc84cc 100644 --- a/qutebrowser/misc/ipc.py +++ b/qutebrowser/misc/ipc.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -20,7 +20,6 @@ """Utilities for IPC with existing instances.""" import os -import sys import time import json import getpass @@ -41,8 +40,8 @@ ATIME_INTERVAL = 60 * 60 * 3 * 1000 # 3 hours PROTOCOL_VERSION = 1 -def _get_socketname_legacy(basedir): - """Legacy implementation of _get_socketname.""" +def _get_socketname_windows(basedir): + """Get a socketname to use for Windows.""" parts = ['qutebrowser', getpass.getuser()] if basedir is not None: md5 = hashlib.md5(basedir.encode('utf-8')).hexdigest() @@ -50,10 +49,10 @@ def _get_socketname_legacy(basedir): return '-'.join(parts) -def _get_socketname(basedir, legacy=False): +def _get_socketname(basedir): """Get a socketname to use.""" - if legacy or os.name == 'nt': - return _get_socketname_legacy(basedir) + if os.name == 'nt': # pragma: no cover + return _get_socketname_windows(basedir) parts_to_hash = [getpass.getuser()] if basedir is not None: @@ -415,41 +414,7 @@ class IPCServer(QObject): self._remove_server() -def _has_legacy_server(name): - """Check if there is a legacy server. - - Args: - name: The name to try to connect to. - - Return: - True if there is a server with the given name, False otherwise. - """ - socket = QLocalSocket() - log.ipc.debug("Trying to connect to {}".format(name)) - socket.connectToServer(name) - - err = socket.error() - - if err != QLocalSocket.UnknownSocketError: - log.ipc.debug("Socket error: {} ({})".format( - socket.errorString(), err)) - - os_x_fail = (sys.platform == 'darwin' and - socket.errorString() == 'QLocalSocket::connectToServer: ' - 'Unknown error 38') - - if err not in [QLocalSocket.ServerNotFoundError, - QLocalSocket.ConnectionRefusedError] and not os_x_fail: - return True - - socket.disconnectFromServer() - if socket.state() != QLocalSocket.UnconnectedState: - socket.waitForDisconnected(CONNECT_TIMEOUT) - return False - - -def send_to_running_instance(socketname, command, target_arg, *, - legacy_name=None, socket=None): +def send_to_running_instance(socketname, command, target_arg, *, socket=None): """Try to send a commandline to a running instance. Blocks for CONNECT_TIMEOUT ms. @@ -459,7 +424,6 @@ def send_to_running_instance(socketname, command, target_arg, *, command: The command to send to the running instance. target_arg: --target command line argument socket: The socket to read data from, or None. - legacy_name: The legacy name to first try to connect to. Return: True if connecting was successful, False if no connection was made. @@ -467,13 +431,8 @@ def send_to_running_instance(socketname, command, target_arg, *, if socket is None: socket = QLocalSocket() - if legacy_name is not None and _has_legacy_server(legacy_name): - name_to_use = legacy_name - else: - name_to_use = socketname - - log.ipc.debug("Connecting to {}".format(name_to_use)) - socket.connectToServer(name_to_use) + log.ipc.debug("Connecting to {}".format(socketname)) + socket.connectToServer(socketname) connected = socket.waitForConnected(CONNECT_TIMEOUT) if connected: @@ -527,12 +486,10 @@ def send_or_listen(args): None if an instance was running and received our request. """ socketname = _get_socketname(args.basedir) - legacy_socketname = _get_socketname(args.basedir, legacy=True) try: try: sent = send_to_running_instance(socketname, args.command, - args.target, - legacy_name=legacy_socketname) + args.target) if sent: return None log.init.debug("Starting IPC server...") @@ -545,8 +502,7 @@ def send_or_listen(args): log.init.debug("Got AddressInUseError, trying again.") time.sleep(0.5) sent = send_to_running_instance(socketname, args.command, - args.target, - legacy_name=legacy_socketname) + args.target) if sent: return None else: diff --git a/qutebrowser/misc/keyhintwidget.py b/qutebrowser/misc/keyhintwidget.py index d32b24eb9..b42612b1b 100644 --- a/qutebrowser/misc/keyhintwidget.py +++ b/qutebrowser/misc/keyhintwidget.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Ryan Roden-Corrent (rcorre) +# Copyright 2016-2017 Ryan Roden-Corrent (rcorre) # # This file is part of qutebrowser. # diff --git a/qutebrowser/misc/lineparser.py b/qutebrowser/misc/lineparser.py index 55bae7142..2256d9697 100644 --- a/qutebrowser/misc/lineparser.py +++ b/qutebrowser/misc/lineparser.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -21,7 +21,6 @@ import os import os.path -import itertools import contextlib from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject @@ -96,7 +95,7 @@ class BaseLineParser(QObject): """ assert self._configfile is not None if self._opened: - raise IOError("Refusing to double-open AppendLineParser.") + raise IOError("Refusing to double-open LineParser.") self._opened = True try: if self._binary: @@ -133,73 +132,6 @@ class BaseLineParser(QObject): raise NotImplementedError -class AppendLineParser(BaseLineParser): - - """LineParser which reads lazily and appends data to existing one. - - Attributes: - _new_data: The data which was added in this session. - """ - - def __init__(self, configdir, fname, *, parent=None): - super().__init__(configdir, fname, binary=False, parent=parent) - self.new_data = [] - self._fileobj = None - - def __iter__(self): - if self._fileobj is None: - raise ValueError("Iterating without open() being called!") - file_iter = (line.rstrip('\n') for line in self._fileobj) - return itertools.chain(file_iter, iter(self.new_data)) - - @contextlib.contextmanager - def open(self): - """Open the on-disk history file. Needed for __iter__.""" - try: - with self._open('r') as f: - self._fileobj = f - yield - except FileNotFoundError: - self._fileobj = [] - yield - finally: - self._fileobj = None - - def get_recent(self, count=4096): - """Get the last count bytes from the underlying file.""" - with self._open('r') as f: - f.seek(0, os.SEEK_END) - size = f.tell() - try: - if size - count > 0: - offset = size - count - else: - offset = 0 - f.seek(offset) - data = f.readlines() - finally: - f.seek(0, os.SEEK_END) - return data - - def save(self): - do_save = self._prepare_save() - if not do_save: - return - with self._open('a') as f: - self._write(f, self.new_data) - self.new_data = [] - self._after_save() - - def clear(self): - do_save = self._prepare_save() - if not do_save: - return - with self._open('w'): - pass - self.new_data = [] - self._after_save() - - class LineParser(BaseLineParser): """Parser for configuration files which are simply line-based. @@ -240,7 +172,7 @@ class LineParser(BaseLineParser): def save(self): """Save the config file.""" if self._opened: - raise IOError("Refusing to double-open AppendLineParser.") + raise IOError("Refusing to double-open LineParser.") do_save = self._prepare_save() if not do_save: return diff --git a/qutebrowser/misc/miscwidgets.py b/qutebrowser/misc/miscwidgets.py index 956d60d4a..b4a17e638 100644 --- a/qutebrowser/misc/miscwidgets.py +++ b/qutebrowser/misc/miscwidgets.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -69,7 +69,7 @@ class CommandLineEdit(QLineEdit): _promptlen: The length of the current prompt. """ - def __init__(self, parent=None): + def __init__(self, *, parent=None): super().__init__(parent) self.history = cmdhistory.History(parent=self) self._validator = _CommandValidator(self) diff --git a/qutebrowser/misc/msgbox.py b/qutebrowser/misc/msgbox.py index f6f29c38d..2c8aaf85e 100644 --- a/qutebrowser/misc/msgbox.py +++ b/qutebrowser/misc/msgbox.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/misc/pastebin.py b/qutebrowser/misc/pastebin.py index 40dd77f33..9609481b6 100644 --- a/qutebrowser/misc/pastebin.py +++ b/qutebrowser/misc/pastebin.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -26,7 +26,7 @@ from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QUrl class PastebinClient(QObject): - """A client for http://p.cmpl.cc/ using HTTPClient. + """A client for Stikked pastebins using HTTPClient. Attributes: _client: The HTTPClient used. @@ -41,7 +41,7 @@ class PastebinClient(QObject): arg: The error message, as string. """ - API_URL = 'http://paste.the-compiler.org/api/' + API_URL = 'https://crashes.qutebrowser.org/api/' success = pyqtSignal(str) error = pyqtSignal(str) diff --git a/qutebrowser/misc/readline.py b/qutebrowser/misc/readline.py index 0089ebe7c..2bc999c4d 100644 --- a/qutebrowser/misc/readline.py +++ b/qutebrowser/misc/readline.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/misc/savemanager.py b/qutebrowser/misc/savemanager.py index 7e85b013e..509e5489a 100644 --- a/qutebrowser/misc/savemanager.py +++ b/qutebrowser/misc/savemanager.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/misc/sessions.py b/qutebrowser/misc/sessions.py index 6ad8358a6..4fe0fe4c7 100644 --- a/qutebrowser/misc/sessions.py +++ b/qutebrowser/misc/sessions.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -20,9 +20,9 @@ """Management of sessions - saved tabs/windows.""" import os -import sip import os.path +import sip from PyQt5.QtCore import pyqtSignal, QUrl, QObject, QPoint, QTimer from PyQt5.QtWidgets import QApplication import yaml @@ -31,10 +31,11 @@ try: except ImportError: # pragma: no cover from yaml import SafeLoader as YamlLoader, SafeDumper as YamlDumper -from qutebrowser.utils import (standarddir, objreg, qtutils, log, usertypes, - message, utils) +from qutebrowser.utils import (standarddir, objreg, qtutils, log, message, + utils) from qutebrowser.commands import cmdexc, cmdutils from qutebrowser.config import config +from qutebrowser.completion.models import miscmodels default = object() # Sentinel value @@ -195,6 +196,9 @@ class SessionManager(QObject): if 'scroll-pos' in user_data: pos = user_data['scroll-pos'] data['scroll-pos'] = {'x': pos.x(), 'y': pos.y()} + + data['pinned'] = tab.data.pinned + return data def _save_tab(self, tab, active): @@ -213,7 +217,7 @@ class SessionManager(QObject): data['history'].append(item_data) return data - def _save_all(self, *, only_window=None): + def _save_all(self, *, only_window=None, with_private=False): """Get a dict with data for all windows/tabs.""" data = {'windows': []} if only_window is not None: @@ -221,7 +225,7 @@ class SessionManager(QObject): else: winlist = objreg.window_registry - for win_id in winlist: + for win_id in sorted(winlist): tabbed_browser = objreg.get('tabbed-browser', scope='window', window=win_id) main_window = objreg.get('main-window', scope='window', @@ -231,12 +235,17 @@ class SessionManager(QObject): if sip.isdeleted(main_window): continue + if tabbed_browser.private and not with_private: + continue + win_data = {} active_window = QApplication.instance().activeWindow() if getattr(active_window, 'win_id', None) == win_id: win_data['active'] = True win_data['geometry'] = bytes(main_window.saveGeometry()) win_data['tabs'] = [] + if tabbed_browser.private: + win_data['private'] = True for i, tab in enumerate(tabbed_browser.widgets()): active = i == tabbed_browser.currentIndex() win_data['tabs'].append(self._save_tab(tab, active)) @@ -260,7 +269,7 @@ class SessionManager(QObject): return name def save(self, name, last_window=False, load_next_time=False, - only_window=None): + only_window=None, with_private=False): """Save a named session. Args: @@ -270,6 +279,7 @@ class SessionManager(QObject): 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. + with_private: Include private windows. Return: The name of the saved session. @@ -284,7 +294,8 @@ class SessionManager(QObject): log.sessions.error("last_window_session is None while saving!") return else: - data = self._save_all(only_window=only_window) + data = self._save_all(only_window=only_window, + with_private=with_private) log.sessions.vdebug("Saving data: {}".format(data)) try: with qtutils.savefile_open(path) as f: @@ -345,6 +356,9 @@ class SessionManager(QObject): pos = histentry['scroll-pos'] user_data['scroll-pos'] = QPoint(pos['x'], pos['y']) + if 'pinned' in histentry: + new_tab.data.pinned = histentry['pinned'] + active = histentry.get('active', False) url = QUrl.fromEncoded(histentry['url'].encode('ascii')) if 'original-url' in histentry: @@ -377,9 +391,11 @@ class SessionManager(QObject): data = yaml.load(f, Loader=YamlLoader) except (OSError, UnicodeDecodeError, yaml.YAMLError) as e: raise SessionError(e) + log.sessions.debug("Loading session {} from {}...".format(name, path)) for win in data['windows']: - window = mainwindow.MainWindow(geometry=win['geometry']) + window = mainwindow.MainWindow(geometry=win['geometry'], + private=win.get('private', None)) window.show() tabbed_browser = objreg.get('tabbed-browser', scope='window', window=window.win_id) @@ -389,18 +405,26 @@ class SessionManager(QObject): self._load_tab(new_tab, tab) if tab.get('active', False): tab_to_focus = i + if new_tab.data.pinned: + tabbed_browser.set_tab_pinned( + new_tab, new_tab.data.pinned, loading=True) if tab_to_focus is not None: tabbed_browser.setCurrentIndex(tab_to_focus) if win.get('active', False): QTimer.singleShot(0, tabbed_browser.activateWindow) - self.did_load = True + + if data['windows']: + self.did_load = True if not name.startswith('_') and not temp: self._current = name def delete(self, name): """Delete a session.""" path = self._get_session_path(name, check_exists=True) - os.remove(path) + try: + os.remove(path) + except OSError as e: + raise SessionError(e) self.update_completion.emit() def list_sessions(self): @@ -413,7 +437,7 @@ class SessionManager(QObject): return sessions @cmdutils.register(instance='session-manager') - @cmdutils.argument('name', completion=usertypes.Completion.sessions) + @cmdutils.argument('name', completion=miscmodels.session) def session_load(self, name, clear=False, temp=False, force=False): """Load a session. @@ -441,10 +465,12 @@ class SessionManager(QObject): win.close() @cmdutils.register(name=['session-save', 'w'], instance='session-manager') - @cmdutils.argument('name', completion=usertypes.Completion.sessions) + @cmdutils.argument('name', completion=miscmodels.session) @cmdutils.argument('win_id', win_id=True) - def session_save(self, name: str=default, current=False, quiet=False, - force=False, only_active_window=False, win_id=None): + @cmdutils.argument('with_private', flag='p') + def session_save(self, name: str = default, current=False, quiet=False, + force=False, only_active_window=False, with_private=False, + win_id=None): """Save a session. Args: @@ -454,10 +480,9 @@ class SessionManager(QObject): 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. + with_private: Include private windows. """ - if (name is not default and - name.startswith('_') and # pylint: disable=no-member - not force): + if name is not default and name.startswith('_') and not force: raise cmdexc.CommandError("{} is an internal session, use --force " "to save anyways.".format(name)) if current: @@ -467,9 +492,10 @@ class SessionManager(QObject): assert not name.startswith('_') try: if only_active_window: - name = self.save(name, only_window=win_id) + name = self.save(name, only_window=win_id, + with_private=with_private) else: - name = self.save(name) + name = self.save(name, with_private=with_private) except SessionError as e: raise cmdexc.CommandError("Error while saving session: {}" .format(e)) @@ -478,7 +504,7 @@ class SessionManager(QObject): message.info("Saved session {}.".format(name)) @cmdutils.register(instance='session-manager') - @cmdutils.argument('name', completion=usertypes.Completion.sessions) + @cmdutils.argument('name', completion=miscmodels.session) def session_delete(self, name, force=False): """Delete a session. @@ -494,7 +520,7 @@ class SessionManager(QObject): self.delete(name) except SessionNotFoundError: raise cmdexc.CommandError("Session {} not found!".format(name)) - except (OSError, SessionError) as e: + except SessionError as e: log.sessions.exception("Error while deleting session!") raise cmdexc.CommandError("Error while deleting session: {}" .format(e)) diff --git a/qutebrowser/misc/split.py b/qutebrowser/misc/split.py index cb7a38d81..3f3b2d362 100644 --- a/qutebrowser/misc/split.py +++ b/qutebrowser/misc/split.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/misc/sql.py b/qutebrowser/misc/sql.py new file mode 100644 index 000000000..a288df475 --- /dev/null +++ b/qutebrowser/misc/sql.py @@ -0,0 +1,256 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2016-2017 Ryan Roden-Corrent (rcorre) +# +# 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 . + +"""Provides access to an in-memory sqlite database.""" + +import collections + +from PyQt5.QtCore import QObject, pyqtSignal +from PyQt5.QtSql import QSqlDatabase, QSqlQuery + +from qutebrowser.utils import log + + +class SqlException(Exception): + + """Raised on an error interacting with the SQL database.""" + + pass + + +def init(db_path): + """Initialize the SQL database connection.""" + database = QSqlDatabase.addDatabase('QSQLITE') + if not database.isValid(): + raise SqlException('Failed to add database. ' + 'Are sqlite and Qt sqlite support installed?') + database.setDatabaseName(db_path) + if not database.open(): + raise SqlException("Failed to open sqlite database at {}: {}" + .format(db_path, database.lastError().text())) + + +def close(): + """Close the SQL connection.""" + QSqlDatabase.removeDatabase(QSqlDatabase.database().connectionName()) + + +def version(): + """Return the sqlite version string.""" + try: + if not QSqlDatabase.database().isOpen(): + init(':memory:') + ver = Query("select sqlite_version()").run().value() + close() + return ver + return Query("select sqlite_version()").run().value() + except SqlException as e: + return 'UNAVAILABLE ({})'.format(e) + + +class Query(QSqlQuery): + + """A prepared SQL Query.""" + + def __init__(self, querystr, forward_only=True): + """Prepare a new sql query. + + Args: + querystr: String to prepare query from. + forward_only: Optimization for queries that will only step forward. + Must be false for completion queries. + """ + super().__init__(QSqlDatabase.database()) + log.sql.debug('Preparing SQL query: "{}"'.format(querystr)) + if not self.prepare(querystr): + raise SqlException('Failed to prepare query "{}": "{}"'.format( + querystr, self.lastError().text())) + self.setForwardOnly(forward_only) + + def __iter__(self): + if not self.isActive(): + raise SqlException("Cannot iterate inactive query") + rec = self.record() + fields = [rec.fieldName(i) for i in range(rec.count())] + rowtype = collections.namedtuple('ResultRow', fields) + + while self.next(): + rec = self.record() + yield rowtype(*[rec.value(i) for i in range(rec.count())]) + + def run(self, **values): + """Execute the prepared query.""" + log.sql.debug('Running SQL query: "{}"'.format(self.lastQuery())) + for key, val in values.items(): + self.bindValue(':{}'.format(key), val) + log.sql.debug('query bindings: {}'.format(self.boundValues())) + if not self.exec_(): + raise SqlException('Failed to exec query "{}": "{}"'.format( + self.lastQuery(), self.lastError().text())) + return self + + def value(self): + """Return the result of a single-value query (e.g. an EXISTS).""" + if not self.next(): + raise SqlException("No result for single-result query") + return self.record().value(0) + + +class SqlTable(QObject): + + """Interface to a sql table. + + Attributes: + _name: Name of the SQL table this wraps. + + Signals: + changed: Emitted when the table is modified. + """ + + changed = pyqtSignal() + + def __init__(self, name, fields, constraints=None, parent=None): + """Create a new table in the sql database. + + Raises SqlException if the table already exists. + + Args: + name: Name of the table. + fields: A list of field names. + constraints: A dict mapping field names to constraint strings. + """ + super().__init__(parent) + self._name = name + + constraints = constraints or {} + column_defs = ['{} {}'.format(field, constraints.get(field, '')) + for field in fields] + q = Query("CREATE TABLE IF NOT EXISTS {name} ({column_defs})" + .format(name=name, column_defs=', '.join(column_defs))) + + q.run() + + def create_index(self, name, field): + """Create an index over this table. + + Args: + name: Name of the index, should be unique. + field: Name of the field to index. + """ + q = Query("CREATE INDEX IF NOT EXISTS {name} ON {table} ({field})" + .format(name=name, table=self._name, field=field)) + q.run() + + def __iter__(self): + """Iterate rows in the table.""" + q = Query("SELECT * FROM {table}".format(table=self._name)) + q.run() + return iter(q) + + def contains_query(self, field): + """Return a prepared query that checks for the existence of an item. + + Args: + field: Field to match. + """ + return Query( + "SELECT EXISTS(SELECT * FROM {table} WHERE {field} = :val)" + .format(table=self._name, field=field)) + + def __len__(self): + """Return the count of rows in the table.""" + q = Query("SELECT count(*) FROM {table}".format(table=self._name)) + q.run() + return q.value() + + def delete(self, field, value): + """Remove all rows for which `field` equals `value`. + + Args: + field: Field to use as the key. + value: Key value to delete. + + Return: + The number of rows deleted. + """ + q = Query("DELETE FROM {table} where {field} = :val" + .format(table=self._name, field=field)) + q.run(val=value) + if not q.numRowsAffected(): + raise KeyError('No row with {} = "{}"'.format(field, value)) + self.changed.emit() + + def _insert_query(self, values, replace): + params = ', '.join(':{}'.format(key) for key in values) + verb = "REPLACE" if replace else "INSERT" + return Query("{verb} INTO {table} ({columns}) values({params})".format( + verb=verb, table=self._name, columns=', '.join(values), + params=params)) + + def insert(self, values, replace=False): + """Append a row to the table. + + Args: + values: A dict with a value to insert for each field name. + replace: If set, replace existing values. + """ + q = self._insert_query(values, replace) + q.run(**values) + self.changed.emit() + + def insert_batch(self, values, replace=False): + """Performantly append multiple rows to the table. + + Args: + values: A dict with a list of values to insert for each field name. + replace: If true, overwrite rows with a primary key match. + """ + q = self._insert_query(values, replace) + for key, val in values.items(): + q.bindValue(':{}'.format(key), val) + + db = QSqlDatabase.database() + db.transaction() + if not q.execBatch(): + raise SqlException('Failed to exec query "{}": "{}"'.format( + q.lastQuery(), q.lastError().text())) + db.commit() + self.changed.emit() + + def delete_all(self): + """Remove all rows from the table.""" + Query("DELETE FROM {table}".format(table=self._name)).run() + self.changed.emit() + + def select(self, sort_by, sort_order, limit=-1): + """Prepare, run, and return a select statement on this table. + + Args: + sort_by: name of column to sort by. + sort_order: 'asc' or 'desc'. + limit: max number of rows in result, defaults to -1 (unlimited). + + Return: A prepared and executed select query. + """ + q = Query("SELECT * FROM {table} ORDER BY {sort_by} {sort_order} " + "LIMIT :limit" + .format(table=self._name, sort_by=sort_by, + sort_order=sort_order)) + q.run(limit=limit) + return q diff --git a/qutebrowser/misc/utilcmds.py b/qutebrowser/misc/utilcmds.py index e617c1af2..d1771c212 100644 --- a/qutebrowser/misc/utilcmds.py +++ b/qutebrowser/misc/utilcmds.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -170,8 +170,15 @@ def debug_cache_stats(): """Print LRU cache stats.""" config_info = objreg.get('config').get.cache_info() style_info = style.get_stylesheet.cache_info() + try: + from PyQt5.QtWebKit import QWebHistoryInterface + interface = QWebHistoryInterface.defaultInterface() + history_info = interface.historyContains.cache_info() + except ImportError: + history_info = None log.misc.debug('config: {}'.format(config_info)) log.misc.debug('style: {}'.format(style_info)) + log.misc.debug('history: {}'.format(history_info)) @cmdutils.register(debug=True) @@ -228,7 +235,7 @@ def debug_pyeval(s, quiet=False): else: tabbed_browser = objreg.get('tabbed-browser', scope='window', window='last-focused') - tabbed_browser.openurl(QUrl('qute:pyeval'), newtab=True) + tabbed_browser.openurl(QUrl('qute://pyeval'), newtab=True) @cmdutils.register(debug=True) @@ -293,15 +300,24 @@ def debug_log_filter(filters: str): """Change the log filter for console logging. Args: - filters: A comma separated list of logger names. + filters: A comma separated list of logger names. Can also be "none" to + clear any existing filters. """ - if set(filters.split(',')).issubset(log.LOGGER_NAMES): - log.console_filter.names = filters.split(',') - else: + if log.console_filter is None: + raise cmdexc.CommandError("No log.console_filter. Not attached " + "to a console?") + + if filters.strip().lower() == 'none': + log.console_filter.names = None + return + + if not set(filters.split(',')).issubset(log.LOGGER_NAMES): raise cmdexc.CommandError("filters: Invalid value {} - expected one " "of: {}".format(filters, ', '.join(log.LOGGER_NAMES))) + log.console_filter.names = filters.split(',') + @cmdutils.register() @cmdutils.argument('current_win_id', win_id=True) diff --git a/qutebrowser/qutebrowser.py b/qutebrowser/qutebrowser.py index 8321fb04b..e163a9f8d 100644 --- a/qutebrowser/qutebrowser.py +++ b/qutebrowser/qutebrowser.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # This file is part of qutebrowser. # @@ -23,7 +23,6 @@ import sys import json import qutebrowser -from qutebrowser.utils import log try: from qutebrowser.misc.checkpyver import check_python_version except ImportError: @@ -38,8 +37,9 @@ except ImportError: sys.stderr.flush() sys.exit(100) check_python_version() +from qutebrowser.utils import log -import argparse +import argparse # pylint: disable=wrong-import-order from qutebrowser.misc import earlyinit @@ -64,8 +64,7 @@ def get_argparser(): help="How URLs should be opened if there is already a " "qutebrowser instance running.") parser.add_argument('--backend', choices=['webkit', 'webengine'], - help="Which backend to use (webengine backend is " - "EXPERIMENTAL!).") + help="Which backend to use.") 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 " @@ -103,10 +102,6 @@ def get_argparser(): help="Silently remove unknown config options.") debug.add_argument('--nowindow', action='store_true', help="Don't show " "the main window.") - debug.add_argument('--debug-exit', help="Turn on debugging of late exit.", - action='store_true') - debug.add_argument('--pdb-postmortem', action='store_true', - help="Drop into pdb on exceptions.") debug.add_argument('--temp-basedir', action='store_true', help="Use a " "temporary basedir.") debug.add_argument('--no-err-windows', action='store_true', help="Don't " @@ -118,6 +113,9 @@ def get_argparser(): action='append') debug.add_argument('--qt-flag', help="Pass an argument to Qt as flag.", nargs=1, action='append') + debug.add_argument('--debug-flag', type=debug_flag_error, default=[], + help="Pass name of debugging feature to be turned on.", + action='append', dest='debug_flags') parser.add_argument('command', nargs='*', help="Commands to execute on " "startup.", metavar=':command') # URLs will actually be in command @@ -131,7 +129,7 @@ def directory(arg): raise argparse.ArgumentTypeError("Invalid empty value") -def logfilter_error(logfilter: str): +def logfilter_error(logfilter): """Validate logger names passed to --logfilter. Args: @@ -145,15 +143,25 @@ def logfilter_error(logfilter: str): logfilter, ', '.join(log.LOGGER_NAMES))) +def debug_flag_error(flag): + """Validate flags passed to --debug-flag. + + Available flags: + debug-exit: Turn on debugging of late exit. + pdb-postmortem: Drop into pdb on exceptions. + """ + valid_flags = ['debug-exit', 'pdb-postmortem'] + + if flag in valid_flags: + return flag + else: + raise argparse.ArgumentTypeError("Invalid debug flag - valid flags: {}" + .format(', '.join(valid_flags))) + + def main(): parser = get_argparser() - if sys.platform == 'darwin' and getattr(sys, 'frozen', False): - # Ignore Mac OS X' idiotic -psn_* argument... - # http://stackoverflow.com/questions/19661298/ - # http://sourceforge.net/p/cx-freeze/mailman/message/31041783/ - argv = [arg for arg in sys.argv[1:] if not arg.startswith('-psn_0_')] - else: - argv = sys.argv[1:] + argv = sys.argv[1:] args = parser.parse_args(argv) if args.json_args is not None: # Restoring after a restart. diff --git a/qutebrowser/utils/__init__.py b/qutebrowser/utils/__init__.py index 763345a7b..c28a40b10 100644 --- a/qutebrowser/utils/__init__.py +++ b/qutebrowser/utils/__init__.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/utils/debug.py b/qutebrowser/utils/debug.py index 89ae62faf..5da5234a9 100644 --- a/qutebrowser/utils/debug.py +++ b/qutebrowser/utils/debug.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -156,6 +156,10 @@ def qflags_key(base, value, add_base=False, klass=None): klass = value.__class__ if klass == int: raise TypeError("Can't guess enum class of an int!") + + if not value: + return qenum_key(base, value, add_base, klass) + bits = [] names = [] mask = 0x01 diff --git a/qutebrowser/utils/docutils.py b/qutebrowser/utils/docutils.py index 40cb0cb70..1a3b4312d 100644 --- a/qutebrowser/utils/docutils.py +++ b/qutebrowser/utils/docutils.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/utils/error.py b/qutebrowser/utils/error.py index 6a818857f..0d045bf19 100644 --- a/qutebrowser/utils/error.py +++ b/qutebrowser/utils/error.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/utils/javascript.py b/qutebrowser/utils/javascript.py index 4fc7e546c..f536fed1f 100644 --- a/qutebrowser/utils/javascript.py +++ b/qutebrowser/utils/javascript.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/utils/jinja.py b/qutebrowser/utils/jinja.py index 2ad2be448..731dec8e4 100644 --- a/qutebrowser/utils/jinja.py +++ b/qutebrowser/utils/jinja.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/utils/log.py b/qutebrowser/utils/log.py index 7c96d4072..6cdc61f41 100644 --- a/qutebrowser/utils/log.py +++ b/qutebrowser/utils/log.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -94,7 +94,7 @@ LOGGER_NAMES = [ 'commands', 'signals', 'downloads', 'js', 'qt', 'rfc6266', 'ipc', 'shlexer', 'save', 'message', 'config', 'sessions', - 'webelem', 'prompt', 'network' + 'webelem', 'prompt', 'network', 'sql' ] @@ -141,6 +141,7 @@ sessions = logging.getLogger('sessions') webelem = logging.getLogger('webelem') prompt = logging.getLogger('prompt') network = logging.getLogger('network') +sql = logging.getLogger('sql') ram_handler = None @@ -161,11 +162,6 @@ def stub(suffix=''): misc.warning(text) -class CriticalQtWarning(Exception): - - """Exception raised when there's a critical Qt warning.""" - - def init_log(args): """Init loggers based on the argparse namespace passed.""" level = args.loglevel.upper() @@ -182,9 +178,10 @@ def init_log(args): root = logging.getLogger() global console_filter if console is not None: + console_filter = LogFilter(None) if args.logfilter is not None: - console_filter = LogFilter(args.logfilter.split(',')) - console.addFilter(console_filter) + console_filter.names = args.logfilter.split(',') + console.addFilter(console_filter) root.addHandler(console) if ram is not None: root.addHandler(ram) @@ -346,7 +343,6 @@ def qt_message_handler(msg_type, context, msg): QtCore.QtFatalMsg: logging.CRITICAL, } try: - # pylint: disable=no-member,useless-suppression qt_to_logging[QtCore.QtInfoMsg] = logging.INFO except AttributeError: # Qt < 5.5 @@ -399,12 +395,10 @@ def qt_message_handler(msg_type, context, msg): "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=", - # 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", + # Installing Qt from the installer may cause it looking for SSL3 or + # OpenSSL 1.0 which may not be available on the system + "QSslSocket: cannot resolve ", + "QSslSocket: cannot call unresolved function ", # When enabling debugging with QtWebEngine "Remote debugging server started successfully. Try pointing a " "Chromium-based browser to ", @@ -423,17 +417,7 @@ def qt_message_handler(msg_type, context, msg): 'with: -9805', # flake8: disable=E131 ] - # Messages which will trigger an exception immediately - critical_msgs = [ - 'Could not parse stylesheet of object', - ] - - if any(msg.strip().startswith(pattern) for pattern in critical_msgs): - # For some reason, the stack gets lost when raising here... - logger = logging.getLogger('misc') - logger.error("Got critical Qt warning!", stack_info=True) - raise CriticalQtWarning(msg) - elif any(msg.strip().startswith(pattern) for pattern in suppressed_msgs): + if any(msg.strip().startswith(pattern) for pattern in suppressed_msgs): level = logging.DEBUG else: level = qt_to_logging[msg_type] diff --git a/qutebrowser/utils/message.py b/qutebrowser/utils/message.py index bb758d78a..35ab604b0 100644 --- a/qutebrowser/utils/message.py +++ b/qutebrowser/utils/message.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -18,7 +18,7 @@ # along with qutebrowser. If not, see . # Because every method needs to have a log_stack argument -# pylint: disable=unused-variable +# pylint: disable=unused-argument """Message singleton so we don't have to define unneeded signals.""" diff --git a/qutebrowser/utils/objreg.py b/qutebrowser/utils/objreg.py index e54502ef8..4df08ba0d 100644 --- a/qutebrowser/utils/objreg.py +++ b/qutebrowser/utils/objreg.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py index e36fd0ffb..15c9e72ce 100644 --- a/qutebrowser/utils/qtutils.py +++ b/qutebrowser/utils/qtutils.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -36,6 +36,10 @@ import contextlib from PyQt5.QtCore import (qVersion, QEventLoop, QDataStream, QByteArray, QIODevice, QSaveFile, QT_VERSION_STR) from PyQt5.QtWidgets import QApplication +try: + from PyQt5.QtWebKit import qWebKitVersion +except ImportError: # pragma: no cover + qWebKitVersion = None from qutebrowser.utils import log @@ -100,13 +104,10 @@ def version_check(version, exact=False, strict=False): 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) > +def is_qtwebkit_ng(): + """Check if the given version is QtWebKit-NG.""" + assert qWebKitVersion is not None + return (pkg_resources.parse_version(qWebKitVersion()) > pkg_resources.parse_version('538.1')) @@ -174,12 +175,6 @@ def ensure_valid(obj): raise QtValueError(obj) -def ensure_not_null(obj): - """Ensure a Qt object with an .isNull() method is not null.""" - if obj.isNull(): - raise QtValueError(obj, null=True) - - def check_qdatastream(stream): """Check the status of a QDataStream and raise OSError if it's not ok.""" status_to_str = { @@ -412,15 +407,12 @@ class QtValueError(ValueError): """Exception which gets raised by ensure_valid.""" - def __init__(self, obj, null=False): + def __init__(self, obj): try: self.reason = obj.errorString() except AttributeError: self.reason = None - if null: - err = "{} is null".format(obj) - else: - err = "{} is not valid".format(obj) + err = "{} is not valid".format(obj) if self.reason: err += ": {}".format(self.reason) super().__init__(err) diff --git a/qutebrowser/utils/standarddir.py b/qutebrowser/utils/standarddir.py index 7c756b805..28d84764f 100644 --- a/qutebrowser/utils/standarddir.py +++ b/qutebrowser/utils/standarddir.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -107,7 +107,7 @@ def runtime(): if sys.platform.startswith('linux'): typ = QStandardPaths.RuntimeLocation else: # pragma: no cover - # RuntimeLocation is a weird path on OS X and Windows. + # RuntimeLocation is a weird path on macOS and Windows. typ = QStandardPaths.TempLocation overridden, path = _from_args(typ, _args) diff --git a/qutebrowser/utils/typing.py b/qutebrowser/utils/typing.py index dca42acd6..358a1a5a3 100644 --- a/qutebrowser/utils/typing.py +++ b/qutebrowser/utils/typing.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/utils/urlutils.py b/qutebrowser/utils/urlutils.py index 1beebbe92..4cd0e94d0 100644 --- a/qutebrowser/utils/urlutils.py +++ b/qutebrowser/utils/urlutils.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -592,6 +592,30 @@ def data_url(mimetype, data): return url +def safe_display_string(qurl): + """Get a IDN-homograph phishing safe form of the given QUrl. + + If we're dealing with a Punycode-encoded URL, this prepends the hostname in + its encoded form, to make sure those URLs are distinguishable. + + See https://github.com/qutebrowser/qutebrowser/issues/2547 + and https://bugreports.qt.io/browse/QTBUG-60365 + """ + if not qurl.isValid(): + raise InvalidUrlError(qurl) + + host = qurl.host(QUrl.FullyEncoded) + if '..' in host: + # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-60364 + return '(unparseable URL!) {}'.format(qurl.toDisplayString()) + + for part in host.split('.'): + if part.startswith('xn--') and host != qurl.host(QUrl.FullyDecoded): + return '({}) {}'.format(host, qurl.toDisplayString()) + + return qurl.toDisplayString() + + class InvalidProxyTypeError(Exception): """Error raised when proxy_from_url gets an unknown proxy type.""" diff --git a/qutebrowser/utils/usertypes.py b/qutebrowser/utils/usertypes.py index 34148f9af..31f2f79cb 100644 --- a/qutebrowser/utils/usertypes.py +++ b/qutebrowser/utils/usertypes.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -236,13 +236,6 @@ KeyMode = enum('KeyMode', ['normal', 'hint', 'command', 'yesno', 'prompt', 'jump_mark', 'record_macro', 'run_macro']) -# Available command completions -Completion = enum('Completion', ['command', 'section', 'option', 'value', - 'helptopic', 'quickmark_by_name', - 'bookmark_by_url', 'url', 'tab', 'sessions', - 'bind']) - - # Exit statuses for errors. Needs to be an int for sys.exit. Exit = enum('Exit', ['ok', 'reserved', 'exception', 'err_ipc', 'err_init', 'err_config', 'err_key_config'], is_int=True, start=0) diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py index c34df896e..b9aa86f20 100644 --- a/qutebrowser/utils/utils.py +++ b/qutebrowser/utils/utils.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -28,7 +28,6 @@ import os.path import collections import functools import contextlib -import itertools import socket import shlex @@ -134,7 +133,8 @@ def read_file(filename, binary=False): The file contents as string. """ if hasattr(sys, 'frozen'): - # cx_Freeze doesn't support pkg_resources :( + # PyInstaller doesn't support pkg_resources :( + # https://github.com/pyinstaller/pyinstaller/wiki/FAQ#misc fn = os.path.join(os.path.dirname(sys.executable), filename) if binary: with open(fn, 'rb') as f: @@ -372,8 +372,8 @@ def keyevent_to_string(e): None if only modifiers are pressed.. """ 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: + # Qt swaps Ctrl/Meta on macOS, so we switch it back here so the user + # can use it in the config as expected. See: # https://github.com/qutebrowser/qutebrowser/issues/110 # http://doc.qt.io/qt-5.4/osx-issues.html#special-keys modmask2str = collections.OrderedDict([ @@ -736,25 +736,6 @@ def sanitize_filename(name, replacement='_'): return name -def newest_slice(iterable, count): - """Get an iterable for the n newest items of the given iterable. - - Args: - count: How many elements to get. - 0: get no items: - n: get the n newest items - -1: get all items - """ - if count < -1: - raise ValueError("count can't be smaller than -1!") - elif count == 0: - return [] - elif count == -1 or len(iterable) < count: - return iterable - else: - return itertools.islice(iterable, len(iterable) - count, len(iterable)) - - def set_clipboard(data, selection=False): """Set the clipboard to some given data.""" if selection and not supports_selection(): @@ -859,11 +840,6 @@ def open_file(filename, cmdline=None): proc.start_detached(cmd, args) -def unused(_arg): - """Function which does nothing to avoid pylint complaining.""" - pass - - def expand_windows_drive(path): r"""Expand a drive-path like E: into E:\. diff --git a/qutebrowser/utils/version.py b/qutebrowser/utils/version.py index e6a011f44..fe3e1aedb 100644 --- a/qutebrowser/utils/version.py +++ b/qutebrowser/utils/version.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -27,9 +27,9 @@ import platform import subprocess import importlib import collections +import pkg_resources -from PyQt5.QtCore import (QT_VERSION_STR, PYQT_VERSION_STR, qVersion, - QLibraryInfo) +from PyQt5.QtCore import PYQT_VERSION_STR, QLibraryInfo from PyQt5.QtNetwork import QSslSocket from PyQt5.QtWidgets import QApplication @@ -45,10 +45,58 @@ except ImportError: # pragma: no cover import qutebrowser from qutebrowser.utils import log, utils, standarddir, usertypes, qtutils -from qutebrowser.misc import objects +from qutebrowser.misc import objects, earlyinit, sql from qutebrowser.browser import pdfjs +DistributionInfo = collections.namedtuple( + 'DistributionInfo', ['id', 'parsed', 'version', 'pretty']) + + +Distribution = usertypes.enum( + 'Distribution', ['unknown', 'ubuntu', 'debian', 'void', 'arch', + 'gentoo', 'fedora', 'opensuse', 'linuxmint', 'manjaro']) + + +def distribution(): + """Get some information about the running Linux distribution. + + Returns: + A DistributionInfo object, or None if no info could be determined. + parsed: A Distribution enum member + version: A Version object, or None + pretty: Always a string (might be "Unknown") + """ + filename = os.environ.get('QUTE_FAKE_OS_RELEASE', '/etc/os-release') + info = {} + try: + with open(filename, 'r', encoding='utf-8') as f: + for line in f: + line = line.strip() + if (not line) or line.startswith('#'): + continue + k, v = line.split("=", maxsplit=1) + info[k] = v.strip('"') + except (OSError, UnicodeDecodeError): + return None + + pretty = info.get('PRETTY_NAME', 'Unknown') + + if 'VERSION_ID' in info: + dist_version = pkg_resources.parse_version(info['VERSION_ID']) + else: + dist_version = None + + dist_id = info.get('ID', None) + try: + parsed = Distribution[dist_id] + except KeyError: + parsed = Distribution.unknown + + return DistributionInfo(parsed=parsed, version=dist_version, pretty=pretty, + id=dist_id) + + def _git_str(): """Try to find out git version. @@ -138,6 +186,7 @@ def _module_versions(): ('yaml', ['__version__']), ('cssutils', ['__version__']), ('typing', []), + ('OpenGL', ['__version__']), ('PyQt5.QtWebEngineWidgets', []), ('PyQt5.QtWebKitWidgets', []), ]) @@ -231,14 +280,6 @@ 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: @@ -256,8 +297,8 @@ def _chromium_version(): 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 '', + return '{} (WebKit {})'.format( + 'QtWebKit-NG' if qtutils.is_qtwebkit_ng() else 'legacy QtWebKit', qWebKitVersion()) else: webengine = usertypes.Backend.QtWebEngine @@ -278,18 +319,18 @@ def version(): '', '{}: {}'.format(platform.python_implementation(), platform.python_version()), - 'Qt: {}'.format(qt_version()), + 'Qt: {}'.format(earlyinit.qt_version()), 'PyQt: {}'.format(PYQT_VERSION_STR), '', ] lines += _module_versions() - lines += ['pdf.js: {}'.format(_pdfjs_version())] - lines += [ - 'SSL: {}'.format(QSslSocket.sslLibraryVersionString()), - '', + 'pdf.js: {}'.format(_pdfjs_version()), + 'sqlite: {}'.format(sql.version()), + 'QtNetwork SSL: {}\n'.format(QSslSocket.sslLibraryVersionString() + if QSslSocket.supportsSsl() else 'no'), ] qapp = QApplication.instance() @@ -302,6 +343,14 @@ def version(): lines += [ 'Platform: {}, {}'.format(platform.platform(), platform.architecture()[0]), + ] + dist = distribution() + if dist is not None: + lines += [ + 'Linux distribution: {} ({})'.format(dist.pretty, dist.parsed.name) + ] + + lines += [ 'Frozen: {}'.format(hasattr(sys, 'frozen')), "Imported from {}".format(importpath), "Qt library executable path: {}, data path: {}".format( @@ -309,7 +358,9 @@ def version(): QLibraryInfo.location(QLibraryInfo.DataPath) ) ] - lines += _os_info() + + if not dist or dist.parsed == Distribution.unknown: + lines += _os_info() lines += [ '', @@ -319,3 +370,53 @@ def version(): lines += ['{}: {}'.format(name, path)] return '\n'.join(lines) + + +def opengl_vendor(): # pragma: no cover + """Get the OpenGL vendor used. + + This returns a string such as 'nouveau' or + 'Intel Open Source Technology Center'; or None if the vendor can't be + determined. + """ + # We're doing those imports here because this is only available with Qt 5.4 + # or newer. + from PyQt5.QtGui import (QOpenGLContext, QOpenGLVersionProfile, + QOffscreenSurface) + assert QApplication.instance() + + old_context = QOpenGLContext.currentContext() + old_surface = None if old_context is None else old_context.surface() + + surface = QOffscreenSurface() + surface.create() + + ctx = QOpenGLContext() + ok = ctx.create() + if not ok: + log.init.debug("opengl_vendor: Creating context failed!") + return None + + ok = ctx.makeCurrent(surface) + if not ok: + log.init.debug("opengl_vendor: Making context current failed!") + return None + + try: + if ctx.isOpenGLES(): + # Can't use versionFunctions there + return None + + vp = QOpenGLVersionProfile() + vp.setVersion(2, 0) + + vf = ctx.versionFunctions(vp) + if vf is None: + log.init.debug("opengl_vendor: Getting version functions failed!") + return None + + return vf.glGetString(vf.GL_VENDOR) + finally: + ctx.doneCurrent() + if old_context and old_surface: + old_context.makeCurrent(old_surface) diff --git a/requirements.txt b/requirements.txt index a62285a08..cbf9ba407 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,10 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -colorama==0.3.7 +colorama==0.3.9 cssutils==1.0.2 -Jinja2==2.9.5 +Jinja2==2.9.6 MarkupSafe==1.0 Pygments==2.2.0 pyPEG2==2.15.2 PyYAML==3.12 +PyOpenGL==3.1.0 diff --git a/scripts/asciidoc2html.py b/scripts/asciidoc2html.py index 613f622a8..6c7fdaf6e 100755 --- a/scripts/asciidoc2html.py +++ b/scripts/asciidoc2html.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # This file is part of qutebrowser. # @@ -184,7 +184,8 @@ class AsciiDoc: with open(modified_src, 'w+', encoding='utf-8') as final_version: final_version.write(title + "\n\n" + header + current_lines) - self.call(modified_src, dst, '--theme=qute') + asciidoc_args = ['--theme=qute', '-a toc', '-a toc-placement=manual'] + self.call(modified_src, dst, *asciidoc_args) def _build_website(self): """Prepare and build the website.""" @@ -279,8 +280,6 @@ def main(colors=False): "asciidoc.py. If not given, it's searched in PATH.", nargs=2, required=False, metavar=('PYTHON', 'ASCIIDOC')) - parser.add_argument('--no-authors', help=argparse.SUPPRESS, - action='store_true') args = parser.parse_args() try: os.mkdir('qutebrowser/html/doc') diff --git a/scripts/dev/build_release.py b/scripts/dev/build_release.py index fb8fe000d..d5c03ad02 100755 --- a/scripts/dev/build_release.py +++ b/scripts/dev/build_release.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -37,7 +37,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir, import qutebrowser from scripts import utils -from scripts.dev import update_3rdparty +# from scripts.dev import update_3rdparty def call_script(name, *args, python=sys.executable): @@ -52,7 +52,7 @@ def call_script(name, *args, python=sys.executable): subprocess.check_call([python, path] + list(args)) -def call_tox(toxenv, *args, python=os.path.dirname(sys.executable)): +def call_tox(toxenv, *args, python=sys.executable): """Call tox. Args: @@ -62,8 +62,9 @@ def call_tox(toxenv, *args, python=os.path.dirname(sys.executable)): """ env = os.environ.copy() env['PYTHON'] = python + env['PATH'] = os.environ['PATH'] + os.pathsep + os.path.dirname(python) subprocess.check_call( - [sys.executable, '-m', 'tox', '-e', toxenv] + list(args), + [sys.executable, '-m', 'tox', '-vv', '-e', toxenv] + list(args), env=env) @@ -91,7 +92,7 @@ def smoke_test(executable): '--temp-basedir', 'about:blank', ':later 500 quit']) -def patch_osx_app(): +def patch_mac_app(): """Patch .app to copy missing data and link some libs. See https://github.com/pyinstaller/pyinstaller/issues/2276 @@ -108,8 +109,11 @@ def patch_osx_app(): 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)) + dir_dest = os.path.join(dest, os.path.basename(f)) + print("Copying directory {} to {}".format(f, dir_dest)) + shutil.copytree(f, dir_dest) else: + print("Copying {} to {}".format(f, dest)) shutil.copy(f, dest) # Link dependencies for lib in ['QtCore', 'QtWebEngineCore', 'QtQuick', 'QtQml', 'QtNetwork', @@ -121,90 +125,117 @@ def patch_osx_app(): os.path.join(dest, lib)) -def build_osx(): - """Build OS X .dmg/.app.""" +def build_mac(): + """Build macOS .dmg/.app.""" + utils.print_title("Cleaning up...") + for f in ['wc.dmg', 'template.dmg']: + try: + os.remove(f) + except FileNotFoundError: + pass + for d in ['dist', 'build']: + shutil.rmtree(d, ignore_errors=True) utils.print_title("Updating 3rdparty content") - update_3rdparty.update_pdfjs() + # Currently disabled because QtWebEngine has no pdfjs support + # update_3rdparty.run(ace=False, pdfjs=True, fancy_dmg=False) utils.print_title("Building .app via pyinstaller") call_tox('pyinstaller', '-r') utils.print_title("Patching .app") - patch_osx_app() + patch_mac_app() utils.print_title("Building .dmg") subprocess.check_call(['make', '-f', 'scripts/dev/Makefile-dmg']) - utils.print_title("Cleaning up...") - for f in ['wc.dmg', 'template.dmg']: - os.remove(f) - for d in ['dist', 'build']: - shutil.rmtree(d) dmg_name = 'qutebrowser-{}.dmg'.format(qutebrowser.__version__) os.rename('qutebrowser.dmg', dmg_name) utils.print_title("Running smoke test") - with tempfile.TemporaryDirectory() as tmpdir: - subprocess.check_call(['hdiutil', 'attach', dmg_name, - '-mountpoint', tmpdir]) - try: - binary = os.path.join(tmpdir, 'qutebrowser.app', 'Contents', - 'MacOS', 'qutebrowser') - smoke_test(binary) - finally: - subprocess.check_call(['hdiutil', 'detach', tmpdir]) - return [(dmg_name, 'application/x-apple-diskimage', 'OS X .dmg')] + try: + with tempfile.TemporaryDirectory() as tmpdir: + subprocess.check_call(['hdiutil', 'attach', dmg_name, + '-mountpoint', tmpdir]) + try: + binary = os.path.join(tmpdir, 'qutebrowser.app', 'Contents', + 'MacOS', 'qutebrowser') + smoke_test(binary) + finally: + subprocess.call(['hdiutil', 'detach', tmpdir]) + except PermissionError as e: + print("Failed to remove tempdir: {}".format(e)) + + return [(dmg_name, 'application/x-apple-diskimage', 'macOS .dmg')] + + +def patch_windows(out_dir): + """Copy missing DLLs for windows into the given output.""" + dll_dir = os.path.join('.tox', 'pyinstaller', 'lib', 'site-packages', + 'PyQt5', 'Qt', 'bin') + dlls = ['libEGL.dll', 'libGLESv2.dll', 'libeay32.dll', 'ssleay32.dll'] + for dll in dlls: + shutil.copy(os.path.join(dll_dir, dll), out_dir) def build_windows(): """Build windows executables/setups.""" utils.print_title("Updating 3rdparty content") - update_3rdparty.update_pdfjs() + # Currently disabled because QtWebEngine has no pdfjs support + # update_3rdparty.run(ace=False, pdfjs=True, fancy_dmg=False) utils.print_title("Building Windows binaries") parts = str(sys.version_info.major), str(sys.version_info.minor) ver = ''.join(parts) - dotver = '.'.join(parts) - python_x86 = r'C:\Python{}_x32'.format(ver) - python_x64 = r'C:\Python{}'.format(ver) + python_x86 = r'C:\Python{}-32\python.exe'.format(ver) + python_x64 = r'C:\Python{}\python.exe'.format(ver) + out_pyinstaller = os.path.join('dist', 'qutebrowser') + out_32 = os.path.join('dist', + 'qutebrowser-{}-x86'.format(qutebrowser.__version__)) + out_64 = os.path.join('dist', + 'qutebrowser-{}-x64'.format(qutebrowser.__version__)) artifacts = [] - utils.print_title("Rebuilding tox environment") - call_tox('cxfreeze-windows', '-r', '--notest') - utils.print_title("Running 32bit freeze.py build_exe") - call_tox('cxfreeze-windows', 'build_exe', python=python_x86) - utils.print_title("Running 32bit freeze.py bdist_msi") - call_tox('cxfreeze-windows', 'bdist_msi', python=python_x86) - utils.print_title("Running 64bit freeze.py build_exe") - call_tox('cxfreeze-windows', 'build_exe', python=python_x64) - utils.print_title("Running 64bit freeze.py bdist_msi") - call_tox('cxfreeze-windows', 'bdist_msi', python=python_x64) + utils.print_title("Running pyinstaller 32bit") + _maybe_remove(out_32) + call_tox('pyinstaller', '-r', python=python_x86) + shutil.move(out_pyinstaller, out_32) + patch_windows(out_32) - name_32 = 'qutebrowser-{}-win32.msi'.format(qutebrowser.__version__) - name_64 = 'qutebrowser-{}-amd64.msi'.format(qutebrowser.__version__) + utils.print_title("Running pyinstaller 64bit") + _maybe_remove(out_64) + call_tox('pyinstaller', '-r', python=python_x64) + shutil.move(out_pyinstaller, out_64) + patch_windows(out_64) + + utils.print_title("Building installers") + subprocess.check_call(['makensis.exe', + '/DVERSION={}'.format(qutebrowser.__version__), + 'misc/qutebrowser.nsi']) + subprocess.check_call(['makensis.exe', + '/DX64', + '/DVERSION={}'.format(qutebrowser.__version__), + 'misc/qutebrowser.nsi']) + + name_32 = 'qutebrowser-{}-win32.exe'.format(qutebrowser.__version__) + name_64 = 'qutebrowser-{}-amd64.exe'.format(qutebrowser.__version__) artifacts += [ - (os.path.join('dist', name_32), 'application/x-msi', + (os.path.join('dist', name_32), + 'application/vnd.microsoft.portable-executable', 'Windows 32bit installer'), - (os.path.join('dist', name_64), 'application/x-msi', + (os.path.join('dist', name_64), + 'application/vnd.microsoft.portable-executable', 'Windows 64bit installer'), ] utils.print_title("Running 32bit smoke test") - smoke_test('build/exe.win32-{}/qutebrowser.exe'.format(dotver)) + smoke_test(os.path.join(out_32, 'qutebrowser.exe')) utils.print_title("Running 64bit smoke test") - smoke_test('build/exe.win-amd64-{}/qutebrowser.exe'.format(dotver)) - - basedirname = 'qutebrowser-{}'.format(qutebrowser.__version__) - builddir = os.path.join('build', basedirname) - _maybe_remove(builddir) + smoke_test(os.path.join(out_64, 'qutebrowser.exe')) utils.print_title("Zipping 32bit standalone...") name = 'qutebrowser-{}-windows-standalone-win32'.format( qutebrowser.__version__) - origin = os.path.join('build', 'exe.win32-{}'.format(dotver)) - os.rename(origin, builddir) - shutil.make_archive(name, 'zip', 'build', basedirname) - shutil.rmtree(builddir) + shutil.make_archive(name, 'zip', 'dist', os.path.basename(out_32)) artifacts.append(('{}.zip'.format(name), 'application/zip', 'Windows 32bit standalone')) @@ -212,10 +243,7 @@ def build_windows(): utils.print_title("Zipping 64bit standalone...") name = 'qutebrowser-{}-windows-standalone-amd64'.format( qutebrowser.__version__) - origin = os.path.join('build', 'exe.win-amd64-{}'.format(dotver)) - os.rename(origin, builddir) - shutil.make_archive(name, 'zip', 'build', basedirname) - shutil.rmtree(builddir) + shutil.make_archive(name, 'zip', 'dist', os.path.basename(out_64)) artifacts.append(('{}.zip'.format(name), 'application/zip', 'Windows 64bit standalone')) @@ -264,6 +292,14 @@ def build_sdist(): return artifacts +def read_github_token(): + """Read the GitHub API token from disk.""" + token_file = os.path.join(os.path.expanduser('~'), '.gh_token') + with open(token_file, encoding='ascii') as f: + token = f.read().strip() + return token + + def github_upload(artifacts, tag): """Upload the given artifacts to GitHub. @@ -274,11 +310,9 @@ def github_upload(artifacts, tag): import github3 utils.print_title("Uploading to github...") - token_file = os.path.join(os.path.expanduser('~'), '.gh_token') - with open(token_file, encoding='ascii') as f: - token = f.read().strip() + token = read_github_token() gh = github3.login(token=token) - repo = gh.repository('The-Compiler', 'qutebrowser') + repo = gh.repository('qutebrowser', 'qutebrowser') release = None # to satisfy pylint for release in repo.iter_releases(): @@ -313,6 +347,12 @@ def main(): upload_to_pypi = False + if args.upload is not None: + # Fail early when trying to upload without github3 installed + # or without API token + import github3 # pylint: disable=unused-variable + read_github_token() + if os.name == 'nt': if sys.maxsize > 2**32: # WORKAROUND @@ -326,7 +366,7 @@ def main(): artifacts = build_windows() elif sys.platform == 'darwin': run_asciidoc2html(args) - artifacts = build_osx() + artifacts = build_mac() else: artifacts = build_sdist() upload_to_pypi = True diff --git a/scripts/dev/check_coverage.py b/scripts/dev/check_coverage.py index c876acd1a..e22d3a155 100644 --- a/scripts/dev/check_coverage.py +++ b/scripts/dev/check_coverage.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # This file is part of qutebrowser. # @@ -41,132 +41,136 @@ MsgType = enum.Enum('MsgType', 'insufficent_coverage, perfect_file') # A list of (test_file, tested_file) tuples. test_file can be None. PERFECT_FILES = [ (None, - 'qutebrowser/commands/cmdexc.py'), + 'commands/cmdexc.py'), ('tests/unit/commands/test_cmdutils.py', - 'qutebrowser/commands/cmdutils.py'), + 'commands/cmdutils.py'), ('tests/unit/commands/test_argparser.py', - 'qutebrowser/commands/argparser.py'), + 'commands/argparser.py'), ('tests/unit/browser/webkit/test_cache.py', - 'qutebrowser/browser/webkit/cache.py'), + 'browser/webkit/cache.py'), ('tests/unit/browser/webkit/test_cookies.py', - 'qutebrowser/browser/webkit/cookies.py'), - ('tests/unit/browser/webkit/test_history.py', - 'qutebrowser/browser/history.py'), - ('tests/unit/browser/webkit/test_history.py', - 'qutebrowser/browser/webkit/webkithistory.py'), + 'browser/webkit/cookies.py'), + ('tests/unit/browser/test_history.py', + 'browser/history.py'), + ('tests/unit/browser/test_history.py', + 'browser/webkit/webkithistory.py'), ('tests/unit/browser/webkit/http/test_http.py', - 'qutebrowser/browser/webkit/http.py'), + 'browser/webkit/http.py'), ('tests/unit/browser/webkit/http/test_content_disposition.py', - 'qutebrowser/browser/webkit/rfc6266.py'), + 'browser/webkit/rfc6266.py'), # ('tests/unit/browser/webkit/test_webkitelem.py', - # 'qutebrowser/browser/webkit/webkitelem.py'), + # 'browser/webkit/webkitelem.py'), # ('tests/unit/browser/webkit/test_webkitelem.py', - # 'qutebrowser/browser/webelem.py'), + # 'browser/webelem.py'), ('tests/unit/browser/webkit/network/test_schemehandler.py', - 'qutebrowser/browser/webkit/network/schemehandler.py'), + 'browser/webkit/network/schemehandler.py'), ('tests/unit/browser/webkit/network/test_filescheme.py', - 'qutebrowser/browser/webkit/network/filescheme.py'), + 'browser/webkit/network/filescheme.py'), ('tests/unit/browser/webkit/network/test_networkreply.py', - 'qutebrowser/browser/webkit/network/networkreply.py'), + 'browser/webkit/network/networkreply.py'), ('tests/unit/browser/test_signalfilter.py', - 'qutebrowser/browser/signalfilter.py'), + 'browser/signalfilter.py'), (None, - 'qutebrowser/browser/webkit/certificateerror.py'), + 'browser/webkit/certificateerror.py'), # ('tests/unit/browser/test_tab.py', - # 'qutebrowser/browser/tab.py'), + # 'browser/tab.py'), ('tests/unit/keyinput/test_basekeyparser.py', - 'qutebrowser/keyinput/basekeyparser.py'), + 'keyinput/basekeyparser.py'), ('tests/unit/misc/test_autoupdate.py', - 'qutebrowser/misc/autoupdate.py'), + 'misc/autoupdate.py'), ('tests/unit/misc/test_readline.py', - 'qutebrowser/misc/readline.py'), + 'misc/readline.py'), ('tests/unit/misc/test_split.py', - 'qutebrowser/misc/split.py'), + 'misc/split.py'), ('tests/unit/misc/test_msgbox.py', - 'qutebrowser/misc/msgbox.py'), + 'misc/msgbox.py'), ('tests/unit/misc/test_checkpyver.py', - 'qutebrowser/misc/checkpyver.py'), + 'misc/checkpyver.py'), ('tests/unit/misc/test_guiprocess.py', - 'qutebrowser/misc/guiprocess.py'), + 'misc/guiprocess.py'), ('tests/unit/misc/test_editor.py', - 'qutebrowser/misc/editor.py'), + 'misc/editor.py'), ('tests/unit/misc/test_cmdhistory.py', - 'qutebrowser/misc/cmdhistory.py'), + 'misc/cmdhistory.py'), ('tests/unit/misc/test_ipc.py', - 'qutebrowser/misc/ipc.py'), + 'misc/ipc.py'), ('tests/unit/misc/test_keyhints.py', - 'qutebrowser/misc/keyhintwidget.py'), + 'misc/keyhintwidget.py'), ('tests/unit/misc/test_pastebin.py', - 'qutebrowser/misc/pastebin.py'), + 'misc/pastebin.py'), (None, - 'qutebrowser/misc/objects.py'), + 'misc/objects.py'), (None, - 'qutebrowser/mainwindow/statusbar/keystring.py'), + 'mainwindow/statusbar/keystring.py'), ('tests/unit/mainwindow/statusbar/test_percentage.py', - 'qutebrowser/mainwindow/statusbar/percentage.py'), + 'mainwindow/statusbar/percentage.py'), ('tests/unit/mainwindow/statusbar/test_progress.py', - 'qutebrowser/mainwindow/statusbar/progress.py'), + 'mainwindow/statusbar/progress.py'), ('tests/unit/mainwindow/statusbar/test_tabindex.py', - 'qutebrowser/mainwindow/statusbar/tabindex.py'), + 'mainwindow/statusbar/tabindex.py'), ('tests/unit/mainwindow/statusbar/test_textbase.py', - 'qutebrowser/mainwindow/statusbar/textbase.py'), + 'mainwindow/statusbar/textbase.py'), ('tests/unit/mainwindow/statusbar/test_url.py', - 'qutebrowser/mainwindow/statusbar/url.py'), + 'mainwindow/statusbar/url.py'), + ('tests/unit/mainwindow/statusbar/test_backforward.py', + 'mainwindow/statusbar/backforward.py'), ('tests/unit/mainwindow/test_messageview.py', - 'qutebrowser/mainwindow/messageview.py'), + 'mainwindow/messageview.py'), ('tests/unit/config/test_configtypes.py', - 'qutebrowser/config/configtypes.py'), + 'config/configtypes.py'), ('tests/unit/config/test_configdata.py', - 'qutebrowser/config/configdata.py'), + 'config/configdata.py'), ('tests/unit/config/test_configexc.py', - 'qutebrowser/config/configexc.py'), + 'config/configexc.py'), ('tests/unit/config/test_textwrapper.py', - 'qutebrowser/config/textwrapper.py'), + 'config/textwrapper.py'), ('tests/unit/config/test_style.py', - 'qutebrowser/config/style.py'), + 'config/style.py'), ('tests/unit/utils/test_qtutils.py', - 'qutebrowser/utils/qtutils.py'), + 'utils/qtutils.py'), ('tests/unit/utils/test_standarddir.py', - 'qutebrowser/utils/standarddir.py'), + 'utils/standarddir.py'), ('tests/unit/utils/test_urlutils.py', - 'qutebrowser/utils/urlutils.py'), + 'utils/urlutils.py'), ('tests/unit/utils/usertypes', - 'qutebrowser/utils/usertypes.py'), + 'utils/usertypes.py'), ('tests/unit/utils/test_utils.py', - 'qutebrowser/utils/utils.py'), + 'utils/utils.py'), ('tests/unit/utils/test_version.py', - 'qutebrowser/utils/version.py'), + 'utils/version.py'), ('tests/unit/utils/test_debug.py', - 'qutebrowser/utils/debug.py'), + 'utils/debug.py'), ('tests/unit/utils/test_jinja.py', - 'qutebrowser/utils/jinja.py'), + 'utils/jinja.py'), ('tests/unit/utils/test_error.py', - 'qutebrowser/utils/error.py'), + 'utils/error.py'), ('tests/unit/utils/test_typing.py', - 'qutebrowser/utils/typing.py'), + 'utils/typing.py'), ('tests/unit/utils/test_javascript.py', - 'qutebrowser/utils/javascript.py'), + 'utils/javascript.py'), ('tests/unit/completion/test_models.py', - 'qutebrowser/completion/models/base.py'), - ('tests/unit/completion/test_sortfilter.py', - 'qutebrowser/completion/models/sortfilter.py'), + 'completion/models/urlmodel.py'), + ('tests/unit/completion/test_histcategory.py', + 'completion/models/histcategory.py'), + ('tests/unit/completion/test_listcategory.py', + 'completion/models/listcategory.py'), ] # 100% coverage because of end2end tests, but no perfect unit tests yet. WHITELISTED_FILES = [ - 'qutebrowser/browser/webkit/webkitinspector.py', - 'qutebrowser/keyinput/macros.py', - 'qutebrowser/browser/webkit/webkitelem.py', + 'browser/webkit/webkitinspector.py', + 'keyinput/macros.py', + 'browser/webkit/webkitelem.py', ] @@ -187,6 +191,8 @@ def _get_filename(filename): common_path = os.path.commonprefix([basedir, filename]) if common_path: filename = filename[len(common_path):].lstrip('/') + if filename.startswith('qutebrowser/'): + filename = filename.split('/', maxsplit=1)[1] return filename @@ -262,7 +268,7 @@ def main_check(): for msg in messages: print(msg.text) print() - filters = ','.join(msg.filename for msg in messages) + filters = ','.join('qutebrowser/' + msg.filename for msg in messages) subprocess.check_call([sys.executable, '-m', 'coverage', 'report', '--show-missing', '--include', filters]) print() diff --git a/scripts/dev/check_doc_changes.py b/scripts/dev/check_doc_changes.py index d072bfb65..ab879b5ac 100755 --- a/scripts/dev/check_doc_changes.py +++ b/scripts/dev/check_doc_changes.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # This file is part of qutebrowser. # diff --git a/scripts/dev/ci/appveyor_install.py b/scripts/dev/ci/appveyor_install.py deleted file mode 100644 index 131906248..000000000 --- a/scripts/dev/ci/appveyor_install.py +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env python2 -# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: - -# Copyright 2015-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 . - -# pylint: skip-file - -"""Install needed prerequisites on the AppVeyor. - -Note this file is written in python2 as this is more readily available on the -CI machines. -""" - -from __future__ import print_function - -import os -import time -import subprocess -import urllib - - -def check_setup(executable): - subprocess.check_call([executable, '-c', 'import PyQt5']) - subprocess.check_call([executable, '-c', 'import sip']) - subprocess.check_call([executable, '--version']) - - -def pip_install(pkg): - subprocess.check_call([r'C:\Python34\python', '-m', 'pip', 'install', '-U', - pkg]) - - -print("Installing tox") -pip_install('pip') -pip_install(r'-rmisc\requirements\requirements-tox.txt') - -print("Linking Python...") -with open(r'C:\Windows\system32\python3.bat', 'w') as f: - f.write(r'@C:\Python34\python %*') - - -if '-pyqt' not in os.environ['TESTENV']: - print("Getting PyQt5...") - qt_version = '5.5.1' - pyqt_version = '5.5.1' - pyqt_url = ('https://www.qutebrowser.org/pyqt/' - 'PyQt5-{}-gpl-Py3.4-Qt{}-x32.exe'.format( - pyqt_version, qt_version)) - - try: - urllib.urlretrieve(pyqt_url, r'C:\install-PyQt5.exe') - except (OSError, IOError): - print("Downloading PyQt failed, trying again in 10 seconds...") - time.sleep(10) - urllib.urlretrieve(pyqt_url, r'C:\install-PyQt5.exe') - - print("Installing PyQt5...") - subprocess.check_call([r'C:\install-PyQt5.exe', '/S']) - - check_setup(r'C:\Python34\python') diff --git a/scripts/dev/ci/travis_install.sh b/scripts/dev/ci/travis_install.sh index e752685f5..53bcf06e8 100644 --- a/scripts/dev/ci/travis_install.sh +++ b/scripts/dev/ci/travis_install.sh @@ -1,6 +1,6 @@ # vim: ft=sh fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # This file is part of qutebrowser. # @@ -43,6 +43,12 @@ travis_retry() { } apt_install() { + sudo tee /etc/apt/sources.list < +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -18,7 +18,7 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -"""Script to clean up the mess made by Python/setuptools/cx_Freeze.""" +"""Script to clean up the mess made by Python/setuptools/PyInstaller.""" import os import os.path diff --git a/scripts/dev/freeze.py b/scripts/dev/freeze.py deleted file mode 100755 index f254f4d90..000000000 --- a/scripts/dev/freeze.py +++ /dev/null @@ -1,145 +0,0 @@ -#!/usr/bin/env python3 -# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: - -# Copyright 2014-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 . - -"""cx_Freeze script for qutebrowser. - -Builds a standalone executable. -""" - - -import os -import os.path -import sys -import distutils - -import cx_Freeze as cx # pylint: disable=import-error,useless-suppression -# cx_Freeze is hard to install (needs C extensions) so we don't check for it. - -sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir, - os.pardir)) -from scripts import setupcommon - - -BASEDIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), - os.path.pardir, os.path.pardir) - - -def get_egl_path(): - """Get the path for PyQt5's libEGL.dll.""" - if not sys.platform.startswith('win'): - return None - return os.path.join(distutils.sysconfig.get_python_lib(), - r'PyQt5\libEGL.dll') - - -def get_plugin_folders(): - """Get the plugin folders to copy to the output.""" - if not sys.platform.startswith('win'): - return [] - plugin_dir = os.path.join(distutils.sysconfig.get_python_lib(), - 'PyQt5', 'plugins') - folders = ['audio', 'iconengines', 'mediaservice', 'printsupport'] - return [os.path.join(plugin_dir, folder) for folder in folders] - - -def get_build_exe_options(skip_html=False): - """Get the options passed as build_exe_options to cx_Freeze. - - If either skip_html or --qute-skip-html as argument is given, doesn't - freeze the documentation. - """ - if '--qute-skip-html' in sys.argv: - skip_html = True - sys.argv.remove('--qute-skip-html') - - include_files = [ - ('qutebrowser/javascript', 'javascript'), - ('qutebrowser/img', 'img'), - ('qutebrowser/git-commit-id', 'git-commit-id'), - ('qutebrowser/utils/testfile', 'utils/testfile'), - ('qutebrowser/html', 'html'), - ] - - if os.path.exists(os.path.join('qutebrowser', '3rdparty', 'pdfjs')): - include_files.append(('qutebrowser/3rdparty/pdfjs', '3rdparty/pdfjs')) - else: - print("Warning: excluding pdfjs as it's not present!") - - if not skip_html: - include_files += [ - ('qutebrowser/html/doc', 'html/doc'), - ] - - egl_path = get_egl_path() - if egl_path is not None: - include_files.append((egl_path, 'libEGL.dll')) - - include_files += get_plugin_folders() - - return { - 'include_files': include_files, - 'include_msvcr': True, - 'includes': [], - 'excludes': ['tkinter'], - 'packages': ['pygments'], - } - - -def get_exe(base, target_name): - """Get the qutebrowser cx.Executable to build.""" - return cx.Executable('qutebrowser/__main__.py', base=base, - targetName=target_name, shortcutName='qutebrowser', - shortcutDir='ProgramMenuFolder', - icon=os.path.join(BASEDIR, 'icons', - 'qutebrowser.ico')) - - -def main(): - if sys.platform.startswith('win'): - base = 'Win32GUI' - target_name = 'qutebrowser.exe' - else: - base = None - target_name = 'qutebrowser' - - bdist_msi_options = { - # random GUID generated by uuid.uuid4() - 'upgrade_code': '{a7119e75-4eb7-466c-ae0d-3c0eccb45196}', - 'add_to_path': False, - } - - try: - setupcommon.write_git_file() - cx.setup( - executables=[get_exe(base, target_name)], - options={ - 'build_exe': get_build_exe_options(), - 'bdist_msi': bdist_msi_options, - }, - **setupcommon.setupdata - ) - finally: - path = os.path.join(BASEDIR, 'qutebrowser', 'git-commit-id') - if os.path.exists(path): - os.remove(path) - - -if __name__ == '__main__': - main() diff --git a/scripts/dev/freeze_tests.py b/scripts/dev/freeze_tests.py deleted file mode 100755 index 9f9e2bbd2..000000000 --- a/scripts/dev/freeze_tests.py +++ /dev/null @@ -1,87 +0,0 @@ -#!/usr/bin/env python3 -# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: - -# Copyright 2015-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 . - -"""cx_Freeze script to freeze qutebrowser and its tests.""" - - -import os -import os.path -import sys -import contextlib - -import cx_Freeze as cx # pylint: disable=import-error,useless-suppression -# cx_Freeze is hard to install (needs C extensions) so we don't check for it. -import pytest - -import httpbin - -sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir, - os.pardir)) -from scripts import setupcommon -from scripts.dev import freeze - - -@contextlib.contextmanager -def temp_git_commit_file(): - """Context manager to temporarily create a fake git-commit-id file.""" - basedir = os.path.join(os.path.dirname(os.path.realpath(__file__)), - os.path.pardir, os.pardir) - path = os.path.join(basedir, 'qutebrowser', 'git-commit-id') - with open(path, 'wb') as f: - f.write(b'fake-frozen-git-commit') - yield - os.remove(path) - - -def get_build_exe_options(): - """Get build_exe options with additional includes.""" - 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', 'cheroot', 'pstats', 'queue'] - - httpbin_dir = os.path.dirname(httpbin.__file__) - opts['include_files'] += [ - ('tests/end2end/data', 'end2end/data'), - (os.path.join(httpbin_dir, 'templates'), 'end2end/templates'), - ] - - opts['packages'].append('qutebrowser') - return opts - - -def main(): - base = 'Win32GUI' if sys.platform.startswith('win') else None - with temp_git_commit_file(): - cx.setup( - executables=[ - cx.Executable('scripts/dev/run_frozen_tests.py', - targetName='run-frozen-tests'), - cx.Executable('tests/end2end/fixtures/webserver_sub.py', - targetName='webserver_sub'), - freeze.get_exe(base, target_name='qutebrowser') - ], - options={'build_exe': get_build_exe_options()}, - **setupcommon.setupdata - ) - - -if __name__ == '__main__': - main() diff --git a/scripts/dev/get_coredumpctl_traces.py b/scripts/dev/get_coredumpctl_traces.py index d1e962834..8cae2a190 100644 --- a/scripts/dev/get_coredumpctl_traces.py +++ b/scripts/dev/get_coredumpctl_traces.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # This file is part of qutebrowser. # diff --git a/scripts/dev/misc_checks.py b/scripts/dev/misc_checks.py index 49ca8e48f..1bb263d15 100644 --- a/scripts/dev/misc_checks.py +++ b/scripts/dev/misc_checks.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/scripts/dev/pylint_checkers/qute_pylint/config.py b/scripts/dev/pylint_checkers/qute_pylint/config.py index b93b211f1..be8ac8da8 100644 --- a/scripts/dev/pylint_checkers/qute_pylint/config.py +++ b/scripts/dev/pylint_checkers/qute_pylint/config.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/scripts/dev/pylint_checkers/qute_pylint/modeline.py b/scripts/dev/pylint_checkers/qute_pylint/modeline.py index 580837b34..ee3de13c9 100644 --- a/scripts/dev/pylint_checkers/qute_pylint/modeline.py +++ b/scripts/dev/pylint_checkers/qute_pylint/modeline.py @@ -1,4 +1,4 @@ -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: # This file is part of qutebrowser. diff --git a/scripts/dev/pylint_checkers/qute_pylint/openencoding.py b/scripts/dev/pylint_checkers/qute_pylint/openencoding.py index 83926fc5e..eccc152ba 100644 --- a/scripts/dev/pylint_checkers/qute_pylint/openencoding.py +++ b/scripts/dev/pylint_checkers/qute_pylint/openencoding.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # This file is part of qutebrowser. # diff --git a/scripts/dev/pylint_checkers/qute_pylint/settrace.py b/scripts/dev/pylint_checkers/qute_pylint/settrace.py index 9c1196daa..2bfa9f06f 100644 --- a/scripts/dev/pylint_checkers/qute_pylint/settrace.py +++ b/scripts/dev/pylint_checkers/qute_pylint/settrace.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/scripts/dev/pylint_checkers/setup.py b/scripts/dev/pylint_checkers/setup.py index eaecf406e..960fdd2b7 100644 --- a/scripts/dev/pylint_checkers/setup.py +++ b/scripts/dev/pylint_checkers/setup.py @@ -2,7 +2,7 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/scripts/dev/recompile_requirements.py b/scripts/dev/recompile_requirements.py index 1e3181344..ddc85dfec 100644 --- a/scripts/dev/recompile_requirements.py +++ b/scripts/dev/recompile_requirements.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -60,20 +60,6 @@ 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. @@ -112,52 +98,36 @@ 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) - name = basename[len('requirements-'):-len('.txt-raw')] - if name == 'cxfreeze' and sys.hexversion >= 0x030600: - print("Warning: Skipping cxfreeze") - else: - yield name - yield 'pip' + yield basename[len('requirements-'):-len('.txt-raw')] def main(): """Re-compile the given (or all) requirement files.""" 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)) - 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 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') - 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 requirements: - converted = convert_line(line, comments) - f.write(converted + '\n') + for line in reqs.splitlines(): + f.write(convert_line(line, comments) + '\n') if __name__ == '__main__': diff --git a/scripts/dev/run_profile.py b/scripts/dev/run_profile.py index 87d2f0ed7..31fe539aa 100755 --- a/scripts/dev/run_profile.py +++ b/scripts/dev/run_profile.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/scripts/dev/run_pylint_on_tests.py b/scripts/dev/run_pylint_on_tests.py index 01dd14ad7..e6263692b 100644 --- a/scripts/dev/run_pylint_on_tests.py +++ b/scripts/dev/run_pylint_on_tests.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # This file is part of qutebrowser. # @@ -54,7 +54,8 @@ def main(): 'missing-docstring', 'protected-access', # https://bitbucket.org/logilab/pylint/issue/511/ - 'undefined-variable', + #'undefined-variable', + 'len-as-condition', # directories without __init__.py... 'import-error', ] @@ -66,7 +67,8 @@ def main(): no_docstring_rgx = ['^__.*__$', '^setup$'] args = (['--disable={}'.format(','.join(disabled)), - '--no-docstring-rgx=({})'.format('|'.join(no_docstring_rgx))] + + '--no-docstring-rgx=({})'.format('|'.join(no_docstring_rgx)), + '--ignored-modules=helpers,pytest,PyQt5'] + sys.argv[2:] + files) env = os.environ.copy() env['PYTHONPATH'] = os.pathsep.join(pythonpath) diff --git a/scripts/dev/run_vulture.py b/scripts/dev/run_vulture.py index 55f4d9c86..8a1886e11 100755 --- a/scripts/dev/run_vulture.py +++ b/scripts/dev/run_vulture.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # This file is part of qutebrowser. # @@ -54,9 +54,7 @@ def whitelist_generator(): attr) # PyQt properties - for attr in ['prompt_active', 'command_active', 'insert_active', - 'caret_mode']: - yield 'qutebrowser.mainwindow.statusbar.bar.StatusBar.' + attr + yield 'qutebrowser.mainwindow.statusbar.bar.StatusBar.color_flags' yield 'qutebrowser.mainwindow.statusbar.url.UrlText.urltype' # Not used yet, but soon (or when debugging) @@ -74,8 +72,9 @@ def whitelist_generator(): yield 'PyQt5.QtWebKit.QWebPage.ErrorPageExtensionReturn().fileNames' yield 'PyQt5.QtWidgets.QStyleOptionViewItem.backgroundColor' - ## qute:... handlers + ## qute://... handlers for name in qutescheme._HANDLERS: # pylint: disable=protected-access + name = name.replace('-', '_') yield 'qutebrowser.browser.qutescheme.qute_' + name # Other false-positives @@ -164,7 +163,8 @@ def run(files): def main(): parser = argparse.ArgumentParser() - parser.add_argument('files', nargs='*', default=['qutebrowser', 'scripts']) + parser.add_argument('files', nargs='*', default=['qutebrowser', 'scripts', + 'setup.py']) args = parser.parse_args() out = run(args.files) for line in out: diff --git a/scripts/dev/segfault_test.py b/scripts/dev/segfault_test.py index cac8c6a14..c5e7c106f 100755 --- a/scripts/dev/segfault_test.py +++ b/scripts/dev/segfault_test.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/scripts/dev/src2asciidoc.py b/scripts/dev/src2asciidoc.py index a8f019d9d..5fe6af6ba 100755 --- a/scripts/dev/src2asciidoc.py +++ b/scripts/dev/src2asciidoc.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # This file is part of qutebrowser. # @@ -27,7 +27,6 @@ import shutil import os.path import inspect import subprocess -import collections import tempfile import argparse @@ -44,7 +43,7 @@ from qutebrowser.utils import docutils, usertypes FILE_HEADER = """ // DO NOT EDIT THIS FILE DIRECTLY! -// It is autogenerated from docstrings by running: +// It is autogenerated by running: // $ python3 scripts/dev/src2asciidoc.py """.lstrip() @@ -365,8 +364,7 @@ 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')] + version_dependent_options = [('general', 'print-element-backgrounds')] for optname, option in sect.items(): f.write("\n") f.write('[[{}-{}]]'.format(sectname, optname) + "\n") @@ -422,31 +420,6 @@ def generate_settings(filename): _generate_setting_section(f, sectname, sect) -def _get_authors(): - """Get a list of authors based on git commit logs.""" - corrections = { - 'binix': 'sbinix', - 'Averrin': 'Alexey "Averrin" Nabrodov', - 'Alexey Nabrodov': 'Alexey "Averrin" Nabrodov', - 'Michael': 'Halfwit', - 'Error 800': 'error800', - 'larryhynes': 'Larry Hynes', - 'Daniel': 'Daniel Schadt', - 'Alexey Glushko': 'haitaka', - '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() - if author not in ignored] - cnt = collections.Counter(authors) - return sorted(cnt, key=lambda k: (cnt[k], k), reverse=True) - - def _format_block(filename, what, data): """Format a block in a file. @@ -493,12 +466,6 @@ def _format_block(filename, what, data): shutil.move(tmpname, filename) -def regenerate_authors(filename): - """Re-generate the authors inside README based on the commits made.""" - data = ['* {}\n'.format(author) for author in _get_authors()] - _format_block(filename, 'authors', data) - - def regenerate_manpage(filename): """Update manpage OPTIONS using an argparse parser.""" # pylint: disable=protected-access @@ -544,9 +511,6 @@ def main(): generate_settings('doc/help/settings.asciidoc') print("Generating command help...") generate_commands('doc/help/commands.asciidoc') - if '--no-authors' not in sys.argv: - print("Generating authors in README...") - regenerate_authors('README.asciidoc') if '--cheatsheet' in sys.argv: print("Regenerating cheatsheet .pngs") regenerate_cheatsheet() diff --git a/scripts/dev/ua_fetch.py b/scripts/dev/ua_fetch.py old mode 100644 new mode 100755 index f37797041..7c39ad596 --- a/scripts/dev/ua_fetch.py +++ b/scripts/dev/ua_fetch.py @@ -1,8 +1,9 @@ #!/usr/bin/env python3 # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 lamarpavel -# Copyright 2015-2016 Alexey Nabrodov (Averrin) +# Copyright 2015-2017 lamarpavel +# Copyright 2015-2017 Alexey Nabrodov (Averrin) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -86,7 +87,10 @@ def add_diversity(table): ('Wget/1.16.1 (linux-gnu)', "wget 1.16.1"), ('curl/7.40.0', - "curl 7.40.0") + "curl 7.40.0"), + ('Mozilla/5.0 (Linux; U; Android 7.1.2) AppleWebKit/534.30 ' + '(KHTML, like Gecko) Version/4.0 Mobile Safari/534.30', + "Mobile Generic Android") ] return table diff --git a/scripts/dev/update_3rdparty.py b/scripts/dev/update_3rdparty.py index 4b88b165f..6fd6810e6 100755 --- a/scripts/dev/update_3rdparty.py +++ b/scripts/dev/update_3rdparty.py @@ -2,6 +2,7 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: # Copyright 2015 Daniel Schadt +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -109,6 +110,16 @@ def update_ace(): urllib.request.urlcleanup() +def run(ace=False, pdfjs=True, fancy_dmg=False, pdfjs_version=None): + """Update components based on the given arguments.""" + if pdfjs: + update_pdfjs(pdfjs_version) + if ace: + update_ace() + if fancy_dmg: + update_dmg_makefile() + + def main(): parser = argparse.ArgumentParser() parser.add_argument( @@ -119,11 +130,8 @@ def main(): parser.add_argument('--fancy-dmg', help="Update fancy-dmg Makefile", action='store_true') args = parser.parse_args() - - update_pdfjs(args.pdfjs) - update_ace() - if args.fancy_dmg: - update_dmg_makefile() + run(ace=True, pdfjs=True, fancy_dmg=args.fancy_dmg, + pdfjs_version=args.pdfjs) if __name__ == '__main__': diff --git a/scripts/hostblock_blame.py b/scripts/hostblock_blame.py index 7e444793b..dde83d91f 100644 --- a/scripts/hostblock_blame.py +++ b/scripts/hostblock_blame.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/scripts/importer.py b/scripts/importer.py index 5e0883cd2..1b3be4d32 100755 --- a/scripts/importer.py +++ b/scripts/importer.py @@ -1,7 +1,8 @@ #!/usr/bin/env python3 # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Claude (longneck) +# Copyright 2014-2017 Claude (longneck) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # This file is part of qutebrowser. # diff --git a/scripts/keytester.py b/scripts/keytester.py index ebed5f62c..b147599b6 100644 --- a/scripts/keytester.py +++ b/scripts/keytester.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # This file is part of qutebrowser. # diff --git a/scripts/link_pyqt.py b/scripts/link_pyqt.py index fffe27fb3..a7de598cd 100644 --- a/scripts/link_pyqt.py +++ b/scripts/link_pyqt.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # This file is part of qutebrowser. # diff --git a/scripts/open_url_in_instance.sh b/scripts/open_url_in_instance.sh new file mode 100755 index 000000000..119c3aa4f --- /dev/null +++ b/scripts/open_url_in_instance.sh @@ -0,0 +1,18 @@ +#!/bin/bash +# initial idea: Florian Bruhin (The-Compiler) +# author: Thore Bödecker (foxxx0) + +_url="$1" +_qb_version='0.10.1' +_proto_version=1 +_ipc_socket="${XDG_RUNTIME_DIR}/qutebrowser/ipc-$(echo -n "$USER" | md5sum | cut -d' ' -f1)" + +if [[ -e "${_ipc_socket}" ]]; then + exec printf '{"args": ["%s"], "target_arg": null, "version": "%s", "protocol_version": %d, "cwd": "%s"}\n' \ + "${_url}" \ + "${_qb_version}" \ + "${_proto_version}" \ + "${PWD}" | socat - UNIX-CONNECT:"${_ipc_socket}" +else + exec /usr/bin/qutebrowser --backend webengine "$@" +fi diff --git a/scripts/setupcommon.py b/scripts/setupcommon.py index 9a447d280..494ab4c46 100644 --- a/scripts/setupcommon.py +++ b/scripts/setupcommon.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # This file is part of qutebrowser. # diff --git a/scripts/testbrowser.py b/scripts/testbrowser.py index 75c36bd95..fbe48c451 100755 --- a/scripts/testbrowser.py +++ b/scripts/testbrowser.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/scripts/utils.py b/scripts/utils.py index 55eb679bd..6793bb2a7 100644 --- a/scripts/utils.py +++ b/scripts/utils.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # This file is part of qutebrowser. # diff --git a/setup.py b/setup.py index 6e594af12..f594009a0 100755 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2015 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/conftest.py b/tests/conftest.py index fe2ca9042..f2d6da8ff 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -17,7 +17,7 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -# pylint: disable=unused-import +# pylint: disable=unused-import,wildcard-import,unused-wildcard-import """The qutebrowser test suite conftest file.""" @@ -34,7 +34,7 @@ pytest.register_assert_rewrite('helpers') 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 helpers.fixtures import * from qutebrowser.utils import qtutils @@ -50,8 +50,8 @@ def _apply_platform_markers(config, item): ('posix', os.name != 'posix', "Requires a POSIX os"), ('windows', os.name != 'nt', "Requires Windows"), ('linux', not sys.platform.startswith('linux'), "Requires Linux"), - ('osx', sys.platform != 'darwin', "Requires OS X"), - ('not_osx', sys.platform == 'darwin', "Skipped on OS X"), + ('mac', sys.platform != 'darwin', "Requires macOS"), + ('not_mac', sys.platform == 'darwin', "Skipped on macOS"), ('not_frozen', getattr(sys, 'frozen', False), "Can't be run when frozen"), ('frozen', not getattr(sys, 'frozen', False), @@ -168,10 +168,9 @@ def pytest_configure(config): webengine_env = os.environ.get('QUTE_BDD_WEBENGINE', '') config.webengine = bool(webengine_arg or webengine_env) # Fail early if QtWebEngine is not available - # pylint: disable=no-name-in-module,unused-variable,useless-suppression + # pylint: disable=unused-variable if config.webengine: import PyQt5.QtWebEngineWidgets - # pylint: enable=no-name-in-module,unused-variable,useless-suppression @pytest.fixture(scope='session', autouse=True) diff --git a/tests/end2end/conftest.py b/tests/end2end/conftest.py index 5dcff3fb5..75c6845f4 100644 --- a/tests/end2end/conftest.py +++ b/tests/end2end/conftest.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -68,7 +68,7 @@ def _get_version_tag(tag): """ version_re = re.compile(r""" (?Pqt|pyqt) - (?P==|>=|!=) + (?P==|>=|!=|<) (?P\d+\.\d+(\.\d+)?) """, re.VERBOSE) @@ -84,6 +84,7 @@ def _get_version_tag(tag): do_skip = { '==': not qtutils.version_check(version, exact=True), '>=': not qtutils.version_check(version), + '<': qtutils.version_check(version), '!=': qtutils.version_check(version, exact=True), } return pytest.mark.skipif(do_skip[op], reason='Needs ' + tag) @@ -135,16 +136,6 @@ 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), @@ -153,12 +144,12 @@ def pytest_collection_modifyitems(config, items): ('qtwebkit_skip', 'Skipped with QtWebKit', pytest.mark.skipif, not config.webengine), ('qtwebkit_ng_xfail', 'Failing with QtWebKit-NG', pytest.mark.xfail, - qtwebkit_ng_used), + not config.webengine and qtutils.is_qtwebkit_ng()), ('qtwebkit_ng_skip', 'Skipped with QtWebKit-NG', pytest.mark.skipif, - qtwebkit_ng_used), + not config.webengine and qtutils.is_qtwebkit_ng()), ('qtwebengine_flaky', 'Flaky with QtWebEngine', pytest.mark.skipif, config.webengine), - ('qtwebengine_osx_xfail', 'Fails on OS X with QtWebEngine', + ('qtwebengine_mac_xfail', 'Fails on macOS with QtWebEngine', pytest.mark.xfail, config.webengine and sys.platform == 'darwin'), ] diff --git a/tests/end2end/data/data_link.html b/tests/end2end/data/data_link.html new file mode 100644 index 000000000..227a9f2f9 --- /dev/null +++ b/tests/end2end/data/data_link.html @@ -0,0 +1,10 @@ + + + + + data: link + + + download + + diff --git a/tests/end2end/data/downloads/download with no title.html b/tests/end2end/data/downloads/download with no title.html new file mode 100644 index 000000000..da4352e59 --- /dev/null +++ b/tests/end2end/data/downloads/download with no title.html @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/tests/end2end/data/downloads/issue1214.html b/tests/end2end/data/downloads/issue1214.html deleted file mode 100644 index db56cdc61..000000000 --- a/tests/end2end/data/downloads/issue1214.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - Wrong filename when using data: links - - - download - - diff --git a/tests/end2end/data/downloads/qutebrowser.png b/tests/end2end/data/downloads/qutebrowser.png new file mode 100644 index 000000000..e8bbb6b56 Binary files /dev/null and b/tests/end2end/data/downloads/qutebrowser.png differ diff --git a/tests/end2end/data/hints/html/javascript.html b/tests/end2end/data/hints/html/javascript.html new file mode 100644 index 000000000..89395ff92 --- /dev/null +++ b/tests/end2end/data/hints/html/javascript.html @@ -0,0 +1,13 @@ + + + + + + + + Javascript link + + + Follow me via JS! + + diff --git a/tests/end2end/data/issue2569.html b/tests/end2end/data/issue2569.html new file mode 100644 index 000000000..8f613be2d --- /dev/null +++ b/tests/end2end/data/issue2569.html @@ -0,0 +1,26 @@ + + + + Form with tagName child + + + + + + + +

+ +
+ + + + + + +
  • List item
+ + diff --git a/tests/end2end/data/javascript/windowsize.html b/tests/end2end/data/javascript/windowsize.html new file mode 100644 index 000000000..d2c077369 --- /dev/null +++ b/tests/end2end/data/javascript/windowsize.html @@ -0,0 +1,24 @@ + + + + + window sizes + + + + +

visible: unknown

+

hidden: unknown

+ + diff --git a/tests/end2end/data/scroll/simple.html b/tests/end2end/data/scroll/simple.html index 7da9df101..7f1bba4d5 100644 --- a/tests/end2end/data/scroll/simple.html +++ b/tests/end2end/data/scroll/simple.html @@ -3,10 +3,24 @@ Scrolling + Just a link +

Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus

 0
 1
@@ -78,7 +92,7 @@
 67
 68
 69
-70
+70
 71
 72
 73
diff --git a/tests/end2end/data/search.html b/tests/end2end/data/search.html
index 2eec560d6..003751cfd 100644
--- a/tests/end2end/data/search.html
+++ b/tests/end2end/data/search.html
@@ -16,6 +16,7 @@
             BAZ
space travel
/slash
+ follow me!

diff --git a/tests/end2end/data/search_select.js b/tests/end2end/data/search_select.js new file mode 100644 index 000000000..874e9e9fe --- /dev/null +++ b/tests/end2end/data/search_select.js @@ -0,0 +1,12 @@ +/* Select all elements marked with toselect */ + +var toSelect = document.getElementsByClassName("toselect"); +var s = window.getSelection(); + +if(s.rangeCount > 0) s.removeAllRanges(); + +for(var i = 0; i < toSelect.length; i++) { + var range = document.createRange(); + range.selectNode(toSelect[i]); + s.addRange(range); +} diff --git a/scripts/dev/run_frozen_tests.py b/tests/end2end/data/userscripts/stdinclose.py old mode 100644 new mode 100755 similarity index 50% rename from scripts/dev/run_frozen_tests.py rename to tests/end2end/data/userscripts/stdinclose.py index e64325417..fa0676f73 --- a/scripts/dev/run_frozen_tests.py +++ b/tests/end2end/data/userscripts/stdinclose.py @@ -1,7 +1,7 @@ -#!/usr/bin/env python3 +#!/usr/bin/python3 # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -18,23 +18,10 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -"""cx_Freeze script to run qutebrowser tests on the frozen executable.""" +"""A userscript to check if the stdin gets closed.""" import sys - -import pytest -import pytestqt.plugin -import pytest_mock -import pytest_catchlog -import pytest_instafail -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_benchmark.plugin])) +import os +sys.stdin.read() +with open(os.environ['QUTE_FIFO'], 'wb') as fifo: + fifo.write(b':message-info "stdin closed"\n') diff --git a/tests/end2end/features/completion.feature b/tests/end2end/features/completion.feature index b6c62336c..e93518199 100644 --- a/tests/end2end/features/completion.feature +++ b/tests/end2end/features/completion.feature @@ -32,23 +32,23 @@ Feature: Using completion Scenario: Using command completion When I run :set-cmd-text : - Then the completion model should be CommandCompletionModel + Then the completion model should be command Scenario: Using help completion When I run :set-cmd-text -s :help - Then the completion model should be HelpCompletionModel + Then the completion model should be helptopic Scenario: Using quickmark completion When I run :set-cmd-text -s :quickmark-load - Then the completion model should be QuickmarkCompletionModel + Then the completion model should be quickmark Scenario: Using bookmark completion When I run :set-cmd-text -s :bookmark-load - Then the completion model should be BookmarkCompletionModel + Then the completion model should be bookmark Scenario: Using bind completion When I run :set-cmd-text -s :bind X - Then the completion model should be BindCompletionModel + Then the completion model should be bind Scenario: Using session completion Given I open data/hello.txt @@ -62,37 +62,11 @@ Feature: Using completion Scenario: Using option completion When I run :set-cmd-text -s :set colors - Then the completion model should be SettingOptionCompletionModel + Then the completion model should be option 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 + Then the completion model should be value Scenario: Deleting an open tab via the completion Given I have a fresh instance diff --git a/tests/end2end/features/conftest.py b/tests/end2end/features/conftest.py index c65ea33ad..0a67742f9 100644 --- a/tests/end2end/features/conftest.py +++ b/tests/end2end/features/conftest.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -61,8 +61,8 @@ def pytest_runtest_makereport(item, call): if (not hasattr(report.longrepr, 'addsection') or not hasattr(report, 'scenario')): - # In some conditions (on OS X and Windows it seems), report.longrepr is - # actually a tuple. This is handled similarily in pytest-qt too. + # In some conditions (on macOS and Windows it seems), report.longrepr + # is actually a tuple. This is handled similarily in pytest-qt too. # # Since this hook is invoked for any test, we also need to skip it for # non-BDD ones. @@ -159,8 +159,8 @@ def clean_open_tabs(quteproc): """Clean up open windows and tabs.""" quteproc.set_setting('tabs', 'last-close', 'blank') quteproc.send_cmd(':window-only') - quteproc.send_cmd(':tab-only') - quteproc.send_cmd(':tab-close') + quteproc.send_cmd(':tab-only --force') + quteproc.send_cmd(':tab-close --force') quteproc.wait_for_load_finished_url('about:blank') @@ -174,22 +174,28 @@ def pdfjs_available(): @bdd.when(bdd.parsers.parse("I open {path}")) -def open_path(quteproc, path): +def open_path(quteproc, httpbin, path): """Open a URL. - If used like "When I open ... in a new tab", the URL is opened in a new - tab. With "... in a new window", it's opened in a new window. With - "... as a URL", it's opened according to new-instance-open-target. + - If used like "When I open ... in a new tab", the URL is opened in a new + tab. + - With "... in a new window", it's opened in a new window. + - With "... in a private window" it's opened in a new private window. + - With "... as a URL", it's opened according to new-instance-open-target. """ + path = path.replace('(port)', str(httpbin.port)) + new_tab = False new_bg_tab = False new_window = False + private = 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' + private_suffix = ' in a private window' do_not_wait_suffix = ' without waiting' as_url_suffix = ' as a URL' @@ -203,6 +209,9 @@ def open_path(quteproc, path): elif path.endswith(new_window_suffix): path = path[:-len(new_window_suffix)] new_window = True + elif path.endswith(private_suffix): + path = path[:-len(private_suffix)] + private = True elif path.endswith(as_url_suffix): path = path[:-len(as_url_suffix)] as_url = True @@ -213,7 +222,8 @@ def open_path(quteproc, path): break quteproc.open_path(path, new_tab=new_tab, new_bg_tab=new_bg_tab, - new_window=new_window, as_url=as_url, wait=wait) + new_window=new_window, private=private, as_url=as_url, + wait=wait) @bdd.when(bdd.parsers.parse("I set {sect} -> {opt} to {value}")) @@ -533,31 +543,46 @@ def check_open_tabs(quteproc, request, tabs): """ session = quteproc.get_session() active_suffix = ' (active)' + pinned_suffix = ' (pinned)' tabs = tabs.splitlines() 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) + has_active = any(active_suffix in line for line in tabs) + has_pinned = any(pinned_suffix in line for line in tabs) for i, line in enumerate(tabs): line = line.strip() assert line.startswith('- ') line = line[2:] # remove "- " prefix - if line.endswith(active_suffix): - path = line[:-len(active_suffix)] - active = True - else: - path = line - active = False + + active = False + pinned = False + + while line.endswith(active_suffix) or line.endswith(pinned_suffix): + if line.endswith(active_suffix): + # active + line = line[:-len(active_suffix)] + active = True + else: + # pinned + line = line[:-len(pinned_suffix)] + pinned = True session_tab = session['windows'][0]['tabs'][i] - assert session_tab['history'][-1]['url'] == quteproc.path_to_url(path) + current_page = session_tab['history'][-1] + assert current_page['url'] == quteproc.path_to_url(line) if active: assert session_tab['active'] elif has_active: assert 'active' not in session_tab + if pinned: + assert current_page['pinned'] + elif has_pinned: + assert not current_page['pinned'] + @bdd.then(bdd.parsers.re(r'the (?Pprimary selection|clipboard) should ' r'contain "(?P.*)"')) diff --git a/tests/end2end/features/downloads.feature b/tests/end2end/features/downloads.feature index 768521183..366414016 100644 --- a/tests/end2end/features/downloads.feature +++ b/tests/end2end/features/downloads.feature @@ -22,6 +22,20 @@ Feature: Downloading things from a website. And I wait until the download is finished Then the downloaded file download.bin should exist + Scenario: Using :download with no URL + When I set storage -> prompt-download-directory to false + And I open data/downloads/downloads.html + And I run :download + And I wait until the download is finished + Then the downloaded file Simple downloads.html should exist + + Scenario: Using :download with no URL on an image + When I set storage -> prompt-download-directory to false + And I open data/downloads/qutebrowser.png + And I run :download + And I wait until the download is finished + Then the downloaded file qutebrowser.png should exist + Scenario: Using hints When I set storage -> prompt-download-directory to false And I open data/downloads/downloads.html @@ -71,7 +85,7 @@ Feature: Downloading things from a website. Scenario: Downloading a data: link (issue 1214) When I set completion -> download-path-suggestion to filename And I set storage -> prompt-download-directory to true - And I open data/downloads/issue1214.html + And I open data/data_link.html And I hint with args "links download" and follow a And I wait for "Asking question text=* title='Save file to:'>, *" in the log And I run :leave-mode @@ -121,14 +135,22 @@ Feature: Downloading things from a website. 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 + @qtwebkit_skip @qt<5.9 + Scenario: Downloading a file with evil content-disposition header (Qt 5.8 or older) # 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 + @qtwebkit_skip @qt>=5.9 + Scenario: Downloading a file with evil content-disposition header (Qt 5.9 or newer) + # 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 ..%2Ffoo should exist + @windows Scenario: Downloading a file to a reserved path When I set storage -> prompt-download-directory to true @@ -571,9 +593,9 @@ Feature: Downloading things from a website. And I wait until the download is finished Then the downloaded file content-size should exist - @posix Scenario: Downloading to unwritable destination - When I set storage -> prompt-download-directory to false + When the unwritable dir is unwritable + And I set storage -> prompt-download-directory to false 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 @@ -597,7 +619,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 @qtwebkit_ng_xfail: https://github.com/annulen/webkit/issues/428 + @qtwebengine_todo: pdfjs is not implemented yet Scenario: Downloading via pdfjs Given pdfjs is available When I set storage -> prompt-download-directory to false @@ -629,7 +651,7 @@ Feature: Downloading things from a website. @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 run :download --dest user-agent And I wait until the download is finished Then the downloaded file user-agent should contain Safari/ diff --git a/tests/end2end/features/hints.feature b/tests/end2end/features/hints.feature index bdcf9b9d8..4fbd17720 100644 --- a/tests/end2end/features/hints.feature +++ b/tests/end2end/features/hints.feature @@ -114,6 +114,12 @@ Feature: Using hints And I hint with args "links yank" and follow a Then the clipboard should contain "nobody" + Scenario: Yanking javascript link to clipboard + When I run :debug-set-fake-clipboard + And I open data/hints/html/javascript.html + And I hint with args "links yank" and follow a + Then the clipboard should contain "javascript:window.location.href='/data/hello.txt'" + Scenario: Rapid hinting When I open data/hints/rapid.html in a new tab And I run :tab-only @@ -237,7 +243,7 @@ Feature: Using hints ### hints -> auto-follow-timeout - @not_osx + @not_mac Scenario: Ignoring key presses after auto-following hints When I set hints -> auto-follow-timeout to 1000 And I set hints -> mode to number diff --git a/tests/end2end/features/history.feature b/tests/end2end/features/history.feature index a28f2c89e..9317bde64 100644 --- a/tests/end2end/features/history.feature +++ b/tests/end2end/features/history.feature @@ -11,44 +11,44 @@ Feature: Page history Scenario: Simple history saving When I open data/numbers/1.txt And I open data/numbers/2.txt - Then the history file should contain: + Then the history should contain: http://localhost:(port)/data/numbers/1.txt http://localhost:(port)/data/numbers/2.txt - + Scenario: History item with title When I open data/title.html - Then the history file should contain: + Then the history should contain: http://localhost:(port)/data/title.html Test title Scenario: History item with redirect When I open redirect-to?url=data/title.html without waiting And I wait until data/title.html is loaded - Then the history file should contain: + Then the history should contain: r http://localhost:(port)/redirect-to?url=data/title.html Test title http://localhost:(port)/data/title.html Test title - + Scenario: History item with spaces in URL When I open data/title with spaces.html - Then the history file should contain: + Then the history should contain: http://localhost:(port)/data/title%20with%20spaces.html Test title Scenario: History item with umlauts When I open data/äöü.html - Then the history file should contain: + Then the history should contain: http://localhost:(port)/data/%C3%A4%C3%B6%C3%BC.html Chäschüechli - + @flaky @qtwebengine_todo: Error page message is not implemented Scenario: History with an error When I run :open file:///does/not/exist And I wait for "Error while loading file:///does/not/exist: Error opening /does/not/exist: *" in the log - Then the history file should contain: + Then the history should contain: file:///does/not/exist Error loading page: file:///does/not/exist @qtwebengine_todo: Error page message is not implemented Scenario: History with a 404 When I open status/404 without waiting And I wait for "Error while loading http://localhost:*/status/404: NOT FOUND" in the log - Then the history file should contain: + Then the history should contain: http://localhost:(port)/status/404 Error loading page: http://localhost:(port)/status/404 Scenario: History with invalid URL @@ -57,29 +57,51 @@ Feature: Page history And I run :click-element id open-invalid Then "Changing title for idx 1 to 'about:blank'" should be logged + Scenario: History with data URL + When I open data/data_link.html + And I run :click-element id link + And I wait until data:;base64,cXV0ZWJyb3dzZXI= is loaded + Then the history should contain: + http://localhost:(port)/data/data_link.html data: link + + Scenario: History with view-source URL + When I open data/title.html + And I run :view-source + And I wait for "Changing title for idx * to 'Source for http://localhost:*/data/title.html'" in the log + Then the history should contain: + http://localhost:(port)/data/title.html Test title + Scenario: Clearing history When I open data/title.html And I run :history-clear --force - Then the history file should be empty + Then the history 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 + Then the history should be empty Scenario: History with yanked URL and 'add to history' flag When I open data/hints/html/simple.html And I hint with args "--add-history links yank" and follow a - Then the history file should contain: + Then the history should contain: 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/data + And I open qute://history + Then the page should contain the plaintext "3.txt" + Then the page should contain the plaintext "4.txt" + + Scenario: Listing history with qute:history redirect + When I open data/numbers/3.txt + And I open data/numbers/4.txt + And I open qute:history without waiting + And I wait until qute://history is loaded Then the page should contain the plaintext "3.txt" Then the page should contain the plaintext "4.txt" @@ -91,4 +113,3 @@ Feature: Page history And I run :open http://foo%40bar@baz Then "QFSFileEngine::open: No file name specified" should be logged And "Error while loading : Host not found" should be logged - And "Ignoring invalid URL being added to history" should be logged diff --git a/tests/end2end/features/javascript.feature b/tests/end2end/features/javascript.feature index ab96866be..c6942f6e0 100644 --- a/tests/end2end/features/javascript.feature +++ b/tests/end2end/features/javascript.feature @@ -50,7 +50,7 @@ Feature: Javascript stuff And I open data/javascript/window_open.html in a new tab And I run :click-element id open-normal And I wait for "Changing title for idx 2 to 'about:blank'" in the log - And I run :tab-focus 2 + And I run :buffer window_open.html And I run :click-element id close-twice And I wait for "Focus object changed: *" in the log Then no crash should happen @@ -72,4 +72,56 @@ Feature: Javascript stuff Scenario: Executing jseval when javascript is disabled When I set content -> allow-javascript to false And I run :jseval console.log('jseval executed') + And I set content -> allow-javascript to true Then the javascript message "jseval executed" should be logged + + ## webelement issues (mostly with QtWebEngine) + + # https://github.com/qutebrowser/qutebrowser/issues/2569 + Scenario: Clicking on form element with tagName child + When I open data/issue2569.html + And I run :click-element id tagnameform + And I wait for "Sending fake click to *" in the log + Then no crash should happen + + Scenario: Clicking on form element with text child + When I open data/issue2569.html + And I run :click-element id textform + And I wait for "Sending fake click to *" in the log + Then no crash should happen + + Scenario: Clicking on svg element + When I open data/issue2569.html + And I run :click-element id icon + And I wait for "Sending fake click to *" in the log + Then no crash should happen + + Scenario: Clicking on li element + When I open data/issue2569.html + And I run :click-element id listitem + And I wait for "Sending fake click to *" in the log + Then no crash should happen + + # We load the tab in the background, and the HTML sets the window size for + # when it's hidden. + # Then, "the window sizes should be the same" uses :jseval to set the size + # when it's shown, and compares the two. + # https://github.com/qutebrowser/qutebrowser/issues/1190 + # https://github.com/qutebrowser/qutebrowser/issues/2495 + + Scenario: Checking visible/invisible window size + When I run :tab-only + And I set general -> log-javascript-console to info + And I open data/javascript/windowsize.html in a new background tab + And I wait for "[*/data/javascript/windowsize.html:*] loaded" in the log + And I run :tab-next + Then the window sizes should be the same + + Scenario: Checking visible/invisible window size with vertical tabbar + When I run :tab-only + And I set general -> log-javascript-console to info + And I set tabs -> position to left + And I open data/javascript/windowsize.html in a new background tab + And I wait for "[*/data/javascript/windowsize.html:*] loaded" in the log + And I run :tab-next + Then the window sizes should be the same diff --git a/tests/end2end/features/keyinput.feature b/tests/end2end/features/keyinput.feature index 452d66757..93fbb1b93 100644 --- a/tests/end2end/features/keyinput.feature +++ b/tests/end2end/features/keyinput.feature @@ -202,34 +202,30 @@ Feature: Keyboard input # Macros Scenario: Recording a simple macro - Given I open data/scroll/simple.html - And I run :tab-only - When I run :scroll down with count 6 - And I wait until the scroll position changed - And I run :record-macro + When I run :record-macro And I press the key "a" - And I run :scroll up - And I run :scroll up - And I wait until the scroll position changed + And I run :message-info "foo 1" + And I run :message-info "bar 1" And I run :record-macro And I run :run-macro with count 2 And I press the key "a" - And I wait until the scroll position changed to 0/0 - Then the page should not be scrolled + Then the message "foo 1" should be shown + And the message "bar 1" should be shown + And the message "foo 1" should be shown + And the message "bar 1" should be shown + And the message "foo 1" should be shown + And the message "bar 1" should be shown Scenario: Recording a named macro - Given I open data/scroll/simple.html - And I run :tab-only - When I run :scroll down with count 6 - And I wait until the scroll position changed + When I run :record-macro foo + And I run :message-info "foo 2" + And I run :message-info "bar 2" And I run :record-macro foo - And I run :scroll up - And I run :scroll up - And I wait until the scroll position changed - And I run :record-macro foo - And I run :run-macro foo with count 2 - And I wait until the scroll position changed to 0/0 - Then the page should not be scrolled + And I run :run-macro foo + Then the message "foo 2" should be shown + And the message "bar 2" should be shown + And the message "foo 2" should be shown + And the message "bar 2" should be shown Scenario: Running an invalid macro Given I open data/scroll/simple.html @@ -264,17 +260,12 @@ Feature: Keyboard input 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 + When 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 :message-info "foo 3" 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 + Then the message "foo 3" should be shown + And the message "foo 3" should be shown diff --git a/tests/end2end/features/marks.feature b/tests/end2end/features/marks.feature index 28de753c9..31ddd034d 100644 --- a/tests/end2end/features/marks.feature +++ b/tests/end2end/features/marks.feature @@ -27,6 +27,7 @@ Feature: Setting positional marks And I wait until the scroll position changed to 10/20 Then the page should be scrolled to 10 20 + @qtwebengine_flaky Scenario: Setting the same local mark on another page When I run :scroll-px 5 10 And I wait until the scroll position changed to 5/10 diff --git a/tests/end2end/features/misc.feature b/tests/end2end/features/misc.feature index cff7d7854..bf2b05697 100644 --- a/tests/end2end/features/misc.feature +++ b/tests/end2end/features/misc.feature @@ -187,6 +187,7 @@ Feature: Various utility commands. # :stop/:reload + @flaky Scenario: :stop Given I have a fresh instance # We can't use "When I open" because we don't want to wait for load @@ -308,6 +309,15 @@ Feature: Various utility commands. - about:blank - qute://help/index.html (active) + # https://github.com/qutebrowser/qutebrowser/issues/2513 + Scenario: Opening link with qute:help + When the documentation is up to date + And I run :tab-only + And I open qute:help without waiting + And I wait for "Changing title for idx 0 to 'qutebrowser help'" in the log + And I hint with args "links normal" and follow a + Then qute://help/quickstart.html should be loaded + # :history Scenario: :history without arguments @@ -354,7 +364,7 @@ Feature: Various utility commands. And I open data/misc/test.pdf Then "Download test.pdf finished" should be logged - @qtwebengine_skip: pdfjs is not implemented yet @qtwebkit_ng_xfail: https://github.com/annulen/webkit/issues/428 + @qtwebengine_skip: pdfjs is not implemented yet 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 @@ -382,7 +392,7 @@ Feature: Various utility commands. And I run :debug-pyeval QApplication.instance().activeModalWidget().close() Then no crash should happen - # On Windows/OS X, we get a "QPrintDialog: Cannot be used on non-native + # On Windows/macOS, we get a "QPrintDialog: Cannot be used on non-native # printers" qWarning. # # Disabled because it causes weird segfaults and QPainter warnings in Qt... @@ -405,12 +415,12 @@ Feature: Various utility commands. # :pyeval Scenario: Running :pyeval When I run :debug-pyeval 1+1 - And I wait until qute:pyeval is loaded + And I wait until qute://pyeval is loaded Then the page should contain the plaintext "2" Scenario: Causing exception in :pyeval When I run :debug-pyeval 1/0 - And I wait until qute:pyeval is loaded + And I wait until qute://pyeval is loaded Then the page should contain the plaintext "ZeroDivisionError" Scenario: Running :pyeval with --quiet @@ -462,26 +472,30 @@ Feature: Various utility commands. Scenario: Setting a custom user-agent header When I set network -> user-agent to toaster And I open headers + And I run :jseval console.log(window.navigator.userAgent) Then the header User-Agent should be set to toaster + And the javascript message "toaster" should be logged Scenario: Setting the default user-agent header When I set network -> user-agent to And I open headers + And I run :jseval console.log(window.navigator.userAgent) Then the header User-Agent should be set to Mozilla/5.0 * + And the javascript message "Mozilla/5.0 *" should be logged ## :messages - Scenario: Showing error messages + Scenario: :messages without level When I run :message-error the-error-message And I run :message-warning the-warning-message And I run :message-info the-info-message And I run :messages - Then qute://log?level=error should be loaded + Then qute://log?level=info should be loaded And the error "the-error-message" should be shown And the warning "the-warning-message" should be shown And the page should contain the plaintext "the-error-message" - And the page should not contain the plaintext "the-warning-message" - And the page should not contain the plaintext "the-info-message" + And the page should contain the plaintext "the-warning-message" + And the page should contain the plaintext "the-info-message" Scenario: Showing messages of type 'warning' or greater When I run :message-error the-error-message @@ -512,20 +526,18 @@ Feature: Various utility commands. When I run :messages cataclysmic Then the error "Invalid log level cataclysmic!" should be shown - Scenario: Using qute:log directly - When I open qute:log + Scenario: Using qute://log directly + When I open qute://log without waiting + # With Qt 5.9, we don't get a loaded message? + And I wait for "Changing title for idx * to 'log'" in the log Then no crash should happen - Scenario: Using qute:plainlog directly - When I open qute:plainlog + Scenario: Using qute://plainlog directly + When I open qute://plainlog without waiting + # With Qt 5.9, we don't get a loaded message? + And I wait for "Changing title for idx * to 'log'" in the log Then no crash should happen - Scenario: Using :messages without messages - Given I have a fresh instance - When I run :messages - Then qute://log?level=error should be loaded - And the page should contain the plaintext "No messages to show." - ## https://github.com/qutebrowser/qutebrowser/issues/1523 Scenario: Completing a single option argument @@ -538,6 +550,16 @@ Feature: Various utility commands. When I run :message-i "Hello World" (invalid command) Then the error "message-i: no such command" should be shown + Scenario: Multiple leading : in command + When I run :::::set-cmd-text ::::message-i "Hello World" + And I run :command-accept + Then the message "Hello World" should be shown + + Scenario: Whitespace in command + When I run : : set-cmd-text : : message-i "Hello World" + And I run :command-accept + Then the message "Hello World" should be shown + # We can't run :message-i as startup command, so we use # :set-cmd-text @@ -546,26 +568,6 @@ Feature: Various utility commands. And I run :command-accept Then the message "Hello World" should be shown - ## https://github.com/qutebrowser/qutebrowser/issues/1219 - - @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 - And I wait until cookies is loaded - And I open cookies in a new tab - And I set general -> private-browsing to false - Then the cookie qute-test should be set to 42 - - ## https://github.com/qutebrowser/qutebrowser/issues/1742 - - @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" - @no_xvfb Scenario: :window-only Given I run :tab-only @@ -632,7 +634,7 @@ Feature: Various utility commands. And I run :command-history-prev And I run :command-accept Then the message "blah" should be shown - + Scenario: Browsing through commands When I run :set-cmd-text :message-info blarg And I run :command-accept @@ -644,7 +646,7 @@ Feature: Various utility commands. And I run :command-history-next And I run :command-accept Then the message "blarg" should be shown - + Scenario: Calling previous command when history is empty Given I have a fresh instance When I run :set-cmd-text : @@ -658,20 +660,6 @@ 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 @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 - And I set general -> private-browsing to true - And I run :set-cmd-text :message-error "This should only be shown once" - And I run :command-accept - And I wait for the error "This should only be shown once" - And I run :set-cmd-text : - And I run :command-history-prev - 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 @@ -681,16 +669,29 @@ Feature: Various utility commands. ## Renderer crashes # Skipped on Windows as "... has stopped working" hangs. - @qtwebkit_skip @no_invalid_lines @posix + @qtwebkit_skip @no_invalid_lines @posix @qt<5.9 Scenario: Renderer crash When I run :open -t chrome://crash Then the error "Renderer process crashed" should be shown - @qtwebkit_skip @no_invalid_lines + @qtwebkit_skip @no_invalid_lines @qt<5.9 Scenario: Renderer kill When I run :open -t chrome://kill Then the error "Renderer process was killed" should be shown + # Skipped on Windows as "... has stopped working" hangs. + @qtwebkit_skip @no_invalid_lines @posix @qt>=5.9 + Scenario: Renderer crash (5.9) + When I run :open -t chrome://crash + Then "Renderer process crashed" should be logged + And "* 'Error loading chrome://crash/'" should be logged + + @qtwebkit_skip @no_invalid_lines @qt>=5.9 + Scenario: Renderer kill (5.9) + When I run :open -t chrome://kill + Then "Renderer process was killed" should be logged + And "* 'Error loading chrome://kill/'" should be logged + # https://github.com/qutebrowser/qutebrowser/issues/2290 @qtwebkit_skip @no_invalid_lines Scenario: Navigating to URL after renderer process is gone @@ -701,5 +702,9 @@ Feature: Various utility commands. 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) + + ## Other + + Scenario: Open qute://version + When I open qute://version + Then the page should contain the plaintext "Version info" diff --git a/tests/end2end/features/open.feature b/tests/end2end/features/open.feature index efc132466..188347bb2 100644 --- a/tests/end2end/features/open.feature +++ b/tests/end2end/features/open.feature @@ -33,7 +33,7 @@ Feature: Opening pages Scenario: :open with -t and -b When I run :open -t -b foo.bar - Then the error "Only one of -t/-b/-w can be given!" should be shown + Then the error "Only one of -t/-b/-w/-p can be given!" should be shown Scenario: Searching with :open When I set general -> auto-search to naive diff --git a/tests/end2end/features/private.feature b/tests/end2end/features/private.feature new file mode 100644 index 000000000..55ded35d7 --- /dev/null +++ b/tests/end2end/features/private.feature @@ -0,0 +1,155 @@ +# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et: + +Feature: Using private browsing + + Background: + Given I open about:blank + And I clean up open tabs + + Scenario: Opening new tab in private window + When I open about:blank in a private window + And I open cookies/set?qute-private-test=42 without waiting in a new tab + And I wait until cookies is loaded + And I run :close + And I wait for "removed: main-window" in the log + And I open cookies + Then the cookie qute-private-test should not be set + + Scenario: Opening new tab in private window with :navigate next + When I open data/navigate in a private window + And I run :navigate -t next + And I wait until data/navigate/next.html is loaded + And I open cookies/set?qute-private-test=42 without waiting + And I wait until cookies is loaded + And I run :close + And I wait for "removed: main-window" in the log + And I open cookies + Then the cookie qute-private-test should not be set + + Scenario: Using command history in a new private browsing window + When I run :set-cmd-text :message-info "Hello World" + And I run :command-accept + And I open about:blank in a private window + And I run :set-cmd-text :message-error "This should only be shown once" + And I run :command-accept + And I wait for the error "This should only be shown once" + And I run :close + And I wait for "removed: main-window" in the log + And I run :set-cmd-text : + And I run :command-history-prev + And I run :command-accept + # Then the error should not be shown again + + ## https://github.com/qutebrowser/qutebrowser/issues/1219 + + @qtwebkit_ng_skip: private browsing is not implemented yet + Scenario: Sharing cookies with private browsing + When I open cookies/set?qute-test=42 without waiting in a private window + And I wait until cookies is loaded + And I open cookies in a new tab + And I set general -> private-browsing to false + Then the cookie qute-test should be set to 42 + + Scenario: Opening private window with :navigate increment + # Private window handled in commands.py + When I open data/numbers/1.txt in a private window + And I run :window-only + And I run :navigate -w increment + And I wait until data/numbers/2.txt is loaded + Then the session should look like: + windows: + - private: True + tabs: + - history: + - url: http://localhost:*/data/numbers/1.txt + - private: True + tabs: + - history: + - url: http://localhost:*/data/numbers/2.txt + + Scenario: Opening private window with :navigate next + # Private window handled in navigate.py + When I open data/navigate in a private window + And I run :window-only + And I run :navigate -w next + And I wait until data/navigate/next.html is loaded + Then the session should look like: + windows: + - private: True + tabs: + - history: + - url: http://localhost:*/data/navigate + - private: True + tabs: + - history: + - url: http://localhost:*/data/navigate/next.html + + Scenario: Opening private window with :tab-clone + When I open data/hello.txt in a private window + And I run :window-only + And I run :tab-clone -w + And I wait until data/hello.txt is loaded + Then the session should look like: + windows: + - private: True + tabs: + - history: + - url: http://localhost:*/data/hello.txt + - private: True + tabs: + - history: + - url: http://localhost:*/data/hello.txt + + Scenario: Opening private window via :click-element + When I open data/click_element.html in a private window + And I run :window-only + And I run :click-element --target window id link + And I wait until data/hello.txt is loaded + Then the session should look like: + windows: + - private: True + tabs: + - history: + - url: http://localhost:*/data/click_element.html + - private: True + tabs: + - history: + - url: http://localhost:*/data/hello.txt + + Scenario: Skipping private window when saving session + When I open data/hello.txt in a private window + And I run :session-save (tmpdir)/session.yml + And I wait for "Saved session */session.yml." in the log + Then the file session.yml should not contain "hello.txt" + + # https://github.com/qutebrowser/qutebrowser/issues/2638 + Scenario: Turning off javascript with private browsing + When I set content -> allow-javascript to false + And I open data/javascript/consolelog.html in a private window + Then the javascript message "console.log works!" should not be logged + + # Probably needs qutewm to work properly... + @qtwebkit_skip: Only applies to QtWebEngine @xfail_norun + Scenario: Make sure local storage is isolated with private browsing + When I open data/hello.txt in a private window + And I run :jseval localStorage.qute_private_test = 42 + And I wait for "42" in the log + And I run :close + And I wait for "removed: main-window" in the log + And I open data/hello.txt + And I run :jseval localStorage.qute_private_test + Then "No output or error" should be logged + + Scenario: Opening quickmark in private window + When I open data/numbers/1.txt in a private window + And I run :window-only + And I run :quickmark-add http://localhost:(port)/data/numbers/2.txt two + And I run :quickmark-load two + And I wait until data/numbers/2.txt is loaded + Then the session should look like: + windows: + - private: True + tabs: + - history: + - url: http://localhost:*/data/numbers/1.txt + - url: http://localhost:*/data/numbers/2.txt diff --git a/tests/end2end/features/prompts.feature b/tests/end2end/features/prompts.feature index 9b5d7aae5..e733d69b1 100644 --- a/tests/end2end/features/prompts.feature +++ b/tests/end2end/features/prompts.feature @@ -219,14 +219,14 @@ Feature: Prompts And I run :click-element id button Then the javascript message "geolocation permission denied" should be logged - @ci @not_osx @qt!=5.8 + @ci @not_mac @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 @qt!=5.8 + @ci @not_mac @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 diff --git a/tests/end2end/features/scroll.feature b/tests/end2end/features/scroll.feature index 44f60aa66..28658e1e6 100644 --- a/tests/end2end/features/scroll.feature +++ b/tests/end2end/features/scroll.feature @@ -39,6 +39,7 @@ Feature: Scrolling And I wait until the scroll position changed to 0/0 Then the page should not be scrolled + @qtwebengine_flaky Scenario: Scrolling left and right with count When I run :scroll-px 10 0 with count 2 And I wait until the scroll position changed to 20/0 @@ -146,7 +147,6 @@ Feature: Scrolling Scenario: Scrolling down with a very big count When I run :scroll down with count 99999999999 - And I wait until the scroll position changed # Make sure it doesn't hang And I run :message-info "Still alive!" Then the message "Still alive!" should be shown @@ -292,6 +292,13 @@ Feature: Scrolling And I run :scroll-page --bottom-navigate next 0 1 Then data/hello2.txt should be loaded + Scenario: :scroll-page with --bottom-navigate when not at the bottom + When I run :scroll-px 0 10 + And I wait until the scroll position changed + And I run :scroll-page --bottom-navigate next 0 1 + Then the following tabs should be open: + - data/scroll/simple.html + Scenario: :scroll-page with --top-navigate When I run :scroll-page --top-navigate prev 0 -1 Then data/hello3.txt should be loaded @@ -314,3 +321,12 @@ Feature: Scrolling And I wait until the scroll position changed And I run :scroll-page --bottom-navigate next 0 1 Then data/hello2.txt should be loaded + + Scenario: Scrolling to anchor in background tab + When I set general -> log-javascript-console to info + And I open about:blank + And I run :tab-only + And I open data/scroll/simple.html#anchor in a new background tab + And I run :tab-next + And I run :jseval --world main checkAnchor() + Then "[*] [PASS] Positions equal: *" should be logged diff --git a/tests/end2end/features/search.feature b/tests/end2end/features/search.feature index 4d8b8e17f..7e0151976 100644 --- a/tests/end2end/features/search.feature +++ b/tests/end2end/features/search.feature @@ -11,42 +11,45 @@ Feature: Searching on a page Scenario: Searching text When I run :search foo - And I run :yank selection - Then the clipboard should contain "foo" + And I wait for "search found foo" in the log + Then "foo" should be found Scenario: Searching twice When I run :search foo + And I wait for "search found foo" in the log And I run :search bar - And I run :yank selection - Then the clipboard should contain "Bar" + And I wait for "search found bar" in the log + Then "Bar" should be found Scenario: Searching with --reverse When I set general -> ignore-case to true And I run :search -r foo - And I run :yank selection - Then the clipboard should contain "Foo" + And I wait for "search found foo with flags FindBackward" in the log + Then "Foo" should be found Scenario: Searching without matches When I run :search doesnotmatch + And I wait for "search didn't find doesnotmatch" in the log Then the warning "Text 'doesnotmatch' not found on page!" should be shown @xfail_norun Scenario: Searching with / and spaces at the end (issue 874) When I run :set-cmd-text -s /space And I run :command-accept - And I run :yank selection - Then the clipboard should contain "space " + And I wait for "search found space " in the log + Then "space " should be found Scenario: Searching with / and slash in search term (issue 507) When I run :set-cmd-text -s //slash And I run :command-accept - And I run :yank selection - Then the clipboard should contain "/slash" + And I wait for "search found /slash" in the log + Then "/slash" should be found # This doesn't work because this is QtWebKit behavior. @xfail_norun Scenario: Searching text with umlauts When I run :search blub + And I wait for "search didn't find blub" in the log Then the warning "Text 'blub' not found on page!" should be shown ## ignore-case @@ -54,49 +57,52 @@ Feature: Searching on a page Scenario: Searching text with ignore-case = true When I set general -> ignore-case to true And I run :search bar - And I run :yank selection - Then the clipboard should contain "Bar" + And I wait for "search found bar" in the log + Then "Bar" should be found Scenario: Searching text with ignore-case = false When I set general -> ignore-case to false And I run :search bar - And I run :yank selection - Then the clipboard should contain "bar" + And I wait for "search found bar with flags FindCaseSensitively" in the log + Then "bar" should be found Scenario: Searching text with ignore-case = smart (lower-case) When I set general -> ignore-case to smart And I run :search bar - And I run :yank selection - Then the clipboard should contain "Bar" + And I wait for "search found bar" in the log + Then "Bar" should be found Scenario: Searching text with ignore-case = smart (upper-case) When I set general -> ignore-case to smart And I run :search Foo - And I run :yank selection - Then the clipboard should contain "Foo" # even though foo was first + And I wait for "search found Foo with flags FindCaseSensitively" in the log + Then "Foo" should be found # even though foo was first ## :search-next Scenario: Jumping to next match When I set general -> ignore-case to true And I run :search foo + And I wait for "search found foo" in the log And I run :search-next - And I run :yank selection - Then the clipboard should contain "Foo" + And I wait for "next_result found foo" in the log + Then "Foo" should be found Scenario: Jumping to next match with count When I set general -> ignore-case to true And I run :search baz + And I wait for "search found baz" in the log And I run :search-next with count 2 - And I run :yank selection - Then the clipboard should contain "BAZ" + And I wait for "next_result found baz" in the log + Then "BAZ" should be found Scenario: Jumping to next match with --reverse When I set general -> ignore-case to true And I run :search --reverse foo + And I wait for "search found foo with flags FindBackward" in the log And I run :search-next - And I run :yank selection - Then the clipboard should contain "foo" + And I wait for "next_result found foo with flags FindBackward" in the log + Then "foo" should be found Scenario: Jumping to next match without search # Make sure there was no search in the same window before @@ -107,46 +113,55 @@ Feature: Searching on a page Scenario: Repeating search in a second tab (issue #940) When I open data/search.html in a new tab And I run :search foo + And I wait for "search found foo" in the log And I run :tab-prev And I run :search-next - And I run :yank selection - Then the clipboard should contain "foo" + And I wait for "search found foo" in the log + Then "foo" should be found # https://github.com/qutebrowser/qutebrowser/issues/2438 Scenario: Jumping to next match after clearing When I set general -> ignore-case to true And I run :search foo + And I wait for "search found foo" in the log And I run :search And I run :search-next - And I run :yank selection - Then the clipboard should contain "foo" + And I wait for "next_result found foo" in the log + Then "foo" should be found ## :search-prev Scenario: Jumping to previous match When I set general -> ignore-case to true And I run :search foo + And I wait for "search found foo" in the log And I run :search-next + And I wait for "next_result found foo" in the log And I run :search-prev - And I run :yank selection - Then the clipboard should contain "foo" + And I wait for "prev_result found foo with flags FindBackward" in the log + Then "foo" should be found Scenario: Jumping to previous match with count When I set general -> ignore-case to true And I run :search baz + And I wait for "search found baz" in the log And I run :search-next + And I wait for "next_result found baz" in the log And I run :search-next + And I wait for "next_result found baz" in the log And I run :search-prev with count 2 - And I run :yank selection - Then the clipboard should contain "baz" + And I wait for "prev_result found baz with flags FindBackward" in the log + Then "baz" should be found Scenario: Jumping to previous match with --reverse When I set general -> ignore-case to true And I run :search --reverse foo + And I wait for "search found foo with flags FindBackward" in the log And I run :search-next + And I wait for "next_result found foo with flags FindBackward" in the log And I run :search-prev - And I run :yank selection - Then the clipboard should contain "Foo" + And I wait for "prev_result found foo" in the log + Then "Foo" should be found Scenario: Jumping to previous match without search # Make sure there was no search in the same window before @@ -158,17 +173,68 @@ Feature: Searching on a page Scenario: Wrapping around page When I run :search foo + And I wait for "search found foo" in the log And I run :search-next + And I wait for "next_result found foo" in the log And I run :search-next - And I run :yank selection - Then the clipboard should contain "foo" + And I wait for "next_result found foo" in the log + Then "foo" should be found Scenario: Wrapping around page with --reverse When I run :search --reverse foo + And I wait for "search found foo with flags FindBackward" in the log And I run :search-next + And I wait for "next_result found foo with flags FindBackward" in the log And I run :search-next - And I run :yank selection - Then the clipboard should contain "Foo" + And I wait for "next_result found foo with flags FindBackward" in the log + Then "Foo" should be found # TODO: wrapping message with scrolling # TODO: wrapping message without scrolling + + ## follow searched links + Scenario: Follow a searched link + When I run :search follow + And I wait for "search found follow" in the log + And I run :follow-selected + Then data/hello.txt should be loaded + + Scenario: Follow a searched link in a new tab + When I run :window-only + And I run :search follow + And I wait for "search found follow" in the log + And I run :follow-selected -t + And I wait until data/hello.txt is loaded + Then the following tabs should be open: + - data/search.html + - data/hello.txt (active) + + Scenario: Don't follow searched text + When I run :window-only + And I run :search foo + And I wait for "search found foo" in the log + And I run :follow-selected + Then the following tabs should be open: + - data/search.html (active) + + Scenario: Don't follow searched text in a new tab + When I run :window-only + And I run :search foo + And I wait for "search found foo" in the log + And I run :follow-selected -t + Then the following tabs should be open: + - data/search.html (active) + + Scenario: Follow a manually selected link + When I run :jseval --file (testdata)/search_select.js + And I run :follow-selected + Then data/hello.txt should be loaded + + Scenario: Follow a manually selected link in a new tab + When I run :window-only + And I run :jseval --file (testdata)/search_select.js + And I run :follow-selected -t + And I wait until data/hello.txt is loaded + Then the following tabs should be open: + - data/search.html + - data/hello.txt (active) diff --git a/tests/end2end/features/sessions.feature b/tests/end2end/features/sessions.feature index 226d3107d..5ec6e168a 100644 --- a/tests/end2end/features/sessions.feature +++ b/tests/end2end/features/sessions.feature @@ -342,7 +342,7 @@ Feature: Saving and loading sessions Scenario: Loading a directory When I run :session-load (tmpdir) Then the error "Error while loading session: *" should be shown - + Scenario: Loading internal session without --force When I run :session-save --force _internal And I run :session-load _internal @@ -367,3 +367,24 @@ Feature: Saving and loading sessions Scenario: Loading a session which doesn't exist When I run :session-load inexistent_session Then the error "Session inexistent_session not found!" should be shown + + + # Test load/save of pinned tabs + + Scenario: Saving/Loading a session with pinned tabs + 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-pin with count 2 + And I run :session-save pin_session + And I run :tab-only --force + And I run :tab-close --force + And I run :session-load -c pin_session + And I wait until data/numbers/3.txt is loaded + And I run :tab-focus 2 + And I run :open hello world + Then the message "Tab is pinned!" should be shown + And the following tabs should be open: + - data/numbers/1.txt + - data/numbers/2.txt (active) (pinned) + - data/numbers/3.txt diff --git a/tests/end2end/features/set.feature b/tests/end2end/features/set.feature index b2b165542..bc144eb6e 100644 --- a/tests/end2end/features/set.feature +++ b/tests/end2end/features/set.feature @@ -78,15 +78,15 @@ Feature: Setting settings. When I run :set -t colors statusbar.bg green Then colors -> statusbar.bg should be green - # qute:settings isn't actually implemented on QtWebEngine, but this works + # qute://settings isn't actually implemented on QtWebEngine, but this works # (and displays a page saying it's not available) - Scenario: Opening qute:settings + Scenario: Opening qute://settings When I run :set - And I wait until qute:settings is loaded + And I wait until qute://settings is loaded Then the following tabs should be open: - - qute:settings (active) + - qute://settings (active) - @qtwebengine_todo: qute:settings is not implemented yet + @qtwebengine_todo: qute://settings is not implemented yet Scenario: Focusing input fields in qute://settings and entering valid value When I set general -> ignore-case to false And I open qute://settings @@ -101,7 +101,7 @@ Feature: Setting settings. And I press the key "" Then general -> ignore-case should be true - @qtwebengine_todo: qute:settings is not implemented yet + @qtwebengine_todo: qute://settings is not implemented yet Scenario: Focusing input fields in qute://settings and entering invalid value When I open qute://settings # scroll to the right - the table does not fit in the default screen diff --git a/tests/end2end/features/spawn.feature b/tests/end2end/features/spawn.feature index 8e3f88bd1..e2b5fdd5b 100644 --- a/tests/end2end/features/spawn.feature +++ b/tests/end2end/features/spawn.feature @@ -55,3 +55,8 @@ Feature: :spawn Then the following tabs should be open: - about:blank - about:blank (active) + + @posix + Scenario: Running :spawn with userscript that expects the stdin getting closed + When I run :spawn -u (testdata)/userscripts/stdinclose.py + Then the message "stdin closed" should be shown diff --git a/tests/end2end/features/tabs.feature b/tests/end2end/features/tabs.feature index 5e571d42d..8097f390e 100644 --- a/tests/end2end/features/tabs.feature +++ b/tests/end2end/features/tabs.feature @@ -236,6 +236,18 @@ Feature: Tab management - data/numbers/2.txt - data/numbers/3.txt + Scenario: :tab-focus with current tab number + 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 1 + And I run :tab-focus 3 + And I run :tab-focus 3 + Then the following tabs should be open: + - data/numbers/1.txt (active) + - data/numbers/2.txt + - data/numbers/3.txt + Scenario: :tab-focus with -1 When I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab @@ -607,12 +619,25 @@ Feature: Tab management title: Test title # https://github.com/qutebrowser/qutebrowser/issues/2289 - @qtwebkit_skip @qt>=5.8 + + @qtwebkit_skip @qt==5.8.0 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 + @qtwebkit_skip @qt<5.9 + Scenario: Cloning a tab with a view-source URL + When I open view-source:http://localhost:(port) + And I run :tab-clone + Then the error "Can't serialize special URL!" should be shown + + @qtwebkit_skip @qt>=5.9 + Scenario: Cloning a tab with a special URL (Qt 5.9) + When I open chrome://gpu + And I run :tab-clone + Then no crash should happen + # :tab-detach Scenario: Detaching a tab @@ -768,18 +793,6 @@ 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 @@ -1025,3 +1038,131 @@ Feature: Tab management - tabs: - history: - url: http://localhost:*/data/hello.txt + + # :tab-pin + + Scenario: :tab-pin command + 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-pin + Then the following tabs should be open: + - data/numbers/1.txt + - data/numbers/2.txt + - data/numbers/3.txt (active) (pinned) + + Scenario: :tab-pin unpin + When I open data/numbers/1.txt + And I run :tab-pin + 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-pin + And I run :tab-pin + Then the following tabs should be open: + - data/numbers/1.txt (pinned) + - data/numbers/2.txt + - data/numbers/3.txt (active) + + Scenario: :tab-pin to index 2 + 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-pin with count 2 + Then the following tabs should be open: + - data/numbers/1.txt + - data/numbers/2.txt (pinned) + - data/numbers/3.txt (active) + + Scenario: :tab-pin with an invalid count + 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-pin with count 23 + Then the following tabs should be open: + - data/numbers/1.txt + - data/numbers/2.txt + - data/numbers/3.txt (active) + + Scenario: Pinned :tab-close prompt yes + When I open data/numbers/1.txt + And I run :tab-pin + And I open data/numbers/2.txt in a new tab + And I run :tab-pin + And I run :tab-close + And I wait for "*want to close a pinned tab*" in the log + And I run :prompt-accept yes + Then the following tabs should be open: + - data/numbers/1.txt (active) (pinned) + + Scenario: Pinned :tab-close prompt no + When I open data/numbers/1.txt + And I run :tab-pin + And I open data/numbers/2.txt in a new tab + And I run :tab-pin + And I run :tab-close + And I wait for "*want to close a pinned tab*" in the log + And I run :prompt-accept no + Then the following tabs should be open: + - data/numbers/1.txt (pinned) + - data/numbers/2.txt (active) (pinned) + + Scenario: Pinned :tab-only prompt yes + When I open data/numbers/1.txt + And I run :tab-pin + And I open data/numbers/2.txt in a new tab + And I run :tab-pin + And I run :tab-next + And I run :tab-only + And I wait for "*want to close a pinned tab*" in the log + And I run :prompt-accept yes + Then the following tabs should be open: + - data/numbers/1.txt (active) (pinned) + + Scenario: Pinned :tab-only prompt no + When I open data/numbers/1.txt + And I run :tab-pin + And I open data/numbers/2.txt in a new tab + And I run :tab-pin + And I run :tab-next + And I run :tab-only + And I wait for "*want to close a pinned tab*" in the log + And I run :prompt-accept no + Then the following tabs should be open: + - data/numbers/1.txt (active) (pinned) + - data/numbers/2.txt (pinned) + + Scenario: Pinned :tab-only close all but pinned tab + When I open data/numbers/1.txt + And I open data/numbers/2.txt in a new tab + And I run :tab-pin + And I run :tab-only + Then the following tabs should be open: + - data/numbers/2.txt (active) (pinned) + + Scenario: :tab-pin open url + When I open data/numbers/1.txt + And I run :tab-pin + And I open data/numbers/2.txt without waiting + Then the message "Tab is pinned!" should be shown + And the following tabs should be open: + - data/numbers/1.txt (active) (pinned) + + Scenario: Cloning a pinned tab + When I open data/numbers/1.txt + And I run :tab-pin + And I run :tab-clone + And I wait until data/numbers/1.txt is loaded + Then the following tabs should be open: + - data/numbers/1.txt (pinned) + - data/numbers/1.txt (pinned) (active) + + Scenario: Undo a pinned tab + When I open data/numbers/1.txt + And I open data/numbers/2.txt in a new tab + And I run :tab-pin + And I run :tab-close --force + And I run :undo + And I wait until data/numbers/2.txt is loaded + Then the following tabs should be open: + - data/numbers/1.txt + - data/numbers/2.txt (pinned) (active) diff --git a/tests/end2end/features/test_adblock_bdd.py b/tests/end2end/features/test_adblock_bdd.py index 9f4ae63b3..069c127aa 100644 --- a/tests/end2end/features/test_adblock_bdd.py +++ b/tests/end2end/features/test_adblock_bdd.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/features/test_backforward_bdd.py b/tests/end2end/features/test_backforward_bdd.py index ede51988a..187882b67 100644 --- a/tests/end2end/features/test_backforward_bdd.py +++ b/tests/end2end/features/test_backforward_bdd.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/features/test_caret_bdd.py b/tests/end2end/features/test_caret_bdd.py index aa42241c4..9e4e1dedd 100644 --- a/tests/end2end/features/test_caret_bdd.py +++ b/tests/end2end/features/test_caret_bdd.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/features/test_completion_bdd.py b/tests/end2end/features/test_completion_bdd.py index 030a16ffc..82e2df030 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 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -24,5 +24,5 @@ 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) + pattern = "Starting {} completion *".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 616c3cb14..4be175a66 100644 --- a/tests/end2end/features/test_downloads_bdd.py +++ b/tests/end2end/features/test_downloads_bdd.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -21,6 +21,7 @@ import os import sys import shlex +import pytest import pytest_bdd as bdd bdd.scenarios('downloads.feature') @@ -53,6 +54,14 @@ def clean_old_downloads(quteproc): quteproc.send_cmd(':download-clear') +@bdd.when("the unwritable dir is unwritable") +def check_unwritable(tmpdir): + unwritable = tmpdir / 'downloads' / 'unwritable' + if os.access(str(unwritable), os.W_OK): + # Docker container or similar + pytest.skip("Unwritable dir was writable") + + @bdd.when("I wait until the download is finished") def wait_for_download_finished(quteproc): quteproc.wait_for(category='downloads', message='Download * finished') diff --git a/tests/end2end/features/test_editor_bdd.py b/tests/end2end/features/test_editor_bdd.py index f26e2956f..7d38be5af 100644 --- a/tests/end2end/features/test_editor_bdd.py +++ b/tests/end2end/features/test_editor_bdd.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/features/test_hints_bdd.py b/tests/end2end/features/test_hints_bdd.py index b5304cb74..f39a15391 100644 --- a/tests/end2end/features/test_hints_bdd.py +++ b/tests/end2end/features/test_hints_bdd.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/features/test_history_bdd.py b/tests/end2end/features/test_history_bdd.py index 871dbcb98..319e36aee 100644 --- a/tests/end2end/features/test_history_bdd.py +++ b/tests/end2end/features/test_history_bdd.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -17,36 +17,29 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -import os.path +import logging +import re import pytest_bdd as bdd + bdd.scenarios('history.feature') -@bdd.then(bdd.parsers.parse("the history file should contain:\n{expected}")) -def check_history(quteproc, httpbin, expected): - history_file = os.path.join(quteproc.basedir, 'data', 'history') - quteproc.send_cmd(':save history') - quteproc.wait_for(message=':save saved history') +@bdd.then(bdd.parsers.parse("the history should contain:\n{expected}")) +def check_history(quteproc, httpbin, tmpdir, expected): + path = tmpdir / 'history' + quteproc.send_cmd(':debug-dump-history "{}"'.format(path)) + quteproc.wait_for(category='message', loglevel=logging.INFO, + message='Dumped history to {}'.format(path)) - expected = expected.replace('(port)', str(httpbin.port)).splitlines() + with path.open('r', encoding='utf-8') as f: + # ignore access times, they will differ in each run + actual = '\n'.join(re.sub('^\\d+-?', '', line).strip() for line in f) - with open(history_file, 'r', encoding='utf-8') as f: - lines = [] - for line in f: - if not line.strip(): - continue - print('history line: ' + line) - atime, line = line.split(' ', maxsplit=1) - line = line.rstrip() - if '-' in atime: - flags = atime.split('-')[1] - line = '{} {}'.format(flags, line) - lines.append(line) - - assert lines == expected + expected = expected.replace('(port)', str(httpbin.port)) + assert actual == expected -@bdd.then("the history file should be empty") -def check_history_empty(quteproc, httpbin): - check_history(quteproc, httpbin, '') +@bdd.then("the history should be empty") +def check_history_empty(quteproc, httpbin, tmpdir): + check_history(quteproc, httpbin, tmpdir, '') diff --git a/tests/end2end/features/test_invoke_bdd.py b/tests/end2end/features/test_invoke_bdd.py index 86faf8107..5d463608e 100644 --- a/tests/end2end/features/test_invoke_bdd.py +++ b/tests/end2end/features/test_invoke_bdd.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/features/test_javascript_bdd.py b/tests/end2end/features/test_javascript_bdd.py index 78d45ab5a..9f6c021ce 100644 --- a/tests/end2end/features/test_javascript_bdd.py +++ b/tests/end2end/features/test_javascript_bdd.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -19,3 +19,13 @@ import pytest_bdd as bdd bdd.scenarios('javascript.feature') + + +@bdd.then("the window sizes should be the same") +def check_window_sizes(quteproc): + hidden = quteproc.wait_for_js('hidden window size: *') + quteproc.send_cmd(':jseval --world main updateText("visible")') + visible = quteproc.wait_for_js('visible window size: *') + hidden_size = hidden.message.split()[-1] + visible_size = visible.message.split()[-1] + assert hidden_size == visible_size diff --git a/tests/end2end/features/test_keyinput_bdd.py b/tests/end2end/features/test_keyinput_bdd.py index d6b3134a3..ef5be0ee9 100644 --- a/tests/end2end/features/test_keyinput_bdd.py +++ b/tests/end2end/features/test_keyinput_bdd.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/features/test_marks_bdd.py b/tests/end2end/features/test_marks_bdd.py index 5e0e623f2..5b8e352dd 100644 --- a/tests/end2end/features/test_marks_bdd.py +++ b/tests/end2end/features/test_marks_bdd.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Ryan Roden-Corrent (rcorre) +# Copyright 2016-2017 Ryan Roden-Corrent (rcorre) # # This file is part of qutebrowser. # diff --git a/tests/end2end/features/test_misc_bdd.py b/tests/end2end/features/test_misc_bdd.py index 177f7f383..a47b89af4 100644 --- a/tests/end2end/features/test_misc_bdd.py +++ b/tests/end2end/features/test_misc_bdd.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -18,7 +18,6 @@ # along with qutebrowser. If not, see . import sys -import json import os.path import subprocess @@ -38,11 +37,13 @@ def update_documentation(): doc_path = os.path.join(base_path, 'html', 'doc') script_path = os.path.join(base_path, '..', 'scripts') - if not os.path.exists(doc_path): - # On CI, we can test this without actually building the docs - return + try: + os.mkdir(doc_path) + except FileExistsError: + pass - if all(docutils.docs_up_to_date(p) for p in os.listdir(doc_path)): + files = os.listdir(doc_path) + if files and all(docutils.docs_up_to_date(p) for p in files): return try: @@ -55,18 +56,6 @@ def update_documentation(): subprocess.call([sys.executable, update_script]) -@bdd.then(bdd.parsers.parse('the cookie {name} should be set to {value}')) -def check_cookie(quteproc, name, value): - """Check if a given cookie is set correctly. - - This assumes we're on the httpbin cookies page. - """ - content = quteproc.get_content() - data = json.loads(content) - print(data) - assert data['cookies'][name] == value - - @bdd.then(bdd.parsers.parse('the PDF {filename} should exist in the tmpdir')) def pdf_exists(quteproc, tmpdir, filename): path = tmpdir / filename diff --git a/tests/end2end/features/test_navigate_bdd.py b/tests/end2end/features/test_navigate_bdd.py index 5f922fedc..03812df83 100644 --- a/tests/end2end/features/test_navigate_bdd.py +++ b/tests/end2end/features/test_navigate_bdd.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/features/test_open_bdd.py b/tests/end2end/features/test_open_bdd.py index 48b7226f7..e5692c615 100644 --- a/tests/end2end/features/test_open_bdd.py +++ b/tests/end2end/features/test_open_bdd.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/features/test_private_bdd.py b/tests/end2end/features/test_private_bdd.py new file mode 100644 index 000000000..a9ed92e6e --- /dev/null +++ b/tests/end2end/features/test_private_bdd.py @@ -0,0 +1,50 @@ +# 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 . + +import json + +import pytest_bdd as bdd +bdd.scenarios('private.feature') + + +@bdd.then(bdd.parsers.parse('the cookie {name} should be set to {value}')) +def check_cookie(quteproc, name, value): + """Check if a given cookie is set correctly. + + This assumes we're on the httpbin cookies page. + """ + content = quteproc.get_content() + data = json.loads(content) + print(data) + assert data['cookies'][name] == value + + +@bdd.then(bdd.parsers.parse('the cookie {name} should not be set')) +def check_cookie_not_set(quteproc, name): + """Check if a given cookie is not set.""" + content = quteproc.get_content() + data = json.loads(content) + print(data) + assert name not in data['cookies'] + + +@bdd.then(bdd.parsers.parse('the file {name} should not contain "{text}"')) +def check_not_contain(tmpdir, name, text): + path = tmpdir / name + assert text not in path.read() diff --git a/tests/end2end/features/test_prompts_bdd.py b/tests/end2end/features/test_prompts_bdd.py index 3ebd53e8c..7a95d48c8 100644 --- a/tests/end2end/features/test_prompts_bdd.py +++ b/tests/end2end/features/test_prompts_bdd.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/features/test_scroll_bdd.py b/tests/end2end/features/test_scroll_bdd.py index de1ed3e0a..69199ebf3 100644 --- a/tests/end2end/features/test_scroll_bdd.py +++ b/tests/end2end/features/test_scroll_bdd.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/features/test_search_bdd.py b/tests/end2end/features/test_search_bdd.py index 12e5d9480..1b0c81488 100644 --- a/tests/end2end/features/test_search_bdd.py +++ b/tests/end2end/features/test_search_bdd.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -17,13 +17,26 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -import pytest +import json + import pytest_bdd as bdd # pylint: disable=unused-import from end2end.features.test_yankpaste_bdd import init_fake_clipboard -bdd.scenarios('search.feature') +@bdd.then(bdd.parsers.parse('"{text}" should be found')) +def check_found_text(request, quteproc, text): + if request.config.webengine: + # WORKAROUND + # This probably should work with Qt 5.9: + # https://codereview.qt-project.org/#/c/192920/ + # https://codereview.qt-project.org/#/c/192921/ + # https://bugreports.qt.io/browse/QTBUG-53134 + return + quteproc.send_cmd(':yank selection') + quteproc.wait_for(message='Setting fake clipboard: {}'.format( + json.dumps(text))) -pytestmark = pytest.mark.qtwebengine_skip("Searched text is not selected...") + +bdd.scenarios('search.feature') diff --git a/tests/end2end/features/test_sessions_bdd.py b/tests/end2end/features/test_sessions_bdd.py index d05b4d434..052748f90 100644 --- a/tests/end2end/features/test_sessions_bdd.py +++ b/tests/end2end/features/test_sessions_bdd.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/features/test_set_bdd.py b/tests/end2end/features/test_set_bdd.py index 2eabd8c56..fa94de88f 100644 --- a/tests/end2end/features/test_set_bdd.py +++ b/tests/end2end/features/test_set_bdd.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/features/test_spawn_bdd.py b/tests/end2end/features/test_spawn_bdd.py index e9ddf0301..432f95a53 100644 --- a/tests/end2end/features/test_spawn_bdd.py +++ b/tests/end2end/features/test_spawn_bdd.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/features/test_tabs_bdd.py b/tests/end2end/features/test_tabs_bdd.py index bcae6d60d..e86204ba9 100644 --- a/tests/end2end/features/test_tabs_bdd.py +++ b/tests/end2end/features/test_tabs_bdd.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/features/test_urlmarks_bdd.py b/tests/end2end/features/test_urlmarks_bdd.py index 170fdd30b..554ede3ec 100644 --- a/tests/end2end/features/test_urlmarks_bdd.py +++ b/tests/end2end/features/test_urlmarks_bdd.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/features/test_utilcmds_bdd.py b/tests/end2end/features/test_utilcmds_bdd.py index f90d587f6..01bfb1440 100644 --- a/tests/end2end/features/test_utilcmds_bdd.py +++ b/tests/end2end/features/test_utilcmds_bdd.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/features/test_yankpaste_bdd.py b/tests/end2end/features/test_yankpaste_bdd.py index 9deb4b3cf..8f2f56938 100644 --- a/tests/end2end/features/test_yankpaste_bdd.py +++ b/tests/end2end/features/test_yankpaste_bdd.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/features/test_zoom_bdd.py b/tests/end2end/features/test_zoom_bdd.py index 3f8728222..3dc94a8cd 100644 --- a/tests/end2end/features/test_zoom_bdd.py +++ b/tests/end2end/features/test_zoom_bdd.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/features/urlmarks.feature b/tests/end2end/features/urlmarks.feature index 873d83563..f2d074a32 100644 --- a/tests/end2end/features/urlmarks.feature +++ b/tests/end2end/features/urlmarks.feature @@ -7,12 +7,12 @@ Feature: quickmarks and bookmarks Scenario: Saving a bookmark When I open data/title.html And I run :bookmark-add - Then the message "Bookmarked http://localhost:*/data/title.html!" should be shown + Then the message "Bookmarked http://localhost:*/data/title.html" should be shown And the bookmark file should contain "http://localhost:*/data/title.html Test title" Scenario: Saving a bookmark with a provided url and title When I run :bookmark-add http://example.com "some example title" - Then the message "Bookmarked http://example.com!" should be shown + Then the message "Bookmarked http://example.com" should be shown And the bookmark file should contain "http://example.com some example title" Scenario: Saving a bookmark with a url but no title @@ -76,7 +76,7 @@ Feature: quickmarks and bookmarks Scenario: Loading a bookmark with -t and -b When I run :bookmark-load -t -b about:blank - Then the error "Only one of -t/-b/-w can be given!" should be shown + Then the error "Only one of -t/-b/-w/-p can be given!" should be shown Scenario: Deleting a bookmark which does not exist When I run :bookmark-del doesnotexist @@ -200,7 +200,7 @@ Feature: quickmarks and bookmarks Scenario: Loading a quickmark with -t and -b When I run :quickmark-add http://localhost:(port)/data/numbers/17.txt seventeen When I run :quickmark-load -t -b seventeen - Then the error "Only one of -t/-b/-w can be given!" should be shown + Then the error "Only one of -t/-b/-w/-p can be given!" should be shown Scenario: Deleting a quickmark which does not exist When I run :quickmark-del doesnotexist @@ -225,12 +225,12 @@ Feature: quickmarks and bookmarks Scenario: Listing quickmarks When I run :quickmark-add http://localhost:(port)/data/numbers/20.txt twenty And I run :quickmark-add http://localhost:(port)/data/numbers/21.txt twentyone - And I open qute:bookmarks + And I open qute://bookmarks Then the page should contain the plaintext "twenty" And the page should contain the plaintext "twentyone" Scenario: Listing bookmarks When I open data/title.html in a new tab And I run :bookmark-add - And I open qute:bookmarks + And I open qute://bookmarks Then the page should contain the plaintext "Test title" diff --git a/tests/end2end/features/utilcmds.feature b/tests/end2end/features/utilcmds.feature index f9c43be98..7fd5952a8 100644 --- a/tests/end2end/features/utilcmds.feature +++ b/tests/end2end/features/utilcmds.feature @@ -145,7 +145,7 @@ Feature: Miscellaneous utility commands exposed to the user. And I run :message-info oldstuff And I run :repeat 20 message-info otherstuff And I run :message-info newstuff - And I open qute:log + And I open qute://log Then the page should contain the plaintext "newstuff" And the page should not contain the plaintext "oldstuff" @@ -164,3 +164,10 @@ Feature: Miscellaneous utility commands exposed to the user. Scenario: Using debug-log-filter with invalid filter When I run :debug-log-filter blah Then the error "filters: Invalid value blah - expected one of: statusbar, *" should be shown + + Scenario: Using debug-log-filter + When I run :debug-log-filter commands,ipc,webview + And I run :enter-mode insert + And I run :debug-log-filter none + And I run :leave-mode + Then "Entering mode KeyMode.insert *" should not be logged diff --git a/tests/end2end/features/yankpaste.feature b/tests/end2end/features/yankpaste.feature index 9cf917c2c..ba5b56ca7 100644 --- a/tests/end2end/features/yankpaste.feature +++ b/tests/end2end/features/yankpaste.feature @@ -291,7 +291,7 @@ Feature: Yanking and pasting. # Compare Then the javascript message "textarea contents: onHello worlde two three four" should be logged - @qtwebengine_osx_xfail + @qtwebengine_mac_xfail Scenario: Inserting text into a text field with undo When I set general -> log-javascript-console to info And I open data/paste_primary.html diff --git a/tests/end2end/features/zoom.feature b/tests/end2end/features/zoom.feature index 015b85b17..bc36afe0d 100644 --- a/tests/end2end/features/zoom.feature +++ b/tests/end2end/features/zoom.feature @@ -51,6 +51,11 @@ Feature: Zooming in and out Then the message "Zoom level: 50%" should be shown And the zoom should be 50% + Scenario: Setting zoom with trailing % + When I run :zoom 50% + Then the message "Zoom level: 50%" should be shown + And the zoom should be 50% + Scenario: Setting zoom with count When I run :zoom with count 40 Then the message "Zoom level: 40%" should be shown diff --git a/tests/end2end/fixtures/quteprocess.py b/tests/end2end/fixtures/quteprocess.py index 15157cc78..9c4f0fea8 100644 --- a/tests/end2end/fixtures/quteprocess.py +++ b/tests/end2end/fixtures/quteprocess.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -55,33 +55,83 @@ def is_ignored_qt_message(message): def is_ignored_lowlevel_message(message): """Check if we want to ignore a lowlevel process output.""" - if 'Running without the SUID sandbox!' in message: - return True - elif message.startswith('Xlib: sequence lost'): + if message.startswith('Xlib: sequence lost'): # 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 - elif 'Unable to locate theme engine in module_path:' in message: - return True elif message == 'getrlimit(RLIMIT_NOFILE) failed': return True return False +def is_ignored_chromium_message(line): + msg_re = re.compile(r""" + \[ + (\d+:\d+:)? # Process/Thread ID + \d{4}/[\d.]+: # MMDD/Time + (?P[A-Z]+): # Log level + [^ :]+ # filename / line + \] + \ (?P.*) # message + """, re.VERBOSE) + match = msg_re.fullmatch(line) + if match is None: + return False + + if match.group('loglevel') == 'INFO': + return True + + message = match.group('message') + ignored_messages = [ + # [27289:27289:0605/195958.776146:INFO:zygote_host_impl_linux.cc(107)] + # No usable sandbox! Update your kernel or see + # https://chromium.googlesource.com/chromium/src/+/master/docs/linux_suid_sandbox_development.md + # for more information on developing with the SUID sandbox. If you want + # to live dangerously and need an immediate workaround, you can try + # using --no-sandbox. + 'No usable sandbox! Update your kernel or see *', + # [30981:30992:0605/200633.041364:ERROR:cert_verify_proc_nss.cc(918)] + # CERT_PKIXVerifyCert for localhost failed err=-8179 + 'CERT_PKIXVerifyCert for localhost failed err=*', + + # Not reproducible anymore? + + 'Running without the SUID sandbox! *', + 'Unable to locate theme engine in module_path: *', + 'Could not bind NETLINK socket: Address already in use', + # Started appearing in sessions.feature with Qt 5.8... + 'Invalid node channel message *', + # Makes tests fail on Quantumcross' machine + ('CreatePlatformSocket() returned an error, errno=97: Address family' + 'not supported by protocol'), + + # Qt 5.9 with debug Chromium + + # [28121:28121:0605/191637.407848:WARNING:resource_bundle_qt.cpp(114)] + # locale_file_path.empty() for locale + 'locale_file_path.empty() for locale', + # [26598:26598:0605/191429.639416:WARNING:audio_manager.cc(317)] + # Multiple instances of AudioManager detected + 'Multiple instances of AudioManager detected', + # [25775:25788:0605/191240.931551:ERROR:quarantine_linux.cc(33)] + # Could not set extended attribute user.xdg.origin.url on file + # /tmp/pytest-of-florian/pytest-32/test_webengine_download_suffix0/ + # downloads/download.bin: Operation not supported + ('Could not set extended attribute user.xdg.* on file *: ' + 'Operation not supported'), + # [5947:5947:0605/192837.856931:ERROR:render_process_impl.cc(112)] + # WebFrame LEAKED 1 TIMES + 'WebFrame LEAKED 1 TIMES', + ] + return any(testutils.pattern_match(pattern=pattern, value=message) + for pattern in ignored_messages) + + class LogLine(testprocess.Line): """A parsed line from the qutebrowser log output. @@ -260,6 +310,7 @@ class QuteProc(testprocess.Process): return None elif (is_ignored_qt_message(line) or is_ignored_lowlevel_message(line) or + is_ignored_chromium_message(line) or self.request.node.get_marker('no_invalid_lines')): self._log("IGNORED: {}".format(line)) return None @@ -312,7 +363,8 @@ class QuteProc(testprocess.Process): URLs like about:... and qute:... are handled specially and returned verbatim. """ - special_schemes = ['about:', 'qute:', 'chrome:'] + special_schemes = ['about:', 'qute:', 'chrome:', 'view-source:', + 'data:'] if any(path.startswith(scheme) for scheme in special_schemes): return path else: @@ -354,7 +406,8 @@ class QuteProc(testprocess.Process): self.wait_for(category='webview', message='Scroll position changed to ' + point) - def wait_for(self, timeout=None, **kwargs): + def wait_for(self, timeout=None, # pylint: disable=arguments-differ + **kwargs): """Extend wait_for to add divisor if a test is xfailing.""" __tracebackhide__ = (lambda e: e.errisinstance(testprocess.WaitForTimeout)) @@ -377,7 +430,8 @@ class QuteProc(testprocess.Process): pattern="load status for <* tab_id=* url='*duckduckgo*'>: *", value=msg.message) - is_log_error = msg.loglevel > logging.INFO + is_log_error = (msg.loglevel > logging.INFO and + not msg.message.startswith("Ignoring world ID")) return is_log_error or is_js_error or is_ddg_load def _maybe_skip(self): @@ -494,18 +548,20 @@ class QuteProc(testprocess.Process): self.set_setting(sect, opt, old_value) def open_path(self, path, *, new_tab=False, new_bg_tab=False, - new_window=False, as_url=False, port=None, https=False, - wait=True): + new_window=False, private=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_bg_tab=new_bg_tab, - new_window=new_window, as_url=as_url, wait=wait) + new_window=new_window, private=private, as_url=as_url, + wait=wait) def open_url(self, url, *, new_tab=False, new_bg_tab=False, - new_window=False, as_url=False, wait=True): + new_window=False, private=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!") + if sum(1 for opt in [new_tab, new_bg_tab, new_window, private, as_url] + if opt) > 1: + raise ValueError("Conflicting options given!") if as_url: self.send_cmd(url, invalid=True) @@ -515,6 +571,8 @@ class QuteProc(testprocess.Process): self.send_cmd(':open -b ' + url) elif new_window: self.send_cmd(':open -w ' + url) + elif private: + self.send_cmd(':open -p ' + url) else: self.send_cmd(':open ' + url) @@ -573,7 +631,7 @@ class QuteProc(testprocess.Process): """Save the session and get the parsed session data.""" with tempfile.TemporaryDirectory() as tmpdir: session = os.path.join(tmpdir, 'session.yml') - self.send_cmd(':session-save "{}"'.format(session)) + self.send_cmd(':session-save --with-private "{}"'.format(session)) self.wait_for(category='message', loglevel=logging.INFO, message='Saved session {}.'.format(session)) with open(session, encoding='utf-8') as f: diff --git a/tests/end2end/fixtures/test_quteprocess.py b/tests/end2end/fixtures/test_quteprocess.py index 748310769..96ae1f47b 100644 --- a/tests/end2end/fixtures/test_quteprocess.py +++ b/tests/end2end/fixtures/test_quteprocess.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -113,7 +113,7 @@ def test_quteproc_error_message_did_fail(qtbot, quteproc, request_mock): def test_quteproc_skip_via_js(qtbot, quteproc): - with pytest.raises(pytest.skip.Exception) as excinfo: + with pytest.raises(pytest.skip.Exception, match='test'): quteproc.send_cmd(':jseval console.log("[SKIP] test");') quteproc.wait_for_js('[SKIP] test') @@ -121,8 +121,6 @@ def test_quteproc_skip_via_js(qtbot, quteproc): # the error to occur during the test rather than at teardown time. quteproc.after_test() - assert str(excinfo.value) == 'test' - def test_quteproc_skip_and_wait_for(qtbot, quteproc): """This test will skip *again* during teardown, but we don't care.""" @@ -147,8 +145,7 @@ def test_quteprocess_quitting(qtbot, quteproc_process): @pytest.mark.parametrize('data, attrs', [ - ( - # Normal message + pytest.param( '{"created": 86400, "msecs": 0, "levelname": "DEBUG", "name": "init", ' '"module": "earlyinit", "funcName": "init_log", "lineno": 280, ' '"levelno": 10, "message": "Log initialized."}', @@ -161,31 +158,31 @@ def test_quteprocess_quitting(qtbot, quteproc_process): 'line': 280, 'message': 'Log initialized.', 'expected': False, - } - ), - ( - # VDEBUG + }, + id='normal'), + + pytest.param( '{"created": 86400, "msecs": 0, "levelname": "VDEBUG", "name": "foo", ' '"module": "foo", "funcName": "foo", "lineno": 0, "levelno": 9, ' '"message": ""}', - {'loglevel': log.VDEBUG_LEVEL} - ), - ( - # Unknown module + {'loglevel': log.VDEBUG_LEVEL}, + id='vdebug'), + + pytest.param( '{"created": 86400, "msecs": 0, "levelname": "DEBUG", "name": "qt", ' '"module": null, "funcName": null, "lineno": 0, "levelno": 10, ' '"message": "test"}', {'module': None, 'function': None, 'line': None}, - ), - ( - # Expected message + id='unknown module'), + + pytest.param( '{"created": 86400, "msecs": 0, "levelname": "VDEBUG", "name": "foo", ' '"module": "foo", "funcName": "foo", "lineno": 0, "levelno": 9, ' '"message": "SpellCheck: test"}', {'expected': True}, - ), - ( - # Weird Qt location + id='expected message'), + + pytest.param( '{"created": 86400, "msecs": 0, "levelname": "DEBUG", "name": "qt", ' '"module": "qnetworkreplyhttpimpl", "funcName": ' '"void QNetworkReplyHttpImplPrivate::error(' @@ -197,9 +194,10 @@ def test_quteprocess_quitting(qtbot, quteproc_process): 'function': 'void QNetworkReplyHttpImplPrivate::error(' 'QNetworkReply::NetworkError, const QString&)', 'line': 1929 - } - ), - ( + }, + id='weird Qt location'), + + pytest.param( '{"created": 86400, "msecs": 0, "levelname": "DEBUG", "name": "qt", ' '"module": "qxcbxsettings", "funcName": "QXcbXSettings::QXcbXSettings(' 'QXcbScreen*)", "lineno": 233, "levelno": 10, "message": ' @@ -209,19 +207,18 @@ def test_quteprocess_quitting(qtbot, quteproc_process): 'module': 'qxcbxsettings', 'function': 'QXcbXSettings::QXcbXSettings(QXcbScreen*)', 'line': 233, - } - ), - ( - # ResourceWarning + }, + id='QXcbXSettings'), + + pytest.param( '{"created": 86400, "msecs": 0, "levelname": "WARNING", ' '"name": "py.warnings", "module": "app", "funcName": "qt_mainloop", ' '"lineno": 121, "levelno": 30, "message": ' '".../app.py:121: ResourceWarning: unclosed file <_io.TextIOWrapper ' 'name=18 mode=\'r\' encoding=\'UTF-8\'>"}', - {'category': 'py.warnings'} - ), -], ids=['normal', 'vdebug', 'unknown module', 'expected message', - 'weird Qt location', 'QXcbXSettings', 'resourcewarning']) + {'category': 'py.warnings'}, + id='resourcewarning'), +]) def test_log_line_parse(data, attrs): line = quteprocess.LogLine(data) for name, expected in attrs.items(): @@ -230,15 +227,15 @@ def test_log_line_parse(data, attrs): @pytest.mark.parametrize('data, colorized, expect_error, expected', [ - ( + pytest.param( {'created': 86400, 'msecs': 0, 'levelname': 'DEBUG', 'name': 'foo', 'module': 'bar', 'funcName': 'qux', 'lineno': 10, 'levelno': 10, 'message': 'quux'}, False, False, '{timestamp} DEBUG foo bar:qux:10 quux', - ), - # Traceback attached - ( + id='normal'), + + pytest.param( {'created': 86400, 'msecs': 0, 'levelname': 'DEBUG', 'name': 'foo', 'module': 'bar', 'funcName': 'qux', 'lineno': 10, 'levelno': 10, 'message': 'quux', 'traceback': 'Traceback (most recent call ' @@ -247,43 +244,42 @@ def test_log_line_parse(data, attrs): '{timestamp} DEBUG foo bar:qux:10 quux\n' 'Traceback (most recent call last):\n' ' here be dragons', - ), - # Colorized - ( + id='traceback'), + + pytest.param( {'created': 86400, 'msecs': 0, 'levelname': 'DEBUG', 'name': 'foo', 'module': 'bar', 'funcName': 'qux', 'lineno': 10, 'levelno': 10, 'message': 'quux'}, True, False, '\033[32m{timestamp}\033[0m \033[37mDEBUG \033[0m \033[36mfoo ' ' bar:qux:10\033[0m \033[37mquux\033[0m', - ), - # Expected error - ( + id='colored'), + + pytest.param( {'created': 86400, 'msecs': 0, 'levelname': 'ERROR', 'name': 'foo', 'module': 'bar', 'funcName': 'qux', 'lineno': 10, 'levelno': 40, 'message': 'quux'}, False, True, '{timestamp} ERROR (expected) foo bar:qux:10 quux', - ), - # Expected other message (i.e. should make no difference) - ( + id='expected error'), + + pytest.param( {'created': 86400, 'msecs': 0, 'levelname': 'DEBUG', 'name': 'foo', 'module': 'bar', 'funcName': 'qux', 'lineno': 10, 'levelno': 10, 'message': 'quux'}, False, True, '{timestamp} DEBUG foo bar:qux:10 quux', - ), - # Expected error colorized (shouldn't be red) - ( + id='expected other'), + + pytest.param( {'created': 86400, 'msecs': 0, 'levelname': 'ERROR', 'name': 'foo', 'module': 'bar', 'funcName': 'qux', 'lineno': 10, 'levelno': 40, 'message': 'quux'}, True, True, '\033[32m{timestamp}\033[0m \033[37mERROR (expected)\033[0m ' '\033[36mfoo bar:qux:10\033[0m \033[37mquux\033[0m', - ), -], ids=['normal', 'traceback', 'colored', 'expected error', 'expected other', - 'expected error colorized']) + id='expected error colorized'), +]) def test_log_line_formatted(data, colorized, expect_error, expected): line = json.dumps(data) record = quteprocess.LogLine(line) @@ -314,14 +310,12 @@ class TestClickElementByText: quteproc.wait_for_js('click_element special chars') def test_duplicate(self, quteproc): - with pytest.raises(ValueError) as excinfo: + with pytest.raises(ValueError, match='not unique'): quteproc.click_element_by_text('Duplicate') - assert 'not unique' in str(excinfo.value) def test_nonexistent(self, quteproc): - with pytest.raises(ValueError) as excinfo: + with pytest.raises(ValueError, match='No element'): quteproc.click_element_by_text('no element exists with this text') - assert 'No element' in str(excinfo.value) @pytest.mark.parametrize('string, expected', [ @@ -343,3 +337,20 @@ def test_set(quteproc, value): quteproc.set_setting('general', 'default-encoding', value) read_back = quteproc.get_setting('general', 'default-encoding') assert read_back == value + + +@pytest.mark.parametrize('message, ignored', [ + # Unparseable + ('Hello World', False), + # Without process/thread ID + ('[0606/135039:ERROR:cert_verify_proc_nss.cc(925)] CERT_PKIXVerifyCert ' + 'for localhost failed err=-8179', True), + # Random ignored message + ('[26598:26598:0605/191429.639416:WARNING:audio_manager.cc(317)] Multiple ' + 'instances of AudioManager detected', True), + # Not ignored + ('[26598:26598:0605/191429.639416:WARNING:audio_manager.cc(317)] Test', + False), +]) +def test_is_ignored_chromium_message(message, ignored): + assert quteprocess.is_ignored_chromium_message(message) == ignored diff --git a/tests/end2end/fixtures/test_testprocess.py b/tests/end2end/fixtures/test_testprocess.py index ef5b445ca..b38e79f56 100644 --- a/tests/end2end/fixtures/test_testprocess.py +++ b/tests/end2end/fixtures/test_testprocess.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/fixtures/test_webserver.py b/tests/end2end/fixtures/test_webserver.py index 5f0fa8ab0..c885ea5b6 100644 --- a/tests/end2end/fixtures/test_webserver.py +++ b/tests/end2end/fixtures/test_webserver.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/fixtures/testprocess.py b/tests/end2end/fixtures/testprocess.py index c6f4edd22..1a2c51baf 100644 --- a/tests/end2end/fixtures/testprocess.py +++ b/tests/end2end/fixtures/testprocess.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -78,8 +78,8 @@ class Line: def _render_log(data, threshold=100): """Shorten the given log without -v and convert to a string.""" data = [str(d) for d in data] - is_exception = any('Traceback (most recent call last):' in line - for line in data) + is_exception = any('Traceback (most recent call last):' in line or + 'Uncaught exception' in line for line in data) verbose = pytest.config.getoption('--verbose') if len(data) > threshold and not verbose and not is_exception: msg = '[{} lines suppressed, use -v to show]'.format( @@ -103,8 +103,8 @@ def pytest_runtest_makereport(item, call): httpbin_log = getattr(item, '_httpbin_log', None) if not hasattr(report.longrepr, 'addsection'): - # In some conditions (on OS X and Windows it seems), report.longrepr is - # actually a tuple. This is handled similarily in pytest-qt too. + # In some conditions (on macOS and Windows it seems), report.longrepr + # is actually a tuple. This is handled similarily in pytest-qt too. return if pytest.config.getoption('--capture') == 'no': diff --git a/tests/end2end/fixtures/webserver.py b/tests/end2end/fixtures/webserver.py index c43765bf6..acb99acfe 100644 --- a/tests/end2end/fixtures/webserver.py +++ b/tests/end2end/fixtures/webserver.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -62,7 +62,7 @@ class Request(testprocess.Line): def _check_status(self): """Check if the http status is what we expected.""" # WORKAROUND for https://github.com/PyCQA/pylint/issues/399 (?) - # pylint: disable=no-member, useless-suppression + # pylint: disable=no-member path_to_statuses = { '/favicon.ico': [http.client.NOT_FOUND], '/does-not-exist': [http.client.NOT_FOUND], diff --git a/tests/end2end/fixtures/webserver_sub.py b/tests/end2end/fixtures/webserver_sub.py index 51a03e0b5..53c033f5b 100644 --- a/tests/end2end/fixtures/webserver_sub.py +++ b/tests/end2end/fixtures/webserver_sub.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/fixtures/webserver_sub_ssl.py b/tests/end2end/fixtures/webserver_sub_ssl.py index dadcb510e..d8a8f1025 100644 --- a/tests/end2end/fixtures/webserver_sub_ssl.py +++ b/tests/end2end/fixtures/webserver_sub_ssl.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/test_dirbrowser.py b/tests/end2end/test_dirbrowser.py index 2359020d7..0a2c3d7b9 100644 --- a/tests/end2end/test_dirbrowser.py +++ b/tests/end2end/test_dirbrowser.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Daniel Schadt +# Copyright 2015-2017 Daniel Schadt # # This file is part of qutebrowser. # @@ -20,10 +20,10 @@ """Test the built-in directory browser.""" import os -import bs4 import collections import pytest +import bs4 from PyQt5.QtCore import QUrl from qutebrowser.utils import urlutils diff --git a/tests/end2end/test_hints_html.py b/tests/end2end/test_hints_html.py index c2d5143bc..272fead29 100644 --- a/tests/end2end/test_hints_html.py +++ b/tests/end2end/test_hints_html.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -21,12 +21,12 @@ import os import os.path +import textwrap +import collections import yaml import pytest import bs4 -import textwrap -import collections def collect_tests(): diff --git a/tests/end2end/test_insert_mode.py b/tests/end2end/test_insert_mode.py index 8b9ed3aca..3bd5d87fd 100644 --- a/tests/end2end/test_insert_mode.py +++ b/tests/end2end/test_insert_mode.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/test_invocations.py b/tests/end2end/test_invocations.py index d6697ec29..d1f4d0e23 100644 --- a/tests/end2end/test_invocations.py +++ b/tests/end2end/test_invocations.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -19,6 +19,7 @@ """Test starting qutebrowser with special arguments/environments.""" +import subprocess import socket import sys import logging @@ -28,7 +29,6 @@ import pytest from PyQt5.QtCore import QProcess -from end2end.fixtures import quteprocess, testprocess from qutebrowser.utils import qtutils @@ -59,7 +59,8 @@ def temp_basedir_env(tmpdir, short_tmpdir): runtime_dir.chmod(0o700) (data_dir / 'qutebrowser' / 'state').write_text( - '[general]\nquickstart-done = 1', encoding='utf-8', ensure=True) + '[general]\nquickstart-done = 1\nbackend-warning-shown=1', + encoding='utf-8', ensure=True) env = { 'XDG_DATA_HOME': str(data_dir), @@ -138,10 +139,10 @@ def test_misconfigured_user_dirs(request, httpbin, temp_basedir_env, def test_no_loglines(request, quteproc_new): - """Test qute:log with --loglines=0.""" + """Test qute://log with --loglines=0.""" quteproc_new.start(args=['--temp-basedir', '--loglines=0'] + _base_args(request.config)) - quteproc_new.open_path('qute:log') + quteproc_new.open_path('qute://log') assert quteproc_new.get_content() == 'Log output was disabled.' @@ -161,25 +162,29 @@ def test_optimize(request, quteproc_new, capfd, level): quteproc_new.wait_for_quit() +@pytest.mark.not_frozen +@pytest.mark.flaky # Fails sometimes with empty output... def test_version(request): """Test invocation with --version argument.""" - args = ['--version'] + _base_args(request.config) + args = ['-m', 'qutebrowser', '--version'] + _base_args(request.config) # can't use quteproc_new here because it's confused by # early process termination - proc = quteprocess.QuteProc(request) - proc.proc.setProcessChannelMode(QProcess.SeparateChannels) + proc = QProcess() + proc.setProcessChannelMode(QProcess.SeparateChannels) - try: - proc.start(args) - proc.wait_for_quit() - except testprocess.ProcessExited: - assert proc.proc.exitStatus() == QProcess.NormalExit - else: - pytest.fail("Process did not exit!") + proc.start(sys.executable, args) + ok = proc.waitForStarted(2000) + assert ok + ok = proc.waitForFinished(2000) + assert ok + assert proc.exitStatus() == QProcess.NormalExit - output = bytes(proc.proc.readAllStandardOutput()).decode('utf-8') + stdout = bytes(proc.readAllStandardOutput()).decode('utf-8') + print(stdout) + stderr = bytes(proc.readAllStandardError()).decode('utf-8') + print(stderr) - assert re.search(r'^qutebrowser\s+v\d+(\.\d+)', output) is not None + assert re.search(r'^qutebrowser\s+v\d+(\.\d+)', stdout) is not None @pytest.mark.skipif(not qtutils.version_check('5.3'), @@ -254,3 +259,52 @@ def test_command_on_start(request, quteproc_new): quteproc_new.start(args) quteproc_new.send_cmd(':quit') quteproc_new.wait_for_quit() + + +def test_launching_with_python2(): + try: + proc = subprocess.Popen(['python2', '-m', 'qutebrowser', + '--no-err-windows'], stderr=subprocess.PIPE) + except FileNotFoundError: + pytest.skip("python2 not found") + _stdout, stderr = proc.communicate() + assert proc.returncode == 1 + error = "At least Python 3.4 is required to run qutebrowser" + assert stderr.decode('ascii').startswith(error) + + +def test_initial_private_browsing(request, quteproc_new): + """Make sure the initial window is private when the setting is set.""" + args = (_base_args(request.config) + + ['--temp-basedir', '-s', 'general', 'private-browsing', 'true']) + quteproc_new.start(args) + + quteproc_new.compare_session(""" + windows: + - private: True + tabs: + - history: + - url: about:blank + """) + + quteproc_new.send_cmd(':quit') + quteproc_new.wait_for_quit() + + +def test_loading_empty_session(tmpdir, request, quteproc_new): + """Make sure loading an empty session opens a window.""" + session = tmpdir / 'session.yml' + session.write('windows: []') + + args = _base_args(request.config) + ['--temp-basedir', '-r', str(session)] + quteproc_new.start(args) + + quteproc_new.compare_session(""" + windows: + - tabs: + - history: + - url: about:blank + """) + + 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 46ece8f0e..f753e6fd4 100644 --- a/tests/end2end/test_mhtml_e2e.py +++ b/tests/end2end/test_mhtml_e2e.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/helpers/fixtures.py b/tests/helpers/fixtures.py index f9e829e40..74c63f251 100644 --- a/tests/helpers/fixtures.py +++ b/tests/helpers/fixtures.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -41,7 +41,7 @@ import helpers.stubs as stubsmod from qutebrowser.config import config from qutebrowser.utils import objreg, standarddir from qutebrowser.browser.webkit import cookies -from qutebrowser.misc import savemanager +from qutebrowser.misc import savemanager, sql from qutebrowser.keyinput import modeman from PyQt5.QtCore import PYQT_VERSION, pyqtSignal, QEvent, QSize, Qt, QObject @@ -257,18 +257,9 @@ def bookmark_manager_stub(stubs): objreg.delete('bookmark-manager') -@pytest.fixture -def web_history_stub(stubs): - """Fixture which provides a fake web-history object.""" - stub = stubs.WebHistoryStub() - objreg.register('web-history', stub) - yield stub - objreg.delete('web-history') - - @pytest.fixture def session_manager_stub(stubs): - """Fixture which provides a fake web-history object.""" + """Fixture which provides a fake session-manager object.""" stub = stubs.SessionManagerStub() objreg.register('session-manager', stub) yield stub @@ -482,3 +473,37 @@ def short_tmpdir(): """A short temporary directory for a XDG_RUNTIME_DIR.""" with tempfile.TemporaryDirectory() as tdir: yield py.path.local(tdir) # pylint: disable=no-member + + +@pytest.fixture +def init_sql(data_tmpdir): + """Initialize the SQL module, and shut it down after the test.""" + path = str(data_tmpdir / 'test.db') + sql.init(path) + yield + sql.close() + + +class ModelValidator: + + """Validates completion models.""" + + def __init__(self, modeltester): + modeltester.data_display_may_return_none = True + self._model = None + self._modeltester = modeltester + + def set_model(self, model): + self._model = model + self._modeltester.check(model) + + def validate(self, expected): + assert self._model.rowCount() == len(expected) + for row, items in enumerate(expected): + for col, item in enumerate(items): + assert self._model.data(self._model.index(row, col)) == item + + +@pytest.fixture +def model_validator(qtmodeltester): + return ModelValidator(qtmodeltester) diff --git a/tests/helpers/logfail.py b/tests/helpers/logfail.py index 2967e3ccf..3d8e3afb8 100644 --- a/tests/helpers/logfail.py +++ b/tests/helpers/logfail.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/helpers/messagemock.py b/tests/helpers/messagemock.py index 7854aabcc..77116115f 100644 --- a/tests/helpers/messagemock.py +++ b/tests/helpers/messagemock.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/helpers/stubs.py b/tests/helpers/stubs.py index dfbcc550d..c0b1e0abe 100644 --- a/tests/helpers/stubs.py +++ b/tests/helpers/stubs.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -29,7 +29,7 @@ from PyQt5.QtNetwork import (QNetworkRequest, QAbstractNetworkCache, QNetworkCacheMetaData) from PyQt5.QtWidgets import QCommonStyle, QLineEdit, QWidget, QTabBar -from qutebrowser.browser import browsertab, history +from qutebrowser.browser import browsertab from qutebrowser.config import configexc from qutebrowser.utils import usertypes, utils from qutebrowser.mainwindow import mainwindow @@ -222,6 +222,24 @@ class FakeWebTabScroller(browsertab.AbstractScroller): return self._pos_perc +class FakeWebTabHistory(browsertab.AbstractHistory): + + """Fake for Web{Kit,Engine}History.""" + + def __init__(self, tab, *, can_go_back, can_go_forward): + super().__init__(tab) + self._can_go_back = can_go_back + self._can_go_forward = can_go_forward + + def can_go_back(self): + assert self._can_go_back is not None + return self._can_go_back + + def can_go_forward(self): + assert self._can_go_forward is not None + return self._can_go_forward + + class FakeWebTab(browsertab.AbstractTab): """Fake AbstractTab to use in tests.""" @@ -229,12 +247,14 @@ class FakeWebTab(browsertab.AbstractTab): def __init__(self, url=FakeUrl(), title='', tab_id=0, *, scroll_pos_perc=(0, 0), load_status=usertypes.LoadStatus.success, - progress=0): - super().__init__(win_id=0, mode_manager=None) + progress=0, can_go_back=None, can_go_forward=None): + super().__init__(win_id=0, mode_manager=None, private=False) self._load_status = load_status self._title = title self._url = url self._progress = progress + self.history = FakeWebTabHistory(self, can_go_back=can_go_back, + can_go_forward=can_go_forward) self.scroller = FakeWebTabScroller(self, scroll_pos_perc) wrapped = QWidget() self._layout.wrap(self, wrapped) @@ -376,9 +396,6 @@ class InstaTimer(QObject): timeout = pyqtSignal() - def __init__(self, parent=None): - super().__init__(parent) - def start(self): self.timeout.emit() @@ -388,6 +405,10 @@ class InstaTimer(QObject): def setInterval(self, interval): pass + @staticmethod + def singleShot(_interval, fun): + fun() + class FakeConfigType: @@ -410,9 +431,6 @@ class StatusBarCommandStub(QLineEdit): show_cmd = pyqtSignal() hide_cmd = pyqtSignal() - def __init__(self, parent=None): - super().__init__(parent) - def prefix(self): return self.text()[0] @@ -524,24 +542,6 @@ class QuickmarkManagerStub(UrlMarkManagerStub): self.delete(key) -class WebHistoryStub(QObject): - - """Stub for the web-history object.""" - - add_completion_item = pyqtSignal(history.Entry) - cleared = pyqtSignal() - - def __init__(self, parent=None): - super().__init__(parent) - self.history_dict = collections.OrderedDict() - - def __iter__(self): - return iter(self.history_dict.values()) - - def __len__(self): - return len(self.history_dict) - - class HostBlockerStub: """Stub for the host-blocker object.""" @@ -572,6 +572,9 @@ class TabbedBrowserStub(QObject): self.tabs = [] self.shutting_down = False self._qtabbar = QTabBar() + self.index_of = None + self.current_index = None + self.opened_url = None def count(self): return len(self.tabs) @@ -588,12 +591,32 @@ class TabbedBrowserStub(QObject): def tabBar(self): return self._qtabbar + def indexOf(self, _tab): + if self.index_of is None: + raise ValueError("indexOf got called with index_of None!") + elif self.index_of is RuntimeError: + raise RuntimeError + else: + return self.index_of + + def currentIndex(self): + if self.current_index is None: + raise ValueError("currentIndex got called with current_index " + "None!") + return self.current_index + + def currentWidget(self): + idx = self.currentIndex() + if idx == -1: + return None + return self.tabs[idx - 1] + + def tabopen(self, url): + self.opened_url = url + class ApplicationStub(QObject): """Stub to insert as the app object in objreg.""" new_window = pyqtSignal(mainwindow.MainWindow) - - def __init__(self): - super().__init__() diff --git a/tests/helpers/test_helper_utils.py b/tests/helpers/test_helper_utils.py index 65c7c29a1..d7eadb894 100644 --- a/tests/helpers/test_helper_utils.py +++ b/tests/helpers/test_helper_utils.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/helpers/test_logfail.py b/tests/helpers/test_logfail.py index 6bc4f364c..b95dec1d6 100644 --- a/tests/helpers/test_logfail.py +++ b/tests/helpers/test_logfail.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/helpers/test_stubs.py b/tests/helpers/test_stubs.py index d542afe54..10fa9e5db 100644 --- a/tests/helpers/test_stubs.py +++ b/tests/helpers/test_stubs.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/helpers/utils.py b/tests/helpers/utils.py index faafefa82..7dbd7dd25 100644 --- a/tests/helpers/utils.py +++ b/tests/helpers/utils.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -17,8 +17,6 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -# pylint: disable=unused-variable - """Partial comparison of dicts/lists.""" @@ -28,6 +26,14 @@ import os.path import pytest +from qutebrowser.utils import qtutils + + +qt58 = pytest.mark.skipif( + qtutils.version_check('5.9'), reason="Needs Qt 5.8 or earlier") +qt59 = pytest.mark.skipif( + not qtutils.version_check('5.9'), reason="Needs Qt 5.9 or newer") + class PartialCompareOutcome: diff --git a/tests/test_conftest.py b/tests/test_conftest.py index 24fb67097..74db4bf78 100644 --- a/tests/test_conftest.py +++ b/tests/test_conftest.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/browser/test_commands.py b/tests/unit/browser/test_commands.py deleted file mode 100644 index e7654a2d7..000000000 --- a/tests/unit/browser/test_commands.py +++ /dev/null @@ -1,53 +0,0 @@ -# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: - -# Copyright 2015-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 collections - -import pytest - -from qutebrowser.browser import commands -from qutebrowser.mainwindow import tabbedbrowser -from qutebrowser.utils import objreg -from qutebrowser.keyinput import modeman - - -ObjectsRet = collections.namedtuple('Dispatcher', ['tb', 'cd']) - -pytestmark = pytest.mark.usefixtures('cookiejar_and_cache') - - -@pytest.fixture -def objects(qtbot, default_config, key_config_stub, tab_registry, - host_blocker_stub): - """Fixture providing a CommandDispatcher and a fake TabbedBrowser.""" - win_id = 0 - modeman.init(win_id, parent=None) - tabbed_browser = tabbedbrowser.TabbedBrowser(win_id) - qtbot.add_widget(tabbed_browser) - objreg.register('tabbed-browser', tabbed_browser, scope='window', - window=win_id) - dispatcher = commands.CommandDispatcher(win_id, tabbed_browser) - objreg.register('command-dispatcher', dispatcher, scope='window', - window=win_id) - yield ObjectsRet(tabbed_browser, dispatcher) - - -@pytest.mark.skipif(True, reason="Work in progress") -def test_openurl(objects): - objects.cd.openurl('localhost') diff --git a/tests/unit/browser/test_history.py b/tests/unit/browser/test_history.py new file mode 100644 index 000000000..81637f3d4 --- /dev/null +++ b/tests/unit/browser/test_history.py @@ -0,0 +1,349 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2016-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 . + +"""Tests for the global page history.""" + +import logging + +import pytest +from PyQt5.QtCore import QUrl + +from qutebrowser.browser import history +from qutebrowser.utils import objreg, urlutils, usertypes +from qutebrowser.commands import cmdexc + + +@pytest.fixture(autouse=True) +def prerequisites(config_stub, fake_save_manager, init_sql): + """Make sure everything is ready to initialize a WebHistory.""" + config_stub.data = {'general': {'private-browsing': False}} + + +@pytest.fixture() +def hist(tmpdir): + return history.WebHistory() + + +@pytest.fixture() +def mock_time(mocker): + m = mocker.patch('qutebrowser.browser.history.time') + m.time.return_value = 12345 + return 12345 + + +def test_iter(hist): + urlstr = 'http://www.example.com/' + url = QUrl(urlstr) + hist.add_url(url, atime=12345) + + assert list(hist) == [(urlstr, '', 12345, False)] + + +def test_len(hist): + assert len(hist) == 0 + + url = QUrl('http://www.example.com/') + hist.add_url(url) + + assert len(hist) == 1 + + +def test_contains(hist): + hist.add_url(QUrl('http://www.example.com/'), title='Title', atime=12345) + assert 'http://www.example.com/' in hist + assert 'www.example.com' not in hist + assert 'Title' not in hist + assert 12345 not in hist + + +def test_get_recent(hist): + hist.add_url(QUrl('http://www.qutebrowser.org/'), atime=67890) + hist.add_url(QUrl('http://example.com/'), atime=12345) + assert list(hist.get_recent()) == [ + ('http://www.qutebrowser.org/', '', 67890, False), + ('http://example.com/', '', 12345, False), + ] + + +def test_entries_between(hist): + hist.add_url(QUrl('http://www.example.com/1'), atime=12345) + hist.add_url(QUrl('http://www.example.com/2'), atime=12346) + hist.add_url(QUrl('http://www.example.com/3'), atime=12347) + hist.add_url(QUrl('http://www.example.com/4'), atime=12348) + hist.add_url(QUrl('http://www.example.com/5'), atime=12348) + hist.add_url(QUrl('http://www.example.com/6'), atime=12349) + hist.add_url(QUrl('http://www.example.com/7'), atime=12350) + + times = [x.atime for x in hist.entries_between(12346, 12349)] + assert times == [12349, 12348, 12348, 12347] + + +def test_entries_before(hist): + hist.add_url(QUrl('http://www.example.com/1'), atime=12346) + hist.add_url(QUrl('http://www.example.com/2'), atime=12346) + hist.add_url(QUrl('http://www.example.com/3'), atime=12347) + hist.add_url(QUrl('http://www.example.com/4'), atime=12348) + hist.add_url(QUrl('http://www.example.com/5'), atime=12348) + hist.add_url(QUrl('http://www.example.com/6'), atime=12348) + hist.add_url(QUrl('http://www.example.com/7'), atime=12349) + hist.add_url(QUrl('http://www.example.com/8'), atime=12349) + + times = [x.atime for x in hist.entries_before(12348, limit=3, offset=2)] + assert times == [12348, 12347, 12346] + + +def test_clear(qtbot, tmpdir, hist, mocker): + hist.add_url(QUrl('http://example.com/')) + hist.add_url(QUrl('http://www.qutebrowser.org/')) + + m = mocker.patch('qutebrowser.browser.history.message.confirm_async', + new=mocker.Mock, spec=[]) + hist.clear() + assert m.called + + +def test_clear_force(qtbot, tmpdir, hist): + hist.add_url(QUrl('http://example.com/')) + hist.add_url(QUrl('http://www.qutebrowser.org/')) + hist.clear(force=True) + assert not len(hist) + assert not len(hist.completion) + + +def test_delete_url(hist): + hist.add_url(QUrl('http://example.com/'), atime=0) + hist.add_url(QUrl('http://example.com/1'), atime=0) + hist.add_url(QUrl('http://example.com/2'), atime=0) + + before = set(hist) + completion_before = set(hist.completion) + + hist.delete_url(QUrl('http://example.com/1')) + + diff = before.difference(set(hist)) + assert diff == {('http://example.com/1', '', 0, False)} + + completion_diff = completion_before.difference(set(hist.completion)) + assert completion_diff == {('http://example.com/1', '', 0)} + + +@pytest.mark.parametrize('url, atime, title, redirect, expected_url', [ + ('http://www.example.com', 12346, 'the title', False, + 'http://www.example.com'), + ('http://www.example.com', 12346, 'the title', True, + 'http://www.example.com'), + ('http://www.example.com/spa ce', 12346, 'the title', False, + 'http://www.example.com/spa%20ce'), + ('https://user:pass@example.com', 12346, 'the title', False, + 'https://user@example.com'), +]) +def test_add_url(qtbot, hist, url, atime, title, redirect, expected_url): + hist.add_url(QUrl(url), atime=atime, title=title, redirect=redirect) + assert list(hist) == [(expected_url, title, atime, redirect)] + if redirect: + assert not len(hist.completion) + else: + assert list(hist.completion) == [(expected_url, title, atime)] + + +def test_add_url_invalid(qtbot, hist, caplog): + with caplog.at_level(logging.WARNING): + hist.add_url(QUrl()) + assert not list(hist) + assert not list(hist.completion) + + +@pytest.mark.parametrize('level, url, req_url, expected', [ + (logging.DEBUG, 'a.com', 'a.com', [('a.com', 'title', 12345, False)]), + (logging.DEBUG, 'a.com', 'b.com', [('a.com', 'title', 12345, False), + ('b.com', 'title', 12345, True)]), + (logging.WARNING, 'a.com', '', [('a.com', 'title', 12345, False)]), + (logging.WARNING, '', '', []), + (logging.WARNING, 'data:foo', '', []), + (logging.WARNING, 'a.com', 'data:foo', []), +]) +def test_add_from_tab(hist, level, url, req_url, expected, mock_time, caplog): + with caplog.at_level(level): + hist.add_from_tab(QUrl(url), QUrl(req_url), 'title') + assert set(hist) == set(expected) + + +@pytest.fixture +def hist_interface(hist): + # pylint: disable=invalid-name + QtWebKit = pytest.importorskip('PyQt5.QtWebKit') + from qutebrowser.browser.webkit import webkithistory + QWebHistoryInterface = QtWebKit.QWebHistoryInterface + # pylint: enable=invalid-name + hist.add_url(url=QUrl('http://www.example.com/'), title='example') + interface = webkithistory.WebHistoryInterface(hist) + QWebHistoryInterface.setDefaultInterface(interface) + yield + QWebHistoryInterface.setDefaultInterface(None) + + +def test_history_interface(qtbot, webview, hist_interface): + html = b"foo" + url = urlutils.data_url('text/html', html) + with qtbot.waitSignal(webview.loadFinished): + webview.load(url) + + +@pytest.fixture +def cleanup_init(): + # prevent test_init from leaking state + yield + hist = objreg.get('web-history', None) + if hist is not None: + hist.setParent(None) + objreg.delete('web-history') + try: + from PyQt5.QtWebKit import QWebHistoryInterface + QWebHistoryInterface.setDefaultInterface(None) + except ImportError: + pass + + +@pytest.mark.parametrize('backend', [usertypes.Backend.QtWebEngine, + usertypes.Backend.QtWebKit]) +def test_init(backend, qapp, tmpdir, monkeypatch, cleanup_init): + if backend == usertypes.Backend.QtWebKit: + pytest.importorskip('PyQt5.QtWebKitWidgets') + else: + assert backend == usertypes.Backend.QtWebEngine + + monkeypatch.setattr(history.objects, 'backend', backend) + history.init(qapp) + hist = objreg.get('web-history') + assert hist.parent() is qapp + + try: + from PyQt5.QtWebKit import QWebHistoryInterface + except ImportError: + QWebHistoryInterface = None + + if backend == usertypes.Backend.QtWebKit: + default_interface = QWebHistoryInterface.defaultInterface() + assert default_interface._history is hist + else: + assert backend == usertypes.Backend.QtWebEngine + if QWebHistoryInterface is None: + default_interface = None + else: + default_interface = QWebHistoryInterface.defaultInterface() + # For this to work, nothing can ever have called setDefaultInterface + # before (so we need to test webengine before webkit) + assert default_interface is None + + +def test_import_txt(hist, data_tmpdir, monkeypatch, stubs): + monkeypatch.setattr(history, 'QTimer', stubs.InstaTimer) + histfile = data_tmpdir / 'history' + # empty line is deliberate, to test skipping empty lines + histfile.write('''12345 http://example.com/ title + 12346 http://qutebrowser.org/ + 67890 http://example.com/path + + 68891-r http://example.com/path/other ''') + + hist.import_txt() + + assert list(hist) == [ + ('http://example.com/', 'title', 12345, False), + ('http://qutebrowser.org/', '', 12346, False), + ('http://example.com/path', '', 67890, False), + ('http://example.com/path/other', '', 68891, True) + ] + + assert not histfile.exists() + assert (data_tmpdir / 'history.bak').exists() + + +@pytest.mark.parametrize('line', [ + '', + '#12345 http://example.com/commented', + + # https://bugreports.qt.io/browse/QTBUG-60364 + '12345 http://.com/', + '12345 https://www..com/', + + # issue #2646 + '12345 data:text/html;charset=UTF-8,%3C%21DOCTYPE%20html%20PUBLIC%20%22-', +]) +def test_import_txt_skip(hist, data_tmpdir, line, monkeypatch, stubs): + """import_txt should skip certain lines silently.""" + monkeypatch.setattr(history, 'QTimer', stubs.InstaTimer) + histfile = data_tmpdir / 'history' + histfile.write(line) + + hist.import_txt() + + assert not histfile.exists() + assert not len(hist) + + +@pytest.mark.parametrize('line', [ + 'xyz http://example.com/bad-timestamp', + '12345', + 'http://example.com/no-timestamp', + '68891-r-r http://example.com/double-flag', + '68891-x http://example.com/bad-flag', + '68891 http://.com', +]) +def test_import_txt_invalid(hist, data_tmpdir, line, monkeypatch, stubs, + caplog): + """import_txt should fail on certain lines.""" + monkeypatch.setattr(history, 'QTimer', stubs.InstaTimer) + histfile = data_tmpdir / 'history' + histfile.write(line) + + with caplog.at_level(logging.ERROR): + hist.import_txt() + + assert any(rec.msg.startswith("Failed to import history:") + for rec in caplog.records) + + assert histfile.exists() + + +def test_import_txt_nonexistent(hist, data_tmpdir, monkeypatch, stubs): + """import_txt should do nothing if the history file doesn't exist.""" + monkeypatch.setattr(history, 'QTimer', stubs.InstaTimer) + hist.import_txt() + + +def test_debug_dump_history(hist, tmpdir): + hist.add_url(QUrl('http://example.com/1'), title="Title1", atime=12345) + hist.add_url(QUrl('http://example.com/2'), title="Title2", atime=12346) + hist.add_url(QUrl('http://example.com/3'), title="Title3", atime=12347) + hist.add_url(QUrl('http://example.com/4'), title="Title4", atime=12348, + redirect=True) + histfile = tmpdir / 'history' + hist.debug_dump_history(str(histfile)) + expected = ['12345 http://example.com/1 Title1', + '12346 http://example.com/2 Title2', + '12347 http://example.com/3 Title3', + '12348-r http://example.com/4 Title4'] + assert histfile.read() == '\n'.join(expected) + + +def test_debug_dump_history_nonexistent(hist, tmpdir): + histfile = tmpdir / 'nonexistent' / 'history' + with pytest.raises(cmdexc.CommandError): + hist.debug_dump_history(str(histfile)) diff --git a/tests/unit/browser/test_qutescheme.py b/tests/unit/browser/test_qutescheme.py index e46038c8d..693b2607c 100644 --- a/tests/unit/browser/test_qutescheme.py +++ b/tests/unit/browser/test_qutescheme.py @@ -89,16 +89,17 @@ class TestHistoryHandler: items = [] for i in range(entry_count): entry_atime = now - i * interval - entry = history.Entry(atime=str(entry_atime), - url=QUrl("www.x.com/" + str(i)), title="Page " + str(i)) + entry = {"atime": str(entry_atime), + "url": QUrl("www.x.com/" + str(i)), + "title": "Page " + str(i)} items.insert(0, entry) return items @pytest.fixture - def fake_web_history(self, fake_save_manager, tmpdir): + def fake_web_history(self, fake_save_manager, tmpdir, init_sql): """Create a fake web-history and register it into objreg.""" - web_history = history.WebHistory(tmpdir.dirname, 'fake-history') + web_history = history.WebHistory() objreg.register('web-history', web_history) yield web_history objreg.delete('web-history') @@ -107,8 +108,7 @@ class TestHistoryHandler: def fake_history(self, fake_web_history, entries): """Create fake history.""" for item in entries: - fake_web_history._add_entry(item) - fake_web_history.save() + fake_web_history.add_url(**item) @pytest.mark.parametrize("start_time_offset, expected_item_count", [ (0, 4), @@ -123,43 +123,25 @@ class TestHistoryHandler: url = QUrl("qute://history/data?start_time=" + str(start_time)) _mimetype, data = qutescheme.qute_history(url) items = json.loads(data) - items = [item for item in items if 'time' in item] # skip 'next' item assert len(items) == expected_item_count # test times end_time = start_time - 24*60*60 for item in items: - assert item['time'] <= start_time * 1000 - assert item['time'] > end_time * 1000 - - @pytest.mark.parametrize("start_time_offset, next_time", [ - (0, 24*60*60), - (24*60*60, 48*60*60), - (48*60*60, -1), - (72*60*60, -1) - ]) - def test_qutehistory_next(self, start_time_offset, next_time, now): - """Ensure qute://history/data returns correct items.""" - start_time = now - start_time_offset - url = QUrl("qute://history/data?start_time=" + str(start_time)) - _mimetype, data = qutescheme.qute_history(url) - items = json.loads(data) - items = [item for item in items if 'next' in item] # 'next' items - assert len(items) == 1 - - if next_time == -1: - assert items[0]["next"] == -1 - else: - assert items[0]["next"] == now - next_time + assert item['time'] <= start_time + assert item['time'] > end_time def test_qute_history_benchmark(self, fake_web_history, benchmark, now): - for t in range(100000): # one history per second - entry = history.Entry( - atime=str(now - t), - url=QUrl('www.x.com/{}'.format(t)), - title='x at {}'.format(t)) - fake_web_history._add_entry(entry) + r = range(100000) + entries = { + 'atime': [int(now - t) for t in r], + 'url': ['www.x.com/{}'.format(t) for t in r], + 'title': ['x at {}'.format(t) for t in r], + 'redirect': [False for _ in r], + } + fake_web_history.insert_batch(entries) url = QUrl("qute://history/data?start_time={}".format(now)) - _mimetype, _data = benchmark(qutescheme.qute_history, url) + _mimetype, data = benchmark(qutescheme.qute_history, url) + assert len(json.loads(data)) > 1 diff --git a/tests/unit/browser/test_shared.py b/tests/unit/browser/test_shared.py index da81c2059..8cd1d6704 100644 --- a/tests/unit/browser/test_shared.py +++ b/tests/unit/browser/test_shared.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/browser/test_signalfilter.py b/tests/unit/browser/test_signalfilter.py index bf8651d7f..b56e4ecc0 100644 --- a/tests/unit/browser/test_signalfilter.py +++ b/tests/unit/browser/test_signalfilter.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -29,27 +29,6 @@ from qutebrowser.browser import signalfilter from qutebrowser.utils import objreg -class FakeTabbedBrowser: - - def __init__(self): - self.index_of = None - self.current_index = None - - def indexOf(self, _tab): - if self.index_of is None: - raise ValueError("indexOf got called with index_of None!") - elif self.index_of is RuntimeError: - raise RuntimeError - else: - return self.index_of - - def currentIndex(self): - if self.current_index is None: - raise ValueError("currentIndex got called with current_index " - "None!") - return self.current_index - - class Signaller(QObject): signal = pyqtSignal(str) @@ -84,8 +63,8 @@ def objects(): @pytest.fixture -def tabbed_browser(win_registry): - tb = FakeTabbedBrowser() +def tabbed_browser(stubs, win_registry): + tb = stubs.TabbedBrowserStub() objreg.register('tabbed-browser', tb, scope='window', window=0) yield tb objreg.delete('tabbed-browser', scope='window', window=0) diff --git a/tests/unit/browser/test_tab.py b/tests/unit/browser/test_tab.py index 7ff1edbde..bfd060956 100644 --- a/tests/unit/browser/test_tab.py +++ b/tests/unit/browser/test_tab.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/browser/webengine/test_webenginedownloads.py b/tests/unit/browser/webengine/test_webenginedownloads.py index ee5b8e22a..043bed1a5 100644 --- a/tests/unit/browser/webengine/test_webenginedownloads.py +++ b/tests/unit/browser/webengine/test_webenginedownloads.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -24,6 +24,7 @@ import pytest pytest.importorskip('PyQt5.QtWebEngineWidgets') from qutebrowser.browser.webengine import webenginedownloads +from helpers import utils @pytest.mark.parametrize('path, expected', [ @@ -31,8 +32,10 @@ from qutebrowser.browser.webengine import webenginedownloads ('foo(1)', 'foo'), ('foo(a)', 'foo(a)'), ('foo1', 'foo1'), - ('foo%20bar', 'foo bar'), - ('foo%2Fbar', 'bar'), + pytest.param('foo%20bar', 'foo bar', marks=utils.qt58), + pytest.param('foo%2Fbar', 'bar', marks=utils.qt58), + pytest.param('foo%20bar', 'foo%20bar', marks=utils.qt59), + pytest.param('foo%2Fbar', 'foo%2Fbar', marks=utils.qt59), ]) def test_get_suggested_filename(path, expected): assert webenginedownloads._get_suggested_filename(path) == expected diff --git a/tests/unit/browser/webkit/http/test_content_disposition.py b/tests/unit/browser/webkit/http/test_content_disposition.py index 7b817c5e9..e1f78eb74 100644 --- a/tests/unit/browser/webkit/http/test_content_disposition.py +++ b/tests/unit/browser/webkit/http/test_content_disposition.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/browser/webkit/http/test_http.py b/tests/unit/browser/webkit/http/test_http.py index ee80baed7..1f9b7d75d 100644 --- a/tests/unit/browser/webkit/http/test_http.py +++ b/tests/unit/browser/webkit/http/test_http.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/browser/webkit/http/test_http_hypothesis.py b/tests/unit/browser/webkit/http/test_http_hypothesis.py index 1fb4dd835..42290a8f4 100644 --- a/tests/unit/browser/webkit/http/test_http_hypothesis.py +++ b/tests/unit/browser/webkit/http/test_http_hypothesis.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/browser/webkit/network/test_filescheme.py b/tests/unit/browser/webkit/network/test_filescheme.py index c3ef870d6..7a1bd9b19 100644 --- a/tests/unit/browser/webkit/network/test_filescheme.py +++ b/tests/unit/browser/webkit/network/test_filescheme.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Antoni Boucher (antoyo) +# Copyright 2015-2017 Antoni Boucher (antoyo) # # This file is part of qutebrowser. # diff --git a/tests/unit/browser/webkit/network/test_networkmanager.py b/tests/unit/browser/webkit/network/test_networkmanager.py index 4ac93435d..b66d71f4d 100644 --- a/tests/unit/browser/webkit/network/test_networkmanager.py +++ b/tests/unit/browser/webkit/network/test_networkmanager.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -22,20 +22,11 @@ import pytest from qutebrowser.browser.webkit.network import networkmanager from qutebrowser.browser.webkit import cookies + pytestmark = pytest.mark.usefixtures('cookiejar_and_cache') -class TestPrivateMode: - - def test_init_with_private_mode(self, config_stub): - config_stub.data = {'general': {'private-browsing': True}} - nam = networkmanager.NetworkManager(0, 0) - assert isinstance(nam.cookieJar(), cookies.RAMCookieJar) - - def test_setting_private_mode_later(self, config_stub): - config_stub.data = {'general': {'private-browsing': False}} - nam = networkmanager.NetworkManager(0, 0) - assert not isinstance(nam.cookieJar(), cookies.RAMCookieJar) - config_stub.data = {'general': {'private-browsing': True}} - nam.on_config_changed() - assert isinstance(nam.cookieJar(), cookies.RAMCookieJar) +def test_init_with_private_mode(config_stub): + nam = networkmanager.NetworkManager(win_id=0, tab_id=0, private=True) + assert isinstance(nam.cookieJar(), cookies.RAMCookieJar) + assert nam.cache() is None diff --git a/tests/unit/browser/webkit/network/test_networkreply.py b/tests/unit/browser/webkit/network/test_networkreply.py index 7a9c89393..8d24ebab8 100644 --- a/tests/unit/browser/webkit/network/test_networkreply.py +++ b/tests/unit/browser/webkit/network/test_networkreply.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -91,3 +91,11 @@ def test_error_network_reply(qtbot, req): assert reply.readData(1) == b'' assert reply.error() == QNetworkReply.UnknownNetworkError assert reply.errorString() == "This is an error" + + +def test_redirect_network_reply(): + url = QUrl('https://www.example.com/') + reply = networkreply.RedirectNetworkReply(url) + assert reply.readData(1) == b'' + assert reply.attribute(QNetworkRequest.RedirectionTargetAttribute) == url + reply.abort() # shouldn't do anything diff --git a/tests/unit/browser/webkit/network/test_pac.py b/tests/unit/browser/webkit/network/test_pac.py index 2ad63a496..9adc65087 100644 --- a/tests/unit/browser/webkit/network/test_pac.py +++ b/tests/unit/browser/webkit/network/test_pac.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -214,17 +214,7 @@ 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"; - } - """ - +def fetcher_test(test_str): class PACHandler(http.server.BaseHTTPRequestHandler): def do_GET(self): self.send_response(200) @@ -250,5 +240,38 @@ def test_fetch(): assert res.fetch_error() is None finally: serve_thread.join() + return res + + +@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_success(): + test_str = """ + function FindProxyForURL(domain, host) { + return "DIRECT; PROXY 127.0.0.1:8080; SOCKS 192.168.1.1:4444"; + } + """ + + res = fetcher_test(test_str) proxies = res.resolve(QNetworkProxyQuery(QUrl("https://example.com/test"))) assert len(proxies) == 3 + + +@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_evalerror(caplog): + test_str = """ + function FindProxyForURL(domain, host) { + return "FOO"; + } + """ + + res = fetcher_test(test_str) + with caplog.at_level(logging.ERROR): + proxies = res.resolve(QNetworkProxyQuery(QUrl("https://example.com/test"))) + assert len(proxies) == 1 + assert proxies[0].port() == 9 diff --git a/tests/unit/browser/webkit/network/test_schemehandler.py b/tests/unit/browser/webkit/network/test_schemehandler.py index d981b8412..1f5464a85 100644 --- a/tests/unit/browser/webkit/network/test_schemehandler.py +++ b/tests/unit/browser/webkit/network/test_schemehandler.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/browser/webkit/network/test_webkitqutescheme.py b/tests/unit/browser/webkit/network/test_webkitqutescheme.py index f941abb6d..769ecc751 100644 --- a/tests/unit/browser/webkit/network/test_webkitqutescheme.py +++ b/tests/unit/browser/webkit/network/test_webkitqutescheme.py @@ -1,6 +1,7 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Daniel Schadt +# Copyright 2016-2017 Daniel Schadt +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -17,9 +18,9 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -import pytest import logging +import pytest from PyQt5.QtCore import QUrl from qutebrowser.utils import usertypes diff --git a/tests/unit/browser/webkit/test_cache.py b/tests/unit/browser/webkit/test_cache.py index c716b9b82..8c0f7b948 100644 --- a/tests/unit/browser/webkit/test_cache.py +++ b/tests/unit/browser/webkit/test_cache.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 lamarpavel +# Copyright 2015-2017 lamarpavel # # This file is part of qutebrowser. # @@ -17,10 +17,17 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . +import pytest from PyQt5.QtCore import QUrl, QDateTime from PyQt5.QtNetwork import QNetworkDiskCache, QNetworkCacheMetaData from qutebrowser.browser.webkit import cache +from qutebrowser.utils import qtutils + + +pytestmark = pytest.mark.skipif( + qtutils.version_check('5.7.1') and not qtutils.version_check('5.9'), + reason="QNetworkDiskCache is broken on Qt 5.7.1 and 5.8") def preload_cache(cache, url='http://www.example.com/', content=b'foobar'): @@ -47,41 +54,6 @@ def test_cache_config_change_cache_size(config_stub, tmpdir): assert disk_cache.maximumCacheSize() == max_cache_size * 2 -def test_cache_config_enable_private_browsing(config_stub, tmpdir): - """Change private-browsing config to True and emit signal.""" - config_stub.data = { - 'storage': {'cache-size': 1024}, - 'general': {'private-browsing': False} - } - disk_cache = cache.DiskCache(str(tmpdir)) - assert disk_cache.cacheSize() == 0 - preload_cache(disk_cache) - assert disk_cache.cacheSize() > 0 - - config_stub.set('general', 'private-browsing', True) - assert disk_cache.cacheSize() == 0 - - -def test_cache_config_disable_private_browsing(config_stub, tmpdir): - """Change private-browsing config to False and emit signal.""" - config_stub.data = { - 'storage': {'cache-size': 1024}, - 'general': {'private-browsing': True} - } - url = 'http://qutebrowser.org' - metadata = QNetworkCacheMetaData() - metadata.setUrl(QUrl(url)) - assert metadata.isValid() - - disk_cache = cache.DiskCache(str(tmpdir)) - assert disk_cache.prepare(metadata) is None - - config_stub.set('general', 'private-browsing', False) - content = b'cute' - preload_cache(disk_cache, url, content) - assert disk_cache.data(QUrl(url)).readAll() == content - - def test_cache_size_leq_max_cache_size(config_stub, tmpdir): """Test cacheSize <= MaximumCacheSize when cache is activated.""" limit = 100 @@ -101,16 +73,6 @@ def test_cache_size_leq_max_cache_size(config_stub, tmpdir): assert disk_cache.cacheSize() < limit + 100 -def test_cache_size_deactivated(config_stub, tmpdir): - """Confirm that the cache size returns 0 when deactivated.""" - config_stub.data = { - 'storage': {'cache-size': 1024}, - 'general': {'private-browsing': True} - } - disk_cache = cache.DiskCache(str(tmpdir)) - assert disk_cache.cacheSize() == 0 - - def test_cache_existing_metadata_file(config_stub, tmpdir): """Test querying existing meta data file from activated cache.""" config_stub.data = { @@ -148,42 +110,6 @@ def test_cache_nonexistent_metadata_file(config_stub, tmpdir): assert not cache_file.isValid() -def test_cache_deactivated_metadata_file(config_stub, tmpdir): - """Test querying meta data file when cache is deactivated.""" - config_stub.data = { - 'storage': {'cache-size': 1024}, - 'general': {'private-browsing': True} - } - disk_cache = cache.DiskCache(str(tmpdir)) - assert disk_cache.fileMetaData("foo") == QNetworkCacheMetaData() - - -def test_cache_deactivated_private_browsing(config_stub, tmpdir): - """Test if cache is deactivated in private-browsing mode.""" - config_stub.data = { - 'storage': {'cache-size': 1024}, - 'general': {'private-browsing': True} - } - disk_cache = cache.DiskCache(str(tmpdir)) - - metadata = QNetworkCacheMetaData() - metadata.setUrl(QUrl('http://www.example.com/')) - assert metadata.isValid() - assert disk_cache.prepare(metadata) is None - - -def test_cache_deactivated_get_data(config_stub, tmpdir): - """Query some data from a deactivated cache.""" - config_stub.data = { - 'storage': {'cache-size': 1024}, - 'general': {'private-browsing': True} - } - disk_cache = cache.DiskCache(str(tmpdir)) - - url = QUrl('http://www.example.com/') - assert disk_cache.data(url) is None - - def test_cache_get_nonexistent_data(config_stub, tmpdir): """Test querying some data that was never inserted.""" config_stub.data = { @@ -196,18 +122,6 @@ def test_cache_get_nonexistent_data(config_stub, tmpdir): assert disk_cache.data(QUrl('http://qutebrowser.org')) is None -def test_cache_deactivated_remove_data(config_stub, tmpdir): - """Test removing some data from a deactivated cache.""" - config_stub.data = { - 'storage': {'cache-size': 1024}, - 'general': {'private-browsing': True} - } - disk_cache = cache.DiskCache(str(tmpdir)) - - url = QUrl('http://www.example.com/') - assert not disk_cache.remove(url) - - def test_cache_insert_data(config_stub, tmpdir): """Test if entries inserted into the cache are actually there.""" config_stub.data = { @@ -225,28 +139,6 @@ def test_cache_insert_data(config_stub, tmpdir): assert disk_cache.data(QUrl(url)).readAll() == content -def test_cache_deactivated_insert_data(config_stub, tmpdir): - """Insert data when cache is deactivated.""" - # First create QNetworkDiskCache just to get a valid QIODevice from it - url = 'http://qutebrowser.org' - disk_cache = QNetworkDiskCache() - disk_cache.setCacheDirectory(str(tmpdir)) - metadata = QNetworkCacheMetaData() - metadata.setUrl(QUrl(url)) - device = disk_cache.prepare(metadata) - assert device is not None - - # Now create a deactivated DiskCache and insert the valid device created - # above (there probably is a better way to get a valid QIODevice...) - config_stub.data = { - 'storage': {'cache-size': 1024}, - 'general': {'private-browsing': True} - } - - deactivated_cache = cache.DiskCache(str(tmpdir)) - assert deactivated_cache.insert(device) is None - - def test_cache_remove_data(config_stub, tmpdir): """Test if a previously inserted entry can be removed from the cache.""" config_stub.data = { @@ -278,16 +170,6 @@ def test_cache_clear_activated(config_stub, tmpdir): assert disk_cache.cacheSize() == 0 -def test_cache_clear_deactivated(config_stub, tmpdir): - """Test method clear() on deactivated cache.""" - config_stub.data = { - 'storage': {'cache-size': 1024}, - 'general': {'private-browsing': True} - } - disk_cache = cache.DiskCache(str(tmpdir)) - assert disk_cache.clear() is None - - def test_cache_metadata(config_stub, tmpdir): """Ensure that DiskCache.metaData() returns exactly what was inserted.""" config_stub.data = { @@ -306,18 +188,6 @@ def test_cache_metadata(config_stub, tmpdir): assert disk_cache.metaData(QUrl(url)) == metadata -def test_cache_deactivated_metadata(config_stub, tmpdir): - """Test querying metaData() on not activated cache.""" - config_stub.data = { - 'storage': {'cache-size': 1024}, - 'general': {'private-browsing': True} - } - url = 'http://qutebrowser.org' - - disk_cache = cache.DiskCache(str(tmpdir)) - assert disk_cache.metaData(QUrl(url)) == QNetworkCacheMetaData() - - def test_cache_update_metadata(config_stub, tmpdir): """Test updating the meta data for an existing cache entry.""" config_stub.data = { @@ -336,21 +206,6 @@ def test_cache_update_metadata(config_stub, tmpdir): assert disk_cache.metaData(QUrl(url)) == metadata -def test_cache_deactivated_update_metadata(config_stub, tmpdir): - """Test updating the meta data when cache is not activated.""" - config_stub.data = { - 'storage': {'cache-size': 1024}, - 'general': {'private-browsing': True} - } - url = 'http://qutebrowser.org' - disk_cache = cache.DiskCache(str(tmpdir)) - - metadata = QNetworkCacheMetaData() - metadata.setUrl(QUrl(url)) - assert metadata.isValid() - assert disk_cache.updateMetaData(metadata) is None - - def test_cache_full(config_stub, tmpdir): """Do a sanity test involving everything.""" config_stub.data = { diff --git a/tests/unit/browser/webkit/test_cookies.py b/tests/unit/browser/webkit/test_cookies.py index bcee01e60..85d045763 100644 --- a/tests/unit/browser/webkit/test_cookies.py +++ b/tests/unit/browser/webkit/test_cookies.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Alexander Cogneau (acogneau) : +# Copyright 2015-2017 Alexander Cogneau (acogneau) : # # This file is part of qutebrowser. # diff --git a/tests/unit/browser/webkit/test_downloads.py b/tests/unit/browser/webkit/test_downloads.py index acee916b2..5a214f638 100644 --- a/tests/unit/browser/webkit/test_downloads.py +++ b/tests/unit/browser/webkit/test_downloads.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -30,6 +30,42 @@ def test_download_model(qapp, qtmodeltester, config_stub, cookiejar_and_cache): qtmodeltester.check(model) +@pytest.mark.parametrize('url, title, out', [ + ('http://qutebrowser.org/INSTALL.html', + 'Installing qutebrowser | qutebrowser', + 'Installing qutebrowser _ qutebrowser.html'), + ('http://qutebrowser.org/INSTALL.html', + 'Installing qutebrowser | qutebrowser.html', + 'Installing qutebrowser _ qutebrowser.html'), + ('http://qutebrowser.org/INSTALL.HTML', + 'Installing qutebrowser | qutebrowser', + 'Installing qutebrowser _ qutebrowser.html'), + ('http://qutebrowser.org/INSTALL.html', + 'Installing qutebrowser | qutebrowser.HTML', + 'Installing qutebrowser _ qutebrowser.HTML'), + ('http://qutebrowser.org/', + 'qutebrowser | qutebrowser', + 'qutebrowser _ qutebrowser.html'), + ('https://github.com/qutebrowser/qutebrowser/releases', + 'Releases · qutebrowser/qutebrowser', + 'Releases · qutebrowser_qutebrowser.html'), + ('http://qutebrowser.org/index.php', + 'qutebrowser | qutebrowser', + 'qutebrowser _ qutebrowser.html'), + ('http://qutebrowser.org/index.php', + 'qutebrowser | qutebrowser - index.php', + 'qutebrowser _ qutebrowser - index.php.html'), + ('https://qutebrowser.org/img/cheatsheet-big.png', + 'cheatsheet-big.png (3342×2060)', + None), + ('http://qutebrowser.org/page-with-no-title.html', + '', + None), +]) +def test_page_titles(url, title, out): + assert downloads.suggested_fn_from_title(url, title) == out + + class TestDownloadTarget: def test_base(self): diff --git a/tests/unit/browser/webkit/test_history.py b/tests/unit/browser/webkit/test_history.py deleted file mode 100644 index cc35dc4e4..000000000 --- a/tests/unit/browser/webkit/test_history.py +++ /dev/null @@ -1,416 +0,0 @@ -# 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 . - -"""Tests for the global page history.""" - -import logging - -import pytest -import hypothesis -from hypothesis import strategies -from PyQt5.QtCore import QUrl - -from qutebrowser.browser import history -from qutebrowser.utils import objreg, urlutils, usertypes - - -class FakeWebHistory: - - """A fake WebHistory object.""" - - def __init__(self, history_dict): - self.history_dict = history_dict - - -@pytest.fixture(autouse=True) -def prerequisites(config_stub, fake_save_manager): - """Make sure everything is ready to initialize a WebHistory.""" - config_stub.data = {'general': {'private-browsing': False}} - - -@pytest.fixture() -def hist(tmpdir): - return history.WebHistory(hist_dir=str(tmpdir), hist_name='history') - - -def test_async_read_twice(monkeypatch, qtbot, tmpdir, caplog): - (tmpdir / 'filled-history').write('\n'.join([ - '12345 http://example.com/ title', - '67890 http://example.com/', - '12345 http://qutebrowser.org/ blah', - ])) - hist = history.WebHistory(hist_dir=str(tmpdir), hist_name='filled-history') - next(hist.async_read()) - with pytest.raises(StopIteration): - next(hist.async_read()) - expected = "Ignoring async_read() because reading is started." - assert len(caplog.records) == 1 - assert caplog.records[0].msg == expected - - -@pytest.mark.parametrize('redirect', [True, False]) -def test_adding_item_during_async_read(qtbot, hist, redirect): - """Check what happens when adding URL while reading the history.""" - url = QUrl('http://www.example.com/') - - with qtbot.assertNotEmitted(hist.add_completion_item), \ - qtbot.assertNotEmitted(hist.item_added): - hist.add_url(url, redirect=redirect, atime=12345) - - if redirect: - with qtbot.assertNotEmitted(hist.add_completion_item): - with qtbot.waitSignal(hist.async_read_done): - list(hist.async_read()) - else: - with qtbot.waitSignals([hist.add_completion_item, - hist.async_read_done], order='strict'): - list(hist.async_read()) - - assert not hist._temp_history - - expected = history.Entry(url=url, atime=12345, redirect=redirect, title="") - assert list(hist.history_dict.values()) == [expected] - - -def test_private_browsing(qtbot, tmpdir, fake_save_manager, config_stub): - """Make sure no data is saved at all with private browsing.""" - config_stub.data = {'general': {'private-browsing': True}} - private_hist = history.WebHistory(hist_dir=str(tmpdir), - hist_name='history') - - # Before initial read - with qtbot.assertNotEmitted(private_hist.add_completion_item), \ - qtbot.assertNotEmitted(private_hist.item_added): - private_hist.add_url(QUrl('http://www.example.com/')) - assert not private_hist._temp_history - - # read - with qtbot.assertNotEmitted(private_hist.add_completion_item), \ - qtbot.assertNotEmitted(private_hist.item_added): - with qtbot.waitSignals([private_hist.async_read_done], order='strict'): - list(private_hist.async_read()) - - # after read - with qtbot.assertNotEmitted(private_hist.add_completion_item), \ - qtbot.assertNotEmitted(private_hist.item_added): - private_hist.add_url(QUrl('http://www.example.com/')) - - assert not private_hist._temp_history - assert not private_hist._new_history - assert not private_hist.history_dict - - -def test_iter(hist): - list(hist.async_read()) - - url = QUrl('http://www.example.com/') - hist.add_url(url, atime=12345) - - entry = history.Entry(url=url, atime=12345, redirect=False, title="") - assert list(hist) == [entry] - - -def test_len(hist): - assert len(hist) == 0 - list(hist.async_read()) - - url = QUrl('http://www.example.com/') - hist.add_url(url) - - assert len(hist) == 1 - - -@pytest.mark.parametrize('line', [ - '12345 http://example.com/ title', # with title - '67890 http://example.com/', # no title - '12345 http://qutebrowser.org/ ', # trailing space - ' ', - '', -]) -def test_read(hist, tmpdir, line): - (tmpdir / 'filled-history').write(line + '\n') - hist = history.WebHistory(hist_dir=str(tmpdir), hist_name='filled-history') - list(hist.async_read()) - - -def test_updated_entries(hist, tmpdir): - (tmpdir / 'filled-history').write('12345 http://example.com/\n' - '67890 http://example.com/\n') - hist = history.WebHistory(hist_dir=str(tmpdir), hist_name='filled-history') - list(hist.async_read()) - - assert hist.history_dict['http://example.com/'].atime == 67890 - hist.add_url(QUrl('http://example.com/'), atime=99999) - assert hist.history_dict['http://example.com/'].atime == 99999 - - -def test_invalid_read(hist, tmpdir, caplog): - (tmpdir / 'filled-history').write('foobar\n12345 http://example.com/') - hist = history.WebHistory(hist_dir=str(tmpdir), hist_name='filled-history') - with caplog.at_level(logging.WARNING): - list(hist.async_read()) - - entries = list(hist.history_dict.values()) - - assert len(entries) == 1 - assert len(caplog.records) == 1 - msg = "Invalid history entry 'foobar': 2 or 3 fields expected!" - assert caplog.records[0].msg == msg - - -def test_get_recent(hist, tmpdir): - (tmpdir / 'filled-history').write('12345 http://example.com/') - hist = history.WebHistory(hist_dir=str(tmpdir), hist_name='filled-history') - list(hist.async_read()) - - hist.add_url(QUrl('http://www.qutebrowser.org/'), atime=67890) - lines = hist.get_recent() - - expected = ['12345 http://example.com/', - '67890 http://www.qutebrowser.org/'] - assert lines == expected - - -def test_save(hist, tmpdir): - hist_file = tmpdir / 'filled-history' - hist_file.write('12345 http://example.com/\n') - - hist = history.WebHistory(hist_dir=str(tmpdir), hist_name='filled-history') - list(hist.async_read()) - - hist.add_url(QUrl('http://www.qutebrowser.org/'), atime=67890) - hist.save() - - lines = hist_file.read().splitlines() - expected = ['12345 http://example.com/', - '67890 http://www.qutebrowser.org/'] - assert lines == expected - - hist.add_url(QUrl('http://www.the-compiler.org/'), atime=99999) - hist.save() - expected.append('99999 http://www.the-compiler.org/') - - lines = hist_file.read().splitlines() - assert lines == expected - - -def test_clear(qtbot, hist, tmpdir): - hist_file = tmpdir / 'filled-history' - hist_file.write('12345 http://example.com/\n') - - hist = history.WebHistory(hist_dir=str(tmpdir), hist_name='filled-history') - list(hist.async_read()) - - hist.add_url(QUrl('http://www.qutebrowser.org/')) - - with qtbot.waitSignal(hist.cleared): - hist._do_clear() - - assert not hist_file.read() - assert not hist.history_dict - assert not hist._new_history - - hist.add_url(QUrl('http://www.the-compiler.org/'), atime=67890) - hist.save() - - lines = hist_file.read().splitlines() - assert lines == ['67890 http://www.the-compiler.org/'] - - -def test_add_item(qtbot, hist): - list(hist.async_read()) - url = 'http://www.example.com/' - - with qtbot.waitSignals([hist.add_completion_item, hist.item_added], - order='strict'): - hist.add_url(QUrl(url), atime=12345, title="the title") - - entry = history.Entry(url=QUrl(url), redirect=False, atime=12345, - title="the title") - assert hist.history_dict[url] == entry - - -def test_add_item_redirect(qtbot, hist): - list(hist.async_read()) - url = 'http://www.example.com/' - with qtbot.assertNotEmitted(hist.add_completion_item): - with qtbot.waitSignal(hist.item_added): - hist.add_url(QUrl(url), redirect=True, atime=12345) - - entry = history.Entry(url=QUrl(url), redirect=True, atime=12345, title="") - assert hist.history_dict[url] == entry - - -def test_add_item_redirect_update(qtbot, tmpdir): - """A redirect update added should override a non-redirect one.""" - url = 'http://www.example.com/' - - hist_file = tmpdir / 'filled-history' - hist_file.write('12345 {}\n'.format(url)) - hist = history.WebHistory(hist_dir=str(tmpdir), hist_name='filled-history') - list(hist.async_read()) - - with qtbot.assertNotEmitted(hist.add_completion_item): - with qtbot.waitSignal(hist.item_added): - hist.add_url(QUrl(url), redirect=True, atime=67890) - - entry = history.Entry(url=QUrl(url), redirect=True, atime=67890, title="") - assert hist.history_dict[url] == entry - - -@pytest.mark.parametrize('line, expected', [ - ( - # old format without title - '12345 http://example.com/', - history.Entry(atime=12345, url=QUrl('http://example.com/'), title='',) - ), - ( - # trailing space without title - '12345 http://example.com/ ', - history.Entry(atime=12345, url=QUrl('http://example.com/'), title='',) - ), - ( - # new format with title - '12345 http://example.com/ this is a title', - history.Entry(atime=12345, url=QUrl('http://example.com/'), - title='this is a title') - ), - ( - # weird NUL bytes - '\x0012345 http://example.com/', - history.Entry(atime=12345, url=QUrl('http://example.com/'), title=''), - ), - ( - # redirect flag - '12345-r http://example.com/ this is a title', - history.Entry(atime=12345, url=QUrl('http://example.com/'), - title='this is a title', redirect=True) - ), -]) -def test_entry_parse_valid(line, expected): - entry = history.Entry.from_str(line) - assert entry == expected - - -@pytest.mark.parametrize('line', [ - '12345', # one field - '12345 ::', # invalid URL - 'xyz http://www.example.com/', # invalid timestamp - '12345-x http://www.example.com/', # invalid flags - '12345-r-r http://www.example.com/', # double flags -]) -def test_entry_parse_invalid(line): - with pytest.raises(ValueError): - history.Entry.from_str(line) - - -@hypothesis.given(strategies.text()) -def test_entry_parse_hypothesis(text): - """Make sure parsing works or gives us ValueError.""" - try: - history.Entry.from_str(text) - except ValueError: - pass - - -@pytest.mark.parametrize('entry, expected', [ - # simple - ( - history.Entry(12345, QUrl('http://example.com/'), "the title"), - "12345 http://example.com/ the title", - ), - # timestamp as float - ( - history.Entry(12345.678, QUrl('http://example.com/'), "the title"), - "12345 http://example.com/ the title", - ), - # no title - ( - history.Entry(12345.678, QUrl('http://example.com/'), ""), - "12345 http://example.com/", - ), - # redirect flag - ( - history.Entry(12345.678, QUrl('http://example.com/'), "", - redirect=True), - "12345-r http://example.com/", - ), -]) -def test_entry_str(entry, expected): - assert str(entry) == expected - - -@pytest.fixture -def hist_interface(): - # pylint: disable=invalid-name - QtWebKit = pytest.importorskip('PyQt5.QtWebKit') - from qutebrowser.browser.webkit import webkithistory - QWebHistoryInterface = QtWebKit.QWebHistoryInterface - # pylint: enable=invalid-name - entry = history.Entry(atime=0, url=QUrl('http://www.example.com/'), - title='example') - history_dict = {'http://www.example.com/': entry} - fake_hist = FakeWebHistory(history_dict) - interface = webkithistory.WebHistoryInterface(fake_hist) - QWebHistoryInterface.setDefaultInterface(interface) - yield - QWebHistoryInterface.setDefaultInterface(None) - - -def test_history_interface(qtbot, webview, hist_interface): - html = b"foo" - url = urlutils.data_url('text/html', html) - with qtbot.waitSignal(webview.loadFinished): - webview.load(url) - - -@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 - - 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 - - try: - from PyQt5.QtWebKit import QWebHistoryInterface - except ImportError: - QWebHistoryInterface = None - - if backend == usertypes.Backend.QtWebKit: - default_interface = QWebHistoryInterface.defaultInterface() - assert default_interface._history is hist - else: - assert backend == usertypes.Backend.QtWebEngine - if QWebHistoryInterface is None: - default_interface = None - else: - default_interface = QWebHistoryInterface.defaultInterface() - # For this to work, nothing can ever have called setDefaultInterface - # before (so we need to test webengine before webkit) - assert default_interface is None - - assert fake_save_manager.add_saveable.called - objreg.delete('web-history') diff --git a/tests/unit/browser/webkit/test_mhtml.py b/tests/unit/browser/webkit/test_mhtml.py index e5fdf4ffd..230565680 100644 --- a/tests/unit/browser/webkit/test_mhtml.py +++ b/tests/unit/browser/webkit/test_mhtml.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Daniel Schadt +# Copyright 2015-2017 Daniel Schadt # # This file is part of qutebrowser. # @@ -104,9 +104,8 @@ def test_refuses_non_ascii_header_value(checker, header, value): } defaults[header] = value writer = mhtml.MHTMLWriter(**defaults) - with pytest.raises(UnicodeEncodeError) as excinfo: + with pytest.raises(UnicodeEncodeError, match="'ascii' codec can't encode"): writer.write_to(checker.fp) - assert "'ascii' codec can't encode" in str(excinfo.value) def test_file_encoded_as_base64(checker): @@ -141,8 +140,9 @@ def test_file_encoded_as_base64(checker): """) -@pytest.mark.parametrize('transfer_encoding', [mhtml.E_BASE64, mhtml.E_QUOPRI], - ids=['base64', 'quoted-printable']) +@pytest.mark.parametrize('transfer_encoding', [ + pytest.param(mhtml.E_BASE64, id='base64'), + pytest.param(mhtml.E_QUOPRI, id='quoted-printable')]) def test_payload_lines_wrap(checker, transfer_encoding): payload = b'1234567890' * 10 writer = mhtml.MHTMLWriter(root_content=b'', content_type='text/plain', @@ -257,21 +257,26 @@ def test_empty_content_type(checker): @pytest.mark.parametrize('has_cssutils', [ - pytest.mark.skipif(cssutils is None, reason="requires cssutils")(True), - False, -], ids=['with_cssutils', 'no_cssutils']) + pytest.param(True, marks=pytest.mark.skipif( + cssutils is None, reason="requires cssutils"), id='with_cssutils'), + pytest.param(False, id='no_cssutils'), +]) @pytest.mark.parametrize('inline, style, expected_urls', [ - (False, "@import 'default.css'", ['default.css']), - (False, '@import "default.css"', ['default.css']), - (False, "@import \t 'tabbed.css'", ['tabbed.css']), - (False, "@import url('default.css')", ['default.css']), - (False, """body { + pytest.param(False, "@import 'default.css'", ['default.css'], + id='import with apostrophe'), + pytest.param(False, '@import "default.css"', ['default.css'], + id='import with quote'), + pytest.param(False, "@import \t 'tabbed.css'", ['tabbed.css'], + id='import with tab'), + pytest.param(False, "@import url('default.css')", ['default.css'], + id='import with url()'), + pytest.param(False, """body { background: url("/bg-img.png") - }""", ['/bg-img.png']), - (True, 'background: url(folder/file.png) no-repeat', ['folder/file.png']), - (True, 'content: url()', []), -], ids=['import with apostrophe', 'import with quote', 'import with tab', - 'import with url()', 'background with body', 'background', 'content']) + }""", ['/bg-img.png'], id='background with body'), + pytest.param(True, 'background: url(folder/file.png) no-repeat', + ['folder/file.png'], id='background'), + pytest.param(True, 'content: url()', [], id='content'), +]) def test_css_url_scanner(monkeypatch, has_cssutils, inline, style, expected_urls): if not has_cssutils: @@ -319,9 +324,8 @@ class TestNoCloseBytesIO: fp = mhtml._NoCloseBytesIO() fp.write(b'Value') fp.actual_close() - with pytest.raises(ValueError) as excinfo: + with pytest.raises(ValueError, match="I/O operation on closed file."): + fp.getvalue() + with pytest.raises(ValueError, match="I/O operation on closed file."): fp.getvalue() - assert str(excinfo.value) == 'I/O operation on closed file.' - with pytest.raises(ValueError) as excinfo: fp.write(b'Closed') - assert str(excinfo.value) == 'I/O operation on closed file.' diff --git a/tests/unit/browser/webkit/test_qt_javascript.py b/tests/unit/browser/webkit/test_qt_javascript.py index cb7c3a2ae..72d1f4ce0 100644 --- a/tests/unit/browser/webkit/test_qt_javascript.py +++ b/tests/unit/browser/webkit/test_qt_javascript.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -51,7 +51,6 @@ def test_element_js_webkit(webview, js_enabled, expected): def test_simple_js_webengine(callback_checker, webengineview, js_enabled, expected): """With QtWebEngine, runJavaScript works even when JS is off.""" - # pylint: disable=no-name-in-module,useless-suppression # If we get there (because of the webengineview fixture) we can be certain # QtWebEngine is available from PyQt5.QtWebEngineWidgets import QWebEngineSettings diff --git a/tests/unit/browser/webkit/test_tabhistory.py b/tests/unit/browser/webkit/test_tabhistory.py index b99d8376d..07b334771 100644 --- a/tests/unit/browser/webkit/test_tabhistory.py +++ b/tests/unit/browser/webkit/test_tabhistory.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/browser/webkit/test_webkitelem.py b/tests/unit/browser/webkit/test_webkitelem.py index c4384fa61..deff844fe 100644 --- a/tests/unit/browser/webkit/test_webkitelem.py +++ b/tests/unit/browser/webkit/test_webkitelem.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -147,21 +147,18 @@ class SelectionAndFilterTests: ('', [webelem.Group.all]), ('', [webelem.Group.all, webelem.Group.links, - webelem.Group.prevnext, webelem.Group.url]), + webelem.Group.url]), ('', [webelem.Group.all, + webelem.Group.links, webelem.Group.url]), ('', [webelem.Group.all]), ('', [webelem.Group.all, webelem.Group.links, - webelem.Group.prevnext, webelem.Group.url]), - ('', [webelem.Group.all, - webelem.Group.url]), + webelem.Group.url]), ('', [webelem.Group.all]), ('', [webelem.Group.all, webelem.Group.links, - webelem.Group.prevnext, webelem.Group.url]), - ('', [webelem.Group.all, - webelem.Group.url]), + webelem.Group.url]), ('