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__) # (for pytest's __tracebackhide__)
# F401: Unused import # F401: Unused import
# N802: function name should be lowercase # N802: function name should be lowercase
# N806: variable in function should be lowercase
# P101: format string does contain unindexed parameters # P101: format string does contain unindexed parameters
# P102: docstring does contain unindexed parameters # P102: docstring does contain unindexed parameters
# P103: other string does contain unindexed parameters # P103: other string does contain unindexed parameters
@ -38,7 +39,7 @@ putty-ignore =
/# pragma: no mccabe/ : +C901 /# pragma: no mccabe/ : +C901
tests/*/test_*.py : +D100,D101,D401 tests/*/test_*.py : +D100,D101,D401
tests/conftest.py : +F403 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/helpers/fixtures.py : +N806
tests/unit/browser/webkit/http/test_content_disposition.py : +D400 tests/unit/browser/webkit/http/test_content_disposition.py : +D400
scripts/dev/ci/appveyor_install.py : +FI53 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 language: python
python: 3.6 python: 3.6
env: TESTENV=py36-pyqt571 env: TESTENV=py36-pyqt571
- os: linux
language: python
python: 3.6
env: TESTENV=py36-pyqt58
- os: linux - os: linux
language: python language: python
python: 3.5 python: 3.5
env: TESTENV=py35-pyqt58 env: TESTENV=py35-pyqt59
- os: linux - os: linux
language: python language: python
python: 3.6 python: 3.6
env: TESTENV=py36-pyqt58 env: TESTENV=py36-pyqt59
- os: osx - os: osx
env: TESTENV=py36 OSX=elcapitan env: TESTENV=py36 OSX=elcapitan
osx_image: xcode7.3 osx_image: xcode7.3

View File

@ -14,9 +14,69 @@ This project adheres to http://semver.org/[Semantic Versioning].
// `Fixed` for any bug fixes. // `Fixed` for any bug fixes.
// `Security` to invite users to upgrade in case of vulnerabilities. // `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 New dependencies
~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~
@ -28,7 +88,10 @@ New dependencies
Added 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 - New "pinned tabs" feature, with a new `:tab-pin` command (bound
to `<Ctrl-p>` by default). to `<Ctrl-p>` by default).
- (QtWebEngine) Implemented `:follow-selected`. - (QtWebEngine) Implemented `:follow-selected`.
@ -45,6 +108,8 @@ Added
customize statusbar colors for private windows. customize statusbar colors for private windows.
- New `{private}` field displaying `[Private Mode]` for - New `{private}` field displaying `[Private Mode]` for
`ui -> window-title-format` and `tabs -> title-format`. `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 Changed
~~~~~~~ ~~~~~~~
@ -52,62 +117,51 @@ Changed
- To prevent elaborate phishing attacks, the Punycode version (`xn--*`) is now - To prevent elaborate phishing attacks, the Punycode version (`xn--*`) is now
shown in addition to the decoded version for international domain names shown in addition to the decoded version for international domain names
(IDN). (IDN).
- Private browsing is now implemented for QtWebEngine, and changed it's - Starting with legacy QtWebKit now shows a warning message.
behavior: The `general -> private-browsing` setting now only applies to newly *With the next release, support for it will be removed.*
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.
- The Windows releases are redone from scratch, which means: - The Windows releases are redone from scratch, which means:
- They now use the new QtWebEngine backend - They now use the new QtWebEngine backend
- The bundled Qt is updated from 5.5 to 5.9 - The bundled Qt is updated from 5.5 to 5.9
- The bundled Python is updated from 3.4 to 3.6 - The bundled Python is updated from 3.4 to 3.6
- They are now generated with PyInstaller instead of cx_Freeze - They are now generated with PyInstaller instead of cx_Freeze
- The installer is now generated using NSIS instead of being a MSI - 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. - 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` - The default level for `:messages` is now `info`, not `error`
- Trying to focus the currently focused tab with `:tab-focus` now focuses the - Trying to focus the currently focused tab with `:tab-focus` now focuses the
last viewed tab. 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 Fixed
~~~~~ ~~~~~
- The macOS .dmg is now built against Qt 5.9 which fixes various - The macOS .dmg is now built against Qt 5.9 which fixes various
important issues (such as not being able to type dead keys). 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. - Fixed crash with `:download` on PyQt 5.9.
- Cloning a page without history doesn't crash anymore. - Cloning a page without history doesn't crash anymore.
- When a download results in a HTTP error, it now shows the error correctly - 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 unbinding an unbound key in the key config.
- Fixed crash when using `:debug-log-filter` when `--filter` wasn't given on startup. - Fixed crash when using `:debug-log-filter` when `--filter` wasn't given on startup.
- Fixed crash with some invalid setting values. - 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. - Continuing a search after clearing it now works correctly.
- The tabbar and completion should now be more consistently and correctly - The tabbar and completion should now be more consistently and correctly
styled with various system styles. styled with various system styles.
@ -125,19 +178,27 @@ Fixed
- The validation for colors in stylesheets is now less strict, - The validation for colors in stylesheets is now less strict,
allowing for all valid Qt values. allowing for all valid Qt values.
- `data:` URLs now aren't added to the history anymore. - `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. - 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. - For some people, running some userscripts crashed - this should now be fixed.
- Various other rare crashes 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 - The settings documentation was truncated with v0.10.1 which should now be
fixed. fixed.
- Scrolling to an anchor in a background tab now works correctly, and javascript - Scrolling to an anchor in a background tab now works correctly, and javascript
gets the correct window size for background tabs. 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 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 - 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 - `: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 - `: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 Changed
~~~~~~~ ~~~~~~~
@ -484,7 +545,7 @@ Fixed
- Fix crash when pressing enter without a command - Fix crash when pressing enter without a command
- Adjust error message to point out QtWebEngine is unsupported with the OS - Adjust error message to point out QtWebEngine is unsupported with the OS
X .app currently. X .app currently.
- Hide Harfbuzz warning with the OS X .app - Hide Harfbuzz warning with the macOS .app
v0.8.0 v0.8.0
------ ------
@ -847,7 +908,7 @@ Fixed
- Fixed scrolling to the very left/right with `:scroll-perc`. - Fixed scrolling to the very left/right with `:scroll-perc`.
- Using an external editor should now work correctly with some funny chars - Using an external editor should now work correctly with some funny chars
(U+2028/U+2029/BOM). (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 upgrade from earlier config versions.
- Fixed crash when killing a running userscript. - Fixed crash when killing a running userscript.
- Fixed characters being passed through when shifted with - Fixed characters being passed through when shifted with
@ -922,7 +983,7 @@ Changed
- The completion widget doesn't show a border anymore. - The completion widget doesn't show a border anymore.
- The tabbar doesn't display ugly arrows anymore if there isn't enough space - The tabbar doesn't display ugly arrows anymore if there isn't enough space
for all tabs. 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. - Better support for Qt 5.5 and Python 3.5.
Fixed Fixed
@ -1033,7 +1094,7 @@ Fixed
- Fixed AssertionError when closing many windows quickly. - Fixed AssertionError when closing many windows quickly.
- Various fixes for deprecated key bindings and auto-migrations. - 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). - 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 crash when downloading a URL without filename (e.g. magnet links) via "Save as...".
- Fixed exception when starting qutebrowser with `:set` as argument. - Fixed exception when starting qutebrowser with `:set` as argument.
- Fixed horrible completion performance when the `shrink` option was set. - 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. - 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. - Various improvements to documentation, logging, and the crash reporter.
- Expand `~` to the users home directory with `:run-userscript`. - 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. - Add luakit-like `gt`/`gT` keybindings to cycle through tabs.
- Show default value for config values in the completion. - Show default value for config values in the completion.
- Clone tab icon, tab text and zoom level when cloning tabs. - 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. * `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] * The tests now use http://pytest.org/[pytest]
* Many new tests added * 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]. * Coverage recording via http://nedbatchelder.com/code/coverage/[coverage.py].
* New `--pdb-postmortem argument` to drop into the pdb debugger on exceptions. * 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. * 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 rare exception when a key is pressed shortly after opening a window
* Fix exception with certain invalid URLs like `http:foo:0` * 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` * Fix exception when a local files can't be read in `:adblock-update`
* Hide 2 more Qt warnings. * Hide 2 more Qt warnings.
* Add `!important` to hint CSS so websites don't override the hint look * 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`/`=`. * Set zoom to default instead of 100% with `:zoom`/`=`.
* Adjust page zoom if default zoom changed. * Adjust page zoom if default zoom changed.
* Force tabs to be focused on `:undo`. * 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. * Allow min-/maximizing of print preview on Windows.
* Various documentation improvements. * Various documentation improvements.
* Various other small improvements and cleanups. * Various other small improvements and cleanups.

View File

@ -5,6 +5,12 @@ The Compiler <mail@qutebrowser.org>
:data-uri: :data-uri:
:toc: :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! I `&lt;3` footnote:[Of course, that says `<3` in HTML.] contributors!
This document contains guidelines for contributing to qutebrowser, as well as 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 * https://github.com/qutebrowser/qutebrowser/labels/easy[Issues which should
be easy to solve] be easy to solve]
* https://github.com/qutebrowser/qutebrowser/labels/not%20code[Issues which * https://github.com/qutebrowser/qutebrowser/labels/component%3A%20docs[Documentation
require little/no coding] * issues which require little/no coding]
If you prefer C++ or Javascript to Python, see the relevant issues which involve If you prefer C++ or Javascript to Python, see the relevant issues which involve
work in those languages: work in those languages:
@ -682,8 +688,9 @@ qutebrowser release
* Add newest config to `tests/unit/config/old_configs` and update `test_upgrade_version` * Add newest config to `tests/unit/config/old_configs` and update `test_upgrade_version`
- `python -m qutebrowser --basedir conf :quit` - `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` - `rm -r conf`
- git add
- commit - commit
* Adjust `__version_info__` in `qutebrowser/__init__.py`. * Adjust `__version_info__` in `qutebrowser/__init__.py`.
* Update changelog (remove *(unreleased)*) * Update changelog (remove *(unreleased)*)
@ -698,8 +705,8 @@ qutebrowser release
as closed. as closed.
* Linux: Run `python3 scripts/dev/build_release.py --upload v0.$x.$y` * 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) * 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)
* OS X: Run `python3 scripts/dev/build_release.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) * 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 * Update `qutebrowser-git` PKGBUILD if dependencies/install changed
* Announce to qutebrowser and qutebrowser-announce mailinglist * 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 be useful to rebind escape to something else in passthrough mode only, to be
able to send an escape keypress to the website. 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 == Troubleshooting
Configuration not saved after modifying config.:: 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 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]. 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.:: My issue is not listed.::
If you experience any segfaults or crashes, you can report the issue in If you experience any segfaults or crashes, you can report the issue in
https://github.com/qutebrowser/qutebrowser/issues[the issue tracker] or 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: 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 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: 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 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>>. Then <<tox,install qutebrowser via tox>>.
On OS X On macOS
------- --------
Prebuilt binary 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 files from the
https://github.com/qutebrowser/qutebrowser/releases[release page]. https://github.com/qutebrowser/qutebrowser/releases[release page].

View File

@ -8,7 +8,7 @@ graft icons
graft doc/img graft doc/img
graft misc/apparmor graft misc/apparmor
graft misc/userscripts graft misc/userscripts
recursive-include scripts *.py recursive-include scripts *.py *.sh
include qutebrowser/utils/testfile include qutebrowser/utils/testfile
include qutebrowser/git-commit-id include qutebrowser/git-commit-id
include COPYING doc/* README.asciidoc CONTRIBUTING.asciidoc FAQ.asciidoc INSTALL.asciidoc CHANGELOG.asciidoc 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 See the https://github.com/qutebrowser/qutebrowser/releases[github releases
page] for available downloads (currently a source archive, and standalone page] for available downloads and the link:INSTALL.asciidoc[INSTALL] file for
packages as well as MSI installers for Windows). detailed instructions on how to get qutebrowser running on various platforms.
See link:INSTALL.asciidoc[INSTALL] for detailed instructions on how to get
qutebrowser running for various platforms.
Documentation 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 at mailto:qutebrowser-announce@lists.qutebrowser.org[] (the announcements also
get sent to the general qutebrowser@ list). 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 Contributions / Bugs
-------------------- --------------------
@ -98,27 +98,35 @@ Requirements
The following software and libraries are required to run qutebrowser: The following software and libraries are required to run qutebrowser:
* http://www.python.org/[Python] 3.4 or newer (3.5 recommended) * http://www.python.org/[Python] 3.4 or newer (3.6 recommended) - note that
* http://qt.io/[Qt] 5.2.0 or newer (5.9.0 recommended) support for Python 3.4
* QtWebKit (old or link:https://github.com/annulen/webkit/wiki[reloaded]/NG) or QtWebEngine 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 * 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] * https://pypi.python.org/pypi/setuptools/[pkg_resources/setuptools]
* http://fdik.org/pyPEG/[pyPEG2] * http://fdik.org/pyPEG/[pyPEG2]
* http://jinja.pocoo.org/[jinja2] * http://jinja.pocoo.org/[jinja2]
* http://pygments.org/[pygments] * http://pygments.org/[pygments]
* http://pyyaml.org/wiki/PyYAML[PyYAML] * 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] * http://cthedot.de/cssutils/[cssutils] (for an improved `:download --mhtml`
with QtWebKit)
To generate the documentation for the `:help` command, when using the git * On Windows, https://pypi.python.org/pypi/colorama/[colorama] for colored log
repository (rather than a release), http://asciidoc.org/[asciidoc] is needed. output.
* http://asciidoc.org/[asciidoc] to generate the documentation for the `:help`
On Windows, https://pypi.python.org/pypi/colorama/[colorama] is needed to command, when using the git repository (rather than a release).
display colored log output.
See link:INSTALL.asciidoc[INSTALL] for directions on how to install qutebrowser See link:INSTALL.asciidoc[INSTALL] for directions on how to install qutebrowser
and its dependencies. and its dependencies.
@ -142,219 +150,59 @@ get in touch!
Authors 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 Additionally, the following people have contributed graphics:
* 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:
* Jad/link:http://yelostudio.com[yelo] (new icon) * Jad/link:http://yelostudio.com[yelo] (new icon)
* WOFall (original icon) * WOFall (original icon)
* regines (key binding cheatsheet) * 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: Similar projects
----------------
* 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)
Many projects with a similar goal as qutebrowser exist.
Most of them were inspirations for qutebrowser in some way, thanks for that! 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 Active
problems and helpful hints: ~~~~~~
* http://eric-ide.python-projects.org/[eric5] / Detlev Offenbach * https://fanglingsu.github.io/vimb/[vimb] (C, GTK+ with WebKit2)
* https://code.google.com/p/devicenzo/[devicenzo] * https://luakit.github.io/luakit/[luakit] (C/Lua, GTK+ with WebKit2)
* portix * http://surf.suckless.org/[surf] (C, GTK+ with WebKit1/WebKit2)
* seir * http://www.uzbl.org/[uzbl] (C, GTK+ with WebKit1/WebKit2)
* nitroxleecher * 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]. * https://bitbucket.org/portix/dwb[dwb] (C, GTK+ with WebKit1,
* Everyone who had the patience to test qutebrowser before v0.1. https://bitbucket.org/portix/dwb/pull-requests/22/several-cleanups-to-increase-portability/diff[unmaintained] -
* Everyone triaging/fixing my bugs in the main inspiration for qutebrowser)
https://bugreports.qt.io/secure/Dashboard.jspa[Qt bugtracker] * http://sourceforge.net/p/vimprobable/wiki/Home/[vimprobable] (C, GTK+ with
* Everyone answering my questions on http://stackoverflow.com/[Stack Overflow] WebKit1)
and in IRC. * http://pwmt.org/projects/jumanji/[jumanji] (C, GTK+ with WebKit1)
* All the projects which were a great help while developing qutebrowser. * 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 License
------- -------

View File

@ -1,5 +1,5 @@
// DO NOT EDIT THIS FILE DIRECTLY! // DO NOT EDIT THIS FILE DIRECTLY!
// It is autogenerated from docstrings by running: // It is autogenerated by running:
// $ python3 scripts/dev/src2asciidoc.py // $ python3 scripts/dev/src2asciidoc.py
= Commands = 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-clear-ssl-errors,debug-clear-ssl-errors>>|Clear remembered SSL error answers.
|<<debug-console,debug-console>>|Show the debugging console. |<<debug-console,debug-console>>|Show the debugging console.
|<<debug-crash,debug-crash>>|Crash for debugging purposes. |<<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-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-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. |<<debug-log-filter,debug-log-filter>>|Change the log filter for console logging.
@ -1599,6 +1600,15 @@ Crash for debugging purposes.
==== positional arguments ==== positional arguments
* +'typ'+: either 'exception' or 'segfault'. * +'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]]
=== debug-dump-page === debug-dump-page
Syntax: +:debug-dump-page [*--plain*] 'dest'+ Syntax: +:debug-dump-page [*--plain*] 'dest'+

View File

@ -1,5 +1,5 @@
// DO NOT EDIT THIS FILE DIRECTLY! // DO NOT EDIT THIS FILE DIRECTLY!
// It is autogenerated from docstrings by running: // It is autogenerated by running:
// $ python3 scripts/dev/src2asciidoc.py // $ python3 scripts/dev/src2asciidoc.py
= Settings = Settings
@ -1067,11 +1067,15 @@ Default: +pass:[white]+
== colors.tabs.selected.even.bg == colors.tabs.selected.even.bg
Background color of selected even tabs. Background color of selected even tabs.
<<<<<<< HEAD
Default: +pass:[black]+ Default: +pass:[black]+
[[colors.tabs.selected.even.fg]] [[colors.tabs.selected.even.fg]]
== colors.tabs.selected.even.fg == colors.tabs.selected.even.fg
Foreground color of selected even tabs. Foreground color of selected even tabs.
=======
Valid values:
>>>>>>> upstream/master
Default: +pass:[white]+ Default: +pass:[white]+
@ -1163,7 +1167,7 @@ Default: +pass:[%Y-%m-%d]+
How many URLs to show in the web history. How many URLs to show in the web history.
0: no history / -1: unlimited 0: no history / -1: unlimited
Default: +pass:[1000]+ Default: +pass:[-1]+
[[confirm_quit]] [[confirm_quit]]
== 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. * Run `:adblock-update` to download adblock lists and activate adblocking.
* If you just cloned the repository, you'll need to run * If you just cloned the repository, you'll need to run
`scripts/asciidoc2html.py` to generate the documentation. `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 * Subscribe to
https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser[the mailinglist] or https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser[the mailinglist] or
https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser-announce[the announce-only mailinglist]. 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 Normal qutebrowser commands can be written to `$QUTE_FIFO` and will be
executed. 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. immediately.
On Windows, this is a regular file, and the commands in it will be executed as 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'], pathex=['misc'],
binaries=None, binaries=None,
datas=get_data_files(), datas=get_data_files(),
hiddenimports=['PyQt5.QtOpenGL'], hiddenimports=['PyQt5.QtOpenGL', 'PyQt5._QOpenGLFunctions_2_0'],
hookspath=[], hookspath=[],
runtime_hooks=[], runtime_hooks=[],
excludes=['tkinter'], excludes=['tkinter'],

