Merge remote-tracking branch 'upstream/master' into HEAD

This commit is contained in:
Ryan Roden-Corrent 2017-08-06 10:54:19 -04:00
commit 71b71dbc58
128 changed files with 3953 additions and 3669 deletions

View File

@ -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
@ -38,7 +39,7 @@ putty-ignore =
/# pragma: no mccabe/ : +C901
tests/*/test_*.py : +D100,D101,D401
tests/conftest.py : +F403
tests/unit/browser/webkit/test_history.py : +N806
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

8
.github/CODEOWNERS vendored Normal file
View File

@ -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

View File

@ -23,14 +23,18 @@ matrix:
language: python
python: 3.6
env: TESTENV=py36-pyqt571
- os: linux
language: python
python: 3.6
env: TESTENV=py36-pyqt58
- os: linux
language: python
python: 3.5
env: TESTENV=py35-pyqt58
env: TESTENV=py35-pyqt59
- os: linux
language: python
python: 3.6
env: TESTENV=py36-pyqt58
env: TESTENV=py36-pyqt59
- os: osx
env: TESTENV=py36 OSX=elcapitan
osx_image: xcode7.3

View File

@ -14,9 +14,69 @@ This project adheres to http://semver.org/[Semantic Versioning].
// `Fixed` for any bug fixes.
// `Security` to invite users to upgrade in case of vulnerabilities.
v0.11.0 (unreleased)
v1.0.0 (unreleased)
-------------------
Breaking changes
~~~~~~~~~~~~~~~~
- Support for legacy QtWebKit (before 5.212 which is distributed
independently from Qt) is dropped.
- Support for Python 3.4 is dropped.
- Support for Qt before 5.7 is dropped.
- New dependency on the QtSql module and Qt sqlite support.
- New dependency on ruamel.yaml; dropped PyYAML dependency.
- The QtWebEngine backend is now used by default if available.
- New config system which ignores the old config file.
- The depedency on PyOpenGL (when using QtWebEngine) got removed. Note
that PyQt5.QtOpenGL is still a dependency.
Major changes
~~~~~~~~~~~~~
- New completion engine based on sqlite, which allows to complete
the entire browsing history.
- Completely rewritten configuration system.
Added
~~~~~
- New back/forward indicator in the statusbar
Changed
~~~~~~~
- 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
`<Escape>` 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
~~~~~~~~~~~~~~~~
@ -28,7 +88,10 @@ New dependencies
Added
~~~~~
- New `-p` flag for `:open` to open a private window.
- 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 `<Ctrl-p>` by default).
- (QtWebEngine) Implemented `:follow-selected`.
@ -45,6 +108,8 @@ Added
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
~~~~~~~
@ -52,62 +117,51 @@ Changed
- To prevent elaborate phishing attacks, the Punycode version (`xn--*`) is now
shown in addition to the decoded version for international domain names
(IDN).
- Private browsing is now implemented for QtWebEngine, and changed it's
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.
- Improved `qute://history` page (with lazy loading)
- Starting with legacy QtWebKit now shows a warning message once.
- 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.
- PAC on QtWebKit now supports SOCKS5 as type.
- Comments in the config file are now 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.
- (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.
- 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.
- (QtWebEngine) The underlying Chromium version is now shown in the version
info.
- `qute:*` pages now use `qute://*` instead (e.g. `qute://version` instead of
`qute:version`), but the old versions are automatically redirected.
- 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.
- Renderer process crashes now show an error page.
- (QtWebKit) storage -> offline-web-application-storage` got renamed to `...-cache`
- 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
~~~~~
- The macOS .dmg is now built against Qt 5.9 which fixes various
important issues (such as not being able to type dead keys).
- (QtWebEngine) Added a workaround for a black screen with some setups
(the workaround requires PyOpenGL to be installed, but it's optional)
- (QtWebEngine) Starting with Nouveau graphics now shows an error message
instead of crashing in Qt. This adds a new dependency on `PyQt5.QtOpenGL`.
- (QtWebEngine) Retrying downloads now shows an error instead of crashing.
- (QtWebEngine) Cloning a view-source tab now doesn't crash anymore.
- (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.
- 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
@ -117,7 +171,6 @@ Fixed
- 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.
- (QtWebKit) Fixed Crash when a PAC file returns an invalid value.
- 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.
@ -125,19 +178,27 @@ Fixed
- 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.
- (QtWebEngine) `window.navigator.userAgent` is now set correctly when
customizing the user agent.
- Accidentally starting with Python 2 now shows a proper error message again.
- (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.
- 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) `:scroll-page` with `--bottom-navigate` now works correctly
- (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
-------
@ -182,7 +243,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
~~~~~~~
@ -484,7 +545,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
------
@ -847,7 +908,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
@ -922,7 +983,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
@ -1033,7 +1094,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.
@ -1131,7 +1192,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.
@ -1151,7 +1212,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.
@ -1287,7 +1348,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
@ -1323,7 +1384,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.

View File

@ -5,6 +5,12 @@ The Compiler <mail@qutebrowser.org>
: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 `&lt;3` footnote:[Of course, that says `<3` in HTML.] contributors!
This document contains guidelines for contributing to qutebrowser, as well as
@ -39,8 +45,8 @@ pointers:
* https://github.com/qutebrowser/qutebrowser/labels/easy[Issues which should
be easy to solve]
* https://github.com/qutebrowser/qutebrowser/labels/not%20code[Issues which
require little/no coding]
* https://github.com/qutebrowser/qutebrowser/labels/component%3A%20docs[Documentation
* issues which require little/no coding]
If you prefer C++ or Javascript to Python, see the relevant issues which involve
work in those languages:
@ -682,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)*)
@ -698,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

View File

@ -171,6 +171,20 @@ What's the difference between insert and passthrough mode?::
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.::
@ -211,6 +225,18 @@ it's still
https://bugreports.qt.io/browse/QTBUG-42417?jql=component%20%3D%20WebKit%20and%20resolution%20%3D%20Done%20and%20fixVersion%20in%20(5.4.0%2C%20%225.4.0%20Alpha%22%2C%20%225.4.0%20Beta%22%2C%20%225.4.0%20RC%22)%20and%20priority%20in%20(%22P2%3A%20Important%22%2C%20%22P1%3A%20Critical%22%2C%20%22P0%3A%20Blocker%22)[nearly
20 important bugs].
When using QtWebEngine, qutebrowser reports "Render Process Crashed" and the console prints a traceback on Gentoo Linux or another Source-Based Distro::
As stated in https://gcc.gnu.org/gcc-6/changes.html[GCC's Website] GCC 6 has introduced some optimizations that could break non-conforming codebases, like QtWebEngine. +
As a workaround, you can disable the nullpointer check optimization by adding the -fno-delete-null-pointer-checks flag while compiling. +
On gentoo, you just need to add it into your make.conf, like this: +
CFLAGS="... -fno-delete-null-pointer-checks"
CXXFLAGS="... -fno-delete-null-pointer-checks"
+
And then re-emerging qtwebengine with: +
emerge -1 qtwebengine
My issue is not listed.::
If you experience any segfaults or crashes, you can report the issue in
https://github.com/qutebrowser/qutebrowser/issues[the issue tracker] or

View File

@ -27,7 +27,7 @@ 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
@ -53,7 +53,7 @@ Build it from git
Install the dependencies via apt-get:
----
# apt-get install python3-pyqt5 python3-pyqt5.qtwebkit python3-pyqt5.qtquick python-tox python3-sip python3-dev
# apt-get install python3-pyqt5 python3-pyqt5.qtwebkit python3-pyqt5.qtquick python-tox python3-sip python3-dev python3-pyqt5.qtsql libqt5sql5-sqlite
----
On Debian Stretch or Ubuntu 17.04 or later, it's also recommended to install
@ -277,13 +277,13 @@ $ pip install tox
Then <<tox,install qutebrowser via tox>>.
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].

View File

@ -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

View File

@ -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
-------------
@ -74,6 +71,9 @@ There's also a https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser-ann
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
--------------------
@ -98,27 +98,35 @@ Requirements
The following software and libraries are required to run qutebrowser:
* http://www.python.org/[Python] 3.4 or newer (3.5 recommended)
* http://qt.io/[Qt] 5.2.0 or newer (5.9.0 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.8.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.
@ -142,219 +150,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
* thuck
* Martin Tournoij
* Imran Sobir
* Alexander Cogneau
* Felix Van der Jeugt
* Daniel Karbach
* Kevin Velghe
* Raphael Pierzina
* Joel Torstensson
* Patric Schmitz
* Tarcisio Fedrizzi
* Jay Kamat
* Claude
* Philipp Hansch
* Fritz Reichwald
* Corentin Julé
* meles5
* Panagiotis Ktistakis
* Artur Shaik
* Nathan Isom
* Thorsten Wißmann
* Austin Anderson
* Jimmy
* Niklas Haas
* Maciej Wołczyk
* Clayton Craft
* sandrosc
* Alexey "Averrin" Nabrodov
* pkill9
* nanjekyejoannah
* avk
* ZDarian
* Milan Svoboda
* John ShaggyTwoDope Jenkins
* Peter Vilim
* Jacob Sword
* knaggita
* Oliver Caldwell
* Nikolay Amiantov
* Julian Weigt
* Tomasz Kramkowski
* Sebastian Frysztak
* 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
* Marius
* Link
* Jussi Timperi
* Cosmin Popescu
* Brian Jackson
* sbinix
* rsteube
* neeasade
* jnphilipp
* Yannis Rohloff
* Tobias Patzl
* Stefan Tatschner
* Samuel Loury
* Peter Michely
* Panashe M. Fundira
* Lucas Hoffmann
* Larry Hynes
* Kirill A. Shutemov
* Johannes Altmanninger
* Jeremy Kaplan
* Ismail
* Iordanis Grigoriou
* Edgar Hipp
* Daryl Finlay
* arza
* adam
* Samir Benmendil
* Regina Hug
* Penaz
* Matthias Lisin
* Mathias Fussenegger
* Marcelo Santos
* Marcel Schilling
* 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
* Steve Peak
* Sorokin Alexei
* Simon Désaulniers
* Rok Mandeljc
* Noah Huesser
* Moez Bouhlel
* MikeinRealLife
* Lazlow Carmichael
* Kevin Wang
* Ján Kobezda
* Justin Partain
* Johannes Martinsson
* Jean-Christophe Petkovich
* Helen Sherwood-Taylor
* HalosGhost
* Gregor Pohl
* Eivind Uggedal
* Dietrich Daroch
* Derek Sivers
* Daniel Lu
* Daniel Jakots
* Arseniy Seroka
* Anton Grensjö
* Andy Balaam
* Andreas Fischer
* Amos Bird
* 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
-------

View File

@ -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
@ -1565,6 +1565,7 @@ These commands are mainly intended for debugging. They are hidden if qutebrowser
|<<debug-clear-ssl-errors,debug-clear-ssl-errors>>|Clear remembered SSL error answers.
|<<debug-console,debug-console>>|Show the debugging console.
|<<debug-crash,debug-crash>>|Crash for debugging purposes.
|<<debug-dump-history,debug-dump-history>>|Dump the history to a file in the old pre-SQL format.
|<<debug-dump-page,debug-dump-page>>|Dump the current page's content to a file.
|<<debug-log-capacity,debug-log-capacity>>|Change the number of log lines to be stored in RAM.
|<<debug-log-filter,debug-log-filter>>|Change the log filter for console logging.
@ -1599,6 +1600,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'+

View File

@ -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
@ -1067,11 +1067,15 @@ Default: +pass:[white]+
== colors.tabs.selected.even.bg
Background color of selected even tabs.
<<<<<<< HEAD
Default: +pass:[black]+
[[colors.tabs.selected.even.fg]]
== colors.tabs.selected.even.fg
Foreground color of selected even tabs.
=======
Valid values:
>>>>>>> upstream/master
Default: +pass:[white]+
@ -1163,7 +1167,7 @@ Default: +pass:[%Y-%m-%d]+
How many URLs to show in the web history.
0: no history / -1: unlimited
Default: +pass:[1000]+
Default: +pass:[-1]+
[[confirm_quit]]
== confirm_quit

196
doc/notes
View File

@ -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 <space><homerow keys> 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 <tautolog_AT_gmail.com>
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

View File

@ -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].

View File

@ -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

View File

@ -41,7 +41,7 @@ a = Analysis(['../qutebrowser/__main__.py'],
pathex=['misc'],
binaries=None,
datas=get_data_files(),
hiddenimports=['PyQt5.QtOpenGL'],
hiddenimports=['PyQt5.QtOpenGL', 'PyQt5._QOpenGLFunctions_2_0'],
hookspath=[],
runtime_hooks=[],
excludes=['tkinter'],

View File

@ -1,9 +1,9 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
certifi==2017.4.17
certifi==2017.7.27.1
chardet==3.0.4
codecov==2.0.9
coverage==4.4.1
idna==2.5
requests==2.18.1
urllib3==1.21.1
requests==2.18.2
urllib3==1.22

View File

@ -3,7 +3,7 @@
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.2
flake8-deprecated==1.2.1
flake8-docstrings==1.0.3 # rq.filter: < 1.1.0
flake8-future-import==0.4.3
flake8-mock==0.3
@ -11,7 +11,7 @@ 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-tidy-imports==1.1.0
flake8-tuple==0.2.13
mccabe==0.6.1
packaging==16.8

View File

@ -3,6 +3,6 @@
appdirs==1.4.3
packaging==16.8
pyparsing==2.2.0
setuptools==36.0.1
setuptools==36.2.5
six==1.10.0
wheel==0.29.0

View File

@ -1,7 +1,7 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
-e git+https://github.com/PyCQA/astroid.git#egg=astroid
certifi==2017.4.17
certifi==2017.7.27.1
chardet==3.0.4
github3.py==0.9.6
idna==2.5
@ -10,9 +10,9 @@ 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.18.1
requests==2.18.2
six==1.10.0
uritemplate==3.0.0
uritemplate.py==3.0.2
urllib3==1.21.1
urllib3==1.22
wrapt==1.10.10

View File

@ -1,7 +1,7 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
astroid==1.5.3
certifi==2017.4.17
certifi==2017.7.27.1
chardet==3.0.4
github3.py==0.9.6
idna==2.5
@ -10,9 +10,9 @@ lazy-object-proxy==1.3.1
mccabe==0.6.1
pylint==1.7.2
./scripts/dev/pylint_checkers
requests==2.18.1
requests==2.18.2
six==1.10.0
uritemplate==3.0.0
uritemplate.py==3.0.2
urllib3==1.21.1
urllib3==1.22
wrapt==1.10.10

View File

@ -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

View File

@ -5,35 +5,35 @@ cheroot==5.7.0
click==6.7
# colorama==0.3.9
coverage==4.4.1
decorator==4.0.11
decorator==4.1.2
EasyProcess==0.2.3
fields==5.0.0
Flask==0.12.2
glob2==0.5
httpbin==0.5.0
hunter==1.4.1
hypothesis==3.11.6
hypothesis==3.14.0
itsdangerous==0.24
# Jinja2==2.9.6
Mako==1.0.6
Mako==1.0.7
# MarkupSafe==1.0
parse==1.8.2
parse-type==0.3.4
py==1.4.34
pytest==3.1.2
pytest==3.1.3
pytest-bdd==2.18.2
pytest-benchmark==3.0.0
pytest-benchmark==3.1.1
pytest-catchlog==1.2.2
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-mock==1.6.2
pytest-qt==2.1.2
pytest-repeat==0.4.1
pytest-rerunfailures==2.2
pytest-travis-fold==1.2.0
pytest-xvfb==1.0.0
PyVirtualDisplay==0.2.1
six==1.10.0
vulture==0.14
vulture==0.21
Werkzeug==0.12.2

View File

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

View File

@ -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('<html>', '<html><head><title>%s</title></head>' % 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('<html>', '<html><head><title>%s</title></head>' % doc.title())
with codecs.open(tmpfile, 'w', 'utf-8') as target:
target.write('<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />')

View File

@ -1,12 +1,13 @@
[pytest]
addopts = --strict -rfEw --faulthandler-timeout=70 --instafail --pythonwarnings error
addopts = --strict -rfEw --faulthandler-timeout=70 --instafail --pythonwarnings error --benchmark-columns=Min,Max,Median
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.
@ -21,7 +22,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
@ -47,6 +48,7 @@ qt_log_ignore =
^QGeoclueMaster error creating GeoclueMasterClient\.
^Geoclue error: Process org\.freedesktop\.Geoclue\.Master exited with status 127
^Failed to create Geoclue client interface. Geoclue error: org\.freedesktop\.DBus\.Error\.Disconnected
^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

View File

@ -26,7 +26,7 @@ __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."

View File

@ -41,9 +41,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 config, websettings, configexc
from qutebrowser.config.parsers import keyconf
from qutebrowser.browser import (urlmarks, adblock, history, browsertab,
downloads)
from qutebrowser.browser.network import proxy
@ -52,10 +53,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, objects)
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.
@ -154,7 +155,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()
@ -400,10 +401,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()
@ -413,6 +412,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)
@ -449,9 +459,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.val.window.hide_wayland_decoration:
os.environ['QT_WAYLAND_DISABLE_WINDOWDECORATION'] = '1'
@ -462,23 +469,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.
@ -626,7 +616,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)
@ -760,7 +750,7 @@ class Quitter:
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.

View File

@ -479,11 +479,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
@ -491,6 +501,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

View File

@ -20,11 +20,12 @@
"""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
@ -38,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:
@ -227,19 +228,6 @@ class CommandDispatcher:
self._tabbed_browser.close_tab(tab)
tabbar.setSelectionBehaviorOnRemove(old_selection_behavior)
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()
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('count', count=True)
def tab_close(self, prev=False, next_=False, opposite=False,
@ -260,7 +248,7 @@ class CommandDispatcher:
close = functools.partial(self._tab_close, tab, prev,
next_, opposite)
self._tab_close_prompt_if_pinned(tab, force, close)
self._tabbed_browser.tab_close_prompt_if_pinned(tab, force, close)
@cmdutils.register(instance='command-dispatcher', scope='window',
name='tab-pin')
@ -280,13 +268,11 @@ class CommandDispatcher:
return
to_pin = not tab.data.pinned
tab_index = self._current_index() if count is None else count - 1
cmdutils.check_overflow(tab_index + 1, 'int')
self._tabbed_browser.set_tab_pinned(tab_index, to_pin)
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, related=False,
bg=False, tab=False, window=False, count=None, secure=False,
@ -438,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')
@ -515,7 +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(idx, curtab.data.pinned)
new_tabbed_browser.set_tab_pinned(newtab, curtab.data.pinned)
return newtab
@cmdutils.register(instance='command-dispatcher', scope='window')
@ -542,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)
@ -920,8 +913,9 @@ class CommandDispatcher:
if not force:
for i, tab in enumerate(self._tabbed_browser.widgets()):
if _to_close(i) and tab.data.pinned:
self._tab_close_prompt_if_pinned(
tab, force,
self._tabbed_browser.tab_close_prompt_if_pinned(
tab,
force,
lambda: self.tab_only(
prev=prev, next_=next_, force=True))
return
@ -1016,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.
@ -1032,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(
@ -1167,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:
@ -1241,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.
@ -1260,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.
@ -1323,7 +1315,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_load(self, url, tab=False, bg=False, window=False,
delete=False):
"""Load a bookmark.
@ -1345,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.
@ -1450,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):
@ -1519,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.
@ -1728,7 +1730,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
@ -2159,6 +2162,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)))

View File

@ -181,6 +181,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."""

View File

@ -19,214 +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.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):
@ -246,12 +114,17 @@ 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):
@ -285,17 +158,130 @@ class WebHistory(QObject):
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://.com/',
'http://www..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):
@ -304,8 +290,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:

View File

@ -412,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:
@ -428,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

View File

@ -26,7 +26,6 @@ Module attributes:
import json
import os
import sys
import time
import urllib.parse
import datetime
@ -185,88 +184,36 @@ def qute_bookmarks(_url):
return 'text/html', html
def history_data(start_time): # noqa
"""Return history data
def history_data(start_time, offset=None):
"""Return history data.
Arguments:
start_time -- select history starting from this timestamp.
start_time: select history starting from this timestamp.
offset: number of items to skip
"""
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.
"""
history = objreg.get('web-history').history_dict.values()
if reverse:
history = reversed(history)
# when history_dict is not reversed, we need to keep track of last item
# so that we can yield its atime
last_item = None
# 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)
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)}
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 list(history)
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."""
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")
@ -274,7 +221,7 @@ def qute_history(url):
except ValueError as e:
raise QuteSchemeError("Query parameter start_time is invalid", e)
return 'text/html', json.dumps(history_data(start_time))
return 'text/html', json.dumps(history_data(start_time, offset))
else:
if (
config.val.content.javascript.enabled and
@ -306,9 +253,9 @@ def qute_history(url):
start_time = time.mktime(next_date.timetuple()) - 1
history = [
(i["url"], i["title"],
datetime.datetime.fromtimestamp(i["time"]/1000),
datetime.datetime.fromtimestamp(i["time"]),
QUrl(i["url"]).host())
for i in history_data(start_time) if "next" not in i
for i in history_data(start_time)
]
return 'text/html', jinja.render(

View File

@ -77,13 +77,9 @@ class UrlMarkManager(QObject):
Signals:
changed: Emitted when anything changed.
added: Emitted when a new quickmark/bookmark was added.
removed: Emitted when an existing quickmark/bookmark was removed.
"""
changed = pyqtSignal()
added = pyqtSignal(str, str)
removed = pyqtSignal(str)
def __init__(self, parent=None):
"""Initialize and read quickmarks."""
@ -121,7 +117,6 @@ class UrlMarkManager(QObject):
"""
del self.marks[key]
self.changed.emit()
self.removed.emit(key)
class QuickmarkManager(UrlMarkManager):
@ -133,7 +128,6 @@ class QuickmarkManager(UrlMarkManager):
- self.marks maps names to URLs.
- changed gets emitted with the name as first argument and the URL as
second argument.
- removed gets emitted with the name as argument.
"""
def _init_lineparser(self):
@ -193,7 +187,6 @@ class QuickmarkManager(UrlMarkManager):
"""Really set the quickmark."""
self.marks[name] = url
self.changed.emit()
self.added.emit(name, url)
log.misc.debug("Added quickmark {} for {}".format(name, url))
if name in self.marks:
@ -243,7 +236,6 @@ class BookmarkManager(UrlMarkManager):
- self.marks maps URLs to titles.
- changed gets emitted with the URL as first argument and the title as
second argument.
- removed gets emitted with the URL as argument.
"""
def _init_lineparser(self):
@ -295,5 +287,4 @@ class BookmarkManager(UrlMarkManager):
else:
self.marks[urlstr] = title
self.changed.emit()
self.added.emit(title, urlstr)
return True

View File

@ -28,7 +28,9 @@ Module attributes:
"""
import os
import logging
import sys
import ctypes
import ctypes.util
from PyQt5.QtGui import QFont
from PyQt5.QtWebEngineWidgets import (QWebEngineSettings, QWebEngineProfile,
@ -203,12 +205,10 @@ 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
from OpenGL import GL # pylint: disable=unused-variable
# WORKAROUND for
# https://bugs.launchpad.net/ubuntu/+source/python-qt4/+bug/941826
if sys.platform == 'linux':
ctypes.CDLL(ctypes.util.find_library("GL"), mode=ctypes.RTLD_GLOBAL)
_init_profiles()

View File

@ -51,12 +51,13 @@ def init():
global _qute_scheme_handler
app = QApplication.instance()
software_rendering = os.environ.get('LIBGL_ALWAYS_SOFTWARE') == '1'
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 "
"LIBGL_ALWAYS_SOFTWARE is set as environment variable).")
"QT_XCB_FORCE_SOFTWARE_OPENGL is set as environment variable).")
log.init.debug("Initializing qute://* handler...")
_qute_scheme_handler = webenginequtescheme.QuteSchemeHandler(parent=app)
@ -406,18 +407,18 @@ 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):
if not qtutils.version_check('5.9'):
# WORKAROUND for
@ -611,6 +612,7 @@ class WebEngineTab(browsertab.AbstractTab):
def shutdown(self):
self.shutting_down.emit()
self.action.exit_fullscreen()
if qtutils.version_check('5.8', exact=True):
# WORKAROUND for
# https://bugreports.qt.io/browse/QTBUG-58563
@ -711,7 +713,8 @@ class WebEngineTab(browsertab.AbstractTab):
@pyqtSlot()
def _on_load_started(self):
"""Clear search when a new load is started if needed."""
if qtutils.version_check('5.9'):
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()

View File

@ -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):

View File

@ -32,7 +32,6 @@ 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.utils import qtutils, objreg, usertypes, utils, log, debug
@ -502,18 +501,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)

View File

@ -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):
@ -38,6 +38,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, parent=None):
@ -50,6 +51,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):
@ -60,37 +62,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.
@ -107,8 +80,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:
@ -117,14 +90,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.
@ -239,6 +209,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()
@ -247,13 +218,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.

View File

@ -198,8 +198,9 @@ class CompletionItemDelegate(QStyledItemDelegate):
self._doc.setDefaultStyleSheet(template.render(conf=config.val))
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'<span class="highlight">\g<0></span>'
text = re.sub(re.escape(pattern).replace(r'\ ', r'|'),

View File

@ -28,8 +28,7 @@ from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QItemSelectionModel, QSize
from qutebrowser.config import config
from qutebrowser.completion import completiondelegate
from qutebrowser.completion.models import base
from qutebrowser.utils import utils, usertypes
from qutebrowser.utils import utils, usertypes, 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,12 +107,10 @@ class CompletionView(QTreeView):
def __init__(self, win_id, parent=None):
super().__init__(parent)
self.pattern = ''
self._win_id = win_id
# FIXME handle new aliases.
# config.instance.changed.connect(self.init_command_completion)
config.instance.changed.connect(self._on_config_changed)
self._column_widths = base.BaseCompletionModel.COLUMN_WIDTHS
self._active = False
self._delegate = completiondelegate.CompletionItemDelegate(self)
@ -148,8 +146,11 @@ class CompletionView(QTreeView):
def _resize_columns(self):
"""Resize the completion columns based on column_widths."""
if self.model() is None:
return
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
@ -252,6 +253,10 @@ class CompletionView(QTreeView):
selmodel.setCurrentIndex(
idx, QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows)
# if the last item is focused, try to fetch more
if idx.row() == self.model().rowCount(idx.parent()) - 1:
self.expandAll()
count = self.model().count()
if count == 0:
self.hide()
@ -260,47 +265,50 @@ class CompletionView(QTreeView):
elif config.val.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.val.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.val.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 +353,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 +373,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)

View File

@ -1,130 +0,0 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-2017 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""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, completion):
"""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

View File

@ -0,0 +1,232 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2017 Ryan Roden-Corrent (rcorre) <ryan@rcorre.net>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""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()

View File

@ -17,145 +17,80 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""CompletionModels for the config."""
"""Functions that return config-related completion models."""
# FIXME:conf
# pylint: disable=no-member
from PyQt5.QtCore import pyqtSlot, Qt
from qutebrowser.config import config, configdata
from qutebrowser.utils import log, qtutils
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
config.instance.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
config.instance.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

View File

@ -0,0 +1,104 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2017 Ryan Roden-Corrent (rcorre) <ryan@rcorre.net>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""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)
while self.rowCount() < row:
self.fetchMore()
return True

View File

@ -1,162 +0,0 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2015-2017 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""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
from qutebrowser.utils import objreg, usertypes, log, debug
_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_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.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)
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]))

View File

@ -0,0 +1,92 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2017 Ryan Roden-Corrent (rcorre) <ryan@rcorre.net>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""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

View File

@ -17,255 +17,142 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Misc. CompletionModels."""
"""Functions that return miscellaneous completion models."""
# FIXME:conf
# pylint: disable=unused-argument
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."""
def delete(data):
"""Delete a quickmark from the completion menu."""
name = data[0]
quickmark_manager = objreg.get('quickmark-manager')
log.completion.debug('Deleting quickmark {}'.format(name))
quickmark_manager.delete(name)
# 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,
delete_func=delete))
return model
class BookmarkCompletionModel(base.BaseCompletionModel):
def bookmark():
"""A CompletionModel filled with all bookmarks."""
def delete(data):
"""Delete a bookmark from the completion menu."""
urlstr = data[0]
log.completion.debug('Deleting bookmark {}'.format(urlstr))
bookmark_manager = objreg.get('bookmark-manager')
bookmark_manager.delete(urlstr)
# 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,
delete_func=delete))
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_text = objreg.get('key-config').get_bindings_for('normal').get(key)
if cmd_text:
cmd_name = cmd_text.split(' ')[0]
cmd = cmdutils.cmd_dict.get(cmd_name)
data = [(cmd_text, 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=''):

View File

@ -1,191 +0,0 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-2017 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""A filtering/sorting base model for completions.
Contains:
CompletionFilterModel -- A QSortFilterProxyModel subclass for completions.
"""
import re
from PyQt5.QtCore import QSortFilterProxyModel, QModelIndex, Qt
from qutebrowser.utils import log, qtutils, debug
from qutebrowser.completion.models import base as completion
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)

View File

@ -17,176 +17,56 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""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.val.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)
config.instance.changed.connect(self._reformat_timestamps)
def _fmt_atime(self, atime):
"""Format an atime to a human-readable string."""
fmt = config.val.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.val.completion.web_history_max_items != 0:
hist_cat = histcategory.HistoryCategory(delete_func=_delete_history)
model.add_category(hist_cat)
return model

View File

@ -29,7 +29,7 @@ from qutebrowser.config import configdata, configexc, configtypes, configfiles
from qutebrowser.utils import utils, objreg, message, log, usertypes
from qutebrowser.misc import objects
from qutebrowser.commands import cmdexc, cmdutils
from qutebrowser.completion.models import configmodel
# An easy way to access the config from other code via config.val.foo
val = None
@ -229,6 +229,7 @@ class ConfigCommands:
self._keyconfig = keyconfig
@cmdutils.register(instance='config-commands', star_args_optional=True)
@cmdutils.argument('option', completion=configmodel.option)
@cmdutils.argument('win_id', win_id=True)
def set(self, win_id, option=None, *values, temp=False, print_=False):
"""Set an option.

View File

@ -70,6 +70,8 @@ the <span class="mono">qute://settings</span> page or caret browsing).</span>
{{ 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 %}

View File

@ -61,7 +61,7 @@ li {
{{ super() }}
function tryagain()
{
location.href = url;
location.href = "{{ url }}";
}
{% endblock %}

View File

@ -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";
@ -157,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++;
}
}
}
@ -182,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);
}
}

View File

@ -123,6 +123,7 @@ 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.
@ -217,6 +218,8 @@ 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:
@ -461,6 +464,8 @@ class MainWindow(QWidget):
tabs.tab_index_changed.connect(status.tabindex.on_tab_index_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.cur_fullscreen_requested.connect(self._on_fullscreen_requested)
@ -475,9 +480,12 @@ class MainWindow(QWidget):
@pyqtSlot(bool)
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()

View File

@ -99,8 +99,10 @@ class MessageView(QWidget):
@config.change_filter('messages.timeout')
def _set_clear_timer_interval(self):
"""Configure self._clear_timer according to the config."""
if config.val.messages.timeout != 0:
self._clear_timer.setInterval(config.val.messages.timeout)
interval = config.val.messages.timeout
if interval > 0:
interval *= min(5, len(self._messages))
self._clear_timer.setInterval(interval)
@pyqtSlot()
def clear_messages(self):
@ -127,12 +129,13 @@ class MessageView(QWidget):
widget = Message(level, text, replace=replace, parent=self)
self._vbox.addWidget(widget)
widget.show()
if config.val.messages.timeout != 0:
self._clear_timer.start()
self._messages.append(widget)
self._last_text = text
self.show()
self.update_geometry.emit()
if config.val.messages.timeout != 0:
self._set_clear_timer_interval()
self._clear_timer.start()
def mousePressEvent(self, e):
"""Clear messages when they are clicked on."""

View File

@ -0,0 +1,49 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2017 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""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))

View File

@ -25,8 +25,9 @@ from PyQt5.QtWidgets import QWidget, QHBoxLayout, QStackedLayout, QSizePolicy
from qutebrowser.browser import browsertab
from qutebrowser.config import config
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
@ -184,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)
@ -329,6 +333,7 @@ class StatusBar(QWidget):
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

View File

@ -23,7 +23,7 @@ import functools
import collections
from PyQt5.QtWidgets import QSizePolicy
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QTimer, QUrl, QSize
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QTimer, QUrl
from PyQt5.QtGui import QIcon
from qutebrowser.config import config
@ -239,6 +239,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.
@ -348,7 +361,7 @@ class TabbedBrowser(tabwidget.TabWidget):
newtab = self.tabopen(url, background=False, idx=idx)
newtab.history.deserialize(history_data)
self.set_tab_pinned(idx, pinned)
self.set_tab_pinned(newtab, pinned)
@pyqtSlot('QUrl', bool)
def openurl(self, url, newtab):
@ -372,7 +385,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):
@ -443,13 +457,7 @@ class TabbedBrowser(tabwidget.TabWidget):
# 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.
if self.tabBar().vertical:
tab_size = QSize(self.width() - self.tabBar().width(),
self.height())
else:
tab_size = QSize(self.width(),
self.height() - self.tabBar().height())
tab.resize(tab_size)
tab.resize(self.currentWidget().size())
self.tab_index_changed.emit(self.currentIndex(), self.count())
else:
self.setCurrentWidget(tab)
@ -705,12 +713,16 @@ class TabbedBrowser(tabwidget.TabWidget):
}
msg = messages[status]
def show_error_page(html):
tab.set_html(html)
log.webview.error(msg)
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)
QTimer.singleShot(0, lambda: tab.set_html(error_page))
url=url_string, error=msg, icon='')
QTimer.singleShot(100, lambda: show_error_page(error_page))
log.webview.error(msg)
else:
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-58698

View File

@ -26,7 +26,7 @@ from PyQt5.QtCore import (pyqtSignal, pyqtSlot, Qt, QSize, QRect, QPoint,
QTimer, QUrl)
from PyQt5.QtWidgets import (QTabWidget, QTabBar, QSizePolicy, QCommonStyle,
QStyle, QStylePainter, QStyleOptionTab,
QStyleFactory)
QStyleFactory, QWidget)
from PyQt5.QtGui import QIcon, QPalette, QColor
from qutebrowser.utils import qtutils, objreg, utils, usertypes, log
@ -94,17 +94,18 @@ class TabWidget(QTabWidget):
bar.set_tab_data(idx, 'indicator-color', color)
bar.update(bar.tabRect(idx))
def set_tab_pinned(self, idx, pinned, *, loading=False):
def set_tab_pinned(self, tab: QWidget,
pinned: bool, *, loading: bool = False) -> None:
"""Set the tab status as pinned.
Args:
idx: The tab index.
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()
tab = self.widget(idx)
idx = self.indexOf(tab)
# Only modify pinned_count if we had a change
# always modify pinned_count if we are loading
@ -487,14 +488,10 @@ 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():
# 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:
try:
pinned = self.tab_data(index, 'pinned')
@ -522,13 +519,13 @@ class TabBar(QTabBar):
width = no_pinned_width / (self.count() - self.pinned_count)
else:
# If we *do* have enough space, tabs should 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
# 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.
# 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:
@ -540,6 +537,10 @@ class TabBar(QTabBar):
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.
width = max(width, minimum_size.width())
size = QSize(width, height)
qtutils.ensure_valid(size)
return size
@ -750,6 +751,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):

View File

@ -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)

View File

@ -337,12 +337,12 @@ def check_libraries(backend):
"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")
modules['OpenGL'] = _missing_str("PyOpenGL")
else:
assert backend == 'webkit'
modules['PyQt5.QtWebKit'] = _missing_str("PyQt5.QtWebKit")

View File

@ -154,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()

View File

@ -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:

View File

@ -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

View File

@ -23,14 +23,15 @@ import os
import os.path
import sip
from PyQt5.QtCore import pyqtSignal, QUrl, QObject, QPoint, QTimer
from PyQt5.QtCore import QUrl, QObject, QPoint, QTimer
from PyQt5.QtWidgets import QApplication
import yaml
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
@ -101,14 +102,8 @@ class SessionManager(QObject):
closed.
_current: The name of the currently loaded session, or None.
did_load: Set when a session was loaded.
Signals:
update_completion: Emitted when the session completion should get
updated.
"""
update_completion = pyqtSignal()
def __init__(self, base_path, parent=None):
super().__init__(parent)
self._current = None
@ -297,8 +292,7 @@ class SessionManager(QObject):
utils.yaml_dump(data, f)
except (OSError, UnicodeEncodeError, yaml.YAMLError) as e:
raise SessionError(e)
else:
self.update_completion.emit()
if load_next_time:
state_config = objreg.get('state-config')
state_config['general']['session'] = name
@ -401,7 +395,7 @@ class SessionManager(QObject):
tab_to_focus = i
if new_tab.data.pinned:
tabbed_browser.set_tab_pinned(
i, new_tab.data.pinned, loading=True)
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):
@ -419,7 +413,6 @@ class SessionManager(QObject):
os.remove(path)
except OSError as e:
raise SessionError(e)
self.update_completion.emit()
def list_sessions(self):
"""Get a list of all session names."""
@ -431,7 +424,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.
@ -459,7 +452,7 @@ class SessionManager(QObject):
win.close()
@cmdutils.register(instance='session-manager')
@cmdutils.argument('name', completion=usertypes.Completion.sessions)
@cmdutils.argument('name', completion=miscmodels.session)
@cmdutils.argument('win_id', win_id=True)
@cmdutils.argument('with_private', flag='p')
def session_save(self, name: str = default, current=False, quiet=False,
@ -498,7 +491,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.

256
qutebrowser/misc/sql.py Normal file
View File

@ -0,0 +1,256 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2016-2017 Ryan Roden-Corrent (rcorre) <ryan@rcorre.net>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""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

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -28,7 +28,6 @@ import os.path
import collections
import functools
import contextlib
import itertools
import socket
import shlex
@ -378,8 +377,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([
@ -742,25 +741,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():

View File

@ -45,7 +45,7 @@ except ImportError: # pragma: no cover
import qutebrowser
from qutebrowser.utils import log, utils, standarddir, usertypes, qtutils
from qutebrowser.misc import objects, earlyinit
from qutebrowser.misc import objects, earlyinit, sql
from qutebrowser.browser import pdfjs
@ -186,7 +186,6 @@ def _module_versions():
('yaml', ['__version__']),
('cssutils', ['__version__']),
('typing', []),
('OpenGL', ['__version__']),
('PyQt5.QtWebEngineWidgets', []),
('PyQt5.QtWebKitWidgets', []),
])
@ -326,11 +325,11 @@ def version():
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()

View File

@ -7,4 +7,3 @@ MarkupSafe==1.0
Pygments==2.2.0
pyPEG2==2.15.2
PyYAML==3.12
PyOpenGL==3.1.0

View File

@ -280,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')

View File

@ -64,7 +64,7 @@ def call_tox(toxenv, *args, python=sys.executable):
env['PYTHON'] = python
env['PATH'] = os.environ['PATH'] + os.pathsep + os.path.dirname(python)
subprocess.check_call(
[sys.executable, '-m', 'tox', '-v', '-e', toxenv] + list(args),
[sys.executable, '-m', 'tox', '-vv', '-e', toxenv] + list(args),
env=env)
@ -92,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
@ -109,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',
@ -122,37 +125,45 @@ 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")
# 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):
@ -167,6 +178,7 @@ def patch_windows(out_dir):
def build_windows():
"""Build windows executables/setups."""
utils.print_title("Updating 3rdparty content")
# Currently disabled because QtWebEngine has no pdfjs support
# update_3rdparty.run(ace=False, pdfjs=True, fancy_dmg=False)
utils.print_title("Building Windows binaries")
@ -203,8 +215,8 @@ def build_windows():
'/DVERSION={}'.format(qutebrowser.__version__),
'misc/qutebrowser.nsi'])
name_32 = 'qutebrowser-{}-win32.msi'.format(qutebrowser.__version__)
name_64 = 'qutebrowser-{}-amd64.msi'.format(qutebrowser.__version__)
name_32 = 'qutebrowser-{}-win32.exe'.format(qutebrowser.__version__)
name_64 = 'qutebrowser-{}-amd64.exe'.format(qutebrowser.__version__)
artifacts += [
(os.path.join('dist', name_32),
@ -280,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.
@ -290,9 +310,7 @@ 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('qutebrowser', 'qutebrowser')
@ -329,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
@ -342,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

View File

@ -51,9 +51,9 @@ PERFECT_FILES = [
'browser/webkit/cache.py'),
('tests/unit/browser/webkit/test_cookies.py',
'browser/webkit/cookies.py'),
('tests/unit/browser/webkit/test_history.py',
('tests/unit/browser/test_history.py',
'browser/history.py'),
('tests/unit/browser/webkit/test_history.py',
('tests/unit/browser/test_history.py',
'browser/webkit/webkithistory.py'),
('tests/unit/browser/webkit/http/test_http.py',
'browser/webkit/http.py'),
@ -117,6 +117,8 @@ PERFECT_FILES = [
'mainwindow/statusbar/textbase.py'),
('tests/unit/mainwindow/statusbar/test_url.py',
'mainwindow/statusbar/url.py'),
('tests/unit/mainwindow/statusbar/test_backforward.py',
'mainwindow/statusbar/backforward.py'),
('tests/unit/mainwindow/test_messageview.py',
'mainwindow/messageview.py'),
@ -155,9 +157,11 @@ PERFECT_FILES = [
'utils/javascript.py'),
('tests/unit/completion/test_models.py',
'completion/models/base.py'),
('tests/unit/completion/test_sortfilter.py',
'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'),
]

View File

@ -109,7 +109,7 @@ elif [[ $TRAVIS_OS_NAME == osx ]]; then
exit 0
fi
pyqt_pkgs="python3-pyqt5 python3-pyqt5.qtquick python3-pyqt5.qtwebkit"
pyqt_pkgs="python3-pyqt5 python3-pyqt5.qtquick python3-pyqt5.qtwebkit python3-pyqt5.qtsql libqt5sql5-sqlite"
pip_install pip
pip_install -r misc/requirements/requirements-tox.txt

View File

@ -4,7 +4,6 @@ if [[ $DOCKER ]]; then
docker run --privileged -v $PWD:/outside -e QUTE_BDD_WEBENGINE=$QUTE_BDD_WEBENGINE -e DOCKER=$DOCKER -e CI=$CI qutebrowser/travis:$DOCKER
else
args=()
[[ $TESTENV == docs ]] && args=('--no-authors')
[[ $TRAVIS_OS_NAME == osx ]] && args=('--qute-bdd-webengine' '--no-xvfb')
tox -e $TESTENV -- "${args[@]}"

View File

@ -89,6 +89,12 @@ def whitelist_generator():
# vulture doesn't notice the hasattr() and thus thinks netrc_used is unused
# in NetworkManager.on_authentication_required
yield 'PyQt5.QtNetwork.QNetworkReply.netrc_used'
yield 'qutebrowser.browser.downloads.last_used_directory'
yield 'PaintContext.clip' # from completiondelegate.py
yield 'logging.LogRecord.log_color' # from logging.py
yield 'scripts.utils.use_color' # from asciidoc2html.py
for attr in ['pyeval_output', 'log_clipboard', 'fake_clipboard']:
yield 'qutebrowser.misc.utilcmds.' + attr
for attr in ['fileno', 'truncate', 'closed', 'readable']:
yield 'qutebrowser.utils.qtutils.PyQIODevice.' + attr
@ -123,7 +129,7 @@ def filter_func(item):
True if the missing function should be filtered/ignored, False
otherwise.
"""
return bool(re.match(r'[a-z]+[A-Z][a-zA-Z]+', str(item)))
return bool(re.match(r'[a-z]+[A-Z][a-zA-Z]+', item.name))
def report(items):
@ -137,7 +143,7 @@ def report(items):
relpath = os.path.relpath(item.filename)
path = relpath if not relpath.startswith('..') else item.filename
output.append("{}:{}: Unused {} '{}'".format(path, item.lineno,
item.typ, item))
item.typ, item.name))
return output

View File

@ -26,7 +26,6 @@ import shutil
import os.path
import inspect
import subprocess
import collections
import tempfile
import argparse
@ -43,7 +42,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()
@ -415,32 +414,6 @@ def generate_settings(filename):
_generate_setting_option(f, opt)
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',
'Spreadyy': 'sandrosc',
}
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.
@ -487,12 +460,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
@ -538,9 +505,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()

18
scripts/open_url_in_instance.sh Executable file
View File

@ -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

View File

@ -52,8 +52,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),

View File

@ -149,7 +149,7 @@ def pytest_collection_modifyitems(config, items):
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'),
]

View File

@ -0,0 +1,8 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -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
@ -60,43 +60,13 @@ Feature: Using completion
And I run :command-accept
Then the error "Session hello not found!" should be shown
# FIXME:conf
Scenario: Using option completion
When I run :set-cmd-text -s :set colors
Then the completion model should be option
# Scenario: Using option completion
# When I run :set-cmd-text -s :set colors
# Then the completion model should be SettingOptionCompletionModel
# Scenario: Using value completion
# When I run :set-cmd-text -s :set colors statusbar.bg
# Then the completion model should be SettingValueCompletionModel
Scenario: Updating the completion in realtime
Given I have a fresh instance
And I set completion.quick 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)
# FIXME:conf
# Scenario: Updating the value completion in realtime
# Given I set colors.statusbar.normal.bg to green
# When I run :set-cmd-text -s :set colors.statusbar.normal.bg
# And I set colors.statusbar.normal.bg to yellow
# And I run :completion-item-focus next
# And I run :completion-item-focus next
# And I set colors.statusbar.normal.bg to red
# And I run :command-accept
# Then the option colors.statusbar.normal.bg should be set to yellow
Scenario: Using value completion
When I run :set-cmd-text -s :set colors statusbar.bg
Then the completion model should be value
Scenario: Deleting an open tab via the completion
Given I have a fresh instance

View File

@ -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.

View File

@ -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 downloads.location.prompt to false
And I open data/downloads/downloads.html
@ -579,7 +593,6 @@ 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 downloads.location.prompt to false
And I run :download http://localhost:(port)/data/downloads/download.bin --dest (tmpdir)/downloads/unwritable
@ -637,7 +650,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/

View File

@ -243,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

View File

@ -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
@ -61,32 +61,32 @@ Feature: Page history
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 file should contain:
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 file should contain:
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

View File

@ -278,7 +278,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...
@ -532,3 +532,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
## Other
Scenario: Open qute://version
When I open qute://version
Then the page should contain the plaintext "Version info"

View File

@ -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

View File

@ -60,8 +60,3 @@ Feature: :spawn
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
@posix
Scenario: Running :spawn -d with userscript that expects the stdin getting closed
When I run :spawn -d -u (testdata)/userscripts/stdinclose.py
Then the message "stdin closed" should be shown

View File

@ -1075,6 +1075,16 @@ Feature: Tab management
- 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

View File

@ -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)

View File

@ -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')

View File

@ -17,36 +17,29 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
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, '')

View File

@ -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 content.javascript.log to info
And I open data/paste_primary.html

View File

@ -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':

View File

@ -163,6 +163,7 @@ def test_optimize(request, quteproc_new, capfd, level):
@pytest.mark.not_frozen
@pytest.mark.flaky # Fails sometimes with empty output...
def test_version(request):
"""Test invocation with --version argument."""
args = ['-m', 'qutebrowser', '--version'] + _base_args(request.config)

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