Merge remote-tracking branch 'upstream/master' into HEAD
This commit is contained in:
commit
71b71dbc58
3
.flake8
3
.flake8
@ -11,6 +11,7 @@ exclude = .*,__pycache__,resources.py
|
||||
# (for pytest's __tracebackhide__)
|
||||
# F401: Unused import
|
||||
# N802: function name should be lowercase
|
||||
# N806: variable in function should be lowercase
|
||||
# P101: format string does contain unindexed parameters
|
||||
# P102: docstring does contain unindexed parameters
|
||||
# P103: other string does contain unindexed parameters
|
||||
@ -38,7 +39,7 @@ putty-ignore =
|
||||
/# pragma: no mccabe/ : +C901
|
||||
tests/*/test_*.py : +D100,D101,D401
|
||||
tests/conftest.py : +F403
|
||||
tests/unit/browser/webkit/test_history.py : +N806
|
||||
tests/unit/browser/test_history.py : +N806
|
||||
tests/helpers/fixtures.py : +N806
|
||||
tests/unit/browser/webkit/http/test_content_disposition.py : +D400
|
||||
scripts/dev/ci/appveyor_install.py : +FI53
|
||||
|
8
.github/CODEOWNERS
vendored
Normal file
8
.github/CODEOWNERS
vendored
Normal 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
|
@ -23,14 +23,18 @@ matrix:
|
||||
language: python
|
||||
python: 3.6
|
||||
env: TESTENV=py36-pyqt571
|
||||
- os: linux
|
||||
language: python
|
||||
python: 3.6
|
||||
env: TESTENV=py36-pyqt58
|
||||
- os: linux
|
||||
language: python
|
||||
python: 3.5
|
||||
env: TESTENV=py35-pyqt58
|
||||
env: TESTENV=py35-pyqt59
|
||||
- os: linux
|
||||
language: python
|
||||
python: 3.6
|
||||
env: TESTENV=py36-pyqt58
|
||||
env: TESTENV=py36-pyqt59
|
||||
- os: osx
|
||||
env: TESTENV=py36 OSX=elcapitan
|
||||
osx_image: xcode7.3
|
||||
|
@ -14,9 +14,69 @@ This project adheres to http://semver.org/[Semantic Versioning].
|
||||
// `Fixed` for any bug fixes.
|
||||
// `Security` to invite users to upgrade in case of vulnerabilities.
|
||||
|
||||
v0.11.0 (unreleased)
|
||||
v1.0.0 (unreleased)
|
||||
-------------------
|
||||
|
||||
Breaking changes
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
- Support for legacy QtWebKit (before 5.212 which is distributed
|
||||
independently from Qt) is dropped.
|
||||
- Support for Python 3.4 is dropped.
|
||||
- Support for Qt before 5.7 is dropped.
|
||||
- New dependency on the QtSql module and Qt sqlite support.
|
||||
- New dependency on ruamel.yaml; dropped PyYAML dependency.
|
||||
- The QtWebEngine backend is now used by default if available.
|
||||
- New config system which ignores the old config file.
|
||||
- The depedency on PyOpenGL (when using QtWebEngine) got removed. Note
|
||||
that PyQt5.QtOpenGL is still a dependency.
|
||||
|
||||
Major changes
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
- New completion engine based on sqlite, which allows to complete
|
||||
the entire browsing history.
|
||||
- Completely rewritten configuration system.
|
||||
|
||||
Added
|
||||
~~~~~
|
||||
|
||||
- New back/forward indicator in the statusbar
|
||||
|
||||
Changed
|
||||
~~~~~~~
|
||||
|
||||
- Upgrading qutebrowser with a version older than v0.4.0 still running now won't
|
||||
work properly anymore.
|
||||
- Using `:download` now uses the page's title as filename.
|
||||
- Using `:back` or `:forward` with a count now skips intermediate pages.
|
||||
- When there are multiple messages shown, the timeout is increased.
|
||||
- `:search` now only clears the search if one was displayed before, so pressing
|
||||
`<Escape>` doesn't un-focus inputs anymore.
|
||||
|
||||
Fixes
|
||||
~~~~~
|
||||
|
||||
- Exiting fullscreen via `:fullscreen` or buttons on a page now
|
||||
restores the correct previous window state (maximized/fullscreen).
|
||||
|
||||
v0.11.1 (unreleased)
|
||||
--------------------
|
||||
|
||||
Fixes
|
||||
~~~~~
|
||||
|
||||
- Fixed empty space being shown after tabs in the tabbar in some cases.
|
||||
- Fixed `:restart` in private browsing mode.
|
||||
- Fixed printing on macOS.
|
||||
- Closing a pinned tab via mouse now also prompts for confirmation.
|
||||
- The "try again" button on error pages works correctly again.
|
||||
- :spawn -u -d is now disallowed.
|
||||
- :spawn -d shows error messages correctly now.
|
||||
|
||||
v0.11.0
|
||||
-------
|
||||
|
||||
New dependencies
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
@ -28,7 +88,10 @@ New dependencies
|
||||
Added
|
||||
~~~~~
|
||||
|
||||
- New `-p` flag for `:open` to open a private window.
|
||||
- Private browsing is now implemented for QtWebEngine, *and changed its
|
||||
behavior*: The `general -> private-browsing` setting now only applies to newly
|
||||
opened windows, and you can use the `-p` flag to `:open` to open a private
|
||||
window.
|
||||
- New "pinned tabs" feature, with a new `:tab-pin` command (bound
|
||||
to `<Ctrl-p>` by default).
|
||||
- (QtWebEngine) Implemented `:follow-selected`.
|
||||
@ -45,6 +108,8 @@ Added
|
||||
customize statusbar colors for private windows.
|
||||
- New `{private}` field displaying `[Private Mode]` for
|
||||
`ui -> window-title-format` and `tabs -> title-format`.
|
||||
- (QtWebEngine) Proxy support with Qt 5.7.1 (already was supported for 5.8 and
|
||||
newer)
|
||||
|
||||
Changed
|
||||
~~~~~~~
|
||||
@ -52,62 +117,51 @@ Changed
|
||||
- To prevent elaborate phishing attacks, the Punycode version (`xn--*`) is now
|
||||
shown in addition to the decoded version for international domain names
|
||||
(IDN).
|
||||
- Private browsing is now implemented for QtWebEngine, and changed it's
|
||||
behavior: The `general -> private-browsing` setting now only applies to newly
|
||||
opened windows, and you can use the `-p` flag to `:open` to open a private
|
||||
window.
|
||||
- Improved `qute://history` page (with lazy loading)
|
||||
- Starting with legacy QtWebKit now shows a warning message once.
|
||||
- Crash reports are not public anymore.
|
||||
- Paths like `C:` are now treated as absolute paths on Windows for downloads,
|
||||
and invalid paths are handled properly.
|
||||
- PAC on QtWebKit now supports SOCKS5 as type.
|
||||
- Comments in the config file are now before the individual options instead of
|
||||
being before sections.
|
||||
- Messages are now hidden when clicked.
|
||||
- stdin is now closed immediately for processes spawned from qutebrowser.
|
||||
- When `ui -> message-timeout` is set to 0, messages are now never cleared.
|
||||
- Middle/right-clicking the blank parts of the tab bar (when vertical) now
|
||||
closes the current tab.
|
||||
- (QtWebEngine) With Qt 5.9, `content -> cookies-store` can now be set without
|
||||
a restart.
|
||||
- (QtWebEngine) With Qt 5.9, better error messages are now shown for failed
|
||||
downloads.
|
||||
- The adblocker now also blocks non-GET requests (e.g. POST).
|
||||
- `javascript:` links can now be hinted.
|
||||
- `:view-source`, `:tab-clone` and `:navigate --tab` now don't open the tab as
|
||||
"explicit" anymore, i.e. (with the default settings) open it next to the
|
||||
active tab.
|
||||
- (QtWebEngine) The underlying Chromium version is now shown in the version
|
||||
info.
|
||||
- `qute:*` pages now use `qute://*` instead (e.g. `qute://version` instead of
|
||||
`qute:version`), but the old versions are automatically redirected.
|
||||
- Starting with legacy QtWebKit now shows a warning message.
|
||||
*With the next release, support for it will be removed.*
|
||||
- The Windows releases are redone from scratch, which means:
|
||||
- They now use the new QtWebEngine backend
|
||||
- The bundled Qt is updated from 5.5 to 5.9
|
||||
- The bundled Python is updated from 3.4 to 3.6
|
||||
- They are now generated with PyInstaller instead of cx_Freeze
|
||||
- The installer is now generated using NSIS instead of being a MSI
|
||||
- Improved `qute://history` page (with lazy loading)
|
||||
- Crash reports are not public anymore.
|
||||
- Paths like `C:` are now treated as absolute paths on Windows for downloads,
|
||||
and invalid paths are handled properly.
|
||||
- Comments in the config file are now placed before the individual options
|
||||
instead of being before sections.
|
||||
- Messages are now hidden when clicked.
|
||||
- stdin is now closed immediately for processes spawned from qutebrowser.
|
||||
- When `ui -> message-timeout` is set to 0, messages are now never cleared.
|
||||
- Middle/right-clicking the blank parts of the tab bar (when vertical) now
|
||||
closes the current tab.
|
||||
- The adblocker now also blocks non-GET requests (e.g. POST).
|
||||
- `javascript:` links can now be hinted.
|
||||
- `:view-source`, `:tab-clone` and `:navigate --tab` now don't open the tab as
|
||||
"explicit" anymore, i.e. (with the default settings) open it next to the
|
||||
active tab.
|
||||
- `qute:*` pages now use `qute://*` instead (e.g. `qute://version` instead of
|
||||
`qute:version`), but the old versions are automatically redirected.
|
||||
- Texts in prompts are now selectable.
|
||||
- Renderer process crashes now show an error page.
|
||||
- (QtWebKit) storage -> offline-web-application-storage` got renamed to `...-cache`
|
||||
- The default level for `:messages` is now `info`, not `error`
|
||||
- Trying to focus the currently focused tab with `:tab-focus` now focuses the
|
||||
last viewed tab.
|
||||
- (QtWebEngine) With Qt 5.9, `content -> cookies-store` can now be set without
|
||||
a restart.
|
||||
- (QtWebEngine) With Qt 5.9, better error messages are now shown for failed
|
||||
downloads.
|
||||
- (QtWebEngine) The underlying Chromium version is now shown in the version
|
||||
info.
|
||||
- (QtWebKit) Renderer process crashes now show an error page on Qt 5.9 or newer.
|
||||
- (QtWebKit) storage -> offline-web-application-storage` got renamed to `...-cache`
|
||||
- (QtWebKit) PAC now supports SOCKS5 as type.
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
|
||||
- The macOS .dmg is now built against Qt 5.9 which fixes various
|
||||
important issues (such as not being able to type dead keys).
|
||||
- (QtWebEngine) Added a workaround for a black screen with some setups
|
||||
(the workaround requires PyOpenGL to be installed, but it's optional)
|
||||
- (QtWebEngine) Starting with Nouveau graphics now shows an error message
|
||||
instead of crashing in Qt. This adds a new dependency on `PyQt5.QtOpenGL`.
|
||||
- (QtWebEngine) Retrying downloads now shows an error instead of crashing.
|
||||
- (QtWebEngine) Cloning a view-source tab now doesn't crash anymore.
|
||||
- (QtWebKit) The HTTP cache is disabled on Qt 5.7.1 and 5.8 now as it leads to
|
||||
frequent crashes due to a Qt bug.
|
||||
- Fixed crash with `:download` on PyQt 5.9.
|
||||
- Cloning a page without history doesn't crash anymore.
|
||||
- When a download results in a HTTP error, it now shows the error correctly
|
||||
@ -117,7 +171,6 @@ Fixed
|
||||
- Fixed crash when unbinding an unbound key in the key config.
|
||||
- Fixed crash when using `:debug-log-filter` when `--filter` wasn't given on startup.
|
||||
- Fixed crash with some invalid setting values.
|
||||
- (QtWebKit) Fixed Crash when a PAC file returns an invalid value.
|
||||
- Continuing a search after clearing it now works correctly.
|
||||
- The tabbar and completion should now be more consistently and correctly
|
||||
styled with various system styles.
|
||||
@ -125,19 +178,27 @@ Fixed
|
||||
- The validation for colors in stylesheets is now less strict,
|
||||
allowing for all valid Qt values.
|
||||
- `data:` URLs now aren't added to the history anymore.
|
||||
- (QtWebEngine) `window.navigator.userAgent` is now set correctly when
|
||||
customizing the user agent.
|
||||
- Accidentally starting with Python 2 now shows a proper error message again.
|
||||
- (QtWebEngine) HTML fullscreen is now tracked for each tab separately, which
|
||||
means it's not possible anymore to accidentally get stuck in fullscreen state
|
||||
by closing a tab with a fullscreen video.
|
||||
- For some people, running some userscripts crashed - this should now be fixed.
|
||||
- Various other rare crashes should now be fixed.
|
||||
- The settings documentation was truncated with v0.10.1 which should now be
|
||||
fixed.
|
||||
- Scrolling to an anchor in a background tab now works correctly, and javascript
|
||||
gets the correct window size for background tabs.
|
||||
- (QtWebEngine) `:scroll-page` with `--bottom-navigate` now works correctly
|
||||
- (QtWebEngine) Added a workaround for a black screen with some setups
|
||||
- (QtWebEngine) Starting with Nouveau graphics now shows an error message
|
||||
instead of crashing in Qt.
|
||||
- (QtWebEngine) Retrying downloads now shows an error instead of crashing.
|
||||
- (QtWebEngine) Cloning a view-source tab now doesn't crash anymore.
|
||||
- (QtWebEngine) `window.navigator.userAgent` is now set correctly when
|
||||
customizing the user agent.
|
||||
- (QtWebEngine) HTML fullscreen is now tracked for each tab separately, which
|
||||
means it's not possible anymore to accidentally get stuck in fullscreen state
|
||||
by closing a tab with a fullscreen video.
|
||||
- (QtWebEngine) `:scroll-page` with `--bottom-navigate` now works correctly.
|
||||
- (QtWebKit) The HTTP cache is disabled on Qt 5.7.1 and 5.8 now as it leads to
|
||||
frequent crashes due to a Qt bug.
|
||||
- (QtWebKit) Fixed Crash when a PAC file returns an invalid value.
|
||||
|
||||
v0.10.1
|
||||
-------
|
||||
@ -182,7 +243,7 @@ Added
|
||||
- Open tabs are now auto-saved on each successful load and restored in case of a crash
|
||||
- `:jseval` now has a `--file` flag so you can pass a javascript file
|
||||
- `:session-save` now has a `--only-active-window` flag to only save the active window
|
||||
- OS X builds are back, and built with QtWebEngine
|
||||
- macOS builds are back, and built with QtWebEngine
|
||||
|
||||
Changed
|
||||
~~~~~~~
|
||||
@ -484,7 +545,7 @@ Fixed
|
||||
- Fix crash when pressing enter without a command
|
||||
- Adjust error message to point out QtWebEngine is unsupported with the OS
|
||||
X .app currently.
|
||||
- Hide Harfbuzz warning with the OS X .app
|
||||
- Hide Harfbuzz warning with the macOS .app
|
||||
|
||||
v0.8.0
|
||||
------
|
||||
@ -847,7 +908,7 @@ Fixed
|
||||
- Fixed scrolling to the very left/right with `:scroll-perc`.
|
||||
- Using an external editor should now work correctly with some funny chars
|
||||
(U+2028/U+2029/BOM).
|
||||
- Movements in caret mode now should work correctly on OS X and Windows.
|
||||
- Movements in caret mode now should work correctly on macOS and Windows.
|
||||
- Fixed upgrade from earlier config versions.
|
||||
- Fixed crash when killing a running userscript.
|
||||
- Fixed characters being passed through when shifted with
|
||||
@ -922,7 +983,7 @@ Changed
|
||||
- The completion widget doesn't show a border anymore.
|
||||
- The tabbar doesn't display ugly arrows anymore if there isn't enough space
|
||||
for all tabs.
|
||||
- Some insignificant Qt warnings which were printed on OS X are now hidden.
|
||||
- Some insignificant Qt warnings which were printed on macOS are now hidden.
|
||||
- Better support for Qt 5.5 and Python 3.5.
|
||||
|
||||
Fixed
|
||||
@ -1033,7 +1094,7 @@ Fixed
|
||||
- Fixed AssertionError when closing many windows quickly.
|
||||
- Various fixes for deprecated key bindings and auto-migrations.
|
||||
- Workaround for qutebrowser not starting when there are NUL-bytes in the history (because of a currently unknown bug).
|
||||
- Fixed handling of keybindings containing Ctrl/Meta on OS X.
|
||||
- Fixed handling of keybindings containing Ctrl/Meta on macOS.
|
||||
- Fixed crash when downloading a URL without filename (e.g. magnet links) via "Save as...".
|
||||
- Fixed exception when starting qutebrowser with `:set` as argument.
|
||||
- Fixed horrible completion performance when the `shrink` option was set.
|
||||
@ -1131,7 +1192,7 @@ Changed
|
||||
- Add a `:search` command in addition to `/foo` so it's more visible and can be used from scripts.
|
||||
- Various improvements to documentation, logging, and the crash reporter.
|
||||
- Expand `~` to the users home directory with `:run-userscript`.
|
||||
- Improve the userscript runner on Linux/OS X by using `QSocketNotifier`.
|
||||
- Improve the userscript runner on Linux/macOS by using `QSocketNotifier`.
|
||||
- Add luakit-like `gt`/`gT` keybindings to cycle through tabs.
|
||||
- Show default value for config values in the completion.
|
||||
- Clone tab icon, tab text and zoom level when cloning tabs.
|
||||
@ -1151,7 +1212,7 @@ Changed
|
||||
* `init_venv.py` and `run_checks.py` have been replaced by http://tox.readthedocs.org/[tox]. Install tox and run `tox -e mkvenv` instead.
|
||||
* The tests now use http://pytest.org/[pytest]
|
||||
* Many new tests added
|
||||
* Mac Mini buildbot to run the tests on OS X.
|
||||
* Mac Mini buildbot to run the tests on macOS.
|
||||
* Coverage recording via http://nedbatchelder.com/code/coverage/[coverage.py].
|
||||
* New `--pdb-postmortem argument` to drop into the pdb debugger on exceptions.
|
||||
* Use https://github.com/ionelmc/python-hunter[hunter] for line tracing instead of a selfmade solution.
|
||||
@ -1287,7 +1348,7 @@ Fixed
|
||||
|
||||
* Fix rare exception when a key is pressed shortly after opening a window
|
||||
* Fix exception with certain invalid URLs like `http:foo:0`
|
||||
* Work around Qt bug which renders checkboxes on OS X unusable
|
||||
* Work around Qt bug which renders checkboxes on macOS unusable
|
||||
* Fix exception when a local files can't be read in `:adblock-update`
|
||||
* Hide 2 more Qt warnings.
|
||||
* Add `!important` to hint CSS so websites don't override the hint look
|
||||
@ -1323,7 +1384,7 @@ Changes
|
||||
* Set zoom to default instead of 100% with `:zoom`/`=`.
|
||||
* Adjust page zoom if default zoom changed.
|
||||
* Force tabs to be focused on `:undo`.
|
||||
* Replace manual installation instructions on OS X with homebrew/macports.
|
||||
* Replace manual installation instructions on macOS with homebrew/macports.
|
||||
* Allow min-/maximizing of print preview on Windows.
|
||||
* Various documentation improvements.
|
||||
* Various other small improvements and cleanups.
|
||||
|
@ -5,6 +5,12 @@ The Compiler <mail@qutebrowser.org>
|
||||
:data-uri:
|
||||
:toc:
|
||||
|
||||
IMPORTANT: I'm currently (July 2017) more busy than usual until September,
|
||||
because of exams coming up. In addition to that, a new config system is coming
|
||||
which will conflict with many non-trivial contributions. Because of that, please
|
||||
refrain from contributing new features until then. If you're reading this note
|
||||
after mid-September, please open an issue.
|
||||
|
||||
I `<3` footnote:[Of course, that says `<3` in HTML.] contributors!
|
||||
|
||||
This document contains guidelines for contributing to qutebrowser, as well as
|
||||
@ -39,8 +45,8 @@ pointers:
|
||||
|
||||
* https://github.com/qutebrowser/qutebrowser/labels/easy[Issues which should
|
||||
be easy to solve]
|
||||
* https://github.com/qutebrowser/qutebrowser/labels/not%20code[Issues which
|
||||
require little/no coding]
|
||||
* https://github.com/qutebrowser/qutebrowser/labels/component%3A%20docs[Documentation
|
||||
* issues which require little/no coding]
|
||||
|
||||
If you prefer C++ or Javascript to Python, see the relevant issues which involve
|
||||
work in those languages:
|
||||
@ -682,8 +688,9 @@ qutebrowser release
|
||||
|
||||
* Add newest config to `tests/unit/config/old_configs` and update `test_upgrade_version`
|
||||
- `python -m qutebrowser --basedir conf :quit`
|
||||
- `sed '/^#/d' conf/config/qutebrowser.conf > tests/unit/config/old_configs/qutebrowser-v0.x.y.conf`
|
||||
- `sed '/^#/d' conf/config/qutebrowser.conf > tests/unit/config/old_configs/qutebrowser-v0.$x.$y.conf`
|
||||
- `rm -r conf`
|
||||
- git add
|
||||
- commit
|
||||
* Adjust `__version_info__` in `qutebrowser/__init__.py`.
|
||||
* Update changelog (remove *(unreleased)*)
|
||||
@ -698,8 +705,8 @@ qutebrowser release
|
||||
as closed.
|
||||
|
||||
* Linux: Run `python3 scripts/dev/build_release.py --upload v0.$x.$y`
|
||||
* Windows: Run `C:\Python34_x32\python scripts\dev\build_release.py --asciidoc C:\Python27\python C:\asciidoc-8.6.9\asciidoc.py --upload v0.X.Y` (replace X/Y by hand)
|
||||
* OS X: Run `python3 scripts/dev/build_release.py --upload v0.X.Y` (replace X/Y by hand)
|
||||
* Windows: Run `C:\Python36-32\python scripts\dev\build_release.py --asciidoc C:\Python27\python C:\asciidoc-8.6.9\asciidoc.py --upload v0.X.Y` (replace X/Y by hand)
|
||||
* macOS: Run `python3 scripts/dev/build_release.py --upload v0.X.Y` (replace X/Y by hand)
|
||||
* On server: Run `python3 scripts/dev/download_release.py v0.X.Y` (replace X/Y by hand)
|
||||
* Update `qutebrowser-git` PKGBUILD if dependencies/install changed
|
||||
* Announce to qutebrowser and qutebrowser-announce mailinglist
|
||||
|
26
FAQ.asciidoc
26
FAQ.asciidoc
@ -171,6 +171,20 @@ What's the difference between insert and passthrough mode?::
|
||||
be useful to rebind escape to something else in passthrough mode only, to be
|
||||
able to send an escape keypress to the website.
|
||||
|
||||
Why takes it longer to open an URL in qutebrowser than in chromium?::
|
||||
When opening an URL in an existing instance the normal qutebrowser
|
||||
Python script is started and a few PyQt libraries need to be
|
||||
loaded until it is detected that there is an instance running
|
||||
where the URL is then passed to. This takes some time.
|
||||
One workaround is to use this
|
||||
https://github.com/qutebrowser/qutebrowser/blob/master/scripts/open_url_in_instance.sh[script]
|
||||
and place it in your $PATH with the name "qutebrowser". This
|
||||
script passes the URL via an unix socket to qutebrowser (if its
|
||||
running already) using socat which is much faster and starts a new
|
||||
qutebrowser if it is not running already. Also check if you want
|
||||
to use webengine as backend in line 17 and change it to your
|
||||
needs.
|
||||
|
||||
== Troubleshooting
|
||||
|
||||
Configuration not saved after modifying config.::
|
||||
@ -211,6 +225,18 @@ it's still
|
||||
https://bugreports.qt.io/browse/QTBUG-42417?jql=component%20%3D%20WebKit%20and%20resolution%20%3D%20Done%20and%20fixVersion%20in%20(5.4.0%2C%20%225.4.0%20Alpha%22%2C%20%225.4.0%20Beta%22%2C%20%225.4.0%20RC%22)%20and%20priority%20in%20(%22P2%3A%20Important%22%2C%20%22P1%3A%20Critical%22%2C%20%22P0%3A%20Blocker%22)[nearly
|
||||
20 important bugs].
|
||||
|
||||
When using QtWebEngine, qutebrowser reports "Render Process Crashed" and the console prints a traceback on Gentoo Linux or another Source-Based Distro::
|
||||
As stated in https://gcc.gnu.org/gcc-6/changes.html[GCC's Website] GCC 6 has introduced some optimizations that could break non-conforming codebases, like QtWebEngine. +
|
||||
As a workaround, you can disable the nullpointer check optimization by adding the -fno-delete-null-pointer-checks flag while compiling. +
|
||||
On gentoo, you just need to add it into your make.conf, like this: +
|
||||
|
||||
CFLAGS="... -fno-delete-null-pointer-checks"
|
||||
CXXFLAGS="... -fno-delete-null-pointer-checks"
|
||||
+
|
||||
And then re-emerging qtwebengine with: +
|
||||
|
||||
emerge -1 qtwebengine
|
||||
|
||||
My issue is not listed.::
|
||||
If you experience any segfaults or crashes, you can report the issue in
|
||||
https://github.com/qutebrowser/qutebrowser/issues[the issue tracker] or
|
||||
|
@ -27,7 +27,7 @@ Using the packages
|
||||
Install the dependencies via apt-get:
|
||||
|
||||
----
|
||||
# apt-get install python3-lxml python-tox python3-pyqt5 python3-pyqt5.qtwebkit python3-pyqt5.qtquick python3-sip python3-jinja2 python3-pygments python3-yaml
|
||||
# apt-get install python3-lxml python-tox python3-pyqt5 python3-pyqt5.qtwebkit python3-pyqt5.qtquick python3-sip python3-jinja2 python3-pygments python3-yaml python3-pyqt5.qtsql libqt5sql5-sqlite
|
||||
----
|
||||
|
||||
On Debian Stretch or Ubuntu 17.04 or later, it's also recommended to use the
|
||||
@ -53,7 +53,7 @@ Build it from git
|
||||
Install the dependencies via apt-get:
|
||||
|
||||
----
|
||||
# apt-get install python3-pyqt5 python3-pyqt5.qtwebkit python3-pyqt5.qtquick python-tox python3-sip python3-dev
|
||||
# apt-get install python3-pyqt5 python3-pyqt5.qtwebkit python3-pyqt5.qtquick python-tox python3-sip python3-dev python3-pyqt5.qtsql libqt5sql5-sqlite
|
||||
----
|
||||
|
||||
On Debian Stretch or Ubuntu 17.04 or later, it's also recommended to install
|
||||
@ -277,13 +277,13 @@ $ pip install tox
|
||||
|
||||
Then <<tox,install qutebrowser via tox>>.
|
||||
|
||||
On OS X
|
||||
-------
|
||||
On macOS
|
||||
--------
|
||||
|
||||
Prebuilt binary
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
The easiest way to install qutebrowser on OS X is to use the prebuilt `.app`
|
||||
The easiest way to install qutebrowser on macOS is to use the prebuilt `.app`
|
||||
files from the
|
||||
https://github.com/qutebrowser/qutebrowser/releases[release page].
|
||||
|
||||
|
@ -8,7 +8,7 @@ graft icons
|
||||
graft doc/img
|
||||
graft misc/apparmor
|
||||
graft misc/userscripts
|
||||
recursive-include scripts *.py
|
||||
recursive-include scripts *.py *.sh
|
||||
include qutebrowser/utils/testfile
|
||||
include qutebrowser/git-commit-id
|
||||
include COPYING doc/* README.asciidoc CONTRIBUTING.asciidoc FAQ.asciidoc INSTALL.asciidoc CHANGELOG.asciidoc
|
||||
|
284
README.asciidoc
284
README.asciidoc
@ -36,11 +36,8 @@ Downloads
|
||||
---------
|
||||
|
||||
See the https://github.com/qutebrowser/qutebrowser/releases[github releases
|
||||
page] for available downloads (currently a source archive, and standalone
|
||||
packages as well as MSI installers for Windows).
|
||||
|
||||
See link:INSTALL.asciidoc[INSTALL] for detailed instructions on how to get
|
||||
qutebrowser running for various platforms.
|
||||
page] for available downloads and the link:INSTALL.asciidoc[INSTALL] file for
|
||||
detailed instructions on how to get qutebrowser running on various platforms.
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
@ -74,6 +71,9 @@ There's also a https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser-ann
|
||||
at mailto:qutebrowser-announce@lists.qutebrowser.org[] (the announcements also
|
||||
get sent to the general qutebrowser@ list).
|
||||
|
||||
If you're a reddit user, there's a
|
||||
https://www.reddit.com/r/qutebrowser/[/r/qutebrowser] subreddit there.
|
||||
|
||||
Contributions / Bugs
|
||||
--------------------
|
||||
|
||||
@ -98,27 +98,35 @@ Requirements
|
||||
|
||||
The following software and libraries are required to run qutebrowser:
|
||||
|
||||
* http://www.python.org/[Python] 3.4 or newer (3.5 recommended)
|
||||
* http://qt.io/[Qt] 5.2.0 or newer (5.9.0 recommended)
|
||||
* QtWebKit (old or link:https://github.com/annulen/webkit/wiki[reloaded]/NG) or QtWebEngine
|
||||
* http://www.python.org/[Python] 3.4 or newer (3.6 recommended) - note that
|
||||
support for Python 3.4
|
||||
https://github.com/qutebrowser/qutebrowser/issues/2742[will be dropped soon].
|
||||
* http://qt.io/[Qt] 5.2.0 or newer (5.9 recommended - note that support for Qt
|
||||
< 5.7.1 will be dropped soon) with the following modules:
|
||||
- QtCore / qtbase
|
||||
- QtQuick (part of qtbase in some distributions)
|
||||
- QtSQL (part of qtbase in some distributions)
|
||||
- QtWebEngine, or
|
||||
- QtWebKit (old or link:https://github.com/annulen/webkit/wiki[reloaded]/NG).
|
||||
Note that support for legacy QtWebKit (before 5.212) will be
|
||||
dropped soon.
|
||||
* http://www.riverbankcomputing.com/software/pyqt/intro[PyQt] 5.2.0 or newer
|
||||
(5.8.1 recommended) for Python 3
|
||||
(5.9 recommended) for Python 3. Note that support for PyQt < 5.7 will be
|
||||
dropped soon.
|
||||
* https://pypi.python.org/pypi/setuptools/[pkg_resources/setuptools]
|
||||
* http://fdik.org/pyPEG/[pyPEG2]
|
||||
* http://jinja.pocoo.org/[jinja2]
|
||||
* http://pygments.org/[pygments]
|
||||
* http://pyyaml.org/wiki/PyYAML[PyYAML]
|
||||
* http://pyopengl.sourceforge.net/[PyOpenGL] when using QtWebEngine
|
||||
|
||||
The following libraries are optional and provide a better user experience:
|
||||
The following libraries are optional:
|
||||
|
||||
* http://cthedot.de/cssutils/[cssutils]
|
||||
|
||||
To generate the documentation for the `:help` command, when using the git
|
||||
repository (rather than a release), http://asciidoc.org/[asciidoc] is needed.
|
||||
|
||||
On Windows, https://pypi.python.org/pypi/colorama/[colorama] is needed to
|
||||
display colored log output.
|
||||
* http://cthedot.de/cssutils/[cssutils] (for an improved `:download --mhtml`
|
||||
with QtWebKit)
|
||||
* On Windows, https://pypi.python.org/pypi/colorama/[colorama] for colored log
|
||||
output.
|
||||
* http://asciidoc.org/[asciidoc] to generate the documentation for the `:help`
|
||||
command, when using the git repository (rather than a release).
|
||||
|
||||
See link:INSTALL.asciidoc[INSTALL] for directions on how to install qutebrowser
|
||||
and its dependencies.
|
||||
@ -142,219 +150,59 @@ get in touch!
|
||||
Authors
|
||||
-------
|
||||
|
||||
Contributors, sorted by the number of commits in descending order:
|
||||
qutebrowser's primary author is Florian Bruhin (The Compiler), but qutebrowser
|
||||
wouldn't be what it is without the help of
|
||||
https://github.com/qutebrowser/qutebrowser/graphs/contributors[hundreds of contributors]!
|
||||
|
||||
// QUTE_AUTHORS_START
|
||||
* Florian Bruhin
|
||||
* Daniel Schadt
|
||||
* Ryan Roden-Corrent
|
||||
* Jan Verbeek
|
||||
* Jakub Klinkovský
|
||||
* Antoni Boucher
|
||||
* Lamar Pavel
|
||||
* Marshall Lochbaum
|
||||
* Bruno Oliveira
|
||||
* thuck
|
||||
* Martin Tournoij
|
||||
* Imran Sobir
|
||||
* Alexander Cogneau
|
||||
* Felix Van der Jeugt
|
||||
* Daniel Karbach
|
||||
* Kevin Velghe
|
||||
* Raphael Pierzina
|
||||
* Joel Torstensson
|
||||
* Patric Schmitz
|
||||
* Tarcisio Fedrizzi
|
||||
* Jay Kamat
|
||||
* Claude
|
||||
* Philipp Hansch
|
||||
* Fritz Reichwald
|
||||
* Corentin Julé
|
||||
* meles5
|
||||
* Panagiotis Ktistakis
|
||||
* Artur Shaik
|
||||
* Nathan Isom
|
||||
* Thorsten Wißmann
|
||||
* Austin Anderson
|
||||
* Jimmy
|
||||
* Niklas Haas
|
||||
* Maciej Wołczyk
|
||||
* Clayton Craft
|
||||
* sandrosc
|
||||
* Alexey "Averrin" Nabrodov
|
||||
* pkill9
|
||||
* nanjekyejoannah
|
||||
* avk
|
||||
* ZDarian
|
||||
* Milan Svoboda
|
||||
* John ShaggyTwoDope Jenkins
|
||||
* Peter Vilim
|
||||
* Jacob Sword
|
||||
* knaggita
|
||||
* Oliver Caldwell
|
||||
* Nikolay Amiantov
|
||||
* Julian Weigt
|
||||
* Tomasz Kramkowski
|
||||
* Sebastian Frysztak
|
||||
* Julie Engel
|
||||
* Jonas Schürmann
|
||||
* error800
|
||||
* Michael Hoang
|
||||
* Liam BEGUIN
|
||||
* Daniel Fiser
|
||||
* skinnay
|
||||
* Zach-Button
|
||||
* Samuel Walladge
|
||||
* Peter Rice
|
||||
* Ismail S
|
||||
* Halfwit
|
||||
* David Vogt
|
||||
* Claire Cavanaugh
|
||||
* rikn00
|
||||
* kanikaa1234
|
||||
* haitaka
|
||||
* Nick Ginther
|
||||
* Michał Góral
|
||||
* Michael Ilsaas
|
||||
* Martin Zimmermann
|
||||
* Marius
|
||||
* Link
|
||||
* Jussi Timperi
|
||||
* Cosmin Popescu
|
||||
* Brian Jackson
|
||||
* sbinix
|
||||
* rsteube
|
||||
* neeasade
|
||||
* jnphilipp
|
||||
* Yannis Rohloff
|
||||
* Tobias Patzl
|
||||
* Stefan Tatschner
|
||||
* Samuel Loury
|
||||
* Peter Michely
|
||||
* Panashe M. Fundira
|
||||
* Lucas Hoffmann
|
||||
* Larry Hynes
|
||||
* Kirill A. Shutemov
|
||||
* Johannes Altmanninger
|
||||
* Jeremy Kaplan
|
||||
* Ismail
|
||||
* Iordanis Grigoriou
|
||||
* Edgar Hipp
|
||||
* Daryl Finlay
|
||||
* arza
|
||||
* adam
|
||||
* Samir Benmendil
|
||||
* Regina Hug
|
||||
* Penaz
|
||||
* Matthias Lisin
|
||||
* Mathias Fussenegger
|
||||
* Marcelo Santos
|
||||
* Marcel Schilling
|
||||
* Joel Bradshaw
|
||||
* Jean-Louis Fuchs
|
||||
* Franz Fellner
|
||||
* Eric Drechsel
|
||||
* zwarag
|
||||
* xd1le
|
||||
* rmortens
|
||||
* oniondreams
|
||||
* issue
|
||||
* haxwithaxe
|
||||
* evan
|
||||
* dylan araps
|
||||
* caveman
|
||||
* addictedtoflames
|
||||
* Xitian9
|
||||
* Vasilij Schneidermann
|
||||
* Tomas Orsava
|
||||
* Tom Janson
|
||||
* Tobias Werth
|
||||
* Tim Harder
|
||||
* Thiago Barroso Perrotta
|
||||
* Steve Peak
|
||||
* Sorokin Alexei
|
||||
* Simon Désaulniers
|
||||
* Rok Mandeljc
|
||||
* Noah Huesser
|
||||
* Moez Bouhlel
|
||||
* MikeinRealLife
|
||||
* Lazlow Carmichael
|
||||
* Kevin Wang
|
||||
* Ján Kobezda
|
||||
* Justin Partain
|
||||
* Johannes Martinsson
|
||||
* Jean-Christophe Petkovich
|
||||
* Helen Sherwood-Taylor
|
||||
* HalosGhost
|
||||
* Gregor Pohl
|
||||
* Eivind Uggedal
|
||||
* Dietrich Daroch
|
||||
* Derek Sivers
|
||||
* Daniel Lu
|
||||
* Daniel Jakots
|
||||
* Arseniy Seroka
|
||||
* Anton Grensjö
|
||||
* Andy Balaam
|
||||
* Andreas Fischer
|
||||
* Amos Bird
|
||||
* Akselmo
|
||||
// QUTE_AUTHORS_END
|
||||
|
||||
The following people have contributed graphics:
|
||||
Additionally, the following people have contributed graphics:
|
||||
|
||||
* Jad/link:http://yelostudio.com[yelo] (new icon)
|
||||
* WOFall (original icon)
|
||||
* regines (key binding cheatsheet)
|
||||
|
||||
Thanks / Similar projects
|
||||
-------------------------
|
||||
Also, thanks to everyone who contributed to one of qutebrowser's
|
||||
link:doc/backers.asciidoc[crowdfunding campaigns]!
|
||||
|
||||
Many projects with a similar goal as qutebrowser exist:
|
||||
|
||||
* http://portix.bitbucket.org/dwb/[dwb] (C, GTK+ with WebKit1, currently
|
||||
http://www.reddit.com/r/linux/comments/2huqbc/dwb_abandoned/[unmaintained] -
|
||||
main inspiration for qutebrowser)
|
||||
* https://github.com/fanglingsu/vimb[vimb] (C, GTK+ with WebKit1, active)
|
||||
* http://sourceforge.net/p/vimprobable/wiki/Home/[vimprobable] (C, GTK+ with
|
||||
WebKit1, dead)
|
||||
* http://surf.suckless.org/[surf] (C, GTK+ with WebKit1, active)
|
||||
* https://mason-larobina.github.io/luakit/[luakit] (C/Lua, GTK+ with
|
||||
WebKit1, not very active)
|
||||
* http://pwmt.org/projects/jumanji/[jumanji] (C, GTK+ with WebKit1, not very
|
||||
active)
|
||||
* http://www.uzbl.org/[uzbl] (C, GTK+ with WebKit1/WebKit2, active)
|
||||
* http://conkeror.org/[conkeror] (Javascript, Emacs-like, XULRunner/Gecko,
|
||||
active)
|
||||
* https://github.com/AeroNotix/lispkit[lispkit] (quite new, lisp, GTK+ with
|
||||
WebKit, active)
|
||||
* http://www.vimperator.org/[Vimperator] (Firefox addon)
|
||||
* http://5digits.org/pentadactyl/[Pentadactyl] (Firefox addon)
|
||||
* https://github.com/akhodakivskiy/VimFx[VimFx] (Firefox addon)
|
||||
* https://github.com/1995eaton/chromium-vim[cVim] (Chrome/Chromium addon)
|
||||
* http://vimium.github.io/[vimium] (Chrome/Chromium addon)
|
||||
* https://chrome.google.com/webstore/detail/vichrome/gghkfhpblkcmlkmpcpgaajbbiikbhpdi?hl=en[ViChrome] (Chrome/Chromium addon)
|
||||
* https://github.com/jinzhu/vrome[Vrome] (Chrome/Chromium addon)
|
||||
Similar projects
|
||||
----------------
|
||||
|
||||
Many projects with a similar goal as qutebrowser exist.
|
||||
Most of them were inspirations for qutebrowser in some way, thanks for that!
|
||||
|
||||
Thanks as well to the following projects and people for helping me with
|
||||
problems and helpful hints:
|
||||
Active
|
||||
~~~~~~
|
||||
|
||||
* http://eric-ide.python-projects.org/[eric5] / Detlev Offenbach
|
||||
* https://code.google.com/p/devicenzo/[devicenzo]
|
||||
* portix
|
||||
* seir
|
||||
* nitroxleecher
|
||||
* https://fanglingsu.github.io/vimb/[vimb] (C, GTK+ with WebKit2)
|
||||
* https://luakit.github.io/luakit/[luakit] (C/Lua, GTK+ with WebKit2)
|
||||
* http://surf.suckless.org/[surf] (C, GTK+ with WebKit1/WebKit2)
|
||||
* http://www.uzbl.org/[uzbl] (C, GTK+ with WebKit1/WebKit2)
|
||||
* Chrome/Chromium addons:
|
||||
https://github.com/1995eaton/chromium-vim[cVim],
|
||||
http://vimium.github.io/[Vimium],
|
||||
https://github.com/brookhong/Surfingkeys[Surfingkeys],
|
||||
http://saka-key.lusakasa.com/[Saka Key]
|
||||
* Firefox addons (based on WebExtensions):
|
||||
https://addons.mozilla.org/en-GB/firefox/addon/vimium-ff/[Vimium-FF] (experimental),
|
||||
http://saka-key.lusakasa.com/[Saka Key]
|
||||
|
||||
Also, thanks to:
|
||||
Inactive
|
||||
~~~~~~~~
|
||||
|
||||
* Everyone contributing to the link:doc/backers.asciidoc[crowdfunding].
|
||||
* Everyone who had the patience to test qutebrowser before v0.1.
|
||||
* Everyone triaging/fixing my bugs in the
|
||||
https://bugreports.qt.io/secure/Dashboard.jspa[Qt bugtracker]
|
||||
* Everyone answering my questions on http://stackoverflow.com/[Stack Overflow]
|
||||
and in IRC.
|
||||
* All the projects which were a great help while developing qutebrowser.
|
||||
* https://bitbucket.org/portix/dwb[dwb] (C, GTK+ with WebKit1,
|
||||
https://bitbucket.org/portix/dwb/pull-requests/22/several-cleanups-to-increase-portability/diff[unmaintained] -
|
||||
main inspiration for qutebrowser)
|
||||
* http://sourceforge.net/p/vimprobable/wiki/Home/[vimprobable] (C, GTK+ with
|
||||
WebKit1)
|
||||
* http://pwmt.org/projects/jumanji/[jumanji] (C, GTK+ with WebKit1)
|
||||
* http://conkeror.org/[conkeror] (Javascript, Emacs-like, XULRunner/Gecko)
|
||||
* Firefox addons (not based on WebExtensions or no recent activity):
|
||||
http://www.vimperator.org/[Vimperator],
|
||||
http://5digits.org/pentadactyl/[Pentadactyl],
|
||||
https://github.com/akhodakivskiy/VimFx[VimFx],
|
||||
https://github.com/shinglyu/QuantumVim[QuantumVim]
|
||||
* Chrome/Chromium addons:
|
||||
https://chrome.google.com/webstore/detail/vichrome/gghkfhpblkcmlkmpcpgaajbbiikbhpdi?hl=en[ViChrome],
|
||||
https://github.com/jinzhu/vrome[Vrome]
|
||||
|
||||
License
|
||||
-------
|
||||
|
@ -1,5 +1,5 @@
|
||||
// DO NOT EDIT THIS FILE DIRECTLY!
|
||||
// It is autogenerated from docstrings by running:
|
||||
// It is autogenerated by running:
|
||||
// $ python3 scripts/dev/src2asciidoc.py
|
||||
|
||||
= Commands
|
||||
@ -1565,6 +1565,7 @@ These commands are mainly intended for debugging. They are hidden if qutebrowser
|
||||
|<<debug-clear-ssl-errors,debug-clear-ssl-errors>>|Clear remembered SSL error answers.
|
||||
|<<debug-console,debug-console>>|Show the debugging console.
|
||||
|<<debug-crash,debug-crash>>|Crash for debugging purposes.
|
||||
|<<debug-dump-history,debug-dump-history>>|Dump the history to a file in the old pre-SQL format.
|
||||
|<<debug-dump-page,debug-dump-page>>|Dump the current page's content to a file.
|
||||
|<<debug-log-capacity,debug-log-capacity>>|Change the number of log lines to be stored in RAM.
|
||||
|<<debug-log-filter,debug-log-filter>>|Change the log filter for console logging.
|
||||
@ -1599,6 +1600,15 @@ Crash for debugging purposes.
|
||||
==== positional arguments
|
||||
* +'typ'+: either 'exception' or 'segfault'.
|
||||
|
||||
[[debug-dump-history]]
|
||||
=== debug-dump-history
|
||||
Syntax: +:debug-dump-history 'dest'+
|
||||
|
||||
Dump the history to a file in the old pre-SQL format.
|
||||
|
||||
==== positional arguments
|
||||
* +'dest'+: Where to write the file to.
|
||||
|
||||
[[debug-dump-page]]
|
||||
=== debug-dump-page
|
||||
Syntax: +:debug-dump-page [*--plain*] 'dest'+
|
||||
|
@ -1,5 +1,5 @@
|
||||
// DO NOT EDIT THIS FILE DIRECTLY!
|
||||
// It is autogenerated from docstrings by running:
|
||||
// It is autogenerated by running:
|
||||
// $ python3 scripts/dev/src2asciidoc.py
|
||||
|
||||
= Settings
|
||||
@ -1067,11 +1067,15 @@ Default: +pass:[white]+
|
||||
== colors.tabs.selected.even.bg
|
||||
Background color of selected even tabs.
|
||||
|
||||
<<<<<<< HEAD
|
||||
Default: +pass:[black]+
|
||||
|
||||
[[colors.tabs.selected.even.fg]]
|
||||
== colors.tabs.selected.even.fg
|
||||
Foreground color of selected even tabs.
|
||||
=======
|
||||
Valid values:
|
||||
>>>>>>> upstream/master
|
||||
|
||||
Default: +pass:[white]+
|
||||
|
||||
@ -1163,7 +1167,7 @@ Default: +pass:[%Y-%m-%d]+
|
||||
How many URLs to show in the web history.
|
||||
0: no history / -1: unlimited
|
||||
|
||||
Default: +pass:[1000]+
|
||||
Default: +pass:[-1]+
|
||||
|
||||
[[confirm_quit]]
|
||||
== confirm_quit
|
||||
|
196
doc/notes
196
doc/notes
@ -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
|
@ -31,7 +31,7 @@ image:http://qutebrowser.org/img/cheatsheet-small.png["qutebrowser key binding c
|
||||
* Run `:adblock-update` to download adblock lists and activate adblocking.
|
||||
* If you just cloned the repository, you'll need to run
|
||||
`scripts/asciidoc2html.py` to generate the documentation.
|
||||
* Go to the link:qute://settings[settings page] to set up qutebrowser the way you want it. (Currently not available with the QtWebEngine backend and on the OS X build - use the `:set` command instead)
|
||||
* Go to the link:qute://settings[settings page] to set up qutebrowser the way you want it. (Currently not available with the QtWebEngine backend and on the macOS build - use the `:set` command instead)
|
||||
* Subscribe to
|
||||
https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser[the mailinglist] or
|
||||
https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser-announce[the announce-only mailinglist].
|
||||
|
@ -60,7 +60,7 @@ Sending commands
|
||||
Normal qutebrowser commands can be written to `$QUTE_FIFO` and will be
|
||||
executed.
|
||||
|
||||
On Unix/OS X, this is a named pipe and commands written to it will get executed
|
||||
On Unix/macOS, this is a named pipe and commands written to it will get executed
|
||||
immediately.
|
||||
|
||||
On Windows, this is a regular file, and the commands in it will be executed as
|
||||
|
@ -41,7 +41,7 @@ a = Analysis(['../qutebrowser/__main__.py'],
|
||||
pathex=['misc'],
|
||||
binaries=None,
|
||||
datas=get_data_files(),
|
||||
hiddenimports=['PyQt5.QtOpenGL'],
|
||||
hiddenimports=['PyQt5.QtOpenGL', 'PyQt5._QOpenGLFunctions_2_0'],
|
||||
hookspath=[],
|
||||
runtime_hooks=[],
|
||||
excludes=['tkinter'],
|
||||
|
@ -1,9 +1,9 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
certifi==2017.4.17
|
||||
certifi==2017.7.27.1
|
||||
chardet==3.0.4
|
||||
codecov==2.0.9
|
||||
coverage==4.4.1
|
||||
idna==2.5
|
||||
requests==2.18.1
|
||||
urllib3==1.21.1
|
||||
requests==2.18.2
|
||||
urllib3==1.22
|
||||
|
@ -3,7 +3,7 @@
|
||||
flake8==2.6.2 # rq.filter: < 3.0.0
|
||||
flake8-copyright==0.2.0
|
||||
flake8-debugger==1.4.0 # rq.filter: != 2.0.0
|
||||
flake8-deprecated==1.2
|
||||
flake8-deprecated==1.2.1
|
||||
flake8-docstrings==1.0.3 # rq.filter: < 1.1.0
|
||||
flake8-future-import==0.4.3
|
||||
flake8-mock==0.3
|
||||
@ -11,7 +11,7 @@ flake8-pep3101==1.0 # rq.filter: < 1.1
|
||||
flake8-polyfill==1.0.1
|
||||
flake8-putty==0.4.0
|
||||
flake8-string-format==0.2.3
|
||||
flake8-tidy-imports==1.0.6
|
||||
flake8-tidy-imports==1.1.0
|
||||
flake8-tuple==0.2.13
|
||||
mccabe==0.6.1
|
||||
packaging==16.8
|
||||
|
@ -3,6 +3,6 @@
|
||||
appdirs==1.4.3
|
||||
packaging==16.8
|
||||
pyparsing==2.2.0
|
||||
setuptools==36.0.1
|
||||
setuptools==36.2.5
|
||||
six==1.10.0
|
||||
wheel==0.29.0
|
||||
|
@ -1,7 +1,7 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
-e git+https://github.com/PyCQA/astroid.git#egg=astroid
|
||||
certifi==2017.4.17
|
||||
certifi==2017.7.27.1
|
||||
chardet==3.0.4
|
||||
github3.py==0.9.6
|
||||
idna==2.5
|
||||
@ -10,9 +10,9 @@ lazy-object-proxy==1.3.1
|
||||
mccabe==0.6.1
|
||||
-e git+https://github.com/PyCQA/pylint.git#egg=pylint
|
||||
./scripts/dev/pylint_checkers
|
||||
requests==2.18.1
|
||||
requests==2.18.2
|
||||
six==1.10.0
|
||||
uritemplate==3.0.0
|
||||
uritemplate.py==3.0.2
|
||||
urllib3==1.21.1
|
||||
urllib3==1.22
|
||||
wrapt==1.10.10
|
||||
|
@ -1,7 +1,7 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
astroid==1.5.3
|
||||
certifi==2017.4.17
|
||||
certifi==2017.7.27.1
|
||||
chardet==3.0.4
|
||||
github3.py==0.9.6
|
||||
idna==2.5
|
||||
@ -10,9 +10,9 @@ lazy-object-proxy==1.3.1
|
||||
mccabe==0.6.1
|
||||
pylint==1.7.2
|
||||
./scripts/dev/pylint_checkers
|
||||
requests==2.18.1
|
||||
requests==2.18.2
|
||||
six==1.10.0
|
||||
uritemplate==3.0.0
|
||||
uritemplate.py==3.0.2
|
||||
urllib3==1.21.1
|
||||
urllib3==1.22
|
||||
wrapt==1.10.10
|
||||
|
@ -1,4 +1,4 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
PyQt5==5.8.2
|
||||
sip==4.19.2
|
||||
PyQt5==5.9
|
||||
sip==4.19.3
|
||||
|
@ -5,35 +5,35 @@ cheroot==5.7.0
|
||||
click==6.7
|
||||
# colorama==0.3.9
|
||||
coverage==4.4.1
|
||||
decorator==4.0.11
|
||||
decorator==4.1.2
|
||||
EasyProcess==0.2.3
|
||||
fields==5.0.0
|
||||
Flask==0.12.2
|
||||
glob2==0.5
|
||||
httpbin==0.5.0
|
||||
hunter==1.4.1
|
||||
hypothesis==3.11.6
|
||||
hypothesis==3.14.0
|
||||
itsdangerous==0.24
|
||||
# Jinja2==2.9.6
|
||||
Mako==1.0.6
|
||||
Mako==1.0.7
|
||||
# MarkupSafe==1.0
|
||||
parse==1.8.2
|
||||
parse-type==0.3.4
|
||||
py==1.4.34
|
||||
pytest==3.1.2
|
||||
pytest==3.1.3
|
||||
pytest-bdd==2.18.2
|
||||
pytest-benchmark==3.0.0
|
||||
pytest-benchmark==3.1.1
|
||||
pytest-catchlog==1.2.2
|
||||
pytest-cov==2.5.1
|
||||
pytest-faulthandler==1.3.1
|
||||
pytest-instafail==0.3.0
|
||||
pytest-mock==1.6.0
|
||||
pytest-qt==2.1.0
|
||||
pytest-mock==1.6.2
|
||||
pytest-qt==2.1.2
|
||||
pytest-repeat==0.4.1
|
||||
pytest-rerunfailures==2.2
|
||||
pytest-travis-fold==1.2.0
|
||||
pytest-xvfb==1.0.0
|
||||
PyVirtualDisplay==0.2.1
|
||||
six==1.10.0
|
||||
vulture==0.14
|
||||
vulture==0.21
|
||||
Werkzeug==0.12.2
|
||||
|
@ -1,3 +1,3 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
vulture==0.14
|
||||
vulture==0.21
|
||||
|
@ -2,20 +2,32 @@
|
||||
#
|
||||
# Executes python-readability on current page and opens the summary as new tab.
|
||||
#
|
||||
# Depends on the python-readability package, or its fork:
|
||||
#
|
||||
# - https://github.com/buriy/python-readability
|
||||
# - https://github.com/bookieio/breadability
|
||||
#
|
||||
# Usage:
|
||||
# :spawn --userscript readability
|
||||
#
|
||||
from __future__ import absolute_import
|
||||
import codecs, os
|
||||
from readability.readability import Document
|
||||
|
||||
tmpfile=os.path.expanduser('~/.local/share/qutebrowser/userscripts/readability.html')
|
||||
if not os.path.exists(os.path.dirname(tmpfile)):
|
||||
os.makedirs(os.path.dirname(tmpfile))
|
||||
|
||||
with codecs.open(os.environ['QUTE_HTML'], 'r', 'utf-8') as source:
|
||||
doc = Document(source.read())
|
||||
content = doc.summary().replace('<html>', '<html><head><title>%s</title></head>' % doc.title())
|
||||
data = source.read()
|
||||
|
||||
try:
|
||||
from breadability.readable import Article as reader
|
||||
doc = reader(data)
|
||||
content = doc.readable
|
||||
except ImportError:
|
||||
from readability import Document
|
||||
doc = Document(data)
|
||||
content = doc.summary().replace('<html>', '<html><head><title>%s</title></head>' % doc.title())
|
||||
|
||||
with codecs.open(tmpfile, 'w', 'utf-8') as target:
|
||||
target.write('<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />')
|
||||
|
10
pytest.ini
10
pytest.ini
@ -1,12 +1,13 @@
|
||||
[pytest]
|
||||
addopts = --strict -rfEw --faulthandler-timeout=70 --instafail --pythonwarnings error
|
||||
addopts = --strict -rfEw --faulthandler-timeout=70 --instafail --pythonwarnings error --benchmark-columns=Min,Max,Median
|
||||
testpaths = tests
|
||||
markers =
|
||||
gui: Tests using the GUI (e.g. spawning widgets)
|
||||
posix: Tests which only can run on a POSIX OS.
|
||||
windows: Tests which only can run on Windows.
|
||||
linux: Tests which only can run on Linux.
|
||||
osx: Tests which only can run on OS X.
|
||||
not_osx: Tests which can not run on OS X.
|
||||
mac: Tests which only can run on macOS.
|
||||
not_mac: Tests which can not run on macOS.
|
||||
not_frozen: Tests which can't be run if sys.frozen is True.
|
||||
no_xvfb: Tests which can't be run with Xvfb.
|
||||
frozen: Tests which can only be run if sys.frozen is True.
|
||||
@ -21,7 +22,7 @@ markers =
|
||||
qtwebkit_ng_xfail: Tests failing with QtWebKit-NG
|
||||
qtwebkit_ng_skip: Tests skipped with QtWebKit-NG
|
||||
qtwebengine_flaky: Tests which are flaky (and currently skipped) with QtWebEngine
|
||||
qtwebengine_osx_xfail: Tests which fail on OS X with QtWebEngine
|
||||
qtwebengine_mac_xfail: Tests which fail on macOS with QtWebEngine
|
||||
js_prompt: Tests needing to display a javascript prompt
|
||||
this: Used to mark tests during development
|
||||
no_invalid_lines: Don't fail on unparseable lines in end2end tests
|
||||
@ -47,6 +48,7 @@ qt_log_ignore =
|
||||
^QGeoclueMaster error creating GeoclueMasterClient\.
|
||||
^Geoclue error: Process org\.freedesktop\.Geoclue\.Master exited with status 127
|
||||
^Failed to create Geoclue client interface. Geoclue error: org\.freedesktop\.DBus\.Error\.Disconnected
|
||||
^QDBusConnection: name 'org.freedesktop.Geoclue.Master' had owner '' but we thought it was ':1.1'
|
||||
^QObject::connect: Cannot connect \(null\)::stateChanged\(QNetworkSession::State\) to QNetworkReplyHttpImpl::_q_networkSessionStateChanged\(QNetworkSession::State\)
|
||||
^QXcbClipboard: Cannot transfer data, no data available
|
||||
^load glyph failed
|
||||
|
@ -26,7 +26,7 @@ __copyright__ = "Copyright 2014-2017 Florian Bruhin (The Compiler)"
|
||||
__license__ = "GPL"
|
||||
__maintainer__ = __author__
|
||||
__email__ = "mail@qutebrowser.org"
|
||||
__version_info__ = (0, 10, 1)
|
||||
__version_info__ = (0, 11, 0)
|
||||
__version__ = '.'.join(str(e) for e in __version_info__)
|
||||
__description__ = "A keyboard-driven, vim-like browser based on PyQt5."
|
||||
|
||||
|
@ -41,9 +41,10 @@ except ImportError:
|
||||
|
||||
import qutebrowser
|
||||
import qutebrowser.resources
|
||||
from qutebrowser.completion.models import instances as completionmodels
|
||||
from qutebrowser.completion.models import miscmodels
|
||||
from qutebrowser.commands import cmdutils, runners, cmdexc
|
||||
from qutebrowser.config import config, websettings, configexc
|
||||
from qutebrowser.config.parsers import keyconf
|
||||
from qutebrowser.browser import (urlmarks, adblock, history, browsertab,
|
||||
downloads)
|
||||
from qutebrowser.browser.network import proxy
|
||||
@ -52,10 +53,10 @@ from qutebrowser.browser.webkit.network import networkmanager
|
||||
from qutebrowser.keyinput import macros
|
||||
from qutebrowser.mainwindow import mainwindow, prompt
|
||||
from qutebrowser.misc import (readline, ipc, savemanager, sessions,
|
||||
crashsignal, earlyinit, objects)
|
||||
crashsignal, earlyinit, objects, sql)
|
||||
from qutebrowser.misc import utilcmds # pylint: disable=unused-import
|
||||
from qutebrowser.utils import (log, version, message, utils, qtutils, urlutils,
|
||||
objreg, usertypes, standarddir, error, debug)
|
||||
objreg, usertypes, standarddir, error)
|
||||
# We import utilcmds to run the cmdutils.register decorators.
|
||||
|
||||
|
||||
@ -154,7 +155,7 @@ def init(args, crash_handler):
|
||||
QDesktopServices.setUrlHandler('https', open_desktopservices_url)
|
||||
QDesktopServices.setUrlHandler('qute', open_desktopservices_url)
|
||||
|
||||
QTimer.singleShot(10, functools.partial(_init_late_modules, args))
|
||||
objreg.get('web-history').import_txt()
|
||||
|
||||
log.init.debug("Init done!")
|
||||
crash_handler.raise_crashdlg()
|
||||
@ -400,10 +401,8 @@ def _init_modules(args, crash_handler):
|
||||
log.init.debug("Initializing network...")
|
||||
networkmanager.init()
|
||||
|
||||
if qtutils.version_check('5.8'):
|
||||
# Otherwise we can only initialize it for QtWebKit because of crashes
|
||||
log.init.debug("Initializing proxy...")
|
||||
proxy.init()
|
||||
log.init.debug("Initializing proxy...")
|
||||
proxy.init()
|
||||
|
||||
log.init.debug("Initializing readline-bridge...")
|
||||
readline_bridge = readline.ReadlineBridge()
|
||||
@ -413,6 +412,17 @@ def _init_modules(args, crash_handler):
|
||||
config.init(qApp)
|
||||
save_manager.init_autosave()
|
||||
|
||||
log.init.debug("Initializing keys...")
|
||||
keyconf.init(qApp)
|
||||
|
||||
log.init.debug("Initializing sql...")
|
||||
try:
|
||||
sql.init(os.path.join(standarddir.data(), 'history.sqlite'))
|
||||
except sql.SqlException as e:
|
||||
error.handle_fatal_exc(e, args, 'Error initializing SQL',
|
||||
pre_text='Error initializing SQL')
|
||||
sys.exit(usertypes.Exit.err_init)
|
||||
|
||||
log.init.debug("Initializing web history...")
|
||||
history.init(qApp)
|
||||
|
||||
@ -449,9 +459,6 @@ def _init_modules(args, crash_handler):
|
||||
diskcache = cache.DiskCache(standarddir.cache(), parent=qApp)
|
||||
objreg.register('cache', diskcache)
|
||||
|
||||
log.init.debug("Initializing completions...")
|
||||
completionmodels.init()
|
||||
|
||||
log.init.debug("Misc initialization...")
|
||||
if config.val.window.hide_wayland_decoration:
|
||||
os.environ['QT_WAYLAND_DISABLE_WINDOWDECORATION'] = '1'
|
||||
@ -462,23 +469,6 @@ def _init_modules(args, crash_handler):
|
||||
browsertab.init()
|
||||
|
||||
|
||||
def _init_late_modules(args):
|
||||
"""Initialize modules which can be inited after the window is shown."""
|
||||
log.init.debug("Reading web history...")
|
||||
reader = objreg.get('web-history').async_read()
|
||||
with debug.log_time(log.init, 'Reading history'):
|
||||
while True:
|
||||
QApplication.processEvents()
|
||||
try:
|
||||
next(reader)
|
||||
except StopIteration:
|
||||
break
|
||||
except (OSError, UnicodeDecodeError) as e:
|
||||
error.handle_fatal_exc(e, args, "Error while initializing!",
|
||||
pre_text="Error while initializing")
|
||||
sys.exit(usertypes.Exit.err_init)
|
||||
|
||||
|
||||
class Quitter:
|
||||
|
||||
"""Utility class to quit/restart the QApplication.
|
||||
@ -626,7 +616,7 @@ class Quitter:
|
||||
# Save the session if one is given.
|
||||
if session is not None:
|
||||
session_manager = objreg.get('session-manager')
|
||||
session_manager.save(session)
|
||||
session_manager.save(session, with_private=True)
|
||||
# Open a new process and immediately shutdown the existing one
|
||||
try:
|
||||
args, cwd = self._get_restart_args(pages, session)
|
||||
@ -760,7 +750,7 @@ class Quitter:
|
||||
QTimer.singleShot(0, functools.partial(qApp.exit, status))
|
||||
|
||||
@cmdutils.register(instance='quitter', name='wq')
|
||||
@cmdutils.argument('name', completion=usertypes.Completion.sessions)
|
||||
@cmdutils.argument('name', completion=miscmodels.session)
|
||||
def save_and_quit(self, name=sessions.default):
|
||||
"""Save open pages and quit.
|
||||
|
||||
|
@ -479,11 +479,21 @@ class AbstractHistory:
|
||||
def current_idx(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def back(self):
|
||||
raise NotImplementedError
|
||||
def back(self, count=1):
|
||||
idx = self.current_idx() - count
|
||||
if idx >= 0:
|
||||
self._go_to_item(self._item_at(idx))
|
||||
else:
|
||||
self._go_to_item(self._item_at(0))
|
||||
raise WebTabError("At beginning of history.")
|
||||
|
||||
def forward(self):
|
||||
raise NotImplementedError
|
||||
def forward(self, count=1):
|
||||
idx = self.current_idx() + count
|
||||
if idx < len(self):
|
||||
self._go_to_item(self._item_at(idx))
|
||||
else:
|
||||
self._go_to_item(self._item_at(len(self) - 1))
|
||||
raise WebTabError("At end of history.")
|
||||
|
||||
def can_go_back(self):
|
||||
raise NotImplementedError
|
||||
@ -491,6 +501,12 @@ class AbstractHistory:
|
||||
def can_go_forward(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def _item_at(self, i):
|
||||
raise NotImplementedError
|
||||
|
||||
def _go_to_item(self, item):
|
||||
raise NotImplementedError
|
||||
|
||||
def serialize(self):
|
||||
"""Serialize into an opaque format understood by self.deserialize."""
|
||||
raise NotImplementedError
|
||||
|
@ -20,11 +20,12 @@
|
||||
"""Command dispatcher for TabbedBrowser."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import os.path
|
||||
import shlex
|
||||
import functools
|
||||
|
||||
from PyQt5.QtWidgets import QApplication, QTabBar
|
||||
from PyQt5.QtWidgets import QApplication, QTabBar, QDialog
|
||||
from PyQt5.QtCore import Qt, QUrl, QEvent, QUrlQuery
|
||||
from PyQt5.QtGui import QKeyEvent
|
||||
from PyQt5.QtPrintSupport import QPrintDialog, QPrintPreviewDialog
|
||||
@ -38,10 +39,10 @@ from qutebrowser.browser import (urlmarks, browsertab, inspector, navigate,
|
||||
webelem, downloads)
|
||||
from qutebrowser.keyinput import modeman
|
||||
from qutebrowser.utils import (message, usertypes, log, qtutils, urlutils,
|
||||
objreg, utils, typing)
|
||||
objreg, utils, typing, debug)
|
||||
from qutebrowser.utils.usertypes import KeyMode
|
||||
from qutebrowser.misc import editor, guiprocess
|
||||
from qutebrowser.completion.models import instances, sortfilter
|
||||
from qutebrowser.completion.models import urlmodel, miscmodels
|
||||
|
||||
|
||||
class CommandDispatcher:
|
||||
@ -227,19 +228,6 @@ class CommandDispatcher:
|
||||
self._tabbed_browser.close_tab(tab)
|
||||
tabbar.setSelectionBehaviorOnRemove(old_selection_behavior)
|
||||
|
||||
def _tab_close_prompt_if_pinned(self, tab, force, yes_action):
|
||||
"""Helper method for tab_close.
|
||||
|
||||
If tab is pinned, prompt. If everything is good, run yes_action.
|
||||
"""
|
||||
if tab.data.pinned and not force:
|
||||
message.confirm_async(
|
||||
title='Pinned Tab',
|
||||
text="Are you sure you want to close a pinned tab?",
|
||||
yes_action=yes_action, default=False)
|
||||
else:
|
||||
yes_action()
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window')
|
||||
@cmdutils.argument('count', count=True)
|
||||
def tab_close(self, prev=False, next_=False, opposite=False,
|
||||
@ -260,7 +248,7 @@ class CommandDispatcher:
|
||||
close = functools.partial(self._tab_close, tab, prev,
|
||||
next_, opposite)
|
||||
|
||||
self._tab_close_prompt_if_pinned(tab, force, close)
|
||||
self._tabbed_browser.tab_close_prompt_if_pinned(tab, force, close)
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window',
|
||||
name='tab-pin')
|
||||
@ -280,13 +268,11 @@ class CommandDispatcher:
|
||||
return
|
||||
|
||||
to_pin = not tab.data.pinned
|
||||
tab_index = self._current_index() if count is None else count - 1
|
||||
cmdutils.check_overflow(tab_index + 1, 'int')
|
||||
self._tabbed_browser.set_tab_pinned(tab_index, to_pin)
|
||||
self._tabbed_browser.set_tab_pinned(tab, to_pin)
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', name='open',
|
||||
maxsplit=0, scope='window')
|
||||
@cmdutils.argument('url', completion=usertypes.Completion.url)
|
||||
@cmdutils.argument('url', completion=urlmodel.url)
|
||||
@cmdutils.argument('count', count=True)
|
||||
def openurl(self, url=None, related=False,
|
||||
bg=False, tab=False, window=False, count=None, secure=False,
|
||||
@ -438,9 +424,18 @@ class CommandDispatcher:
|
||||
message.error("Printing failed!")
|
||||
diag.deleteLater()
|
||||
|
||||
def do_print():
|
||||
"""Called when the dialog was closed."""
|
||||
tab.printing.to_printer(diag.printer(), print_callback)
|
||||
|
||||
diag = QPrintDialog(tab)
|
||||
diag.open(lambda: tab.printing.to_printer(diag.printer(),
|
||||
print_callback))
|
||||
if sys.platform == 'darwin':
|
||||
# For some reason we get a segfault when using open() on macOS
|
||||
ret = diag.exec_()
|
||||
if ret == QDialog.Accepted:
|
||||
do_print()
|
||||
else:
|
||||
diag.open(do_print)
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', name='print',
|
||||
scope='window')
|
||||
@ -515,7 +510,7 @@ class CommandDispatcher:
|
||||
newtab.data.keep_icon = True
|
||||
newtab.history.deserialize(history)
|
||||
newtab.zoom.set_factor(curtab.zoom.factor())
|
||||
new_tabbed_browser.set_tab_pinned(idx, curtab.data.pinned)
|
||||
new_tabbed_browser.set_tab_pinned(newtab, curtab.data.pinned)
|
||||
return newtab
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window')
|
||||
@ -542,15 +537,13 @@ class CommandDispatcher:
|
||||
else:
|
||||
widget = self._current_widget()
|
||||
|
||||
for _ in range(count):
|
||||
try:
|
||||
if forward:
|
||||
if not widget.history.can_go_forward():
|
||||
raise cmdexc.CommandError("At end of history.")
|
||||
widget.history.forward()
|
||||
widget.history.forward(count)
|
||||
else:
|
||||
if not widget.history.can_go_back():
|
||||
raise cmdexc.CommandError("At beginning of history.")
|
||||
widget.history.back()
|
||||
widget.history.back(count)
|
||||
except browsertab.WebTabError as e:
|
||||
raise cmdexc.CommandError(e)
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window')
|
||||
@cmdutils.argument('count', count=True)
|
||||
@ -920,8 +913,9 @@ class CommandDispatcher:
|
||||
if not force:
|
||||
for i, tab in enumerate(self._tabbed_browser.widgets()):
|
||||
if _to_close(i) and tab.data.pinned:
|
||||
self._tab_close_prompt_if_pinned(
|
||||
tab, force,
|
||||
self._tabbed_browser.tab_close_prompt_if_pinned(
|
||||
tab,
|
||||
force,
|
||||
lambda: self.tab_only(
|
||||
prev=prev, next_=next_, force=True))
|
||||
return
|
||||
@ -1016,7 +1010,7 @@ class CommandDispatcher:
|
||||
self._open(url, tab, bg, window)
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window')
|
||||
@cmdutils.argument('index', completion=usertypes.Completion.tab)
|
||||
@cmdutils.argument('index', completion=miscmodels.buffer)
|
||||
def buffer(self, index):
|
||||
"""Select tab by index or url/title best match.
|
||||
|
||||
@ -1032,11 +1026,10 @@ class CommandDispatcher:
|
||||
for part in index_parts:
|
||||
int(part)
|
||||
except ValueError:
|
||||
model = instances.get(usertypes.Completion.tab)
|
||||
sf = sortfilter.CompletionFilterModel(source=model)
|
||||
sf.set_pattern(index)
|
||||
if sf.count() > 0:
|
||||
index = sf.data(sf.first_item())
|
||||
model = miscmodels.buffer()
|
||||
model.set_pattern(index)
|
||||
if model.count() > 0:
|
||||
index = model.data(model.first_item())
|
||||
index_parts = index.split('/', 1)
|
||||
else:
|
||||
raise cmdexc.CommandError(
|
||||
@ -1167,6 +1160,7 @@ class CommandDispatcher:
|
||||
detach: Whether the command should be detached from qutebrowser.
|
||||
cmdline: The commandline to execute.
|
||||
"""
|
||||
cmdutils.check_exclusive((userscript, detach), 'ud')
|
||||
try:
|
||||
cmd, *args = shlex.split(cmdline)
|
||||
except ValueError as e:
|
||||
@ -1241,8 +1235,7 @@ class CommandDispatcher:
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window',
|
||||
maxsplit=0)
|
||||
@cmdutils.argument('name',
|
||||
completion=usertypes.Completion.quickmark_by_name)
|
||||
@cmdutils.argument('name', completion=miscmodels.quickmark)
|
||||
def quickmark_load(self, name, tab=False, bg=False, window=False):
|
||||
"""Load a quickmark.
|
||||
|
||||
@ -1260,8 +1253,7 @@ class CommandDispatcher:
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window',
|
||||
maxsplit=0)
|
||||
@cmdutils.argument('name',
|
||||
completion=usertypes.Completion.quickmark_by_name)
|
||||
@cmdutils.argument('name', completion=miscmodels.quickmark)
|
||||
def quickmark_del(self, name=None):
|
||||
"""Delete a quickmark.
|
||||
|
||||
@ -1323,7 +1315,7 @@ class CommandDispatcher:
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window',
|
||||
maxsplit=0)
|
||||
@cmdutils.argument('url', completion=usertypes.Completion.bookmark_by_url)
|
||||
@cmdutils.argument('url', completion=miscmodels.bookmark)
|
||||
def bookmark_load(self, url, tab=False, bg=False, window=False,
|
||||
delete=False):
|
||||
"""Load a bookmark.
|
||||
@ -1345,7 +1337,7 @@ class CommandDispatcher:
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window',
|
||||
maxsplit=0)
|
||||
@cmdutils.argument('url', completion=usertypes.Completion.bookmark_by_url)
|
||||
@cmdutils.argument('url', completion=miscmodels.bookmark)
|
||||
def bookmark_del(self, url=None):
|
||||
"""Delete a bookmark.
|
||||
|
||||
@ -1450,8 +1442,18 @@ class CommandDispatcher:
|
||||
download_manager.get_mhtml(tab, target)
|
||||
else:
|
||||
qnam = tab.networkaccessmanager()
|
||||
download_manager.get(self._current_url(), user_agent=user_agent,
|
||||
qnam=qnam, target=target)
|
||||
|
||||
suggested_fn = downloads.suggested_fn_from_title(
|
||||
self._current_url().path(), tab.title()
|
||||
)
|
||||
|
||||
download_manager.get(
|
||||
self._current_url(),
|
||||
user_agent=user_agent,
|
||||
qnam=qnam,
|
||||
target=target,
|
||||
suggested_fn=suggested_fn
|
||||
)
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window')
|
||||
def view_source(self):
|
||||
@ -1519,7 +1521,7 @@ class CommandDispatcher:
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', name='help',
|
||||
scope='window')
|
||||
@cmdutils.argument('topic', completion=usertypes.Completion.helptopic)
|
||||
@cmdutils.argument('topic', completion=miscmodels.helptopic)
|
||||
def show_help(self, tab=False, bg=False, window=False, topic=None):
|
||||
r"""Show help about a command or setting.
|
||||
|
||||
@ -1728,7 +1730,8 @@ class CommandDispatcher:
|
||||
"""
|
||||
self.set_mark("'")
|
||||
tab = self._current_widget()
|
||||
tab.search.clear()
|
||||
if tab.search.search_displayed:
|
||||
tab.search.clear()
|
||||
|
||||
if not text:
|
||||
return
|
||||
@ -2159,6 +2162,10 @@ class CommandDispatcher:
|
||||
|
||||
window = self._tabbed_browser.window()
|
||||
if window.isFullScreen():
|
||||
window.showNormal()
|
||||
window.setWindowState(
|
||||
window.state_before_fullscreen & ~Qt.WindowFullScreen)
|
||||
else:
|
||||
window.state_before_fullscreen = window.windowState()
|
||||
window.showFullScreen()
|
||||
log.misc.debug('state before fullscreen: {}'.format(
|
||||
debug.qflags_key(Qt, window.state_before_fullscreen)))
|
||||
|
@ -181,6 +181,28 @@ def transform_path(path):
|
||||
return path
|
||||
|
||||
|
||||
def suggested_fn_from_title(url_path, title=None):
|
||||
"""Suggest a filename depending on the URL extension and page title.
|
||||
|
||||
Args:
|
||||
url_path: a string with the URL path
|
||||
title: the page title string
|
||||
|
||||
Return:
|
||||
The download filename based on the title, or None if the extension is
|
||||
not found in the whitelist (or if there is no page title).
|
||||
"""
|
||||
ext_whitelist = [".html", ".htm", ".php", ""]
|
||||
_, ext = os.path.splitext(url_path)
|
||||
if ext.lower() in ext_whitelist and title:
|
||||
suggested_fn = utils.sanitize_filename(title)
|
||||
if not suggested_fn.lower().endswith((".html", ".htm")):
|
||||
suggested_fn += ".html"
|
||||
else:
|
||||
suggested_fn = None
|
||||
return suggested_fn
|
||||
|
||||
|
||||
class NoFilenameError(Exception):
|
||||
|
||||
"""Raised when we can't find out a filename in DownloadTarget."""
|
||||
|
@ -19,214 +19,82 @@
|
||||
|
||||
"""Simple history which gets written to disk."""
|
||||
|
||||
import os
|
||||
import time
|
||||
import collections
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QUrl, QObject
|
||||
from PyQt5.QtCore import pyqtSlot, QUrl, QTimer
|
||||
|
||||
from qutebrowser.commands import cmdutils
|
||||
from qutebrowser.utils import (utils, objreg, standarddir, log, qtutils,
|
||||
usertypes, message)
|
||||
from qutebrowser.misc import lineparser, objects
|
||||
from qutebrowser.commands import cmdutils, cmdexc
|
||||
from qutebrowser.utils import (utils, objreg, log, usertypes, message,
|
||||
debug, standarddir)
|
||||
from qutebrowser.misc import objects, sql
|
||||
|
||||
|
||||
class Entry:
|
||||
class CompletionHistory(sql.SqlTable):
|
||||
|
||||
"""A single entry in the web history.
|
||||
"""History which only has the newest entry for each URL."""
|
||||
|
||||
Attributes:
|
||||
atime: The time the page was accessed.
|
||||
url: The URL which was accessed as QUrl.
|
||||
redirect: If True, don't save this entry to disk
|
||||
"""
|
||||
|
||||
def __init__(self, atime, url, title, redirect=False):
|
||||
self.atime = float(atime)
|
||||
self.url = url
|
||||
self.title = title
|
||||
self.redirect = redirect
|
||||
qtutils.ensure_valid(url)
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self, constructor=True, atime=self.atime,
|
||||
url=self.url_str(), title=self.title,
|
||||
redirect=self.redirect)
|
||||
|
||||
def __str__(self):
|
||||
atime = str(int(self.atime))
|
||||
if self.redirect:
|
||||
atime += '-r' # redirect flag
|
||||
elems = [atime, self.url_str()]
|
||||
if self.title:
|
||||
elems.append(self.title)
|
||||
return ' '.join(elems)
|
||||
|
||||
def __eq__(self, other):
|
||||
return (self.atime == other.atime and
|
||||
self.title == other.title and
|
||||
self.url == other.url and
|
||||
self.redirect == other.redirect)
|
||||
|
||||
def url_str(self):
|
||||
"""Get the URL as a lossless string."""
|
||||
return self.url.toString(QUrl.FullyEncoded | QUrl.RemovePassword)
|
||||
|
||||
@classmethod
|
||||
def from_str(cls, line):
|
||||
"""Parse a history line like '12345 http://example.com title'."""
|
||||
data = line.split(maxsplit=2)
|
||||
if len(data) == 2:
|
||||
atime, url = data
|
||||
title = ""
|
||||
elif len(data) == 3:
|
||||
atime, url, title = data
|
||||
else:
|
||||
raise ValueError("2 or 3 fields expected")
|
||||
|
||||
url = QUrl(url)
|
||||
if not url.isValid():
|
||||
raise ValueError("Invalid URL: {}".format(url.errorString()))
|
||||
|
||||
# https://github.com/qutebrowser/qutebrowser/issues/670
|
||||
atime = atime.lstrip('\0')
|
||||
|
||||
if '-' in atime:
|
||||
atime, flags = atime.split('-')
|
||||
else:
|
||||
flags = ''
|
||||
|
||||
if not set(flags).issubset('r'):
|
||||
raise ValueError("Invalid flags {!r}".format(flags))
|
||||
|
||||
redirect = 'r' in flags
|
||||
|
||||
return cls(atime, url, title, redirect=redirect)
|
||||
def __init__(self, parent=None):
|
||||
super().__init__("CompletionHistory", ['url', 'title', 'last_atime'],
|
||||
constraints={'url': 'PRIMARY KEY'}, parent=parent)
|
||||
self.create_index('CompletionHistoryAtimeIndex', 'last_atime')
|
||||
|
||||
|
||||
class WebHistory(QObject):
|
||||
class WebHistory(sql.SqlTable):
|
||||
|
||||
"""The global history of visited pages.
|
||||
"""The global history of visited pages."""
|
||||
|
||||
This is a little more complex as you'd expect so the history can be read
|
||||
from disk async while new history is already arriving.
|
||||
def __init__(self, parent=None):
|
||||
super().__init__("History", ['url', 'title', 'atime', 'redirect'],
|
||||
parent=parent)
|
||||
self.completion = CompletionHistory(parent=self)
|
||||
self.create_index('HistoryIndex', 'url')
|
||||
self.create_index('HistoryAtimeIndex', 'atime')
|
||||
self._contains_query = self.contains_query('url')
|
||||
self._between_query = sql.Query('SELECT * FROM History '
|
||||
'where not redirect '
|
||||
'and not url like "qute://%" '
|
||||
'and atime > :earliest '
|
||||
'and atime <= :latest '
|
||||
'ORDER BY atime desc')
|
||||
|
||||
self.history_dict is the main place where the history is stored, in an
|
||||
OrderedDict (sorted by time) of URL strings mapped to Entry objects.
|
||||
|
||||
While reading from disk is still ongoing, the history is saved in
|
||||
self._temp_history instead, and then appended to self.history_dict once
|
||||
that's fully populated.
|
||||
|
||||
All history which is new in this session (rather than read from disk from a
|
||||
previous browsing session) is also stored in self._new_history.
|
||||
self._saved_count tracks how many of those entries were already written to
|
||||
disk, so we can always append to the existing data.
|
||||
|
||||
Attributes:
|
||||
history_dict: An OrderedDict of URLs read from the on-disk history.
|
||||
_lineparser: The AppendLineParser used to save the history.
|
||||
_new_history: A list of Entry items of the current session.
|
||||
_saved_count: How many HistoryEntries have been written to disk.
|
||||
_initial_read_started: Whether async_read was called.
|
||||
_initial_read_done: Whether async_read has completed.
|
||||
_temp_history: OrderedDict of temporary history entries before
|
||||
async_read was called.
|
||||
|
||||
Signals:
|
||||
add_completion_item: Emitted before a new Entry is added.
|
||||
Used to sync with the completion.
|
||||
arg: The new Entry.
|
||||
item_added: Emitted after a new Entry is added.
|
||||
Used to tell the savemanager that the history is dirty.
|
||||
arg: The new Entry.
|
||||
cleared: Emitted after the history is cleared.
|
||||
"""
|
||||
|
||||
add_completion_item = pyqtSignal(Entry)
|
||||
item_added = pyqtSignal(Entry)
|
||||
cleared = pyqtSignal()
|
||||
async_read_done = pyqtSignal()
|
||||
|
||||
def __init__(self, hist_dir, hist_name, parent=None):
|
||||
super().__init__(parent)
|
||||
self._initial_read_started = False
|
||||
self._initial_read_done = False
|
||||
self._lineparser = lineparser.AppendLineParser(hist_dir, hist_name,
|
||||
parent=self)
|
||||
self.history_dict = collections.OrderedDict()
|
||||
self._temp_history = collections.OrderedDict()
|
||||
self._new_history = []
|
||||
self._saved_count = 0
|
||||
objreg.get('save-manager').add_saveable(
|
||||
'history', self.save, self.item_added)
|
||||
self._before_query = sql.Query('SELECT * FROM History '
|
||||
'where not redirect '
|
||||
'and not url like "qute://%" '
|
||||
'and atime <= :latest '
|
||||
'ORDER BY atime desc '
|
||||
'limit :limit offset :offset')
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self, length=len(self))
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.history_dict.values())
|
||||
|
||||
def __len__(self):
|
||||
return len(self.history_dict)
|
||||
|
||||
def async_read(self):
|
||||
"""Read the initial history."""
|
||||
if self._initial_read_started:
|
||||
log.init.debug("Ignoring async_read() because reading is started.")
|
||||
return
|
||||
self._initial_read_started = True
|
||||
|
||||
with self._lineparser.open():
|
||||
for line in self._lineparser:
|
||||
yield
|
||||
|
||||
line = line.rstrip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
try:
|
||||
entry = Entry.from_str(line)
|
||||
except ValueError as e:
|
||||
log.init.warning("Invalid history entry {!r}: {}!".format(
|
||||
line, e))
|
||||
continue
|
||||
|
||||
# This de-duplicates history entries; only the latest
|
||||
# entry for each URL is kept. If you want to keep
|
||||
# information about previous hits change the items in
|
||||
# old_urls to be lists or change Entry to have a
|
||||
# list of atimes.
|
||||
self._add_entry(entry)
|
||||
|
||||
self._initial_read_done = True
|
||||
self.async_read_done.emit()
|
||||
|
||||
for entry in self._temp_history.values():
|
||||
self._add_entry(entry)
|
||||
self._new_history.append(entry)
|
||||
if not entry.redirect:
|
||||
self.add_completion_item.emit(entry)
|
||||
self._temp_history.clear()
|
||||
|
||||
def _add_entry(self, entry, target=None):
|
||||
"""Add an entry to self.history_dict or another given OrderedDict."""
|
||||
if target is None:
|
||||
target = self.history_dict
|
||||
url_str = entry.url_str()
|
||||
target[url_str] = entry
|
||||
target.move_to_end(url_str)
|
||||
def __contains__(self, url):
|
||||
return self._contains_query.run(val=url).value()
|
||||
|
||||
def get_recent(self):
|
||||
"""Get the most recent history entries."""
|
||||
old = self._lineparser.get_recent()
|
||||
return old + [str(e) for e in self._new_history]
|
||||
return self.select(sort_by='atime', sort_order='desc', limit=100)
|
||||
|
||||
def save(self):
|
||||
"""Save the history to disk."""
|
||||
new = (str(e) for e in self._new_history[self._saved_count:])
|
||||
self._lineparser.new_data = new
|
||||
self._lineparser.save()
|
||||
self._saved_count = len(self._new_history)
|
||||
def entries_between(self, earliest, latest):
|
||||
"""Iterate non-redirect, non-qute entries between two timestamps.
|
||||
|
||||
Args:
|
||||
earliest: Omit timestamps earlier than this.
|
||||
latest: Omit timestamps later than this.
|
||||
"""
|
||||
self._between_query.run(earliest=earliest, latest=latest)
|
||||
return iter(self._between_query)
|
||||
|
||||
def entries_before(self, latest, limit, offset):
|
||||
"""Iterate non-redirect, non-qute entries occurring before a timestamp.
|
||||
|
||||
Args:
|
||||
latest: Omit timestamps more recent than this.
|
||||
limit: Max number of entries to include.
|
||||
offset: Number of entries to skip.
|
||||
"""
|
||||
self._before_query.run(latest=latest, limit=limit, offset=offset)
|
||||
return iter(self._before_query)
|
||||
|
||||
@cmdutils.register(name='history-clear', instance='web-history')
|
||||
def clear(self, force=False):
|
||||
@ -246,12 +114,17 @@ class WebHistory(QObject):
|
||||
"history?")
|
||||
|
||||
def _do_clear(self):
|
||||
self._lineparser.clear()
|
||||
self.history_dict.clear()
|
||||
self._temp_history.clear()
|
||||
self._new_history.clear()
|
||||
self._saved_count = 0
|
||||
self.cleared.emit()
|
||||
self.delete_all()
|
||||
self.completion.delete_all()
|
||||
|
||||
def delete_url(self, url):
|
||||
"""Remove all history entries with the given url.
|
||||
|
||||
Args:
|
||||
url: URL string to delete.
|
||||
"""
|
||||
self.delete('url', url)
|
||||
self.completion.delete('url', url)
|
||||
|
||||
@pyqtSlot(QUrl, QUrl, str)
|
||||
def add_from_tab(self, url, requested_url, title):
|
||||
@ -285,17 +158,130 @@ class WebHistory(QObject):
|
||||
log.misc.warning("Ignoring invalid URL being added to history")
|
||||
return
|
||||
|
||||
if atime is None:
|
||||
atime = time.time()
|
||||
entry = Entry(atime, url, title, redirect=redirect)
|
||||
if self._initial_read_done:
|
||||
self._add_entry(entry)
|
||||
self._new_history.append(entry)
|
||||
self.item_added.emit(entry)
|
||||
if not entry.redirect:
|
||||
self.add_completion_item.emit(entry)
|
||||
atime = int(atime) if (atime is not None) else int(time.time())
|
||||
url_str = url.toString(QUrl.FullyEncoded | QUrl.RemovePassword)
|
||||
self.insert({'url': url_str,
|
||||
'title': title,
|
||||
'atime': atime,
|
||||
'redirect': redirect})
|
||||
if not redirect:
|
||||
self.completion.insert({'url': url_str,
|
||||
'title': title,
|
||||
'last_atime': atime},
|
||||
replace=True)
|
||||
|
||||
def _parse_entry(self, line):
|
||||
"""Parse a history line like '12345 http://example.com title'."""
|
||||
if not line or line.startswith('#'):
|
||||
return None
|
||||
data = line.split(maxsplit=2)
|
||||
if len(data) == 2:
|
||||
atime, url = data
|
||||
title = ""
|
||||
elif len(data) == 3:
|
||||
atime, url, title = data
|
||||
else:
|
||||
self._add_entry(entry, target=self._temp_history)
|
||||
raise ValueError("2 or 3 fields expected")
|
||||
|
||||
# http://xn--pple-43d.com/ with
|
||||
# https://bugreports.qt.io/browse/QTBUG-60364
|
||||
if url in ['http://.com/', 'https://.com/',
|
||||
'http://www..com/', 'https://www..com/']:
|
||||
return None
|
||||
|
||||
url = QUrl(url)
|
||||
if not url.isValid():
|
||||
raise ValueError("Invalid URL: {}".format(url.errorString()))
|
||||
|
||||
# https://github.com/qutebrowser/qutebrowser/issues/2646
|
||||
if url.scheme() == 'data':
|
||||
return None
|
||||
|
||||
# https://github.com/qutebrowser/qutebrowser/issues/670
|
||||
atime = atime.lstrip('\0')
|
||||
|
||||
if '-' in atime:
|
||||
atime, flags = atime.split('-')
|
||||
else:
|
||||
flags = ''
|
||||
|
||||
if not set(flags).issubset('r'):
|
||||
raise ValueError("Invalid flags {!r}".format(flags))
|
||||
|
||||
redirect = 'r' in flags
|
||||
return (url, title, int(atime), redirect)
|
||||
|
||||
def import_txt(self):
|
||||
"""Import a history text file into sqlite if it exists.
|
||||
|
||||
In older versions of qutebrowser, history was stored in a text format.
|
||||
This converts that file into the new sqlite format and moves it to a
|
||||
backup location.
|
||||
"""
|
||||
path = os.path.join(standarddir.data(), 'history')
|
||||
if not os.path.isfile(path):
|
||||
return
|
||||
|
||||
def action():
|
||||
with debug.log_time(log.init, 'Import old history file to sqlite'):
|
||||
try:
|
||||
self._read(path)
|
||||
except ValueError as ex:
|
||||
message.error('Failed to import history: {}'.format(ex))
|
||||
else:
|
||||
bakpath = path + '.bak'
|
||||
message.info('History import complete. Moving {} to {}'
|
||||
.format(path, bakpath))
|
||||
os.rename(path, bakpath)
|
||||
|
||||
# delay to give message time to appear before locking down for import
|
||||
message.info('Converting {} to sqlite...'.format(path))
|
||||
QTimer.singleShot(100, action)
|
||||
|
||||
def _read(self, path):
|
||||
"""Import a text file into the sql database."""
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
data = {'url': [], 'title': [], 'atime': [], 'redirect': []}
|
||||
completion_data = {'url': [], 'title': [], 'last_atime': []}
|
||||
for (i, line) in enumerate(f):
|
||||
try:
|
||||
parsed = self._parse_entry(line.strip())
|
||||
if parsed is None:
|
||||
continue
|
||||
url, title, atime, redirect = parsed
|
||||
data['url'].append(url)
|
||||
data['title'].append(title)
|
||||
data['atime'].append(atime)
|
||||
data['redirect'].append(redirect)
|
||||
if not redirect:
|
||||
completion_data['url'].append(url)
|
||||
completion_data['title'].append(title)
|
||||
completion_data['last_atime'].append(atime)
|
||||
except ValueError as ex:
|
||||
raise ValueError('Failed to parse line #{} of {}: "{}"'
|
||||
.format(i, path, ex))
|
||||
self.insert_batch(data)
|
||||
self.completion.insert_batch(completion_data, replace=True)
|
||||
|
||||
@cmdutils.register(instance='web-history', debug=True)
|
||||
def debug_dump_history(self, dest):
|
||||
"""Dump the history to a file in the old pre-SQL format.
|
||||
|
||||
Args:
|
||||
dest: Where to write the file to.
|
||||
"""
|
||||
dest = os.path.expanduser(dest)
|
||||
|
||||
lines = ('{}{} {} {}'
|
||||
.format(int(x.atime), '-r' * x.redirect, x.url, x.title)
|
||||
for x in self.select(sort_by='atime', sort_order='asc'))
|
||||
|
||||
try:
|
||||
with open(dest, 'w', encoding='utf-8') as f:
|
||||
f.write('\n'.join(lines))
|
||||
message.info("Dumped history to {}".format(dest))
|
||||
except OSError as e:
|
||||
raise cmdexc.CommandError('Could not write history: {}', e)
|
||||
|
||||
|
||||
def init(parent=None):
|
||||
@ -304,8 +290,7 @@ def init(parent=None):
|
||||
Args:
|
||||
parent: The parent to use for WebHistory.
|
||||
"""
|
||||
history = WebHistory(hist_dir=standarddir.data(), hist_name='history',
|
||||
parent=parent)
|
||||
history = WebHistory(parent=parent)
|
||||
objreg.register('web-history', history)
|
||||
|
||||
if objects.backend == usertypes.Backend.QtWebKit:
|
||||
|
@ -412,7 +412,8 @@ class DownloadManager(downloads.AbstractDownloadManager):
|
||||
mhtml.start_download_checked, tab=tab))
|
||||
message.global_bridge.ask(question, blocking=False)
|
||||
|
||||
def get_request(self, request, *, target=None, **kwargs):
|
||||
def get_request(self, request, *, target=None,
|
||||
suggested_fn=None, **kwargs):
|
||||
"""Start a download with a QNetworkRequest.
|
||||
|
||||
Args:
|
||||
@ -428,7 +429,9 @@ class DownloadManager(downloads.AbstractDownloadManager):
|
||||
request.setAttribute(QNetworkRequest.CacheLoadControlAttribute,
|
||||
QNetworkRequest.AlwaysNetwork)
|
||||
|
||||
if request.url().scheme().lower() != 'data':
|
||||
if suggested_fn is not None:
|
||||
pass
|
||||
elif request.url().scheme().lower() != 'data':
|
||||
suggested_fn = urlutils.filename_from_url(request.url())
|
||||
else:
|
||||
# We might be downloading a binary blob embedded on a page or even
|
||||
|
@ -26,7 +26,6 @@ Module attributes:
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import urllib.parse
|
||||
import datetime
|
||||
@ -185,88 +184,36 @@ def qute_bookmarks(_url):
|
||||
return 'text/html', html
|
||||
|
||||
|
||||
def history_data(start_time): # noqa
|
||||
"""Return history data
|
||||
def history_data(start_time, offset=None):
|
||||
"""Return history data.
|
||||
|
||||
Arguments:
|
||||
start_time -- select history starting from this timestamp.
|
||||
start_time: select history starting from this timestamp.
|
||||
offset: number of items to skip
|
||||
"""
|
||||
def history_iter(start_time, reverse=False):
|
||||
"""Iterate through the history and get items we're interested.
|
||||
|
||||
Arguments:
|
||||
reverse -- whether to reverse the history_dict before iterating.
|
||||
"""
|
||||
history = objreg.get('web-history').history_dict.values()
|
||||
if reverse:
|
||||
history = reversed(history)
|
||||
|
||||
# when history_dict is not reversed, we need to keep track of last item
|
||||
# so that we can yield its atime
|
||||
last_item = None
|
||||
|
||||
# history atimes are stored as ints, ensure start_time is not a float
|
||||
start_time = int(start_time)
|
||||
hist = objreg.get('web-history')
|
||||
if offset is not None:
|
||||
entries = hist.entries_before(start_time, limit=1000, offset=offset)
|
||||
else:
|
||||
# end is 24hrs earlier than start
|
||||
end_time = start_time - 24*60*60
|
||||
entries = hist.entries_between(end_time, start_time)
|
||||
|
||||
for item in history:
|
||||
# Skip redirects
|
||||
# Skip qute:// links
|
||||
if item.redirect or item.url.scheme() == 'qute':
|
||||
continue
|
||||
|
||||
# Skip items out of time window
|
||||
item_newer = item.atime > start_time
|
||||
item_older = item.atime <= end_time
|
||||
if reverse:
|
||||
# history_dict is reversed, we are going back in history.
|
||||
# so:
|
||||
# abort if item is older than start_time+24hr
|
||||
# skip if item is newer than start
|
||||
if item_older:
|
||||
yield {"next": int(item.atime)}
|
||||
return
|
||||
if item_newer:
|
||||
continue
|
||||
else:
|
||||
# history_dict isn't reversed, we are going forward in history.
|
||||
# so:
|
||||
# abort if item is newer than start_time
|
||||
# skip if item is older than start_time+24hrs
|
||||
if item_older:
|
||||
last_item = item
|
||||
continue
|
||||
if item_newer:
|
||||
yield {"next": int(last_item.atime if last_item else -1)}
|
||||
return
|
||||
|
||||
# Use item's url as title if there's no title.
|
||||
item_url = item.url.toDisplayString()
|
||||
item_title = item.title if item.title else item_url
|
||||
item_time = int(item.atime * 1000)
|
||||
|
||||
yield {"url": item_url, "title": item_title, "time": item_time}
|
||||
|
||||
# if we reached here, we had reached the end of history
|
||||
yield {"next": int(last_item.atime if last_item else -1)}
|
||||
|
||||
if sys.hexversion >= 0x03050000:
|
||||
# On Python >= 3.5 we can reverse the ordereddict in-place and thus
|
||||
# apply an additional performance improvement in history_iter.
|
||||
# On my machine, this gets us down from 550ms to 72us with 500k old
|
||||
# items.
|
||||
history = history_iter(start_time, reverse=True)
|
||||
else:
|
||||
# On Python 3.4, we can't do that, so we'd need to copy the entire
|
||||
# history to a list. There, filter first and then reverse it here.
|
||||
history = reversed(list(history_iter(start_time, reverse=False)))
|
||||
|
||||
return list(history)
|
||||
return [{"url": e.url, "title": e.title or e.url, "time": e.atime}
|
||||
for e in entries]
|
||||
|
||||
|
||||
@add_handler('history')
|
||||
def qute_history(url):
|
||||
"""Handler for qute://history. Display and serve history."""
|
||||
if url.path() == '/data':
|
||||
try:
|
||||
offset = QUrlQuery(url).queryItemValue("offset")
|
||||
offset = int(offset) if offset else None
|
||||
except ValueError as e:
|
||||
raise QuteSchemeError("Query parameter offset is invalid", e)
|
||||
# Use start_time in query or current time.
|
||||
try:
|
||||
start_time = QUrlQuery(url).queryItemValue("start_time")
|
||||
@ -274,7 +221,7 @@ def qute_history(url):
|
||||
except ValueError as e:
|
||||
raise QuteSchemeError("Query parameter start_time is invalid", e)
|
||||
|
||||
return 'text/html', json.dumps(history_data(start_time))
|
||||
return 'text/html', json.dumps(history_data(start_time, offset))
|
||||
else:
|
||||
if (
|
||||
config.val.content.javascript.enabled and
|
||||
@ -306,9 +253,9 @@ def qute_history(url):
|
||||
start_time = time.mktime(next_date.timetuple()) - 1
|
||||
history = [
|
||||
(i["url"], i["title"],
|
||||
datetime.datetime.fromtimestamp(i["time"]/1000),
|
||||
datetime.datetime.fromtimestamp(i["time"]),
|
||||
QUrl(i["url"]).host())
|
||||
for i in history_data(start_time) if "next" not in i
|
||||
for i in history_data(start_time)
|
||||
]
|
||||
|
||||
return 'text/html', jinja.render(
|
||||
|
@ -77,13 +77,9 @@ class UrlMarkManager(QObject):
|
||||
|
||||
Signals:
|
||||
changed: Emitted when anything changed.
|
||||
added: Emitted when a new quickmark/bookmark was added.
|
||||
removed: Emitted when an existing quickmark/bookmark was removed.
|
||||
"""
|
||||
|
||||
changed = pyqtSignal()
|
||||
added = pyqtSignal(str, str)
|
||||
removed = pyqtSignal(str)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
"""Initialize and read quickmarks."""
|
||||
@ -121,7 +117,6 @@ class UrlMarkManager(QObject):
|
||||
"""
|
||||
del self.marks[key]
|
||||
self.changed.emit()
|
||||
self.removed.emit(key)
|
||||
|
||||
|
||||
class QuickmarkManager(UrlMarkManager):
|
||||
@ -133,7 +128,6 @@ class QuickmarkManager(UrlMarkManager):
|
||||
- self.marks maps names to URLs.
|
||||
- changed gets emitted with the name as first argument and the URL as
|
||||
second argument.
|
||||
- removed gets emitted with the name as argument.
|
||||
"""
|
||||
|
||||
def _init_lineparser(self):
|
||||
@ -193,7 +187,6 @@ class QuickmarkManager(UrlMarkManager):
|
||||
"""Really set the quickmark."""
|
||||
self.marks[name] = url
|
||||
self.changed.emit()
|
||||
self.added.emit(name, url)
|
||||
log.misc.debug("Added quickmark {} for {}".format(name, url))
|
||||
|
||||
if name in self.marks:
|
||||
@ -243,7 +236,6 @@ class BookmarkManager(UrlMarkManager):
|
||||
- self.marks maps URLs to titles.
|
||||
- changed gets emitted with the URL as first argument and the title as
|
||||
second argument.
|
||||
- removed gets emitted with the URL as argument.
|
||||
"""
|
||||
|
||||
def _init_lineparser(self):
|
||||
@ -295,5 +287,4 @@ class BookmarkManager(UrlMarkManager):
|
||||
else:
|
||||
self.marks[urlstr] = title
|
||||
self.changed.emit()
|
||||
self.added.emit(title, urlstr)
|
||||
return True
|
||||
|
@ -28,7 +28,9 @@ Module attributes:
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
import sys
|
||||
import ctypes
|
||||
import ctypes.util
|
||||
|
||||
from PyQt5.QtGui import QFont
|
||||
from PyQt5.QtWebEngineWidgets import (QWebEngineSettings, QWebEngineProfile,
|
||||
@ -203,12 +205,10 @@ def init(args):
|
||||
if args.enable_webengine_inspector:
|
||||
os.environ['QTWEBENGINE_REMOTE_DEBUGGING'] = str(utils.random_port())
|
||||
|
||||
# Workaround for a black screen with some setups
|
||||
# https://github.com/spyder-ide/spyder/issues/3226
|
||||
if not os.environ.get('QUTE_NO_OPENGL_WORKAROUND'):
|
||||
# Hide "No OpenGL_accelerate module loaded: ..." message
|
||||
logging.getLogger('OpenGL.acceleratesupport').propagate = False
|
||||
from OpenGL import GL # pylint: disable=unused-variable
|
||||
# WORKAROUND for
|
||||
# https://bugs.launchpad.net/ubuntu/+source/python-qt4/+bug/941826
|
||||
if sys.platform == 'linux':
|
||||
ctypes.CDLL(ctypes.util.find_library("GL"), mode=ctypes.RTLD_GLOBAL)
|
||||
|
||||
_init_profiles()
|
||||
|
||||
|
@ -51,12 +51,13 @@ def init():
|
||||
global _qute_scheme_handler
|
||||
app = QApplication.instance()
|
||||
|
||||
software_rendering = os.environ.get('LIBGL_ALWAYS_SOFTWARE') == '1'
|
||||
software_rendering = (os.environ.get('LIBGL_ALWAYS_SOFTWARE') == '1' or
|
||||
'QT_XCB_FORCE_SOFTWARE_OPENGL' in os.environ)
|
||||
if version.opengl_vendor() == 'nouveau' and not software_rendering:
|
||||
# FIXME:qtwebengine display something more sophisticated here
|
||||
raise browsertab.WebTabError(
|
||||
"QtWebEngine is not supported with Nouveau graphics (unless "
|
||||
"LIBGL_ALWAYS_SOFTWARE is set as environment variable).")
|
||||
"QT_XCB_FORCE_SOFTWARE_OPENGL is set as environment variable).")
|
||||
|
||||
log.init.debug("Initializing qute://* handler...")
|
||||
_qute_scheme_handler = webenginequtescheme.QuteSchemeHandler(parent=app)
|
||||
@ -406,18 +407,18 @@ class WebEngineHistory(browsertab.AbstractHistory):
|
||||
def current_idx(self):
|
||||
return self._history.currentItemIndex()
|
||||
|
||||
def back(self):
|
||||
self._history.back()
|
||||
|
||||
def forward(self):
|
||||
self._history.forward()
|
||||
|
||||
def can_go_back(self):
|
||||
return self._history.canGoBack()
|
||||
|
||||
def can_go_forward(self):
|
||||
return self._history.canGoForward()
|
||||
|
||||
def _item_at(self, i):
|
||||
return self._history.itemAt(i)
|
||||
|
||||
def _go_to_item(self, item):
|
||||
return self._history.goToItem(item)
|
||||
|
||||
def serialize(self):
|
||||
if not qtutils.version_check('5.9'):
|
||||
# WORKAROUND for
|
||||
@ -611,6 +612,7 @@ class WebEngineTab(browsertab.AbstractTab):
|
||||
|
||||
def shutdown(self):
|
||||
self.shutting_down.emit()
|
||||
self.action.exit_fullscreen()
|
||||
if qtutils.version_check('5.8', exact=True):
|
||||
# WORKAROUND for
|
||||
# https://bugreports.qt.io/browse/QTBUG-58563
|
||||
@ -711,7 +713,8 @@ class WebEngineTab(browsertab.AbstractTab):
|
||||
@pyqtSlot()
|
||||
def _on_load_started(self):
|
||||
"""Clear search when a new load is started if needed."""
|
||||
if qtutils.version_check('5.9'):
|
||||
if (qtutils.version_check('5.9') and
|
||||
not qtutils.version_check('5.9.2')):
|
||||
# WORKAROUND for
|
||||
# https://bugreports.qt.io/browse/QTBUG-61506
|
||||
self.search.clear()
|
||||
|
@ -19,9 +19,12 @@
|
||||
|
||||
"""QtWebKit specific part of history."""
|
||||
|
||||
import functools
|
||||
|
||||
from PyQt5.QtWebKit import QWebHistoryInterface
|
||||
|
||||
from qutebrowser.utils import debug
|
||||
|
||||
|
||||
class WebHistoryInterface(QWebHistoryInterface):
|
||||
|
||||
@ -34,11 +37,13 @@ class WebHistoryInterface(QWebHistoryInterface):
|
||||
def __init__(self, webhistory, parent=None):
|
||||
super().__init__(parent)
|
||||
self._history = webhistory
|
||||
self._history.changed.connect(self.historyContains.cache_clear)
|
||||
|
||||
def addHistoryEntry(self, url_string):
|
||||
"""Required for a QWebHistoryInterface impl, obsoleted by add_url."""
|
||||
pass
|
||||
|
||||
@functools.lru_cache(maxsize=32768)
|
||||
def historyContains(self, url_string):
|
||||
"""Called by WebKit to determine if a URL is contained in the history.
|
||||
|
||||
@ -48,7 +53,8 @@ class WebHistoryInterface(QWebHistoryInterface):
|
||||
Return:
|
||||
True if the url is in the history, False otherwise.
|
||||
"""
|
||||
return url_string in self._history.history_dict
|
||||
with debug.log_time('sql', 'historyContains'):
|
||||
return url_string in self._history
|
||||
|
||||
|
||||
def init(history):
|
||||
|
@ -32,7 +32,6 @@ from PyQt5.QtWebKit import QWebSettings
|
||||
from PyQt5.QtPrintSupport import QPrinter
|
||||
|
||||
from qutebrowser.browser import browsertab
|
||||
from qutebrowser.browser.network import proxy
|
||||
from qutebrowser.browser.webkit import webview, tabhistory, webkitelem
|
||||
from qutebrowser.utils import qtutils, objreg, usertypes, utils, log, debug
|
||||
|
||||
@ -502,18 +501,18 @@ class WebKitHistory(browsertab.AbstractHistory):
|
||||
def current_idx(self):
|
||||
return self._history.currentItemIndex()
|
||||
|
||||
def back(self):
|
||||
self._history.back()
|
||||
|
||||
def forward(self):
|
||||
self._history.forward()
|
||||
|
||||
def can_go_back(self):
|
||||
return self._history.canGoBack()
|
||||
|
||||
def can_go_forward(self):
|
||||
return self._history.canGoForward()
|
||||
|
||||
def _item_at(self, i):
|
||||
return self._history.itemAt(i)
|
||||
|
||||
def _go_to_item(self, item):
|
||||
return self._history.goToItem(item)
|
||||
|
||||
def serialize(self):
|
||||
return qtutils.serialize(self._history)
|
||||
|
||||
|
@ -23,8 +23,8 @@ from PyQt5.QtCore import pyqtSlot, QObject, QTimer
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.commands import cmdutils, runners
|
||||
from qutebrowser.utils import usertypes, log, utils
|
||||
from qutebrowser.completion.models import instances, sortfilter
|
||||
from qutebrowser.utils import log, utils, debug
|
||||
from qutebrowser.completion.models import miscmodels
|
||||
|
||||
|
||||
class Completer(QObject):
|
||||
@ -38,6 +38,7 @@ class Completer(QObject):
|
||||
_last_cursor_pos: The old cursor position so we avoid double completion
|
||||
updates.
|
||||
_last_text: The old command text so we avoid double completion updates.
|
||||
_last_completion_func: The completion function used for the last text.
|
||||
"""
|
||||
|
||||
def __init__(self, cmd, parent=None):
|
||||
@ -50,6 +51,7 @@ class Completer(QObject):
|
||||
self._timer.timeout.connect(self._update_completion)
|
||||
self._last_cursor_pos = None
|
||||
self._last_text = None
|
||||
self._last_completion_func = None
|
||||
self._cmd.update_completion.connect(self.schedule_completion_update)
|
||||
|
||||
def __repr__(self):
|
||||
@ -60,37 +62,8 @@ class Completer(QObject):
|
||||
completion = self.parent()
|
||||
return completion.model()
|
||||
|
||||
def _get_completion_model(self, completion, pos_args):
|
||||
"""Get a completion model based on an enum member.
|
||||
|
||||
Args:
|
||||
completion: A usertypes.Completion member.
|
||||
pos_args: The positional args entered before the cursor.
|
||||
|
||||
Return:
|
||||
A completion model or None.
|
||||
"""
|
||||
if completion == usertypes.Completion.option:
|
||||
section = pos_args[0]
|
||||
model = instances.get(completion).get(section)
|
||||
elif completion == usertypes.Completion.value:
|
||||
section = pos_args[0]
|
||||
option = pos_args[1]
|
||||
try:
|
||||
model = instances.get(completion)[section][option]
|
||||
except KeyError:
|
||||
# No completion model for this section/option.
|
||||
model = None
|
||||
else:
|
||||
model = instances.get(completion)
|
||||
|
||||
if model is None:
|
||||
return None
|
||||
else:
|
||||
return sortfilter.CompletionFilterModel(source=model, parent=self)
|
||||
|
||||
def _get_new_completion(self, before_cursor, under_cursor):
|
||||
"""Get a new completion.
|
||||
"""Get the completion function based on the current command text.
|
||||
|
||||
Args:
|
||||
before_cursor: The command chunks before the cursor.
|
||||
@ -107,8 +80,8 @@ class Completer(QObject):
|
||||
log.completion.debug("After removing flags: {}".format(before_cursor))
|
||||
if not before_cursor:
|
||||
# '|' or 'set|'
|
||||
model = instances.get(usertypes.Completion.command)
|
||||
return sortfilter.CompletionFilterModel(source=model, parent=self)
|
||||
log.completion.debug('Starting command completion')
|
||||
return miscmodels.command
|
||||
try:
|
||||
cmd = cmdutils.cmd_dict[before_cursor[0]]
|
||||
except KeyError:
|
||||
@ -117,14 +90,11 @@ class Completer(QObject):
|
||||
return None
|
||||
argpos = len(before_cursor) - 1
|
||||
try:
|
||||
completion = cmd.get_pos_arg_info(argpos).completion
|
||||
func = cmd.get_pos_arg_info(argpos).completion
|
||||
except IndexError:
|
||||
log.completion.debug("No completion in position {}".format(argpos))
|
||||
return None
|
||||
if completion is None:
|
||||
return None
|
||||
model = self._get_completion_model(completion, before_cursor[1:])
|
||||
return model
|
||||
return func
|
||||
|
||||
def _quote(self, s):
|
||||
"""Quote s if it needs quoting for the commandline.
|
||||
@ -239,6 +209,7 @@ class Completer(QObject):
|
||||
# FIXME complete searches
|
||||
# https://github.com/qutebrowser/qutebrowser/issues/32
|
||||
completion.set_model(None)
|
||||
self._last_completion_func = None
|
||||
return
|
||||
|
||||
before_cursor, pattern, after_cursor = self._partition()
|
||||
@ -247,13 +218,24 @@ class Completer(QObject):
|
||||
before_cursor, pattern, after_cursor))
|
||||
|
||||
pattern = pattern.strip("'\"")
|
||||
model = self._get_new_completion(before_cursor, pattern)
|
||||
func = self._get_new_completion(before_cursor, pattern)
|
||||
|
||||
log.completion.debug("Setting completion model to {} with pattern '{}'"
|
||||
.format(model.srcmodel.__class__.__name__ if model else 'None',
|
||||
pattern))
|
||||
if func is None:
|
||||
log.completion.debug('Clearing completion')
|
||||
completion.set_model(None)
|
||||
self._last_completion_func = None
|
||||
return
|
||||
|
||||
completion.set_model(model, pattern)
|
||||
if func != self._last_completion_func:
|
||||
self._last_completion_func = func
|
||||
args = (x for x in before_cursor[1:] if not x.startswith('-'))
|
||||
with debug.log_time(log.completion,
|
||||
'Starting {} completion'.format(func.__name__)):
|
||||
model = func(*args)
|
||||
with debug.log_time(log.completion, 'Set completion model'):
|
||||
completion.set_model(model)
|
||||
|
||||
completion.set_pattern(pattern)
|
||||
|
||||
def _change_completed_part(self, newtext, before, after, immediate=False):
|
||||
"""Change the part we're currently completing in the commandline.
|
||||
|
@ -198,8 +198,9 @@ class CompletionItemDelegate(QStyledItemDelegate):
|
||||
self._doc.setDefaultStyleSheet(template.render(conf=config.val))
|
||||
|
||||
if index.parent().isValid():
|
||||
pattern = index.model().pattern
|
||||
columns_to_filter = index.model().srcmodel.columns_to_filter
|
||||
view = self.parent()
|
||||
pattern = view.pattern
|
||||
columns_to_filter = index.model().columns_to_filter(index)
|
||||
if index.column() in columns_to_filter and pattern:
|
||||
repl = r'<span class="highlight">\g<0></span>'
|
||||
text = re.sub(re.escape(pattern).replace(r'\ ', r'|'),
|
||||
|
@ -28,8 +28,7 @@ from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QItemSelectionModel, QSize
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.completion import completiondelegate
|
||||
from qutebrowser.completion.models import base
|
||||
from qutebrowser.utils import utils, usertypes
|
||||
from qutebrowser.utils import utils, usertypes, debug, log
|
||||
from qutebrowser.commands import cmdexc, cmdutils
|
||||
|
||||
|
||||
@ -41,6 +40,7 @@ class CompletionView(QTreeView):
|
||||
headers, and children show as flat list.
|
||||
|
||||
Attributes:
|
||||
pattern: Current filter pattern, used for highlighting.
|
||||
_win_id: The ID of the window this CompletionView is associated with.
|
||||
_height: The height to use for the CompletionView.
|
||||
_height_perc: Either None or a percentage if height should be relative.
|
||||
@ -107,12 +107,10 @@ class CompletionView(QTreeView):
|
||||
|
||||
def __init__(self, win_id, parent=None):
|
||||
super().__init__(parent)
|
||||
self.pattern = ''
|
||||
self._win_id = win_id
|
||||
# FIXME handle new aliases.
|
||||
# config.instance.changed.connect(self.init_command_completion)
|
||||
config.instance.changed.connect(self._on_config_changed)
|
||||
|
||||
self._column_widths = base.BaseCompletionModel.COLUMN_WIDTHS
|
||||
self._active = False
|
||||
|
||||
self._delegate = completiondelegate.CompletionItemDelegate(self)
|
||||
@ -148,8 +146,11 @@ class CompletionView(QTreeView):
|
||||
|
||||
def _resize_columns(self):
|
||||
"""Resize the completion columns based on column_widths."""
|
||||
if self.model() is None:
|
||||
return
|
||||
width = self.size().width()
|
||||
pixel_widths = [(width * perc // 100) for perc in self._column_widths]
|
||||
column_widths = self.model().column_widths
|
||||
pixel_widths = [(width * perc // 100) for perc in column_widths]
|
||||
|
||||
if self.verticalScrollBar().isVisible():
|
||||
delta = self.style().pixelMetric(QStyle.PM_ScrollBarExtent) + 5
|
||||
@ -252,6 +253,10 @@ class CompletionView(QTreeView):
|
||||
selmodel.setCurrentIndex(
|
||||
idx, QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows)
|
||||
|
||||
# if the last item is focused, try to fetch more
|
||||
if idx.row() == self.model().rowCount(idx.parent()) - 1:
|
||||
self.expandAll()
|
||||
|
||||
count = self.model().count()
|
||||
if count == 0:
|
||||
self.hide()
|
||||
@ -260,47 +265,50 @@ class CompletionView(QTreeView):
|
||||
elif config.val.completion.show == 'auto':
|
||||
self.show()
|
||||
|
||||
def set_model(self, model, pattern=None):
|
||||
def set_model(self, model):
|
||||
"""Switch completion to a new model.
|
||||
|
||||
Called from on_update_completion().
|
||||
|
||||
Args:
|
||||
model: The model to use.
|
||||
pattern: The filter pattern to set (what the user entered).
|
||||
"""
|
||||
if self.model() is not None and model is not self.model():
|
||||
self.model().deleteLater()
|
||||
self.selectionModel().deleteLater()
|
||||
|
||||
self.setModel(model)
|
||||
|
||||
if model is None:
|
||||
self._active = False
|
||||
self.hide()
|
||||
return
|
||||
|
||||
old_model = self.model()
|
||||
if model is not old_model:
|
||||
sel_model = self.selectionModel()
|
||||
|
||||
self.setModel(model)
|
||||
self._active = True
|
||||
|
||||
if sel_model is not None:
|
||||
sel_model.deleteLater()
|
||||
if old_model is not None:
|
||||
old_model.deleteLater()
|
||||
|
||||
if (config.val.completion.show == 'always' and
|
||||
model.count() > 0):
|
||||
self.show()
|
||||
else:
|
||||
self.hide()
|
||||
model.setParent(self)
|
||||
self._active = True
|
||||
self._maybe_show()
|
||||
|
||||
self._resize_columns()
|
||||
for i in range(model.rowCount()):
|
||||
self.expand(model.index(i, 0))
|
||||
|
||||
if pattern is not None:
|
||||
model.set_pattern(pattern)
|
||||
def set_pattern(self, pattern):
|
||||
"""Set the pattern on the underlying model."""
|
||||
if not self.model():
|
||||
return
|
||||
self.pattern = pattern
|
||||
with debug.log_time(log.completion, 'Set pattern {}'.format(pattern)):
|
||||
self.model().set_pattern(pattern)
|
||||
self.selectionModel().clear()
|
||||
self._maybe_update_geometry()
|
||||
self._maybe_show()
|
||||
|
||||
self._column_widths = model.srcmodel.COLUMN_WIDTHS
|
||||
self._resize_columns()
|
||||
self._maybe_update_geometry()
|
||||
def _maybe_show(self):
|
||||
if (config.val.completion.show == 'always' and
|
||||
self.model().count() > 0):
|
||||
self.show()
|
||||
else:
|
||||
self.hide()
|
||||
|
||||
def _maybe_update_geometry(self):
|
||||
"""Emit the update_geometry signal if the config says so."""
|
||||
@ -345,7 +353,7 @@ class CompletionView(QTreeView):
|
||||
indexes = selected.indexes()
|
||||
if not indexes:
|
||||
return
|
||||
data = self.model().data(indexes[0])
|
||||
data = str(self.model().data(indexes[0]))
|
||||
self.selection_changed.emit(data)
|
||||
|
||||
def resizeEvent(self, e):
|
||||
@ -365,9 +373,7 @@ class CompletionView(QTreeView):
|
||||
modes=[usertypes.KeyMode.command], scope='window')
|
||||
def completion_item_del(self):
|
||||
"""Delete the current completion item."""
|
||||
if not self.currentIndex().isValid():
|
||||
index = self.currentIndex()
|
||||
if not index.isValid():
|
||||
raise cmdexc.CommandError("No item selected!")
|
||||
try:
|
||||
self.model().srcmodel.delete_cur_item(self)
|
||||
except NotImplementedError:
|
||||
raise cmdexc.CommandError("Cannot delete this item.")
|
||||
self.model().delete_cur_item(index)
|
||||
|
@ -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
|
232
qutebrowser/completion/models/completionmodel.py
Normal file
232
qutebrowser/completion/models/completionmodel.py
Normal 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()
|
@ -17,145 +17,80 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""CompletionModels for the config."""
|
||||
"""Functions that return config-related completion models."""
|
||||
|
||||
# FIXME:conf
|
||||
# pylint: disable=no-member
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot, Qt
|
||||
|
||||
from qutebrowser.config import config, configdata
|
||||
from qutebrowser.utils import log, qtutils
|
||||
from qutebrowser.completion.models import base
|
||||
from qutebrowser.config import configdata, configexc
|
||||
from qutebrowser.completion.models import completionmodel, listcategory
|
||||
from qutebrowser.utils import objreg
|
||||
|
||||
|
||||
class SettingSectionCompletionModel(base.BaseCompletionModel):
|
||||
|
||||
def section():
|
||||
"""A CompletionModel filled with settings sections."""
|
||||
|
||||
# https://github.com/qutebrowser/qutebrowser/issues/545
|
||||
# pylint: disable=abstract-method
|
||||
|
||||
COLUMN_WIDTHS = (20, 70, 10)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
cat = self.new_category("Sections")
|
||||
for name in configdata.DATA:
|
||||
desc = configdata.SECTION_DESC[name].splitlines()[0].strip()
|
||||
self.new_item(cat, name, desc)
|
||||
model = completionmodel.CompletionModel(column_widths=(20, 70, 10))
|
||||
sections = ((name, configdata.SECTION_DESC[name].splitlines()[0].strip())
|
||||
for name in configdata.DATA)
|
||||
model.add_category(listcategory.ListCategory("Sections", sections))
|
||||
return model
|
||||
|
||||
|
||||
class SettingOptionCompletionModel(base.BaseCompletionModel):
|
||||
|
||||
def option(sectname):
|
||||
"""A CompletionModel filled with settings and their descriptions.
|
||||
|
||||
Attributes:
|
||||
_misc_items: A dict of the misc. column items which will be set later.
|
||||
_section: The config section this model shows.
|
||||
Args:
|
||||
sectname: The name of the config section this model shows.
|
||||
"""
|
||||
|
||||
# https://github.com/qutebrowser/qutebrowser/issues/545
|
||||
# pylint: disable=abstract-method
|
||||
|
||||
COLUMN_WIDTHS = (20, 70, 10)
|
||||
|
||||
def __init__(self, section, parent=None):
|
||||
super().__init__(parent)
|
||||
cat = self.new_category(section)
|
||||
sectdata = configdata.DATA[section]
|
||||
self._misc_items = {}
|
||||
self._section = section
|
||||
config.instance.changed.connect(self._update_misc_column)
|
||||
for name in sectdata:
|
||||
try:
|
||||
desc = sectdata.descriptions[name]
|
||||
except (KeyError, AttributeError):
|
||||
# Some stuff (especially ValueList items) don't have a
|
||||
# description.
|
||||
desc = ""
|
||||
else:
|
||||
desc = desc.splitlines()[0]
|
||||
value = config.get(section, name, raw=True)
|
||||
_valitem, _descitem, miscitem = self.new_item(cat, name, desc,
|
||||
value)
|
||||
self._misc_items[name] = miscitem
|
||||
|
||||
@pyqtSlot(str, str)
|
||||
def _update_misc_column(self, section, option):
|
||||
"""Update misc column when config changed."""
|
||||
if section != self._section:
|
||||
return
|
||||
model = completionmodel.CompletionModel(column_widths=(20, 70, 10))
|
||||
try:
|
||||
sectdata = configdata.DATA[sectname]
|
||||
except KeyError:
|
||||
return None
|
||||
options = []
|
||||
for name in sectdata:
|
||||
try:
|
||||
item = self._misc_items[option]
|
||||
except KeyError:
|
||||
log.completion.debug("Couldn't get item {}.{} from model!".format(
|
||||
section, option))
|
||||
# changed before init
|
||||
return
|
||||
val = config.get(section, option, raw=True)
|
||||
idx = item.index()
|
||||
qtutils.ensure_valid(idx)
|
||||
ok = self.setData(idx, val, Qt.DisplayRole)
|
||||
if not ok:
|
||||
raise ValueError("Setting data failed! (section: {}, option: {}, "
|
||||
"value: {})".format(section, option, val))
|
||||
desc = sectdata.descriptions[name]
|
||||
except (KeyError, AttributeError):
|
||||
# Some stuff (especially ValueList items) don't have a
|
||||
# description.
|
||||
desc = ""
|
||||
else:
|
||||
desc = desc.splitlines()[0]
|
||||
config = objreg.get('config')
|
||||
val = config.get(sectname, name, raw=True)
|
||||
options.append((name, desc, val))
|
||||
model.add_category(listcategory.ListCategory(sectname, options))
|
||||
return model
|
||||
|
||||
|
||||
class SettingValueCompletionModel(base.BaseCompletionModel):
|
||||
|
||||
def value(sectname, optname):
|
||||
"""A CompletionModel filled with setting values.
|
||||
|
||||
Attributes:
|
||||
_section: The config section this model shows.
|
||||
_option: The config option this model shows.
|
||||
Args:
|
||||
sectname: The name of the config section this model shows.
|
||||
optname: The name of the config option this model shows.
|
||||
"""
|
||||
model = completionmodel.CompletionModel(column_widths=(20, 70, 10))
|
||||
config = objreg.get('config')
|
||||
|
||||
# https://github.com/qutebrowser/qutebrowser/issues/545
|
||||
# pylint: disable=abstract-method
|
||||
try:
|
||||
current = config.get(sectname, optname, raw=True) or '""'
|
||||
except (configexc.NoSectionError, configexc.NoOptionError):
|
||||
return None
|
||||
|
||||
COLUMN_WIDTHS = (20, 70, 10)
|
||||
default = configdata.DATA[sectname][optname].default() or '""'
|
||||
|
||||
def __init__(self, section, option, parent=None):
|
||||
super().__init__(parent)
|
||||
self._section = section
|
||||
self._option = option
|
||||
config.instance.changed.connect(self._update_current_value)
|
||||
cur_cat = self.new_category("Current/Default", sort=0)
|
||||
value = config.get(section, option, raw=True)
|
||||
if not value:
|
||||
value = '""'
|
||||
self.cur_item, _descitem, _miscitem = self.new_item(cur_cat, value,
|
||||
"Current value")
|
||||
default_value = configdata.DATA[section][option].default()
|
||||
if not default_value:
|
||||
default_value = '""'
|
||||
self.new_item(cur_cat, default_value, "Default value")
|
||||
if hasattr(configdata.DATA[section], 'valtype'):
|
||||
# Same type for all values (ValueList)
|
||||
vals = configdata.DATA[section].valtype.complete()
|
||||
else:
|
||||
if option is None:
|
||||
raise ValueError("option may only be None for ValueList "
|
||||
"sections, but {} is not!".format(section))
|
||||
# Different type for each value (KeyValue)
|
||||
vals = configdata.DATA[section][option].typ.complete()
|
||||
if vals is not None:
|
||||
cat = self.new_category("Completions", sort=1)
|
||||
for (val, desc) in vals:
|
||||
self.new_item(cat, val, desc)
|
||||
if hasattr(configdata.DATA[sectname], 'valtype'):
|
||||
# Same type for all values (ValueList)
|
||||
vals = configdata.DATA[sectname].valtype.complete()
|
||||
else:
|
||||
if optname is None:
|
||||
raise ValueError("optname may only be None for ValueList "
|
||||
"sections, but {} is not!".format(sectname))
|
||||
# Different type for each value (KeyValue)
|
||||
vals = configdata.DATA[sectname][optname].typ.complete()
|
||||
|
||||
@pyqtSlot(str, str)
|
||||
def _update_current_value(self, section, option):
|
||||
"""Update current value when config changed."""
|
||||
if (section, option) != (self._section, self._option):
|
||||
return
|
||||
value = config.get(section, option, raw=True)
|
||||
if not value:
|
||||
value = '""'
|
||||
idx = self.cur_item.index()
|
||||
qtutils.ensure_valid(idx)
|
||||
ok = self.setData(idx, value, Qt.DisplayRole)
|
||||
if not ok:
|
||||
raise ValueError("Setting data failed! (section: {}, option: {}, "
|
||||
"value: {})".format(section, option, value))
|
||||
cur_cat = listcategory.ListCategory("Current/Default",
|
||||
[(current, "Current value"), (default, "Default value")])
|
||||
model.add_category(cur_cat)
|
||||
if vals is not None:
|
||||
model.add_category(listcategory.ListCategory("Completions", vals))
|
||||
return model
|
||||
|
104
qutebrowser/completion/models/histcategory.py
Normal file
104
qutebrowser/completion/models/histcategory.py
Normal 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
|
@ -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]))
|
92
qutebrowser/completion/models/listcategory.py
Normal file
92
qutebrowser/completion/models/listcategory.py
Normal 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
|
@ -17,255 +17,142 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Misc. CompletionModels."""
|
||||
"""Functions that return miscellaneous completion models."""
|
||||
|
||||
# FIXME:conf
|
||||
# pylint: disable=unused-argument
|
||||
|
||||
from PyQt5.QtCore import Qt, QTimer, pyqtSlot
|
||||
|
||||
from qutebrowser.browser import browsertab
|
||||
from qutebrowser.config import config, configdata
|
||||
from qutebrowser.utils import objreg, log, qtutils
|
||||
from qutebrowser.utils import objreg, log
|
||||
from qutebrowser.commands import cmdutils
|
||||
from qutebrowser.completion.models import base
|
||||
from qutebrowser.completion.models import completionmodel, listcategory
|
||||
|
||||
|
||||
class CommandCompletionModel(base.BaseCompletionModel):
|
||||
|
||||
def command():
|
||||
"""A CompletionModel filled with non-hidden commands and descriptions."""
|
||||
|
||||
# https://github.com/qutebrowser/qutebrowser/issues/545
|
||||
# pylint: disable=abstract-method
|
||||
|
||||
COLUMN_WIDTHS = (20, 60, 20)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
cmdlist = _get_cmd_completions(include_aliases=True,
|
||||
include_hidden=False)
|
||||
cat = self.new_category("Commands")
|
||||
for (name, desc, misc) in cmdlist:
|
||||
self.new_item(cat, name, desc, misc)
|
||||
model = completionmodel.CompletionModel(column_widths=(20, 60, 20))
|
||||
cmdlist = _get_cmd_completions(include_aliases=True, include_hidden=False)
|
||||
model.add_category(listcategory.ListCategory("Commands", cmdlist))
|
||||
return model
|
||||
|
||||
|
||||
class HelpCompletionModel(base.BaseCompletionModel):
|
||||
|
||||
def helptopic():
|
||||
"""A CompletionModel filled with help topics."""
|
||||
model = completionmodel.CompletionModel()
|
||||
|
||||
# https://github.com/qutebrowser/qutebrowser/issues/545
|
||||
# pylint: disable=abstract-method
|
||||
cmdlist = _get_cmd_completions(include_aliases=False, include_hidden=True,
|
||||
prefix=':')
|
||||
settings = []
|
||||
for sectname, sectdata in configdata.DATA.items():
|
||||
for optname in sectdata:
|
||||
try:
|
||||
desc = sectdata.descriptions[optname]
|
||||
except (KeyError, AttributeError):
|
||||
# Some stuff (especially ValueList items) don't have a
|
||||
# description.
|
||||
desc = ""
|
||||
else:
|
||||
desc = desc.splitlines()[0]
|
||||
name = '{}->{}'.format(sectname, optname)
|
||||
settings.append((name, desc))
|
||||
|
||||
COLUMN_WIDTHS = (20, 60, 20)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._init_commands()
|
||||
self._init_settings()
|
||||
|
||||
def _init_commands(self):
|
||||
"""Fill completion with :command entries."""
|
||||
cmdlist = _get_cmd_completions(include_aliases=False,
|
||||
include_hidden=True, prefix=':')
|
||||
cat = self.new_category("Commands")
|
||||
for (name, desc, misc) in cmdlist:
|
||||
self.new_item(cat, name, desc, misc)
|
||||
|
||||
def _init_settings(self):
|
||||
"""Fill completion with section->option entries."""
|
||||
cat = self.new_category("Settings")
|
||||
for sectname, sectdata in configdata.DATA.items():
|
||||
for optname in sectdata:
|
||||
try:
|
||||
desc = sectdata.descriptions[optname]
|
||||
except (KeyError, AttributeError):
|
||||
# Some stuff (especially ValueList items) don't have a
|
||||
# description.
|
||||
desc = ""
|
||||
else:
|
||||
desc = desc.splitlines()[0]
|
||||
name = '{}->{}'.format(sectname, optname)
|
||||
self.new_item(cat, name, desc)
|
||||
model.add_category(listcategory.ListCategory("Commands", cmdlist))
|
||||
model.add_category(listcategory.ListCategory("Settings", settings))
|
||||
return model
|
||||
|
||||
|
||||
class QuickmarkCompletionModel(base.BaseCompletionModel):
|
||||
|
||||
def quickmark():
|
||||
"""A CompletionModel filled with all quickmarks."""
|
||||
def delete(data):
|
||||
"""Delete a quickmark from the completion menu."""
|
||||
name = data[0]
|
||||
quickmark_manager = objreg.get('quickmark-manager')
|
||||
log.completion.debug('Deleting quickmark {}'.format(name))
|
||||
quickmark_manager.delete(name)
|
||||
|
||||
# https://github.com/qutebrowser/qutebrowser/issues/545
|
||||
# pylint: disable=abstract-method
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
cat = self.new_category("Quickmarks")
|
||||
quickmarks = objreg.get('quickmark-manager').marks.items()
|
||||
for qm_name, qm_url in quickmarks:
|
||||
self.new_item(cat, qm_name, qm_url)
|
||||
model = completionmodel.CompletionModel(column_widths=(30, 70, 0))
|
||||
marks = objreg.get('quickmark-manager').marks.items()
|
||||
model.add_category(listcategory.ListCategory('Quickmarks', marks,
|
||||
delete_func=delete))
|
||||
return model
|
||||
|
||||
|
||||
class BookmarkCompletionModel(base.BaseCompletionModel):
|
||||
|
||||
def bookmark():
|
||||
"""A CompletionModel filled with all bookmarks."""
|
||||
def delete(data):
|
||||
"""Delete a bookmark from the completion menu."""
|
||||
urlstr = data[0]
|
||||
log.completion.debug('Deleting bookmark {}'.format(urlstr))
|
||||
bookmark_manager = objreg.get('bookmark-manager')
|
||||
bookmark_manager.delete(urlstr)
|
||||
|
||||
# https://github.com/qutebrowser/qutebrowser/issues/545
|
||||
# pylint: disable=abstract-method
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
cat = self.new_category("Bookmarks")
|
||||
bookmarks = objreg.get('bookmark-manager').marks.items()
|
||||
for bm_url, bm_title in bookmarks:
|
||||
self.new_item(cat, bm_url, bm_title)
|
||||
model = completionmodel.CompletionModel(column_widths=(30, 70, 0))
|
||||
marks = objreg.get('bookmark-manager').marks.items()
|
||||
model.add_category(listcategory.ListCategory('Bookmarks', marks,
|
||||
delete_func=delete))
|
||||
return model
|
||||
|
||||
|
||||
class SessionCompletionModel(base.BaseCompletionModel):
|
||||
|
||||
def session():
|
||||
"""A CompletionModel filled with session names."""
|
||||
|
||||
# https://github.com/qutebrowser/qutebrowser/issues/545
|
||||
# pylint: disable=abstract-method
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
cat = self.new_category("Sessions")
|
||||
try:
|
||||
for name in objreg.get('session-manager').list_sessions():
|
||||
if not name.startswith('_'):
|
||||
self.new_item(cat, name)
|
||||
except OSError:
|
||||
log.completion.exception("Failed to list sessions!")
|
||||
model = completionmodel.CompletionModel()
|
||||
try:
|
||||
manager = objreg.get('session-manager')
|
||||
sessions = ((name,) for name in manager.list_sessions()
|
||||
if not name.startswith('_'))
|
||||
model.add_category(listcategory.ListCategory("Sessions", sessions))
|
||||
except OSError:
|
||||
log.completion.exception("Failed to list sessions!")
|
||||
return model
|
||||
|
||||
|
||||
class TabCompletionModel(base.BaseCompletionModel):
|
||||
|
||||
def buffer():
|
||||
"""A model to complete on open tabs across all windows.
|
||||
|
||||
Used for switching the buffer command.
|
||||
"""
|
||||
|
||||
IDX_COLUMN = 0
|
||||
URL_COLUMN = 1
|
||||
TEXT_COLUMN = 2
|
||||
|
||||
COLUMN_WIDTHS = (6, 40, 54)
|
||||
DUMB_SORT = Qt.DescendingOrder
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.columns_to_filter = [self.IDX_COLUMN, self.URL_COLUMN,
|
||||
self.TEXT_COLUMN]
|
||||
|
||||
for win_id in objreg.window_registry:
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=win_id)
|
||||
for i in range(tabbed_browser.count()):
|
||||
tab = tabbed_browser.widget(i)
|
||||
tab.url_changed.connect(self.rebuild)
|
||||
tab.title_changed.connect(self.rebuild)
|
||||
tab.shutting_down.connect(self.delayed_rebuild)
|
||||
tabbed_browser.new_tab.connect(self.on_new_tab)
|
||||
tabbed_browser.tabBar().tabMoved.connect(self.rebuild)
|
||||
objreg.get("app").new_window.connect(self.on_new_window)
|
||||
self.rebuild()
|
||||
|
||||
def on_new_window(self, window):
|
||||
"""Add hooks to new windows."""
|
||||
window.tabbed_browser.new_tab.connect(self.on_new_tab)
|
||||
|
||||
@pyqtSlot(browsertab.AbstractTab)
|
||||
def on_new_tab(self, tab):
|
||||
"""Add hooks to new tabs."""
|
||||
tab.url_changed.connect(self.rebuild)
|
||||
tab.title_changed.connect(self.rebuild)
|
||||
tab.shutting_down.connect(self.delayed_rebuild)
|
||||
self.rebuild()
|
||||
|
||||
@pyqtSlot()
|
||||
def delayed_rebuild(self):
|
||||
"""Fire a rebuild indirectly so widgets get a chance to update."""
|
||||
QTimer.singleShot(0, self.rebuild)
|
||||
|
||||
@pyqtSlot()
|
||||
def rebuild(self):
|
||||
"""Rebuild completion model from current tabs.
|
||||
|
||||
Very lazy method of keeping the model up to date. We could connect to
|
||||
signals for new tab, tab url/title changed, tab close, tab moved and
|
||||
make sure we handled background loads too ... but iterating over a
|
||||
few/few dozen/few hundred tabs doesn't take very long at all.
|
||||
"""
|
||||
window_count = 0
|
||||
for win_id in objreg.window_registry:
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=win_id)
|
||||
if not tabbed_browser.shutting_down:
|
||||
window_count += 1
|
||||
|
||||
if window_count < self.rowCount():
|
||||
self.removeRows(window_count, self.rowCount() - window_count)
|
||||
|
||||
for i, win_id in enumerate(objreg.window_registry):
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=win_id)
|
||||
if tabbed_browser.shutting_down:
|
||||
continue
|
||||
if i >= self.rowCount():
|
||||
c = self.new_category("{}".format(win_id))
|
||||
else:
|
||||
c = self.item(i, 0)
|
||||
c.setData("{}".format(win_id), Qt.DisplayRole)
|
||||
if tabbed_browser.count() < c.rowCount():
|
||||
c.removeRows(tabbed_browser.count(),
|
||||
c.rowCount() - tabbed_browser.count())
|
||||
for idx in range(tabbed_browser.count()):
|
||||
tab = tabbed_browser.widget(idx)
|
||||
if idx >= c.rowCount():
|
||||
self.new_item(c, "{}/{}".format(win_id, idx + 1),
|
||||
tab.url().toDisplayString(),
|
||||
tabbed_browser.page_title(idx))
|
||||
else:
|
||||
c.child(idx, 0).setData("{}/{}".format(win_id, idx + 1),
|
||||
Qt.DisplayRole)
|
||||
c.child(idx, 1).setData(tab.url().toDisplayString(),
|
||||
Qt.DisplayRole)
|
||||
c.child(idx, 2).setData(tabbed_browser.page_title(idx),
|
||||
Qt.DisplayRole)
|
||||
|
||||
def delete_cur_item(self, completion):
|
||||
"""Delete the selected item.
|
||||
|
||||
Args:
|
||||
completion: The Completion object to use.
|
||||
"""
|
||||
index = completion.currentIndex()
|
||||
qtutils.ensure_valid(index)
|
||||
category = index.parent()
|
||||
qtutils.ensure_valid(category)
|
||||
index = category.child(index.row(), self.IDX_COLUMN)
|
||||
win_id, tab_index = index.data().split('/')
|
||||
|
||||
def delete_buffer(data):
|
||||
"""Close the selected tab."""
|
||||
win_id, tab_index = data[0].split('/')
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=int(win_id))
|
||||
tabbed_browser.on_tab_close_requested(int(tab_index) - 1)
|
||||
|
||||
model = completionmodel.CompletionModel(column_widths=(6, 40, 54))
|
||||
|
||||
class BindCompletionModel(base.BaseCompletionModel):
|
||||
for win_id in objreg.window_registry:
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=win_id)
|
||||
if tabbed_browser.shutting_down:
|
||||
continue
|
||||
tabs = []
|
||||
for idx in range(tabbed_browser.count()):
|
||||
tab = tabbed_browser.widget(idx)
|
||||
tabs.append(("{}/{}".format(win_id, idx + 1),
|
||||
tab.url().toDisplayString(),
|
||||
tabbed_browser.page_title(idx)))
|
||||
cat = listcategory.ListCategory("{}".format(win_id), tabs,
|
||||
delete_func=delete_buffer)
|
||||
model.add_category(cat)
|
||||
|
||||
"""A CompletionModel filled with all bindable commands and descriptions."""
|
||||
return model
|
||||
|
||||
# https://github.com/qutebrowser/qutebrowser/issues/545
|
||||
# pylint: disable=abstract-method
|
||||
|
||||
COLUMN_WIDTHS = (20, 60, 20)
|
||||
def bind(key):
|
||||
"""A CompletionModel filled with all bindable commands and descriptions.
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
cmdlist = _get_cmd_completions(include_hidden=True,
|
||||
include_aliases=True)
|
||||
cat = self.new_category("Commands")
|
||||
for (name, desc, misc) in cmdlist:
|
||||
self.new_item(cat, name, desc, misc)
|
||||
Args:
|
||||
key: the key being bound.
|
||||
"""
|
||||
model = completionmodel.CompletionModel(column_widths=(20, 60, 20))
|
||||
cmd_text = objreg.get('key-config').get_bindings_for('normal').get(key)
|
||||
|
||||
if cmd_text:
|
||||
cmd_name = cmd_text.split(' ')[0]
|
||||
cmd = cmdutils.cmd_dict.get(cmd_name)
|
||||
data = [(cmd_text, cmd.desc, key)]
|
||||
model.add_category(listcategory.ListCategory("Current", data))
|
||||
|
||||
cmdlist = _get_cmd_completions(include_hidden=True, include_aliases=True)
|
||||
model.add_category(listcategory.ListCategory("Commands", cmdlist))
|
||||
return model
|
||||
|
||||
|
||||
def _get_cmd_completions(include_hidden, include_aliases, prefix=''):
|
||||
|
@ -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)
|
@ -17,176 +17,56 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""CompletionModels for URLs."""
|
||||
"""Function to return the url completion model for the `open` command."""
|
||||
|
||||
import datetime
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot, Qt
|
||||
|
||||
from qutebrowser.utils import objreg, utils, qtutils, log
|
||||
from qutebrowser.completion.models import base
|
||||
from qutebrowser.completion.models import (completionmodel, listcategory,
|
||||
histcategory)
|
||||
from qutebrowser.utils import log, objreg
|
||||
from qutebrowser.config import config
|
||||
|
||||
|
||||
class UrlCompletionModel(base.BaseCompletionModel):
|
||||
_URLCOL = 0
|
||||
_TEXTCOL = 1
|
||||
|
||||
|
||||
def _delete_history(data):
|
||||
urlstr = data[_URLCOL]
|
||||
log.completion.debug('Deleting history entry {}'.format(urlstr))
|
||||
hist = objreg.get('web-history')
|
||||
hist.delete_url(urlstr)
|
||||
|
||||
|
||||
def _delete_bookmark(data):
|
||||
urlstr = data[_URLCOL]
|
||||
log.completion.debug('Deleting bookmark {}'.format(urlstr))
|
||||
bookmark_manager = objreg.get('bookmark-manager')
|
||||
bookmark_manager.delete(urlstr)
|
||||
|
||||
|
||||
def _delete_quickmark(data):
|
||||
name = data[_TEXTCOL]
|
||||
quickmark_manager = objreg.get('quickmark-manager')
|
||||
log.completion.debug('Deleting quickmark {}'.format(name))
|
||||
quickmark_manager.delete(name)
|
||||
|
||||
|
||||
def url():
|
||||
"""A model which combines bookmarks, quickmarks and web history URLs.
|
||||
|
||||
Used for the `open` command.
|
||||
"""
|
||||
model = completionmodel.CompletionModel(column_widths=(40, 50, 10))
|
||||
|
||||
URL_COLUMN = 0
|
||||
TEXT_COLUMN = 1
|
||||
TIME_COLUMN = 2
|
||||
quickmarks = ((url, name) for (name, url)
|
||||
in objreg.get('quickmark-manager').marks.items())
|
||||
bookmarks = objreg.get('bookmark-manager').marks.items()
|
||||
|
||||
COLUMN_WIDTHS = (40, 50, 10)
|
||||
DUMB_SORT = Qt.DescendingOrder
|
||||
model.add_category(listcategory.ListCategory(
|
||||
'Quickmarks', quickmarks, delete_func=_delete_quickmark))
|
||||
model.add_category(listcategory.ListCategory(
|
||||
'Bookmarks', bookmarks, delete_func=_delete_bookmark))
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.columns_to_filter = [self.URL_COLUMN, self.TEXT_COLUMN]
|
||||
|
||||
self._quickmark_cat = self.new_category("Quickmarks")
|
||||
self._bookmark_cat = self.new_category("Bookmarks")
|
||||
self._history_cat = self.new_category("History")
|
||||
|
||||
quickmark_manager = objreg.get('quickmark-manager')
|
||||
quickmarks = quickmark_manager.marks.items()
|
||||
for qm_name, qm_url in quickmarks:
|
||||
self.new_item(self._quickmark_cat, qm_url, qm_name)
|
||||
quickmark_manager.added.connect(
|
||||
lambda name, url: self.new_item(self._quickmark_cat, url, name))
|
||||
quickmark_manager.removed.connect(self.on_quickmark_removed)
|
||||
|
||||
bookmark_manager = objreg.get('bookmark-manager')
|
||||
bookmarks = bookmark_manager.marks.items()
|
||||
for bm_url, bm_title in bookmarks:
|
||||
self.new_item(self._bookmark_cat, bm_url, bm_title)
|
||||
bookmark_manager.added.connect(
|
||||
lambda name, url: self.new_item(self._bookmark_cat, url, name))
|
||||
bookmark_manager.removed.connect(self.on_bookmark_removed)
|
||||
|
||||
self._history = objreg.get('web-history')
|
||||
self._max_history = config.val.completion.web_history_max_items
|
||||
history = utils.newest_slice(self._history, self._max_history)
|
||||
for entry in history:
|
||||
if not entry.redirect:
|
||||
self._add_history_entry(entry)
|
||||
self._history.add_completion_item.connect(self.on_history_item_added)
|
||||
self._history.cleared.connect(self.on_history_cleared)
|
||||
|
||||
config.instance.changed.connect(self._reformat_timestamps)
|
||||
|
||||
def _fmt_atime(self, atime):
|
||||
"""Format an atime to a human-readable string."""
|
||||
fmt = config.val.completion.timestamp_format
|
||||
if fmt is None:
|
||||
return ''
|
||||
try:
|
||||
dt = datetime.datetime.fromtimestamp(atime)
|
||||
except (ValueError, OSError, OverflowError):
|
||||
# Different errors which can occur for too large values...
|
||||
log.misc.error("Got invalid timestamp {}!".format(atime))
|
||||
return '(invalid)'
|
||||
else:
|
||||
return dt.strftime(fmt)
|
||||
|
||||
def _remove_oldest_history(self):
|
||||
"""Remove the oldest history entry."""
|
||||
self._history_cat.removeRow(0)
|
||||
|
||||
def _add_history_entry(self, entry):
|
||||
"""Add a new history entry to the completion."""
|
||||
self.new_item(self._history_cat, entry.url.toDisplayString(),
|
||||
entry.title,
|
||||
self._fmt_atime(entry.atime), sort=int(entry.atime),
|
||||
userdata=entry.url)
|
||||
|
||||
if (self._max_history != -1 and
|
||||
self._history_cat.rowCount() > self._max_history):
|
||||
self._remove_oldest_history()
|
||||
|
||||
@config.change_filter('completion.timestamp_format')
|
||||
def _reformat_timestamps(self):
|
||||
"""Reformat the timestamps if the config option was changed."""
|
||||
for i in range(self._history_cat.rowCount()):
|
||||
url_item = self._history_cat.child(i, self.URL_COLUMN)
|
||||
atime_item = self._history_cat.child(i, self.TIME_COLUMN)
|
||||
atime = url_item.data(base.Role.sort)
|
||||
atime_item.setText(self._fmt_atime(atime))
|
||||
|
||||
@pyqtSlot(object)
|
||||
def on_history_item_added(self, entry):
|
||||
"""Slot called when a new history item was added."""
|
||||
for i in range(self._history_cat.rowCount()):
|
||||
url_item = self._history_cat.child(i, self.URL_COLUMN)
|
||||
atime_item = self._history_cat.child(i, self.TIME_COLUMN)
|
||||
title_item = self._history_cat.child(i, self.TEXT_COLUMN)
|
||||
url = url_item.data(base.Role.userdata)
|
||||
if url == entry.url:
|
||||
atime_item.setText(self._fmt_atime(entry.atime))
|
||||
title_item.setText(entry.title)
|
||||
url_item.setData(int(entry.atime), base.Role.sort)
|
||||
break
|
||||
else:
|
||||
self._add_history_entry(entry)
|
||||
|
||||
@pyqtSlot()
|
||||
def on_history_cleared(self):
|
||||
self._history_cat.removeRows(0, self._history_cat.rowCount())
|
||||
|
||||
def _remove_item(self, data, category, column):
|
||||
"""Helper function for on_quickmark_removed and on_bookmark_removed.
|
||||
|
||||
Args:
|
||||
data: The item to search for.
|
||||
category: The category to search in.
|
||||
column: The column to use for matching.
|
||||
"""
|
||||
for i in range(category.rowCount()):
|
||||
item = category.child(i, column)
|
||||
if item.data(Qt.DisplayRole) == data:
|
||||
category.removeRow(i)
|
||||
break
|
||||
|
||||
@pyqtSlot(str)
|
||||
def on_quickmark_removed(self, name):
|
||||
"""Called when a quickmark has been removed by the user.
|
||||
|
||||
Args:
|
||||
name: The name of the quickmark which has been removed.
|
||||
"""
|
||||
self._remove_item(name, self._quickmark_cat, self.TEXT_COLUMN)
|
||||
|
||||
@pyqtSlot(str)
|
||||
def on_bookmark_removed(self, url):
|
||||
"""Called when a bookmark has been removed by the user.
|
||||
|
||||
Args:
|
||||
url: The url of the bookmark which has been removed.
|
||||
"""
|
||||
self._remove_item(url, self._bookmark_cat, self.URL_COLUMN)
|
||||
|
||||
def delete_cur_item(self, completion):
|
||||
"""Delete the selected item.
|
||||
|
||||
Args:
|
||||
completion: The Completion object to use.
|
||||
"""
|
||||
index = completion.currentIndex()
|
||||
qtutils.ensure_valid(index)
|
||||
category = index.parent()
|
||||
index = category.child(index.row(), self.URL_COLUMN)
|
||||
url = index.data()
|
||||
qtutils.ensure_valid(category)
|
||||
|
||||
if category.data() == 'Bookmarks':
|
||||
bookmark_manager = objreg.get('bookmark-manager')
|
||||
bookmark_manager.delete(url)
|
||||
elif category.data() == 'Quickmarks':
|
||||
quickmark_manager = objreg.get('quickmark-manager')
|
||||
sibling = index.sibling(index.row(), self.TEXT_COLUMN)
|
||||
qtutils.ensure_valid(sibling)
|
||||
name = sibling.data()
|
||||
quickmark_manager.delete(name)
|
||||
if config.val.completion.web_history_max_items != 0:
|
||||
hist_cat = histcategory.HistoryCategory(delete_func=_delete_history)
|
||||
model.add_category(hist_cat)
|
||||
return model
|
||||
|
@ -29,7 +29,7 @@ from qutebrowser.config import configdata, configexc, configtypes, configfiles
|
||||
from qutebrowser.utils import utils, objreg, message, log, usertypes
|
||||
from qutebrowser.misc import objects
|
||||
from qutebrowser.commands import cmdexc, cmdutils
|
||||
|
||||
from qutebrowser.completion.models import configmodel
|
||||
|
||||
# An easy way to access the config from other code via config.val.foo
|
||||
val = None
|
||||
@ -229,6 +229,7 @@ class ConfigCommands:
|
||||
self._keyconfig = keyconfig
|
||||
|
||||
@cmdutils.register(instance='config-commands', star_args_optional=True)
|
||||
@cmdutils.argument('option', completion=configmodel.option)
|
||||
@cmdutils.argument('win_id', win_id=True)
|
||||
def set(self, win_id, option=None, *values, temp=False, print_=False):
|
||||
"""Set an option.
|
||||
|
@ -70,6 +70,8 @@ the <span class="mono">qute://settings</span> page or caret browsing).</span>
|
||||
{{ install_webengine('qt5-qtwebengine') }}
|
||||
{% elif distribution.parsed == Distribution.opensuse %}
|
||||
{{ install_webengine('libqt5-qtwebengine') }}
|
||||
{% elif distribution.parsed == Distribution.gentoo %}
|
||||
{{ install_webengine('dev-qt/qtwebengine') }}
|
||||
{% else %}
|
||||
{{ unknown_system() }}
|
||||
{% endif %}
|
||||
|
@ -61,7 +61,7 @@ li {
|
||||
{{ super() }}
|
||||
function tryagain()
|
||||
{
|
||||
location.href = url;
|
||||
location.href = "{{ url }}";
|
||||
}
|
||||
{% endblock %}
|
||||
|
||||
|
@ -23,8 +23,12 @@ window.loadHistory = (function() {
|
||||
// Date of last seen item.
|
||||
var lastItemDate = null;
|
||||
|
||||
// The time to load next.
|
||||
// Each request for new items includes the time of the last item and an
|
||||
// offset. The offset is equal to the number of items from the previous
|
||||
// request that had time=nextTime, and causes the next request to skip
|
||||
// those items to avoid duplicates.
|
||||
var nextTime = null;
|
||||
var nextOffset = 0;
|
||||
|
||||
// The URL to fetch data from.
|
||||
var DATA_URL = "qute://history/data";
|
||||
@ -157,23 +161,28 @@ window.loadHistory = (function() {
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0, len = history.length - 1; i < len; i++) {
|
||||
var item = history[i];
|
||||
var currentItemDate = new Date(item.time);
|
||||
getSessionNode(currentItemDate).appendChild(makeHistoryRow(
|
||||
item.url, item.title, currentItemDate.toLocaleTimeString()
|
||||
));
|
||||
lastItemDate = currentItemDate;
|
||||
}
|
||||
|
||||
var next = history[history.length - 1].next;
|
||||
if (next === -1) {
|
||||
if (history.length === 0) {
|
||||
// Reached end of history
|
||||
window.onscroll = null;
|
||||
EOF_MESSAGE.style.display = "block";
|
||||
LOAD_LINK.style.display = "none";
|
||||
} else {
|
||||
nextTime = next;
|
||||
return;
|
||||
}
|
||||
|
||||
nextTime = history[history.length - 1].time;
|
||||
nextOffset = 0;
|
||||
|
||||
for (var i = 0, len = history.length; i < len; i++) {
|
||||
var item = history[i];
|
||||
// python's time.time returns seconds, but js Date expects ms
|
||||
var currentItemDate = new Date(item.time * 1000);
|
||||
getSessionNode(currentItemDate).appendChild(makeHistoryRow(
|
||||
item.url, item.title, currentItemDate.toLocaleTimeString()
|
||||
));
|
||||
lastItemDate = currentItemDate;
|
||||
if (item.time === nextTime) {
|
||||
nextOffset++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -182,10 +191,11 @@ window.loadHistory = (function() {
|
||||
* @return {void}
|
||||
*/
|
||||
function loadHistory() {
|
||||
var url = DATA_URL.concat("?offset=", nextOffset.toString());
|
||||
if (nextTime === null) {
|
||||
getJSON(DATA_URL, receiveHistory);
|
||||
getJSON(url, receiveHistory);
|
||||
} else {
|
||||
var url = DATA_URL.concat("?start_time=", nextTime.toString());
|
||||
url = url.concat("&start_time=", nextTime.toString());
|
||||
getJSON(url, receiveHistory);
|
||||
}
|
||||
}
|
||||
|
@ -123,6 +123,7 @@ class MainWindow(QWidget):
|
||||
Attributes:
|
||||
status: The StatusBar widget.
|
||||
tabbed_browser: The TabbedBrowser widget.
|
||||
state_before_fullscreen: window state before activation of fullscreen.
|
||||
_downloadview: The DownloadView widget.
|
||||
_vbox: The main QVBoxLayout.
|
||||
_commandrunner: The main CommandRunner instance.
|
||||
@ -217,6 +218,8 @@ class MainWindow(QWidget):
|
||||
|
||||
objreg.get("app").new_window.emit(self)
|
||||
|
||||
self.state_before_fullscreen = self.windowState()
|
||||
|
||||
def _init_geometry(self, geometry):
|
||||
"""Initialize the window geometry or load it from disk."""
|
||||
if geometry is not None:
|
||||
@ -461,6 +464,8 @@ class MainWindow(QWidget):
|
||||
tabs.tab_index_changed.connect(status.tabindex.on_tab_index_changed)
|
||||
|
||||
tabs.cur_url_changed.connect(status.url.set_url)
|
||||
tabs.cur_url_changed.connect(functools.partial(
|
||||
status.backforward.on_tab_cur_url_changed, tabs=tabs))
|
||||
tabs.cur_link_hovered.connect(status.url.set_hover_url)
|
||||
tabs.cur_load_status_changed.connect(status.url.on_load_status_changed)
|
||||
tabs.cur_fullscreen_requested.connect(self._on_fullscreen_requested)
|
||||
@ -475,9 +480,12 @@ class MainWindow(QWidget):
|
||||
@pyqtSlot(bool)
|
||||
def _on_fullscreen_requested(self, on):
|
||||
if on:
|
||||
self.state_before_fullscreen = self.windowState()
|
||||
self.showFullScreen()
|
||||
else:
|
||||
self.showNormal()
|
||||
elif self.isFullScreen():
|
||||
self.setWindowState(self.state_before_fullscreen)
|
||||
log.misc.debug('on: {}, state before fullscreen: {}'.format(
|
||||
on, debug.qflags_key(Qt, self.state_before_fullscreen)))
|
||||
|
||||
@cmdutils.register(instance='main-window', scope='window')
|
||||
@pyqtSlot()
|
||||
|
@ -99,8 +99,10 @@ class MessageView(QWidget):
|
||||
@config.change_filter('messages.timeout')
|
||||
def _set_clear_timer_interval(self):
|
||||
"""Configure self._clear_timer according to the config."""
|
||||
if config.val.messages.timeout != 0:
|
||||
self._clear_timer.setInterval(config.val.messages.timeout)
|
||||
interval = config.val.messages.timeout
|
||||
if interval > 0:
|
||||
interval *= min(5, len(self._messages))
|
||||
self._clear_timer.setInterval(interval)
|
||||
|
||||
@pyqtSlot()
|
||||
def clear_messages(self):
|
||||
@ -127,12 +129,13 @@ class MessageView(QWidget):
|
||||
widget = Message(level, text, replace=replace, parent=self)
|
||||
self._vbox.addWidget(widget)
|
||||
widget.show()
|
||||
if config.val.messages.timeout != 0:
|
||||
self._clear_timer.start()
|
||||
self._messages.append(widget)
|
||||
self._last_text = text
|
||||
self.show()
|
||||
self.update_geometry.emit()
|
||||
if config.val.messages.timeout != 0:
|
||||
self._set_clear_timer_interval()
|
||||
self._clear_timer.start()
|
||||
|
||||
def mousePressEvent(self, e):
|
||||
"""Clear messages when they are clicked on."""
|
||||
|
49
qutebrowser/mainwindow/statusbar/backforward.py
Normal file
49
qutebrowser/mainwindow/statusbar/backforward.py
Normal 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))
|
@ -25,8 +25,9 @@ from PyQt5.QtWidgets import QWidget, QHBoxLayout, QStackedLayout, QSizePolicy
|
||||
from qutebrowser.browser import browsertab
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import usertypes, log, objreg, utils
|
||||
from qutebrowser.mainwindow.statusbar import (command, progress, keystring,
|
||||
percentage, url, tabindex)
|
||||
from qutebrowser.mainwindow.statusbar import (backforward, command, progress,
|
||||
keystring, percentage, url,
|
||||
tabindex)
|
||||
from qutebrowser.mainwindow.statusbar import text as textwidget
|
||||
|
||||
|
||||
@ -184,6 +185,9 @@ class StatusBar(QWidget):
|
||||
self.percentage = percentage.Percentage()
|
||||
self._hbox.addWidget(self.percentage)
|
||||
|
||||
self.backforward = backforward.Backforward()
|
||||
self._hbox.addWidget(self.backforward)
|
||||
|
||||
self.tabindex = tabindex.TabIndex()
|
||||
self._hbox.addWidget(self.tabindex)
|
||||
|
||||
@ -329,6 +333,7 @@ class StatusBar(QWidget):
|
||||
self.url.on_tab_changed(tab)
|
||||
self.prog.on_tab_changed(tab)
|
||||
self.percentage.on_tab_changed(tab)
|
||||
self.backforward.on_tab_changed(tab)
|
||||
self.maybe_hide()
|
||||
assert tab.private == self._color_flags.private
|
||||
|
||||
|
@ -23,7 +23,7 @@ import functools
|
||||
import collections
|
||||
|
||||
from PyQt5.QtWidgets import QSizePolicy
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QTimer, QUrl, QSize
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QTimer, QUrl
|
||||
from PyQt5.QtGui import QIcon
|
||||
|
||||
from qutebrowser.config import config
|
||||
@ -239,6 +239,19 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
for tab in self.widgets():
|
||||
self._remove_tab(tab)
|
||||
|
||||
def tab_close_prompt_if_pinned(self, tab, force, yes_action):
|
||||
"""Helper method for tab_close.
|
||||
|
||||
If tab is pinned, prompt. If everything is good, run yes_action.
|
||||
"""
|
||||
if tab.data.pinned and not force:
|
||||
message.confirm_async(
|
||||
title='Pinned Tab',
|
||||
text="Are you sure you want to close a pinned tab?",
|
||||
yes_action=yes_action, default=False)
|
||||
else:
|
||||
yes_action()
|
||||
|
||||
def close_tab(self, tab, *, add_undo=True):
|
||||
"""Close a tab.
|
||||
|
||||
@ -348,7 +361,7 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
newtab = self.tabopen(url, background=False, idx=idx)
|
||||
|
||||
newtab.history.deserialize(history_data)
|
||||
self.set_tab_pinned(idx, pinned)
|
||||
self.set_tab_pinned(newtab, pinned)
|
||||
|
||||
@pyqtSlot('QUrl', bool)
|
||||
def openurl(self, url, newtab):
|
||||
@ -372,7 +385,8 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
log.webview.debug("Got invalid tab {} for index {}!".format(
|
||||
tab, idx))
|
||||
return
|
||||
self.close_tab(tab)
|
||||
self.tab_close_prompt_if_pinned(
|
||||
tab, False, lambda: self.close_tab(tab))
|
||||
|
||||
@pyqtSlot(browsertab.AbstractTab)
|
||||
def on_window_close_requested(self, widget):
|
||||
@ -443,13 +457,7 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
# Make sure the background tab has the correct initial size.
|
||||
# With a foreground tab, it's going to be resized correctly by the
|
||||
# layout anyways.
|
||||
if self.tabBar().vertical:
|
||||
tab_size = QSize(self.width() - self.tabBar().width(),
|
||||
self.height())
|
||||
else:
|
||||
tab_size = QSize(self.width(),
|
||||
self.height() - self.tabBar().height())
|
||||
tab.resize(tab_size)
|
||||
tab.resize(self.currentWidget().size())
|
||||
self.tab_index_changed.emit(self.currentIndex(), self.count())
|
||||
else:
|
||||
self.setCurrentWidget(tab)
|
||||
@ -705,12 +713,16 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
}
|
||||
msg = messages[status]
|
||||
|
||||
def show_error_page(html):
|
||||
tab.set_html(html)
|
||||
log.webview.error(msg)
|
||||
|
||||
if qtutils.version_check('5.9'):
|
||||
url_string = tab.url(requested=True).toDisplayString()
|
||||
error_page = jinja.render(
|
||||
'error.html', title="Error loading {}".format(url_string),
|
||||
url=url_string, error=msg)
|
||||
QTimer.singleShot(0, lambda: tab.set_html(error_page))
|
||||
url=url_string, error=msg, icon='')
|
||||
QTimer.singleShot(100, lambda: show_error_page(error_page))
|
||||
log.webview.error(msg)
|
||||
else:
|
||||
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-58698
|
||||
|
@ -26,7 +26,7 @@ from PyQt5.QtCore import (pyqtSignal, pyqtSlot, Qt, QSize, QRect, QPoint,
|
||||
QTimer, QUrl)
|
||||
from PyQt5.QtWidgets import (QTabWidget, QTabBar, QSizePolicy, QCommonStyle,
|
||||
QStyle, QStylePainter, QStyleOptionTab,
|
||||
QStyleFactory)
|
||||
QStyleFactory, QWidget)
|
||||
from PyQt5.QtGui import QIcon, QPalette, QColor
|
||||
|
||||
from qutebrowser.utils import qtutils, objreg, utils, usertypes, log
|
||||
@ -94,17 +94,18 @@ class TabWidget(QTabWidget):
|
||||
bar.set_tab_data(idx, 'indicator-color', color)
|
||||
bar.update(bar.tabRect(idx))
|
||||
|
||||
def set_tab_pinned(self, idx, pinned, *, loading=False):
|
||||
def set_tab_pinned(self, tab: QWidget,
|
||||
pinned: bool, *, loading: bool = False) -> None:
|
||||
"""Set the tab status as pinned.
|
||||
|
||||
Args:
|
||||
idx: The tab index.
|
||||
tab: The tab to pin
|
||||
pinned: Pinned tab state to set.
|
||||
loading: Whether to ignore current data state when
|
||||
counting pinned_count.
|
||||
"""
|
||||
bar = self.tabBar()
|
||||
tab = self.widget(idx)
|
||||
idx = self.indexOf(tab)
|
||||
|
||||
# Only modify pinned_count if we had a change
|
||||
# always modify pinned_count if we are loading
|
||||
@ -487,14 +488,10 @@ class TabBar(QTabBar):
|
||||
width = int(confwidth)
|
||||
size = QSize(max(minimum_size.width(), width), height)
|
||||
elif self.count() == 0:
|
||||
# This happens on startup on OS X.
|
||||
# This happens on startup on macOS.
|
||||
# We return it directly rather than setting `size' because we don't
|
||||
# want to ensure it's valid in this special case.
|
||||
return QSize()
|
||||
elif self.count() * minimum_size.width() > self.width():
|
||||
# If we don't have enough space, we return the minimum size so we
|
||||
# get scroll buttons as soon as needed.
|
||||
size = minimum_size
|
||||
else:
|
||||
try:
|
||||
pinned = self.tab_data(index, 'pinned')
|
||||
@ -522,13 +519,13 @@ class TabBar(QTabBar):
|
||||
width = no_pinned_width / (self.count() - self.pinned_count)
|
||||
else:
|
||||
|
||||
# If we *do* have enough space, tabs should occupy the whole
|
||||
# window width. If there are pinned tabs their size will be
|
||||
# subtracted from the total window width.
|
||||
# During shutdown the self.count goes down,
|
||||
# but the self.pinned_count not - this generates some odd
|
||||
# Tabs should attempt to occupy the whole window width. If
|
||||
# there are pinned tabs their size will be subtracted from the
|
||||
# total window width. During shutdown the self.count goes
|
||||
# down, but the self.pinned_count not - this generates some odd
|
||||
# behavior. To avoid this we compare self.count against
|
||||
# self.pinned_count.
|
||||
# self.pinned_count. If we end up having too little space, we
|
||||
# set the minimum size below.
|
||||
if self.pinned_count > 0 and no_pinned_count > 0:
|
||||
width = no_pinned_width / no_pinned_count
|
||||
else:
|
||||
@ -540,6 +537,10 @@ class TabBar(QTabBar):
|
||||
index < no_pinned_width % no_pinned_count):
|
||||
width += 1
|
||||
|
||||
# If we don't have enough space, we return the minimum size so we
|
||||
# get scroll buttons as soon as needed.
|
||||
width = max(width, minimum_size.width())
|
||||
|
||||
size = QSize(width, height)
|
||||
qtutils.ensure_valid(size)
|
||||
return size
|
||||
@ -750,6 +751,17 @@ class TabBarStyle(QCommonStyle):
|
||||
rct = super().subElementRect(sr, opt, widget)
|
||||
return rct
|
||||
else:
|
||||
try:
|
||||
# We need this so the left scroll button is aligned properly.
|
||||
# Otherwise, empty space will be shown after the last tab even
|
||||
# though the button width is set to 0
|
||||
#
|
||||
# QStyle.SE_TabBarScrollLeftButton was added in Qt 5.7
|
||||
if sr == QStyle.SE_TabBarScrollLeftButton:
|
||||
return super().subElementRect(sr, opt, widget)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
return self._style.subElementRect(sr, opt, widget)
|
||||
|
||||
def _tab_layout(self, opt):
|
||||
|
@ -28,6 +28,11 @@ import functools
|
||||
import faulthandler
|
||||
import os.path
|
||||
import collections
|
||||
try:
|
||||
# WORKAROUND for segfaults when using pdb in pytest for some reason...
|
||||
import readline # pylint: disable=unused-import
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
from PyQt5.QtCore import (pyqtSlot, qInstallMessageHandler, QObject,
|
||||
QSocketNotifier, QTimer, QUrl)
|
||||
|
@ -337,12 +337,12 @@ def check_libraries(backend):
|
||||
"or Install via pip.",
|
||||
pip="PyYAML"),
|
||||
'PyQt5.QtQml': _missing_str("PyQt5.QtQml"),
|
||||
'PyQt5.QtSql': _missing_str("PyQt5.QtSql"),
|
||||
}
|
||||
if backend == 'webengine':
|
||||
modules['PyQt5.QtWebEngineWidgets'] = _missing_str("QtWebEngine",
|
||||
webengine=True)
|
||||
modules['PyQt5.QtOpenGL'] = _missing_str("PyQt5.QtOpenGL")
|
||||
modules['OpenGL'] = _missing_str("PyOpenGL")
|
||||
else:
|
||||
assert backend == 'webkit'
|
||||
modules['PyQt5.QtWebKit'] = _missing_str("PyQt5.QtWebKit")
|
||||
|
@ -154,8 +154,8 @@ class GUIProcess(QObject):
|
||||
log.procs.debug("Process started.")
|
||||
self._started = True
|
||||
else:
|
||||
message.error("Error while spawning {}: {}.".format(
|
||||
self._what, self._proc.error()))
|
||||
message.error("Error while spawning {}: {}".format(
|
||||
self._what, ERROR_STRINGS[self._proc.error()]))
|
||||
|
||||
def exit_status(self):
|
||||
return self._proc.exitStatus()
|
||||
|
@ -20,7 +20,6 @@
|
||||
"""Utilities for IPC with existing instances."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import json
|
||||
import getpass
|
||||
@ -41,8 +40,8 @@ ATIME_INTERVAL = 60 * 60 * 3 * 1000 # 3 hours
|
||||
PROTOCOL_VERSION = 1
|
||||
|
||||
|
||||
def _get_socketname_legacy(basedir):
|
||||
"""Legacy implementation of _get_socketname."""
|
||||
def _get_socketname_windows(basedir):
|
||||
"""Get a socketname to use for Windows."""
|
||||
parts = ['qutebrowser', getpass.getuser()]
|
||||
if basedir is not None:
|
||||
md5 = hashlib.md5(basedir.encode('utf-8')).hexdigest()
|
||||
@ -50,10 +49,10 @@ def _get_socketname_legacy(basedir):
|
||||
return '-'.join(parts)
|
||||
|
||||
|
||||
def _get_socketname(basedir, legacy=False):
|
||||
def _get_socketname(basedir):
|
||||
"""Get a socketname to use."""
|
||||
if legacy or os.name == 'nt':
|
||||
return _get_socketname_legacy(basedir)
|
||||
if os.name == 'nt': # pragma: no cover
|
||||
return _get_socketname_windows(basedir)
|
||||
|
||||
parts_to_hash = [getpass.getuser()]
|
||||
if basedir is not None:
|
||||
@ -415,41 +414,7 @@ class IPCServer(QObject):
|
||||
self._remove_server()
|
||||
|
||||
|
||||
def _has_legacy_server(name):
|
||||
"""Check if there is a legacy server.
|
||||
|
||||
Args:
|
||||
name: The name to try to connect to.
|
||||
|
||||
Return:
|
||||
True if there is a server with the given name, False otherwise.
|
||||
"""
|
||||
socket = QLocalSocket()
|
||||
log.ipc.debug("Trying to connect to {}".format(name))
|
||||
socket.connectToServer(name)
|
||||
|
||||
err = socket.error()
|
||||
|
||||
if err != QLocalSocket.UnknownSocketError:
|
||||
log.ipc.debug("Socket error: {} ({})".format(
|
||||
socket.errorString(), err))
|
||||
|
||||
os_x_fail = (sys.platform == 'darwin' and
|
||||
socket.errorString() == 'QLocalSocket::connectToServer: '
|
||||
'Unknown error 38')
|
||||
|
||||
if err not in [QLocalSocket.ServerNotFoundError,
|
||||
QLocalSocket.ConnectionRefusedError] and not os_x_fail:
|
||||
return True
|
||||
|
||||
socket.disconnectFromServer()
|
||||
if socket.state() != QLocalSocket.UnconnectedState:
|
||||
socket.waitForDisconnected(CONNECT_TIMEOUT)
|
||||
return False
|
||||
|
||||
|
||||
def send_to_running_instance(socketname, command, target_arg, *,
|
||||
legacy_name=None, socket=None):
|
||||
def send_to_running_instance(socketname, command, target_arg, *, socket=None):
|
||||
"""Try to send a commandline to a running instance.
|
||||
|
||||
Blocks for CONNECT_TIMEOUT ms.
|
||||
@ -459,7 +424,6 @@ def send_to_running_instance(socketname, command, target_arg, *,
|
||||
command: The command to send to the running instance.
|
||||
target_arg: --target command line argument
|
||||
socket: The socket to read data from, or None.
|
||||
legacy_name: The legacy name to first try to connect to.
|
||||
|
||||
Return:
|
||||
True if connecting was successful, False if no connection was made.
|
||||
@ -467,13 +431,8 @@ def send_to_running_instance(socketname, command, target_arg, *,
|
||||
if socket is None:
|
||||
socket = QLocalSocket()
|
||||
|
||||
if legacy_name is not None and _has_legacy_server(legacy_name):
|
||||
name_to_use = legacy_name
|
||||
else:
|
||||
name_to_use = socketname
|
||||
|
||||
log.ipc.debug("Connecting to {}".format(name_to_use))
|
||||
socket.connectToServer(name_to_use)
|
||||
log.ipc.debug("Connecting to {}".format(socketname))
|
||||
socket.connectToServer(socketname)
|
||||
|
||||
connected = socket.waitForConnected(CONNECT_TIMEOUT)
|
||||
if connected:
|
||||
@ -527,12 +486,10 @@ def send_or_listen(args):
|
||||
None if an instance was running and received our request.
|
||||
"""
|
||||
socketname = _get_socketname(args.basedir)
|
||||
legacy_socketname = _get_socketname(args.basedir, legacy=True)
|
||||
try:
|
||||
try:
|
||||
sent = send_to_running_instance(socketname, args.command,
|
||||
args.target,
|
||||
legacy_name=legacy_socketname)
|
||||
args.target)
|
||||
if sent:
|
||||
return None
|
||||
log.init.debug("Starting IPC server...")
|
||||
@ -545,8 +502,7 @@ def send_or_listen(args):
|
||||
log.init.debug("Got AddressInUseError, trying again.")
|
||||
time.sleep(0.5)
|
||||
sent = send_to_running_instance(socketname, args.command,
|
||||
args.target,
|
||||
legacy_name=legacy_socketname)
|
||||
args.target)
|
||||
if sent:
|
||||
return None
|
||||
else:
|
||||
|
@ -21,7 +21,6 @@
|
||||
|
||||
import os
|
||||
import os.path
|
||||
import itertools
|
||||
import contextlib
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject
|
||||
@ -96,7 +95,7 @@ class BaseLineParser(QObject):
|
||||
"""
|
||||
assert self._configfile is not None
|
||||
if self._opened:
|
||||
raise IOError("Refusing to double-open AppendLineParser.")
|
||||
raise IOError("Refusing to double-open LineParser.")
|
||||
self._opened = True
|
||||
try:
|
||||
if self._binary:
|
||||
@ -133,73 +132,6 @@ class BaseLineParser(QObject):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class AppendLineParser(BaseLineParser):
|
||||
|
||||
"""LineParser which reads lazily and appends data to existing one.
|
||||
|
||||
Attributes:
|
||||
_new_data: The data which was added in this session.
|
||||
"""
|
||||
|
||||
def __init__(self, configdir, fname, *, parent=None):
|
||||
super().__init__(configdir, fname, binary=False, parent=parent)
|
||||
self.new_data = []
|
||||
self._fileobj = None
|
||||
|
||||
def __iter__(self):
|
||||
if self._fileobj is None:
|
||||
raise ValueError("Iterating without open() being called!")
|
||||
file_iter = (line.rstrip('\n') for line in self._fileobj)
|
||||
return itertools.chain(file_iter, iter(self.new_data))
|
||||
|
||||
@contextlib.contextmanager
|
||||
def open(self):
|
||||
"""Open the on-disk history file. Needed for __iter__."""
|
||||
try:
|
||||
with self._open('r') as f:
|
||||
self._fileobj = f
|
||||
yield
|
||||
except FileNotFoundError:
|
||||
self._fileobj = []
|
||||
yield
|
||||
finally:
|
||||
self._fileobj = None
|
||||
|
||||
def get_recent(self, count=4096):
|
||||
"""Get the last count bytes from the underlying file."""
|
||||
with self._open('r') as f:
|
||||
f.seek(0, os.SEEK_END)
|
||||
size = f.tell()
|
||||
try:
|
||||
if size - count > 0:
|
||||
offset = size - count
|
||||
else:
|
||||
offset = 0
|
||||
f.seek(offset)
|
||||
data = f.readlines()
|
||||
finally:
|
||||
f.seek(0, os.SEEK_END)
|
||||
return data
|
||||
|
||||
def save(self):
|
||||
do_save = self._prepare_save()
|
||||
if not do_save:
|
||||
return
|
||||
with self._open('a') as f:
|
||||
self._write(f, self.new_data)
|
||||
self.new_data = []
|
||||
self._after_save()
|
||||
|
||||
def clear(self):
|
||||
do_save = self._prepare_save()
|
||||
if not do_save:
|
||||
return
|
||||
with self._open('w'):
|
||||
pass
|
||||
self.new_data = []
|
||||
self._after_save()
|
||||
|
||||
|
||||
class LineParser(BaseLineParser):
|
||||
|
||||
"""Parser for configuration files which are simply line-based.
|
||||
@ -240,7 +172,7 @@ class LineParser(BaseLineParser):
|
||||
def save(self):
|
||||
"""Save the config file."""
|
||||
if self._opened:
|
||||
raise IOError("Refusing to double-open AppendLineParser.")
|
||||
raise IOError("Refusing to double-open LineParser.")
|
||||
do_save = self._prepare_save()
|
||||
if not do_save:
|
||||
return
|
||||
|
@ -23,14 +23,15 @@ import os
|
||||
import os.path
|
||||
|
||||
import sip
|
||||
from PyQt5.QtCore import pyqtSignal, QUrl, QObject, QPoint, QTimer
|
||||
from PyQt5.QtCore import QUrl, QObject, QPoint, QTimer
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
import yaml
|
||||
|
||||
from qutebrowser.utils import (standarddir, objreg, qtutils, log, usertypes,
|
||||
message, utils)
|
||||
from qutebrowser.utils import (standarddir, objreg, qtutils, log, message,
|
||||
utils)
|
||||
from qutebrowser.commands import cmdexc, cmdutils
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.completion.models import miscmodels
|
||||
|
||||
|
||||
default = object() # Sentinel value
|
||||
@ -101,14 +102,8 @@ class SessionManager(QObject):
|
||||
closed.
|
||||
_current: The name of the currently loaded session, or None.
|
||||
did_load: Set when a session was loaded.
|
||||
|
||||
Signals:
|
||||
update_completion: Emitted when the session completion should get
|
||||
updated.
|
||||
"""
|
||||
|
||||
update_completion = pyqtSignal()
|
||||
|
||||
def __init__(self, base_path, parent=None):
|
||||
super().__init__(parent)
|
||||
self._current = None
|
||||
@ -297,8 +292,7 @@ class SessionManager(QObject):
|
||||
utils.yaml_dump(data, f)
|
||||
except (OSError, UnicodeEncodeError, yaml.YAMLError) as e:
|
||||
raise SessionError(e)
|
||||
else:
|
||||
self.update_completion.emit()
|
||||
|
||||
if load_next_time:
|
||||
state_config = objreg.get('state-config')
|
||||
state_config['general']['session'] = name
|
||||
@ -401,7 +395,7 @@ class SessionManager(QObject):
|
||||
tab_to_focus = i
|
||||
if new_tab.data.pinned:
|
||||
tabbed_browser.set_tab_pinned(
|
||||
i, new_tab.data.pinned, loading=True)
|
||||
new_tab, new_tab.data.pinned, loading=True)
|
||||
if tab_to_focus is not None:
|
||||
tabbed_browser.setCurrentIndex(tab_to_focus)
|
||||
if win.get('active', False):
|
||||
@ -419,7 +413,6 @@ class SessionManager(QObject):
|
||||
os.remove(path)
|
||||
except OSError as e:
|
||||
raise SessionError(e)
|
||||
self.update_completion.emit()
|
||||
|
||||
def list_sessions(self):
|
||||
"""Get a list of all session names."""
|
||||
@ -431,7 +424,7 @@ class SessionManager(QObject):
|
||||
return sessions
|
||||
|
||||
@cmdutils.register(instance='session-manager')
|
||||
@cmdutils.argument('name', completion=usertypes.Completion.sessions)
|
||||
@cmdutils.argument('name', completion=miscmodels.session)
|
||||
def session_load(self, name, clear=False, temp=False, force=False):
|
||||
"""Load a session.
|
||||
|
||||
@ -459,7 +452,7 @@ class SessionManager(QObject):
|
||||
win.close()
|
||||
|
||||
@cmdutils.register(instance='session-manager')
|
||||
@cmdutils.argument('name', completion=usertypes.Completion.sessions)
|
||||
@cmdutils.argument('name', completion=miscmodels.session)
|
||||
@cmdutils.argument('win_id', win_id=True)
|
||||
@cmdutils.argument('with_private', flag='p')
|
||||
def session_save(self, name: str = default, current=False, quiet=False,
|
||||
@ -498,7 +491,7 @@ class SessionManager(QObject):
|
||||
message.info("Saved session {}.".format(name))
|
||||
|
||||
@cmdutils.register(instance='session-manager')
|
||||
@cmdutils.argument('name', completion=usertypes.Completion.sessions)
|
||||
@cmdutils.argument('name', completion=miscmodels.session)
|
||||
def session_delete(self, name, force=False):
|
||||
"""Delete a session.
|
||||
|
||||
|
256
qutebrowser/misc/sql.py
Normal file
256
qutebrowser/misc/sql.py
Normal 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
|
@ -94,7 +94,7 @@ LOGGER_NAMES = [
|
||||
'commands', 'signals', 'downloads',
|
||||
'js', 'qt', 'rfc6266', 'ipc', 'shlexer',
|
||||
'save', 'message', 'config', 'sessions',
|
||||
'webelem', 'prompt', 'network'
|
||||
'webelem', 'prompt', 'network', 'sql'
|
||||
]
|
||||
|
||||
|
||||
@ -141,6 +141,7 @@ sessions = logging.getLogger('sessions')
|
||||
webelem = logging.getLogger('webelem')
|
||||
prompt = logging.getLogger('prompt')
|
||||
network = logging.getLogger('network')
|
||||
sql = logging.getLogger('sql')
|
||||
|
||||
|
||||
ram_handler = None
|
||||
|
@ -107,7 +107,7 @@ def runtime():
|
||||
if sys.platform.startswith('linux'):
|
||||
typ = QStandardPaths.RuntimeLocation
|
||||
else: # pragma: no cover
|
||||
# RuntimeLocation is a weird path on OS X and Windows.
|
||||
# RuntimeLocation is a weird path on macOS and Windows.
|
||||
typ = QStandardPaths.TempLocation
|
||||
|
||||
overridden, path = _from_args(typ, _args)
|
||||
|
@ -236,13 +236,6 @@ KeyMode = enum('KeyMode', ['normal', 'hint', 'command', 'yesno', 'prompt',
|
||||
'jump_mark', 'record_macro', 'run_macro'])
|
||||
|
||||
|
||||
# Available command completions
|
||||
Completion = enum('Completion', ['command', 'section', 'option', 'value',
|
||||
'helptopic', 'quickmark_by_name',
|
||||
'bookmark_by_url', 'url', 'tab', 'sessions',
|
||||
'bind'])
|
||||
|
||||
|
||||
# Exit statuses for errors. Needs to be an int for sys.exit.
|
||||
Exit = enum('Exit', ['ok', 'reserved', 'exception', 'err_ipc', 'err_init',
|
||||
'err_config', 'err_key_config'], is_int=True, start=0)
|
||||
|
@ -28,7 +28,6 @@ import os.path
|
||||
import collections
|
||||
import functools
|
||||
import contextlib
|
||||
import itertools
|
||||
import socket
|
||||
import shlex
|
||||
|
||||
@ -378,8 +377,8 @@ def keyevent_to_string(e):
|
||||
None if only modifiers are pressed..
|
||||
"""
|
||||
if sys.platform == 'darwin':
|
||||
# Qt swaps Ctrl/Meta on OS X, so we switch it back here so the user can
|
||||
# use it in the config as expected. See:
|
||||
# Qt swaps Ctrl/Meta on macOS, so we switch it back here so the user
|
||||
# can use it in the config as expected. See:
|
||||
# https://github.com/qutebrowser/qutebrowser/issues/110
|
||||
# http://doc.qt.io/qt-5.4/osx-issues.html#special-keys
|
||||
modmask2str = collections.OrderedDict([
|
||||
@ -742,25 +741,6 @@ def sanitize_filename(name, replacement='_'):
|
||||
return name
|
||||
|
||||
|
||||
def newest_slice(iterable, count):
|
||||
"""Get an iterable for the n newest items of the given iterable.
|
||||
|
||||
Args:
|
||||
count: How many elements to get.
|
||||
0: get no items:
|
||||
n: get the n newest items
|
||||
-1: get all items
|
||||
"""
|
||||
if count < -1:
|
||||
raise ValueError("count can't be smaller than -1!")
|
||||
elif count == 0:
|
||||
return []
|
||||
elif count == -1 or len(iterable) < count:
|
||||
return iterable
|
||||
else:
|
||||
return itertools.islice(iterable, len(iterable) - count, len(iterable))
|
||||
|
||||
|
||||
def set_clipboard(data, selection=False):
|
||||
"""Set the clipboard to some given data."""
|
||||
if selection and not supports_selection():
|
||||
|
@ -45,7 +45,7 @@ except ImportError: # pragma: no cover
|
||||
|
||||
import qutebrowser
|
||||
from qutebrowser.utils import log, utils, standarddir, usertypes, qtutils
|
||||
from qutebrowser.misc import objects, earlyinit
|
||||
from qutebrowser.misc import objects, earlyinit, sql
|
||||
from qutebrowser.browser import pdfjs
|
||||
|
||||
|
||||
@ -186,7 +186,6 @@ def _module_versions():
|
||||
('yaml', ['__version__']),
|
||||
('cssutils', ['__version__']),
|
||||
('typing', []),
|
||||
('OpenGL', ['__version__']),
|
||||
('PyQt5.QtWebEngineWidgets', []),
|
||||
('PyQt5.QtWebKitWidgets', []),
|
||||
])
|
||||
@ -326,11 +325,11 @@ def version():
|
||||
|
||||
lines += _module_versions()
|
||||
|
||||
lines += ['pdf.js: {}'.format(_pdfjs_version())]
|
||||
|
||||
lines += [
|
||||
'SSL: {}'.format(QSslSocket.sslLibraryVersionString()),
|
||||
'',
|
||||
'pdf.js: {}'.format(_pdfjs_version()),
|
||||
'sqlite: {}'.format(sql.version()),
|
||||
'QtNetwork SSL: {}\n'.format(QSslSocket.sslLibraryVersionString()
|
||||
if QSslSocket.supportsSsl() else 'no'),
|
||||
]
|
||||
|
||||
qapp = QApplication.instance()
|
||||
|
@ -7,4 +7,3 @@ MarkupSafe==1.0
|
||||
Pygments==2.2.0
|
||||
pyPEG2==2.15.2
|
||||
PyYAML==3.12
|
||||
PyOpenGL==3.1.0
|
||||
|
@ -280,8 +280,6 @@ def main(colors=False):
|
||||
"asciidoc.py. If not given, it's searched in PATH.",
|
||||
nargs=2, required=False,
|
||||
metavar=('PYTHON', 'ASCIIDOC'))
|
||||
parser.add_argument('--no-authors', help=argparse.SUPPRESS,
|
||||
action='store_true')
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
os.mkdir('qutebrowser/html/doc')
|
||||
|
@ -64,7 +64,7 @@ def call_tox(toxenv, *args, python=sys.executable):
|
||||
env['PYTHON'] = python
|
||||
env['PATH'] = os.environ['PATH'] + os.pathsep + os.path.dirname(python)
|
||||
subprocess.check_call(
|
||||
[sys.executable, '-m', 'tox', '-v', '-e', toxenv] + list(args),
|
||||
[sys.executable, '-m', 'tox', '-vv', '-e', toxenv] + list(args),
|
||||
env=env)
|
||||
|
||||
|
||||
@ -92,7 +92,7 @@ def smoke_test(executable):
|
||||
'--temp-basedir', 'about:blank', ':later 500 quit'])
|
||||
|
||||
|
||||
def patch_osx_app():
|
||||
def patch_mac_app():
|
||||
"""Patch .app to copy missing data and link some libs.
|
||||
|
||||
See https://github.com/pyinstaller/pyinstaller/issues/2276
|
||||
@ -109,8 +109,11 @@ def patch_osx_app():
|
||||
for f in glob.glob(os.path.join(qtwe_core_dir, 'Resources', '*')):
|
||||
dest = os.path.join(app_path, 'Contents', 'Resources')
|
||||
if os.path.isdir(f):
|
||||
shutil.copytree(f, os.path.join(dest, f))
|
||||
dir_dest = os.path.join(dest, os.path.basename(f))
|
||||
print("Copying directory {} to {}".format(f, dir_dest))
|
||||
shutil.copytree(f, dir_dest)
|
||||
else:
|
||||
print("Copying {} to {}".format(f, dest))
|
||||
shutil.copy(f, dest)
|
||||
# Link dependencies
|
||||
for lib in ['QtCore', 'QtWebEngineCore', 'QtQuick', 'QtQml', 'QtNetwork',
|
||||
@ -122,37 +125,45 @@ def patch_osx_app():
|
||||
os.path.join(dest, lib))
|
||||
|
||||
|
||||
def build_osx():
|
||||
"""Build OS X .dmg/.app."""
|
||||
def build_mac():
|
||||
"""Build macOS .dmg/.app."""
|
||||
utils.print_title("Cleaning up...")
|
||||
for f in ['wc.dmg', 'template.dmg']:
|
||||
try:
|
||||
os.remove(f)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
for d in ['dist', 'build']:
|
||||
shutil.rmtree(d, ignore_errors=True)
|
||||
utils.print_title("Updating 3rdparty content")
|
||||
# Currently disabled because QtWebEngine has no pdfjs support
|
||||
# update_3rdparty.run(ace=False, pdfjs=True, fancy_dmg=False)
|
||||
utils.print_title("Building .app via pyinstaller")
|
||||
call_tox('pyinstaller', '-r')
|
||||
utils.print_title("Patching .app")
|
||||
patch_osx_app()
|
||||
patch_mac_app()
|
||||
utils.print_title("Building .dmg")
|
||||
subprocess.check_call(['make', '-f', 'scripts/dev/Makefile-dmg'])
|
||||
utils.print_title("Cleaning up...")
|
||||
for f in ['wc.dmg', 'template.dmg']:
|
||||
os.remove(f)
|
||||
for d in ['dist', 'build']:
|
||||
shutil.rmtree(d)
|
||||
|
||||
dmg_name = 'qutebrowser-{}.dmg'.format(qutebrowser.__version__)
|
||||
os.rename('qutebrowser.dmg', dmg_name)
|
||||
|
||||
utils.print_title("Running smoke test")
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
subprocess.check_call(['hdiutil', 'attach', dmg_name,
|
||||
'-mountpoint', tmpdir])
|
||||
try:
|
||||
binary = os.path.join(tmpdir, 'qutebrowser.app', 'Contents',
|
||||
'MacOS', 'qutebrowser')
|
||||
smoke_test(binary)
|
||||
finally:
|
||||
subprocess.check_call(['hdiutil', 'detach', tmpdir])
|
||||
|
||||
return [(dmg_name, 'application/x-apple-diskimage', 'OS X .dmg')]
|
||||
try:
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
subprocess.check_call(['hdiutil', 'attach', dmg_name,
|
||||
'-mountpoint', tmpdir])
|
||||
try:
|
||||
binary = os.path.join(tmpdir, 'qutebrowser.app', 'Contents',
|
||||
'MacOS', 'qutebrowser')
|
||||
smoke_test(binary)
|
||||
finally:
|
||||
subprocess.call(['hdiutil', 'detach', tmpdir])
|
||||
except PermissionError as e:
|
||||
print("Failed to remove tempdir: {}".format(e))
|
||||
|
||||
return [(dmg_name, 'application/x-apple-diskimage', 'macOS .dmg')]
|
||||
|
||||
|
||||
def patch_windows(out_dir):
|
||||
@ -167,6 +178,7 @@ def patch_windows(out_dir):
|
||||
def build_windows():
|
||||
"""Build windows executables/setups."""
|
||||
utils.print_title("Updating 3rdparty content")
|
||||
# Currently disabled because QtWebEngine has no pdfjs support
|
||||
# update_3rdparty.run(ace=False, pdfjs=True, fancy_dmg=False)
|
||||
|
||||
utils.print_title("Building Windows binaries")
|
||||
@ -203,8 +215,8 @@ def build_windows():
|
||||
'/DVERSION={}'.format(qutebrowser.__version__),
|
||||
'misc/qutebrowser.nsi'])
|
||||
|
||||
name_32 = 'qutebrowser-{}-win32.msi'.format(qutebrowser.__version__)
|
||||
name_64 = 'qutebrowser-{}-amd64.msi'.format(qutebrowser.__version__)
|
||||
name_32 = 'qutebrowser-{}-win32.exe'.format(qutebrowser.__version__)
|
||||
name_64 = 'qutebrowser-{}-amd64.exe'.format(qutebrowser.__version__)
|
||||
|
||||
artifacts += [
|
||||
(os.path.join('dist', name_32),
|
||||
@ -280,6 +292,14 @@ def build_sdist():
|
||||
return artifacts
|
||||
|
||||
|
||||
def read_github_token():
|
||||
"""Read the GitHub API token from disk."""
|
||||
token_file = os.path.join(os.path.expanduser('~'), '.gh_token')
|
||||
with open(token_file, encoding='ascii') as f:
|
||||
token = f.read().strip()
|
||||
return token
|
||||
|
||||
|
||||
def github_upload(artifacts, tag):
|
||||
"""Upload the given artifacts to GitHub.
|
||||
|
||||
@ -290,9 +310,7 @@ def github_upload(artifacts, tag):
|
||||
import github3
|
||||
utils.print_title("Uploading to github...")
|
||||
|
||||
token_file = os.path.join(os.path.expanduser('~'), '.gh_token')
|
||||
with open(token_file, encoding='ascii') as f:
|
||||
token = f.read().strip()
|
||||
token = read_github_token()
|
||||
gh = github3.login(token=token)
|
||||
repo = gh.repository('qutebrowser', 'qutebrowser')
|
||||
|
||||
@ -329,6 +347,12 @@ def main():
|
||||
|
||||
upload_to_pypi = False
|
||||
|
||||
if args.upload is not None:
|
||||
# Fail early when trying to upload without github3 installed
|
||||
# or without API token
|
||||
import github3 # pylint: disable=unused-variable
|
||||
read_github_token()
|
||||
|
||||
if os.name == 'nt':
|
||||
if sys.maxsize > 2**32:
|
||||
# WORKAROUND
|
||||
@ -342,7 +366,7 @@ def main():
|
||||
artifacts = build_windows()
|
||||
elif sys.platform == 'darwin':
|
||||
run_asciidoc2html(args)
|
||||
artifacts = build_osx()
|
||||
artifacts = build_mac()
|
||||
else:
|
||||
artifacts = build_sdist()
|
||||
upload_to_pypi = True
|
||||
|
@ -51,9 +51,9 @@ PERFECT_FILES = [
|
||||
'browser/webkit/cache.py'),
|
||||
('tests/unit/browser/webkit/test_cookies.py',
|
||||
'browser/webkit/cookies.py'),
|
||||
('tests/unit/browser/webkit/test_history.py',
|
||||
('tests/unit/browser/test_history.py',
|
||||
'browser/history.py'),
|
||||
('tests/unit/browser/webkit/test_history.py',
|
||||
('tests/unit/browser/test_history.py',
|
||||
'browser/webkit/webkithistory.py'),
|
||||
('tests/unit/browser/webkit/http/test_http.py',
|
||||
'browser/webkit/http.py'),
|
||||
@ -117,6 +117,8 @@ PERFECT_FILES = [
|
||||
'mainwindow/statusbar/textbase.py'),
|
||||
('tests/unit/mainwindow/statusbar/test_url.py',
|
||||
'mainwindow/statusbar/url.py'),
|
||||
('tests/unit/mainwindow/statusbar/test_backforward.py',
|
||||
'mainwindow/statusbar/backforward.py'),
|
||||
('tests/unit/mainwindow/test_messageview.py',
|
||||
'mainwindow/messageview.py'),
|
||||
|
||||
@ -155,9 +157,11 @@ PERFECT_FILES = [
|
||||
'utils/javascript.py'),
|
||||
|
||||
('tests/unit/completion/test_models.py',
|
||||
'completion/models/base.py'),
|
||||
('tests/unit/completion/test_sortfilter.py',
|
||||
'completion/models/sortfilter.py'),
|
||||
'completion/models/urlmodel.py'),
|
||||
('tests/unit/completion/test_histcategory.py',
|
||||
'completion/models/histcategory.py'),
|
||||
('tests/unit/completion/test_listcategory.py',
|
||||
'completion/models/listcategory.py'),
|
||||
|
||||
]
|
||||
|
||||
|
@ -109,7 +109,7 @@ elif [[ $TRAVIS_OS_NAME == osx ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
pyqt_pkgs="python3-pyqt5 python3-pyqt5.qtquick python3-pyqt5.qtwebkit"
|
||||
pyqt_pkgs="python3-pyqt5 python3-pyqt5.qtquick python3-pyqt5.qtwebkit python3-pyqt5.qtsql libqt5sql5-sqlite"
|
||||
|
||||
pip_install pip
|
||||
pip_install -r misc/requirements/requirements-tox.txt
|
||||
|
@ -4,7 +4,6 @@ if [[ $DOCKER ]]; then
|
||||
docker run --privileged -v $PWD:/outside -e QUTE_BDD_WEBENGINE=$QUTE_BDD_WEBENGINE -e DOCKER=$DOCKER -e CI=$CI qutebrowser/travis:$DOCKER
|
||||
else
|
||||
args=()
|
||||
[[ $TESTENV == docs ]] && args=('--no-authors')
|
||||
[[ $TRAVIS_OS_NAME == osx ]] && args=('--qute-bdd-webengine' '--no-xvfb')
|
||||
|
||||
tox -e $TESTENV -- "${args[@]}"
|
||||
|
@ -89,6 +89,12 @@ def whitelist_generator():
|
||||
# vulture doesn't notice the hasattr() and thus thinks netrc_used is unused
|
||||
# in NetworkManager.on_authentication_required
|
||||
yield 'PyQt5.QtNetwork.QNetworkReply.netrc_used'
|
||||
yield 'qutebrowser.browser.downloads.last_used_directory'
|
||||
yield 'PaintContext.clip' # from completiondelegate.py
|
||||
yield 'logging.LogRecord.log_color' # from logging.py
|
||||
yield 'scripts.utils.use_color' # from asciidoc2html.py
|
||||
for attr in ['pyeval_output', 'log_clipboard', 'fake_clipboard']:
|
||||
yield 'qutebrowser.misc.utilcmds.' + attr
|
||||
|
||||
for attr in ['fileno', 'truncate', 'closed', 'readable']:
|
||||
yield 'qutebrowser.utils.qtutils.PyQIODevice.' + attr
|
||||
@ -123,7 +129,7 @@ def filter_func(item):
|
||||
True if the missing function should be filtered/ignored, False
|
||||
otherwise.
|
||||
"""
|
||||
return bool(re.match(r'[a-z]+[A-Z][a-zA-Z]+', str(item)))
|
||||
return bool(re.match(r'[a-z]+[A-Z][a-zA-Z]+', item.name))
|
||||
|
||||
|
||||
def report(items):
|
||||
@ -137,7 +143,7 @@ def report(items):
|
||||
relpath = os.path.relpath(item.filename)
|
||||
path = relpath if not relpath.startswith('..') else item.filename
|
||||
output.append("{}:{}: Unused {} '{}'".format(path, item.lineno,
|
||||
item.typ, item))
|
||||
item.typ, item.name))
|
||||
return output
|
||||
|
||||
|
||||
|
@ -26,7 +26,6 @@ import shutil
|
||||
import os.path
|
||||
import inspect
|
||||
import subprocess
|
||||
import collections
|
||||
import tempfile
|
||||
import argparse
|
||||
|
||||
@ -43,7 +42,7 @@ from qutebrowser.utils import docutils, usertypes
|
||||
|
||||
FILE_HEADER = """
|
||||
// DO NOT EDIT THIS FILE DIRECTLY!
|
||||
// It is autogenerated from docstrings by running:
|
||||
// It is autogenerated by running:
|
||||
// $ python3 scripts/dev/src2asciidoc.py
|
||||
|
||||
""".lstrip()
|
||||
@ -415,32 +414,6 @@ def generate_settings(filename):
|
||||
_generate_setting_option(f, opt)
|
||||
|
||||
|
||||
def _get_authors():
|
||||
"""Get a list of authors based on git commit logs."""
|
||||
corrections = {
|
||||
'binix': 'sbinix',
|
||||
'Averrin': 'Alexey "Averrin" Nabrodov',
|
||||
'Alexey Nabrodov': 'Alexey "Averrin" Nabrodov',
|
||||
'Michael': 'Halfwit',
|
||||
'Error 800': 'error800',
|
||||
'larryhynes': 'Larry Hynes',
|
||||
'Daniel': 'Daniel Schadt',
|
||||
'Alexey Glushko': 'haitaka',
|
||||
'Corentin Jule': 'Corentin Julé',
|
||||
'Claire C.C': 'Claire Cavanaugh',
|
||||
'Rahid': 'Maciej Wołczyk',
|
||||
'Fritz V155 Reichwald': 'Fritz Reichwald',
|
||||
'Spreadyy': 'sandrosc',
|
||||
}
|
||||
ignored = ['pyup-bot']
|
||||
commits = subprocess.check_output(['git', 'log', '--format=%aN'])
|
||||
authors = [corrections.get(author, author)
|
||||
for author in commits.decode('utf-8').splitlines()
|
||||
if author not in ignored]
|
||||
cnt = collections.Counter(authors)
|
||||
return sorted(cnt, key=lambda k: (cnt[k], k), reverse=True)
|
||||
|
||||
|
||||
def _format_block(filename, what, data):
|
||||
"""Format a block in a file.
|
||||
|
||||
@ -487,12 +460,6 @@ def _format_block(filename, what, data):
|
||||
shutil.move(tmpname, filename)
|
||||
|
||||
|
||||
def regenerate_authors(filename):
|
||||
"""Re-generate the authors inside README based on the commits made."""
|
||||
data = ['* {}\n'.format(author) for author in _get_authors()]
|
||||
_format_block(filename, 'authors', data)
|
||||
|
||||
|
||||
def regenerate_manpage(filename):
|
||||
"""Update manpage OPTIONS using an argparse parser."""
|
||||
# pylint: disable=protected-access
|
||||
@ -538,9 +505,6 @@ def main():
|
||||
generate_settings('doc/help/settings.asciidoc')
|
||||
print("Generating command help...")
|
||||
generate_commands('doc/help/commands.asciidoc')
|
||||
if '--no-authors' not in sys.argv:
|
||||
print("Generating authors in README...")
|
||||
regenerate_authors('README.asciidoc')
|
||||
if '--cheatsheet' in sys.argv:
|
||||
print("Regenerating cheatsheet .pngs")
|
||||
regenerate_cheatsheet()
|
||||
|
18
scripts/open_url_in_instance.sh
Executable file
18
scripts/open_url_in_instance.sh
Executable 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
|
@ -52,8 +52,8 @@ def _apply_platform_markers(config, item):
|
||||
('posix', os.name != 'posix', "Requires a POSIX os"),
|
||||
('windows', os.name != 'nt', "Requires Windows"),
|
||||
('linux', not sys.platform.startswith('linux'), "Requires Linux"),
|
||||
('osx', sys.platform != 'darwin', "Requires OS X"),
|
||||
('not_osx', sys.platform == 'darwin', "Skipped on OS X"),
|
||||
('mac', sys.platform != 'darwin', "Requires macOS"),
|
||||
('not_mac', sys.platform == 'darwin', "Skipped on macOS"),
|
||||
('not_frozen', getattr(sys, 'frozen', False),
|
||||
"Can't be run when frozen"),
|
||||
('frozen', not getattr(sys, 'frozen', False),
|
||||
|
@ -149,7 +149,7 @@ def pytest_collection_modifyitems(config, items):
|
||||
not config.webengine and qtutils.is_qtwebkit_ng()),
|
||||
('qtwebengine_flaky', 'Flaky with QtWebEngine', pytest.mark.skipif,
|
||||
config.webengine),
|
||||
('qtwebengine_osx_xfail', 'Fails on OS X with QtWebEngine',
|
||||
('qtwebengine_mac_xfail', 'Fails on macOS with QtWebEngine',
|
||||
pytest.mark.xfail, config.webengine and sys.platform == 'darwin'),
|
||||
]
|
||||
|
||||
|
8
tests/end2end/data/downloads/download with no title.html
Normal file
8
tests/end2end/data/downloads/download with no title.html
Normal file
@ -0,0 +1,8 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
BIN
tests/end2end/data/downloads/qutebrowser.png
Normal file
BIN
tests/end2end/data/downloads/qutebrowser.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
@ -32,23 +32,23 @@ Feature: Using completion
|
||||
|
||||
Scenario: Using command completion
|
||||
When I run :set-cmd-text :
|
||||
Then the completion model should be CommandCompletionModel
|
||||
Then the completion model should be command
|
||||
|
||||
Scenario: Using help completion
|
||||
When I run :set-cmd-text -s :help
|
||||
Then the completion model should be HelpCompletionModel
|
||||
Then the completion model should be helptopic
|
||||
|
||||
Scenario: Using quickmark completion
|
||||
When I run :set-cmd-text -s :quickmark-load
|
||||
Then the completion model should be QuickmarkCompletionModel
|
||||
Then the completion model should be quickmark
|
||||
|
||||
Scenario: Using bookmark completion
|
||||
When I run :set-cmd-text -s :bookmark-load
|
||||
Then the completion model should be BookmarkCompletionModel
|
||||
Then the completion model should be bookmark
|
||||
|
||||
Scenario: Using bind completion
|
||||
When I run :set-cmd-text -s :bind X
|
||||
Then the completion model should be BindCompletionModel
|
||||
Then the completion model should be bind
|
||||
|
||||
Scenario: Using session completion
|
||||
Given I open data/hello.txt
|
||||
@ -60,43 +60,13 @@ Feature: Using completion
|
||||
And I run :command-accept
|
||||
Then the error "Session hello not found!" should be shown
|
||||
|
||||
# FIXME:conf
|
||||
Scenario: Using option completion
|
||||
When I run :set-cmd-text -s :set colors
|
||||
Then the completion model should be option
|
||||
|
||||
# Scenario: Using option completion
|
||||
# When I run :set-cmd-text -s :set colors
|
||||
# Then the completion model should be SettingOptionCompletionModel
|
||||
|
||||
# Scenario: Using value completion
|
||||
# When I run :set-cmd-text -s :set colors statusbar.bg
|
||||
# Then the completion model should be SettingValueCompletionModel
|
||||
|
||||
Scenario: Updating the completion in realtime
|
||||
Given I have a fresh instance
|
||||
And I set completion.quick to false
|
||||
When I open data/hello.txt
|
||||
And I run :set-cmd-text -s :buffer
|
||||
And I run :completion-item-focus next
|
||||
And I open data/hello2.txt in a new background tab
|
||||
And I run :completion-item-focus next
|
||||
And I open data/hello3.txt in a new background tab
|
||||
And I run :completion-item-focus next
|
||||
And I run :command-accept
|
||||
Then the following tabs should be open:
|
||||
- data/hello.txt
|
||||
- data/hello2.txt
|
||||
- data/hello3.txt (active)
|
||||
|
||||
# FIXME:conf
|
||||
|
||||
# Scenario: Updating the value completion in realtime
|
||||
# Given I set colors.statusbar.normal.bg to green
|
||||
# When I run :set-cmd-text -s :set colors.statusbar.normal.bg
|
||||
# And I set colors.statusbar.normal.bg to yellow
|
||||
# And I run :completion-item-focus next
|
||||
# And I run :completion-item-focus next
|
||||
# And I set colors.statusbar.normal.bg to red
|
||||
# And I run :command-accept
|
||||
# Then the option colors.statusbar.normal.bg should be set to yellow
|
||||
Scenario: Using value completion
|
||||
When I run :set-cmd-text -s :set colors statusbar.bg
|
||||
Then the completion model should be value
|
||||
|
||||
Scenario: Deleting an open tab via the completion
|
||||
Given I have a fresh instance
|
||||
|
@ -61,8 +61,8 @@ def pytest_runtest_makereport(item, call):
|
||||
|
||||
if (not hasattr(report.longrepr, 'addsection') or
|
||||
not hasattr(report, 'scenario')):
|
||||
# In some conditions (on OS X and Windows it seems), report.longrepr is
|
||||
# actually a tuple. This is handled similarily in pytest-qt too.
|
||||
# In some conditions (on macOS and Windows it seems), report.longrepr
|
||||
# is actually a tuple. This is handled similarily in pytest-qt too.
|
||||
#
|
||||
# Since this hook is invoked for any test, we also need to skip it for
|
||||
# non-BDD ones.
|
||||
|
@ -22,6 +22,20 @@ Feature: Downloading things from a website.
|
||||
And I wait until the download is finished
|
||||
Then the downloaded file download.bin should exist
|
||||
|
||||
Scenario: Using :download with no URL
|
||||
When I set storage -> prompt-download-directory to false
|
||||
And I open data/downloads/downloads.html
|
||||
And I run :download
|
||||
And I wait until the download is finished
|
||||
Then the downloaded file Simple downloads.html should exist
|
||||
|
||||
Scenario: Using :download with no URL on an image
|
||||
When I set storage -> prompt-download-directory to false
|
||||
And I open data/downloads/qutebrowser.png
|
||||
And I run :download
|
||||
And I wait until the download is finished
|
||||
Then the downloaded file qutebrowser.png should exist
|
||||
|
||||
Scenario: Using hints
|
||||
When I set downloads.location.prompt to false
|
||||
And I open data/downloads/downloads.html
|
||||
@ -579,7 +593,6 @@ Feature: Downloading things from a website.
|
||||
And I wait until the download is finished
|
||||
Then the downloaded file content-size should exist
|
||||
|
||||
@posix
|
||||
Scenario: Downloading to unwritable destination
|
||||
When I set downloads.location.prompt to false
|
||||
And I run :download http://localhost:(port)/data/downloads/download.bin --dest (tmpdir)/downloads/unwritable
|
||||
@ -637,7 +650,7 @@ Feature: Downloading things from a website.
|
||||
@qtwebengine_skip: We can't get the UA from the page there
|
||||
Scenario: user-agent when using :download
|
||||
When I open user-agent
|
||||
And I run :download
|
||||
And I run :download --dest user-agent
|
||||
And I wait until the download is finished
|
||||
Then the downloaded file user-agent should contain Safari/
|
||||
|
||||
|
@ -243,7 +243,7 @@ Feature: Using hints
|
||||
|
||||
### hints.auto_follow.timeout
|
||||
|
||||
@not_osx
|
||||
@not_mac
|
||||
Scenario: Ignoring key presses after auto-following hints
|
||||
When I set hints.auto_follow_timeout to 1000
|
||||
And I set hints.mode to number
|
||||
|
@ -11,44 +11,44 @@ Feature: Page history
|
||||
Scenario: Simple history saving
|
||||
When I open data/numbers/1.txt
|
||||
And I open data/numbers/2.txt
|
||||
Then the history file should contain:
|
||||
Then the history should contain:
|
||||
http://localhost:(port)/data/numbers/1.txt
|
||||
http://localhost:(port)/data/numbers/2.txt
|
||||
|
||||
|
||||
Scenario: History item with title
|
||||
When I open data/title.html
|
||||
Then the history file should contain:
|
||||
Then the history should contain:
|
||||
http://localhost:(port)/data/title.html Test title
|
||||
|
||||
Scenario: History item with redirect
|
||||
When I open redirect-to?url=data/title.html without waiting
|
||||
And I wait until data/title.html is loaded
|
||||
Then the history file should contain:
|
||||
Then the history should contain:
|
||||
r http://localhost:(port)/redirect-to?url=data/title.html Test title
|
||||
http://localhost:(port)/data/title.html Test title
|
||||
|
||||
|
||||
Scenario: History item with spaces in URL
|
||||
When I open data/title with spaces.html
|
||||
Then the history file should contain:
|
||||
Then the history should contain:
|
||||
http://localhost:(port)/data/title%20with%20spaces.html Test title
|
||||
|
||||
Scenario: History item with umlauts
|
||||
When I open data/äöü.html
|
||||
Then the history file should contain:
|
||||
Then the history should contain:
|
||||
http://localhost:(port)/data/%C3%A4%C3%B6%C3%BC.html Chäschüechli
|
||||
|
||||
|
||||
@flaky @qtwebengine_todo: Error page message is not implemented
|
||||
Scenario: History with an error
|
||||
When I run :open file:///does/not/exist
|
||||
And I wait for "Error while loading file:///does/not/exist: Error opening /does/not/exist: *" in the log
|
||||
Then the history file should contain:
|
||||
Then the history should contain:
|
||||
file:///does/not/exist Error loading page: file:///does/not/exist
|
||||
|
||||
@qtwebengine_todo: Error page message is not implemented
|
||||
Scenario: History with a 404
|
||||
When I open status/404 without waiting
|
||||
And I wait for "Error while loading http://localhost:*/status/404: NOT FOUND" in the log
|
||||
Then the history file should contain:
|
||||
Then the history should contain:
|
||||
http://localhost:(port)/status/404 Error loading page: http://localhost:(port)/status/404
|
||||
|
||||
Scenario: History with invalid URL
|
||||
@ -61,32 +61,32 @@ Feature: Page history
|
||||
When I open data/data_link.html
|
||||
And I run :click-element id link
|
||||
And I wait until data:;base64,cXV0ZWJyb3dzZXI= is loaded
|
||||
Then the history file should contain:
|
||||
Then the history should contain:
|
||||
http://localhost:(port)/data/data_link.html data: link
|
||||
|
||||
Scenario: History with view-source URL
|
||||
When I open data/title.html
|
||||
And I run :view-source
|
||||
And I wait for "Changing title for idx * to 'Source for http://localhost:*/data/title.html'" in the log
|
||||
Then the history file should contain:
|
||||
Then the history should contain:
|
||||
http://localhost:(port)/data/title.html Test title
|
||||
|
||||
Scenario: Clearing history
|
||||
When I open data/title.html
|
||||
And I run :history-clear --force
|
||||
Then the history file should be empty
|
||||
Then the history should be empty
|
||||
|
||||
Scenario: Clearing history with confirmation
|
||||
When I open data/title.html
|
||||
And I run :history-clear
|
||||
And I wait for "Asking question <* title='Clear all browsing history?'>, *" in the log
|
||||
And I run :prompt-accept yes
|
||||
Then the history file should be empty
|
||||
Then the history should be empty
|
||||
|
||||
Scenario: History with yanked URL and 'add to history' flag
|
||||
When I open data/hints/html/simple.html
|
||||
And I hint with args "--add-history links yank" and follow a
|
||||
Then the history file should contain:
|
||||
Then the history should contain:
|
||||
http://localhost:(port)/data/hints/html/simple.html Simple link
|
||||
http://localhost:(port)/data/hello.txt
|
||||
|
||||
|
@ -278,7 +278,7 @@ Feature: Various utility commands.
|
||||
And I run :debug-pyeval QApplication.instance().activeModalWidget().close()
|
||||
Then no crash should happen
|
||||
|
||||
# On Windows/OS X, we get a "QPrintDialog: Cannot be used on non-native
|
||||
# On Windows/macOS, we get a "QPrintDialog: Cannot be used on non-native
|
||||
# printers" qWarning.
|
||||
#
|
||||
# Disabled because it causes weird segfaults and QPainter warnings in Qt...
|
||||
@ -532,3 +532,9 @@ Feature: Various utility commands.
|
||||
And I wait for "Renderer process was killed" in the log
|
||||
And I open data/numbers/3.txt
|
||||
Then no crash should happen
|
||||
|
||||
## Other
|
||||
|
||||
Scenario: Open qute://version
|
||||
When I open qute://version
|
||||
Then the page should contain the plaintext "Version info"
|
||||
|
@ -219,14 +219,14 @@ Feature: Prompts
|
||||
And I run :click-element id button
|
||||
Then the javascript message "geolocation permission denied" should be logged
|
||||
|
||||
@ci @not_osx @qt!=5.8
|
||||
@ci @not_mac @qt!=5.8
|
||||
Scenario: Always accepting geolocation
|
||||
When I set content.geolocation to true
|
||||
And I open data/prompt/geolocation.html in a new tab
|
||||
And I run :click-element id button
|
||||
Then the javascript message "geolocation permission denied" should not be logged
|
||||
|
||||
@ci @not_osx @qt!=5.8
|
||||
@ci @not_mac @qt!=5.8
|
||||
Scenario: geolocation with ask -> true
|
||||
When I set content.geolocation to ask
|
||||
And I open data/prompt/geolocation.html in a new tab
|
||||
|
@ -60,8 +60,3 @@ Feature: :spawn
|
||||
Scenario: Running :spawn with userscript that expects the stdin getting closed
|
||||
When I run :spawn -u (testdata)/userscripts/stdinclose.py
|
||||
Then the message "stdin closed" should be shown
|
||||
|
||||
@posix
|
||||
Scenario: Running :spawn -d with userscript that expects the stdin getting closed
|
||||
When I run :spawn -d -u (testdata)/userscripts/stdinclose.py
|
||||
Then the message "stdin closed" should be shown
|
||||
|
@ -1075,6 +1075,16 @@ Feature: Tab management
|
||||
- data/numbers/2.txt (pinned)
|
||||
- data/numbers/3.txt (active)
|
||||
|
||||
Scenario: :tab-pin with an invalid count
|
||||
When I open data/numbers/1.txt
|
||||
And I open data/numbers/2.txt in a new tab
|
||||
And I open data/numbers/3.txt in a new tab
|
||||
And I run :tab-pin with count 23
|
||||
Then the following tabs should be open:
|
||||
- data/numbers/1.txt
|
||||
- data/numbers/2.txt
|
||||
- data/numbers/3.txt (active)
|
||||
|
||||
Scenario: Pinned :tab-close prompt yes
|
||||
When I open data/numbers/1.txt
|
||||
And I run :tab-pin
|
||||
|
@ -24,5 +24,5 @@ bdd.scenarios('completion.feature')
|
||||
@bdd.then(bdd.parsers.parse("the completion model should be {model}"))
|
||||
def check_model(quteproc, model):
|
||||
"""Make sure the completion model was set to something."""
|
||||
pattern = "Setting completion model to {} with pattern *".format(model)
|
||||
pattern = "Starting {} completion *".format(model)
|
||||
quteproc.wait_for(message=pattern)
|
||||
|
@ -21,6 +21,7 @@ import os
|
||||
import sys
|
||||
import shlex
|
||||
|
||||
import pytest
|
||||
import pytest_bdd as bdd
|
||||
bdd.scenarios('downloads.feature')
|
||||
|
||||
@ -53,6 +54,14 @@ def clean_old_downloads(quteproc):
|
||||
quteproc.send_cmd(':download-clear')
|
||||
|
||||
|
||||
@bdd.when("the unwritable dir is unwritable")
|
||||
def check_unwritable(tmpdir):
|
||||
unwritable = tmpdir / 'downloads' / 'unwritable'
|
||||
if os.access(str(unwritable), os.W_OK):
|
||||
# Docker container or similar
|
||||
pytest.skip("Unwritable dir was writable")
|
||||
|
||||
|
||||
@bdd.when("I wait until the download is finished")
|
||||
def wait_for_download_finished(quteproc):
|
||||
quteproc.wait_for(category='downloads', message='Download * finished')
|
||||
|
@ -17,36 +17,29 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os.path
|
||||
import logging
|
||||
import re
|
||||
|
||||
import pytest_bdd as bdd
|
||||
|
||||
bdd.scenarios('history.feature')
|
||||
|
||||
|
||||
@bdd.then(bdd.parsers.parse("the history file should contain:\n{expected}"))
|
||||
def check_history(quteproc, httpbin, expected):
|
||||
history_file = os.path.join(quteproc.basedir, 'data', 'history')
|
||||
quteproc.send_cmd(':save history')
|
||||
quteproc.wait_for(message=':save saved history')
|
||||
@bdd.then(bdd.parsers.parse("the history should contain:\n{expected}"))
|
||||
def check_history(quteproc, httpbin, tmpdir, expected):
|
||||
path = tmpdir / 'history'
|
||||
quteproc.send_cmd(':debug-dump-history "{}"'.format(path))
|
||||
quteproc.wait_for(category='message', loglevel=logging.INFO,
|
||||
message='Dumped history to {}'.format(path))
|
||||
|
||||
expected = expected.replace('(port)', str(httpbin.port)).splitlines()
|
||||
with path.open('r', encoding='utf-8') as f:
|
||||
# ignore access times, they will differ in each run
|
||||
actual = '\n'.join(re.sub('^\\d+-?', '', line).strip() for line in f)
|
||||
|
||||
with open(history_file, 'r', encoding='utf-8') as f:
|
||||
lines = []
|
||||
for line in f:
|
||||
if not line.strip():
|
||||
continue
|
||||
print('history line: ' + line)
|
||||
atime, line = line.split(' ', maxsplit=1)
|
||||
line = line.rstrip()
|
||||
if '-' in atime:
|
||||
flags = atime.split('-')[1]
|
||||
line = '{} {}'.format(flags, line)
|
||||
lines.append(line)
|
||||
|
||||
assert lines == expected
|
||||
expected = expected.replace('(port)', str(httpbin.port))
|
||||
assert actual == expected
|
||||
|
||||
|
||||
@bdd.then("the history file should be empty")
|
||||
def check_history_empty(quteproc, httpbin):
|
||||
check_history(quteproc, httpbin, '')
|
||||
@bdd.then("the history should be empty")
|
||||
def check_history_empty(quteproc, httpbin, tmpdir):
|
||||
check_history(quteproc, httpbin, tmpdir, '')
|
||||
|
@ -291,7 +291,7 @@ Feature: Yanking and pasting.
|
||||
# Compare
|
||||
Then the javascript message "textarea contents: onHello worlde two three four" should be logged
|
||||
|
||||
@qtwebengine_osx_xfail
|
||||
@qtwebengine_mac_xfail
|
||||
Scenario: Inserting text into a text field with undo
|
||||
When I set content.javascript.log to info
|
||||
And I open data/paste_primary.html
|
||||
|
@ -103,8 +103,8 @@ def pytest_runtest_makereport(item, call):
|
||||
httpbin_log = getattr(item, '_httpbin_log', None)
|
||||
|
||||
if not hasattr(report.longrepr, 'addsection'):
|
||||
# In some conditions (on OS X and Windows it seems), report.longrepr is
|
||||
# actually a tuple. This is handled similarily in pytest-qt too.
|
||||
# In some conditions (on macOS and Windows it seems), report.longrepr
|
||||
# is actually a tuple. This is handled similarily in pytest-qt too.
|
||||
return
|
||||
|
||||
if pytest.config.getoption('--capture') == 'no':
|
||||
|
@ -163,6 +163,7 @@ def test_optimize(request, quteproc_new, capfd, level):
|
||||
|
||||
|
||||
@pytest.mark.not_frozen
|
||||
@pytest.mark.flaky # Fails sometimes with empty output...
|
||||
def test_version(request):
|
||||
"""Test invocation with --version argument."""
|
||||
args = ['-m', 'qutebrowser', '--version'] + _base_args(request.config)
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user