View File

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

View File

@ -3,7 +3,7 @@
flake8==2.6.2 # rq.filter: < 3.0.0 flake8==2.6.2 # rq.filter: < 3.0.0
flake8-copyright==0.2.0 flake8-copyright==0.2.0
flake8-debugger==1.4.0 # rq.filter: != 2.0.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-docstrings==1.0.3 # rq.filter: < 1.1.0
flake8-future-import==0.4.3 flake8-future-import==0.4.3
flake8-mock==0.3 flake8-mock==0.3
@ -11,7 +11,7 @@ flake8-pep3101==1.0 # rq.filter: < 1.1
flake8-polyfill==1.0.1 flake8-polyfill==1.0.1
flake8-putty==0.4.0 flake8-putty==0.4.0
flake8-string-format==0.2.3 flake8-string-format==0.2.3
flake8-tidy-imports==1.0.6 flake8-tidy-imports==1.1.0
flake8-tuple==0.2.13 flake8-tuple==0.2.13
mccabe==0.6.1 mccabe==0.6.1
packaging==16.8 packaging==16.8

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py # This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt5==5.8.2 PyQt5==5.9
sip==4.19.2 sip==4.19.3

View File

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

View File

@ -1,3 +1,3 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py # 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. # 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: # Usage:
# :spawn --userscript readability # :spawn --userscript readability
# #
from __future__ import absolute_import from __future__ import absolute_import
import codecs, os import codecs, os
from readability.readability import Document
tmpfile=os.path.expanduser('~/.local/share/qutebrowser/userscripts/readability.html') tmpfile=os.path.expanduser('~/.local/share/qutebrowser/userscripts/readability.html')
if not os.path.exists(os.path.dirname(tmpfile)): if not os.path.exists(os.path.dirname(tmpfile)):
os.makedirs(os.path.dirname(tmpfile)) os.makedirs(os.path.dirname(tmpfile))
with codecs.open(os.environ['QUTE_HTML'], 'r', 'utf-8') as source: with codecs.open(os.environ['QUTE_HTML'], 'r', 'utf-8') as source:
doc = Document(source.read()) data = source.read()
content = doc.summary().replace('<html>', '<html><head><title>%s</title></head>' % doc.title())
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: with codecs.open(tmpfile, 'w', 'utf-8') as target:
target.write('<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />') target.write('<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />')

View File

@ -1,12 +1,13 @@
[pytest] [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 = markers =
gui: Tests using the GUI (e.g. spawning widgets) gui: Tests using the GUI (e.g. spawning widgets)
posix: Tests which only can run on a POSIX OS. posix: Tests which only can run on a POSIX OS.
windows: Tests which only can run on Windows. windows: Tests which only can run on Windows.
linux: Tests which only can run on Linux. linux: Tests which only can run on Linux.
osx: Tests which only can run on OS X. mac: Tests which only can run on macOS.
not_osx: Tests which can not run on OS X. not_mac: Tests which can not run on macOS.
not_frozen: Tests which can't be run if sys.frozen is True. not_frozen: Tests which can't be run if sys.frozen is True.
no_xvfb: Tests which can't be run with Xvfb. no_xvfb: Tests which can't be run with Xvfb.
frozen: Tests which can only be run if sys.frozen is True. 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_xfail: Tests failing with QtWebKit-NG
qtwebkit_ng_skip: Tests skipped with QtWebKit-NG qtwebkit_ng_skip: Tests skipped with QtWebKit-NG
qtwebengine_flaky: Tests which are flaky (and currently skipped) with QtWebEngine 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 js_prompt: Tests needing to display a javascript prompt
this: Used to mark tests during development this: Used to mark tests during development
no_invalid_lines: Don't fail on unparseable lines in end2end tests no_invalid_lines: Don't fail on unparseable lines in end2end tests
@ -47,6 +48,7 @@ qt_log_ignore =
^QGeoclueMaster error creating GeoclueMasterClient\. ^QGeoclueMaster error creating GeoclueMasterClient\.
^Geoclue error: Process org\.freedesktop\.Geoclue\.Master exited with status 127 ^Geoclue error: Process org\.freedesktop\.Geoclue\.Master exited with status 127
^Failed to create Geoclue client interface. Geoclue error: org\.freedesktop\.DBus\.Error\.Disconnected ^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\) ^QObject::connect: Cannot connect \(null\)::stateChanged\(QNetworkSession::State\) to QNetworkReplyHttpImpl::_q_networkSessionStateChanged\(QNetworkSession::State\)
^QXcbClipboard: Cannot transfer data, no data available ^QXcbClipboard: Cannot transfer data, no data available
^load glyph failed ^load glyph failed

View File

@ -26,7 +26,7 @@ __copyright__ = "Copyright 2014-2017 Florian Bruhin (The Compiler)"
__license__ = "GPL" __license__ = "GPL"
__maintainer__ = __author__ __maintainer__ = __author__
__email__ = "mail@qutebrowser.org" __email__ = "mail@qutebrowser.org"
__version_info__ = (0, 10, 1) __version_info__ = (0, 11, 0)
__version__ = '.'.join(str(e) for e in __version_info__) __version__ = '.'.join(str(e) for e in __version_info__)
__description__ = "A keyboard-driven, vim-like browser based on PyQt5." __description__ = "A keyboard-driven, vim-like browser based on PyQt5."

View File

@ -41,9 +41,10 @@ except ImportError:
import qutebrowser import qutebrowser
import qutebrowser.resources 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.commands import cmdutils, runners, cmdexc
from qutebrowser.config import config, websettings, configexc from qutebrowser.config import config, websettings, configexc
from qutebrowser.config.parsers import keyconf
from qutebrowser.browser import (urlmarks, adblock, history, browsertab, from qutebrowser.browser import (urlmarks, adblock, history, browsertab,
downloads) downloads)
from qutebrowser.browser.network import proxy from qutebrowser.browser.network import proxy
@ -52,10 +53,10 @@ from qutebrowser.browser.webkit.network import networkmanager
from qutebrowser.keyinput import macros from qutebrowser.keyinput import macros
from qutebrowser.mainwindow import mainwindow, prompt from qutebrowser.mainwindow import mainwindow, prompt
from qutebrowser.misc import (readline, ipc, savemanager, sessions, 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.misc import utilcmds # pylint: disable=unused-import
from qutebrowser.utils import (log, version, message, utils, qtutils, urlutils, 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. # 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('https', open_desktopservices_url)
QDesktopServices.setUrlHandler('qute', 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!") log.init.debug("Init done!")
crash_handler.raise_crashdlg() crash_handler.raise_crashdlg()
@ -400,10 +401,8 @@ def _init_modules(args, crash_handler):
log.init.debug("Initializing network...") log.init.debug("Initializing network...")
networkmanager.init() networkmanager.init()
if qtutils.version_check('5.8'): log.init.debug("Initializing proxy...")
# Otherwise we can only initialize it for QtWebKit because of crashes proxy.init()
log.init.debug("Initializing proxy...")
proxy.init()
log.init.debug("Initializing readline-bridge...") log.init.debug("Initializing readline-bridge...")
readline_bridge = readline.ReadlineBridge() readline_bridge = readline.ReadlineBridge()
@ -413,6 +412,17 @@ def _init_modules(args, crash_handler):
config.init(qApp) config.init(qApp)
save_manager.init_autosave() 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...") log.init.debug("Initializing web history...")
history.init(qApp) history.init(qApp)
@ -449,9 +459,6 @@ def _init_modules(args, crash_handler):
diskcache = cache.DiskCache(standarddir.cache(), parent=qApp) diskcache = cache.DiskCache(standarddir.cache(), parent=qApp)
objreg.register('cache', diskcache) objreg.register('cache', diskcache)
log.init.debug("Initializing completions...")
completionmodels.init()
log.init.debug("Misc initialization...") log.init.debug("Misc initialization...")
if config.val.window.hide_wayland_decoration: if config.val.window.hide_wayland_decoration:
os.environ['QT_WAYLAND_DISABLE_WINDOWDECORATION'] = '1' os.environ['QT_WAYLAND_DISABLE_WINDOWDECORATION'] = '1'
@ -462,23 +469,6 @@ def _init_modules(args, crash_handler):
browsertab.init() 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: class Quitter:
"""Utility class to quit/restart the QApplication. """Utility class to quit/restart the QApplication.
@ -626,7 +616,7 @@ class Quitter:
# Save the session if one is given. # Save the session if one is given.
if session is not None: if session is not None:
session_manager = objreg.get('session-manager') 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 # Open a new process and immediately shutdown the existing one
try: try:
args, cwd = self._get_restart_args(pages, session) args, cwd = self._get_restart_args(pages, session)
@ -760,7 +750,7 @@ class Quitter:
QTimer.singleShot(0, functools.partial(qApp.exit, status)) QTimer.singleShot(0, functools.partial(qApp.exit, status))
@cmdutils.register(instance='quitter', name='wq') @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): def save_and_quit(self, name=sessions.default):
"""Save open pages and quit. """Save open pages and quit.

View File

@ -479,11 +479,21 @@ class AbstractHistory:
def current_idx(self): def current_idx(self):
raise NotImplementedError raise NotImplementedError
def back(self): def back(self, count=1):
raise NotImplementedError 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): def forward(self, count=1):
raise NotImplementedError 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): def can_go_back(self):
raise NotImplementedError raise NotImplementedError
@ -491,6 +501,12 @@ class AbstractHistory:
def can_go_forward(self): def can_go_forward(self):
raise NotImplementedError raise NotImplementedError
def _item_at(self, i):
raise NotImplementedError
def _go_to_item(self, item):
raise NotImplementedError
def serialize(self): def serialize(self):
"""Serialize into an opaque format understood by self.deserialize.""" """Serialize into an opaque format understood by self.deserialize."""
raise NotImplementedError raise NotImplementedError

View File

