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