Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
dadbf7657f
@ -7,11 +7,14 @@ environment:
|
||||
PYTHONUNBUFFERED: 1
|
||||
matrix:
|
||||
- TESTENV: py34
|
||||
- TESTENV: py36-pyqt58
|
||||
PYTHON: C:\Python36\python.exe
|
||||
- TESTENV: unittests-frozen
|
||||
- TESTENV: pylint
|
||||
|
||||
install:
|
||||
- C:\Python27\python -u scripts\dev\ci\appveyor_install.py
|
||||
- set PATH=%PATH%;C:\Python36
|
||||
|
||||
test_script:
|
||||
- C:\Python34\Scripts\tox -e %TESTENV%
|
||||
|
@ -38,7 +38,8 @@ disable=no-self-use,
|
||||
suppressed-message,
|
||||
too-many-return-statements,
|
||||
duplicate-code,
|
||||
wrong-import-position
|
||||
wrong-import-position,
|
||||
no-else-return
|
||||
|
||||
[BASIC]
|
||||
function-rgx=[a-z_][a-z0-9_]{2,50}$
|
||||
|
@ -21,18 +21,41 @@ Added
|
||||
~~~~~
|
||||
|
||||
- New `:clear-messages` command to clear shown messages.
|
||||
- New `ui -> keyhint-delay` setting to configure the delay until
|
||||
the keyhint overlay pops up.
|
||||
- New `-s` option for `:open` to force a HTTPS scheme.
|
||||
- `:debug-log-filter` now accepts `none` as an argument to clear any log
|
||||
filters.
|
||||
|
||||
Changed
|
||||
~~~~~~~
|
||||
|
||||
- When using QtWebEngine, the underlying Chromium version is now shown in the
|
||||
version info.
|
||||
- Improved `qute:history` page with lazy loading
|
||||
- Messages are now hidden when clicked
|
||||
- Paths like `C:` 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.
|
||||
- The HTTP cache is disabled with QtWebKit on Qt 5.8 now as it leads to frequent
|
||||
crashes due to a Qt bug.
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
|
||||
- Added a workaround for a black screen with QtWebEngine with some setups
|
||||
(requires PyOpenGL to be installed)
|
||||
(the workaround requires PyOpenGL to be installed, but it's optional)
|
||||
- Crash when trying to retry downloads with QtWebEngine
|
||||
- Crash when cloning page without history
|
||||
- Continuing a search after clearing it
|
||||
- Crash when downloading a download resulting in a HTTP error
|
||||
- Crash when pressing ctrl-c while a config error is shown
|
||||
- Crash when the key config isn't writable
|
||||
- Crash when unbinding an unbound key in the key config
|
||||
- Crash when using `:debug-log-filter` when `--filter` wasn't given on startup.
|
||||
- Various rare crashes
|
||||
|
||||
v0.10.1
|
||||
-------
|
||||
|
28
FAQ.asciidoc
28
FAQ.asciidoc
@ -124,6 +124,34 @@ When using quickmark, you can give them all names, like
|
||||
`:open foodrecipes`, you will see a list of all the food recipe sites,
|
||||
without having to remember the exact website title or address.
|
||||
|
||||
How do I use spell checking?::
|
||||
Qutebrowser's support for spell checking is somewhat limited at the moment
|
||||
(see https://github.com/qutebrowser/qutebrowser/issues/700[#700]), but it
|
||||
can be done.
|
||||
+
|
||||
For QtWebKit:
|
||||
|
||||
. Install https://github.com/QupZilla/qtwebkit-plugins[qtwebkit-plugins].
|
||||
. Note: with QtWebKit reloaded you may experience some issues. See
|
||||
https://github.com/QupZilla/qtwebkit-plugins/issues/10[#10].
|
||||
. The dictionary to use is taken from the `DICTIONARY` environment variable.
|
||||
The default is `en_US`. For example to use Dutch spell check set `DICTIONARY`
|
||||
to `nl_NL`; you can't use multiple dictionaries or change them at runtime at
|
||||
the moment.
|
||||
(also see the README file for `qtwebkit-plugins`).
|
||||
. Remember to install the hunspell dictionaries if you don't have them already
|
||||
(most distros should have packages for this).
|
||||
|
||||
+
|
||||
For QtWebEngine:
|
||||
|
||||
. Not yet supported unfortunately :-( +
|
||||
Adding it shouldn't be too hard though, since QtWebEngine 5.8 added an API for
|
||||
this (see
|
||||
https://github.com/qutebrowser/qutebrowser/issues/700#issuecomment-290780706[this
|
||||
comment for a basic example]), so what are you waiting for and why aren't you
|
||||
hacking qutebrowser yet?
|
||||
|
||||
== Troubleshooting
|
||||
|
||||
Configuration not saved after modifying config.::
|
||||
|
@ -135,12 +135,42 @@ If video or sound don't seem to work, try installing the gstreamer plugins:
|
||||
On Gentoo
|
||||
---------
|
||||
|
||||
qutebrowser is available in the main repository and can be installed with:
|
||||
A version of qutebrowser is available in the main repository and can be installed with:
|
||||
|
||||
----
|
||||
# emerge -av qutebrowser
|
||||
----
|
||||
|
||||
However it is suggested to install the Live version (-9999) to take advantage
|
||||
of the newest features introduced.
|
||||
|
||||
First of all you need to edit your package.accept_keywords file to accept the live
|
||||
version:
|
||||
|
||||
----
|
||||
# nano /etc/portage/package.accept_keywords
|
||||
----
|
||||
|
||||
And add the following line to it:
|
||||
|
||||
=www-client/qutebrowser-9999 **
|
||||
|
||||
Save the file and then install qutebrowser via
|
||||
|
||||
----
|
||||
# emerge -av qutebrowser
|
||||
----
|
||||
|
||||
Or rebuild your system if you already installed it.
|
||||
|
||||
To update to the last Live version, remember to do
|
||||
|
||||
----
|
||||
# emerge -uDNav @live-rebuild @world
|
||||
----
|
||||
|
||||
To include qutebrowser among the updates.
|
||||
|
||||
Make sure you have `python3_4` in your `PYTHON_TARGETS`
|
||||
(`/etc/portage/make.conf`) and rebuild your system (`emerge -uDNav @world`) if
|
||||
necessary.
|
||||
|
@ -71,7 +71,8 @@ https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser[mailinglist] at
|
||||
mailto:qutebrowser@lists.qutebrowser.org[].
|
||||
|
||||
There's also a https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser-announce[announce-only mailinglist]
|
||||
at mailto:qutebrowser-announce@lists.qutebrowser.org[].
|
||||
at mailto:qutebrowser-announce@lists.qutebrowser.org[] (the announcements also
|
||||
get sent to the general qutebrowser@ list).
|
||||
|
||||
Contributions / Bugs
|
||||
--------------------
|
||||
@ -152,10 +153,11 @@ Contributors, sorted by the number of commits in descending order:
|
||||
* Lamar Pavel
|
||||
* Marshall Lochbaum
|
||||
* Bruno Oliveira
|
||||
* Martin Tournoij
|
||||
* Alexander Cogneau
|
||||
* Imran Sobir
|
||||
* Felix Van der Jeugt
|
||||
* Daniel Karbach
|
||||
* Martin Tournoij
|
||||
* Kevin Velghe
|
||||
* Raphael Pierzina
|
||||
* Joel Torstensson
|
||||
@ -165,7 +167,6 @@ Contributors, sorted by the number of commits in descending order:
|
||||
* Corentin Julé
|
||||
* meles5
|
||||
* Philipp Hansch
|
||||
* Imran Sobir
|
||||
* Panagiotis Ktistakis
|
||||
* Artur Shaik
|
||||
* Nathan Isom
|
||||
@ -177,6 +178,7 @@ Contributors, sorted by the number of commits in descending order:
|
||||
* Maciej Wołczyk
|
||||
* Spreadyy
|
||||
* Alexey "Averrin" Nabrodov
|
||||
* pkill9
|
||||
* nanjekyejoannah
|
||||
* avk
|
||||
* ZDarian
|
||||
@ -205,7 +207,6 @@ Contributors, sorted by the number of commits in descending order:
|
||||
* David Vogt
|
||||
* Claire Cavanaugh
|
||||
* rikn00
|
||||
* pkill9
|
||||
* kanikaa1234
|
||||
* haitaka
|
||||
* Nick Ginther
|
||||
@ -239,6 +240,7 @@ Contributors, sorted by the number of commits in descending order:
|
||||
* adam
|
||||
* Samir Benmendil
|
||||
* Regina Hug
|
||||
* Penaz
|
||||
* Mathias Fussenegger
|
||||
* Marcelo Santos
|
||||
* Joel Bradshaw
|
||||
@ -253,6 +255,7 @@ Contributors, sorted by the number of commits in descending order:
|
||||
* haxwithaxe
|
||||
* evan
|
||||
* dylan araps
|
||||
* caveman
|
||||
* addictedtoflames
|
||||
* Xitian9
|
||||
* Vasilij Schneidermann
|
||||
@ -284,6 +287,7 @@ Contributors, sorted by the number of commits in descending order:
|
||||
* Arseniy Seroka
|
||||
* Andy Balaam
|
||||
* Andreas Fischer
|
||||
* Amos Bird
|
||||
* Akselmo
|
||||
// QUTE_AUTHORS_END
|
||||
|
||||
|
@ -544,7 +544,7 @@ For `increment` and `decrement`, the number to change the URL by. For `up`, the
|
||||
|
||||
[[open]]
|
||||
=== open
|
||||
Syntax: +:open [*--implicit*] [*--bg*] [*--tab*] [*--window*] ['url']+
|
||||
Syntax: +:open [*--implicit*] [*--bg*] [*--tab*] [*--window*] [*--secure*] ['url']+
|
||||
|
||||
Open a URL in the current/[count]th tab.
|
||||
|
||||
@ -559,6 +559,7 @@ If the URL contains newlines, each line gets opened in its own tab.
|
||||
* +*-b*+, +*--bg*+: Open in a new background tab.
|
||||
* +*-t*+, +*--tab*+: Open in a new tab.
|
||||
* +*-w*+, +*--window*+: Open in a new window.
|
||||
* +*-s*+, +*--secure*+: Force HTTPS.
|
||||
|
||||
==== count
|
||||
The tab index to open the URL in.
|
||||
@ -1598,7 +1599,8 @@ Syntax: +:debug-log-filter 'filters'+
|
||||
Change the log filter for console logging.
|
||||
|
||||
==== positional arguments
|
||||
* +'filters'+: A comma separated list of logger names.
|
||||
* +'filters'+: A comma separated list of logger names. Can also be "none" to clear any existing filters.
|
||||
|
||||
|
||||
[[debug-log-level]]
|
||||
=== debug-log-level
|
||||
|
@ -36,6 +36,7 @@
|
||||
[options="header",width="75%",cols="25%,75%"]
|
||||
|==============
|
||||
|Setting|Description
|
||||
|<<ui-history-session-interval,history-session-interval>>|The maximum time in minutes between two history items for them to be considered being from the same session. Use -1 to disable separation.
|
||||
|<<ui-zoom-levels,zoom-levels>>|The available zoom levels, separated by commas.
|
||||
|<<ui-default-zoom,default-zoom>>|The default zoom level.
|
||||
|<<ui-downloads-position,downloads-position>>|Where to show the downloaded files.
|
||||
@ -56,6 +57,7 @@
|
||||
|<<ui-modal-js-dialog,modal-js-dialog>>|Use standard JavaScript modal dialog for alert() and confirm()
|
||||
|<<ui-hide-wayland-decoration,hide-wayland-decoration>>|Hide the window decoration when using wayland (requires restart)
|
||||
|<<ui-keyhint-blacklist,keyhint-blacklist>>|Keychains that shouldn't be shown in the keyhint dialog
|
||||
|<<ui-keyhint-delay,keyhint-delay>>|Time from pressing a key to seeing the keyhint dialog (ms)
|
||||
|<<ui-prompt-radius,prompt-radius>>|The rounding radius for the edges of prompts.
|
||||
|<<ui-prompt-filebrowser,prompt-filebrowser>>|Show a filebrowser in upload/download prompts.
|
||||
|==============
|
||||
@ -536,6 +538,12 @@ Default: +pass:[path,query]+
|
||||
== ui
|
||||
General options related to the user interface.
|
||||
|
||||
[[ui-history-session-interval]]
|
||||
=== history-session-interval
|
||||
The maximum time in minutes between two history items for them to be considered being from the same session. Use -1 to disable separation.
|
||||
|
||||
Default: +pass:[30]+
|
||||
|
||||
[[ui-zoom-levels]]
|
||||
=== zoom-levels
|
||||
The available zoom levels, separated by commas.
|
||||
@ -732,6 +740,12 @@ Globs are supported, so ';*' will blacklist all keychainsstarting with ';'. Use
|
||||
|
||||
Default: empty
|
||||
|
||||
[[ui-keyhint-delay]]
|
||||
=== keyhint-delay
|
||||
Time from pressing a key to seeing the keyhint dialog (ms)
|
||||
|
||||
Default: +pass:[500]+
|
||||
|
||||
[[ui-prompt-radius]]
|
||||
=== prompt-radius
|
||||
The rounding radius for the edges of prompts.
|
||||
|
@ -3,6 +3,6 @@
|
||||
appdirs==1.4.3
|
||||
packaging==16.8
|
||||
pyparsing==2.2.0
|
||||
setuptools==34.3.2
|
||||
setuptools==34.3.3
|
||||
six==1.10.0
|
||||
wheel==0.29.0
|
||||
|
@ -2,10 +2,13 @@
|
||||
|
||||
-e git+https://github.com/PyCQA/astroid.git#egg=astroid
|
||||
editdistance==0.3.1
|
||||
github3.py==0.9.6
|
||||
isort==4.2.5
|
||||
lazy-object-proxy==1.2.2
|
||||
mccabe==0.6.1
|
||||
-e git+https://github.com/PyCQA/pylint.git#egg=pylint
|
||||
./scripts/dev/pylint_checkers
|
||||
requests==2.13.0
|
||||
uritemplate==3.0.0
|
||||
uritemplate.py==3.0.2
|
||||
wrapt==1.10.10
|
||||
|
@ -2,6 +2,7 @@
|
||||
-e git+https://github.com/PyCQA/pylint.git#egg=pylint
|
||||
./scripts/dev/pylint_checkers
|
||||
requests
|
||||
github3.py
|
||||
|
||||
# remove @commit-id for scm installs
|
||||
#@ replace: @.*# #
|
||||
|
@ -1,4 +1,4 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
PyQt5==5.8.1.1
|
||||
sip==4.19.1
|
||||
PyQt5==5.8.2
|
||||
sip==4.19.2
|
||||
|
@ -1,15 +1,15 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
beautifulsoup4==4.5.3
|
||||
cheroot==5.3.0
|
||||
cheroot==5.4.0
|
||||
click==6.7
|
||||
coverage==4.3.4
|
||||
decorator==4.0.11
|
||||
EasyProcess==0.2.3
|
||||
Flask==0.12
|
||||
Flask==0.12.1
|
||||
glob2==0.5
|
||||
httpbin==0.5.0
|
||||
hypothesis==3.6.1
|
||||
hypothesis==3.7.0
|
||||
itsdangerous==0.24
|
||||
# Jinja2==2.9.5
|
||||
Mako==1.0.6
|
||||
@ -24,7 +24,7 @@ pytest-catchlog==1.2.2
|
||||
pytest-cov==2.4.0
|
||||
pytest-faulthandler==1.3.1
|
||||
pytest-instafail==0.3.0
|
||||
pytest-mock==1.5.0
|
||||
pytest-mock==1.6.0
|
||||
pytest-qt==2.1.0
|
||||
pytest-repeat==0.4.1
|
||||
pytest-rerunfailures==2.1.0
|
||||
|
@ -2,5 +2,5 @@
|
||||
|
||||
pluggy==0.4.0
|
||||
py==1.4.33
|
||||
tox==2.6.0
|
||||
tox==2.7.0
|
||||
virtualenv==15.1.0
|
||||
|
@ -9,7 +9,7 @@ directly ask me via IRC (nickname thorsten\`) in #qutebrowser on freenode.
|
||||
|
||||
$blink!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!$reset
|
||||
WARNING: the passwords are stored in qutebrowser's
|
||||
debug log reachable via the url qute:log
|
||||
debug log reachable via the url qute://log
|
||||
$blink!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!$reset
|
||||
|
||||
Usage: run as a userscript form qutebrowser, e.g.:
|
||||
|
@ -24,11 +24,12 @@ markers =
|
||||
js_prompt: Tests needing to display a javascript prompt
|
||||
this: Used to mark tests during development
|
||||
no_invalid_lines: Don't fail on unparseable lines in end2end tests
|
||||
issue2478: Tests which are broken on Windows with QtWebEngine, https://github.com/qutebrowser/qutebrowser/issues/2478
|
||||
qt_log_level_fail = WARNING
|
||||
qt_log_ignore =
|
||||
^SpellCheck: .*
|
||||
^SetProcessDpiAwareness failed: .*
|
||||
^QWindowsWindow::setGeometryDp: Unable to set geometry .*
|
||||
^QWindowsWindow::setGeometry(Dp)?: Unable to set geometry .*
|
||||
^QProcess: Destroyed while process .* is still running\.
|
||||
^"Method "GetAll" with signature "s" on interface "org\.freedesktop\.DBus\.Properties" doesn't exist
|
||||
^"Method \\"GetAll\\" with signature \\"s\\" on interface \\"org\.freedesktop\.DBus\.Properties\\" doesn't exist\\n"
|
||||
@ -51,4 +52,5 @@ qt_log_ignore =
|
||||
^Image of format '' blocked because it is not considered safe. If you are sure it is safe to do so, you can white-list the format by setting the environment variable QTWEBKIT_IMAGEFORMAT_WHITELIST=
|
||||
^QPainter::end: Painter ended with \d+ saved states
|
||||
^QSslSocket: cannot resolve SSLv[23]_(client|server)_method
|
||||
^QQuickWidget::invalidateRenderControl could not make context current
|
||||
xfail_strict = true
|
||||
|
@ -170,12 +170,15 @@ def _init_icon():
|
||||
for size in [16, 24, 32, 48, 64, 96, 128, 256, 512]:
|
||||
filename = ':/icons/qutebrowser-{}x{}.png'.format(size, size)
|
||||
pixmap = QPixmap(filename)
|
||||
qtutils.ensure_not_null(pixmap)
|
||||
fallback_icon.addPixmap(pixmap)
|
||||
qtutils.ensure_not_null(fallback_icon)
|
||||
if pixmap.isNull():
|
||||
log.init.warning("Failed to load {}".format(filename))
|
||||
else:
|
||||
fallback_icon.addPixmap(pixmap)
|
||||
icon = QIcon.fromTheme('qutebrowser', fallback_icon)
|
||||
qtutils.ensure_not_null(icon)
|
||||
qApp.setWindowIcon(icon)
|
||||
if icon.isNull():
|
||||
log.init.warning("Failed to load icon")
|
||||
else:
|
||||
qApp.setWindowIcon(icon)
|
||||
|
||||
|
||||
def _process_args(args):
|
||||
@ -301,7 +304,7 @@ def _open_startpage(win_id=None):
|
||||
window_ids = [win_id]
|
||||
else:
|
||||
window_ids = objreg.window_registry
|
||||
for cur_win_id in window_ids:
|
||||
for cur_win_id in list(window_ids): # Copying as the dict could change
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=cur_win_id)
|
||||
if tabbed_browser.count() == 0:
|
||||
@ -340,8 +343,9 @@ def _open_quickstart(args):
|
||||
|
||||
def _save_version():
|
||||
"""Save the current version to the state config."""
|
||||
state_config = objreg.get('state-config')
|
||||
state_config['general']['version'] = qutebrowser.__version__
|
||||
state_config = objreg.get('state-config', None)
|
||||
if state_config is not None:
|
||||
state_config['general']['version'] = qutebrowser.__version__
|
||||
|
||||
|
||||
def on_focus_changed(_old, new):
|
||||
@ -647,14 +651,14 @@ class Quitter:
|
||||
self._shutting_down = True
|
||||
log.destroy.debug("Shutting down with status {}, session {}...".format(
|
||||
status, session))
|
||||
|
||||
session_manager = objreg.get('session-manager')
|
||||
if session is not None:
|
||||
session_manager.save(session, last_window=last_window,
|
||||
load_next_time=True)
|
||||
elif config.get('general', 'save-session'):
|
||||
session_manager.save(sessions.default, last_window=last_window,
|
||||
load_next_time=True)
|
||||
session_manager = objreg.get('session-manager', None)
|
||||
if session_manager is not None:
|
||||
if session is not None:
|
||||
session_manager.save(session, last_window=last_window,
|
||||
load_next_time=True)
|
||||
elif config.get('general', 'save-session'):
|
||||
session_manager.save(sessions.default, last_window=last_window,
|
||||
load_next_time=True)
|
||||
|
||||
if prompt.prompt_queue.shutdown():
|
||||
# If shutdown was called while we were asking a question, we're in
|
||||
@ -671,7 +675,7 @@ class Quitter:
|
||||
# event loop, so we can shut down immediately.
|
||||
self._shutdown(status, restart=restart)
|
||||
|
||||
def _shutdown(self, status, restart):
|
||||
def _shutdown(self, status, restart): # noqa
|
||||
"""Second stage of shutdown."""
|
||||
log.destroy.debug("Stage 2 of shutting down...")
|
||||
if qApp is None:
|
||||
@ -680,7 +684,9 @@ class Quitter:
|
||||
# Remove eventfilter
|
||||
try:
|
||||
log.destroy.debug("Removing eventfilter...")
|
||||
qApp.removeEventFilter(objreg.get('event-filter'))
|
||||
event_filter = objreg.get('event-filter', None)
|
||||
if event_filter is not None:
|
||||
qApp.removeEventFilter(event_filter)
|
||||
except AttributeError:
|
||||
pass
|
||||
# Close all windows
|
||||
@ -722,7 +728,9 @@ class Quitter:
|
||||
# Now we can hopefully quit without segfaults
|
||||
log.destroy.debug("Deferring QApplication::exit...")
|
||||
objreg.get('signal-handler').deactivate()
|
||||
objreg.get('session-manager').delete_autosave()
|
||||
session_manager = objreg.get('session-manager', None)
|
||||
if session_manager is not None:
|
||||
session_manager.delete_autosave()
|
||||
# We use a singleshot timer to exit here to minimize the likelihood of
|
||||
# segfaults.
|
||||
QTimer.singleShot(0, functools.partial(qApp.exit, status))
|
||||
|
@ -236,7 +236,7 @@ class CommandDispatcher:
|
||||
@cmdutils.argument('url', completion=usertypes.Completion.url)
|
||||
@cmdutils.argument('count', count=True)
|
||||
def openurl(self, url=None, implicit=False,
|
||||
bg=False, tab=False, window=False, count=None):
|
||||
bg=False, tab=False, window=False, count=None, secure=False):
|
||||
"""Open a URL in the current/[count]th tab.
|
||||
|
||||
If the URL contains newlines, each line gets opened in its own tab.
|
||||
@ -249,6 +249,7 @@ class CommandDispatcher:
|
||||
implicit: If opening a new tab, treat the tab as implicit (like
|
||||
clicking on a link).
|
||||
count: The tab index to open the URL in, or None.
|
||||
secure: Force HTTPS.
|
||||
"""
|
||||
if url is None:
|
||||
urls = [config.get('general', 'default-page')]
|
||||
@ -256,6 +257,8 @@ class CommandDispatcher:
|
||||
urls = self._parse_url_input(url)
|
||||
|
||||
for i, cur_url in enumerate(urls):
|
||||
if secure:
|
||||
cur_url.setScheme('https')
|
||||
if not window and i > 0:
|
||||
tab = False
|
||||
bg = True
|
||||
@ -1334,6 +1337,9 @@ class CommandDispatcher:
|
||||
scope='window', window=self._win_id)
|
||||
target = None
|
||||
if dest is not None:
|
||||
dest = downloads.transform_path(dest)
|
||||
if dest is None:
|
||||
raise cmdexc.CommandError("Invalid target filename")
|
||||
target = downloads.FileDownloadTarget(dest)
|
||||
|
||||
tab = self._current_widget()
|
||||
@ -1536,10 +1542,7 @@ class CommandDispatcher:
|
||||
backend=usertypes.Backend.QtWebKit)
|
||||
def paste_primary(self):
|
||||
"""Paste the primary selection at cursor position."""
|
||||
try:
|
||||
self.insert_text(utils.get_clipboard(selection=True))
|
||||
except utils.SelectionUnsupportedError:
|
||||
self.insert_text(utils.get_clipboard())
|
||||
self.insert_text(utils.get_clipboard(selection=True, fallback=True))
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', maxsplit=0,
|
||||
scope='window')
|
||||
@ -1650,21 +1653,22 @@ class CommandDispatcher:
|
||||
tab = self._current_widget()
|
||||
tab.search.clear()
|
||||
|
||||
if not text:
|
||||
return
|
||||
|
||||
options = {
|
||||
'ignore_case': config.get('general', 'ignore-case'),
|
||||
'reverse': reverse,
|
||||
}
|
||||
|
||||
self._tabbed_browser.search_text = text
|
||||
self._tabbed_browser.search_options = dict(options)
|
||||
|
||||
if text:
|
||||
cb = functools.partial(self._search_cb, tab=tab,
|
||||
old_scroll_pos=tab.scroller.pos_px(),
|
||||
options=options, text=text, prev=False)
|
||||
else:
|
||||
cb = None
|
||||
|
||||
cb = functools.partial(self._search_cb, tab=tab,
|
||||
old_scroll_pos=tab.scroller.pos_px(),
|
||||
options=options, text=text, prev=False)
|
||||
options['result_cb'] = cb
|
||||
|
||||
tab.search.search(text, **options)
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', hide=True,
|
||||
|
@ -19,11 +19,13 @@
|
||||
|
||||
"""Shared QtWebKit/QtWebEngine code for downloads."""
|
||||
|
||||
import re
|
||||
import sys
|
||||
import html
|
||||
import os.path
|
||||
import collections
|
||||
import functools
|
||||
import pathlib
|
||||
import tempfile
|
||||
|
||||
import sip
|
||||
@ -161,6 +163,25 @@ def get_filename_question(*, suggested_filename, url, parent=None):
|
||||
return q
|
||||
|
||||
|
||||
def transform_path(path):
|
||||
r"""Do platform-specific transformations, like changing E: to E:\.
|
||||
|
||||
Returns None if the path is invalid on the current platform.
|
||||
"""
|
||||
if sys.platform != "win32":
|
||||
return path
|
||||
path = utils.expand_windows_drive(path)
|
||||
# Drive dependent working directories are not supported, e.g.
|
||||
# E:filename is invalid
|
||||
if re.match(r'[A-Z]:[^\\]', path, re.IGNORECASE):
|
||||
return None
|
||||
# Paths like COM1, ...
|
||||
# See https://github.com/qutebrowser/qutebrowser/issues/82
|
||||
if pathlib.Path(path).is_reserved():
|
||||
return None
|
||||
return path
|
||||
|
||||
|
||||
class NoFilenameError(Exception):
|
||||
|
||||
"""Raised when we can't find out a filename in DownloadTarget."""
|
||||
@ -507,6 +528,14 @@ class AbstractDownloadItem(QObject):
|
||||
"""Retry a failed download."""
|
||||
raise NotImplementedError
|
||||
|
||||
@pyqtSlot()
|
||||
def try_retry(self):
|
||||
"""Try to retry a download and show an error if it's unsupported."""
|
||||
try:
|
||||
self.retry()
|
||||
except UnsupportedOperationError as e:
|
||||
message.error(str(e))
|
||||
|
||||
def _get_open_filename(self):
|
||||
"""Get the filename to open a download.
|
||||
|
||||
@ -968,7 +997,7 @@ class DownloadModel(QAbstractListModel):
|
||||
raise cmdexc.CommandError("No failed downloads!")
|
||||
else:
|
||||
download = to_retry[0]
|
||||
download.retry()
|
||||
download.try_retry()
|
||||
|
||||
def can_clear(self):
|
||||
"""Check if there are finished downloads to clear."""
|
||||
|
@ -134,7 +134,7 @@ class DownloadView(QListView):
|
||||
if item.successful:
|
||||
actions.append(("Open", item.open_file))
|
||||
else:
|
||||
actions.append(("Retry", item.retry))
|
||||
actions.append(("Retry", item.try_retry))
|
||||
actions.append(("Remove", item.remove))
|
||||
else:
|
||||
actions.append(("Cancel", item.cancel))
|
||||
|
@ -155,7 +155,7 @@ class PACResolver:
|
||||
raise ParseProxyError("Invalid number of parameters for PROXY")
|
||||
host, port = PACResolver._parse_proxy_host(config[1])
|
||||
return QNetworkProxy(QNetworkProxy.HttpProxy, host, port)
|
||||
elif config[0] == "SOCKS":
|
||||
elif config[0] in ["SOCKS", "SOCKS5"]:
|
||||
if len(config) != 2:
|
||||
raise ParseProxyError("Invalid number of parameters for SOCKS")
|
||||
host, port = PACResolver._parse_proxy_host(config[1])
|
||||
|
@ -110,6 +110,9 @@ class DownloadItem(downloads.AbstractDownloadItem):
|
||||
def _do_die(self):
|
||||
"""Abort the download and emit an error."""
|
||||
self._read_timer.stop()
|
||||
if self._reply is None:
|
||||
log.downloads.debug("Reply gone while dying")
|
||||
return
|
||||
self._reply.downloadProgress.disconnect()
|
||||
self._reply.finished.disconnect()
|
||||
self._reply.error.disconnect()
|
||||
@ -270,7 +273,7 @@ class DownloadItem(downloads.AbstractDownloadItem):
|
||||
if self.fileobj is None or self._reply is None:
|
||||
# No filename has been set yet (so we don't empty the buffer) or we
|
||||
# got a readyRead after the reply was finished (which happens on
|
||||
# qute:log for example).
|
||||
# qute://log for example).
|
||||
return
|
||||
if not self._reply.isOpen():
|
||||
raise OSError("Reply is closed!")
|
||||
|
@ -17,23 +17,26 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Backend-independent qute:* code.
|
||||
"""Backend-independent qute://* code.
|
||||
|
||||
Module attributes:
|
||||
pyeval_output: The output of the last :pyeval command.
|
||||
_HANDLERS: The handlers registered via decorators.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import datetime
|
||||
import urllib.parse
|
||||
import datetime
|
||||
|
||||
from PyQt5.QtCore import QUrlQuery
|
||||
from PyQt5.QtCore import QUrlQuery, QUrl
|
||||
|
||||
import qutebrowser
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import (version, utils, jinja, log, message, docutils,
|
||||
objreg)
|
||||
objreg, usertypes, qtutils)
|
||||
from qutebrowser.misc import objects
|
||||
|
||||
|
||||
@ -75,12 +78,25 @@ class QuteSchemeError(Exception):
|
||||
super().__init__(errorstring)
|
||||
|
||||
|
||||
class add_handler: # pylint: disable=invalid-name
|
||||
class Redirect(Exception):
|
||||
|
||||
"""Decorator to register a qute:* URL handler.
|
||||
"""Exception to signal a redirect should happen.
|
||||
|
||||
Attributes:
|
||||
_name: The 'foo' part of qute:foo
|
||||
url: The URL to redirect to, as a QUrl.
|
||||
"""
|
||||
|
||||
def __init__(self, url):
|
||||
super().__init__(url.toDisplayString())
|
||||
self.url = url
|
||||
|
||||
|
||||
class add_handler: # pylint: disable=invalid-name
|
||||
|
||||
"""Decorator to register a qute://* URL handler.
|
||||
|
||||
Attributes:
|
||||
_name: The 'foo' part of qute://foo
|
||||
backend: Limit which backends the handler can run with.
|
||||
"""
|
||||
|
||||
@ -103,7 +119,7 @@ class add_handler: # pylint: disable=invalid-name
|
||||
def wrong_backend_handler(self, url):
|
||||
"""Show an error page about using the invalid backend."""
|
||||
html = jinja.render('error.html',
|
||||
title="Error while opening qute:url",
|
||||
title="Error while opening qute://url",
|
||||
url=url.toDisplayString(),
|
||||
error='{} is not available with this '
|
||||
'backend'.format(url.toDisplayString()),
|
||||
@ -125,13 +141,17 @@ def data_for_url(url):
|
||||
# A url like "qute:foo" is split as "scheme:path", not "scheme:host".
|
||||
log.misc.debug("url: {}, path: {}, host {}".format(
|
||||
url.toDisplayString(), path, host))
|
||||
if path and not host:
|
||||
new_url = QUrl()
|
||||
new_url.setScheme('qute')
|
||||
new_url.setHost(path)
|
||||
raise Redirect(new_url)
|
||||
|
||||
try:
|
||||
handler = _HANDLERS[path]
|
||||
handler = _HANDLERS[host]
|
||||
except KeyError:
|
||||
try:
|
||||
handler = _HANDLERS[host]
|
||||
except KeyError:
|
||||
raise NoHandlerFound(url)
|
||||
raise NoHandlerFound(url)
|
||||
|
||||
try:
|
||||
mimetype, data = handler(url)
|
||||
except OSError as e:
|
||||
@ -150,7 +170,7 @@ def data_for_url(url):
|
||||
|
||||
@add_handler('bookmarks')
|
||||
def qute_bookmarks(_url):
|
||||
"""Handler for qute:bookmarks. Display all quickmarks / bookmarks."""
|
||||
"""Handler for qute://bookmarks. Display all quickmarks / bookmarks."""
|
||||
bookmarks = sorted(objreg.get('bookmark-manager').marks.items(),
|
||||
key=lambda x: x[1]) # Sort by title
|
||||
quickmarks = sorted(objreg.get('quickmark-manager').marks.items(),
|
||||
@ -163,90 +183,164 @@ def qute_bookmarks(_url):
|
||||
return 'text/html', html
|
||||
|
||||
|
||||
@add_handler('history') # noqa
|
||||
def qute_history(url):
|
||||
"""Handler for qute:history. Display history."""
|
||||
# Get current date from query parameter, if not given choose today.
|
||||
curr_date = datetime.date.today()
|
||||
try:
|
||||
query_date = QUrlQuery(url).queryItemValue("date")
|
||||
if query_date:
|
||||
curr_date = datetime.datetime.strptime(query_date, "%Y-%m-%d")
|
||||
curr_date = curr_date.date()
|
||||
except ValueError:
|
||||
log.misc.debug("Invalid date passed to qute:history: " + query_date)
|
||||
def history_data(start_time): # noqa
|
||||
"""Return history data
|
||||
|
||||
one_day = datetime.timedelta(days=1)
|
||||
next_date = curr_date + one_day
|
||||
prev_date = curr_date - one_day
|
||||
Arguments:
|
||||
start_time -- select history starting from this timestamp.
|
||||
"""
|
||||
def history_iter(start_time, reverse=False):
|
||||
"""Iterate through the history and get items we're interested.
|
||||
|
||||
def history_iter(reverse):
|
||||
"""Iterate through the history and get items we're interested in."""
|
||||
curr_timestamp = time.mktime(curr_date.timetuple())
|
||||
Arguments:
|
||||
reverse -- whether to reverse the history_dict before iterating.
|
||||
"""
|
||||
history = objreg.get('web-history').history_dict.values()
|
||||
if reverse:
|
||||
history = reversed(history)
|
||||
|
||||
# when history_dict is not reversed, we need to keep track of last item
|
||||
# so that we can yield its atime
|
||||
last_item = None
|
||||
|
||||
# end is 24hrs earlier than start
|
||||
end_time = start_time - 24*60*60
|
||||
|
||||
for item in history:
|
||||
# If we can't apply the reverse performance trick below,
|
||||
# at least continue as early as possible with old items.
|
||||
# This gets us down from 550ms to 123ms with 500k old items on my
|
||||
# machine.
|
||||
if item.atime < curr_timestamp and not reverse:
|
||||
continue
|
||||
|
||||
# Convert timestamp
|
||||
try:
|
||||
item_atime = datetime.datetime.fromtimestamp(item.atime)
|
||||
except (ValueError, OSError, OverflowError):
|
||||
log.misc.debug("Invalid timestamp {}.".format(item.atime))
|
||||
continue
|
||||
|
||||
if reverse and item_atime.date() < curr_date:
|
||||
# If we could reverse the history in-place, and this entry is
|
||||
# older than today, only older entries will follow, so we can
|
||||
# abort here.
|
||||
return
|
||||
|
||||
# Skip items not on curr_date
|
||||
# Skip redirects
|
||||
# Skip qute:// links
|
||||
is_internal = item.url.scheme() == 'qute'
|
||||
is_not_today = item_atime.date() != curr_date
|
||||
if item.redirect or is_internal or is_not_today:
|
||||
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
|
||||
display_atime = item_atime.strftime("%X")
|
||||
item_time = int(item.atime * 1000)
|
||||
|
||||
yield (item_url, item_title, display_atime)
|
||||
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 = list(history_iter(reverse=True))
|
||||
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(reverse=False)))
|
||||
history = reversed(list(history_iter(start_time, reverse=False)))
|
||||
|
||||
html = jinja.render('history.html',
|
||||
title='History',
|
||||
history=history,
|
||||
curr_date=curr_date,
|
||||
next_date=next_date,
|
||||
prev_date=prev_date,
|
||||
today=datetime.date.today())
|
||||
return 'text/html', html
|
||||
return list(history)
|
||||
|
||||
|
||||
@add_handler('history')
|
||||
def qute_history(url):
|
||||
"""Handler for qute://history. Display and serve history."""
|
||||
if url.path() == '/data':
|
||||
# Use start_time in query or current time.
|
||||
try:
|
||||
start_time = QUrlQuery(url).queryItemValue("start_time")
|
||||
start_time = float(start_time) if start_time else time.time()
|
||||
except ValueError as e:
|
||||
raise QuteSchemeError("Query parameter start_time is invalid", e)
|
||||
|
||||
return 'text/html', json.dumps(history_data(start_time))
|
||||
else:
|
||||
try:
|
||||
from PyQt5.QtWebKit import qWebKitVersion
|
||||
is_webkit_ng = qtutils.is_qtwebkit_ng(qWebKitVersion())
|
||||
except ImportError: # pragma: no cover
|
||||
is_webkit_ng = False
|
||||
|
||||
if (
|
||||
config.get('content', 'allow-javascript') and
|
||||
(objects.backend == usertypes.Backend.QtWebEngine or is_webkit_ng)
|
||||
):
|
||||
return 'text/html', jinja.render(
|
||||
'history.html',
|
||||
title='History',
|
||||
session_interval=config.get('ui', 'history-session-interval')
|
||||
)
|
||||
else:
|
||||
# Get current date from query parameter, if not given choose today.
|
||||
curr_date = datetime.date.today()
|
||||
try:
|
||||
query_date = QUrlQuery(url).queryItemValue("date")
|
||||
if query_date:
|
||||
curr_date = datetime.datetime.strptime(query_date,
|
||||
"%Y-%m-%d").date()
|
||||
except ValueError:
|
||||
log.misc.debug("Invalid date passed to qute:history: " +
|
||||
query_date)
|
||||
|
||||
one_day = datetime.timedelta(days=1)
|
||||
next_date = curr_date + one_day
|
||||
prev_date = curr_date - one_day
|
||||
|
||||
# start_time is the last second of curr_date
|
||||
start_time = time.mktime(next_date.timetuple()) - 1
|
||||
history = [
|
||||
(i["url"], i["title"],
|
||||
datetime.datetime.fromtimestamp(i["time"]/1000))
|
||||
for i in history_data(start_time) if "next" not in i
|
||||
]
|
||||
|
||||
return 'text/html', jinja.render(
|
||||
'history_nojs.html',
|
||||
title='History',
|
||||
history=history,
|
||||
curr_date=curr_date,
|
||||
next_date=next_date,
|
||||
prev_date=prev_date,
|
||||
today=datetime.date.today(),
|
||||
)
|
||||
|
||||
|
||||
@add_handler('javascript')
|
||||
def qute_javascript(url):
|
||||
"""Handler for qute://javascript.
|
||||
|
||||
Return content of file given as query parameter.
|
||||
"""
|
||||
path = url.path()
|
||||
if path:
|
||||
path = "javascript" + os.sep.join(path.split('/'))
|
||||
return 'text/html', utils.read_file(path, binary=False)
|
||||
else:
|
||||
raise QuteSchemeError("No file specified", ValueError())
|
||||
|
||||
|
||||
@add_handler('pyeval')
|
||||
def qute_pyeval(_url):
|
||||
"""Handler for qute:pyeval."""
|
||||
"""Handler for qute://pyeval."""
|
||||
html = jinja.render('pre.html', title='pyeval', content=pyeval_output)
|
||||
return 'text/html', html
|
||||
|
||||
@ -254,7 +348,7 @@ def qute_pyeval(_url):
|
||||
@add_handler('version')
|
||||
@add_handler('verizon')
|
||||
def qute_version(_url):
|
||||
"""Handler for qute:version."""
|
||||
"""Handler for qute://version."""
|
||||
html = jinja.render('version.html', title='Version info',
|
||||
version=version.version(),
|
||||
copyright=qutebrowser.__copyright__)
|
||||
@ -263,7 +357,7 @@ def qute_version(_url):
|
||||
|
||||
@add_handler('plainlog')
|
||||
def qute_plainlog(url):
|
||||
"""Handler for qute:plainlog.
|
||||
"""Handler for qute://plainlog.
|
||||
|
||||
An optional query parameter specifies the minimum log level to print.
|
||||
For example, qute://log?level=warning prints warnings and errors.
|
||||
@ -283,7 +377,7 @@ def qute_plainlog(url):
|
||||
|
||||
@add_handler('log')
|
||||
def qute_log(url):
|
||||
"""Handler for qute:log.
|
||||
"""Handler for qute://log.
|
||||
|
||||
An optional query parameter specifies the minimum log level to print.
|
||||
For example, qute://log?level=warning prints warnings and errors.
|
||||
@ -304,13 +398,13 @@ def qute_log(url):
|
||||
|
||||
@add_handler('gpl')
|
||||
def qute_gpl(_url):
|
||||
"""Handler for qute:gpl. Return HTML content as string."""
|
||||
"""Handler for qute://gpl. Return HTML content as string."""
|
||||
return 'text/html', utils.read_file('html/COPYING.html')
|
||||
|
||||
|
||||
@add_handler('help')
|
||||
def qute_help(url):
|
||||
"""Handler for qute:help."""
|
||||
"""Handler for qute://help."""
|
||||
try:
|
||||
utils.read_file('html/doc/index.html')
|
||||
except OSError:
|
||||
|
@ -78,24 +78,24 @@ class DownloadItem(downloads.AbstractDownloadItem):
|
||||
self.stats.finish()
|
||||
elif state == QWebEngineDownloadItem.DownloadInterrupted:
|
||||
self.successful = False
|
||||
self.done = True
|
||||
# https://bugreports.qt.io/browse/QTBUG-56839
|
||||
self.error.emit("Download failed")
|
||||
self.stats.finish()
|
||||
self._die("Download failed")
|
||||
else:
|
||||
raise ValueError("_on_state_changed was called with unknown state "
|
||||
"{}".format(state_name))
|
||||
|
||||
def _do_die(self):
|
||||
self._qt_item.downloadProgress.disconnect()
|
||||
self._qt_item.cancel()
|
||||
if self._qt_item.state() != QWebEngineDownloadItem.DownloadInterrupted:
|
||||
self._qt_item.cancel()
|
||||
|
||||
def _do_cancel(self):
|
||||
self._qt_item.cancel()
|
||||
|
||||
def retry(self):
|
||||
# https://bugreports.qt.io/browse/QTBUG-56840
|
||||
raise downloads.UnsupportedOperationError
|
||||
raise downloads.UnsupportedOperationError(
|
||||
"Retrying downloads is unsupported with QtWebEngine")
|
||||
|
||||
def _get_open_filename(self):
|
||||
return self._filename
|
||||
@ -104,6 +104,7 @@ class DownloadItem(downloads.AbstractDownloadItem):
|
||||
raise downloads.UnsupportedOperationError
|
||||
|
||||
def _set_tempfile(self, fileobj):
|
||||
fileobj.close()
|
||||
self._set_filename(fileobj.name, force_overwrite=True,
|
||||
remember_directory=False)
|
||||
|
||||
|
@ -17,7 +17,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""QtWebEngine specific qute:* handlers and glue code."""
|
||||
"""QtWebEngine specific qute://* handlers and glue code."""
|
||||
|
||||
from PyQt5.QtCore import QBuffer, QIODevice
|
||||
# pylint: disable=no-name-in-module,import-error,useless-suppression
|
||||
@ -26,15 +26,15 @@ from PyQt5.QtWebEngineCore import (QWebEngineUrlSchemeHandler,
|
||||
# pylint: enable=no-name-in-module,import-error,useless-suppression
|
||||
|
||||
from qutebrowser.browser import qutescheme
|
||||
from qutebrowser.utils import log
|
||||
from qutebrowser.utils import log, qtutils
|
||||
|
||||
|
||||
class QuteSchemeHandler(QWebEngineUrlSchemeHandler):
|
||||
|
||||
"""Handle qute:* requests on QtWebEngine."""
|
||||
"""Handle qute://* requests on QtWebEngine."""
|
||||
|
||||
def install(self, profile):
|
||||
"""Install the handler for qute: URLs on the given profile."""
|
||||
"""Install the handler for qute:// URLs on the given profile."""
|
||||
profile.installUrlSchemeHandler(b'qute', self)
|
||||
|
||||
def requestStarted(self, job):
|
||||
@ -58,12 +58,15 @@ class QuteSchemeHandler(QWebEngineUrlSchemeHandler):
|
||||
job.fail(QWebEngineUrlRequestJob.UrlNotFound)
|
||||
except qutescheme.QuteSchemeOSError:
|
||||
# FIXME:qtwebengine how do we show a better error here?
|
||||
log.misc.exception("OSError while handling qute:* URL")
|
||||
log.misc.exception("OSError while handling qute://* URL")
|
||||
job.fail(QWebEngineUrlRequestJob.UrlNotFound)
|
||||
except qutescheme.QuteSchemeError:
|
||||
# FIXME:qtwebengine how do we show a better error here?
|
||||
log.misc.exception("Error while handling qute:* URL")
|
||||
log.misc.exception("Error while handling qute://* URL")
|
||||
job.fail(QWebEngineUrlRequestJob.RequestFailed)
|
||||
except qutescheme.Redirect as e:
|
||||
qtutils.ensure_valid(e.url)
|
||||
job.redirect(e.url)
|
||||
else:
|
||||
log.misc.debug("Returning {} data".format(mimetype))
|
||||
|
||||
|
@ -55,7 +55,7 @@ def init():
|
||||
app = QApplication.instance()
|
||||
profile = QWebEngineProfile.defaultProfile()
|
||||
|
||||
log.init.debug("Initializing qute:* handler...")
|
||||
log.init.debug("Initializing qute://* handler...")
|
||||
_qute_scheme_handler = webenginequtescheme.QuteSchemeHandler(parent=app)
|
||||
_qute_scheme_handler.install(profile)
|
||||
|
||||
@ -369,8 +369,13 @@ class WebEngineHistory(browsertab.AbstractHistory):
|
||||
return self._history.canGoForward()
|
||||
|
||||
def serialize(self):
|
||||
# WORKAROUND for https://github.com/qutebrowser/qutebrowser/issues/2289
|
||||
# FIXME:qtwebengine can we get rid of this with Qt 5.8.1?
|
||||
# WORKAROUND (remove this when we bump the requirements to 5.9)
|
||||
# https://bugreports.qt.io/browse/QTBUG-59599
|
||||
if self._history.count() == 0:
|
||||
raise browsertab.WebTabError("Can't serialize page without "
|
||||
"history!")
|
||||
# WORKAROUND (FIXME: remove this when we bump the requirements to 5.9?)
|
||||
# https://github.com/qutebrowser/qutebrowser/issues/2289
|
||||
scheme = self._history.currentItem().url().scheme()
|
||||
if scheme in ['view-source', 'chrome']:
|
||||
raise browsertab.WebTabError("Can't serialize special URL!")
|
||||
|
@ -25,7 +25,7 @@ from PyQt5.QtCore import pyqtSlot
|
||||
from PyQt5.QtNetwork import QNetworkDiskCache, QNetworkCacheMetaData
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import utils, objreg
|
||||
from qutebrowser.utils import utils, objreg, qtutils
|
||||
|
||||
|
||||
class DiskCache(QNetworkDiskCache):
|
||||
@ -53,6 +53,9 @@ class DiskCache(QNetworkDiskCache):
|
||||
size = config.get('storage', 'cache-size')
|
||||
if size is None:
|
||||
size = 1024 * 1024 * 50 # default from QNetworkDiskCachePrivate
|
||||
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-59909
|
||||
if qtutils.version_check('5.7.1'): # pragma: no cover
|
||||
size = 0
|
||||
self.setMaximumCacheSize(size)
|
||||
|
||||
def _maybe_activate(self):
|
||||
|
@ -19,6 +19,10 @@
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# For some reason, a segfault will be triggered if the unnecessary lambdas in
|
||||
# this file aren't there.
|
||||
# pylint: disable=unnecessary-lambda
|
||||
|
||||
"""Special network replies.."""
|
||||
|
||||
@ -114,9 +118,6 @@ class ErrorNetworkReply(QNetworkReply):
|
||||
# the device to avoid getting a warning.
|
||||
self.setOpenMode(QIODevice.ReadOnly)
|
||||
self.setError(error, errorstring)
|
||||
# For some reason, a segfault will be triggered if these lambdas aren't
|
||||
# there.
|
||||
# pylint: disable=unnecessary-lambda
|
||||
QTimer.singleShot(0, lambda: self.error.emit(error))
|
||||
QTimer.singleShot(0, lambda: self.finished.emit())
|
||||
|
||||
@ -137,3 +138,16 @@ class ErrorNetworkReply(QNetworkReply):
|
||||
|
||||
def isRunning(self):
|
||||
return False
|
||||
|
||||
|
||||
class RedirectNetworkReply(QNetworkReply):
|
||||
|
||||
"""A reply which redirects to the given URL."""
|
||||
|
||||
def __init__(self, new_url, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setAttribute(QNetworkRequest.RedirectionTargetAttribute, new_url)
|
||||
QTimer.singleShot(0, lambda: self.finished.emit())
|
||||
|
||||
def readData(self, _maxlen):
|
||||
return bytes()
|
||||
|
@ -17,7 +17,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""QtWebKit specific qute:* handlers and glue code."""
|
||||
"""QtWebKit specific qute://* handlers and glue code."""
|
||||
|
||||
import mimetypes
|
||||
import functools
|
||||
@ -28,13 +28,13 @@ from PyQt5.QtNetwork import QNetworkReply
|
||||
|
||||
from qutebrowser.browser import pdfjs, qutescheme
|
||||
from qutebrowser.browser.webkit.network import schemehandler, networkreply
|
||||
from qutebrowser.utils import jinja, log, message, objreg, usertypes
|
||||
from qutebrowser.utils import jinja, log, message, objreg, usertypes, qtutils
|
||||
from qutebrowser.config import configexc, configdata
|
||||
|
||||
|
||||
class QuteSchemeHandler(schemehandler.SchemeHandler):
|
||||
|
||||
"""Scheme handler for qute: URLs."""
|
||||
"""Scheme handler for qute:// URLs."""
|
||||
|
||||
def createRequest(self, _op, request, _outgoing_data):
|
||||
"""Create a new request.
|
||||
@ -62,6 +62,9 @@ class QuteSchemeHandler(schemehandler.SchemeHandler):
|
||||
except qutescheme.QuteSchemeError as e:
|
||||
return networkreply.ErrorNetworkReply(request, e.errorstring,
|
||||
e.error, self.parent())
|
||||
except qutescheme.Redirect as e:
|
||||
qtutils.ensure_valid(e.url)
|
||||
return networkreply.RedirectNetworkReply(e.url, self.parent())
|
||||
|
||||
return networkreply.FixedDataNetworkReply(request, data, mimetype,
|
||||
self.parent())
|
||||
@ -69,15 +72,15 @@ class QuteSchemeHandler(schemehandler.SchemeHandler):
|
||||
|
||||
class JSBridge(QObject):
|
||||
|
||||
"""Javascript-bridge for special qute:... pages."""
|
||||
"""Javascript-bridge for special qute://... pages."""
|
||||
|
||||
@pyqtSlot(str, str, str)
|
||||
def set(self, sectname, optname, value):
|
||||
"""Slot to set a setting from qute:settings."""
|
||||
"""Slot to set a setting from qute://settings."""
|
||||
# https://github.com/qutebrowser/qutebrowser/issues/727
|
||||
if ((sectname, optname) == ('content', 'allow-javascript') and
|
||||
value == 'false'):
|
||||
message.error("Refusing to disable javascript via qute:settings "
|
||||
message.error("Refusing to disable javascript via qute://settings "
|
||||
"as it needs javascript support.")
|
||||
return
|
||||
try:
|
||||
@ -88,7 +91,7 @@ class JSBridge(QObject):
|
||||
|
||||
@qutescheme.add_handler('settings', backend=usertypes.Backend.QtWebKit)
|
||||
def qute_settings(_url):
|
||||
"""Handler for qute:settings. View/change qute configuration."""
|
||||
"""Handler for qute://settings. View/change qute configuration."""
|
||||
config_getter = functools.partial(objreg.get('config').get, raw=True)
|
||||
html = jinja.render('settings.html', title='settings', config=configdata,
|
||||
confget=config_getter)
|
||||
|
@ -23,6 +23,7 @@ import sys
|
||||
import functools
|
||||
import xml.etree.ElementTree
|
||||
|
||||
import sip
|
||||
from PyQt5.QtCore import (pyqtSlot, Qt, QEvent, QUrl, QPoint, QTimer, QSizeF,
|
||||
QSize)
|
||||
from PyQt5.QtGui import QKeyEvent
|
||||
@ -707,6 +708,9 @@ class WebKitTab(browsertab.AbstractTab):
|
||||
@pyqtSlot()
|
||||
def _on_webkit_icon_changed(self):
|
||||
"""Emit iconChanged with a QIcon like QWebEngineView does."""
|
||||
if sip.isdeleted(self._widget):
|
||||
log.webview.debug("Got _on_webkit_icon_changed for deleted view!")
|
||||
return
|
||||
self.icon_changed.emit(self._widget.icon())
|
||||
|
||||
@pyqtSlot(QWebFrame)
|
||||
|
@ -140,7 +140,7 @@ class WebView(QWebView):
|
||||
|
||||
@pyqtSlot()
|
||||
def add_js_bridge(self):
|
||||
"""Add the javascript bridge for qute:... pages."""
|
||||
"""Add the javascript bridge for qute://... pages."""
|
||||
frame = self.sender()
|
||||
if not isinstance(frame, QWebFrame):
|
||||
log.webview.error("Got non-QWebFrame {!r} in "
|
||||
|
@ -133,7 +133,8 @@ class CommandRunner(QObject):
|
||||
Yields:
|
||||
ParseResult tuples.
|
||||
"""
|
||||
if not text.strip():
|
||||
text = text.strip().lstrip(':').strip()
|
||||
if not text:
|
||||
raise cmdexc.NoSuchCommandError("No command given")
|
||||
|
||||
if aliases:
|
||||
|
@ -186,7 +186,8 @@ def _init_key_config(parent):
|
||||
key_config = keyconf.KeyConfigParser(standarddir.config(), 'keys.conf',
|
||||
args.relaxed_config,
|
||||
parent=parent)
|
||||
except (keyconf.KeyConfigError, UnicodeDecodeError) as e:
|
||||
except (keyconf.KeyConfigError, cmdexc.CommandError,
|
||||
UnicodeDecodeError) as e:
|
||||
log.init.exception(e)
|
||||
errstr = "Error while reading key config:\n"
|
||||
if e.lineno is not None:
|
||||
@ -471,10 +472,9 @@ class ConfigManager(QObject):
|
||||
"""Get the whole config as a string."""
|
||||
lines = configdata.FIRST_COMMENT.strip('\n').splitlines()
|
||||
for sectname, sect in self.sections.items():
|
||||
lines.append('\n[{}]'.format(sectname))
|
||||
lines += self._str_section_desc(sectname)
|
||||
lines += self._str_option_desc(sectname, sect)
|
||||
lines += self._str_items(sect)
|
||||
lines += ['\n'] + self._str_section_desc(sectname)
|
||||
lines.append('[{}]'.format(sectname))
|
||||
lines += self._str_items(sectname, sect)
|
||||
return '\n'.join(lines) + '\n'
|
||||
|
||||
def _str_section_desc(self, sectname):
|
||||
@ -489,42 +489,7 @@ class ConfigManager(QObject):
|
||||
lines += wrapper.wrap(secline)
|
||||
return lines
|
||||
|
||||
def _str_option_desc(self, sectname, sect):
|
||||
"""Get the option description strings for sect/sectname."""
|
||||
wrapper = textwrapper.TextWrapper(initial_indent='#' + ' ' * 5,
|
||||
subsequent_indent='#' + ' ' * 5)
|
||||
lines = []
|
||||
if not getattr(sect, 'descriptions', None):
|
||||
return lines
|
||||
|
||||
for optname, option in sect.items():
|
||||
|
||||
lines.append('#')
|
||||
typestr = ' ({})'.format(option.typ.get_name())
|
||||
lines.append("# {}{}:".format(optname, typestr))
|
||||
|
||||
try:
|
||||
desc = self.sections[sectname].descriptions[optname]
|
||||
except KeyError:
|
||||
log.config.exception("No description for {}.{}!".format(
|
||||
sectname, optname))
|
||||
continue
|
||||
for descline in desc.splitlines():
|
||||
lines += wrapper.wrap(descline)
|
||||
valid_values = option.typ.get_valid_values()
|
||||
if valid_values is not None:
|
||||
if valid_values.descriptions:
|
||||
for val in valid_values:
|
||||
desc = valid_values.descriptions[val]
|
||||
lines += wrapper.wrap(" {}: {}".format(val, desc))
|
||||
else:
|
||||
lines += wrapper.wrap("Valid values: {}".format(', '.join(
|
||||
valid_values)))
|
||||
lines += wrapper.wrap("Default: {}".format(
|
||||
option.values['default']))
|
||||
return lines
|
||||
|
||||
def _str_items(self, sect):
|
||||
def _str_items(self, sectname, sect):
|
||||
"""Get the option items as string for sect."""
|
||||
lines = []
|
||||
for optname, option in sect.items():
|
||||
@ -535,9 +500,43 @@ class ConfigManager(QObject):
|
||||
# configparser can't handle = in keys :(
|
||||
optname = optname.replace('=', '<eq>')
|
||||
keyval = '{} = {}'.format(optname, value)
|
||||
lines += self._str_option_desc(sectname, sect, optname, option)
|
||||
lines.append(keyval)
|
||||
return lines
|
||||
|
||||
def _str_option_desc(self, sectname, sect, optname, option):
|
||||
"""Get the option description strings for a single option."""
|
||||
wrapper = textwrapper.TextWrapper(initial_indent='#' + ' ' * 5,
|
||||
subsequent_indent='#' + ' ' * 5)
|
||||
lines = []
|
||||
if not getattr(sect, 'descriptions', None):
|
||||
return lines
|
||||
|
||||
lines.append('')
|
||||
typestr = ' ({})'.format(option.typ.get_name())
|
||||
lines.append("# {}{}:".format(optname, typestr))
|
||||
|
||||
try:
|
||||
desc = self.sections[sectname].descriptions[optname]
|
||||
except KeyError:
|
||||
log.config.exception("No description for {}.{}!".format(
|
||||
sectname, optname))
|
||||
return []
|
||||
for descline in desc.splitlines():
|
||||
lines += wrapper.wrap(descline)
|
||||
valid_values = option.typ.get_valid_values()
|
||||
if valid_values is not None:
|
||||
if valid_values.descriptions:
|
||||
for val in valid_values:
|
||||
desc = valid_values.descriptions[val]
|
||||
lines += wrapper.wrap(" {}: {}".format(val, desc))
|
||||
else:
|
||||
lines += wrapper.wrap("Valid values: {}".format(', '.join(
|
||||
valid_values)))
|
||||
lines += wrapper.wrap("Default: {}".format(
|
||||
option.values['default']))
|
||||
return lines
|
||||
|
||||
def _get_real_sectname(self, cp, sectname):
|
||||
"""Get an old or new section name based on a configparser.
|
||||
|
||||
@ -806,7 +805,7 @@ class ConfigManager(QObject):
|
||||
if section_ is None and option is None:
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=win_id)
|
||||
tabbed_browser.openurl(QUrl('qute:settings'), newtab=False)
|
||||
tabbed_browser.openurl(QUrl('qute://settings'), newtab=False)
|
||||
return
|
||||
|
||||
if option.endswith('?') and option != '?':
|
||||
|
@ -292,6 +292,12 @@ def data(readonly=False):
|
||||
)),
|
||||
|
||||
('ui', sect.KeyValue(
|
||||
('history-session-interval',
|
||||
SettingValue(typ.Int(), '30'),
|
||||
"The maximum time in minutes between two history items for them "
|
||||
"to be considered being from the same session. Use -1 to "
|
||||
"disable separation."),
|
||||
|
||||
('zoom-levels',
|
||||
SettingValue(typ.List(typ.Perc(minval=0)),
|
||||
'25%,33%,50%,67%,75%,90%,100%,110%,125%,150%,175%,'
|
||||
@ -400,6 +406,10 @@ def data(readonly=False):
|
||||
"Globs are supported, so ';*' will blacklist all keychains"
|
||||
"starting with ';'. Use '*' to disable keyhints"),
|
||||
|
||||
('keyhint-delay',
|
||||
SettingValue(typ.Int(minval=0), '500'),
|
||||
"Time from pressing a key to seeing the keyhint dialog (ms)"),
|
||||
|
||||
('prompt-radius',
|
||||
SettingValue(typ.Int(minval=0), '8'),
|
||||
"The rounding radius for the edges of prompts."),
|
||||
@ -1679,7 +1689,7 @@ KEY_DATA = collections.OrderedDict([
|
||||
('home', ['<Ctrl-h>']),
|
||||
('stop', ['<Ctrl-s>']),
|
||||
('print', ['<Ctrl-Alt-p>']),
|
||||
('open qute:settings', ['Ss']),
|
||||
('open qute://settings', ['Ss']),
|
||||
('follow-selected', RETURN_KEYS),
|
||||
('follow-selected -t', ['<Ctrl-Return>', '<Ctrl-Enter>']),
|
||||
('repeat-command', ['.']),
|
||||
|
@ -142,9 +142,14 @@ class KeyConfigParser(QObject):
|
||||
def save(self):
|
||||
"""Save the key config file."""
|
||||
log.destroy.debug("Saving key config to {}".format(self._configfile))
|
||||
with qtutils.savefile_open(self._configfile, encoding='utf-8') as f:
|
||||
data = str(self)
|
||||
f.write(data)
|
||||
|
||||
try:
|
||||
with qtutils.savefile_open(self._configfile,
|
||||
encoding='utf-8') as f:
|
||||
data = str(self)
|
||||
f.write(data)
|
||||
except OSError as e:
|
||||
message.error("Could not save key config: {}".format(e))
|
||||
|
||||
@cmdutils.register(instance='key-config', maxsplit=1, no_cmd_split=True,
|
||||
no_replace_variables=True)
|
||||
@ -252,6 +257,7 @@ class KeyConfigParser(QObject):
|
||||
"""
|
||||
# {'sectname': {'keychain1': 'command', 'keychain2': 'command'}, ...}
|
||||
bindings_to_add = collections.OrderedDict()
|
||||
mark_dirty = False
|
||||
|
||||
for sectname, sect in configdata.KEY_DATA.items():
|
||||
sectname = self._normalize_sectname(sectname)
|
||||
@ -261,6 +267,7 @@ class KeyConfigParser(QObject):
|
||||
if not only_new or self._is_new(sectname, command, e):
|
||||
assert e not in bindings_to_add[sectname]
|
||||
bindings_to_add[sectname][e] = command
|
||||
mark_dirty = True
|
||||
|
||||
for sectname, sect in bindings_to_add.items():
|
||||
if not sect:
|
||||
@ -271,7 +278,7 @@ class KeyConfigParser(QObject):
|
||||
self._add_binding(sectname, keychain, command)
|
||||
self.changed.emit(sectname)
|
||||
|
||||
if bindings_to_add:
|
||||
if mark_dirty:
|
||||
self._mark_config_dirty()
|
||||
|
||||
def _is_new(self, sectname, command, keychain):
|
||||
@ -315,7 +322,7 @@ class KeyConfigParser(QObject):
|
||||
else:
|
||||
line = line.strip()
|
||||
self._read_command(line)
|
||||
except KeyConfigError as e:
|
||||
except (KeyConfigError, cmdexc.CommandError) as e:
|
||||
if relaxed:
|
||||
continue
|
||||
else:
|
||||
|
@ -16,43 +16,62 @@ td.time {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
table {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.date {
|
||||
color: #888;
|
||||
font-size: 14pt;
|
||||
padding-left: 25px;
|
||||
}
|
||||
|
||||
.pagination-link {
|
||||
display: inline-block;
|
||||
margin-bottom: 10px;
|
||||
margin-top: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.pagination-link > a {
|
||||
color: #333;
|
||||
color: #555;
|
||||
font-size: 12pt;
|
||||
padding-bottom: 15px;
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
}
|
||||
{% endblock %}
|
||||
|
||||
#load {
|
||||
color: #555;
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#eof {
|
||||
color: #aaa;
|
||||
margin-bottom: 30px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.session-separator {
|
||||
color: #aaa;
|
||||
height: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<h1>Browsing history</h1>
|
||||
<div id="hist-container"></div>
|
||||
<span id="eof" style="display: none">end</span>
|
||||
<a href="#" id="load" style="display: none">Show more</a>
|
||||
<script type="text/javascript" src="qute://javascript/history.js"></script>
|
||||
<script type="text/javascript">
|
||||
window.SESSION_INTERVAL = {{session_interval}} * 60 * 1000;
|
||||
|
||||
<h1>Browsing history <span class="date">{{curr_date.strftime("%a, %d %B %Y")}}</span></h1>
|
||||
window.onload = function() {
|
||||
var loadLink = document.getElementById('load');
|
||||
loadLink.style.display = null;
|
||||
loadLink.addEventListener('click', function(ev) {
|
||||
ev.preventDefault();
|
||||
window.loadHistory();
|
||||
});
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
{% for url, title, time in history %}
|
||||
<tr>
|
||||
<td class="title"><a href="{{url}}">{{title}}</a></td>
|
||||
<td class="time">{{time}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<span class="pagination-link"><a href="qute://history/?date={{prev_date.strftime("%Y-%m-%d")}}" rel="prev">Previous</a></span>
|
||||
{% if today >= next_date %}
|
||||
<span class="pagination-link"><a href="qute://history/?date={{next_date.strftime("%Y-%m-%d")}}" rel="next">Next</a></span>
|
||||
{% endif %}
|
||||
window.onscroll = function(ev) {
|
||||
if ((window.innerHeight + window.scrollY) >= document.body.scrollHeight) {
|
||||
window.loadHistory();
|
||||
}
|
||||
};
|
||||
|
||||
window.loadHistory();
|
||||
};
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
58
qutebrowser/html/history_nojs.html
Normal file
58
qutebrowser/html/history_nojs.html
Normal file
@ -0,0 +1,58 @@
|
||||
{% extends "styled.html" %}
|
||||
|
||||
{% block style %}
|
||||
{{super()}}
|
||||
body {
|
||||
max-width: 1440px;
|
||||
}
|
||||
|
||||
td.title {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
td.time {
|
||||
color: #555;
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
table {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.date {
|
||||
color: #555;
|
||||
font-size: 12pt;
|
||||
padding-bottom: 15px;
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.pagination-link {
|
||||
color: #555;
|
||||
font-weight: bold;
|
||||
margn-bottom: 15px;
|
||||
text-decoration: none;
|
||||
}
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<h1>Browsing history</h1>
|
||||
|
||||
<table>
|
||||
<caption class="date">{{curr_date.strftime("%a, %d %B %Y")}}</caption>
|
||||
<tbody>
|
||||
{% for url, title, time in history %}
|
||||
<tr>
|
||||
<td class="title"><a href="{{url}}">{{title}}</a></td>
|
||||
<td class="time">{{time.strftime("%X")}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<span class="pagination-link"><a href="qute://history/?date={{prev_date.strftime("%Y-%m-%d")}}" rel="prev">Previous</a></span>
|
||||
{% if today >= next_date %}
|
||||
<span class="pagination-link"><a href="qute://history/?date={{next_date.strftime("%Y-%m-%d")}}" rel="next">Next</a></span>
|
||||
{% endif %}
|
||||
{% endblock %}
|
190
qutebrowser/javascript/history.js
Normal file
190
qutebrowser/javascript/history.js
Normal file
@ -0,0 +1,190 @@
|
||||
/**
|
||||
* Copyright 2017 Imran Sobir
|
||||
*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
window.loadHistory = (function() {
|
||||
// Date of last seen item.
|
||||
var lastItemDate = null;
|
||||
|
||||
// The time to load next.
|
||||
var nextTime = null;
|
||||
|
||||
// The URL to fetch data from.
|
||||
var DATA_URL = "qute://history/data";
|
||||
|
||||
// Various fixed elements
|
||||
var EOF_MESSAGE = document.getElementById("eof");
|
||||
var LOAD_LINK = document.getElementById("load");
|
||||
var HIST_CONTAINER = document.getElementById("hist-container");
|
||||
|
||||
/**
|
||||
* Finds or creates the session table>tbody to which item with given date
|
||||
* should be added.
|
||||
*
|
||||
* @param {Date} date - the date of the item being added.
|
||||
* @returns {Element} the element to which new rows should be added.
|
||||
*/
|
||||
function getSessionNode(date) {
|
||||
// Find/create table
|
||||
var tableId = ["hist", date.getDate(), date.getMonth(),
|
||||
date.getYear()].join("-");
|
||||
var table = document.getElementById(tableId);
|
||||
if (table === null) {
|
||||
table = document.createElement("table");
|
||||
table.id = tableId;
|
||||
|
||||
// Caption contains human-readable date
|
||||
var caption = document.createElement("caption");
|
||||
caption.className = "date";
|
||||
var options = {
|
||||
"weekday": "long",
|
||||
"year": "numeric",
|
||||
"month": "long",
|
||||
"day": "numeric",
|
||||
};
|
||||
caption.innerHTML = date.toLocaleDateString("en-US", options);
|
||||
table.appendChild(caption);
|
||||
|
||||
// Add table to page
|
||||
HIST_CONTAINER.appendChild(table);
|
||||
}
|
||||
|
||||
// Find/create tbody
|
||||
var tbody = table.lastChild;
|
||||
if (tbody.tagName !== "TBODY") {
|
||||
tbody = document.createElement("tbody");
|
||||
table.appendChild(tbody);
|
||||
}
|
||||
|
||||
// Create session-separator and new tbody if necessary
|
||||
if (tbody.lastChild !== null && lastItemDate !== null &&
|
||||
window.SESSION_INTERVAL > 0) {
|
||||
var interval = lastItemDate.getTime() - date.getTime();
|
||||
if (interval > window.SESSION_INTERVAL) {
|
||||
// Add session-separator
|
||||
var sessionSeparator = document.createElement("td");
|
||||
sessionSeparator.className = "session-separator";
|
||||
sessionSeparator.colSpan = 2;
|
||||
sessionSeparator.innerHTML = "§";
|
||||
table.appendChild(document.createElement("tr"));
|
||||
table.lastChild.appendChild(sessionSeparator);
|
||||
|
||||
// Create new tbody
|
||||
tbody = document.createElement("tbody");
|
||||
table.appendChild(tbody);
|
||||
}
|
||||
}
|
||||
|
||||
return tbody;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a history item, create and return <tr> for it.
|
||||
*
|
||||
* @param {string} itemUrl - The url for this item.
|
||||
* @param {string} itemTitle - The title for this item.
|
||||
* @param {string} itemTime - The formatted time for this item.
|
||||
* @returns {Element} the completed tr.
|
||||
*/
|
||||
function makeHistoryRow(itemUrl, itemTitle, itemTime) {
|
||||
var row = document.createElement("tr");
|
||||
|
||||
var title = document.createElement("td");
|
||||
title.className = "title";
|
||||
var link = document.createElement("a");
|
||||
link.href = itemUrl;
|
||||
link.innerHTML = itemTitle;
|
||||
title.appendChild(link);
|
||||
|
||||
var time = document.createElement("td");
|
||||
time.className = "time";
|
||||
time.innerHTML = itemTime;
|
||||
|
||||
row.appendChild(title);
|
||||
row.appendChild(time);
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get JSON from given URL.
|
||||
*
|
||||
* @param {string} url - the url to fetch data from.
|
||||
* @param {function} callback - the function to callback with data.
|
||||
* @returns {void}
|
||||
*/
|
||||
function getJSON(url, callback) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open("GET", url, true);
|
||||
xhr.responseType = "json";
|
||||
xhr.onload = function() {
|
||||
var status = xhr.status;
|
||||
callback(status, xhr.response);
|
||||
};
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
/**
|
||||
* Receive history data from qute://history/data.
|
||||
*
|
||||
* @param {Number} status - The status of the query.
|
||||
* @param {Array} history - History data.
|
||||
* @returns {void}
|
||||
*/
|
||||
function receiveHistory(status, history) {
|
||||
if (history === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0, len = history.length - 1; i < len; i++) {
|
||||
var item = history[i];
|
||||
var currentItemDate = new Date(item.time);
|
||||
getSessionNode(currentItemDate).appendChild(makeHistoryRow(
|
||||
item.url, item.title, currentItemDate.toLocaleTimeString()
|
||||
));
|
||||
lastItemDate = currentItemDate;
|
||||
}
|
||||
|
||||
var next = history[history.length - 1].next;
|
||||
if (next === -1) {
|
||||
// Reached end of history
|
||||
window.onscroll = null;
|
||||
EOF_MESSAGE.style.display = "block";
|
||||
LOAD_LINK.style.display = "none";
|
||||
} else {
|
||||
nextTime = next;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load new history.
|
||||
* @return {void}
|
||||
*/
|
||||
function loadHistory() {
|
||||
if (nextTime === null) {
|
||||
getJSON(DATA_URL, receiveHistory);
|
||||
} else {
|
||||
var url = DATA_URL.concat("?start_time=", nextTime.toString());
|
||||
getJSON(url, receiveHistory);
|
||||
}
|
||||
}
|
||||
|
||||
return loadHistory;
|
||||
})();
|
@ -184,8 +184,6 @@ class MainWindow(QWidget):
|
||||
|
||||
self._keyhint = keyhintwidget.KeyHintView(self.win_id, self)
|
||||
self._add_overlay(self._keyhint, self._keyhint.update_geometry)
|
||||
self._messageview = messageview.MessageView(parent=self)
|
||||
self._add_overlay(self._messageview, self._messageview.update_geometry)
|
||||
|
||||
self._prompt_container = prompt.PromptContainer(self.win_id, self)
|
||||
self._add_overlay(self._prompt_container,
|
||||
@ -195,6 +193,9 @@ class MainWindow(QWidget):
|
||||
scope='window', window=self.win_id)
|
||||
self._prompt_container.hide()
|
||||
|
||||
self._messageview = messageview.MessageView(parent=self)
|
||||
self._add_overlay(self._messageview, self._messageview.update_geometry)
|
||||
|
||||
if geometry is not None:
|
||||
self._load_geometry(geometry)
|
||||
elif self.win_id == 0:
|
||||
|
@ -130,3 +130,8 @@ class MessageView(QWidget):
|
||||
self._last_text = text
|
||||
self.show()
|
||||
self.update_geometry.emit()
|
||||
|
||||
def mousePressEvent(self, e):
|
||||
"""Clear messages when they are clicked on."""
|
||||
if e.button() in [Qt.LeftButton, Qt.MiddleButton, Qt.RightButton]:
|
||||
self.clear_messages()
|
||||
|
@ -437,13 +437,13 @@ class LineEdit(QLineEdit):
|
||||
"""Override keyPressEvent to paste primary selection on Shift + Ins."""
|
||||
if e.key() == Qt.Key_Insert and e.modifiers() == Qt.ShiftModifier:
|
||||
try:
|
||||
text = utils.get_clipboard(selection=True)
|
||||
text = utils.get_clipboard(selection=True, fallback=True)
|
||||
except utils.ClipboardError: # pragma: no cover
|
||||
pass
|
||||
e.ignore()
|
||||
else:
|
||||
e.accept()
|
||||
self.insert(text)
|
||||
return
|
||||
return
|
||||
super().keyPressEvent(e)
|
||||
|
||||
def __repr__(self):
|
||||
@ -644,6 +644,10 @@ class FilenamePrompt(_BasePrompt):
|
||||
|
||||
def accept(self, value=None):
|
||||
text = value if value is not None else self._lineedit.text()
|
||||
text = downloads.transform_path(text)
|
||||
if text is None:
|
||||
message.error("Invalid filename")
|
||||
return False
|
||||
self.question.answer = text
|
||||
return True
|
||||
|
||||
@ -694,9 +698,11 @@ class DownloadFilenamePrompt(FilenamePrompt):
|
||||
self._file_model.setFilter(QDir.AllDirs | QDir.Drives | QDir.NoDot)
|
||||
|
||||
def accept(self, value=None):
|
||||
text = value if value is not None else self._lineedit.text()
|
||||
self.question.answer = downloads.FileDownloadTarget(text)
|
||||
return True
|
||||
done = super().accept(value)
|
||||
answer = self.question.answer
|
||||
if answer is not None:
|
||||
self.question.answer = downloads.FileDownloadTarget(answer)
|
||||
return done
|
||||
|
||||
def download_open(self, cmdline):
|
||||
self.question.answer = downloads.OpenFileDownloadTarget(cmdline)
|
||||
|
@ -146,10 +146,14 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
We don't implement this as generator so we can delete tabs while
|
||||
iterating over the list.
|
||||
"""
|
||||
w = []
|
||||
widgets = []
|
||||
for i in range(self.count()):
|
||||
w.append(self.widget(i))
|
||||
return w
|
||||
widget = self.widget(i)
|
||||
if widget is None:
|
||||
log.webview.debug("Got None-widget in tabbedbrowser!")
|
||||
else:
|
||||
widgets.append(widget)
|
||||
return widgets
|
||||
|
||||
@config.change_filter('ui', 'window-title-format')
|
||||
def update_window_title(self):
|
||||
|
@ -118,6 +118,9 @@ class TabWidget(QTabWidget):
|
||||
def get_tab_fields(self, idx):
|
||||
"""Get the tab field data."""
|
||||
tab = self.widget(idx)
|
||||
if tab is None:
|
||||
log.misc.debug("Got None-tab in get_tab_fields!")
|
||||
|
||||
page_title = self.page_title(idx)
|
||||
|
||||
fields = {}
|
||||
|
@ -171,14 +171,14 @@ class CrashHandler(QObject):
|
||||
"""
|
||||
try:
|
||||
pages = self._recover_pages(forgiving=True)
|
||||
except Exception:
|
||||
log.destroy.exception("Error while recovering pages")
|
||||
except Exception as e:
|
||||
log.destroy.exception("Error while recovering pages: {}".format(e))
|
||||
pages = []
|
||||
|
||||
try:
|
||||
cmd_history = objreg.get('command-history')[-5:]
|
||||
except Exception:
|
||||
log.destroy.exception("Error while getting history: {}")
|
||||
except Exception as e:
|
||||
log.destroy.exception("Error while getting history: {}".format(e))
|
||||
cmd_history = []
|
||||
|
||||
try:
|
||||
|
@ -27,7 +27,7 @@ import getpass
|
||||
import binascii
|
||||
import hashlib
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, Qt, QTimer
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, Qt
|
||||
from PyQt5.QtNetwork import QLocalSocket, QLocalServer, QAbstractSocket
|
||||
|
||||
import qutebrowser
|
||||
@ -182,6 +182,7 @@ class IPCServer(QObject):
|
||||
self._server.newConnection.connect(self.handle_connection)
|
||||
|
||||
self._socket = None
|
||||
self._old_socket = None
|
||||
self._socketopts_ok = os.name == 'nt'
|
||||
if self._socketopts_ok: # pragma: no cover
|
||||
# If we use setSocketOptions on Unix with Qt < 5.4, we get a
|
||||
@ -278,15 +279,8 @@ class IPCServer(QObject):
|
||||
log.ipc.debug("Client disconnected from socket 0x{:x}.".format(
|
||||
id(self._socket)))
|
||||
self._timer.stop()
|
||||
if self._socket is None:
|
||||
log.ipc.debug("In on_disconnected with None socket!")
|
||||
else:
|
||||
# For some reason Qt can still get delayed canReadNotifications
|
||||
# internally, so if we call deleteLater() right away and then call
|
||||
# QApplication::processEvents() somewhere in the code, we can get a
|
||||
# segfault.
|
||||
QTimer.singleShot(500, self._socket.deleteLater)
|
||||
self._socket = None
|
||||
self._old_socket = self._socket
|
||||
self._socket = None
|
||||
# Maybe another connection is waiting.
|
||||
self.handle_connection()
|
||||
|
||||
@ -349,17 +343,23 @@ class IPCServer(QObject):
|
||||
@pyqtSlot()
|
||||
def on_ready_read(self):
|
||||
"""Read json data from the client."""
|
||||
if self._socket is None:
|
||||
if self._socket is None: # pragma: no cover
|
||||
# This happens when doing a connection while another one is already
|
||||
# active for some reason.
|
||||
log.ipc.warning("In on_ready_read with None socket!")
|
||||
return
|
||||
if self._old_socket is None:
|
||||
log.ipc.warning("In on_ready_read with None socket and "
|
||||
"old_socket!")
|
||||
return
|
||||
log.ipc.debug("In on_ready_read with None socket!")
|
||||
socket = self._old_socket
|
||||
else:
|
||||
socket = self._socket
|
||||
self._timer.stop()
|
||||
while self._socket is not None and self._socket.canReadLine():
|
||||
data = bytes(self._socket.readLine())
|
||||
while socket is not None and socket.canReadLine():
|
||||
data = bytes(socket.readLine())
|
||||
self.got_raw.emit(data)
|
||||
log.ipc.debug("Read from socket 0x{:x}: {!r}".format(
|
||||
id(self._socket), data))
|
||||
id(socket), data))
|
||||
self._handle_data(data)
|
||||
self._timer.start()
|
||||
|
||||
|
@ -67,7 +67,6 @@ class KeyHintView(QLabel):
|
||||
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Minimum)
|
||||
self.hide()
|
||||
self._show_timer = usertypes.Timer(self, 'keyhint_show')
|
||||
self._show_timer.setInterval(500)
|
||||
self._show_timer.timeout.connect(self.show)
|
||||
style.set_register_stylesheet(self)
|
||||
|
||||
@ -108,6 +107,7 @@ class KeyHintView(QLabel):
|
||||
return
|
||||
|
||||
# delay so a quickly typed keychain doesn't display hints
|
||||
self._show_timer.setInterval(config.get('ui', 'keyhint-delay'))
|
||||
self._show_timer.start()
|
||||
suffix_color = html.escape(config.get('colors', 'keyhint.fg.suffix'))
|
||||
|
||||
|
@ -46,13 +46,13 @@ class MinimalLineEditMixin:
|
||||
"""Override keyPressEvent to paste primary selection on Shift + Ins."""
|
||||
if e.key() == Qt.Key_Insert and e.modifiers() == Qt.ShiftModifier:
|
||||
try:
|
||||
text = utils.get_clipboard(selection=True)
|
||||
text = utils.get_clipboard(selection=True, fallback=True)
|
||||
except utils.ClipboardError:
|
||||
pass
|
||||
e.ignore()
|
||||
else:
|
||||
e.accept()
|
||||
self.insert(text)
|
||||
return
|
||||
return
|
||||
super().keyPressEvent(e)
|
||||
|
||||
def __repr__(self):
|
||||
|
@ -228,7 +228,7 @@ def debug_pyeval(s, quiet=False):
|
||||
else:
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window='last-focused')
|
||||
tabbed_browser.openurl(QUrl('qute:pyeval'), newtab=True)
|
||||
tabbed_browser.openurl(QUrl('qute://pyeval'), newtab=True)
|
||||
|
||||
|
||||
@cmdutils.register(debug=True)
|
||||
@ -293,15 +293,24 @@ def debug_log_filter(filters: str):
|
||||
"""Change the log filter for console logging.
|
||||
|
||||
Args:
|
||||
filters: A comma separated list of logger names.
|
||||
filters: A comma separated list of logger names. Can also be "none" to
|
||||
clear any existing filters.
|
||||
"""
|
||||
if set(filters.split(',')).issubset(log.LOGGER_NAMES):
|
||||
log.console_filter.names = filters.split(',')
|
||||
else:
|
||||
if log.console_filter is None:
|
||||
raise cmdexc.CommandError("No log.console_filter. Not attached "
|
||||
"to a console?")
|
||||
|
||||
if filters.strip().lower() == 'none':
|
||||
log.console_filter.names = None
|
||||
return
|
||||
|
||||
if not set(filters.split(',')).issubset(log.LOGGER_NAMES):
|
||||
raise cmdexc.CommandError("filters: Invalid value {} - expected one "
|
||||
"of: {}".format(filters,
|
||||
', '.join(log.LOGGER_NAMES)))
|
||||
|
||||
log.console_filter.names = filters.split(',')
|
||||
|
||||
|
||||
@cmdutils.register()
|
||||
@cmdutils.argument('current_win_id', win_id=True)
|
||||
|
@ -182,9 +182,10 @@ def init_log(args):
|
||||
root = logging.getLogger()
|
||||
global console_filter
|
||||
if console is not None:
|
||||
console_filter = LogFilter(None)
|
||||
if args.logfilter is not None:
|
||||
console_filter = LogFilter(args.logfilter.split(','))
|
||||
console.addFilter(console_filter)
|
||||
console_filter.names = args.logfilter.split(',')
|
||||
console.addFilter(console_filter)
|
||||
root.addHandler(console)
|
||||
if ram is not None:
|
||||
root.addHandler(ram)
|
||||
|
@ -174,12 +174,6 @@ def ensure_valid(obj):
|
||||
raise QtValueError(obj)
|
||||
|
||||
|
||||
def ensure_not_null(obj):
|
||||
"""Ensure a Qt object with an .isNull() method is not null."""
|
||||
if obj.isNull():
|
||||
raise QtValueError(obj, null=True)
|
||||
|
||||
|
||||
def check_qdatastream(stream):
|
||||
"""Check the status of a QDataStream and raise OSError if it's not ok."""
|
||||
status_to_str = {
|
||||
@ -412,15 +406,12 @@ class QtValueError(ValueError):
|
||||
|
||||
"""Exception which gets raised by ensure_valid."""
|
||||
|
||||
def __init__(self, obj, null=False):
|
||||
def __init__(self, obj):
|
||||
try:
|
||||
self.reason = obj.errorString()
|
||||
except AttributeError:
|
||||
self.reason = None
|
||||
if null:
|
||||
err = "{} is null".format(obj)
|
||||
else:
|
||||
err = "{} is not valid".format(obj)
|
||||
err = "{} is not valid".format(obj)
|
||||
if self.reason:
|
||||
err += ": {}".format(self.reason)
|
||||
super().__init__(err)
|
||||
|
@ -20,6 +20,7 @@
|
||||
"""Other utilities which don't fit anywhere else."""
|
||||
|
||||
import io
|
||||
import re
|
||||
import sys
|
||||
import enum
|
||||
import json
|
||||
@ -53,6 +54,10 @@ class SelectionUnsupportedError(ClipboardError):
|
||||
|
||||
"""Raised if [gs]et_clipboard is used and selection=True is unsupported."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__("Primary selection is not supported on this "
|
||||
"platform!")
|
||||
|
||||
|
||||
class ClipboardEmptyError(ClipboardError):
|
||||
|
||||
@ -762,11 +767,23 @@ def set_clipboard(data, selection=False):
|
||||
QApplication.clipboard().setText(data, mode=mode)
|
||||
|
||||
|
||||
def get_clipboard(selection=False):
|
||||
"""Get data from the clipboard."""
|
||||
def get_clipboard(selection=False, fallback=False):
|
||||
"""Get data from the clipboard.
|
||||
|
||||
Args:
|
||||
selection: Use the primary selection.
|
||||
fallback: Fall back to the clipboard if primary selection is
|
||||
unavailable.
|
||||
"""
|
||||
global fake_clipboard
|
||||
if fallback and not selection:
|
||||
raise ValueError("fallback given without selection!")
|
||||
|
||||
if selection and not supports_selection():
|
||||
raise SelectionUnsupportedError
|
||||
if fallback:
|
||||
selection = False
|
||||
else:
|
||||
raise SelectionUnsupportedError
|
||||
|
||||
if fake_clipboard is not None:
|
||||
data = fake_clipboard
|
||||
@ -845,3 +862,21 @@ def open_file(filename, cmdline=None):
|
||||
def unused(_arg):
|
||||
"""Function which does nothing to avoid pylint complaining."""
|
||||
pass
|
||||
|
||||
|
||||
def expand_windows_drive(path):
|
||||
r"""Expand a drive-path like E: into E:\.
|
||||
|
||||
Does nothing for other paths.
|
||||
|
||||
Args:
|
||||
path: The path to expand.
|
||||
"""
|
||||
# Usually, "E:" on Windows refers to the current working directory on drive
|
||||
# E:\. The correct way to specifify drive E: is "E:\", but most users
|
||||
# probably don't use the "multiple working directories" feature and expect
|
||||
# "E:" and "E:\" to be equal.
|
||||
if re.match(r'[A-Z]:$', path, re.IGNORECASE):
|
||||
return path + "\\"
|
||||
else:
|
||||
return path
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
colorama==0.3.7
|
||||
cssutils==1.0.2
|
||||
Jinja2==2.9.5
|
||||
Jinja2==2.9.6
|
||||
MarkupSafe==1.0
|
||||
Pygments==2.2.0
|
||||
pyPEG2==2.15.2
|
||||
|
@ -28,6 +28,7 @@ CI machines.
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import os
|
||||
import time
|
||||
import subprocess
|
||||
import urllib
|
||||
@ -44,23 +45,6 @@ def pip_install(pkg):
|
||||
pkg])
|
||||
|
||||
|
||||
print("Getting PyQt5...")
|
||||
qt_version = '5.5.1'
|
||||
pyqt_version = '5.5.1'
|
||||
pyqt_url = ('https://www.qutebrowser.org/pyqt/'
|
||||
'PyQt5-{}-gpl-Py3.4-Qt{}-x32.exe'.format(
|
||||
pyqt_version, qt_version))
|
||||
|
||||
try:
|
||||
urllib.urlretrieve(pyqt_url, r'C:\install-PyQt5.exe')
|
||||
except (OSError, IOError):
|
||||
print("Downloading PyQt failed, trying again in 10 seconds...")
|
||||
time.sleep(10)
|
||||
urllib.urlretrieve(pyqt_url, r'C:\install-PyQt5.exe')
|
||||
|
||||
print("Installing PyQt5...")
|
||||
subprocess.check_call([r'C:\install-PyQt5.exe', '/S'])
|
||||
|
||||
print("Installing tox")
|
||||
pip_install('pip')
|
||||
pip_install(r'-rmisc\requirements\requirements-tox.txt')
|
||||
@ -69,4 +53,23 @@ print("Linking Python...")
|
||||
with open(r'C:\Windows\system32\python3.bat', 'w') as f:
|
||||
f.write(r'@C:\Python34\python %*')
|
||||
|
||||
check_setup(r'C:\Python34\python')
|
||||
|
||||
if '-pyqt' not in os.environ['TESTENV']:
|
||||
print("Getting PyQt5...")
|
||||
qt_version = '5.5.1'
|
||||
pyqt_version = '5.5.1'
|
||||
pyqt_url = ('https://www.qutebrowser.org/pyqt/'
|
||||
'PyQt5-{}-gpl-Py3.4-Qt{}-x32.exe'.format(
|
||||
pyqt_version, qt_version))
|
||||
|
||||
try:
|
||||
urllib.urlretrieve(pyqt_url, r'C:\install-PyQt5.exe')
|
||||
except (OSError, IOError):
|
||||
print("Downloading PyQt failed, trying again in 10 seconds...")
|
||||
time.sleep(10)
|
||||
urllib.urlretrieve(pyqt_url, r'C:\install-PyQt5.exe')
|
||||
|
||||
print("Installing PyQt5...")
|
||||
subprocess.check_call([r'C:\install-PyQt5.exe', '/S'])
|
||||
|
||||
check_setup(r'C:\Python34\python')
|
||||
|
@ -1,57 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2016 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/>.
|
||||
|
||||
"""Wrapper around pytest to ignore segfaults on exit."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import signal
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
script_path = os.path.abspath(os.path.dirname(__file__))
|
||||
pytest_status_file = os.path.join(script_path, '..', '..', '.cache',
|
||||
'pytest_status')
|
||||
|
||||
try:
|
||||
os.remove(pytest_status_file)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
try:
|
||||
subprocess.check_call([sys.executable, '-m', 'pytest'] + sys.argv[1:])
|
||||
except subprocess.CalledProcessError as exc:
|
||||
is_segfault = exc.returncode in [128 + signal.SIGSEGV, -signal.SIGSEGV]
|
||||
if is_segfault and os.path.exists(pytest_status_file):
|
||||
print("Ignoring segfault on exit!")
|
||||
with open(pytest_status_file, 'r', encoding='ascii') as f:
|
||||
exit_status = int(f.read())
|
||||
else:
|
||||
exit_status = exc.returncode
|
||||
else:
|
||||
exit_status = 0
|
||||
|
||||
try:
|
||||
os.remove(pytest_status_file)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
sys.exit(exit_status)
|
@ -74,7 +74,7 @@ def whitelist_generator():
|
||||
yield 'PyQt5.QtWebKit.QWebPage.ErrorPageExtensionReturn().fileNames'
|
||||
yield 'PyQt5.QtWidgets.QStyleOptionViewItem.backgroundColor'
|
||||
|
||||
## qute:... handlers
|
||||
## qute://... handlers
|
||||
for name in qutescheme._HANDLERS: # pylint: disable=protected-access
|
||||
yield 'qutebrowser.browser.qutescheme.qute_' + name
|
||||
|
||||
|
@ -46,7 +46,7 @@ def parse_args():
|
||||
if QWebEngineView is not None:
|
||||
parser.add_argument('--webengine', help='Use QtWebEngine',
|
||||
default=False, action='store_true')
|
||||
return parser.parse_args()
|
||||
return parser.parse_known_args()[0]
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
@ -44,7 +44,7 @@ hypothesis.settings.register_profile('default',
|
||||
hypothesis.settings.load_profile('default')
|
||||
|
||||
|
||||
def _apply_platform_markers(item):
|
||||
def _apply_platform_markers(config, item):
|
||||
"""Apply a skip marker to a given item."""
|
||||
markers = [
|
||||
('posix', os.name != 'posix', "Requires a POSIX os"),
|
||||
@ -57,6 +57,8 @@ def _apply_platform_markers(item):
|
||||
('frozen', not getattr(sys, 'frozen', False),
|
||||
"Can only run when frozen"),
|
||||
('ci', 'CI' not in os.environ, "Only runs on CI."),
|
||||
('issue2478', os.name == 'nt' and config.webengine,
|
||||
"Broken with QtWebEngine on Windows"),
|
||||
]
|
||||
|
||||
for searched_marker, condition, default_reason in markers:
|
||||
@ -117,7 +119,7 @@ def pytest_collection_modifyitems(config, items):
|
||||
if module_root_dir == 'end2end':
|
||||
item.add_marker(pytest.mark.end2end)
|
||||
|
||||
_apply_platform_markers(item)
|
||||
_apply_platform_markers(config, item)
|
||||
if item.get_marker('xfail_norun'):
|
||||
item.add_marker(pytest.mark.xfail(run=False))
|
||||
if item.get_marker('js_prompt'):
|
||||
@ -192,21 +194,3 @@ def pytest_runtest_makereport(item, call):
|
||||
outcome = yield
|
||||
rep = outcome.get_result()
|
||||
setattr(item, "rep_" + rep.when, rep)
|
||||
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_sessionfinish(exitstatus):
|
||||
"""Create a file to tell run_pytest.py how pytest exited."""
|
||||
outcome = yield
|
||||
outcome.get_result()
|
||||
|
||||
cache_dir = os.path.join(os.path.abspath(os.path.dirname(__file__)),
|
||||
'..', '.cache')
|
||||
try:
|
||||
os.mkdir(cache_dir)
|
||||
except FileExistsError:
|
||||
pass
|
||||
|
||||
status_file = os.path.join(cache_dir, 'pytest_status')
|
||||
with open(status_file, 'w', encoding='ascii') as f:
|
||||
f.write(str(exitstatus))
|
||||
|
10
tests/end2end/data/downloads/issue2298.html
Normal file
10
tests/end2end/data/downloads/issue2298.html
Normal file
@ -0,0 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Retrying failing download with QtWebEngine</title>
|
||||
</head>
|
||||
<body>
|
||||
<a href="/does-not-exist" download id="download">download</a>
|
||||
</body>
|
||||
</html>
|
@ -5,8 +5,8 @@
|
||||
<title>Test title</title>
|
||||
<script type="text/javascript">
|
||||
window.onload = function () {
|
||||
console.log("Calling history.replaceState");
|
||||
history.replaceState({}, '', window.location + '?state=2');
|
||||
console.log("Called history.replaceState");
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
@ -1,3 +1,5 @@
|
||||
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
Feature: Ad blocking
|
||||
|
||||
Scenario: Simple adblock update
|
||||
|
@ -1,3 +1,5 @@
|
||||
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
Feature: Going back and forward.
|
||||
Testing the :back/:forward commands.
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
Feature: Caret mode
|
||||
In caret mode, the user can select and yank text using the keyboard.
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
Feature: Using completion
|
||||
|
||||
Scenario: No warnings when completing with one entry (#1600)
|
||||
|
@ -161,6 +161,7 @@ def clean_open_tabs(quteproc):
|
||||
quteproc.send_cmd(':window-only')
|
||||
quteproc.send_cmd(':tab-only')
|
||||
quteproc.send_cmd(':tab-close')
|
||||
quteproc.wait_for_load_finished_url('about:blank')
|
||||
|
||||
|
||||
@bdd.given('pdfjs is available')
|
||||
|
@ -1,3 +1,5 @@
|
||||
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
Feature: Downloading things from a website.
|
||||
|
||||
Background:
|
||||
@ -78,6 +80,7 @@ Feature: Downloading things from a website.
|
||||
Scenario: Downloading with SSL errors (issue 1413)
|
||||
When I clear SSL errors
|
||||
And I set network -> ssl-strict to ask
|
||||
And I set storage -> prompt-download-directory to false
|
||||
And I download an SSL page
|
||||
And I wait for "Entering mode KeyMode.* (reason: question asked)" in the log
|
||||
And I run :prompt-accept
|
||||
@ -126,6 +129,34 @@ Feature: Downloading things from a website.
|
||||
Then the downloaded file ../foo should not exist
|
||||
And the downloaded file foo should exist
|
||||
|
||||
@windows
|
||||
Scenario: Downloading a file to a reserved path
|
||||
When I set storage -> prompt-download-directory to true
|
||||
And I open data/downloads/download.bin without waiting
|
||||
And I wait for "Asking question <qutebrowser.utils.usertypes.Question default='*' mode=<PromptMode.download: 5> text='Please enter a location for <b>http://localhost:*/data/downloads/download.bin</b>' title='Save file to:'>, *" in the log
|
||||
And I run :prompt-accept COM1
|
||||
And I run :leave-mode
|
||||
Then the error "Invalid filename" should be shown
|
||||
|
||||
@windows
|
||||
Scenario: Downloading a file to a drive-relative working directory
|
||||
When I set storage -> prompt-download-directory to true
|
||||
And I open data/downloads/download.bin without waiting
|
||||
And I wait for "Asking question <qutebrowser.utils.usertypes.Question default='*' mode=<PromptMode.download: 5> text='Please enter a location for <b>http://localhost:*/data/downloads/download.bin</b>' title='Save file to:'>, *" in the log
|
||||
And I run :prompt-accept C:foobar
|
||||
And I run :leave-mode
|
||||
Then the error "Invalid filename" should be shown
|
||||
|
||||
@windows
|
||||
Scenario: Downloading a file to a reserved path with :download
|
||||
When I run :download data/downloads/download.bin --dest=COM1
|
||||
Then the error "Invalid target filename" should be shown
|
||||
|
||||
@windows
|
||||
Scenario: Download a file to a drive-relative working directory with :download
|
||||
When I run :download data/downloads/download.bin --dest=C:foobar
|
||||
Then the error "Invalid target filename" should be shown
|
||||
|
||||
## :download-retry
|
||||
|
||||
Scenario: Retrying a failed download
|
||||
@ -137,6 +168,14 @@ Feature: Downloading things from a website.
|
||||
does-not-exist
|
||||
does-not-exist
|
||||
|
||||
@qtwebkit_skip
|
||||
Scenario: Retrying a failed download with QtWebEngine
|
||||
When I open data/downloads/issue2298.html
|
||||
And I run :click-element id download
|
||||
And I wait for "Download error: *" in the log
|
||||
And I run :download-retry
|
||||
Then the error "Retrying downloads is unsupported with QtWebEngine" should be shown
|
||||
|
||||
Scenario: Retrying with count
|
||||
When I run :download http://localhost:(port)/data/downloads/download.bin
|
||||
And I run :download http://localhost:(port)/does-not-exist
|
||||
@ -603,3 +642,9 @@ Feature: Downloading things from a website.
|
||||
And I run :follow-hint 0
|
||||
And I wait until the download is finished
|
||||
Then the downloaded file user-agent should contain Safari/
|
||||
|
||||
@qtwebengine_skip: Handled by QtWebEngine, not by us
|
||||
Scenario: Downloading a "Internal server error" with disposition: inline (#2304)
|
||||
When I set storage -> prompt-download-directory to false
|
||||
And I open custom/500-inline
|
||||
Then the error "Download error: *INTERNAL SERVER ERROR" should be shown
|
||||
|
@ -1,3 +1,5 @@
|
||||
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
Feature: Opening external editors
|
||||
|
||||
## :edit-url
|
||||
|
@ -1,3 +1,5 @@
|
||||
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
Feature: Using hints
|
||||
|
||||
# https://bugreports.qt.io/browse/QTBUG-58381
|
||||
@ -45,17 +47,17 @@ Feature: Using hints
|
||||
|
||||
Scenario: Using :hint spawn with flags and -- (issue 797)
|
||||
When I open data/hints/html/simple.html
|
||||
And I hint with args "-- all spawn -v echo" and follow a
|
||||
And I hint with args "-- all spawn -v python -c ''" and follow a
|
||||
Then the message "Command exited successfully." should be shown
|
||||
|
||||
Scenario: Using :hint spawn with flags (issue 797)
|
||||
When I open data/hints/html/simple.html
|
||||
And I hint with args "all spawn -v echo" and follow a
|
||||
And I hint with args "all spawn -v python -c ''" and follow a
|
||||
Then the message "Command exited successfully." should be shown
|
||||
|
||||
Scenario: Using :hint spawn with flags and --rapid (issue 797)
|
||||
When I open data/hints/html/simple.html
|
||||
And I hint with args "--rapid all spawn -v echo" and follow a
|
||||
And I hint with args "--rapid all spawn -v python -c ''" and follow a
|
||||
Then the message "Command exited successfully." should be shown
|
||||
|
||||
@posix
|
||||
|
@ -1,3 +1,5 @@
|
||||
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
Feature: Page history
|
||||
|
||||
Make sure the global page history is saved correctly.
|
||||
@ -77,7 +79,15 @@ Feature: Page history
|
||||
Scenario: Listing history
|
||||
When I open data/numbers/3.txt
|
||||
And I open data/numbers/4.txt
|
||||
And I open qute:history
|
||||
And I open qute://history
|
||||
Then the page should contain the plaintext "3.txt"
|
||||
Then the page should contain the plaintext "4.txt"
|
||||
|
||||
Scenario: Listing history with qute:history redirect
|
||||
When I open data/numbers/3.txt
|
||||
And I open data/numbers/4.txt
|
||||
And I open qute:history without waiting
|
||||
And I wait until qute://history is loaded
|
||||
Then the page should contain the plaintext "3.txt"
|
||||
Then the page should contain the plaintext "4.txt"
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
Feature: Invoking a new process
|
||||
Simulate what happens when running qutebrowser with an existing instance
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
Feature: Javascript stuff
|
||||
|
||||
Integration with javascript.
|
||||
|
@ -1,3 +1,5 @@
|
||||
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
Feature: Keyboard input
|
||||
|
||||
Tests for :bind and :unbind, :clear-keychain and other keyboard input
|
||||
|
@ -1,3 +1,5 @@
|
||||
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
Feature: Setting positional marks
|
||||
|
||||
Background:
|
||||
|
@ -1,3 +1,5 @@
|
||||
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
Feature: Various utility commands.
|
||||
|
||||
## :set-cmd-text
|
||||
@ -79,6 +81,7 @@ Feature: Various utility commands.
|
||||
And I run :jseval --world=1 console.log("Hello from JS!");
|
||||
And I wait for the javascript message "Hello from JS!"
|
||||
Then "Ignoring world ID 1" should be logged
|
||||
And "No output or error" should be logged
|
||||
|
||||
@qtwebkit_skip
|
||||
Scenario: :jseval uses separate world without --world
|
||||
@ -87,6 +90,7 @@ Feature: Various utility commands.
|
||||
And I run :jseval do_log()
|
||||
Then the javascript message "Hello from the page!" should not be logged
|
||||
And the javascript message "Uncaught ReferenceError: do_log is not defined" should be logged
|
||||
And "No output or error" should be logged
|
||||
|
||||
@qtwebkit_skip
|
||||
Scenario: :jseval using the main world
|
||||
@ -94,6 +98,7 @@ Feature: Various utility commands.
|
||||
And I open data/misc/jseval.html
|
||||
And I run :jseval --world 0 do_log()
|
||||
Then the javascript message "Hello from the page!" should be logged
|
||||
And "No output or error" should be logged
|
||||
|
||||
@qtwebkit_skip
|
||||
Scenario: :jseval using the main world as name
|
||||
@ -101,12 +106,14 @@ Feature: Various utility commands.
|
||||
And I open data/misc/jseval.html
|
||||
And I run :jseval --world main do_log()
|
||||
Then the javascript message "Hello from the page!" should be logged
|
||||
And "No output or error" should be logged
|
||||
|
||||
Scenario: :jseval --file using a file that exists as js-code
|
||||
When I set general -> log-javascript-console to info
|
||||
And I run :jseval --file (testdata)/misc/jseval_file.js
|
||||
Then the javascript message "Hello from JS!" should be logged
|
||||
And the javascript message "Hello again from JS!" should be logged
|
||||
And "No output or error" should be logged
|
||||
|
||||
Scenario: :jseval --file using a file that doesn't exist as js-code
|
||||
When I run :jseval --file nonexistentfile
|
||||
@ -398,12 +405,12 @@ Feature: Various utility commands.
|
||||
# :pyeval
|
||||
Scenario: Running :pyeval
|
||||
When I run :debug-pyeval 1+1
|
||||
And I wait until qute:pyeval is loaded
|
||||
And I wait until qute://pyeval is loaded
|
||||
Then the page should contain the plaintext "2"
|
||||
|
||||
Scenario: Causing exception in :pyeval
|
||||
When I run :debug-pyeval 1/0
|
||||
And I wait until qute:pyeval is loaded
|
||||
And I wait until qute://pyeval is loaded
|
||||
Then the page should contain the plaintext "ZeroDivisionError"
|
||||
|
||||
Scenario: Running :pyeval with --quiet
|
||||
@ -505,12 +512,12 @@ Feature: Various utility commands.
|
||||
When I run :messages cataclysmic
|
||||
Then the error "Invalid log level cataclysmic!" should be shown
|
||||
|
||||
Scenario: Using qute:log directly
|
||||
When I open qute:log
|
||||
Scenario: Using qute://log directly
|
||||
When I open qute://log
|
||||
Then no crash should happen
|
||||
|
||||
Scenario: Using qute:plainlog directly
|
||||
When I open qute:plainlog
|
||||
Scenario: Using qute://plainlog directly
|
||||
When I open qute://plainlog
|
||||
Then no crash should happen
|
||||
|
||||
Scenario: Using :messages without messages
|
||||
@ -531,6 +538,16 @@ Feature: Various utility commands.
|
||||
When I run :message-i "Hello World" (invalid command)
|
||||
Then the error "message-i: no such command" should be shown
|
||||
|
||||
Scenario: Multiple leading : in command
|
||||
When I run :::::set-cmd-text ::::message-i "Hello World"
|
||||
And I run :command-accept
|
||||
Then the message "Hello World" should be shown
|
||||
|
||||
Scenario: Whitespace in command
|
||||
When I run : : set-cmd-text : : message-i "Hello World"
|
||||
And I run :command-accept
|
||||
Then the message "Hello World" should be shown
|
||||
|
||||
# We can't run :message-i as startup command, so we use
|
||||
# :set-cmd-text
|
||||
|
||||
@ -625,7 +642,7 @@ Feature: Various utility commands.
|
||||
And I run :command-history-prev
|
||||
And I run :command-accept
|
||||
Then the message "blah" should be shown
|
||||
|
||||
|
||||
Scenario: Browsing through commands
|
||||
When I run :set-cmd-text :message-info blarg
|
||||
And I run :command-accept
|
||||
@ -637,7 +654,7 @@ Feature: Various utility commands.
|
||||
And I run :command-history-next
|
||||
And I run :command-accept
|
||||
Then the message "blarg" should be shown
|
||||
|
||||
|
||||
Scenario: Calling previous command when history is empty
|
||||
Given I have a fresh instance
|
||||
When I run :set-cmd-text :
|
||||
@ -673,7 +690,8 @@ Feature: Various utility commands.
|
||||
|
||||
## Renderer crashes
|
||||
|
||||
@qtwebkit_skip @no_invalid_lines
|
||||
# Skipped on Windows as "... has stopped working" hangs.
|
||||
@qtwebkit_skip @no_invalid_lines @posix
|
||||
Scenario: Renderer crash
|
||||
When I run :open -t chrome://crash
|
||||
Then the error "Renderer process crashed" should be shown
|
||||
|
@ -1,3 +1,5 @@
|
||||
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
Feature: Using :navigate
|
||||
|
||||
Scenario: :navigate with invalid argument
|
||||
|
@ -1,3 +1,5 @@
|
||||
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
Feature: Opening pages
|
||||
|
||||
Scenario: :open with URL
|
||||
|
@ -1,3 +1,5 @@
|
||||
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
Feature: Prompts
|
||||
Various prompts (javascript, SSL errors, authentification, etc.)
|
||||
|
||||
@ -150,12 +152,13 @@ Feature: Prompts
|
||||
Scenario: Pasting via shift-insert without it being supported
|
||||
When selection is not supported
|
||||
And I put "insert test" into the primary selection
|
||||
And I put "clipboard test" into the clipboard
|
||||
And I open data/prompt/jsprompt.html
|
||||
And I run :click-element id button
|
||||
And I wait for a prompt
|
||||
And I press the keys "<Shift-Insert>"
|
||||
And I run :prompt-accept
|
||||
Then the javascript message "Prompt reply: " should be logged
|
||||
Then the javascript message "Prompt reply: clipboard test" should be logged
|
||||
|
||||
@js_prompt
|
||||
Scenario: Using content -> ignore-javascript-prompt
|
||||
@ -174,6 +177,7 @@ Feature: Prompts
|
||||
Then the error "Certificate error: *" should be shown
|
||||
And the page should contain the plaintext "Hello World via SSL!"
|
||||
|
||||
@issue2478
|
||||
Scenario: SSL error with ssl-strict = true
|
||||
When I clear SSL errors
|
||||
And I set network -> ssl-strict to true
|
||||
@ -189,6 +193,7 @@ Feature: Prompts
|
||||
And I wait until the SSL page finished loading
|
||||
Then the page should contain the plaintext "Hello World via SSL!"
|
||||
|
||||
@issue2478
|
||||
Scenario: SSL error with ssl-strict = ask -> no
|
||||
When I clear SSL errors
|
||||
And I set network -> ssl-strict to ask
|
||||
@ -197,6 +202,7 @@ Feature: Prompts
|
||||
And I run :prompt-accept no
|
||||
Then a SSL error page should be shown
|
||||
|
||||
@issue2478
|
||||
Scenario: SSL error with ssl-strict = ask -> abort
|
||||
When I clear SSL errors
|
||||
And I set network -> ssl-strict to ask
|
||||
|
@ -1,3 +1,5 @@
|
||||
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
Feature: Scrolling
|
||||
Tests the various scroll commands.
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
Feature: Searching on a page
|
||||
Searching text on the page (like /foo) with different options.
|
||||
|
||||
@ -110,6 +112,15 @@ Feature: Searching on a page
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "foo"
|
||||
|
||||
# https://github.com/qutebrowser/qutebrowser/issues/2438
|
||||
Scenario: Jumping to next match after clearing
|
||||
When I set general -> ignore-case to true
|
||||
And I run :search foo
|
||||
And I run :search
|
||||
And I run :search-next
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "foo"
|
||||
|
||||
## :search-prev
|
||||
|
||||
Scenario: Jumping to previous match
|
||||
|
@ -1,3 +1,5 @@
|
||||
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
Feature: Saving and loading sessions
|
||||
|
||||
Background:
|
||||
@ -198,7 +200,7 @@ Feature: Saving and loading sessions
|
||||
|
||||
Scenario: Saving a session with a page using history.replaceState()
|
||||
When I open data/sessions/history_replace_state.html without waiting
|
||||
Then the javascript message "Calling history.replaceState" should be logged
|
||||
Then the javascript message "Called history.replaceState" should be logged
|
||||
And the session should look like:
|
||||
windows:
|
||||
- tabs:
|
||||
@ -212,7 +214,7 @@ Feature: Saving and loading sessions
|
||||
Scenario: Saving a session with a page using history.replaceState() and navigating away (qtwebkit)
|
||||
When I open data/sessions/history_replace_state.html
|
||||
And I open data/hello.txt
|
||||
Then the javascript message "Calling history.replaceState" should be logged
|
||||
Then the javascript message "Called history.replaceState" should be logged
|
||||
And the session should look like:
|
||||
windows:
|
||||
- tabs:
|
||||
@ -229,7 +231,7 @@ Feature: Saving and loading sessions
|
||||
@qtwebkit_skip
|
||||
Scenario: Saving a session with a page using history.replaceState() and navigating away
|
||||
When I open data/sessions/history_replace_state.html without waiting
|
||||
And I wait for "* Calling history.replaceState" in the log
|
||||
And I wait for "* Called history.replaceState" in the log
|
||||
And I open data/hello.txt
|
||||
Then the session should look like:
|
||||
windows:
|
||||
|
@ -1,3 +1,5 @@
|
||||
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
Feature: Setting settings.
|
||||
|
||||
Background:
|
||||
@ -76,15 +78,15 @@ Feature: Setting settings.
|
||||
When I run :set -t colors statusbar.bg green
|
||||
Then colors -> statusbar.bg should be green
|
||||
|
||||
# qute:settings isn't actually implemented on QtWebEngine, but this works
|
||||
# qute://settings isn't actually implemented on QtWebEngine, but this works
|
||||
# (and displays a page saying it's not available)
|
||||
Scenario: Opening qute:settings
|
||||
Scenario: Opening qute://settings
|
||||
When I run :set
|
||||
And I wait until qute:settings is loaded
|
||||
And I wait until qute://settings is loaded
|
||||
Then the following tabs should be open:
|
||||
- qute:settings (active)
|
||||
- qute://settings (active)
|
||||
|
||||
@qtwebengine_todo: qute:settings is not implemented yet
|
||||
@qtwebengine_todo: qute://settings is not implemented yet
|
||||
Scenario: Focusing input fields in qute://settings and entering valid value
|
||||
When I set general -> ignore-case to false
|
||||
And I open qute://settings
|
||||
@ -99,7 +101,7 @@ Feature: Setting settings.
|
||||
And I press the key "<Tab>"
|
||||
Then general -> ignore-case should be true
|
||||
|
||||
@qtwebengine_todo: qute:settings is not implemented yet
|
||||
@qtwebengine_todo: qute://settings is not implemented yet
|
||||
Scenario: Focusing input fields in qute://settings and entering invalid value
|
||||
When I open qute://settings
|
||||
# scroll to the right - the table does not fit in the default screen
|
||||
|
@ -1,3 +1,5 @@
|
||||
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
Feature: :spawn
|
||||
|
||||
Scenario: Running :spawn
|
||||
|
@ -1,3 +1,5 @@
|
||||
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
Feature: Tab management
|
||||
Tests for various :tab-* commands.
|
||||
|
||||
|
@ -68,6 +68,8 @@ def wait_for_download_finished_name(quteproc, name):
|
||||
def wait_for_download_prompt(tmpdir, quteproc, path):
|
||||
full_path = path.replace('(tmpdir)', str(tmpdir)).replace('/', os.sep)
|
||||
quteproc.wait_for(message=PROMPT_MSG.format(full_path))
|
||||
quteproc.wait_for(message="Entering mode KeyMode.prompt "
|
||||
"(reason: question asked)")
|
||||
|
||||
|
||||
@bdd.when("I download an SSL page")
|
||||
|
@ -17,5 +17,18 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import logging
|
||||
|
||||
import pytest_bdd as bdd
|
||||
bdd.scenarios('open.feature')
|
||||
|
||||
|
||||
def test_open_s(quteproc, ssl_server):
|
||||
"""Test :open with -s."""
|
||||
quteproc.set_setting('network', 'ssl-strict', 'false')
|
||||
quteproc.send_cmd(':open -s http://localhost:{}/'.format(ssl_server.port))
|
||||
quteproc.mark_expected(category='message',
|
||||
loglevel=logging.ERROR,
|
||||
message="Certificate error: *")
|
||||
quteproc.wait_for_load_finished('/', port=ssl_server.port, https=True,
|
||||
load_status='warn')
|
||||
|
@ -1,3 +1,5 @@
|
||||
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
Feature: quickmarks and bookmarks
|
||||
|
||||
## bookmarks
|
||||
@ -223,12 +225,12 @@ Feature: quickmarks and bookmarks
|
||||
Scenario: Listing quickmarks
|
||||
When I run :quickmark-add http://localhost:(port)/data/numbers/20.txt twenty
|
||||
And I run :quickmark-add http://localhost:(port)/data/numbers/21.txt twentyone
|
||||
And I open qute:bookmarks
|
||||
And I open qute://bookmarks
|
||||
Then the page should contain the plaintext "twenty"
|
||||
And the page should contain the plaintext "twentyone"
|
||||
|
||||
Scenario: Listing bookmarks
|
||||
When I open data/title.html
|
||||
When I open data/title.html in a new tab
|
||||
And I run :bookmark-add
|
||||
And I open qute:bookmarks
|
||||
And I open qute://bookmarks
|
||||
Then the page should contain the plaintext "Test title"
|
||||
|
@ -1,3 +1,5 @@
|
||||
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
Feature: Miscellaneous utility commands exposed to the user.
|
||||
|
||||
Background:
|
||||
@ -129,6 +131,7 @@ Feature: Miscellaneous utility commands exposed to the user.
|
||||
And I hint with args "all tab-fg"
|
||||
And I run :leave-mode
|
||||
And I run :repeat-command
|
||||
And I wait for "hints: *" in the log
|
||||
And I run :follow-hint a
|
||||
And I wait until data/hello.txt is loaded
|
||||
Then the following tabs should be open:
|
||||
@ -142,7 +145,7 @@ Feature: Miscellaneous utility commands exposed to the user.
|
||||
And I run :message-info oldstuff
|
||||
And I run :repeat 20 message-info otherstuff
|
||||
And I run :message-info newstuff
|
||||
And I open qute:log
|
||||
And I open qute://log
|
||||
Then the page should contain the plaintext "newstuff"
|
||||
And the page should not contain the plaintext "oldstuff"
|
||||
|
||||
@ -161,3 +164,10 @@ Feature: Miscellaneous utility commands exposed to the user.
|
||||
Scenario: Using debug-log-filter with invalid filter
|
||||
When I run :debug-log-filter blah
|
||||
Then the error "filters: Invalid value blah - expected one of: statusbar, *" should be shown
|
||||
|
||||
Scenario: Using debug-log-filter
|
||||
When I run :debug-log-filter commands,ipc,webview
|
||||
And I run :enter-mode insert
|
||||
And I run :debug-log-filter none
|
||||
And I run :leave-mode
|
||||
Then "Entering mode KeyMode.insert *" should not be logged
|
||||
|
@ -1,3 +1,5 @@
|
||||
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
Feature: Yanking and pasting.
|
||||
:yank, {clipboard} and {primary} can be used to copy/paste the URL or title
|
||||
from/to the clipboard and primary selection.
|
||||
@ -99,6 +101,11 @@ Feature: Yanking and pasting.
|
||||
And I run :open {primary} (invalid command)
|
||||
Then the error "Primary selection is empty." should be shown
|
||||
|
||||
Scenario: Pasting without primary selection being supported
|
||||
When selection is not supported
|
||||
And I run :open {primary} (invalid command)
|
||||
Then the error "Primary selection is not supported on this platform!" should be shown
|
||||
|
||||
Scenario: Pasting with a space in clipboard
|
||||
When I put " " into the clipboard
|
||||
And I run :open {clipboard} (invalid command)
|
||||
|
@ -1,3 +1,5 @@
|
||||
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
Feature: Zooming in and out
|
||||
|
||||
Background:
|
||||
|
@ -75,6 +75,10 @@ def is_ignored_lowlevel_message(message):
|
||||
"not supported by protocol" in message):
|
||||
# Makes tests fail on Quantumcross' machine
|
||||
return True
|
||||
elif 'Unable to locate theme engine in module_path:' in message:
|
||||
return True
|
||||
elif message == 'getrlimit(RLIMIT_NOFILE) failed':
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
|
@ -149,11 +149,11 @@ def test_quteprocess_quitting(qtbot, quteproc_process):
|
||||
@pytest.mark.parametrize('data, attrs', [
|
||||
(
|
||||
# Normal message
|
||||
'{"created": 0, "msecs": 0, "levelname": "DEBUG", "name": "init", '
|
||||
'{"created": 86400, "msecs": 0, "levelname": "DEBUG", "name": "init", '
|
||||
'"module": "earlyinit", "funcName": "init_log", "lineno": 280, '
|
||||
'"levelno": 10, "message": "Log initialized."}',
|
||||
{
|
||||
'timestamp': datetime.datetime.fromtimestamp(0),
|
||||
'timestamp': datetime.datetime.fromtimestamp(86400),
|
||||
'loglevel': logging.DEBUG,
|
||||
'category': 'init',
|
||||
'module': 'earlyinit',
|
||||
@ -165,28 +165,28 @@ def test_quteprocess_quitting(qtbot, quteproc_process):
|
||||
),
|
||||
(
|
||||
# VDEBUG
|
||||
'{"created": 0, "msecs": 0, "levelname": "VDEBUG", "name": "foo", '
|
||||
'{"created": 86400, "msecs": 0, "levelname": "VDEBUG", "name": "foo", '
|
||||
'"module": "foo", "funcName": "foo", "lineno": 0, "levelno": 9, '
|
||||
'"message": ""}',
|
||||
{'loglevel': log.VDEBUG_LEVEL}
|
||||
),
|
||||
(
|
||||
# Unknown module
|
||||
'{"created": 0, "msecs": 0, "levelname": "DEBUG", "name": "qt", '
|
||||
'{"created": 86400, "msecs": 0, "levelname": "DEBUG", "name": "qt", '
|
||||
'"module": null, "funcName": null, "lineno": 0, "levelno": 10, '
|
||||
'"message": "test"}',
|
||||
{'module': None, 'function': None, 'line': None},
|
||||
),
|
||||
(
|
||||
# Expected message
|
||||
'{"created": 0, "msecs": 0, "levelname": "VDEBUG", "name": "foo", '
|
||||
'{"created": 86400, "msecs": 0, "levelname": "VDEBUG", "name": "foo", '
|
||||
'"module": "foo", "funcName": "foo", "lineno": 0, "levelno": 9, '
|
||||
'"message": "SpellCheck: test"}',
|
||||
{'expected': True},
|
||||
),
|
||||
(
|
||||
# Weird Qt location
|
||||
'{"created": 0, "msecs": 0, "levelname": "DEBUG", "name": "qt", '
|
||||
'{"created": 86400, "msecs": 0, "levelname": "DEBUG", "name": "qt", '
|
||||
'"module": "qnetworkreplyhttpimpl", "funcName": '
|
||||
'"void QNetworkReplyHttpImplPrivate::error('
|
||||
'QNetworkReply::NetworkError, const QString&)", "lineno": 1929, '
|
||||
@ -200,7 +200,7 @@ def test_quteprocess_quitting(qtbot, quteproc_process):
|
||||
}
|
||||
),
|
||||
(
|
||||
'{"created": 0, "msecs": 0, "levelname": "DEBUG", "name": "qt", '
|
||||
'{"created": 86400, "msecs": 0, "levelname": "DEBUG", "name": "qt", '
|
||||
'"module": "qxcbxsettings", "funcName": "QXcbXSettings::QXcbXSettings('
|
||||
'QXcbScreen*)", "lineno": 233, "levelno": 10, "message": '
|
||||
'"QXcbXSettings::QXcbXSettings(QXcbScreen*) Failed to get selection '
|
||||
@ -213,7 +213,7 @@ def test_quteprocess_quitting(qtbot, quteproc_process):
|
||||
),
|
||||
(
|
||||
# ResourceWarning
|
||||
'{"created": 0, "msecs": 0, "levelname": "WARNING", '
|
||||
'{"created": 86400, "msecs": 0, "levelname": "WARNING", '
|
||||
'"name": "py.warnings", "module": "app", "funcName": "qt_mainloop", '
|
||||
'"lineno": 121, "levelno": 30, "message": '
|
||||
'".../app.py:121: ResourceWarning: unclosed file <_io.TextIOWrapper '
|
||||
@ -231,7 +231,7 @@ def test_log_line_parse(data, attrs):
|
||||
|
||||
@pytest.mark.parametrize('data, colorized, expect_error, expected', [
|
||||
(
|
||||
{'created': 0, 'msecs': 0, 'levelname': 'DEBUG', 'name': 'foo',
|
||||
{'created': 86400, 'msecs': 0, 'levelname': 'DEBUG', 'name': 'foo',
|
||||
'module': 'bar', 'funcName': 'qux', 'lineno': 10, 'levelno': 10,
|
||||
'message': 'quux'},
|
||||
False, False,
|
||||
@ -239,7 +239,7 @@ def test_log_line_parse(data, attrs):
|
||||
),
|
||||
# Traceback attached
|
||||
(
|
||||
{'created': 0, 'msecs': 0, 'levelname': 'DEBUG', 'name': 'foo',
|
||||
{'created': 86400, 'msecs': 0, 'levelname': 'DEBUG', 'name': 'foo',
|
||||
'module': 'bar', 'funcName': 'qux', 'lineno': 10, 'levelno': 10,
|
||||
'message': 'quux', 'traceback': 'Traceback (most recent call '
|
||||
'last):\n here be dragons'},
|
||||
@ -250,7 +250,7 @@ def test_log_line_parse(data, attrs):
|
||||
),
|
||||
# Colorized
|
||||
(
|
||||
{'created': 0, 'msecs': 0, 'levelname': 'DEBUG', 'name': 'foo',
|
||||
{'created': 86400, 'msecs': 0, 'levelname': 'DEBUG', 'name': 'foo',
|
||||
'module': 'bar', 'funcName': 'qux', 'lineno': 10, 'levelno': 10,
|
||||
'message': 'quux'},
|
||||
True, False,
|
||||
@ -259,7 +259,7 @@ def test_log_line_parse(data, attrs):
|
||||
),
|
||||
# Expected error
|
||||
(
|
||||
{'created': 0, 'msecs': 0, 'levelname': 'ERROR', 'name': 'foo',
|
||||
{'created': 86400, 'msecs': 0, 'levelname': 'ERROR', 'name': 'foo',
|
||||
'module': 'bar', 'funcName': 'qux', 'lineno': 10, 'levelno': 40,
|
||||
'message': 'quux'},
|
||||
False, True,
|
||||
@ -267,7 +267,7 @@ def test_log_line_parse(data, attrs):
|
||||
),
|
||||
# Expected other message (i.e. should make no difference)
|
||||
(
|
||||
{'created': 0, 'msecs': 0, 'levelname': 'DEBUG', 'name': 'foo',
|
||||
{'created': 86400, 'msecs': 0, 'levelname': 'DEBUG', 'name': 'foo',
|
||||
'module': 'bar', 'funcName': 'qux', 'lineno': 10, 'levelno': 10,
|
||||
'message': 'quux'},
|
||||
False, True,
|
||||
@ -275,7 +275,7 @@ def test_log_line_parse(data, attrs):
|
||||
),
|
||||
# Expected error colorized (shouldn't be red)
|
||||
(
|
||||
{'created': 0, 'msecs': 0, 'levelname': 'ERROR', 'name': 'foo',
|
||||
{'created': 86400, 'msecs': 0, 'levelname': 'ERROR', 'name': 'foo',
|
||||
'module': 'bar', 'funcName': 'qux', 'lineno': 10, 'levelno': 40,
|
||||
'message': 'quux'},
|
||||
True, True,
|
||||
|
@ -74,6 +74,8 @@ class Request(testprocess.Line):
|
||||
'/redirect-to': [http.client.FOUND],
|
||||
|
||||
'/cookies/set': [http.client.FOUND],
|
||||
|
||||
'/custom/500-inline': [http.client.INTERNAL_SERVER_ERROR],
|
||||
}
|
||||
for i in range(15):
|
||||
path_to_statuses['/redirect/{}'.format(i)] = [http.client.FOUND]
|
||||
|
@ -122,6 +122,17 @@ def redirect_self():
|
||||
return app.make_response(flask.redirect(flask.url_for('redirect_self')))
|
||||
|
||||
|
||||
@app.route('/custom/500-inline')
|
||||
def internal_error_attachment():
|
||||
"""A 500 error with Content-Disposition: inline."""
|
||||
response = flask.Response(b"", headers={
|
||||
"Content-Type": "application/octet-stream",
|
||||
"Content-Disposition": 'inline; filename="attachment.jpg"',
|
||||
})
|
||||
response.status_code = 500
|
||||
return response
|
||||
|
||||
|
||||
@app.after_request
|
||||
def log_request(response):
|
||||
"""Log a webserver request."""
|
||||
|
@ -77,8 +77,6 @@ def test_ascii_locale(request, httpbin, tmpdir, quteproc_new):
|
||||
https://github.com/qutebrowser/qutebrowser/issues/908
|
||||
https://github.com/qutebrowser/qutebrowser/issues/1726
|
||||
"""
|
||||
if request.config.webengine:
|
||||
pytest.skip("Downloads are not implemented with QtWebEngine yet")
|
||||
args = ['--temp-basedir'] + _base_args(request.config)
|
||||
quteproc_new.start(args, env={'LC_ALL': 'C'})
|
||||
quteproc_new.set_setting('storage', 'download-directory', str(tmpdir))
|
||||
@ -113,9 +111,6 @@ def test_misconfigured_user_dirs(request, httpbin, temp_basedir_env,
|
||||
https://github.com/qutebrowser/qutebrowser/issues/866
|
||||
https://github.com/qutebrowser/qutebrowser/issues/1269
|
||||
"""
|
||||
if request.config.webengine:
|
||||
pytest.skip("Downloads are not implemented with QtWebEngine yet")
|
||||
|
||||
home = tmpdir / 'home'
|
||||
home.ensure(dir=True)
|
||||
temp_basedir_env['HOME'] = str(home)
|
||||
@ -143,12 +138,10 @@ def test_misconfigured_user_dirs(request, httpbin, temp_basedir_env,
|
||||
|
||||
|
||||
def test_no_loglines(request, quteproc_new):
|
||||
"""Test qute:log with --loglines=0."""
|
||||
if request.config.webengine:
|
||||
pytest.skip("qute:log is not implemented with QtWebEngine yet")
|
||||
"""Test qute://log with --loglines=0."""
|
||||
quteproc_new.start(args=['--temp-basedir', '--loglines=0'] +
|
||||
_base_args(request.config))
|
||||
quteproc_new.open_path('qute:log')
|
||||
quteproc_new.open_path('qute://log')
|
||||
assert quteproc_new.get_content() == 'Log output was disabled.'
|
||||
|
||||
|
||||
|
@ -17,8 +17,9 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import datetime
|
||||
import collections
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
|
||||
from PyQt5.QtCore import QUrl
|
||||
import pytest
|
||||
@ -27,30 +28,72 @@ from qutebrowser.browser import history, qutescheme
|
||||
from qutebrowser.utils import objreg
|
||||
|
||||
|
||||
Dates = collections.namedtuple('Dates', ['yesterday', 'today', 'tomorrow'])
|
||||
class TestJavascriptHandler:
|
||||
|
||||
"""Test the qute://javascript endpoint."""
|
||||
|
||||
# Tuples of fake JS files and their content.
|
||||
js_files = [
|
||||
('foo.js', "var a = 'foo';"),
|
||||
('bar.js', "var a = 'bar';"),
|
||||
]
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def patch_read_file(self, monkeypatch):
|
||||
"""Patch utils.read_file to return few fake JS files."""
|
||||
def _read_file(path, binary=False):
|
||||
"""Faked utils.read_file."""
|
||||
assert not binary
|
||||
for filename, content in self.js_files:
|
||||
if path == os.path.join('javascript', filename):
|
||||
return content
|
||||
raise OSError("File not found {}!".format(path))
|
||||
|
||||
monkeypatch.setattr('qutebrowser.utils.utils.read_file', _read_file)
|
||||
|
||||
@pytest.mark.parametrize("filename, content", js_files)
|
||||
def test_qutejavascript(self, filename, content):
|
||||
url = QUrl("qute://javascript/{}".format(filename))
|
||||
_mimetype, data = qutescheme.qute_javascript(url)
|
||||
|
||||
assert data == content
|
||||
|
||||
def test_qutejavascript_404(self):
|
||||
url = QUrl("qute://javascript/404.js")
|
||||
|
||||
with pytest.raises(qutescheme.QuteSchemeOSError):
|
||||
qutescheme.data_for_url(url)
|
||||
|
||||
def test_qutejavascript_empty_query(self):
|
||||
url = QUrl("qute://javascript")
|
||||
|
||||
with pytest.raises(qutescheme.QuteSchemeError):
|
||||
qutescheme.qute_javascript(url)
|
||||
|
||||
|
||||
class TestHistoryHandler:
|
||||
|
||||
"""Test the qute://history endpoint."""
|
||||
|
||||
@pytest.fixture
|
||||
def dates(self):
|
||||
one_day = datetime.timedelta(days=1)
|
||||
today = datetime.datetime.today()
|
||||
tomorrow = today + one_day
|
||||
yesterday = today - one_day
|
||||
return Dates(yesterday, today, tomorrow)
|
||||
@pytest.fixture(scope="module")
|
||||
def now(self):
|
||||
return int(time.time())
|
||||
|
||||
@pytest.fixture
|
||||
def entries(self, dates):
|
||||
today = history.Entry(atime=str(dates.today.timestamp()),
|
||||
url=QUrl('www.today.com'), title='today')
|
||||
tomorrow = history.Entry(atime=str(dates.tomorrow.timestamp()),
|
||||
url=QUrl('www.tomorrow.com'), title='tomorrow')
|
||||
yesterday = history.Entry(atime=str(dates.yesterday.timestamp()),
|
||||
url=QUrl('www.yesterday.com'), title='yesterday')
|
||||
return Dates(yesterday, today, tomorrow)
|
||||
def entries(self, now):
|
||||
"""Create fake history entries."""
|
||||
# create 12 history items spaced 6 hours apart, starting from now
|
||||
entry_count = 12
|
||||
interval = 6 * 60 * 60
|
||||
|
||||
items = []
|
||||
for i in range(entry_count):
|
||||
entry_atime = now - i * interval
|
||||
entry = history.Entry(atime=str(entry_atime),
|
||||
url=QUrl("www.x.com/" + str(i)), title="Page " + str(i))
|
||||
items.insert(0, entry)
|
||||
|
||||
return items
|
||||
|
||||
@pytest.fixture
|
||||
def fake_web_history(self, fake_save_manager, tmpdir):
|
||||
@ -62,78 +105,61 @@ class TestHistoryHandler:
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def fake_history(self, fake_web_history, entries):
|
||||
"""Create fake history for three different days."""
|
||||
fake_web_history._add_entry(entries.yesterday)
|
||||
fake_web_history._add_entry(entries.today)
|
||||
fake_web_history._add_entry(entries.tomorrow)
|
||||
"""Create fake history."""
|
||||
for item in entries:
|
||||
fake_web_history._add_entry(item)
|
||||
fake_web_history.save()
|
||||
|
||||
def test_history_without_query(self):
|
||||
"""Ensure qute://history shows today's history without any query."""
|
||||
_mimetype, data = qutescheme.qute_history(QUrl("qute://history"))
|
||||
key = "<span class=\"date\">{}</span>".format(
|
||||
datetime.date.today().strftime("%a, %d %B %Y"))
|
||||
assert key in data
|
||||
|
||||
def test_history_with_bad_query(self):
|
||||
"""Ensure qute://history shows today's history with bad query."""
|
||||
url = QUrl("qute://history?date=204-blaah")
|
||||
@pytest.mark.parametrize("start_time_offset, expected_item_count", [
|
||||
(0, 4),
|
||||
(24*60*60, 4),
|
||||
(48*60*60, 4),
|
||||
(72*60*60, 0)
|
||||
])
|
||||
def test_qutehistory_data(self, start_time_offset, expected_item_count,
|
||||
now):
|
||||
"""Ensure qute://history/data returns correct items."""
|
||||
start_time = now - start_time_offset
|
||||
url = QUrl("qute://history/data?start_time=" + str(start_time))
|
||||
_mimetype, data = qutescheme.qute_history(url)
|
||||
key = "<span class=\"date\">{}</span>".format(
|
||||
datetime.date.today().strftime("%a, %d %B %Y"))
|
||||
assert key in data
|
||||
items = json.loads(data)
|
||||
items = [item for item in items if 'time' in item] # skip 'next' item
|
||||
|
||||
def test_history_today(self):
|
||||
"""Ensure qute://history shows history for today."""
|
||||
url = QUrl("qute://history")
|
||||
assert len(items) == expected_item_count
|
||||
|
||||
# test times
|
||||
end_time = start_time - 24*60*60
|
||||
for item in items:
|
||||
assert item['time'] <= start_time * 1000
|
||||
assert item['time'] > end_time * 1000
|
||||
|
||||
@pytest.mark.parametrize("start_time_offset, next_time", [
|
||||
(0, 24*60*60),
|
||||
(24*60*60, 48*60*60),
|
||||
(48*60*60, -1),
|
||||
(72*60*60, -1)
|
||||
])
|
||||
def test_qutehistory_next(self, start_time_offset, next_time, now):
|
||||
"""Ensure qute://history/data returns correct items."""
|
||||
start_time = now - start_time_offset
|
||||
url = QUrl("qute://history/data?start_time=" + str(start_time))
|
||||
_mimetype, data = qutescheme.qute_history(url)
|
||||
assert "today" in data
|
||||
assert "tomorrow" not in data
|
||||
assert "yesterday" not in data
|
||||
items = json.loads(data)
|
||||
items = [item for item in items if 'next' in item] # 'next' items
|
||||
assert len(items) == 1
|
||||
|
||||
def test_history_yesterday(self, dates):
|
||||
"""Ensure qute://history shows history for yesterday."""
|
||||
url = QUrl("qute://history?date=" +
|
||||
dates.yesterday.strftime("%Y-%m-%d"))
|
||||
_mimetype, data = qutescheme.qute_history(url)
|
||||
assert "today" not in data
|
||||
assert "tomorrow" not in data
|
||||
assert "yesterday" in data
|
||||
if next_time == -1:
|
||||
assert items[0]["next"] == -1
|
||||
else:
|
||||
assert items[0]["next"] == now - next_time
|
||||
|
||||
def test_history_tomorrow(self, dates):
|
||||
"""Ensure qute://history shows history for tomorrow."""
|
||||
url = QUrl("qute://history?date=" +
|
||||
dates.tomorrow.strftime("%Y-%m-%d"))
|
||||
_mimetype, data = qutescheme.qute_history(url)
|
||||
assert "today" not in data
|
||||
assert "tomorrow" in data
|
||||
assert "yesterday" not in data
|
||||
|
||||
def test_no_next_link_to_future(self, dates):
|
||||
"""Ensure there's no next link pointing to the future."""
|
||||
url = QUrl("qute://history")
|
||||
_mimetype, data = qutescheme.qute_history(url)
|
||||
assert "Next" not in data
|
||||
|
||||
url = QUrl("qute://history?date=" +
|
||||
dates.tomorrow.strftime("%Y-%m-%d"))
|
||||
_mimetype, data = qutescheme.qute_history(url)
|
||||
assert "Next" not in data
|
||||
|
||||
def test_qute_history_benchmark(self, dates, entries, fake_web_history,
|
||||
benchmark):
|
||||
for i in range(100000):
|
||||
def test_qute_history_benchmark(self, fake_web_history, benchmark, now):
|
||||
for t in range(100000): # one history per second
|
||||
entry = history.Entry(
|
||||
atime=str(dates.yesterday.timestamp()),
|
||||
url=QUrl('www.yesterday.com/{}'.format(i)),
|
||||
title='yesterday')
|
||||
atime=str(now - t),
|
||||
url=QUrl('www.x.com/{}'.format(t)),
|
||||
title='x at {}'.format(t))
|
||||
fake_web_history._add_entry(entry)
|
||||
fake_web_history._add_entry(entries.today)
|
||||
fake_web_history._add_entry(entries.tomorrow)
|
||||
|
||||
url = QUrl("qute://history")
|
||||
_mimetype, data = benchmark(qutescheme.qute_history, url)
|
||||
|
||||
assert "today" in data
|
||||
assert "tomorrow" not in data
|
||||
assert "yesterday" not in data
|
||||
url = QUrl("qute://history/data?start_time={}".format(now))
|
||||
_mimetype, _data = benchmark(qutescheme.qute_history, url)
|
||||
|
@ -91,3 +91,10 @@ def test_error_network_reply(qtbot, req):
|
||||
assert reply.readData(1) == b''
|
||||
assert reply.error() == QNetworkReply.UnknownNetworkError
|
||||
assert reply.errorString() == "This is an error"
|
||||
|
||||
|
||||
def test_redirect_network_reply():
|
||||
url = QUrl('https://www.example.com/')
|
||||
reply = networkreply.RedirectNetworkReply(url)
|
||||
assert reply.readData(1) == b''
|
||||
assert reply.attribute(QNetworkRequest.RedirectionTargetAttribute) == url
|
||||
|
@ -17,10 +17,17 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import pytest
|
||||
from PyQt5.QtCore import QUrl, QDateTime
|
||||
from PyQt5.QtNetwork import QNetworkDiskCache, QNetworkCacheMetaData
|
||||
|
||||
from qutebrowser.browser.webkit import cache
|
||||
from qutebrowser.utils import qtutils
|
||||
|
||||
|
||||
pytestmark = pytest.mark.skipif(qtutils.version_check('5.7.1'),
|
||||
reason="QNetworkDiskCache is broken on Qt >= "
|
||||
"5.7.1")
|
||||
|
||||
|
||||
def preload_cache(cache, url='http://www.example.com/', content=b'foobar'):
|
||||
|
@ -18,6 +18,7 @@
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import pytest
|
||||
from PyQt5.QtCore import Qt
|
||||
|
||||
from qutebrowser.mainwindow import messageview
|
||||
from qutebrowser.utils import usertypes
|
||||
@ -114,3 +115,17 @@ def test_replaced_messages(view, replace1, replace2, length):
|
||||
view.show_message(usertypes.MessageLevel.info, 'test', replace=replace1)
|
||||
view.show_message(usertypes.MessageLevel.info, 'test 2', replace=replace2)
|
||||
assert len(view._messages) == length
|
||||
|
||||
|
||||
@pytest.mark.parametrize('button, count', [
|
||||
(Qt.LeftButton, 0),
|
||||
(Qt.MiddleButton, 0),
|
||||
(Qt.RightButton, 0),
|
||||
(Qt.BackButton, 2),
|
||||
])
|
||||
def test_click_messages(qtbot, view, button, count):
|
||||
"""Messages should dissappear when we click on them."""
|
||||
view.show_message(usertypes.MessageLevel.info, 'test mouse click')
|
||||
view.show_message(usertypes.MessageLevel.info, 'test mouse click 2')
|
||||
qtbot.mousePress(view, button)
|
||||
assert len(view._messages) == count
|
||||
|
@ -41,6 +41,7 @@ def proc(qtbot, caplog):
|
||||
p._proc.terminate()
|
||||
if not blocker.signal_triggered:
|
||||
p._proc.kill()
|
||||
p._proc.waitForFinished()
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
|
@ -587,22 +587,20 @@ def test_timeout(qtbot, caplog, qlocalsocket, ipc_server):
|
||||
assert caplog.records[-1].message.startswith("IPC connection timed out")
|
||||
|
||||
|
||||
@pytest.mark.parametrize('method, args, is_warning', [
|
||||
pytest.mark.posix(('on_error', [0], False)),
|
||||
('on_disconnected', [], False),
|
||||
('on_ready_read', [], True),
|
||||
])
|
||||
def test_ipcserver_socket_none(ipc_server, caplog, method, args, is_warning):
|
||||
func = getattr(ipc_server, method)
|
||||
def test_ipcserver_socket_none_readyread(ipc_server, caplog):
|
||||
assert ipc_server._socket is None
|
||||
assert ipc_server._old_socket is None
|
||||
with caplog.at_level(logging.WARNING):
|
||||
ipc_server.on_ready_read()
|
||||
msg = "In on_ready_read with None socket and old_socket!"
|
||||
assert msg in [r.message for r in caplog.records]
|
||||
|
||||
if is_warning:
|
||||
with caplog.at_level(logging.WARNING):
|
||||
func(*args)
|
||||
else:
|
||||
func(*args)
|
||||
|
||||
msg = "In {} with None socket!".format(method)
|
||||
@pytest.mark.posix
|
||||
def test_ipcserver_socket_none_error(ipc_server, caplog):
|
||||
assert ipc_server._socket is None
|
||||
ipc_server.on_error(0)
|
||||
msg = "In on_error with None socket!"
|
||||
assert msg in [r.message for r in caplog.records]
|
||||
|
||||
|
||||
|
@ -44,8 +44,8 @@ def expected_text(*args):
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def keyhint(qtbot, config_stub, key_config_stub):
|
||||
"""Fixture to initialize a KeyHintView."""
|
||||
def keyhint_config(config_stub):
|
||||
"""Fixture providing the necessary config settings for the KeyHintView."""
|
||||
config_stub.data = {
|
||||
'colors': {
|
||||
'keyhint.fg': 'white',
|
||||
@ -55,9 +55,16 @@ def keyhint(qtbot, config_stub, key_config_stub):
|
||||
'fonts': {'keyhint': 'Comic Sans'},
|
||||
'ui': {
|
||||
'keyhint-blacklist': '',
|
||||
'keyhint-delay': 500,
|
||||
'status-position': 'bottom',
|
||||
},
|
||||
}
|
||||
return config_stub
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def keyhint(qtbot, keyhint_config, key_config_stub):
|
||||
"""Fixture to initialize a KeyHintView."""
|
||||
keyhint = KeyHintView(0, None)
|
||||
qtbot.add_widget(keyhint)
|
||||
assert keyhint.text() == ''
|
||||
@ -161,3 +168,16 @@ def test_blacklist_all(keyhint, config_stub, key_config_stub):
|
||||
|
||||
keyhint.update_keyhint('normal', 'a')
|
||||
assert not keyhint.text()
|
||||
|
||||
|
||||
def test_delay(qtbot, stubs, monkeypatch, keyhint_config, key_config_stub):
|
||||
timer = stubs.FakeTimer()
|
||||
monkeypatch.setattr(
|
||||
'qutebrowser.misc.keyhintwidget.usertypes.Timer',
|
||||
lambda *_: timer)
|
||||
interval = 200
|
||||
keyhint_config.set('ui', 'keyhint-delay', interval)
|
||||
key_config_stub.set_bindings_for('normal', OrderedDict([('aa', 'cmd-aa')]))
|
||||
keyhint = KeyHintView(0, None)
|
||||
keyhint.update_keyhint('normal', 'a')
|
||||
assert timer.interval() == interval
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user