@ -20,11 +20,12 @@
"""Command dispatcher for TabbedBrowser.""" """Command dispatcher for TabbedBrowser."""
import os import os
import sys
import os.path import os.path
import shlex import shlex
import functools 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.QtCore import Qt, QUrl, QEvent, QUrlQuery
from PyQt5.QtGui import QKeyEvent from PyQt5.QtGui import QKeyEvent
from PyQt5.QtPrintSupport import QPrintDialog, QPrintPreviewDialog from PyQt5.QtPrintSupport import QPrintDialog, QPrintPreviewDialog
@ -38,10 +39,10 @@ from qutebrowser.browser import (urlmarks, browsertab, inspector, navigate,
webelem, downloads) webelem, downloads)
from qutebrowser.keyinput import modeman from qutebrowser.keyinput import modeman
from qutebrowser.utils import (message, usertypes, log, qtutils, urlutils, from qutebrowser.utils import (message, usertypes, log, qtutils, urlutils,
objreg, utils, typing) objreg, utils, typing, debug)
from qutebrowser.utils.usertypes import KeyMode from qutebrowser.utils.usertypes import KeyMode
from qutebrowser.misc import editor, guiprocess from qutebrowser.misc import editor, guiprocess
from qutebrowser.completion.models import instances, sortfilter from qutebrowser.completion.models import urlmodel, miscmodels
class CommandDispatcher: class CommandDispatcher:
@ -227,19 +228,6 @@ class CommandDispatcher:
self._tabbed_browser.close_tab(tab) self._tabbed_browser.close_tab(tab)
tabbar.setSelectionBehaviorOnRemove(old_selection_behavior) 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.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('count', count=True) @cmdutils.argument('count', count=True)
def tab_close(self, prev=False, next_=False, opposite=False, def tab_close(self, prev=False, next_=False, opposite=False,
@ -260,7 +248,7 @@ class CommandDispatcher:
close = functools.partial(self._tab_close, tab, prev, close = functools.partial(self._tab_close, tab, prev,
next_, opposite) 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', @cmdutils.register(instance='command-dispatcher', scope='window',
name='tab-pin') name='tab-pin')
@ -280,13 +268,11 @@ class CommandDispatcher:
return return
to_pin = not tab.data.pinned to_pin = not tab.data.pinned
tab_index = self._current_index() if count is None else count - 1 self._tabbed_browser.set_tab_pinned(tab, to_pin)
cmdutils.check_overflow(tab_index + 1, 'int')
self._tabbed_browser.set_tab_pinned(tab_index, to_pin)
@cmdutils.register(instance='command-dispatcher', name='open', @cmdutils.register(instance='command-dispatcher', name='open',
maxsplit=0, scope='window') maxsplit=0, scope='window')
@cmdutils.argument('url', completion=usertypes.Completion.url) @cmdutils.argument('url', completion=urlmodel.url)
@cmdutils.argument('count', count=True) @cmdutils.argument('count', count=True)
def openurl(self, url=None, related=False, def openurl(self, url=None, related=False,
bg=False, tab=False, window=False, count=None, secure=False, bg=False, tab=False, window=False, count=None, secure=False,
@ -438,9 +424,18 @@ class CommandDispatcher:
message.error("Printing failed!") message.error("Printing failed!")
diag.deleteLater() diag.deleteLater()
def do_print():
"""Called when the dialog was closed."""
tab.printing.to_printer(diag.printer(), print_callback)
diag = QPrintDialog(tab) diag = QPrintDialog(tab)
diag.open(lambda: tab.printing.to_printer(diag.printer(), if sys.platform == 'darwin':
print_callback)) # 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', @cmdutils.register(instance='command-dispatcher', name='print',
scope='window') scope='window')
@ -515,7 +510,7 @@ class CommandDispatcher:
newtab.data.keep_icon = True newtab.data.keep_icon = True
newtab.history.deserialize(history) newtab.history.deserialize(history)
newtab.zoom.set_factor(curtab.zoom.factor()) 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 return newtab
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window')
@ -542,15 +537,13 @@ class CommandDispatcher:
else: else:
widget = self._current_widget() widget = self._current_widget()
for _ in range(count): try:
if forward: if forward:
if not widget.history.can_go_forward(): widget.history.forward(count)
raise cmdexc.CommandError("At end of history.")
widget.history.forward()
else: else:
if not widget.history.can_go_back(): widget.history.back(count)
raise cmdexc.CommandError("At beginning of history.") except browsertab.WebTabError as e:
widget.history.back() raise cmdexc.CommandError(e)
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('count', count=True) @cmdutils.argument('count', count=True)
@ -920,8 +913,9 @@ class CommandDispatcher:
if not force: if not force:
for i, tab in enumerate(self._tabbed_browser.widgets()): for i, tab in enumerate(self._tabbed_browser.widgets()):
if _to_close(i) and tab.data.pinned: if _to_close(i) and tab.data.pinned:
self._tab_close_prompt_if_pinned( self._tabbed_browser.tab_close_prompt_if_pinned(
tab, force, tab,
force,
lambda: self.tab_only( lambda: self.tab_only(
prev=prev, next_=next_, force=True)) prev=prev, next_=next_, force=True))
return return
@ -1016,7 +1010,7 @@ class CommandDispatcher:
self._open(url, tab, bg, window) self._open(url, tab, bg, window)
@cmdutils.register(instance='command-dispatcher', scope='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): def buffer(self, index):
"""Select tab by index or url/title best match. """Select tab by index or url/title best match.
@ -1032,11 +1026,10 @@ class CommandDispatcher:
for part in index_parts: for part in index_parts:
int(part) int(part)
except ValueError: except ValueError:
model = instances.get(usertypes.Completion.tab) model = miscmodels.buffer()
sf = sortfilter.CompletionFilterModel(source=model) model.set_pattern(index)
sf.set_pattern(index) if model.count() > 0:
if sf.count() > 0: index = model.data(model.first_item())
index = sf.data(sf.first_item())
index_parts = index.split('/', 1) index_parts = index.split('/', 1)
else: else:
raise cmdexc.CommandError( raise cmdexc.CommandError(
@ -1167,6 +1160,7 @@ class CommandDispatcher:
detach: Whether the command should be detached from qutebrowser. detach: Whether the command should be detached from qutebrowser.
cmdline: The commandline to execute. cmdline: The commandline to execute.
""" """
cmdutils.check_exclusive((userscript, detach), 'ud')
try: try:
cmd, *args = shlex.split(cmdline) cmd, *args = shlex.split(cmdline)
except ValueError as e: except ValueError as e:
@ -1241,8 +1235,7 @@ class CommandDispatcher:
@cmdutils.register(instance='command-dispatcher', scope='window', @cmdutils.register(instance='command-dispatcher', scope='window',
maxsplit=0) maxsplit=0)
@cmdutils.argument('name', @cmdutils.argument('name', completion=miscmodels.quickmark)
completion=usertypes.Completion.quickmark_by_name)
def quickmark_load(self, name, tab=False, bg=False, window=False): def quickmark_load(self, name, tab=False, bg=False, window=False):
"""Load a quickmark. """Load a quickmark.
@ -1260,8 +1253,7 @@ class CommandDispatcher:
@cmdutils.register(instance='command-dispatcher', scope='window', @cmdutils.register(instance='command-dispatcher', scope='window',
maxsplit=0) maxsplit=0)
@cmdutils.argument('name', @cmdutils.argument('name', completion=miscmodels.quickmark)
completion=usertypes.Completion.quickmark_by_name)
def quickmark_del(self, name=None): def quickmark_del(self, name=None):
"""Delete a quickmark. """Delete a quickmark.
@ -1323,7 +1315,7 @@ class CommandDispatcher:
@cmdutils.register(instance='command-dispatcher', scope='window', @cmdutils.register(instance='command-dispatcher', scope='window',
maxsplit=0) 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, def bookmark_load(self, url, tab=False, bg=False, window=False,
delete=False): delete=False):
"""Load a bookmark. """Load a bookmark.
@ -1345,7 +1337,7 @@ class CommandDispatcher:
@cmdutils.register(instance='command-dispatcher', scope='window', @cmdutils.register(instance='command-dispatcher', scope='window',
maxsplit=0) maxsplit=0)
@cmdutils.argument('url', completion=usertypes.Completion.bookmark_by_url) @cmdutils.argument('url', completion=miscmodels.bookmark)
def bookmark_del(self, url=None): def bookmark_del(self, url=None):
"""Delete a bookmark. """Delete a bookmark.
@ -1450,8 +1442,18 @@ class CommandDispatcher:
download_manager.get_mhtml(tab, target) download_manager.get_mhtml(tab, target)
else: else:
qnam = tab.networkaccessmanager() 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') @cmdutils.register(instance='command-dispatcher', scope='window')
def view_source(self): def view_source(self):
@ -1519,7 +1521,7 @@ class CommandDispatcher:
@cmdutils.register(instance='command-dispatcher', name='help', @cmdutils.register(instance='command-dispatcher', name='help',
scope='window') 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): def show_help(self, tab=False, bg=False, window=False, topic=None):
r"""Show help about a command or setting. r"""Show help about a command or setting.
@ -1728,7 +1730,8 @@ class CommandDispatcher:
""" """
self.set_mark("'") self.set_mark("'")
tab = self._current_widget() tab = self._current_widget()
tab.search.clear() if tab.search.search_displayed:
tab.search.clear()
if not text: if not text:
return return
@ -2159,6 +2162,10 @@ class CommandDispatcher:
window = self._tabbed_browser.window() window = self._tabbed_browser.window()
if window.isFullScreen(): if window.isFullScreen():
window.showNormal() window.setWindowState(
window.state_before_fullscreen & ~Qt.WindowFullScreen)
else: else:
window.state_before_fullscreen = window.windowState()
window.showFullScreen() 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 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): class NoFilenameError(Exception):
"""Raised when we can't find out a filename in DownloadTarget.""" """Raised when we can't find out a filename in DownloadTarget."""

View File

@ -19,214 +19,82 @@
"""Simple history which gets written to disk.""" """Simple history which gets written to disk."""
import os
import time 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.commands import cmdutils, cmdexc
from qutebrowser.utils import (utils, objreg, standarddir, log, qtutils, from qutebrowser.utils import (utils, objreg, log, usertypes, message,
usertypes, message) debug, standarddir)
from qutebrowser.misc import lineparser, objects 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: def __init__(self, parent=None):
atime: The time the page was accessed. super().__init__("CompletionHistory", ['url', 'title', 'last_atime'],
url: The URL which was accessed as QUrl. constraints={'url': 'PRIMARY KEY'}, parent=parent)
redirect: If True, don't save this entry to disk self.create_index('CompletionHistoryAtimeIndex', 'last_atime')
"""
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)
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 def __init__(self, parent=None):
from disk async while new history is already arriving. 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 self._before_query = sql.Query('SELECT * FROM History '
OrderedDict (sorted by time) of URL strings mapped to Entry objects. 'where not redirect '
'and not url like "qute://%" '
While reading from disk is still ongoing, the history is saved in 'and atime <= :latest '
self._temp_history instead, and then appended to self.history_dict once 'ORDER BY atime desc '
that's fully populated. 'limit :limit offset :offset')
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)
def __repr__(self): def __repr__(self):
return utils.get_repr(self, length=len(self)) return utils.get_repr(self, length=len(self))
def __iter__(self): def __contains__(self, url):
return iter(self.history_dict.values()) return self._contains_query.run(val=url).value()
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 get_recent(self): def get_recent(self):
"""Get the most recent history entries.""" """Get the most recent history entries."""
old = self._lineparser.get_recent() return self.select(sort_by='atime', sort_order='desc', limit=100)
return old + [str(e) for e in self._new_history]
def save(self): def entries_between(self, earliest, latest):
"""Save the history to disk.""" """Iterate non-redirect, non-qute entries between two timestamps.
new = (str(e) for e in self._new_history[self._saved_count:])
self._lineparser.new_data = new Args:
self._lineparser.save() earliest: Omit timestamps earlier than this.
self._saved_count = len(self._new_history) 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') @cmdutils.register(name='history-clear', instance='web-history')
def clear(self, force=False): def clear(self, force=False):
@ -246,12 +114,17 @@ class WebHistory(QObject):
"history?") "history?")
def _do_clear(self): def _do_clear(self):
self._lineparser.clear() self.delete_all()
self.history_dict.clear() self.completion.delete_all()
self._temp_history.clear()
self._new_history.clear() def delete_url(self, url):
self._saved_count = 0 """Remove all history entries with the given url.
self.cleared.emit()
Args:
url: URL string to delete.
"""
self.delete('url', url)
self.completion.delete('url', url)
@pyqtSlot(QUrl, QUrl, str) @pyqtSlot(QUrl, QUrl, str)
def add_from_tab(self, url, requested_url, title): 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") log.misc.warning("Ignoring invalid URL being added to history")
return return
if atime is None: atime = int(atime) if (atime is not None) else int(time.time())
atime = time.time() url_str = url.toString(QUrl.FullyEncoded | QUrl.RemovePassword)
entry = Entry(atime, url, title, redirect=redirect) self.insert({'url': url_str,
if self._initial_read_done: 'title': title,
self._add_entry(entry) 'atime': atime,
self._new_history.append(entry) 'redirect': redirect})
self.item_added.emit(entry) if not redirect:
if not entry.redirect: self.completion.insert({'url': url_str,
self.add_completion_item.emit(entry) '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: 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): def init(parent=None):
@ -304,8 +290,7 @@ def init(parent=None):
Args: Args:
parent: The parent to use for WebHistory. parent: The parent to use for WebHistory.
""" """
history = WebHistory(hist_dir=standarddir.data(), hist_name='history', history = WebHistory(parent=parent)
parent=parent)
objreg.register('web-history', history) objreg.register('web-history', history)
if objects.backend == usertypes.Backend.QtWebKit: if objects.backend == usertypes.Backend.QtWebKit:

View File

@ -412,7 +412,8 @@ class DownloadManager(downloads.AbstractDownloadManager):
mhtml.start_download_checked, tab=tab)) mhtml.start_download_checked, tab=tab))
message.global_bridge.ask(question, blocking=False) 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. """Start a download with a QNetworkRequest.
Args: Args:
@ -428,7 +429,9 @@ class DownloadManager(downloads.AbstractDownloadManager):
request.setAttribute(QNetworkRequest.CacheLoadControlAttribute, request.setAttribute(QNetworkRequest.CacheLoadControlAttribute,
QNetworkRequest.AlwaysNetwork) 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()) suggested_fn = urlutils.filename_from_url(request.url())
else: else:
# We might be downloading a binary blob embedded on a page or even # We might be downloading a binary blob embedded on a page or even

View File

@ -26,7 +26,6 @@ Module attributes:
import json import json
import os import os
import sys
import time import time
import urllib.parse import urllib.parse
import datetime import datetime
@ -185,88 +184,36 @@ def qute_bookmarks(_url):
return 'text/html', html return 'text/html', html
def history_data(start_time): # noqa def history_data(start_time, offset=None):
"""Return history data """Return history data.
Arguments: 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): # history atimes are stored as ints, ensure start_time is not a float
"""Iterate through the history and get items we're interested. start_time = int(start_time)
hist = objreg.get('web-history')
Arguments: if offset is not None:
reverse -- whether to reverse the history_dict before iterating. entries = hist.entries_before(start_time, limit=1000, offset=offset)
""" else:
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
# end is 24hrs earlier than start # end is 24hrs earlier than start
end_time = start_time - 24*60*60 end_time = start_time - 24*60*60
entries = hist.entries_between(end_time, start_time)
for item in history: return [{"url": e.url, "title": e.title or e.url, "time": e.atime}
# Skip redirects for e in entries]
# 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)
@add_handler('history') @add_handler('history')
def qute_history(url): def qute_history(url):
"""Handler for qute://history. Display and serve history.""" """Handler for qute://history. Display and serve history."""
if url.path() == '/data': 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. # Use start_time in query or current time.
try: try:
start_time = QUrlQuery(url).queryItemValue("start_time") start_time = QUrlQuery(url).queryItemValue("start_time")
@ -274,7 +221,7 @@ def qute_history(url):
except ValueError as e: except ValueError as e:
raise QuteSchemeError("Query parameter start_time is invalid", 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: else:
if ( if (
config.val.content.javascript.enabled and config.val.content.javascript.enabled and
@ -306,9 +253,9 @@ def qute_history(url):
start_time = time.mktime(next_date.timetuple()) - 1 start_time = time.mktime(next_date.timetuple()) - 1
history = [ history = [
(i["url"], i["title"], (i["url"], i["title"],
datetime.datetime.fromtimestamp(i["time"]/1000), datetime.datetime.fromtimestamp(i["time"]),
QUrl(i["url"]).host()) 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( return 'text/html', jinja.render(

View File

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

View File

@ -28,7 +28,9 @@ Module attributes:
""" """
import os import os
import logging import sys
import ctypes
import ctypes.util
from PyQt5.QtGui import QFont from PyQt5.QtGui import QFont
from PyQt5.QtWebEngineWidgets import (QWebEngineSettings, QWebEngineProfile, from PyQt5.QtWebEngineWidgets import (QWebEngineSettings, QWebEngineProfile,
@ -203,12 +205,10 @@ def init(args):
if args.enable_webengine_inspector: if args.enable_webengine_inspector:
os.environ['QTWEBENGINE_REMOTE_DEBUGGING'] = str(utils.random_port()) os.environ['QTWEBENGINE_REMOTE_DEBUGGING'] = str(utils.random_port())
# Workaround for a black screen with some setups # WORKAROUND for
# https://github.com/spyder-ide/spyder/issues/3226 # https://bugs.launchpad.net/ubuntu/+source/python-qt4/+bug/941826
if not os.environ.get('QUTE_NO_OPENGL_WORKAROUND'): if sys.platform == 'linux':
# Hide "No OpenGL_accelerate module loaded: ..." message ctypes.CDLL(ctypes.util.find_library("GL"), mode=ctypes.RTLD_GLOBAL)
logging.getLogger('OpenGL.acceleratesupport').propagate = False
from OpenGL import GL # pylint: disable=unused-variable
_init_profiles() _init_profiles()

View File

@ -51,12 +51,13 @@ def init():
global _qute_scheme_handler global _qute_scheme_handler
app = QApplication.instance() 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: if version.opengl_vendor() == 'nouveau' and not software_rendering:
# FIXME:qtwebengine display something more sophisticated here # FIXME:qtwebengine display something more sophisticated here
raise browsertab.WebTabError( raise browsertab.WebTabError(
"QtWebEngine is not supported with Nouveau graphics (unless " "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...") log.init.debug("Initializing qute://* handler...")
_qute_scheme_handler = webenginequtescheme.QuteSchemeHandler(parent=app) _qute_scheme_handler = webenginequtescheme.QuteSchemeHandler(parent=app)
@ -406,18 +407,18 @@ class WebEngineHistory(browsertab.AbstractHistory):
def current_idx(self): def current_idx(self):
return self._history.currentItemIndex() return self._history.currentItemIndex()
def back(self):
self._history.back()
def forward(self):
self._history.forward()
def can_go_back(self): def can_go_back(self):
return self._history.canGoBack() return self._history.canGoBack()
def can_go_forward(self): def can_go_forward(self):
return self._history.canGoForward() 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): def serialize(self):
if not qtutils.version_check('5.9'): if not qtutils.version_check('5.9'):
# WORKAROUND for # WORKAROUND for
@ -611,6 +612,7 @@ class WebEngineTab(browsertab.AbstractTab):
def shutdown(self): def shutdown(self):
self.shutting_down.emit() self.shutting_down.emit()
self.action.exit_fullscreen()
if qtutils.version_check('5.8', exact=True): if qtutils.version_check('5.8', exact=True):
# WORKAROUND for # WORKAROUND for
# https://bugreports.qt.io/browse/QTBUG-58563 # https://bugreports.qt.io/browse/QTBUG-58563
@ -711,7 +713,8 @@ class WebEngineTab(browsertab.AbstractTab):
@pyqtSlot() @pyqtSlot()
def _on_load_started(self): def _on_load_started(self):
"""Clear search when a new load is started if needed.""" """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 # WORKAROUND for
# https://bugreports.qt.io/browse/QTBUG-61506 # https://bugreports.qt.io/browse/QTBUG-61506
self.search.clear() self.search.clear()

View File

@ -19,9 +19,12 @@
"""QtWebKit specific part of history.""" """QtWebKit specific part of history."""
import functools
from PyQt5.QtWebKit import QWebHistoryInterface from PyQt5.QtWebKit import QWebHistoryInterface
from qutebrowser.utils import debug
class WebHistoryInterface(QWebHistoryInterface): class WebHistoryInterface(QWebHistoryInterface):
@ -34,11 +37,13 @@ class WebHistoryInterface(QWebHistoryInterface):
def __init__(self, webhistory, parent=None): def __init__(self, webhistory, parent=None):
super().__init__(parent) super().__init__(parent)
self._history = webhistory self._history = webhistory
self._history.changed.connect(self.historyContains.cache_clear)
def addHistoryEntry(self, url_string): def addHistoryEntry(self, url_string):
"""Required for a QWebHistoryInterface impl, obsoleted by add_url.""" """Required for a QWebHistoryInterface impl, obsoleted by add_url."""
pass pass
@functools.lru_cache(maxsize=32768)
def historyContains(self, url_string): def historyContains(self, url_string):
"""Called by WebKit to determine if a URL is contained in the history. """Called by WebKit to determine if a URL is contained in the history.
@ -48,7 +53,8 @@ class WebHistoryInterface(QWebHistoryInterface):
Return: Return:
True if the url is in the history, False otherwise. 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): def init(history):

View File

@ -32,7 +32,6 @@ from PyQt5.QtWebKit import QWebSettings
from PyQt5.QtPrintSupport import QPrinter from PyQt5.QtPrintSupport import QPrinter
from qutebrowser.browser import browsertab from qutebrowser.browser import browsertab
from qutebrowser.browser.network import proxy
from qutebrowser.browser.webkit import webview, tabhistory, webkitelem from qutebrowser.browser.webkit import webview, tabhistory, webkitelem
from qutebrowser.utils import qtutils, objreg, usertypes, utils, log, debug from qutebrowser.utils import qtutils, objreg, usertypes, utils, log, debug
@ -502,18 +501,18 @@ class WebKitHistory(browsertab.AbstractHistory):
def current_idx(self): def current_idx(self):
return self._history.currentItemIndex() return self._history.currentItemIndex()
def back(self):
self._history.back()
def forward(self):
self._history.forward()
def can_go_back(self): def can_go_back(self):
return self._history.canGoBack() return self._history.canGoBack()
def can_go_forward(self): def can_go_forward(self):
return self._history.canGoForward() 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): def serialize(self):
return qtutils.serialize(self._history) 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.config import config
from qutebrowser.commands import cmdutils, runners from qutebrowser.commands import cmdutils, runners
from qutebrowser.utils import usertypes, log, utils from qutebrowser.utils import log, utils, debug
from qutebrowser.completion.models import instances, sortfilter from qutebrowser.completion.models import miscmodels
class Completer(QObject): class Completer(QObject):
@ -38,6 +38,7 @@ class Completer(QObject):
_last_cursor_pos: The old cursor position so we avoid double completion _last_cursor_pos: The old cursor position so we avoid double completion
updates. updates.
_last_text: The old command text 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): def __init__(self, cmd, parent=None):
@ -50,6 +51,7 @@ class Completer(QObject):
self._timer.timeout.connect(self._update_completion) self._timer.timeout.connect(self._update_completion)
self._last_cursor_pos = None self._last_cursor_pos = None
self._last_text = None self._last_text = None
self._last_completion_func = None
self._cmd.update_completion.connect(self.schedule_completion_update) self._cmd.update_completion.connect(self.schedule_completion_update)
def __repr__(self): def __repr__(self):
@ -60,37 +62,8 @@ class Completer(QObject):
completion = self.parent() completion = self.parent()
return completion.model() 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): def _get_new_completion(self, before_cursor, under_cursor):
"""Get a new completion. """Get the completion function based on the current command text.
Args: Args:
before_cursor: The command chunks before the cursor. before_cursor: The command chunks before the cursor.
@ -107,8 +80,8 @@ class Completer(QObject):
log.completion.debug("After removing flags: {}".format(before_cursor)) log.completion.debug("After removing flags: {}".format(before_cursor))
if not before_cursor: if not before_cursor:
# '|' or 'set|' # '|' or 'set|'
model = instances.get(usertypes.Completion.command) log.completion.debug('Starting command completion')
return sortfilter.CompletionFilterModel(source=model, parent=self) return miscmodels.command
try: try:
cmd = cmdutils.cmd_dict[before_cursor[0]] cmd = cmdutils.cmd_dict[before_cursor[0]]
except KeyError: except KeyError:
@ -117,14 +90,11 @@ class Completer(QObject):
return None return None
argpos = len(before_cursor) - 1 argpos = len(before_cursor) - 1
try: try:
completion = cmd.get_pos_arg_info(argpos).completion func = cmd.get_pos_arg_info(argpos).completion
except IndexError: except IndexError:
log.completion.debug("No completion in position {}".format(argpos)) log.completion.debug("No completion in position {}".format(argpos))
return None return None
if completion is None: return func
return None
model = self._get_completion_model(completion, before_cursor[1:])
return model
def _quote(self, s): def _quote(self, s):
"""Quote s if it needs quoting for the commandline. """Quote s if it needs quoting for the commandline.
@ -239,6 +209,7 @@ class Completer(QObject):
# FIXME complete searches # FIXME complete searches
# https://github.com/qutebrowser/qutebrowser/issues/32 # https://github.com/qutebrowser/qutebrowser/issues/32
completion.set_model(None) completion.set_model(None)
self._last_completion_func = None
return return
before_cursor, pattern, after_cursor = self._partition() before_cursor, pattern, after_cursor = self._partition()
@ -247,13 +218,24 @@ class Completer(QObject):
before_cursor, pattern, after_cursor)) before_cursor, pattern, after_cursor))
pattern = pattern.strip("'\"") 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 '{}'" if func is None:
.format(model.srcmodel.__class__.__name__ if model else 'None', log.completion.debug('Clearing completion')
pattern)) 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): def _change_completed_part(self, newtext, before, after, immediate=False):
"""Change the part we're currently completing in the commandline. """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)) self._doc.setDefaultStyleSheet(template.render(conf=config.val))
if index.parent().isValid(): if index.parent().isValid():
pattern = index.model().pattern view = self.parent()
columns_to_filter = index.model().srcmodel.columns_to_filter pattern = view.pattern
columns_to_filter = index.model().columns_to_filter(index)
if index.column() in columns_to_filter and pattern: if index.column() in columns_to_filter and pattern:
repl = r'<span class="highlight">\g<0></span>' repl = r'<span class="highlight">\g<0></span>'
text = re.sub(re.escape(pattern).replace(r'\ ', r'|'), 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.config import config
from qutebrowser.completion import completiondelegate from qutebrowser.completion import completiondelegate
from qutebrowser.completion.models import base from qutebrowser.utils import utils, usertypes, debug, log
from qutebrowser.utils import utils, usertypes
from qutebrowser.commands import cmdexc, cmdutils from qutebrowser.commands import cmdexc, cmdutils
@ -41,6 +40,7 @@ class CompletionView(QTreeView):
headers, and children show as flat list. headers, and children show as flat list.
Attributes: Attributes:
pattern: Current filter pattern, used for highlighting.
_win_id: The ID of the window this CompletionView is associated with. _win_id: The ID of the window this CompletionView is associated with.
_height: The height to use for the CompletionView. _height: The height to use for the CompletionView.
_height_perc: Either None or a percentage if height should be relative. _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): def __init__(self, win_id, parent=None):
super().__init__(parent) super().__init__(parent)
self.pattern = ''
self._win_id = win_id 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) config.instance.changed.connect(self._on_config_changed)
self._column_widths = base.BaseCompletionModel.COLUMN_WIDTHS
self._active = False self._active = False
self._delegate = completiondelegate.CompletionItemDelegate(self) self._delegate = completiondelegate.CompletionItemDelegate(self)
@ -148,8 +146,11 @@ class CompletionView(QTreeView):
def _resize_columns(self): def _resize_columns(self):
"""Resize the completion columns based on column_widths.""" """Resize the completion columns based on column_widths."""
if self.model() is None:
return
width = self.size().width() 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(): if self.verticalScrollBar().isVisible():
delta = self.style().pixelMetric(QStyle.PM_ScrollBarExtent) + 5 delta = self.style().pixelMetric(QStyle.PM_ScrollBarExtent) + 5
@ -252,6 +253,10 @@ class CompletionView(QTreeView):
selmodel.setCurrentIndex( selmodel.setCurrentIndex(
idx, QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows) 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() count = self.model().count()
if count == 0: if count == 0:
self.hide() self.hide()
@ -260,47 +265,50 @@ class CompletionView(QTreeView):
elif config.val.completion.show == 'auto': elif config.val.completion.show == 'auto':
self.show() self.show()
def set_model(self, model, pattern=None): def set_model(self, model):
"""Switch completion to a new model. """Switch completion to a new model.
Called from on_update_completion(). Called from on_update_completion().
Args: Args:
model: The model to use. 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: if model is None:
self._active = False self._active = False
self.hide() self.hide()
return return
old_model = self.model() model.setParent(self)
if model is not old_model: self._active = True
sel_model = self.selectionModel() self._maybe_show()
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()
self._resize_columns()
for i in range(model.rowCount()): for i in range(model.rowCount()):
self.expand(model.index(i, 0)) self.expand(model.index(i, 0))
if pattern is not None: def set_pattern(self, pattern):
model.set_pattern(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 def _maybe_show(self):
self._resize_columns() if (config.val.completion.show == 'always' and
self._maybe_update_geometry() self.model().count() > 0):
self.show()
else:
self.hide()
def _maybe_update_geometry(self): def _maybe_update_geometry(self):
"""Emit the update_geometry signal if the config says so.""" """Emit the update_geometry signal if the config says so."""
@ -345,7 +353,7 @@ class CompletionView(QTreeView):
indexes = selected.indexes() indexes = selected.indexes()
if not indexes: if not indexes:
return return
data = self.model().data(indexes[0]) data = str(self.model().data(indexes[0]))
self.selection_changed.emit(data) self.selection_changed.emit(data)
def resizeEvent(self, e): def resizeEvent(self, e):
@ -365,9 +373,7 @@ class CompletionView(QTreeView):
modes=[usertypes.KeyMode.command], scope='window') modes=[usertypes.KeyMode.command], scope='window')
def completion_item_del(self): def completion_item_del(self):
"""Delete the current completion item.""" """Delete the current completion item."""
if not self.currentIndex().isValid(): index = self.currentIndex()
if not index.isValid():
raise cmdexc.CommandError("No item selected!") raise cmdexc.CommandError("No item selected!")
try: self.model().delete_cur_item(index)
self.model().srcmodel.delete_cur_item(self)
except NotImplementedError:
raise cmdexc.CommandError("Cannot delete this item.")

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 # You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. # along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""CompletionModels for the config.""" """Functions that return config-related completion models."""
# FIXME:conf from qutebrowser.config import configdata, configexc
# pylint: disable=no-member from qutebrowser.completion.models import completionmodel, listcategory
from qutebrowser.utils import objreg
from PyQt5.QtCore import pyqtSlot, Qt
from qutebrowser.config import config, configdata
from qutebrowser.utils import log, qtutils
from qutebrowser.completion.models import base
class SettingSectionCompletionModel(base.BaseCompletionModel): def section():
"""A CompletionModel filled with settings sections.""" """A CompletionModel filled with settings sections."""
model = completionmodel.CompletionModel(column_widths=(20, 70, 10))
# https://github.com/qutebrowser/qutebrowser/issues/545 sections = ((name, configdata.SECTION_DESC[name].splitlines()[0].strip())
# pylint: disable=abstract-method for name in configdata.DATA)
model.add_category(listcategory.ListCategory("Sections", sections))
COLUMN_WIDTHS = (20, 70, 10) return model
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)
class SettingOptionCompletionModel(base.BaseCompletionModel): def option(sectname):
"""A CompletionModel filled with settings and their descriptions. """A CompletionModel filled with settings and their descriptions.
Attributes: Args:
_misc_items: A dict of the misc. column items which will be set later. sectname: The name of the config section this model shows.
_section: The config section this model shows.
""" """
model = completionmodel.CompletionModel(column_widths=(20, 70, 10))
# https://github.com/qutebrowser/qutebrowser/issues/545 try:
# pylint: disable=abstract-method sectdata = configdata.DATA[sectname]
except KeyError:
COLUMN_WIDTHS = (20, 70, 10) return None
options = []
def __init__(self, section, parent=None): for name in sectdata:
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
try: try:
item = self._misc_items[option] desc = sectdata.descriptions[name]
except KeyError: except (KeyError, AttributeError):
log.completion.debug("Couldn't get item {}.{} from model!".format( # Some stuff (especially ValueList items) don't have a
section, option)) # description.
# changed before init desc = ""
return else:
val = config.get(section, option, raw=True) desc = desc.splitlines()[0]
idx = item.index() config = objreg.get('config')
qtutils.ensure_valid(idx) val = config.get(sectname, name, raw=True)
ok = self.setData(idx, val, Qt.DisplayRole) options.append((name, desc, val))
if not ok: model.add_category(listcategory.ListCategory(sectname, options))
raise ValueError("Setting data failed! (section: {}, option: {}, " return model
"value: {})".format(section, option, val))
class SettingValueCompletionModel(base.BaseCompletionModel): def value(sectname, optname):
"""A CompletionModel filled with setting values. """A CompletionModel filled with setting values.
Attributes: Args:
_section: The config section this model shows. sectname: The name of the config section this model shows.
_option: The config option 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 try:
# pylint: disable=abstract-method 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): if hasattr(configdata.DATA[sectname], 'valtype'):
super().__init__(parent) # Same type for all values (ValueList)
self._section = section vals = configdata.DATA[sectname].valtype.complete()
self._option = option else:
config.instance.changed.connect(self._update_current_value) if optname is None:
cur_cat = self.new_category("Current/Default", sort=0) raise ValueError("optname may only be None for ValueList "
value = config.get(section, option, raw=True) "sections, but {} is not!".format(sectname))
if not value: # Different type for each value (KeyValue)
value = '""' vals = configdata.DATA[sectname][optname].typ.complete()
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)
@pyqtSlot(str, str) cur_cat = listcategory.ListCategory("Current/Default",
def _update_current_value(self, section, option): [(current, "Current value"), (default, "Default value")])
"""Update current value when config changed.""" model.add_category(cur_cat)
if (section, option) != (self._section, self._option): if vals is not None:
return model.add_category(listcategory.ListCategory("Completions", vals))
value = config.get(section, option, raw=True) return model
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))

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 # You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. # 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.config import config, configdata
from qutebrowser.utils import objreg, log, qtutils from qutebrowser.utils import objreg, log
from qutebrowser.commands import cmdutils 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.""" """A CompletionModel filled with non-hidden commands and descriptions."""
model = completionmodel.CompletionModel(column_widths=(20, 60, 20))
# https://github.com/qutebrowser/qutebrowser/issues/545 cmdlist = _get_cmd_completions(include_aliases=True, include_hidden=False)
# pylint: disable=abstract-method model.add_category(listcategory.ListCategory("Commands", cmdlist))
return model
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)
class HelpCompletionModel(base.BaseCompletionModel): def helptopic():
"""A CompletionModel filled with help topics.""" """A CompletionModel filled with help topics."""
model = completionmodel.CompletionModel()
# https://github.com/qutebrowser/qutebrowser/issues/545 cmdlist = _get_cmd_completions(include_aliases=False, include_hidden=True,
# pylint: disable=abstract-method 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) model.add_category(listcategory.ListCategory("Commands", cmdlist))
model.add_category(listcategory.ListCategory("Settings", settings))
def __init__(self, parent=None): return model
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)
class QuickmarkCompletionModel(base.BaseCompletionModel): def quickmark():
"""A CompletionModel filled with all quickmarks.""" """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 model = completionmodel.CompletionModel(column_widths=(30, 70, 0))
# pylint: disable=abstract-method marks = objreg.get('quickmark-manager').marks.items()
model.add_category(listcategory.ListCategory('Quickmarks', marks,
def __init__(self, parent=None): delete_func=delete))
super().__init__(parent) return model
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)
class BookmarkCompletionModel(base.BaseCompletionModel): def bookmark():
"""A CompletionModel filled with all bookmarks.""" """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 model = completionmodel.CompletionModel(column_widths=(30, 70, 0))
# pylint: disable=abstract-method marks = objreg.get('bookmark-manager').marks.items()
model.add_category(listcategory.ListCategory('Bookmarks', marks,
def __init__(self, parent=None): delete_func=delete))
super().__init__(parent) return model
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)
class SessionCompletionModel(base.BaseCompletionModel): def session():
"""A CompletionModel filled with session names.""" """A CompletionModel filled with session names."""
model = completionmodel.CompletionModel()
# https://github.com/qutebrowser/qutebrowser/issues/545 try:
# pylint: disable=abstract-method manager = objreg.get('session-manager')
sessions = ((name,) for name in manager.list_sessions()
def __init__(self, parent=None): if not name.startswith('_'))
super().__init__(parent) model.add_category(listcategory.ListCategory("Sessions", sessions))
cat = self.new_category("Sessions") except OSError:
try: log.completion.exception("Failed to list sessions!")
for name in objreg.get('session-manager').list_sessions(): return model
if not name.startswith('_'):
self.new_item(cat, name)
except OSError:
log.completion.exception("Failed to list sessions!")
class TabCompletionModel(base.BaseCompletionModel): def buffer():
"""A model to complete on open tabs across all windows. """A model to complete on open tabs across all windows.
Used for switching the buffer command. Used for switching the buffer command.
""" """
def delete_buffer(data):
IDX_COLUMN = 0 """Close the selected tab."""
URL_COLUMN = 1 win_id, tab_index = data[0].split('/')
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('/')
tabbed_browser = objreg.get('tabbed-browser', scope='window', tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=int(win_id)) window=int(win_id))
tabbed_browser.on_tab_close_requested(int(tab_index) - 1) 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): Args:
super().__init__(parent) key: the key being bound.
cmdlist = _get_cmd_completions(include_hidden=True, """
include_aliases=True) model = completionmodel.CompletionModel(column_widths=(20, 60, 20))
cat = self.new_category("Commands") cmd_text = objreg.get('key-config').get_bindings_for('normal').get(key)
for (name, desc, misc) in cmdlist:
self.new_item(cat, name, desc, misc) 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=''): 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 # You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. # 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 qutebrowser.completion.models import (completionmodel, listcategory,
histcategory)
from PyQt5.QtCore import pyqtSlot, Qt from qutebrowser.utils import log, objreg
from qutebrowser.utils import objreg, utils, qtutils, log
from qutebrowser.completion.models import base
from qutebrowser.config import config 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. """A model which combines bookmarks, quickmarks and web history URLs.
Used for the `open` command. Used for the `open` command.
""" """
model = completionmodel.CompletionModel(column_widths=(40, 50, 10))
URL_COLUMN = 0 quickmarks = ((url, name) for (name, url)
TEXT_COLUMN = 1 in objreg.get('quickmark-manager').marks.items())
TIME_COLUMN = 2 bookmarks = objreg.get('bookmark-manager').marks.items()
COLUMN_WIDTHS = (40, 50, 10) model.add_category(listcategory.ListCategory(
DUMB_SORT = Qt.DescendingOrder 'Quickmarks', quickmarks, delete_func=_delete_quickmark))
model.add_category(listcategory.ListCategory(
'Bookmarks', bookmarks, delete_func=_delete_bookmark))
def __init__(self, parent=None): if config.val.completion.web_history_max_items != 0:
super().__init__(parent) hist_cat = histcategory.HistoryCategory(delete_func=_delete_history)
model.add_category(hist_cat)
self.columns_to_filter = [self.URL_COLUMN, self.TEXT_COLUMN] return model
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)

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.utils import utils, objreg, message, log, usertypes
from qutebrowser.misc import objects from qutebrowser.misc import objects
from qutebrowser.commands import cmdexc, cmdutils 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 # An easy way to access the config from other code via config.val.foo
val = None val = None
@ -229,6 +229,7 @@ class ConfigCommands:
self._keyconfig = keyconfig self._keyconfig = keyconfig
@cmdutils.register(instance='config-commands', star_args_optional=True) @cmdutils.register(instance='config-commands', star_args_optional=True)
@cmdutils.argument('option', completion=configmodel.option)
@cmdutils.argument('win_id', win_id=True) @cmdutils.argument('win_id', win_id=True)
def set(self, win_id, option=None, *values, temp=False, print_=False): def set(self, win_id, option=None, *values, temp=False, print_=False):
"""Set an option. """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') }} {{ install_webengine('qt5-qtwebengine') }}
{% elif distribution.parsed == Distribution.opensuse %} {% elif distribution.parsed == Distribution.opensuse %}
{{ install_webengine('libqt5-qtwebengine') }} {{ install_webengine('libqt5-qtwebengine') }}
{% elif distribution.parsed == Distribution.gentoo %}
{{ install_webengine('dev-qt/qtwebengine') }}
{% else %} {% else %}
{{ unknown_system() }} {{ unknown_system() }}
{% endif %} {% endif %}

View File

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

View File

@ -23,8 +23,12 @@ window.loadHistory = (function() {
// Date of last seen item. // Date of last seen item.
var lastItemDate = null; 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 nextTime = null;
var nextOffset = 0;
// The URL to fetch data from. // The URL to fetch data from.
var DATA_URL = "qute://history/data"; var DATA_URL = "qute://history/data";
@ -157,23 +161,28 @@ window.loadHistory = (function() {
return; return;
} }
for (var i = 0, len = history.length - 1; i < len; i++) { if (history.length === 0) {
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) {
// Reached end of history // Reached end of history
window.onscroll = null; window.onscroll = null;
EOF_MESSAGE.style.display = "block"; EOF_MESSAGE.style.display = "block";
LOAD_LINK.style.display = "none"; LOAD_LINK.style.display = "none";
} else { return;
nextTime = next; }
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} * @return {void}
*/ */
function loadHistory() { function loadHistory() {
var url = DATA_URL.concat("?offset=", nextOffset.toString());
if (nextTime === null) { if (nextTime === null) {
getJSON(DATA_URL, receiveHistory); getJSON(url, receiveHistory);
} else { } else {
var url = DATA_URL.concat("?start_time=", nextTime.toString()); url = url.concat("&start_time=", nextTime.toString());
getJSON(url, receiveHistory); getJSON(url, receiveHistory);
} }
} }

View File

@ -123,6 +123,7 @@ class MainWindow(QWidget):
Attributes: Attributes:
status: The StatusBar widget. status: The StatusBar widget.
tabbed_browser: The TabbedBrowser widget. tabbed_browser: The TabbedBrowser widget.
state_before_fullscreen: window state before activation of fullscreen.
_downloadview: The DownloadView widget. _downloadview: The DownloadView widget.
_vbox: The main QVBoxLayout. _vbox: The main QVBoxLayout.
_commandrunner: The main CommandRunner instance. _commandrunner: The main CommandRunner instance.
@ -217,6 +218,8 @@ class MainWindow(QWidget):
objreg.get("app").new_window.emit(self) objreg.get("app").new_window.emit(self)
self.state_before_fullscreen = self.windowState()
def _init_geometry(self, geometry): def _init_geometry(self, geometry):
"""Initialize the window geometry or load it from disk.""" """Initialize the window geometry or load it from disk."""
if geometry is not None: if geometry is not None:
@ -461,6 +464,8 @@ class MainWindow(QWidget):
tabs.tab_index_changed.connect(status.tabindex.on_tab_index_changed) 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(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_link_hovered.connect(status.url.set_hover_url)
tabs.cur_load_status_changed.connect(status.url.on_load_status_changed) tabs.cur_load_status_changed.connect(status.url.on_load_status_changed)
tabs.cur_fullscreen_requested.connect(self._on_fullscreen_requested) tabs.cur_fullscreen_requested.connect(self._on_fullscreen_requested)
@ -475,9 +480,12 @@ class MainWindow(QWidget):
@pyqtSlot(bool) @pyqtSlot(bool)
def _on_fullscreen_requested(self, on): def _on_fullscreen_requested(self, on):
if on: if on:
self.state_before_fullscreen = self.windowState()
self.showFullScreen() self.showFullScreen()
else: elif self.isFullScreen():
self.showNormal() 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') @cmdutils.register(instance='main-window', scope='window')
@pyqtSlot() @pyqtSlot()

View File

@ -99,8 +99,10 @@ class MessageView(QWidget):
@config.change_filter('messages.timeout') @config.change_filter('messages.timeout')
def _set_clear_timer_interval(self): def _set_clear_timer_interval(self):
"""Configure self._clear_timer according to the config.""" """Configure self._clear_timer according to the config."""
if config.val.messages.timeout != 0: interval = config.val.messages.timeout
self._clear_timer.setInterval(config.val.messages.timeout) if interval > 0:
interval *= min(5, len(self._messages))
self._clear_timer.setInterval(interval)
@pyqtSlot() @pyqtSlot()
def clear_messages(self): def clear_messages(self):
@ -127,12 +129,13 @@ class MessageView(QWidget):
widget = Message(level, text, replace=replace, parent=self) widget = Message(level, text, replace=replace, parent=self)
self._vbox.addWidget(widget) self._vbox.addWidget(widget)
widget.show() widget.show()
if config.val.messages.timeout != 0:
self._clear_timer.start()
self._messages.append(widget) self._messages.append(widget)
self._last_text = text self._last_text = text
self.show() self.show()
self.update_geometry.emit() self.update_geometry.emit()
if config.val.messages.timeout != 0:
self._set_clear_timer_interval()
self._clear_timer.start()
def mousePressEvent(self, e): def mousePressEvent(self, e):
"""Clear messages when they are clicked on.""" """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.browser import browsertab
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.utils import usertypes, log, objreg, utils from qutebrowser.utils import usertypes, log, objreg, utils
from qutebrowser.mainwindow.statusbar import (command, progress, keystring, from qutebrowser.mainwindow.statusbar import (backforward, command, progress,
percentage, url, tabindex) keystring, percentage, url,
tabindex)
from qutebrowser.mainwindow.statusbar import text as textwidget from qutebrowser.mainwindow.statusbar import text as textwidget
@ -184,6 +185,9 @@ class StatusBar(QWidget):
self.percentage = percentage.Percentage() self.percentage = percentage.Percentage()
self._hbox.addWidget(self.percentage) self._hbox.addWidget(self.percentage)
self.backforward = backforward.Backforward()
self._hbox.addWidget(self.backforward)
self.tabindex = tabindex.TabIndex() self.tabindex = tabindex.TabIndex()
self._hbox.addWidget(self.tabindex) self._hbox.addWidget(self.tabindex)
@ -329,6 +333,7 @@ class StatusBar(QWidget):
self.url.on_tab_changed(tab) self.url.on_tab_changed(tab)
self.prog.on_tab_changed(tab) self.prog.on_tab_changed(tab)
self.percentage.on_tab_changed(tab) self.percentage.on_tab_changed(tab)
self.backforward.on_tab_changed(tab)
self.maybe_hide() self.maybe_hide()
assert tab.private == self._color_flags.private assert tab.private == self._color_flags.private

View File

@ -23,7 +23,7 @@ import functools
import collections import collections
from PyQt5.QtWidgets import QSizePolicy 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 PyQt5.QtGui import QIcon
from qutebrowser.config import config from qutebrowser.config import config
@ -239,6 +239,19 @@ class TabbedBrowser(tabwidget.TabWidget):
for tab in self.widgets(): for tab in self.widgets():
self._remove_tab(tab) 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): def close_tab(self, tab, *, add_undo=True):
"""Close a tab. """Close a tab.
@ -348,7 +361,7 @@ class TabbedBrowser(tabwidget.TabWidget):
newtab = self.tabopen(url, background=False, idx=idx) newtab = self.tabopen(url, background=False, idx=idx)
newtab.history.deserialize(history_data) newtab.history.deserialize(history_data)
self.set_tab_pinned(idx, pinned) self.set_tab_pinned(newtab, pinned)
@pyqtSlot('QUrl', bool) @pyqtSlot('QUrl', bool)
def openurl(self, url, newtab): def openurl(self, url, newtab):
@ -372,7 +385,8 @@ class TabbedBrowser(tabwidget.TabWidget):
log.webview.debug("Got invalid tab {} for index {}!".format( log.webview.debug("Got invalid tab {} for index {}!".format(
tab, idx)) tab, idx))
return return
self.close_tab(tab) self.tab_close_prompt_if_pinned(
tab, False, lambda: self.close_tab(tab))
@pyqtSlot(browsertab.AbstractTab) @pyqtSlot(browsertab.AbstractTab)
def on_window_close_requested(self, widget): 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. # Make sure the background tab has the correct initial size.
# With a foreground tab, it's going to be resized correctly by the # With a foreground tab, it's going to be resized correctly by the
# layout anyways. # layout anyways.
if self.tabBar().vertical: tab.resize(self.currentWidget().size())
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)
self.tab_index_changed.emit(self.currentIndex(), self.count()) self.tab_index_changed.emit(self.currentIndex(), self.count())
else: else:
self.setCurrentWidget(tab) self.setCurrentWidget(tab)
@ -705,12 +713,16 @@ class TabbedBrowser(tabwidget.TabWidget):
} }
msg = messages[status] msg = messages[status]
def show_error_page(html):
tab.set_html(html)
log.webview.error(msg)
if qtutils.version_check('5.9'): if qtutils.version_check('5.9'):
url_string = tab.url(requested=True).toDisplayString() url_string = tab.url(requested=True).toDisplayString()
error_page = jinja.render( error_page = jinja.render(
'error.html', title="Error loading {}".format(url_string), 'error.html', title="Error loading {}".format(url_string),
url=url_string, error=msg) url=url_string, error=msg, icon='')
QTimer.singleShot(0, lambda: tab.set_html(error_page)) QTimer.singleShot(100, lambda: show_error_page(error_page))
log.webview.error(msg) log.webview.error(msg)
else: else:
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-58698 # 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) QTimer, QUrl)
from PyQt5.QtWidgets import (QTabWidget, QTabBar, QSizePolicy, QCommonStyle, from PyQt5.QtWidgets import (QTabWidget, QTabBar, QSizePolicy, QCommonStyle,
QStyle, QStylePainter, QStyleOptionTab, QStyle, QStylePainter, QStyleOptionTab,
QStyleFactory) QStyleFactory, QWidget)
from PyQt5.QtGui import QIcon, QPalette, QColor from PyQt5.QtGui import QIcon, QPalette, QColor
from qutebrowser.utils import qtutils, objreg, utils, usertypes, log 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.set_tab_data(idx, 'indicator-color', color)
bar.update(bar.tabRect(idx)) 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. """Set the tab status as pinned.
Args: Args:
idx: The tab index. tab: The tab to pin
pinned: Pinned tab state to set. pinned: Pinned tab state to set.
loading: Whether to ignore current data state when loading: Whether to ignore current data state when
counting pinned_count. counting pinned_count.
""" """
bar = self.tabBar() bar = self.tabBar()
tab = self.widget(idx) idx = self.indexOf(tab)
# Only modify pinned_count if we had a change # Only modify pinned_count if we had a change
# always modify pinned_count if we are loading # always modify pinned_count if we are loading
@ -487,14 +488,10 @@ class TabBar(QTabBar):
width = int(confwidth) width = int(confwidth)
size = QSize(max(minimum_size.width(), width), height) size = QSize(max(minimum_size.width(), width), height)
elif self.count() == 0: 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 # We return it directly rather than setting `size' because we don't
# want to ensure it's valid in this special case. # want to ensure it's valid in this special case.
return QSize() 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: else:
try: try:
pinned = self.tab_data(index, 'pinned') pinned = self.tab_data(index, 'pinned')
@ -522,13 +519,13 @@ class TabBar(QTabBar):
width = no_pinned_width / (self.count() - self.pinned_count) width = no_pinned_width / (self.count() - self.pinned_count)
else: else:
# If we *do* have enough space, tabs should occupy the whole # Tabs should attempt to occupy the whole window width. If
# window width. If there are pinned tabs their size will be # there are pinned tabs their size will be subtracted from the
# subtracted from the total window width. # total window width. During shutdown the self.count goes
# During shutdown the self.count goes down, # down, but the self.pinned_count not - this generates some odd
# but the self.pinned_count not - this generates some odd
# behavior. To avoid this we compare self.count against # 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: if self.pinned_count > 0 and no_pinned_count > 0:
width = no_pinned_width / no_pinned_count width = no_pinned_width / no_pinned_count
else: else:
@ -540,6 +537,10 @@ class TabBar(QTabBar):
index < no_pinned_width % no_pinned_count): index < no_pinned_width % no_pinned_count):
width += 1 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) size = QSize(width, height)
qtutils.ensure_valid(size) qtutils.ensure_valid(size)
return size return size
@ -750,6 +751,17 @@ class TabBarStyle(QCommonStyle):
rct = super().subElementRect(sr, opt, widget) rct = super().subElementRect(sr, opt, widget)
return rct return rct
else: 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) return self._style.subElementRect(sr, opt, widget)
def _tab_layout(self, opt): def _tab_layout(self, opt):

View File

@ -28,6 +28,11 @@ import functools
import faulthandler import faulthandler
import os.path import os.path
import collections 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, from PyQt5.QtCore import (pyqtSlot, qInstallMessageHandler, QObject,
QSocketNotifier, QTimer, QUrl) QSocketNotifier, QTimer, QUrl)

View File

@ -337,12 +337,12 @@ def check_libraries(backend):
"or Install via pip.", "or Install via pip.",
pip="PyYAML"), pip="PyYAML"),
'PyQt5.QtQml': _missing_str("PyQt5.QtQml"), 'PyQt5.QtQml': _missing_str("PyQt5.QtQml"),
'PyQt5.QtSql': _missing_str("PyQt5.QtSql"),
} }
if backend == 'webengine': if backend == 'webengine':
modules['PyQt5.QtWebEngineWidgets'] = _missing_str("QtWebEngine", modules['PyQt5.QtWebEngineWidgets'] = _missing_str("QtWebEngine",
webengine=True) webengine=True)
modules['PyQt5.QtOpenGL'] = _missing_str("PyQt5.QtOpenGL") modules['PyQt5.QtOpenGL'] = _missing_str("PyQt5.QtOpenGL")
modules['OpenGL'] = _missing_str("PyOpenGL")
else: else:
assert backend == 'webkit' assert backend == 'webkit'
modules['PyQt5.QtWebKit'] = _missing_str("PyQt5.QtWebKit") modules['PyQt5.QtWebKit'] = _missing_str("PyQt5.QtWebKit")

View File

@ -154,8 +154,8 @@ class GUIProcess(QObject):
log.procs.debug("Process started.") log.procs.debug("Process started.")
self._started = True self._started = True
else: else:
message.error("Error while spawning {}: {}.".format( message.error("Error while spawning {}: {}".format(
self._what, self._proc.error())) self._what, ERROR_STRINGS[self._proc.error()]))
def exit_status(self): def exit_status(self):
return self._proc.exitStatus() return self._proc.exitStatus()

View File

@ -20,7 +20,6 @@
"""Utilities for IPC with existing instances.""" """Utilities for IPC with existing instances."""
import os import os
import sys
import time import time
import json import json
import getpass import getpass
@ -41,8 +40,8 @@ ATIME_INTERVAL = 60 * 60 * 3 * 1000 # 3 hours
PROTOCOL_VERSION = 1 PROTOCOL_VERSION = 1
def _get_socketname_legacy(basedir): def _get_socketname_windows(basedir):
"""Legacy implementation of _get_socketname.""" """Get a socketname to use for Windows."""
parts = ['qutebrowser', getpass.getuser()] parts = ['qutebrowser', getpass.getuser()]
if basedir is not None: if basedir is not None:
md5 = hashlib.md5(basedir.encode('utf-8')).hexdigest() md5 = hashlib.md5(basedir.encode('utf-8')).hexdigest()
@ -50,10 +49,10 @@ def _get_socketname_legacy(basedir):
return '-'.join(parts) return '-'.join(parts)
def _get_socketname(basedir, legacy=False): def _get_socketname(basedir):
"""Get a socketname to use.""" """Get a socketname to use."""
if legacy or os.name == 'nt': if os.name == 'nt': # pragma: no cover
return _get_socketname_legacy(basedir) return _get_socketname_windows(basedir)
parts_to_hash = [getpass.getuser()] parts_to_hash = [getpass.getuser()]
if basedir is not None: if basedir is not None:
@ -415,41 +414,7 @@ class IPCServer(QObject):
self._remove_server() self._remove_server()
def _has_legacy_server(name): def send_to_running_instance(socketname, command, target_arg, *, socket=None):
"""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):
"""Try to send a commandline to a running instance. """Try to send a commandline to a running instance.
Blocks for CONNECT_TIMEOUT ms. 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. command: The command to send to the running instance.
target_arg: --target command line argument target_arg: --target command line argument
socket: The socket to read data from, or None. socket: The socket to read data from, or None.
legacy_name: The legacy name to first try to connect to.
Return: Return:
True if connecting was successful, False if no connection was made. 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: if socket is None:
socket = QLocalSocket() socket = QLocalSocket()
if legacy_name is not None and _has_legacy_server(legacy_name): log.ipc.debug("Connecting to {}".format(socketname))
name_to_use = legacy_name socket.connectToServer(socketname)
else:
name_to_use = socketname
log.ipc.debug("Connecting to {}".format(name_to_use))
socket.connectToServer(name_to_use)
connected = socket.waitForConnected(CONNECT_TIMEOUT) connected = socket.waitForConnected(CONNECT_TIMEOUT)
if connected: if connected:
@ -527,12 +486,10 @@ def send_or_listen(args):
None if an instance was running and received our request. None if an instance was running and received our request.
""" """
socketname = _get_socketname(args.basedir) socketname = _get_socketname(args.basedir)
legacy_socketname = _get_socketname(args.basedir, legacy=True)
try: try:
try: try:
sent = send_to_running_instance(socketname, args.command, sent = send_to_running_instance(socketname, args.command,
args.target, args.target)
legacy_name=legacy_socketname)
if sent: if sent:
return None return None
log.init.debug("Starting IPC server...") log.init.debug("Starting IPC server...")
@ -545,8 +502,7 @@ def send_or_listen(args):
log.init.debug("Got AddressInUseError, trying again.") log.init.debug("Got AddressInUseError, trying again.")
time.sleep(0.5) time.sleep(0.5)
sent = send_to_running_instance(socketname, args.command, sent = send_to_running_instance(socketname, args.command,
args.target, args.target)
legacy_name=legacy_socketname)
if sent: if sent:
return None return None
else: else:

View File

@ -21,7 +21,6 @@
import os import os
import os.path import os.path
import itertools
import contextlib import contextlib
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject
@ -96,7 +95,7 @@ class BaseLineParser(QObject):
""" """
assert self._configfile is not None assert self._configfile is not None
if self._opened: if self._opened:
raise IOError("Refusing to double-open AppendLineParser.") raise IOError("Refusing to double-open LineParser.")
self._opened = True self._opened = True
try: try:
if self._binary: if self._binary:
@ -133,73 +132,6 @@ class BaseLineParser(QObject):
raise NotImplementedError 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): class LineParser(BaseLineParser):
"""Parser for configuration files which are simply line-based. """Parser for configuration files which are simply line-based.
@ -240,7 +172,7 @@ class LineParser(BaseLineParser):
def save(self): def save(self):
"""Save the config file.""" """Save the config file."""
if self._opened: if self._opened:
raise IOError("Refusing to double-open AppendLineParser.") raise IOError("Refusing to double-open LineParser.")
do_save = self._prepare_save() do_save = self._prepare_save()
if not do_save: if not do_save:
return return

View File

@ -23,14 +23,15 @@ import os
import os.path import os.path
import sip import sip
from PyQt5.QtCore import pyqtSignal, QUrl, QObject, QPoint, QTimer from PyQt5.QtCore import QUrl, QObject, QPoint, QTimer
from PyQt5.QtWidgets import QApplication from PyQt5.QtWidgets import QApplication
import yaml import yaml
from qutebrowser.utils import (standarddir, objreg, qtutils, log, usertypes, from qutebrowser.utils import (standarddir, objreg, qtutils, log, message,
message, utils) utils)
from qutebrowser.commands import cmdexc, cmdutils from qutebrowser.commands import cmdexc, cmdutils
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.completion.models import miscmodels
default = object() # Sentinel value default = object() # Sentinel value
@ -101,14 +102,8 @@ class SessionManager(QObject):
closed. closed.
_current: The name of the currently loaded session, or None. _current: The name of the currently loaded session, or None.
did_load: Set when a session was loaded. 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): def __init__(self, base_path, parent=None):
super().__init__(parent) super().__init__(parent)
self._current = None self._current = None
@ -297,8 +292,7 @@ class SessionManager(QObject):
utils.yaml_dump(data, f) utils.yaml_dump(data, f)
except (OSError, UnicodeEncodeError, yaml.YAMLError) as e: except (OSError, UnicodeEncodeError, yaml.YAMLError) as e:
raise SessionError(e) raise SessionError(e)
else:
self.update_completion.emit()
if load_next_time: if load_next_time:
state_config = objreg.get('state-config') state_config = objreg.get('state-config')
state_config['general']['session'] = name state_config['general']['session'] = name
@ -401,7 +395,7 @@ class SessionManager(QObject):
tab_to_focus = i tab_to_focus = i
if new_tab.data.pinned: if new_tab.data.pinned:
tabbed_browser.set_tab_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: if tab_to_focus is not None:
tabbed_browser.setCurrentIndex(tab_to_focus) tabbed_browser.setCurrentIndex(tab_to_focus)
if win.get('active', False): if win.get('active', False):
@ -419,7 +413,6 @@ class SessionManager(QObject):
os.remove(path) os.remove(path)
except OSError as e: except OSError as e:
raise SessionError(e) raise SessionError(e)
self.update_completion.emit()
def list_sessions(self): def list_sessions(self):
"""Get a list of all session names.""" """Get a list of all session names."""
@ -431,7 +424,7 @@ class SessionManager(QObject):
return sessions return sessions
@cmdutils.register(instance='session-manager') @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): def session_load(self, name, clear=False, temp=False, force=False):
"""Load a session. """Load a session.
@ -459,7 +452,7 @@ class SessionManager(QObject):
win.close() win.close()
@cmdutils.register(instance='session-manager') @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('win_id', win_id=True)
@cmdutils.argument('with_private', flag='p') @cmdutils.argument('with_private', flag='p')
def session_save(self, name: str = default, current=False, quiet=False, def session_save(self, name: str = default, current=False, quiet=False,
@ -498,7 +491,7 @@ class SessionManager(QObject):
message.info("Saved session {}.".format(name)) message.info("Saved session {}.".format(name))
@cmdutils.register(instance='session-manager') @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): def session_delete(self, name, force=False):
"""Delete a session. """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', 'commands', 'signals', 'downloads',
'js', 'qt', 'rfc6266', 'ipc', 'shlexer', 'js', 'qt', 'rfc6266', 'ipc', 'shlexer',
'save', 'message', 'config', 'sessions', 'save', 'message', 'config', 'sessions',
'webelem', 'prompt', 'network' 'webelem', 'prompt', 'network', 'sql'
] ]
@ -141,6 +141,7 @@ sessions = logging.getLogger('sessions')
webelem = logging.getLogger('webelem') webelem = logging.getLogger('webelem')
prompt = logging.getLogger('prompt') prompt = logging.getLogger('prompt')
network = logging.getLogger('network') network = logging.getLogger('network')
sql = logging.getLogger('sql')
ram_handler = None ram_handler = None

View File

@ -107,7 +107,7 @@ def runtime():
if sys.platform.startswith('linux'): if sys.platform.startswith('linux'):
typ = QStandardPaths.RuntimeLocation typ = QStandardPaths.RuntimeLocation
else: # pragma: no cover 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 typ = QStandardPaths.TempLocation
overridden, path = _from_args(typ, _args) 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']) '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 statuses for errors. Needs to be an int for sys.exit.
Exit = enum('Exit', ['ok', 'reserved', 'exception', 'err_ipc', 'err_init', Exit = enum('Exit', ['ok', 'reserved', 'exception', 'err_ipc', 'err_init',
'err_config', 'err_key_config'], is_int=True, start=0) 'err_config', 'err_key_config'], is_int=True, start=0)

View File

@ -28,7 +28,6 @@ import os.path
import collections import collections
import functools import functools
import contextlib import contextlib
import itertools
import socket import socket
import shlex import shlex
@ -378,8 +377,8 @@ def keyevent_to_string(e):
None if only modifiers are pressed.. None if only modifiers are pressed..
""" """
if sys.platform == 'darwin': if sys.platform == 'darwin':
# Qt swaps Ctrl/Meta on OS X, so we switch it back here so the user can # Qt swaps Ctrl/Meta on macOS, so we switch it back here so the user
# use it in the config as expected. See: # can use it in the config as expected. See:
# https://github.com/qutebrowser/qutebrowser/issues/110 # https://github.com/qutebrowser/qutebrowser/issues/110
# http://doc.qt.io/qt-5.4/osx-issues.html#special-keys # http://doc.qt.io/qt-5.4/osx-issues.html#special-keys
modmask2str = collections.OrderedDict([ modmask2str = collections.OrderedDict([
@ -742,25 +741,6 @@ def sanitize_filename(name, replacement='_'):
return name 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): def set_clipboard(data, selection=False):
"""Set the clipboard to some given data.""" """Set the clipboard to some given data."""
if selection and not supports_selection(): if selection and not supports_selection():

View File

@ -45,7 +45,7 @@ except ImportError: # pragma: no cover
import qutebrowser import qutebrowser
from qutebrowser.utils import log, utils, standarddir, usertypes, qtutils 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 from qutebrowser.browser import pdfjs
@ -186,7 +186,6 @@ def _module_versions():
('yaml', ['__version__']), ('yaml', ['__version__']),
('cssutils', ['__version__']), ('cssutils', ['__version__']),
('typing', []), ('typing', []),
('OpenGL', ['__version__']),
('PyQt5.QtWebEngineWidgets', []), ('PyQt5.QtWebEngineWidgets', []),
('PyQt5.QtWebKitWidgets', []), ('PyQt5.QtWebKitWidgets', []),
]) ])
@ -326,11 +325,11 @@ def version():
lines += _module_versions() lines += _module_versions()
lines += ['pdf.js: {}'.format(_pdfjs_version())]
lines += [ 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() qapp = QApplication.instance()

View File

@ -7,4 +7,3 @@ MarkupSafe==1.0
Pygments==2.2.0 Pygments==2.2.0
pyPEG2==2.15.2 pyPEG2==2.15.2
PyYAML==3.12 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.", "asciidoc.py. If not given, it's searched in PATH.",
nargs=2, required=False, nargs=2, required=False,
metavar=('PYTHON', 'ASCIIDOC')) metavar=('PYTHON', 'ASCIIDOC'))
parser.add_argument('--no-authors', help=argparse.SUPPRESS,
action='store_true')
args = parser.parse_args() args = parser.parse_args()
try: try:
os.mkdir('qutebrowser/html/doc') os.mkdir('qutebrowser/html/doc')

View File

@ -64,7 +64,7 @@ def call_tox(toxenv, *args, python=sys.executable):
env['PYTHON'] = python env['PYTHON'] = python
env['PATH'] = os.environ['PATH'] + os.pathsep + os.path.dirname(python) env['PATH'] = os.environ['PATH'] + os.pathsep + os.path.dirname(python)
subprocess.check_call( subprocess.check_call(
[sys.executable, '-m', 'tox', '-v', '-e', toxenv] + list(args), [sys.executable, '-m', 'tox', '-vv', '-e', toxenv] + list(args),
env=env) env=env)
@ -92,7 +92,7 @@ def smoke_test(executable):
'--temp-basedir', 'about:blank', ':later 500 quit']) '--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. """Patch .app to copy missing data and link some libs.
See https://github.com/pyinstaller/pyinstaller/issues/2276 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', '*')): for f in glob.glob(os.path.join(qtwe_core_dir, 'Resources', '*')):
dest = os.path.join(app_path, 'Contents', 'Resources') dest = os.path.join(app_path, 'Contents', 'Resources')
if os.path.isdir(f): 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: else:
print("Copying {} to {}".format(f, dest))
shutil.copy(f, dest) shutil.copy(f, dest)
# Link dependencies # Link dependencies
for lib in ['QtCore', 'QtWebEngineCore', 'QtQuick', 'QtQml', 'QtNetwork', for lib in ['QtCore', 'QtWebEngineCore', 'QtQuick', 'QtQml', 'QtNetwork',
@ -122,37 +125,45 @@ def patch_osx_app():
os.path.join(dest, lib)) os.path.join(dest, lib))
def build_osx(): def build_mac():
"""Build OS X .dmg/.app.""" """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") utils.print_title("Updating 3rdparty content")
# Currently disabled because QtWebEngine has no pdfjs support
# update_3rdparty.run(ace=False, pdfjs=True, fancy_dmg=False) # update_3rdparty.run(ace=False, pdfjs=True, fancy_dmg=False)
utils.print_title("Building .app via pyinstaller") utils.print_title("Building .app via pyinstaller")
call_tox('pyinstaller', '-r') call_tox('pyinstaller', '-r')
utils.print_title("Patching .app") utils.print_title("Patching .app")
patch_osx_app() patch_mac_app()
utils.print_title("Building .dmg") utils.print_title("Building .dmg")
subprocess.check_call(['make', '-f', 'scripts/dev/Makefile-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__) dmg_name = 'qutebrowser-{}.dmg'.format(qutebrowser.__version__)
os.rename('qutebrowser.dmg', dmg_name) os.rename('qutebrowser.dmg', dmg_name)
utils.print_title("Running smoke test") 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): def patch_windows(out_dir):
@ -167,6 +178,7 @@ def patch_windows(out_dir):
def build_windows(): def build_windows():
"""Build windows executables/setups.""" """Build windows executables/setups."""
utils.print_title("Updating 3rdparty content") utils.print_title("Updating 3rdparty content")
# Currently disabled because QtWebEngine has no pdfjs support
# update_3rdparty.run(ace=False, pdfjs=True, fancy_dmg=False) # update_3rdparty.run(ace=False, pdfjs=True, fancy_dmg=False)
utils.print_title("Building Windows binaries") utils.print_title("Building Windows binaries")
@ -203,8 +215,8 @@ def build_windows():
'/DVERSION={}'.format(qutebrowser.__version__), '/DVERSION={}'.format(qutebrowser.__version__),
'misc/qutebrowser.nsi']) 'misc/qutebrowser.nsi'])
name_32 = 'qutebrowser-{}-win32.msi'.format(qutebrowser.__version__) name_32 = 'qutebrowser-{}-win32.exe'.format(qutebrowser.__version__)
name_64 = 'qutebrowser-{}-amd64.msi'.format(qutebrowser.__version__) name_64 = 'qutebrowser-{}-amd64.exe'.format(qutebrowser.__version__)
artifacts += [ artifacts += [
(os.path.join('dist', name_32), (os.path.join('dist', name_32),
@ -280,6 +292,14 @@ def build_sdist():
return artifacts 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): def github_upload(artifacts, tag):
"""Upload the given artifacts to GitHub. """Upload the given artifacts to GitHub.
@ -290,9 +310,7 @@ def github_upload(artifacts, tag):
import github3 import github3
utils.print_title("Uploading to github...") utils.print_title("Uploading to github...")
token_file = os.path.join(os.path.expanduser('~'), '.gh_token') token = read_github_token()
with open(token_file, encoding='ascii') as f:
token = f.read().strip()
gh = github3.login(token=token) gh = github3.login(token=token)
repo = gh.repository('qutebrowser', 'qutebrowser') repo = gh.repository('qutebrowser', 'qutebrowser')
@ -329,6 +347,12 @@ def main():
upload_to_pypi = False 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 os.name == 'nt':
if sys.maxsize > 2**32: if sys.maxsize > 2**32:
# WORKAROUND # WORKAROUND
@ -342,7 +366,7 @@ def main():
artifacts = build_windows() artifacts = build_windows()
elif sys.platform == 'darwin': elif sys.platform == 'darwin':
run_asciidoc2html(args) run_asciidoc2html(args)
artifacts = build_osx() artifacts = build_mac()
else: else:
artifacts = build_sdist() artifacts = build_sdist()
upload_to_pypi = True upload_to_pypi = True

View File

@ -51,9 +51,9 @@ PERFECT_FILES = [
'browser/webkit/cache.py'), 'browser/webkit/cache.py'),
('tests/unit/browser/webkit/test_cookies.py', ('tests/unit/browser/webkit/test_cookies.py',
'browser/webkit/cookies.py'), 'browser/webkit/cookies.py'),
('tests/unit/browser/webkit/test_history.py', ('tests/unit/browser/test_history.py',
'browser/history.py'), 'browser/history.py'),
('tests/unit/browser/webkit/test_history.py', ('tests/unit/browser/test_history.py',
'browser/webkit/webkithistory.py'), 'browser/webkit/webkithistory.py'),
('tests/unit/browser/webkit/http/test_http.py', ('tests/unit/browser/webkit/http/test_http.py',
'browser/webkit/http.py'), 'browser/webkit/http.py'),
@ -117,6 +117,8 @@ PERFECT_FILES = [
'mainwindow/statusbar/textbase.py'), 'mainwindow/statusbar/textbase.py'),
('tests/unit/mainwindow/statusbar/test_url.py', ('tests/unit/mainwindow/statusbar/test_url.py',
'mainwindow/statusbar/url.py'), 'mainwindow/statusbar/url.py'),
('tests/unit/mainwindow/statusbar/test_backforward.py',
'mainwindow/statusbar/backforward.py'),
('tests/unit/mainwindow/test_messageview.py', ('tests/unit/mainwindow/test_messageview.py',
'mainwindow/messageview.py'), 'mainwindow/messageview.py'),
@ -155,9 +157,11 @@ PERFECT_FILES = [
'utils/javascript.py'), 'utils/javascript.py'),
('tests/unit/completion/test_models.py', ('tests/unit/completion/test_models.py',
'completion/models/base.py'), 'completion/models/urlmodel.py'),
('tests/unit/completion/test_sortfilter.py', ('tests/unit/completion/test_histcategory.py',
'completion/models/sortfilter.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 exit 0
fi 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 pip
pip_install -r misc/requirements/requirements-tox.txt 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 docker run --privileged -v $PWD:/outside -e QUTE_BDD_WEBENGINE=$QUTE_BDD_WEBENGINE -e DOCKER=$DOCKER -e CI=$CI qutebrowser/travis:$DOCKER
else else
args=() args=()
[[ $TESTENV == docs ]] && args=('--no-authors')
[[ $TRAVIS_OS_NAME == osx ]] && args=('--qute-bdd-webengine' '--no-xvfb') [[ $TRAVIS_OS_NAME == osx ]] && args=('--qute-bdd-webengine' '--no-xvfb')
tox -e $TESTENV -- "${args[@]}" 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 # vulture doesn't notice the hasattr() and thus thinks netrc_used is unused
# in NetworkManager.on_authentication_required # in NetworkManager.on_authentication_required
yield 'PyQt5.QtNetwork.QNetworkReply.netrc_used' 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']: for attr in ['fileno', 'truncate', 'closed', 'readable']:
yield 'qutebrowser.utils.qtutils.PyQIODevice.' + attr yield 'qutebrowser.utils.qtutils.PyQIODevice.' + attr
@ -123,7 +129,7 @@ def filter_func(item):
True if the missing function should be filtered/ignored, False True if the missing function should be filtered/ignored, False
otherwise. 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): def report(items):
@ -137,7 +143,7 @@ def report(items):
relpath = os.path.relpath(item.filename) relpath = os.path.relpath(item.filename)
path = relpath if not relpath.startswith('..') else item.filename path = relpath if not relpath.startswith('..') else item.filename
output.append("{}:{}: Unused {} '{}'".format(path, item.lineno, output.append("{}:{}: Unused {} '{}'".format(path, item.lineno,
item.typ, item)) item.typ, item.name))
return output return output

View File

@ -26,7 +26,6 @@ import shutil
import os.path import os.path
import inspect import inspect
import subprocess import subprocess
import collections
import tempfile import tempfile
import argparse import argparse
@ -43,7 +42,7 @@ from qutebrowser.utils import docutils, usertypes
FILE_HEADER = """ FILE_HEADER = """
// DO NOT EDIT THIS FILE DIRECTLY! // DO NOT EDIT THIS FILE DIRECTLY!
// It is autogenerated from docstrings by running: // It is autogenerated by running:
// $ python3 scripts/dev/src2asciidoc.py // $ python3 scripts/dev/src2asciidoc.py
""".lstrip() """.lstrip()
@ -415,32 +414,6 @@ def generate_settings(filename):
_generate_setting_option(f, opt) _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): def _format_block(filename, what, data):
"""Format a block in a file. """Format a block in a file.
@ -487,12 +460,6 @@ def _format_block(filename, what, data):
shutil.move(tmpname, filename) 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): def regenerate_manpage(filename):
"""Update manpage OPTIONS using an argparse parser.""" """Update manpage OPTIONS using an argparse parser."""
# pylint: disable=protected-access # pylint: disable=protected-access
@ -538,9 +505,6 @@ def main():
generate_settings('doc/help/settings.asciidoc') generate_settings('doc/help/settings.asciidoc')
print("Generating command help...") print("Generating command help...")
generate_commands('doc/help/commands.asciidoc') 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: if '--cheatsheet' in sys.argv:
print("Regenerating cheatsheet .pngs") print("Regenerating cheatsheet .pngs")
regenerate_cheatsheet() 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"), ('posix', os.name != 'posix', "Requires a POSIX os"),
('windows', os.name != 'nt', "Requires Windows"), ('windows', os.name != 'nt', "Requires Windows"),
('linux', not sys.platform.startswith('linux'), "Requires Linux"), ('linux', not sys.platform.startswith('linux'), "Requires Linux"),
('osx', sys.platform != 'darwin', "Requires OS X"), ('mac', sys.platform != 'darwin', "Requires macOS"),
('not_osx', sys.platform == 'darwin', "Skipped on OS X"), ('not_mac', sys.platform == 'darwin', "Skipped on macOS"),
('not_frozen', getattr(sys, 'frozen', False), ('not_frozen', getattr(sys, 'frozen', False),
"Can't be run when frozen"), "Can't be run when frozen"),
('frozen', not getattr(sys, 'frozen', False), ('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()), not config.webengine and qtutils.is_qtwebkit_ng()),
('qtwebengine_flaky', 'Flaky with QtWebEngine', pytest.mark.skipif, ('qtwebengine_flaky', 'Flaky with QtWebEngine', pytest.mark.skipif,
config.webengine), 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'), 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 Scenario: Using command completion
When I run :set-cmd-text : When I run :set-cmd-text :
Then the completion model should be CommandCompletionModel Then the completion model should be command
Scenario: Using help completion Scenario: Using help completion
When I run :set-cmd-text -s :help 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 Scenario: Using quickmark completion
When I run :set-cmd-text -s :quickmark-load 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 Scenario: Using bookmark completion
When I run :set-cmd-text -s :bookmark-load 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 Scenario: Using bind completion
When I run :set-cmd-text -s :bind X 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 Scenario: Using session completion
Given I open data/hello.txt Given I open data/hello.txt
@ -60,43 +60,13 @@ Feature: Using completion
And I run :command-accept And I run :command-accept
Then the error "Session hello not found!" should be shown 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 Scenario: Using value completion
# When I run :set-cmd-text -s :set colors When I run :set-cmd-text -s :set colors statusbar.bg
# Then the completion model should be SettingOptionCompletionModel Then the completion model should be value
# 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: Deleting an open tab via the completion Scenario: Deleting an open tab via the completion
Given I have a fresh instance 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 if (not hasattr(report.longrepr, 'addsection') or
not hasattr(report, 'scenario')): not hasattr(report, 'scenario')):
# In some conditions (on OS X and Windows it seems), report.longrepr is # In some conditions (on macOS and Windows it seems), report.longrepr
# actually a tuple. This is handled similarily in pytest-qt too. # 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 # Since this hook is invoked for any test, we also need to skip it for
# non-BDD ones. # non-BDD ones.

View File

@ -22,6 +22,20 @@ Feature: Downloading things from a website.
And I wait until the download is finished And I wait until the download is finished
Then the downloaded file download.bin should exist 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 Scenario: Using hints
When I set downloads.location.prompt to false When I set downloads.location.prompt to false
And I open data/downloads/downloads.html 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 And I wait until the download is finished
Then the downloaded file content-size should exist Then the downloaded file content-size should exist
@posix
Scenario: Downloading to unwritable destination Scenario: Downloading to unwritable destination
When I set downloads.location.prompt to false When I set downloads.location.prompt to false
And I run :download http://localhost:(port)/data/downloads/download.bin --dest (tmpdir)/downloads/unwritable 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 @qtwebengine_skip: We can't get the UA from the page there
Scenario: user-agent when using :download Scenario: user-agent when using :download
When I open user-agent When I open user-agent
And I run :download And I run :download --dest user-agent
And I wait until the download is finished And I wait until the download is finished
Then the downloaded file user-agent should contain Safari/ Then the downloaded file user-agent should contain Safari/

View File

@ -243,7 +243,7 @@ Feature: Using hints
### hints.auto_follow.timeout ### hints.auto_follow.timeout
@not_osx @not_mac
Scenario: Ignoring key presses after auto-following hints Scenario: Ignoring key presses after auto-following hints
When I set hints.auto_follow_timeout to 1000 When I set hints.auto_follow_timeout to 1000
And I set hints.mode to number And I set hints.mode to number

View File

@ -11,44 +11,44 @@ Feature: Page history
Scenario: Simple history saving Scenario: Simple history saving
When I open data/numbers/1.txt When I open data/numbers/1.txt
And I open data/numbers/2.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/1.txt
http://localhost:(port)/data/numbers/2.txt http://localhost:(port)/data/numbers/2.txt
Scenario: History item with title Scenario: History item with title
When I open data/title.html 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 http://localhost:(port)/data/title.html Test title
Scenario: History item with redirect Scenario: History item with redirect
When I open redirect-to?url=data/title.html without waiting When I open redirect-to?url=data/title.html without waiting
And I wait until data/title.html is loaded 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 r http://localhost:(port)/redirect-to?url=data/title.html Test title
http://localhost:(port)/data/title.html Test title http://localhost:(port)/data/title.html Test title
Scenario: History item with spaces in URL Scenario: History item with spaces in URL
When I open data/title with spaces.html 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 http://localhost:(port)/data/title%20with%20spaces.html Test title
Scenario: History item with umlauts Scenario: History item with umlauts
When I open data/äöü.html 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 http://localhost:(port)/data/%C3%A4%C3%B6%C3%BC.html Chäschüechli
@flaky @qtwebengine_todo: Error page message is not implemented @flaky @qtwebengine_todo: Error page message is not implemented
Scenario: History with an error Scenario: History with an error
When I run :open file:///does/not/exist 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 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 file:///does/not/exist Error loading page: file:///does/not/exist
@qtwebengine_todo: Error page message is not implemented @qtwebengine_todo: Error page message is not implemented
Scenario: History with a 404 Scenario: History with a 404
When I open status/404 without waiting When I open status/404 without waiting
And I wait for "Error while loading http://localhost:*/status/404: NOT FOUND" in the log 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 http://localhost:(port)/status/404 Error loading page: http://localhost:(port)/status/404
Scenario: History with invalid URL Scenario: History with invalid URL
@ -61,32 +61,32 @@ Feature: Page history
When I open data/data_link.html When I open data/data_link.html
And I run :click-element id link And I run :click-element id link
And I wait until data:;base64,cXV0ZWJyb3dzZXI= is loaded 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 http://localhost:(port)/data/data_link.html data: link
Scenario: History with view-source URL Scenario: History with view-source URL
When I open data/title.html When I open data/title.html
And I run :view-source And I run :view-source
And I wait for "Changing title for idx * to 'Source for http://localhost:*/data/title.html'" in the log 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 http://localhost:(port)/data/title.html Test title
Scenario: Clearing history Scenario: Clearing history
When I open data/title.html When I open data/title.html
And I run :history-clear --force And I run :history-clear --force
Then the history file should be empty Then the history should be empty
Scenario: Clearing history with confirmation Scenario: Clearing history with confirmation
When I open data/title.html When I open data/title.html
And I run :history-clear And I run :history-clear
And I wait for "Asking question <* title='Clear all browsing history?'>, *" in the log And I wait for "Asking question <* title='Clear all browsing history?'>, *" in the log
And I run :prompt-accept yes 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 Scenario: History with yanked URL and 'add to history' flag
When I open data/hints/html/simple.html When I open data/hints/html/simple.html
And I hint with args "--add-history links yank" and follow a 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/hints/html/simple.html Simple link
http://localhost:(port)/data/hello.txt 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() And I run :debug-pyeval QApplication.instance().activeModalWidget().close()
Then no crash should happen 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. # printers" qWarning.
# #
# Disabled because it causes weird segfaults and QPainter warnings in Qt... # 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 wait for "Renderer process was killed" in the log
And I open data/numbers/3.txt And I open data/numbers/3.txt
Then no crash should happen 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 And I run :click-element id button
Then the javascript message "geolocation permission denied" should be logged 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 Scenario: Always accepting geolocation
When I set content.geolocation to true When I set content.geolocation to true
And I open data/prompt/geolocation.html in a new tab And I open data/prompt/geolocation.html in a new tab
And I run :click-element id button And I run :click-element id button
Then the javascript message "geolocation permission denied" should not be logged 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 Scenario: geolocation with ask -> true
When I set content.geolocation to ask When I set content.geolocation to ask
And I open data/prompt/geolocation.html in a new tab 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 Scenario: Running :spawn with userscript that expects the stdin getting closed
When I run :spawn -u (testdata)/userscripts/stdinclose.py When I run :spawn -u (testdata)/userscripts/stdinclose.py
Then the message "stdin closed" should be shown 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/2.txt (pinned)
- data/numbers/3.txt (active) - 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 Scenario: Pinned :tab-close prompt yes
When I open data/numbers/1.txt When I open data/numbers/1.txt
And I run :tab-pin 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}")) @bdd.then(bdd.parsers.parse("the completion model should be {model}"))
def check_model(quteproc, model): def check_model(quteproc, model):
"""Make sure the completion model was set to something.""" """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) quteproc.wait_for(message=pattern)

View File

@ -21,6 +21,7 @@ import os
import sys import sys
import shlex import shlex
import pytest
import pytest_bdd as bdd import pytest_bdd as bdd
bdd.scenarios('downloads.feature') bdd.scenarios('downloads.feature')
@ -53,6 +54,14 @@ def clean_old_downloads(quteproc):
quteproc.send_cmd(':download-clear') 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") @bdd.when("I wait until the download is finished")
def wait_for_download_finished(quteproc): def wait_for_download_finished(quteproc):
quteproc.wait_for(category='downloads', message='Download * finished') 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 # You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. # along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
import os.path import logging
import re
import pytest_bdd as bdd import pytest_bdd as bdd
bdd.scenarios('history.feature') bdd.scenarios('history.feature')
@bdd.then(bdd.parsers.parse("the history file should contain:\n{expected}")) @bdd.then(bdd.parsers.parse("the history should contain:\n{expected}"))
def check_history(quteproc, httpbin, expected): def check_history(quteproc, httpbin, tmpdir, expected):
history_file = os.path.join(quteproc.basedir, 'data', 'history') path = tmpdir / 'history'
quteproc.send_cmd(':save history') quteproc.send_cmd(':debug-dump-history "{}"'.format(path))
quteproc.wait_for(message=':save saved history') 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: expected = expected.replace('(port)', str(httpbin.port))
lines = [] assert actual == expected
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
@bdd.then("the history file should be empty") @bdd.then("the history should be empty")
def check_history_empty(quteproc, httpbin): def check_history_empty(quteproc, httpbin, tmpdir):
check_history(quteproc, httpbin, '') check_history(quteproc, httpbin, tmpdir, '')

View File

@ -291,7 +291,7 @@ Feature: Yanking and pasting.
# Compare # Compare
Then the javascript message "textarea contents: onHello worlde two three four" should be logged 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 Scenario: Inserting text into a text field with undo
When I set content.javascript.log to info When I set content.javascript.log to info
And I open data/paste_primary.html 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) httpbin_log = getattr(item, '_httpbin_log', None)
if not hasattr(report.longrepr, 'addsection'): if not hasattr(report.longrepr, 'addsection'):
# In some conditions (on OS X and Windows it seems), report.longrepr is # In some conditions (on macOS and Windows it seems), report.longrepr
# actually a tuple. This is handled similarily in pytest-qt too. # is actually a tuple. This is handled similarily in pytest-qt too.
return return
if pytest.config.getoption('--capture') == 'no': 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.not_frozen
@pytest.mark.flaky # Fails sometimes with empty output...
def test_version(request): def test_version(request):
"""Test invocation with --version argument.""" """Test invocation with --version argument."""
args = ['-m', 'qutebrowser', '--version'] + _base_args(request.config) args = ['-m', 'qutebrowser', '--version'] + _base_args(request.config)

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