Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
dadbf7657f
@ -7,11 +7,14 @@ environment:
|
|||||||
PYTHONUNBUFFERED: 1
|
PYTHONUNBUFFERED: 1
|
||||||
matrix:
|
matrix:
|
||||||
- TESTENV: py34
|
- TESTENV: py34
|
||||||
|
- TESTENV: py36-pyqt58
|
||||||
|
PYTHON: C:\Python36\python.exe
|
||||||
- TESTENV: unittests-frozen
|
- TESTENV: unittests-frozen
|
||||||
- TESTENV: pylint
|
- TESTENV: pylint
|
||||||
|
|
||||||
install:
|
install:
|
||||||
- C:\Python27\python -u scripts\dev\ci\appveyor_install.py
|
- C:\Python27\python -u scripts\dev\ci\appveyor_install.py
|
||||||
|
- set PATH=%PATH%;C:\Python36
|
||||||
|
|
||||||
test_script:
|
test_script:
|
||||||
- C:\Python34\Scripts\tox -e %TESTENV%
|
- C:\Python34\Scripts\tox -e %TESTENV%
|
||||||
|
@ -38,7 +38,8 @@ disable=no-self-use,
|
|||||||
suppressed-message,
|
suppressed-message,
|
||||||
too-many-return-statements,
|
too-many-return-statements,
|
||||||
duplicate-code,
|
duplicate-code,
|
||||||
wrong-import-position
|
wrong-import-position,
|
||||||
|
no-else-return
|
||||||
|
|
||||||
[BASIC]
|
[BASIC]
|
||||||
function-rgx=[a-z_][a-z0-9_]{2,50}$
|
function-rgx=[a-z_][a-z0-9_]{2,50}$
|
||||||
|
@ -21,18 +21,41 @@ Added
|
|||||||
~~~~~
|
~~~~~
|
||||||
|
|
||||||
- New `:clear-messages` command to clear shown messages.
|
- 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
|
Changed
|
||||||
~~~~~~~
|
~~~~~~~
|
||||||
|
|
||||||
- When using QtWebEngine, the underlying Chromium version is now shown in the
|
- When using QtWebEngine, the underlying Chromium version is now shown in the
|
||||||
version info.
|
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
|
Fixed
|
||||||
~~~~~
|
~~~~~
|
||||||
|
|
||||||
- Added a workaround for a black screen with QtWebEngine with some setups
|
- 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
|
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,
|
`:open foodrecipes`, you will see a list of all the food recipe sites,
|
||||||
without having to remember the exact website title or address.
|
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
|
== Troubleshooting
|
||||||
|
|
||||||
Configuration not saved after modifying config.::
|
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
|
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
|
# 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`
|
Make sure you have `python3_4` in your `PYTHON_TARGETS`
|
||||||
(`/etc/portage/make.conf`) and rebuild your system (`emerge -uDNav @world`) if
|
(`/etc/portage/make.conf`) and rebuild your system (`emerge -uDNav @world`) if
|
||||||
necessary.
|
necessary.
|
||||||
|
@ -71,7 +71,8 @@ https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser[mailinglist] at
|
|||||||
mailto:qutebrowser@lists.qutebrowser.org[].
|
mailto:qutebrowser@lists.qutebrowser.org[].
|
||||||
|
|
||||||
There's also a https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser-announce[announce-only mailinglist]
|
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
|
Contributions / Bugs
|
||||||
--------------------
|
--------------------
|
||||||
@ -152,10 +153,11 @@ Contributors, sorted by the number of commits in descending order:
|
|||||||
* Lamar Pavel
|
* Lamar Pavel
|
||||||
* Marshall Lochbaum
|
* Marshall Lochbaum
|
||||||
* Bruno Oliveira
|
* Bruno Oliveira
|
||||||
|
* Martin Tournoij
|
||||||
* Alexander Cogneau
|
* Alexander Cogneau
|
||||||
|
* Imran Sobir
|
||||||
* Felix Van der Jeugt
|
* Felix Van der Jeugt
|
||||||
* Daniel Karbach
|
* Daniel Karbach
|
||||||
* Martin Tournoij
|
|
||||||
* Kevin Velghe
|
* Kevin Velghe
|
||||||
* Raphael Pierzina
|
* Raphael Pierzina
|
||||||
* Joel Torstensson
|
* Joel Torstensson
|
||||||
@ -165,7 +167,6 @@ Contributors, sorted by the number of commits in descending order:
|
|||||||
* Corentin Julé
|
* Corentin Julé
|
||||||
* meles5
|
* meles5
|
||||||
* Philipp Hansch
|
* Philipp Hansch
|
||||||
* Imran Sobir
|
|
||||||
* Panagiotis Ktistakis
|
* Panagiotis Ktistakis
|
||||||
* Artur Shaik
|
* Artur Shaik
|
||||||
* Nathan Isom
|
* Nathan Isom
|
||||||
@ -177,6 +178,7 @@ Contributors, sorted by the number of commits in descending order:
|
|||||||
* Maciej Wołczyk
|
* Maciej Wołczyk
|
||||||
* Spreadyy
|
* Spreadyy
|
||||||
* Alexey "Averrin" Nabrodov
|
* Alexey "Averrin" Nabrodov
|
||||||
|
* pkill9
|
||||||
* nanjekyejoannah
|
* nanjekyejoannah
|
||||||
* avk
|
* avk
|
||||||
* ZDarian
|
* ZDarian
|
||||||
@ -205,7 +207,6 @@ Contributors, sorted by the number of commits in descending order:
|
|||||||
* David Vogt
|
* David Vogt
|
||||||
* Claire Cavanaugh
|
* Claire Cavanaugh
|
||||||
* rikn00
|
* rikn00
|
||||||
* pkill9
|
|
||||||
* kanikaa1234
|
* kanikaa1234
|
||||||
* haitaka
|
* haitaka
|
||||||
* Nick Ginther
|
* Nick Ginther
|
||||||
@ -239,6 +240,7 @@ Contributors, sorted by the number of commits in descending order:
|
|||||||
* adam
|
* adam
|
||||||
* Samir Benmendil
|
* Samir Benmendil
|
||||||
* Regina Hug
|
* Regina Hug
|
||||||
|
* Penaz
|
||||||
* Mathias Fussenegger
|
* Mathias Fussenegger
|
||||||
* Marcelo Santos
|
* Marcelo Santos
|
||||||
* Joel Bradshaw
|
* Joel Bradshaw
|
||||||
@ -253,6 +255,7 @@ Contributors, sorted by the number of commits in descending order:
|
|||||||
* haxwithaxe
|
* haxwithaxe
|
||||||
* evan
|
* evan
|
||||||
* dylan araps
|
* dylan araps
|
||||||
|
* caveman
|
||||||
* addictedtoflames
|
* addictedtoflames
|
||||||
* Xitian9
|
* Xitian9
|
||||||
* Vasilij Schneidermann
|
* Vasilij Schneidermann
|
||||||
@ -284,6 +287,7 @@ Contributors, sorted by the number of commits in descending order:
|
|||||||
* Arseniy Seroka
|
* Arseniy Seroka
|
||||||
* Andy Balaam
|
* Andy Balaam
|
||||||
* Andreas Fischer
|
* Andreas Fischer
|
||||||
|
* Amos Bird
|
||||||
* Akselmo
|
* Akselmo
|
||||||
// QUTE_AUTHORS_END
|
// QUTE_AUTHORS_END
|
||||||
|
|
||||||
|
@ -544,7 +544,7 @@ For `increment` and `decrement`, the number to change the URL by. For `up`, the
|
|||||||
|
|
||||||
[[open]]
|
[[open]]
|
||||||
=== 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.
|
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.
|
* +*-b*+, +*--bg*+: Open in a new background tab.
|
||||||
* +*-t*+, +*--tab*+: Open in a new tab.
|
* +*-t*+, +*--tab*+: Open in a new tab.
|
||||||
* +*-w*+, +*--window*+: Open in a new window.
|
* +*-w*+, +*--window*+: Open in a new window.
|
||||||
|
* +*-s*+, +*--secure*+: Force HTTPS.
|
||||||
|
|
||||||
==== count
|
==== count
|
||||||
The tab index to open the URL in.
|
The tab index to open the URL in.
|
||||||
@ -1598,7 +1599,8 @@ Syntax: +:debug-log-filter 'filters'+
|
|||||||
Change the log filter for console logging.
|
Change the log filter for console logging.
|
||||||
|
|
||||||
==== positional arguments
|
==== 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]]
|
||||||
=== debug-log-level
|
=== debug-log-level
|
||||||
|
@ -36,6 +36,7 @@
|
|||||||
[options="header",width="75%",cols="25%,75%"]
|
[options="header",width="75%",cols="25%,75%"]
|
||||||
|==============
|
|==============
|
||||||
|Setting|Description
|
|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-zoom-levels,zoom-levels>>|The available zoom levels, separated by commas.
|
||||||
|<<ui-default-zoom,default-zoom>>|The default zoom level.
|
|<<ui-default-zoom,default-zoom>>|The default zoom level.
|
||||||
|<<ui-downloads-position,downloads-position>>|Where to show the downloaded files.
|
|<<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-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-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-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-radius,prompt-radius>>|The rounding radius for the edges of prompts.
|
||||||
|<<ui-prompt-filebrowser,prompt-filebrowser>>|Show a filebrowser in upload/download prompts.
|
|<<ui-prompt-filebrowser,prompt-filebrowser>>|Show a filebrowser in upload/download prompts.
|
||||||
|==============
|
|==============
|
||||||
@ -536,6 +538,12 @@ Default: +pass:[path,query]+
|
|||||||
== ui
|
== ui
|
||||||
General options related to the user interface.
|
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]]
|
[[ui-zoom-levels]]
|
||||||
=== zoom-levels
|
=== zoom-levels
|
||||||
The available zoom levels, separated by commas.
|
The available zoom levels, separated by commas.
|
||||||
@ -732,6 +740,12 @@ Globs are supported, so ';*' will blacklist all keychainsstarting with ';'. Use
|
|||||||
|
|
||||||
Default: empty
|
Default: empty
|
||||||
|
|
||||||
|
[[ui-keyhint-delay]]
|
||||||
|
=== keyhint-delay
|
||||||
|
Time from pressing a key to seeing the keyhint dialog (ms)
|
||||||
|
|
||||||
|
Default: +pass:[500]+
|
||||||
|
|
||||||
[[ui-prompt-radius]]
|
[[ui-prompt-radius]]
|
||||||
=== prompt-radius
|
=== prompt-radius
|
||||||
The rounding radius for the edges of prompts.
|
The rounding radius for the edges of prompts.
|
||||||
|
@ -3,6 +3,6 @@
|
|||||||
appdirs==1.4.3
|
appdirs==1.4.3
|
||||||
packaging==16.8
|
packaging==16.8
|
||||||
pyparsing==2.2.0
|
pyparsing==2.2.0
|
||||||
setuptools==34.3.2
|
setuptools==34.3.3
|
||||||
six==1.10.0
|
six==1.10.0
|
||||||
wheel==0.29.0
|
wheel==0.29.0
|
||||||
|
@ -2,10 +2,13 @@
|
|||||||
|
|
||||||
-e git+https://github.com/PyCQA/astroid.git#egg=astroid
|
-e git+https://github.com/PyCQA/astroid.git#egg=astroid
|
||||||
editdistance==0.3.1
|
editdistance==0.3.1
|
||||||
|
github3.py==0.9.6
|
||||||
isort==4.2.5
|
isort==4.2.5
|
||||||
lazy-object-proxy==1.2.2
|
lazy-object-proxy==1.2.2
|
||||||
mccabe==0.6.1
|
mccabe==0.6.1
|
||||||
-e git+https://github.com/PyCQA/pylint.git#egg=pylint
|
-e git+https://github.com/PyCQA/pylint.git#egg=pylint
|
||||||
./scripts/dev/pylint_checkers
|
./scripts/dev/pylint_checkers
|
||||||
requests==2.13.0
|
requests==2.13.0
|
||||||
|
uritemplate==3.0.0
|
||||||
|
uritemplate.py==3.0.2
|
||||||
wrapt==1.10.10
|
wrapt==1.10.10
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
-e git+https://github.com/PyCQA/pylint.git#egg=pylint
|
-e git+https://github.com/PyCQA/pylint.git#egg=pylint
|
||||||
./scripts/dev/pylint_checkers
|
./scripts/dev/pylint_checkers
|
||||||
requests
|
requests
|
||||||
|
github3.py
|
||||||
|
|
||||||
# remove @commit-id for scm installs
|
# remove @commit-id for scm installs
|
||||||
#@ replace: @.*# #
|
#@ replace: @.*# #
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||||
|
|
||||||
PyQt5==5.8.1.1
|
PyQt5==5.8.2
|
||||||
sip==4.19.1
|
sip==4.19.2
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||||
|
|
||||||
beautifulsoup4==4.5.3
|
beautifulsoup4==4.5.3
|
||||||
cheroot==5.3.0
|
cheroot==5.4.0
|
||||||
click==6.7
|
click==6.7
|
||||||
coverage==4.3.4
|
coverage==4.3.4
|
||||||
decorator==4.0.11
|
decorator==4.0.11
|
||||||
EasyProcess==0.2.3
|
EasyProcess==0.2.3
|
||||||
Flask==0.12
|
Flask==0.12.1
|
||||||
glob2==0.5
|
glob2==0.5
|
||||||
httpbin==0.5.0
|
httpbin==0.5.0
|
||||||
hypothesis==3.6.1
|
hypothesis==3.7.0
|
||||||
itsdangerous==0.24
|
itsdangerous==0.24
|
||||||
# Jinja2==2.9.5
|
# Jinja2==2.9.5
|
||||||
Mako==1.0.6
|
Mako==1.0.6
|
||||||
@ -24,7 +24,7 @@ pytest-catchlog==1.2.2
|
|||||||
pytest-cov==2.4.0
|
pytest-cov==2.4.0
|
||||||
pytest-faulthandler==1.3.1
|
pytest-faulthandler==1.3.1
|
||||||
pytest-instafail==0.3.0
|
pytest-instafail==0.3.0
|
||||||
pytest-mock==1.5.0
|
pytest-mock==1.6.0
|
||||||
pytest-qt==2.1.0
|
pytest-qt==2.1.0
|
||||||
pytest-repeat==0.4.1
|
pytest-repeat==0.4.1
|
||||||
pytest-rerunfailures==2.1.0
|
pytest-rerunfailures==2.1.0
|
||||||
|
@ -2,5 +2,5 @@
|
|||||||
|
|
||||||
pluggy==0.4.0
|
pluggy==0.4.0
|
||||||
py==1.4.33
|
py==1.4.33
|
||||||
tox==2.6.0
|
tox==2.7.0
|
||||||
virtualenv==15.1.0
|
virtualenv==15.1.0
|
||||||
|
@ -9,7 +9,7 @@ directly ask me via IRC (nickname thorsten\`) in #qutebrowser on freenode.
|
|||||||
|
|
||||||
$blink!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!$reset
|
$blink!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!$reset
|
||||||
WARNING: the passwords are stored in qutebrowser's
|
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
|
$blink!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!$reset
|
||||||
|
|
||||||
Usage: run as a userscript form qutebrowser, e.g.:
|
Usage: run as a userscript form qutebrowser, e.g.:
|
||||||
|
@ -24,11 +24,12 @@ markers =
|
|||||||
js_prompt: Tests needing to display a javascript prompt
|
js_prompt: Tests needing to display a javascript prompt
|
||||||
this: Used to mark tests during development
|
this: Used to mark tests during development
|
||||||
no_invalid_lines: Don't fail on unparseable lines in end2end tests
|
no_invalid_lines: Don't fail on unparseable lines in end2end tests
|
||||||
|
issue2478: Tests which are broken on Windows with QtWebEngine, https://github.com/qutebrowser/qutebrowser/issues/2478
|
||||||
qt_log_level_fail = WARNING
|
qt_log_level_fail = WARNING
|
||||||
qt_log_ignore =
|
qt_log_ignore =
|
||||||
^SpellCheck: .*
|
^SpellCheck: .*
|
||||||
^SetProcessDpiAwareness failed: .*
|
^SetProcessDpiAwareness failed: .*
|
||||||
^QWindowsWindow::setGeometryDp: Unable to set geometry .*
|
^QWindowsWindow::setGeometry(Dp)?: Unable to set geometry .*
|
||||||
^QProcess: Destroyed while process .* is still running\.
|
^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
|
||||||
^"Method \\"GetAll\\" with signature \\"s\\" on interface \\"org\.freedesktop\.DBus\.Properties\\" doesn't exist\\n"
|
^"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=
|
^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
|
^QPainter::end: Painter ended with \d+ saved states
|
||||||
^QSslSocket: cannot resolve SSLv[23]_(client|server)_method
|
^QSslSocket: cannot resolve SSLv[23]_(client|server)_method
|
||||||
|
^QQuickWidget::invalidateRenderControl could not make context current
|
||||||
xfail_strict = true
|
xfail_strict = true
|
||||||
|
@ -170,11 +170,14 @@ def _init_icon():
|
|||||||
for size in [16, 24, 32, 48, 64, 96, 128, 256, 512]:
|
for size in [16, 24, 32, 48, 64, 96, 128, 256, 512]:
|
||||||
filename = ':/icons/qutebrowser-{}x{}.png'.format(size, size)
|
filename = ':/icons/qutebrowser-{}x{}.png'.format(size, size)
|
||||||
pixmap = QPixmap(filename)
|
pixmap = QPixmap(filename)
|
||||||
qtutils.ensure_not_null(pixmap)
|
if pixmap.isNull():
|
||||||
|
log.init.warning("Failed to load {}".format(filename))
|
||||||
|
else:
|
||||||
fallback_icon.addPixmap(pixmap)
|
fallback_icon.addPixmap(pixmap)
|
||||||
qtutils.ensure_not_null(fallback_icon)
|
|
||||||
icon = QIcon.fromTheme('qutebrowser', fallback_icon)
|
icon = QIcon.fromTheme('qutebrowser', fallback_icon)
|
||||||
qtutils.ensure_not_null(icon)
|
if icon.isNull():
|
||||||
|
log.init.warning("Failed to load icon")
|
||||||
|
else:
|
||||||
qApp.setWindowIcon(icon)
|
qApp.setWindowIcon(icon)
|
||||||
|
|
||||||
|
|
||||||
@ -301,7 +304,7 @@ def _open_startpage(win_id=None):
|
|||||||
window_ids = [win_id]
|
window_ids = [win_id]
|
||||||
else:
|
else:
|
||||||
window_ids = objreg.window_registry
|
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',
|
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||||
window=cur_win_id)
|
window=cur_win_id)
|
||||||
if tabbed_browser.count() == 0:
|
if tabbed_browser.count() == 0:
|
||||||
@ -340,7 +343,8 @@ def _open_quickstart(args):
|
|||||||
|
|
||||||
def _save_version():
|
def _save_version():
|
||||||
"""Save the current version to the state config."""
|
"""Save the current version to the state config."""
|
||||||
state_config = objreg.get('state-config')
|
state_config = objreg.get('state-config', None)
|
||||||
|
if state_config is not None:
|
||||||
state_config['general']['version'] = qutebrowser.__version__
|
state_config['general']['version'] = qutebrowser.__version__
|
||||||
|
|
||||||
|
|
||||||
@ -647,8 +651,8 @@ class Quitter:
|
|||||||
self._shutting_down = True
|
self._shutting_down = True
|
||||||
log.destroy.debug("Shutting down with status {}, session {}...".format(
|
log.destroy.debug("Shutting down with status {}, session {}...".format(
|
||||||
status, session))
|
status, session))
|
||||||
|
session_manager = objreg.get('session-manager', None)
|
||||||
session_manager = objreg.get('session-manager')
|
if session_manager is not None:
|
||||||
if session is not None:
|
if session is not None:
|
||||||
session_manager.save(session, last_window=last_window,
|
session_manager.save(session, last_window=last_window,
|
||||||
load_next_time=True)
|
load_next_time=True)
|
||||||
@ -671,7 +675,7 @@ class Quitter:
|
|||||||
# event loop, so we can shut down immediately.
|
# event loop, so we can shut down immediately.
|
||||||
self._shutdown(status, restart=restart)
|
self._shutdown(status, restart=restart)
|
||||||
|
|
||||||
def _shutdown(self, status, restart):
|
def _shutdown(self, status, restart): # noqa
|
||||||
"""Second stage of shutdown."""
|
"""Second stage of shutdown."""
|
||||||
log.destroy.debug("Stage 2 of shutting down...")
|
log.destroy.debug("Stage 2 of shutting down...")
|
||||||
if qApp is None:
|
if qApp is None:
|
||||||
@ -680,7 +684,9 @@ class Quitter:
|
|||||||
# Remove eventfilter
|
# Remove eventfilter
|
||||||
try:
|
try:
|
||||||
log.destroy.debug("Removing eventfilter...")
|
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:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
# Close all windows
|
# Close all windows
|
||||||
@ -722,7 +728,9 @@ class Quitter:
|
|||||||
# Now we can hopefully quit without segfaults
|
# Now we can hopefully quit without segfaults
|
||||||
log.destroy.debug("Deferring QApplication::exit...")
|
log.destroy.debug("Deferring QApplication::exit...")
|
||||||
objreg.get('signal-handler').deactivate()
|
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
|
# We use a singleshot timer to exit here to minimize the likelihood of
|
||||||
# segfaults.
|
# segfaults.
|
||||||
QTimer.singleShot(0, functools.partial(qApp.exit, status))
|
QTimer.singleShot(0, functools.partial(qApp.exit, status))
|
||||||
|
@ -236,7 +236,7 @@ class CommandDispatcher:
|
|||||||
@cmdutils.argument('url', completion=usertypes.Completion.url)
|
@cmdutils.argument('url', completion=usertypes.Completion.url)
|
||||||
@cmdutils.argument('count', count=True)
|
@cmdutils.argument('count', count=True)
|
||||||
def openurl(self, url=None, implicit=False,
|
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.
|
"""Open a URL in the current/[count]th tab.
|
||||||
|
|
||||||
If the URL contains newlines, each line gets opened in its own 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
|
implicit: If opening a new tab, treat the tab as implicit (like
|
||||||
clicking on a link).
|
clicking on a link).
|
||||||
count: The tab index to open the URL in, or None.
|
count: The tab index to open the URL in, or None.
|
||||||
|
secure: Force HTTPS.
|
||||||
"""
|
"""
|
||||||
if url is None:
|
if url is None:
|
||||||
urls = [config.get('general', 'default-page')]
|
urls = [config.get('general', 'default-page')]
|
||||||
@ -256,6 +257,8 @@ class CommandDispatcher:
|
|||||||
urls = self._parse_url_input(url)
|
urls = self._parse_url_input(url)
|
||||||
|
|
||||||
for i, cur_url in enumerate(urls):
|
for i, cur_url in enumerate(urls):
|
||||||
|
if secure:
|
||||||
|
cur_url.setScheme('https')
|
||||||
if not window and i > 0:
|
if not window and i > 0:
|
||||||
tab = False
|
tab = False
|
||||||
bg = True
|
bg = True
|
||||||
@ -1334,6 +1337,9 @@ class CommandDispatcher:
|
|||||||
scope='window', window=self._win_id)
|
scope='window', window=self._win_id)
|
||||||
target = None
|
target = None
|
||||||
if dest is not 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)
|
target = downloads.FileDownloadTarget(dest)
|
||||||
|
|
||||||
tab = self._current_widget()
|
tab = self._current_widget()
|
||||||
@ -1536,10 +1542,7 @@ class CommandDispatcher:
|
|||||||
backend=usertypes.Backend.QtWebKit)
|
backend=usertypes.Backend.QtWebKit)
|
||||||
def paste_primary(self):
|
def paste_primary(self):
|
||||||
"""Paste the primary selection at cursor position."""
|
"""Paste the primary selection at cursor position."""
|
||||||
try:
|
self.insert_text(utils.get_clipboard(selection=True, fallback=True))
|
||||||
self.insert_text(utils.get_clipboard(selection=True))
|
|
||||||
except utils.SelectionUnsupportedError:
|
|
||||||
self.insert_text(utils.get_clipboard())
|
|
||||||
|
|
||||||
@cmdutils.register(instance='command-dispatcher', maxsplit=0,
|
@cmdutils.register(instance='command-dispatcher', maxsplit=0,
|
||||||
scope='window')
|
scope='window')
|
||||||
@ -1650,21 +1653,22 @@ class CommandDispatcher:
|
|||||||
tab = self._current_widget()
|
tab = self._current_widget()
|
||||||
tab.search.clear()
|
tab.search.clear()
|
||||||
|
|
||||||
|
if not text:
|
||||||
|
return
|
||||||
|
|
||||||
options = {
|
options = {
|
||||||
'ignore_case': config.get('general', 'ignore-case'),
|
'ignore_case': config.get('general', 'ignore-case'),
|
||||||
'reverse': reverse,
|
'reverse': reverse,
|
||||||
}
|
}
|
||||||
|
|
||||||
self._tabbed_browser.search_text = text
|
self._tabbed_browser.search_text = text
|
||||||
self._tabbed_browser.search_options = dict(options)
|
self._tabbed_browser.search_options = dict(options)
|
||||||
|
|
||||||
if text:
|
|
||||||
cb = functools.partial(self._search_cb, tab=tab,
|
cb = functools.partial(self._search_cb, tab=tab,
|
||||||
old_scroll_pos=tab.scroller.pos_px(),
|
old_scroll_pos=tab.scroller.pos_px(),
|
||||||
options=options, text=text, prev=False)
|
options=options, text=text, prev=False)
|
||||||
else:
|
|
||||||
cb = None
|
|
||||||
|
|
||||||
options['result_cb'] = cb
|
options['result_cb'] = cb
|
||||||
|
|
||||||
tab.search.search(text, **options)
|
tab.search.search(text, **options)
|
||||||
|
|
||||||
@cmdutils.register(instance='command-dispatcher', hide=True,
|
@cmdutils.register(instance='command-dispatcher', hide=True,
|
||||||
|
@ -19,11 +19,13 @@
|
|||||||
|
|
||||||
"""Shared QtWebKit/QtWebEngine code for downloads."""
|
"""Shared QtWebKit/QtWebEngine code for downloads."""
|
||||||
|
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
import html
|
import html
|
||||||
import os.path
|
import os.path
|
||||||
import collections
|
import collections
|
||||||
import functools
|
import functools
|
||||||
|
import pathlib
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
import sip
|
import sip
|
||||||
@ -161,6 +163,25 @@ def get_filename_question(*, suggested_filename, url, parent=None):
|
|||||||
return q
|
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):
|
class NoFilenameError(Exception):
|
||||||
|
|
||||||
"""Raised when we can't find out a filename in DownloadTarget."""
|
"""Raised when we can't find out a filename in DownloadTarget."""
|
||||||
@ -507,6 +528,14 @@ class AbstractDownloadItem(QObject):
|
|||||||
"""Retry a failed download."""
|
"""Retry a failed download."""
|
||||||
raise NotImplementedError
|
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):
|
def _get_open_filename(self):
|
||||||
"""Get the filename to open a download.
|
"""Get the filename to open a download.
|
||||||
|
|
||||||
@ -968,7 +997,7 @@ class DownloadModel(QAbstractListModel):
|
|||||||
raise cmdexc.CommandError("No failed downloads!")
|
raise cmdexc.CommandError("No failed downloads!")
|
||||||
else:
|
else:
|
||||||
download = to_retry[0]
|
download = to_retry[0]
|
||||||
download.retry()
|
download.try_retry()
|
||||||
|
|
||||||
def can_clear(self):
|
def can_clear(self):
|
||||||
"""Check if there are finished downloads to clear."""
|
"""Check if there are finished downloads to clear."""
|
||||||
|
@ -134,7 +134,7 @@ class DownloadView(QListView):
|
|||||||
if item.successful:
|
if item.successful:
|
||||||
actions.append(("Open", item.open_file))
|
actions.append(("Open", item.open_file))
|
||||||
else:
|
else:
|
||||||
actions.append(("Retry", item.retry))
|
actions.append(("Retry", item.try_retry))
|
||||||
actions.append(("Remove", item.remove))
|
actions.append(("Remove", item.remove))
|
||||||
else:
|
else:
|
||||||
actions.append(("Cancel", item.cancel))
|
actions.append(("Cancel", item.cancel))
|
||||||
|
@ -155,7 +155,7 @@ class PACResolver:
|
|||||||
raise ParseProxyError("Invalid number of parameters for PROXY")
|
raise ParseProxyError("Invalid number of parameters for PROXY")
|
||||||
host, port = PACResolver._parse_proxy_host(config[1])
|
host, port = PACResolver._parse_proxy_host(config[1])
|
||||||
return QNetworkProxy(QNetworkProxy.HttpProxy, host, port)
|
return QNetworkProxy(QNetworkProxy.HttpProxy, host, port)
|
||||||
elif config[0] == "SOCKS":
|
elif config[0] in ["SOCKS", "SOCKS5"]:
|
||||||
if len(config) != 2:
|
if len(config) != 2:
|
||||||
raise ParseProxyError("Invalid number of parameters for SOCKS")
|
raise ParseProxyError("Invalid number of parameters for SOCKS")
|
||||||
host, port = PACResolver._parse_proxy_host(config[1])
|
host, port = PACResolver._parse_proxy_host(config[1])
|
||||||
|
@ -110,6 +110,9 @@ class DownloadItem(downloads.AbstractDownloadItem):
|
|||||||
def _do_die(self):
|
def _do_die(self):
|
||||||
"""Abort the download and emit an error."""
|
"""Abort the download and emit an error."""
|
||||||
self._read_timer.stop()
|
self._read_timer.stop()
|
||||||
|
if self._reply is None:
|
||||||
|
log.downloads.debug("Reply gone while dying")
|
||||||
|
return
|
||||||
self._reply.downloadProgress.disconnect()
|
self._reply.downloadProgress.disconnect()
|
||||||
self._reply.finished.disconnect()
|
self._reply.finished.disconnect()
|
||||||
self._reply.error.disconnect()
|
self._reply.error.disconnect()
|
||||||
@ -270,7 +273,7 @@ class DownloadItem(downloads.AbstractDownloadItem):
|
|||||||
if self.fileobj is None or self._reply is None:
|
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
|
# 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
|
# got a readyRead after the reply was finished (which happens on
|
||||||
# qute:log for example).
|
# qute://log for example).
|
||||||
return
|
return
|
||||||
if not self._reply.isOpen():
|
if not self._reply.isOpen():
|
||||||
raise OSError("Reply is closed!")
|
raise OSError("Reply is closed!")
|
||||||
|
@ -17,23 +17,26 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
"""Backend-independent qute:* code.
|
"""Backend-independent qute://* code.
|
||||||
|
|
||||||
Module attributes:
|
Module attributes:
|
||||||
pyeval_output: The output of the last :pyeval command.
|
pyeval_output: The output of the last :pyeval command.
|
||||||
_HANDLERS: The handlers registered via decorators.
|
_HANDLERS: The handlers registered via decorators.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import datetime
|
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
import datetime
|
||||||
|
|
||||||
from PyQt5.QtCore import QUrlQuery
|
from PyQt5.QtCore import QUrlQuery, QUrl
|
||||||
|
|
||||||
import qutebrowser
|
import qutebrowser
|
||||||
|
from qutebrowser.config import config
|
||||||
from qutebrowser.utils import (version, utils, jinja, log, message, docutils,
|
from qutebrowser.utils import (version, utils, jinja, log, message, docutils,
|
||||||
objreg)
|
objreg, usertypes, qtutils)
|
||||||
from qutebrowser.misc import objects
|
from qutebrowser.misc import objects
|
||||||
|
|
||||||
|
|
||||||
@ -75,12 +78,25 @@ class QuteSchemeError(Exception):
|
|||||||
super().__init__(errorstring)
|
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:
|
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.
|
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):
|
def wrong_backend_handler(self, url):
|
||||||
"""Show an error page about using the invalid backend."""
|
"""Show an error page about using the invalid backend."""
|
||||||
html = jinja.render('error.html',
|
html = jinja.render('error.html',
|
||||||
title="Error while opening qute:url",
|
title="Error while opening qute://url",
|
||||||
url=url.toDisplayString(),
|
url=url.toDisplayString(),
|
||||||
error='{} is not available with this '
|
error='{} is not available with this '
|
||||||
'backend'.format(url.toDisplayString()),
|
'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".
|
# A url like "qute:foo" is split as "scheme:path", not "scheme:host".
|
||||||
log.misc.debug("url: {}, path: {}, host {}".format(
|
log.misc.debug("url: {}, path: {}, host {}".format(
|
||||||
url.toDisplayString(), path, host))
|
url.toDisplayString(), path, host))
|
||||||
try:
|
if path and not host:
|
||||||
handler = _HANDLERS[path]
|
new_url = QUrl()
|
||||||
except KeyError:
|
new_url.setScheme('qute')
|
||||||
|
new_url.setHost(path)
|
||||||
|
raise Redirect(new_url)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
handler = _HANDLERS[host]
|
handler = _HANDLERS[host]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise NoHandlerFound(url)
|
raise NoHandlerFound(url)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
mimetype, data = handler(url)
|
mimetype, data = handler(url)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
@ -150,7 +170,7 @@ def data_for_url(url):
|
|||||||
|
|
||||||
@add_handler('bookmarks')
|
@add_handler('bookmarks')
|
||||||
def qute_bookmarks(_url):
|
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(),
|
bookmarks = sorted(objreg.get('bookmark-manager').marks.items(),
|
||||||
key=lambda x: x[1]) # Sort by title
|
key=lambda x: x[1]) # Sort by title
|
||||||
quickmarks = sorted(objreg.get('quickmark-manager').marks.items(),
|
quickmarks = sorted(objreg.get('quickmark-manager').marks.items(),
|
||||||
@ -163,90 +183,164 @@ def qute_bookmarks(_url):
|
|||||||
return 'text/html', html
|
return 'text/html', html
|
||||||
|
|
||||||
|
|
||||||
@add_handler('history') # noqa
|
def history_data(start_time): # noqa
|
||||||
def qute_history(url):
|
"""Return history data
|
||||||
"""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)
|
|
||||||
|
|
||||||
one_day = datetime.timedelta(days=1)
|
Arguments:
|
||||||
next_date = curr_date + one_day
|
start_time -- select history starting from this timestamp.
|
||||||
prev_date = curr_date - one_day
|
"""
|
||||||
|
def history_iter(start_time, reverse=False):
|
||||||
|
"""Iterate through the history and get items we're interested.
|
||||||
|
|
||||||
def history_iter(reverse):
|
Arguments:
|
||||||
"""Iterate through the history and get items we're interested in."""
|
reverse -- whether to reverse the history_dict before iterating.
|
||||||
curr_timestamp = time.mktime(curr_date.timetuple())
|
"""
|
||||||
history = objreg.get('web-history').history_dict.values()
|
history = objreg.get('web-history').history_dict.values()
|
||||||
if reverse:
|
if reverse:
|
||||||
history = reversed(history)
|
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:
|
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 redirects
|
||||||
# Skip qute:// links
|
# Skip qute:// links
|
||||||
is_internal = item.url.scheme() == 'qute'
|
if item.redirect or item.url.scheme() == 'qute':
|
||||||
is_not_today = item_atime.date() != curr_date
|
|
||||||
if item.redirect or is_internal or is_not_today:
|
|
||||||
continue
|
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.
|
# Use item's url as title if there's no title.
|
||||||
item_url = item.url.toDisplayString()
|
item_url = item.url.toDisplayString()
|
||||||
item_title = item.title if item.title else item_url
|
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:
|
if sys.hexversion >= 0x03050000:
|
||||||
# On Python >= 3.5 we can reverse the ordereddict in-place and thus
|
# On Python >= 3.5 we can reverse the ordereddict in-place and thus
|
||||||
# apply an additional performance improvement in history_iter.
|
# apply an additional performance improvement in history_iter.
|
||||||
# On my machine, this gets us down from 550ms to 72us with 500k old
|
# On my machine, this gets us down from 550ms to 72us with 500k old
|
||||||
# items.
|
# items.
|
||||||
history = list(history_iter(reverse=True))
|
history = history_iter(start_time, reverse=True)
|
||||||
else:
|
else:
|
||||||
# On Python 3.4, we can't do that, so we'd need to copy the entire
|
# 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 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',
|
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',
|
title='History',
|
||||||
history=history,
|
history=history,
|
||||||
curr_date=curr_date,
|
curr_date=curr_date,
|
||||||
next_date=next_date,
|
next_date=next_date,
|
||||||
prev_date=prev_date,
|
prev_date=prev_date,
|
||||||
today=datetime.date.today())
|
today=datetime.date.today(),
|
||||||
return 'text/html', html
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@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')
|
@add_handler('pyeval')
|
||||||
def qute_pyeval(_url):
|
def qute_pyeval(_url):
|
||||||
"""Handler for qute:pyeval."""
|
"""Handler for qute://pyeval."""
|
||||||
html = jinja.render('pre.html', title='pyeval', content=pyeval_output)
|
html = jinja.render('pre.html', title='pyeval', content=pyeval_output)
|
||||||
return 'text/html', html
|
return 'text/html', html
|
||||||
|
|
||||||
@ -254,7 +348,7 @@ def qute_pyeval(_url):
|
|||||||
@add_handler('version')
|
@add_handler('version')
|
||||||
@add_handler('verizon')
|
@add_handler('verizon')
|
||||||
def qute_version(_url):
|
def qute_version(_url):
|
||||||
"""Handler for qute:version."""
|
"""Handler for qute://version."""
|
||||||
html = jinja.render('version.html', title='Version info',
|
html = jinja.render('version.html', title='Version info',
|
||||||
version=version.version(),
|
version=version.version(),
|
||||||
copyright=qutebrowser.__copyright__)
|
copyright=qutebrowser.__copyright__)
|
||||||
@ -263,7 +357,7 @@ def qute_version(_url):
|
|||||||
|
|
||||||
@add_handler('plainlog')
|
@add_handler('plainlog')
|
||||||
def qute_plainlog(url):
|
def qute_plainlog(url):
|
||||||
"""Handler for qute:plainlog.
|
"""Handler for qute://plainlog.
|
||||||
|
|
||||||
An optional query parameter specifies the minimum log level to print.
|
An optional query parameter specifies the minimum log level to print.
|
||||||
For example, qute://log?level=warning prints warnings and errors.
|
For example, qute://log?level=warning prints warnings and errors.
|
||||||
@ -283,7 +377,7 @@ def qute_plainlog(url):
|
|||||||
|
|
||||||
@add_handler('log')
|
@add_handler('log')
|
||||||
def qute_log(url):
|
def qute_log(url):
|
||||||
"""Handler for qute:log.
|
"""Handler for qute://log.
|
||||||
|
|
||||||
An optional query parameter specifies the minimum log level to print.
|
An optional query parameter specifies the minimum log level to print.
|
||||||
For example, qute://log?level=warning prints warnings and errors.
|
For example, qute://log?level=warning prints warnings and errors.
|
||||||
@ -304,13 +398,13 @@ def qute_log(url):
|
|||||||
|
|
||||||
@add_handler('gpl')
|
@add_handler('gpl')
|
||||||
def qute_gpl(_url):
|
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')
|
return 'text/html', utils.read_file('html/COPYING.html')
|
||||||
|
|
||||||
|
|
||||||
@add_handler('help')
|
@add_handler('help')
|
||||||
def qute_help(url):
|
def qute_help(url):
|
||||||
"""Handler for qute:help."""
|
"""Handler for qute://help."""
|
||||||
try:
|
try:
|
||||||
utils.read_file('html/doc/index.html')
|
utils.read_file('html/doc/index.html')
|
||||||
except OSError:
|
except OSError:
|
||||||
|
@ -78,16 +78,15 @@ class DownloadItem(downloads.AbstractDownloadItem):
|
|||||||
self.stats.finish()
|
self.stats.finish()
|
||||||
elif state == QWebEngineDownloadItem.DownloadInterrupted:
|
elif state == QWebEngineDownloadItem.DownloadInterrupted:
|
||||||
self.successful = False
|
self.successful = False
|
||||||
self.done = True
|
|
||||||
# https://bugreports.qt.io/browse/QTBUG-56839
|
# https://bugreports.qt.io/browse/QTBUG-56839
|
||||||
self.error.emit("Download failed")
|
self._die("Download failed")
|
||||||
self.stats.finish()
|
|
||||||
else:
|
else:
|
||||||
raise ValueError("_on_state_changed was called with unknown state "
|
raise ValueError("_on_state_changed was called with unknown state "
|
||||||
"{}".format(state_name))
|
"{}".format(state_name))
|
||||||
|
|
||||||
def _do_die(self):
|
def _do_die(self):
|
||||||
self._qt_item.downloadProgress.disconnect()
|
self._qt_item.downloadProgress.disconnect()
|
||||||
|
if self._qt_item.state() != QWebEngineDownloadItem.DownloadInterrupted:
|
||||||
self._qt_item.cancel()
|
self._qt_item.cancel()
|
||||||
|
|
||||||
def _do_cancel(self):
|
def _do_cancel(self):
|
||||||
@ -95,7 +94,8 @@ class DownloadItem(downloads.AbstractDownloadItem):
|
|||||||
|
|
||||||
def retry(self):
|
def retry(self):
|
||||||
# https://bugreports.qt.io/browse/QTBUG-56840
|
# https://bugreports.qt.io/browse/QTBUG-56840
|
||||||
raise downloads.UnsupportedOperationError
|
raise downloads.UnsupportedOperationError(
|
||||||
|
"Retrying downloads is unsupported with QtWebEngine")
|
||||||
|
|
||||||
def _get_open_filename(self):
|
def _get_open_filename(self):
|
||||||
return self._filename
|
return self._filename
|
||||||
@ -104,6 +104,7 @@ class DownloadItem(downloads.AbstractDownloadItem):
|
|||||||
raise downloads.UnsupportedOperationError
|
raise downloads.UnsupportedOperationError
|
||||||
|
|
||||||
def _set_tempfile(self, fileobj):
|
def _set_tempfile(self, fileobj):
|
||||||
|
fileobj.close()
|
||||||
self._set_filename(fileobj.name, force_overwrite=True,
|
self._set_filename(fileobj.name, force_overwrite=True,
|
||||||
remember_directory=False)
|
remember_directory=False)
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
"""QtWebEngine specific qute:* handlers and glue code."""
|
"""QtWebEngine specific qute://* handlers and glue code."""
|
||||||
|
|
||||||
from PyQt5.QtCore import QBuffer, QIODevice
|
from PyQt5.QtCore import QBuffer, QIODevice
|
||||||
# pylint: disable=no-name-in-module,import-error,useless-suppression
|
# 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
|
# pylint: enable=no-name-in-module,import-error,useless-suppression
|
||||||
|
|
||||||
from qutebrowser.browser import qutescheme
|
from qutebrowser.browser import qutescheme
|
||||||
from qutebrowser.utils import log
|
from qutebrowser.utils import log, qtutils
|
||||||
|
|
||||||
|
|
||||||
class QuteSchemeHandler(QWebEngineUrlSchemeHandler):
|
class QuteSchemeHandler(QWebEngineUrlSchemeHandler):
|
||||||
|
|
||||||
"""Handle qute:* requests on QtWebEngine."""
|
"""Handle qute://* requests on QtWebEngine."""
|
||||||
|
|
||||||
def install(self, profile):
|
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)
|
profile.installUrlSchemeHandler(b'qute', self)
|
||||||
|
|
||||||
def requestStarted(self, job):
|
def requestStarted(self, job):
|
||||||
@ -58,12 +58,15 @@ class QuteSchemeHandler(QWebEngineUrlSchemeHandler):
|
|||||||
job.fail(QWebEngineUrlRequestJob.UrlNotFound)
|
job.fail(QWebEngineUrlRequestJob.UrlNotFound)
|
||||||
except qutescheme.QuteSchemeOSError:
|
except qutescheme.QuteSchemeOSError:
|
||||||
# FIXME:qtwebengine how do we show a better error here?
|
# 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)
|
job.fail(QWebEngineUrlRequestJob.UrlNotFound)
|
||||||
except qutescheme.QuteSchemeError:
|
except qutescheme.QuteSchemeError:
|
||||||
# FIXME:qtwebengine how do we show a better error here?
|
# 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)
|
job.fail(QWebEngineUrlRequestJob.RequestFailed)
|
||||||
|
except qutescheme.Redirect as e:
|
||||||
|
qtutils.ensure_valid(e.url)
|
||||||
|
job.redirect(e.url)
|
||||||
else:
|
else:
|
||||||
log.misc.debug("Returning {} data".format(mimetype))
|
log.misc.debug("Returning {} data".format(mimetype))
|
||||||
|
|
||||||
|
@ -55,7 +55,7 @@ def init():
|
|||||||
app = QApplication.instance()
|
app = QApplication.instance()
|
||||||
profile = QWebEngineProfile.defaultProfile()
|
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 = webenginequtescheme.QuteSchemeHandler(parent=app)
|
||||||
_qute_scheme_handler.install(profile)
|
_qute_scheme_handler.install(profile)
|
||||||
|
|
||||||
@ -369,8 +369,13 @@ class WebEngineHistory(browsertab.AbstractHistory):
|
|||||||
return self._history.canGoForward()
|
return self._history.canGoForward()
|
||||||
|
|
||||||
def serialize(self):
|
def serialize(self):
|
||||||
# WORKAROUND for https://github.com/qutebrowser/qutebrowser/issues/2289
|
# WORKAROUND (remove this when we bump the requirements to 5.9)
|
||||||
# FIXME:qtwebengine can we get rid of this with Qt 5.8.1?
|
# 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()
|
scheme = self._history.currentItem().url().scheme()
|
||||||
if scheme in ['view-source', 'chrome']:
|
if scheme in ['view-source', 'chrome']:
|
||||||
raise browsertab.WebTabError("Can't serialize special URL!")
|
raise browsertab.WebTabError("Can't serialize special URL!")
|
||||||
|
@ -25,7 +25,7 @@ from PyQt5.QtCore import pyqtSlot
|
|||||||
from PyQt5.QtNetwork import QNetworkDiskCache, QNetworkCacheMetaData
|
from PyQt5.QtNetwork import QNetworkDiskCache, QNetworkCacheMetaData
|
||||||
|
|
||||||
from qutebrowser.config import config
|
from qutebrowser.config import config
|
||||||
from qutebrowser.utils import utils, objreg
|
from qutebrowser.utils import utils, objreg, qtutils
|
||||||
|
|
||||||
|
|
||||||
class DiskCache(QNetworkDiskCache):
|
class DiskCache(QNetworkDiskCache):
|
||||||
@ -53,6 +53,9 @@ class DiskCache(QNetworkDiskCache):
|
|||||||
size = config.get('storage', 'cache-size')
|
size = config.get('storage', 'cache-size')
|
||||||
if size is None:
|
if size is None:
|
||||||
size = 1024 * 1024 * 50 # default from QNetworkDiskCachePrivate
|
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)
|
self.setMaximumCacheSize(size)
|
||||||
|
|
||||||
def _maybe_activate(self):
|
def _maybe_activate(self):
|
||||||
|
@ -19,6 +19,10 @@
|
|||||||
#
|
#
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
# 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.."""
|
"""Special network replies.."""
|
||||||
|
|
||||||
@ -114,9 +118,6 @@ class ErrorNetworkReply(QNetworkReply):
|
|||||||
# the device to avoid getting a warning.
|
# the device to avoid getting a warning.
|
||||||
self.setOpenMode(QIODevice.ReadOnly)
|
self.setOpenMode(QIODevice.ReadOnly)
|
||||||
self.setError(error, errorstring)
|
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.error.emit(error))
|
||||||
QTimer.singleShot(0, lambda: self.finished.emit())
|
QTimer.singleShot(0, lambda: self.finished.emit())
|
||||||
|
|
||||||
@ -137,3 +138,16 @@ class ErrorNetworkReply(QNetworkReply):
|
|||||||
|
|
||||||
def isRunning(self):
|
def isRunning(self):
|
||||||
return False
|
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
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
"""QtWebKit specific qute:* handlers and glue code."""
|
"""QtWebKit specific qute://* handlers and glue code."""
|
||||||
|
|
||||||
import mimetypes
|
import mimetypes
|
||||||
import functools
|
import functools
|
||||||
@ -28,13 +28,13 @@ from PyQt5.QtNetwork import QNetworkReply
|
|||||||
|
|
||||||
from qutebrowser.browser import pdfjs, qutescheme
|
from qutebrowser.browser import pdfjs, qutescheme
|
||||||
from qutebrowser.browser.webkit.network import schemehandler, networkreply
|
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
|
from qutebrowser.config import configexc, configdata
|
||||||
|
|
||||||
|
|
||||||
class QuteSchemeHandler(schemehandler.SchemeHandler):
|
class QuteSchemeHandler(schemehandler.SchemeHandler):
|
||||||
|
|
||||||
"""Scheme handler for qute: URLs."""
|
"""Scheme handler for qute:// URLs."""
|
||||||
|
|
||||||
def createRequest(self, _op, request, _outgoing_data):
|
def createRequest(self, _op, request, _outgoing_data):
|
||||||
"""Create a new request.
|
"""Create a new request.
|
||||||
@ -62,6 +62,9 @@ class QuteSchemeHandler(schemehandler.SchemeHandler):
|
|||||||
except qutescheme.QuteSchemeError as e:
|
except qutescheme.QuteSchemeError as e:
|
||||||
return networkreply.ErrorNetworkReply(request, e.errorstring,
|
return networkreply.ErrorNetworkReply(request, e.errorstring,
|
||||||
e.error, self.parent())
|
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,
|
return networkreply.FixedDataNetworkReply(request, data, mimetype,
|
||||||
self.parent())
|
self.parent())
|
||||||
@ -69,15 +72,15 @@ class QuteSchemeHandler(schemehandler.SchemeHandler):
|
|||||||
|
|
||||||
class JSBridge(QObject):
|
class JSBridge(QObject):
|
||||||
|
|
||||||
"""Javascript-bridge for special qute:... pages."""
|
"""Javascript-bridge for special qute://... pages."""
|
||||||
|
|
||||||
@pyqtSlot(str, str, str)
|
@pyqtSlot(str, str, str)
|
||||||
def set(self, sectname, optname, value):
|
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
|
# https://github.com/qutebrowser/qutebrowser/issues/727
|
||||||
if ((sectname, optname) == ('content', 'allow-javascript') and
|
if ((sectname, optname) == ('content', 'allow-javascript') and
|
||||||
value == 'false'):
|
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.")
|
"as it needs javascript support.")
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
@ -88,7 +91,7 @@ class JSBridge(QObject):
|
|||||||
|
|
||||||
@qutescheme.add_handler('settings', backend=usertypes.Backend.QtWebKit)
|
@qutescheme.add_handler('settings', backend=usertypes.Backend.QtWebKit)
|
||||||
def qute_settings(_url):
|
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)
|
config_getter = functools.partial(objreg.get('config').get, raw=True)
|
||||||
html = jinja.render('settings.html', title='settings', config=configdata,
|
html = jinja.render('settings.html', title='settings', config=configdata,
|
||||||
confget=config_getter)
|
confget=config_getter)
|
||||||
|
@ -23,6 +23,7 @@ import sys
|
|||||||
import functools
|
import functools
|
||||||
import xml.etree.ElementTree
|
import xml.etree.ElementTree
|
||||||
|
|
||||||
|
import sip
|
||||||
from PyQt5.QtCore import (pyqtSlot, Qt, QEvent, QUrl, QPoint, QTimer, QSizeF,
|
from PyQt5.QtCore import (pyqtSlot, Qt, QEvent, QUrl, QPoint, QTimer, QSizeF,
|
||||||
QSize)
|
QSize)
|
||||||
from PyQt5.QtGui import QKeyEvent
|
from PyQt5.QtGui import QKeyEvent
|
||||||
@ -707,6 +708,9 @@ class WebKitTab(browsertab.AbstractTab):
|
|||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def _on_webkit_icon_changed(self):
|
def _on_webkit_icon_changed(self):
|
||||||
"""Emit iconChanged with a QIcon like QWebEngineView does."""
|
"""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())
|
self.icon_changed.emit(self._widget.icon())
|
||||||
|
|
||||||
@pyqtSlot(QWebFrame)
|
@pyqtSlot(QWebFrame)
|
||||||
|
@ -140,7 +140,7 @@ class WebView(QWebView):
|
|||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def add_js_bridge(self):
|
def add_js_bridge(self):
|
||||||
"""Add the javascript bridge for qute:... pages."""
|
"""Add the javascript bridge for qute://... pages."""
|
||||||
frame = self.sender()
|
frame = self.sender()
|
||||||
if not isinstance(frame, QWebFrame):
|
if not isinstance(frame, QWebFrame):
|
||||||
log.webview.error("Got non-QWebFrame {!r} in "
|
log.webview.error("Got non-QWebFrame {!r} in "
|
||||||
|
@ -133,7 +133,8 @@ class CommandRunner(QObject):
|
|||||||
Yields:
|
Yields:
|
||||||
ParseResult tuples.
|
ParseResult tuples.
|
||||||
"""
|
"""
|
||||||
if not text.strip():
|
text = text.strip().lstrip(':').strip()
|
||||||
|
if not text:
|
||||||
raise cmdexc.NoSuchCommandError("No command given")
|
raise cmdexc.NoSuchCommandError("No command given")
|
||||||
|
|
||||||
if aliases:
|
if aliases:
|
||||||
|
@ -186,7 +186,8 @@ def _init_key_config(parent):
|
|||||||
key_config = keyconf.KeyConfigParser(standarddir.config(), 'keys.conf',
|
key_config = keyconf.KeyConfigParser(standarddir.config(), 'keys.conf',
|
||||||
args.relaxed_config,
|
args.relaxed_config,
|
||||||
parent=parent)
|
parent=parent)
|
||||||
except (keyconf.KeyConfigError, UnicodeDecodeError) as e:
|
except (keyconf.KeyConfigError, cmdexc.CommandError,
|
||||||
|
UnicodeDecodeError) as e:
|
||||||
log.init.exception(e)
|
log.init.exception(e)
|
||||||
errstr = "Error while reading key config:\n"
|
errstr = "Error while reading key config:\n"
|
||||||
if e.lineno is not None:
|
if e.lineno is not None:
|
||||||
@ -471,10 +472,9 @@ class ConfigManager(QObject):
|
|||||||
"""Get the whole config as a string."""
|
"""Get the whole config as a string."""
|
||||||
lines = configdata.FIRST_COMMENT.strip('\n').splitlines()
|
lines = configdata.FIRST_COMMENT.strip('\n').splitlines()
|
||||||
for sectname, sect in self.sections.items():
|
for sectname, sect in self.sections.items():
|
||||||
lines.append('\n[{}]'.format(sectname))
|
lines += ['\n'] + self._str_section_desc(sectname)
|
||||||
lines += self._str_section_desc(sectname)
|
lines.append('[{}]'.format(sectname))
|
||||||
lines += self._str_option_desc(sectname, sect)
|
lines += self._str_items(sectname, sect)
|
||||||
lines += self._str_items(sect)
|
|
||||||
return '\n'.join(lines) + '\n'
|
return '\n'.join(lines) + '\n'
|
||||||
|
|
||||||
def _str_section_desc(self, sectname):
|
def _str_section_desc(self, sectname):
|
||||||
@ -489,17 +489,30 @@ class ConfigManager(QObject):
|
|||||||
lines += wrapper.wrap(secline)
|
lines += wrapper.wrap(secline)
|
||||||
return lines
|
return lines
|
||||||
|
|
||||||
def _str_option_desc(self, sectname, sect):
|
def _str_items(self, sectname, sect):
|
||||||
"""Get the option description strings for sect/sectname."""
|
"""Get the option items as string for sect."""
|
||||||
|
lines = []
|
||||||
|
for optname, option in sect.items():
|
||||||
|
value = option.value(startlayer='conf')
|
||||||
|
for c in self.KEY_ESCAPE:
|
||||||
|
if optname.startswith(c):
|
||||||
|
optname = optname.replace(c, self.ESCAPE_CHAR + c, 1)
|
||||||
|
# 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,
|
wrapper = textwrapper.TextWrapper(initial_indent='#' + ' ' * 5,
|
||||||
subsequent_indent='#' + ' ' * 5)
|
subsequent_indent='#' + ' ' * 5)
|
||||||
lines = []
|
lines = []
|
||||||
if not getattr(sect, 'descriptions', None):
|
if not getattr(sect, 'descriptions', None):
|
||||||
return lines
|
return lines
|
||||||
|
|
||||||
for optname, option in sect.items():
|
lines.append('')
|
||||||
|
|
||||||
lines.append('#')
|
|
||||||
typestr = ' ({})'.format(option.typ.get_name())
|
typestr = ' ({})'.format(option.typ.get_name())
|
||||||
lines.append("# {}{}:".format(optname, typestr))
|
lines.append("# {}{}:".format(optname, typestr))
|
||||||
|
|
||||||
@ -508,7 +521,7 @@ class ConfigManager(QObject):
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
log.config.exception("No description for {}.{}!".format(
|
log.config.exception("No description for {}.{}!".format(
|
||||||
sectname, optname))
|
sectname, optname))
|
||||||
continue
|
return []
|
||||||
for descline in desc.splitlines():
|
for descline in desc.splitlines():
|
||||||
lines += wrapper.wrap(descline)
|
lines += wrapper.wrap(descline)
|
||||||
valid_values = option.typ.get_valid_values()
|
valid_values = option.typ.get_valid_values()
|
||||||
@ -524,20 +537,6 @@ class ConfigManager(QObject):
|
|||||||
option.values['default']))
|
option.values['default']))
|
||||||
return lines
|
return lines
|
||||||
|
|
||||||
def _str_items(self, sect):
|
|
||||||
"""Get the option items as string for sect."""
|
|
||||||
lines = []
|
|
||||||
for optname, option in sect.items():
|
|
||||||
value = option.value(startlayer='conf')
|
|
||||||
for c in self.KEY_ESCAPE:
|
|
||||||
if optname.startswith(c):
|
|
||||||
optname = optname.replace(c, self.ESCAPE_CHAR + c, 1)
|
|
||||||
# configparser can't handle = in keys :(
|
|
||||||
optname = optname.replace('=', '<eq>')
|
|
||||||
keyval = '{} = {}'.format(optname, value)
|
|
||||||
lines.append(keyval)
|
|
||||||
return lines
|
|
||||||
|
|
||||||
def _get_real_sectname(self, cp, sectname):
|
def _get_real_sectname(self, cp, sectname):
|
||||||
"""Get an old or new section name based on a configparser.
|
"""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:
|
if section_ is None and option is None:
|
||||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||||
window=win_id)
|
window=win_id)
|
||||||
tabbed_browser.openurl(QUrl('qute:settings'), newtab=False)
|
tabbed_browser.openurl(QUrl('qute://settings'), newtab=False)
|
||||||
return
|
return
|
||||||
|
|
||||||
if option.endswith('?') and option != '?':
|
if option.endswith('?') and option != '?':
|
||||||
|
@ -292,6 +292,12 @@ def data(readonly=False):
|
|||||||
)),
|
)),
|
||||||
|
|
||||||
('ui', sect.KeyValue(
|
('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',
|
('zoom-levels',
|
||||||
SettingValue(typ.List(typ.Perc(minval=0)),
|
SettingValue(typ.List(typ.Perc(minval=0)),
|
||||||
'25%,33%,50%,67%,75%,90%,100%,110%,125%,150%,175%,'
|
'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"
|
"Globs are supported, so ';*' will blacklist all keychains"
|
||||||
"starting with ';'. Use '*' to disable keyhints"),
|
"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',
|
('prompt-radius',
|
||||||
SettingValue(typ.Int(minval=0), '8'),
|
SettingValue(typ.Int(minval=0), '8'),
|
||||||
"The rounding radius for the edges of prompts."),
|
"The rounding radius for the edges of prompts."),
|
||||||
@ -1679,7 +1689,7 @@ KEY_DATA = collections.OrderedDict([
|
|||||||
('home', ['<Ctrl-h>']),
|
('home', ['<Ctrl-h>']),
|
||||||
('stop', ['<Ctrl-s>']),
|
('stop', ['<Ctrl-s>']),
|
||||||
('print', ['<Ctrl-Alt-p>']),
|
('print', ['<Ctrl-Alt-p>']),
|
||||||
('open qute:settings', ['Ss']),
|
('open qute://settings', ['Ss']),
|
||||||
('follow-selected', RETURN_KEYS),
|
('follow-selected', RETURN_KEYS),
|
||||||
('follow-selected -t', ['<Ctrl-Return>', '<Ctrl-Enter>']),
|
('follow-selected -t', ['<Ctrl-Return>', '<Ctrl-Enter>']),
|
||||||
('repeat-command', ['.']),
|
('repeat-command', ['.']),
|
||||||
|
@ -142,9 +142,14 @@ class KeyConfigParser(QObject):
|
|||||||
def save(self):
|
def save(self):
|
||||||
"""Save the key config file."""
|
"""Save the key config file."""
|
||||||
log.destroy.debug("Saving key config to {}".format(self._configfile))
|
log.destroy.debug("Saving key config to {}".format(self._configfile))
|
||||||
with qtutils.savefile_open(self._configfile, encoding='utf-8') as f:
|
|
||||||
|
try:
|
||||||
|
with qtutils.savefile_open(self._configfile,
|
||||||
|
encoding='utf-8') as f:
|
||||||
data = str(self)
|
data = str(self)
|
||||||
f.write(data)
|
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,
|
@cmdutils.register(instance='key-config', maxsplit=1, no_cmd_split=True,
|
||||||
no_replace_variables=True)
|
no_replace_variables=True)
|
||||||
@ -252,6 +257,7 @@ class KeyConfigParser(QObject):
|
|||||||
"""
|
"""
|
||||||
# {'sectname': {'keychain1': 'command', 'keychain2': 'command'}, ...}
|
# {'sectname': {'keychain1': 'command', 'keychain2': 'command'}, ...}
|
||||||
bindings_to_add = collections.OrderedDict()
|
bindings_to_add = collections.OrderedDict()
|
||||||
|
mark_dirty = False
|
||||||
|
|
||||||
for sectname, sect in configdata.KEY_DATA.items():
|
for sectname, sect in configdata.KEY_DATA.items():
|
||||||
sectname = self._normalize_sectname(sectname)
|
sectname = self._normalize_sectname(sectname)
|
||||||
@ -261,6 +267,7 @@ class KeyConfigParser(QObject):
|
|||||||
if not only_new or self._is_new(sectname, command, e):
|
if not only_new or self._is_new(sectname, command, e):
|
||||||
assert e not in bindings_to_add[sectname]
|
assert e not in bindings_to_add[sectname]
|
||||||
bindings_to_add[sectname][e] = command
|
bindings_to_add[sectname][e] = command
|
||||||
|
mark_dirty = True
|
||||||
|
|
||||||
for sectname, sect in bindings_to_add.items():
|
for sectname, sect in bindings_to_add.items():
|
||||||
if not sect:
|
if not sect:
|
||||||
@ -271,7 +278,7 @@ class KeyConfigParser(QObject):
|
|||||||
self._add_binding(sectname, keychain, command)
|
self._add_binding(sectname, keychain, command)
|
||||||
self.changed.emit(sectname)
|
self.changed.emit(sectname)
|
||||||
|
|
||||||
if bindings_to_add:
|
if mark_dirty:
|
||||||
self._mark_config_dirty()
|
self._mark_config_dirty()
|
||||||
|
|
||||||
def _is_new(self, sectname, command, keychain):
|
def _is_new(self, sectname, command, keychain):
|
||||||
@ -315,7 +322,7 @@ class KeyConfigParser(QObject):
|
|||||||
else:
|
else:
|
||||||
line = line.strip()
|
line = line.strip()
|
||||||
self._read_command(line)
|
self._read_command(line)
|
||||||
except KeyConfigError as e:
|
except (KeyConfigError, cmdexc.CommandError) as e:
|
||||||
if relaxed:
|
if relaxed:
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
|
@ -16,43 +16,62 @@ td.time {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
.date {
|
.date {
|
||||||
color: #888;
|
color: #555;
|
||||||
font-size: 14pt;
|
font-size: 12pt;
|
||||||
padding-left: 25px;
|
padding-bottom: 15px;
|
||||||
}
|
|
||||||
|
|
||||||
.pagination-link {
|
|
||||||
display: inline-block;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
margin-top: 10px;
|
|
||||||
padding-right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagination-link > a {
|
|
||||||
color: #333;
|
|
||||||
font-weight: bold;
|
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 %}
|
{% 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>
|
window.onscroll = function(ev) {
|
||||||
<tbody>
|
if ((window.innerHeight + window.scrollY) >= document.body.scrollHeight) {
|
||||||
{% for url, title, time in history %}
|
window.loadHistory();
|
||||||
<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.loadHistory();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% 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._keyhint = keyhintwidget.KeyHintView(self.win_id, self)
|
||||||
self._add_overlay(self._keyhint, self._keyhint.update_geometry)
|
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._prompt_container = prompt.PromptContainer(self.win_id, self)
|
||||||
self._add_overlay(self._prompt_container,
|
self._add_overlay(self._prompt_container,
|
||||||
@ -195,6 +193,9 @@ class MainWindow(QWidget):
|
|||||||
scope='window', window=self.win_id)
|
scope='window', window=self.win_id)
|
||||||
self._prompt_container.hide()
|
self._prompt_container.hide()
|
||||||
|
|
||||||
|
self._messageview = messageview.MessageView(parent=self)
|
||||||
|
self._add_overlay(self._messageview, self._messageview.update_geometry)
|
||||||
|
|
||||||
if geometry is not None:
|
if geometry is not None:
|
||||||
self._load_geometry(geometry)
|
self._load_geometry(geometry)
|
||||||
elif self.win_id == 0:
|
elif self.win_id == 0:
|
||||||
|
@ -130,3 +130,8 @@ class MessageView(QWidget):
|
|||||||
self._last_text = text
|
self._last_text = text
|
||||||
self.show()
|
self.show()
|
||||||
self.update_geometry.emit()
|
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,9 +437,9 @@ class LineEdit(QLineEdit):
|
|||||||
"""Override keyPressEvent to paste primary selection on Shift + Ins."""
|
"""Override keyPressEvent to paste primary selection on Shift + Ins."""
|
||||||
if e.key() == Qt.Key_Insert and e.modifiers() == Qt.ShiftModifier:
|
if e.key() == Qt.Key_Insert and e.modifiers() == Qt.ShiftModifier:
|
||||||
try:
|
try:
|
||||||
text = utils.get_clipboard(selection=True)
|
text = utils.get_clipboard(selection=True, fallback=True)
|
||||||
except utils.ClipboardError: # pragma: no cover
|
except utils.ClipboardError: # pragma: no cover
|
||||||
pass
|
e.ignore()
|
||||||
else:
|
else:
|
||||||
e.accept()
|
e.accept()
|
||||||
self.insert(text)
|
self.insert(text)
|
||||||
@ -644,6 +644,10 @@ class FilenamePrompt(_BasePrompt):
|
|||||||
|
|
||||||
def accept(self, value=None):
|
def accept(self, value=None):
|
||||||
text = value if value is not None else self._lineedit.text()
|
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
|
self.question.answer = text
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -694,9 +698,11 @@ class DownloadFilenamePrompt(FilenamePrompt):
|
|||||||
self._file_model.setFilter(QDir.AllDirs | QDir.Drives | QDir.NoDot)
|
self._file_model.setFilter(QDir.AllDirs | QDir.Drives | QDir.NoDot)
|
||||||
|
|
||||||
def accept(self, value=None):
|
def accept(self, value=None):
|
||||||
text = value if value is not None else self._lineedit.text()
|
done = super().accept(value)
|
||||||
self.question.answer = downloads.FileDownloadTarget(text)
|
answer = self.question.answer
|
||||||
return True
|
if answer is not None:
|
||||||
|
self.question.answer = downloads.FileDownloadTarget(answer)
|
||||||
|
return done
|
||||||
|
|
||||||
def download_open(self, cmdline):
|
def download_open(self, cmdline):
|
||||||
self.question.answer = downloads.OpenFileDownloadTarget(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
|
We don't implement this as generator so we can delete tabs while
|
||||||
iterating over the list.
|
iterating over the list.
|
||||||
"""
|
"""
|
||||||
w = []
|
widgets = []
|
||||||
for i in range(self.count()):
|
for i in range(self.count()):
|
||||||
w.append(self.widget(i))
|
widget = self.widget(i)
|
||||||
return w
|
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')
|
@config.change_filter('ui', 'window-title-format')
|
||||||
def update_window_title(self):
|
def update_window_title(self):
|
||||||
|
@ -118,6 +118,9 @@ class TabWidget(QTabWidget):
|
|||||||
def get_tab_fields(self, idx):
|
def get_tab_fields(self, idx):
|
||||||
"""Get the tab field data."""
|
"""Get the tab field data."""
|
||||||
tab = self.widget(idx)
|
tab = self.widget(idx)
|
||||||
|
if tab is None:
|
||||||
|
log.misc.debug("Got None-tab in get_tab_fields!")
|
||||||
|
|
||||||
page_title = self.page_title(idx)
|
page_title = self.page_title(idx)
|
||||||
|
|
||||||
fields = {}
|
fields = {}
|
||||||
|
@ -171,14 +171,14 @@ class CrashHandler(QObject):
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
pages = self._recover_pages(forgiving=True)
|
pages = self._recover_pages(forgiving=True)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
log.destroy.exception("Error while recovering pages")
|
log.destroy.exception("Error while recovering pages: {}".format(e))
|
||||||
pages = []
|
pages = []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cmd_history = objreg.get('command-history')[-5:]
|
cmd_history = objreg.get('command-history')[-5:]
|
||||||
except Exception:
|
except Exception as e:
|
||||||
log.destroy.exception("Error while getting history: {}")
|
log.destroy.exception("Error while getting history: {}".format(e))
|
||||||
cmd_history = []
|
cmd_history = []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -27,7 +27,7 @@ import getpass
|
|||||||
import binascii
|
import binascii
|
||||||
import hashlib
|
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
|
from PyQt5.QtNetwork import QLocalSocket, QLocalServer, QAbstractSocket
|
||||||
|
|
||||||
import qutebrowser
|
import qutebrowser
|
||||||
@ -182,6 +182,7 @@ class IPCServer(QObject):
|
|||||||
self._server.newConnection.connect(self.handle_connection)
|
self._server.newConnection.connect(self.handle_connection)
|
||||||
|
|
||||||
self._socket = None
|
self._socket = None
|
||||||
|
self._old_socket = None
|
||||||
self._socketopts_ok = os.name == 'nt'
|
self._socketopts_ok = os.name == 'nt'
|
||||||
if self._socketopts_ok: # pragma: no cover
|
if self._socketopts_ok: # pragma: no cover
|
||||||
# If we use setSocketOptions on Unix with Qt < 5.4, we get a
|
# If we use setSocketOptions on Unix with Qt < 5.4, we get a
|
||||||
@ -278,14 +279,7 @@ class IPCServer(QObject):
|
|||||||
log.ipc.debug("Client disconnected from socket 0x{:x}.".format(
|
log.ipc.debug("Client disconnected from socket 0x{:x}.".format(
|
||||||
id(self._socket)))
|
id(self._socket)))
|
||||||
self._timer.stop()
|
self._timer.stop()
|
||||||
if self._socket is None:
|
self._old_socket = self._socket
|
||||||
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._socket = None
|
||||||
# Maybe another connection is waiting.
|
# Maybe another connection is waiting.
|
||||||
self.handle_connection()
|
self.handle_connection()
|
||||||
@ -349,17 +343,23 @@ class IPCServer(QObject):
|
|||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def on_ready_read(self):
|
def on_ready_read(self):
|
||||||
"""Read json data from the client."""
|
"""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
|
# This happens when doing a connection while another one is already
|
||||||
# active for some reason.
|
# active for some reason.
|
||||||
log.ipc.warning("In on_ready_read with None socket!")
|
if self._old_socket is None:
|
||||||
|
log.ipc.warning("In on_ready_read with None socket and "
|
||||||
|
"old_socket!")
|
||||||
return
|
return
|
||||||
|
log.ipc.debug("In on_ready_read with None socket!")
|
||||||
|
socket = self._old_socket
|
||||||
|
else:
|
||||||
|
socket = self._socket
|
||||||
self._timer.stop()
|
self._timer.stop()
|
||||||
while self._socket is not None and self._socket.canReadLine():
|
while socket is not None and socket.canReadLine():
|
||||||
data = bytes(self._socket.readLine())
|
data = bytes(socket.readLine())
|
||||||
self.got_raw.emit(data)
|
self.got_raw.emit(data)
|
||||||
log.ipc.debug("Read from socket 0x{:x}: {!r}".format(
|
log.ipc.debug("Read from socket 0x{:x}: {!r}".format(
|
||||||
id(self._socket), data))
|
id(socket), data))
|
||||||
self._handle_data(data)
|
self._handle_data(data)
|
||||||
self._timer.start()
|
self._timer.start()
|
||||||
|
|
||||||
|
@ -67,7 +67,6 @@ class KeyHintView(QLabel):
|
|||||||
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Minimum)
|
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Minimum)
|
||||||
self.hide()
|
self.hide()
|
||||||
self._show_timer = usertypes.Timer(self, 'keyhint_show')
|
self._show_timer = usertypes.Timer(self, 'keyhint_show')
|
||||||
self._show_timer.setInterval(500)
|
|
||||||
self._show_timer.timeout.connect(self.show)
|
self._show_timer.timeout.connect(self.show)
|
||||||
style.set_register_stylesheet(self)
|
style.set_register_stylesheet(self)
|
||||||
|
|
||||||
@ -108,6 +107,7 @@ class KeyHintView(QLabel):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# delay so a quickly typed keychain doesn't display hints
|
# delay so a quickly typed keychain doesn't display hints
|
||||||
|
self._show_timer.setInterval(config.get('ui', 'keyhint-delay'))
|
||||||
self._show_timer.start()
|
self._show_timer.start()
|
||||||
suffix_color = html.escape(config.get('colors', 'keyhint.fg.suffix'))
|
suffix_color = html.escape(config.get('colors', 'keyhint.fg.suffix'))
|
||||||
|
|
||||||
|
@ -46,9 +46,9 @@ class MinimalLineEditMixin:
|
|||||||
"""Override keyPressEvent to paste primary selection on Shift + Ins."""
|
"""Override keyPressEvent to paste primary selection on Shift + Ins."""
|
||||||
if e.key() == Qt.Key_Insert and e.modifiers() == Qt.ShiftModifier:
|
if e.key() == Qt.Key_Insert and e.modifiers() == Qt.ShiftModifier:
|
||||||
try:
|
try:
|
||||||
text = utils.get_clipboard(selection=True)
|
text = utils.get_clipboard(selection=True, fallback=True)
|
||||||
except utils.ClipboardError:
|
except utils.ClipboardError:
|
||||||
pass
|
e.ignore()
|
||||||
else:
|
else:
|
||||||
e.accept()
|
e.accept()
|
||||||
self.insert(text)
|
self.insert(text)
|
||||||
|
@ -228,7 +228,7 @@ def debug_pyeval(s, quiet=False):
|
|||||||
else:
|
else:
|
||||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||||
window='last-focused')
|
window='last-focused')
|
||||||
tabbed_browser.openurl(QUrl('qute:pyeval'), newtab=True)
|
tabbed_browser.openurl(QUrl('qute://pyeval'), newtab=True)
|
||||||
|
|
||||||
|
|
||||||
@cmdutils.register(debug=True)
|
@cmdutils.register(debug=True)
|
||||||
@ -293,15 +293,24 @@ def debug_log_filter(filters: str):
|
|||||||
"""Change the log filter for console logging.
|
"""Change the log filter for console logging.
|
||||||
|
|
||||||
Args:
|
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):
|
if log.console_filter is None:
|
||||||
log.console_filter.names = filters.split(',')
|
raise cmdexc.CommandError("No log.console_filter. Not attached "
|
||||||
else:
|
"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 "
|
raise cmdexc.CommandError("filters: Invalid value {} - expected one "
|
||||||
"of: {}".format(filters,
|
"of: {}".format(filters,
|
||||||
', '.join(log.LOGGER_NAMES)))
|
', '.join(log.LOGGER_NAMES)))
|
||||||
|
|
||||||
|
log.console_filter.names = filters.split(',')
|
||||||
|
|
||||||
|
|
||||||
@cmdutils.register()
|
@cmdutils.register()
|
||||||
@cmdutils.argument('current_win_id', win_id=True)
|
@cmdutils.argument('current_win_id', win_id=True)
|
||||||
|
@ -182,8 +182,9 @@ def init_log(args):
|
|||||||
root = logging.getLogger()
|
root = logging.getLogger()
|
||||||
global console_filter
|
global console_filter
|
||||||
if console is not None:
|
if console is not None:
|
||||||
|
console_filter = LogFilter(None)
|
||||||
if args.logfilter is not None:
|
if args.logfilter is not None:
|
||||||
console_filter = LogFilter(args.logfilter.split(','))
|
console_filter.names = args.logfilter.split(',')
|
||||||
console.addFilter(console_filter)
|
console.addFilter(console_filter)
|
||||||
root.addHandler(console)
|
root.addHandler(console)
|
||||||
if ram is not None:
|
if ram is not None:
|
||||||
|
@ -174,12 +174,6 @@ def ensure_valid(obj):
|
|||||||
raise QtValueError(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):
|
def check_qdatastream(stream):
|
||||||
"""Check the status of a QDataStream and raise OSError if it's not ok."""
|
"""Check the status of a QDataStream and raise OSError if it's not ok."""
|
||||||
status_to_str = {
|
status_to_str = {
|
||||||
@ -412,14 +406,11 @@ class QtValueError(ValueError):
|
|||||||
|
|
||||||
"""Exception which gets raised by ensure_valid."""
|
"""Exception which gets raised by ensure_valid."""
|
||||||
|
|
||||||
def __init__(self, obj, null=False):
|
def __init__(self, obj):
|
||||||
try:
|
try:
|
||||||
self.reason = obj.errorString()
|
self.reason = obj.errorString()
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
self.reason = None
|
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:
|
if self.reason:
|
||||||
err += ": {}".format(self.reason)
|
err += ": {}".format(self.reason)
|
||||||
|
@ -20,6 +20,7 @@
|
|||||||
"""Other utilities which don't fit anywhere else."""
|
"""Other utilities which don't fit anywhere else."""
|
||||||
|
|
||||||
import io
|
import io
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
import enum
|
import enum
|
||||||
import json
|
import json
|
||||||
@ -53,6 +54,10 @@ class SelectionUnsupportedError(ClipboardError):
|
|||||||
|
|
||||||
"""Raised if [gs]et_clipboard is used and selection=True is unsupported."""
|
"""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):
|
class ClipboardEmptyError(ClipboardError):
|
||||||
|
|
||||||
@ -762,10 +767,22 @@ def set_clipboard(data, selection=False):
|
|||||||
QApplication.clipboard().setText(data, mode=mode)
|
QApplication.clipboard().setText(data, mode=mode)
|
||||||
|
|
||||||
|
|
||||||
def get_clipboard(selection=False):
|
def get_clipboard(selection=False, fallback=False):
|
||||||
"""Get data from the clipboard."""
|
"""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
|
global fake_clipboard
|
||||||
|
if fallback and not selection:
|
||||||
|
raise ValueError("fallback given without selection!")
|
||||||
|
|
||||||
if selection and not supports_selection():
|
if selection and not supports_selection():
|
||||||
|
if fallback:
|
||||||
|
selection = False
|
||||||
|
else:
|
||||||
raise SelectionUnsupportedError
|
raise SelectionUnsupportedError
|
||||||
|
|
||||||
if fake_clipboard is not None:
|
if fake_clipboard is not None:
|
||||||
@ -845,3 +862,21 @@ def open_file(filename, cmdline=None):
|
|||||||
def unused(_arg):
|
def unused(_arg):
|
||||||
"""Function which does nothing to avoid pylint complaining."""
|
"""Function which does nothing to avoid pylint complaining."""
|
||||||
pass
|
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
|
colorama==0.3.7
|
||||||
cssutils==1.0.2
|
cssutils==1.0.2
|
||||||
Jinja2==2.9.5
|
Jinja2==2.9.6
|
||||||
MarkupSafe==1.0
|
MarkupSafe==1.0
|
||||||
Pygments==2.2.0
|
Pygments==2.2.0
|
||||||
pyPEG2==2.15.2
|
pyPEG2==2.15.2
|
||||||
|
@ -28,6 +28,7 @@ CI machines.
|
|||||||
|
|
||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import os
|
||||||
import time
|
import time
|
||||||
import subprocess
|
import subprocess
|
||||||
import urllib
|
import urllib
|
||||||
@ -44,6 +45,16 @@ def pip_install(pkg):
|
|||||||
pkg])
|
pkg])
|
||||||
|
|
||||||
|
|
||||||
|
print("Installing tox")
|
||||||
|
pip_install('pip')
|
||||||
|
pip_install(r'-rmisc\requirements\requirements-tox.txt')
|
||||||
|
|
||||||
|
print("Linking Python...")
|
||||||
|
with open(r'C:\Windows\system32\python3.bat', 'w') as f:
|
||||||
|
f.write(r'@C:\Python34\python %*')
|
||||||
|
|
||||||
|
|
||||||
|
if '-pyqt' not in os.environ['TESTENV']:
|
||||||
print("Getting PyQt5...")
|
print("Getting PyQt5...")
|
||||||
qt_version = '5.5.1'
|
qt_version = '5.5.1'
|
||||||
pyqt_version = '5.5.1'
|
pyqt_version = '5.5.1'
|
||||||
@ -61,12 +72,4 @@ except (OSError, IOError):
|
|||||||
print("Installing PyQt5...")
|
print("Installing PyQt5...")
|
||||||
subprocess.check_call([r'C:\install-PyQt5.exe', '/S'])
|
subprocess.check_call([r'C:\install-PyQt5.exe', '/S'])
|
||||||
|
|
||||||
print("Installing tox")
|
|
||||||
pip_install('pip')
|
|
||||||
pip_install(r'-rmisc\requirements\requirements-tox.txt')
|
|
||||||
|
|
||||||
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')
|
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.QtWebKit.QWebPage.ErrorPageExtensionReturn().fileNames'
|
||||||
yield 'PyQt5.QtWidgets.QStyleOptionViewItem.backgroundColor'
|
yield 'PyQt5.QtWidgets.QStyleOptionViewItem.backgroundColor'
|
||||||
|
|
||||||
## qute:... handlers
|
## qute://... handlers
|
||||||
for name in qutescheme._HANDLERS: # pylint: disable=protected-access
|
for name in qutescheme._HANDLERS: # pylint: disable=protected-access
|
||||||
yield 'qutebrowser.browser.qutescheme.qute_' + name
|
yield 'qutebrowser.browser.qutescheme.qute_' + name
|
||||||
|
|
||||||
|
@ -46,7 +46,7 @@ def parse_args():
|
|||||||
if QWebEngineView is not None:
|
if QWebEngineView is not None:
|
||||||
parser.add_argument('--webengine', help='Use QtWebEngine',
|
parser.add_argument('--webengine', help='Use QtWebEngine',
|
||||||
default=False, action='store_true')
|
default=False, action='store_true')
|
||||||
return parser.parse_args()
|
return parser.parse_known_args()[0]
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
@ -44,7 +44,7 @@ hypothesis.settings.register_profile('default',
|
|||||||
hypothesis.settings.load_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."""
|
"""Apply a skip marker to a given item."""
|
||||||
markers = [
|
markers = [
|
||||||
('posix', os.name != 'posix', "Requires a POSIX os"),
|
('posix', os.name != 'posix', "Requires a POSIX os"),
|
||||||
@ -57,6 +57,8 @@ def _apply_platform_markers(item):
|
|||||||
('frozen', not getattr(sys, 'frozen', False),
|
('frozen', not getattr(sys, 'frozen', False),
|
||||||
"Can only run when frozen"),
|
"Can only run when frozen"),
|
||||||
('ci', 'CI' not in os.environ, "Only runs on CI."),
|
('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:
|
for searched_marker, condition, default_reason in markers:
|
||||||
@ -117,7 +119,7 @@ def pytest_collection_modifyitems(config, items):
|
|||||||
if module_root_dir == 'end2end':
|
if module_root_dir == 'end2end':
|
||||||
item.add_marker(pytest.mark.end2end)
|
item.add_marker(pytest.mark.end2end)
|
||||||
|
|
||||||
_apply_platform_markers(item)
|
_apply_platform_markers(config, item)
|
||||||
if item.get_marker('xfail_norun'):
|
if item.get_marker('xfail_norun'):
|
||||||
item.add_marker(pytest.mark.xfail(run=False))
|
item.add_marker(pytest.mark.xfail(run=False))
|
||||||
if item.get_marker('js_prompt'):
|
if item.get_marker('js_prompt'):
|
||||||
@ -192,21 +194,3 @@ def pytest_runtest_makereport(item, call):
|
|||||||
outcome = yield
|
outcome = yield
|
||||||
rep = outcome.get_result()
|
rep = outcome.get_result()
|
||||||
setattr(item, "rep_" + rep.when, rep)
|
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>
|
<title>Test title</title>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
window.onload = function () {
|
window.onload = function () {
|
||||||
console.log("Calling history.replaceState");
|
|
||||||
history.replaceState({}, '', window.location + '?state=2');
|
history.replaceState({}, '', window.location + '?state=2');
|
||||||
|
console.log("Called history.replaceState");
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
|
||||||
|
|
||||||
Feature: Ad blocking
|
Feature: Ad blocking
|
||||||
|
|
||||||
Scenario: Simple adblock update
|
Scenario: Simple adblock update
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
|
||||||
|
|
||||||
Feature: Going back and forward.
|
Feature: Going back and forward.
|
||||||
Testing the :back/:forward commands.
|
Testing the :back/:forward commands.
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
|
||||||
|
|
||||||
Feature: Caret mode
|
Feature: Caret mode
|
||||||
In caret mode, the user can select and yank text using the keyboard.
|
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
|
Feature: Using completion
|
||||||
|
|
||||||
Scenario: No warnings when completing with one entry (#1600)
|
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(':window-only')
|
||||||
quteproc.send_cmd(':tab-only')
|
quteproc.send_cmd(':tab-only')
|
||||||
quteproc.send_cmd(':tab-close')
|
quteproc.send_cmd(':tab-close')
|
||||||
|
quteproc.wait_for_load_finished_url('about:blank')
|
||||||
|
|
||||||
|
|
||||||
@bdd.given('pdfjs is available')
|
@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.
|
Feature: Downloading things from a website.
|
||||||
|
|
||||||
Background:
|
Background:
|
||||||
@ -78,6 +80,7 @@ Feature: Downloading things from a website.
|
|||||||
Scenario: Downloading with SSL errors (issue 1413)
|
Scenario: Downloading with SSL errors (issue 1413)
|
||||||
When I clear SSL errors
|
When I clear SSL errors
|
||||||
And I set network -> ssl-strict to ask
|
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 download an SSL page
|
||||||
And I wait for "Entering mode KeyMode.* (reason: question asked)" in the log
|
And I wait for "Entering mode KeyMode.* (reason: question asked)" in the log
|
||||||
And I run :prompt-accept
|
And I run :prompt-accept
|
||||||
@ -126,6 +129,34 @@ Feature: Downloading things from a website.
|
|||||||
Then the downloaded file ../foo should not exist
|
Then the downloaded file ../foo should not exist
|
||||||
And the downloaded file foo should 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
|
## :download-retry
|
||||||
|
|
||||||
Scenario: Retrying a failed download
|
Scenario: Retrying a failed download
|
||||||
@ -137,6 +168,14 @@ Feature: Downloading things from a website.
|
|||||||
does-not-exist
|
does-not-exist
|
||||||
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
|
Scenario: Retrying with count
|
||||||
When I run :download http://localhost:(port)/data/downloads/download.bin
|
When I run :download http://localhost:(port)/data/downloads/download.bin
|
||||||
And I run :download http://localhost:(port)/does-not-exist
|
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 run :follow-hint 0
|
||||||
And I wait until the download is finished
|
And I wait until the download is finished
|
||||||
Then the downloaded file user-agent should contain Safari/
|
Then the downloaded file user-agent should contain Safari/
|
||||||
|
|
||||||
|
@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
|
Feature: Opening external editors
|
||||||
|
|
||||||
## :edit-url
|
## :edit-url
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
|
||||||
|
|
||||||
Feature: Using hints
|
Feature: Using hints
|
||||||
|
|
||||||
# https://bugreports.qt.io/browse/QTBUG-58381
|
# https://bugreports.qt.io/browse/QTBUG-58381
|
||||||
@ -45,17 +47,17 @@ Feature: Using hints
|
|||||||
|
|
||||||
Scenario: Using :hint spawn with flags and -- (issue 797)
|
Scenario: Using :hint spawn with flags and -- (issue 797)
|
||||||
When I open data/hints/html/simple.html
|
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
|
Then the message "Command exited successfully." should be shown
|
||||||
|
|
||||||
Scenario: Using :hint spawn with flags (issue 797)
|
Scenario: Using :hint spawn with flags (issue 797)
|
||||||
When I open data/hints/html/simple.html
|
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
|
Then the message "Command exited successfully." should be shown
|
||||||
|
|
||||||
Scenario: Using :hint spawn with flags and --rapid (issue 797)
|
Scenario: Using :hint spawn with flags and --rapid (issue 797)
|
||||||
When I open data/hints/html/simple.html
|
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
|
Then the message "Command exited successfully." should be shown
|
||||||
|
|
||||||
@posix
|
@posix
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
|
||||||
|
|
||||||
Feature: Page history
|
Feature: Page history
|
||||||
|
|
||||||
Make sure the global page history is saved correctly.
|
Make sure the global page history is saved correctly.
|
||||||
@ -77,7 +79,15 @@ Feature: Page history
|
|||||||
Scenario: Listing history
|
Scenario: Listing history
|
||||||
When I open data/numbers/3.txt
|
When I open data/numbers/3.txt
|
||||||
And I open data/numbers/4.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 "3.txt"
|
||||||
Then the page should contain the plaintext "4.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
|
Feature: Invoking a new process
|
||||||
Simulate what happens when running qutebrowser with an existing instance
|
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
|
Feature: Javascript stuff
|
||||||
|
|
||||||
Integration with javascript.
|
Integration with javascript.
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
|
||||||
|
|
||||||
Feature: Keyboard input
|
Feature: Keyboard input
|
||||||
|
|
||||||
Tests for :bind and :unbind, :clear-keychain and other 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
|
Feature: Setting positional marks
|
||||||
|
|
||||||
Background:
|
Background:
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
|
||||||
|
|
||||||
Feature: Various utility commands.
|
Feature: Various utility commands.
|
||||||
|
|
||||||
## :set-cmd-text
|
## :set-cmd-text
|
||||||
@ -79,6 +81,7 @@ Feature: Various utility commands.
|
|||||||
And I run :jseval --world=1 console.log("Hello from JS!");
|
And I run :jseval --world=1 console.log("Hello from JS!");
|
||||||
And I wait for the javascript message "Hello from JS!"
|
And I wait for the javascript message "Hello from JS!"
|
||||||
Then "Ignoring world ID 1" should be logged
|
Then "Ignoring world ID 1" should be logged
|
||||||
|
And "No output or error" should be logged
|
||||||
|
|
||||||
@qtwebkit_skip
|
@qtwebkit_skip
|
||||||
Scenario: :jseval uses separate world without --world
|
Scenario: :jseval uses separate world without --world
|
||||||
@ -87,6 +90,7 @@ Feature: Various utility commands.
|
|||||||
And I run :jseval do_log()
|
And I run :jseval do_log()
|
||||||
Then the javascript message "Hello from the page!" should not be logged
|
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 the javascript message "Uncaught ReferenceError: do_log is not defined" should be logged
|
||||||
|
And "No output or error" should be logged
|
||||||
|
|
||||||
@qtwebkit_skip
|
@qtwebkit_skip
|
||||||
Scenario: :jseval using the main world
|
Scenario: :jseval using the main world
|
||||||
@ -94,6 +98,7 @@ Feature: Various utility commands.
|
|||||||
And I open data/misc/jseval.html
|
And I open data/misc/jseval.html
|
||||||
And I run :jseval --world 0 do_log()
|
And I run :jseval --world 0 do_log()
|
||||||
Then the javascript message "Hello from the page!" should be logged
|
Then the javascript message "Hello from the page!" should be logged
|
||||||
|
And "No output or error" should be logged
|
||||||
|
|
||||||
@qtwebkit_skip
|
@qtwebkit_skip
|
||||||
Scenario: :jseval using the main world as name
|
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 open data/misc/jseval.html
|
||||||
And I run :jseval --world main do_log()
|
And I run :jseval --world main do_log()
|
||||||
Then the javascript message "Hello from the page!" should be logged
|
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
|
Scenario: :jseval --file using a file that exists as js-code
|
||||||
When I set general -> log-javascript-console to info
|
When I set general -> log-javascript-console to info
|
||||||
And I run :jseval --file (testdata)/misc/jseval_file.js
|
And I run :jseval --file (testdata)/misc/jseval_file.js
|
||||||
Then the javascript message "Hello from JS!" should be logged
|
Then the javascript message "Hello from JS!" should be logged
|
||||||
And the javascript message "Hello again 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
|
Scenario: :jseval --file using a file that doesn't exist as js-code
|
||||||
When I run :jseval --file nonexistentfile
|
When I run :jseval --file nonexistentfile
|
||||||
@ -398,12 +405,12 @@ Feature: Various utility commands.
|
|||||||
# :pyeval
|
# :pyeval
|
||||||
Scenario: Running :pyeval
|
Scenario: Running :pyeval
|
||||||
When I run :debug-pyeval 1+1
|
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"
|
Then the page should contain the plaintext "2"
|
||||||
|
|
||||||
Scenario: Causing exception in :pyeval
|
Scenario: Causing exception in :pyeval
|
||||||
When I run :debug-pyeval 1/0
|
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"
|
Then the page should contain the plaintext "ZeroDivisionError"
|
||||||
|
|
||||||
Scenario: Running :pyeval with --quiet
|
Scenario: Running :pyeval with --quiet
|
||||||
@ -505,12 +512,12 @@ Feature: Various utility commands.
|
|||||||
When I run :messages cataclysmic
|
When I run :messages cataclysmic
|
||||||
Then the error "Invalid log level cataclysmic!" should be shown
|
Then the error "Invalid log level cataclysmic!" should be shown
|
||||||
|
|
||||||
Scenario: Using qute:log directly
|
Scenario: Using qute://log directly
|
||||||
When I open qute:log
|
When I open qute://log
|
||||||
Then no crash should happen
|
Then no crash should happen
|
||||||
|
|
||||||
Scenario: Using qute:plainlog directly
|
Scenario: Using qute://plainlog directly
|
||||||
When I open qute:plainlog
|
When I open qute://plainlog
|
||||||
Then no crash should happen
|
Then no crash should happen
|
||||||
|
|
||||||
Scenario: Using :messages without messages
|
Scenario: Using :messages without messages
|
||||||
@ -531,6 +538,16 @@ Feature: Various utility commands.
|
|||||||
When I run :message-i "Hello World" (invalid command)
|
When I run :message-i "Hello World" (invalid command)
|
||||||
Then the error "message-i: no such command" should be shown
|
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
|
# We can't run :message-i as startup command, so we use
|
||||||
# :set-cmd-text
|
# :set-cmd-text
|
||||||
|
|
||||||
@ -673,7 +690,8 @@ Feature: Various utility commands.
|
|||||||
|
|
||||||
## Renderer crashes
|
## Renderer crashes
|
||||||
|
|
||||||
@qtwebkit_skip @no_invalid_lines
|
# Skipped on Windows as "... has stopped working" hangs.
|
||||||
|
@qtwebkit_skip @no_invalid_lines @posix
|
||||||
Scenario: Renderer crash
|
Scenario: Renderer crash
|
||||||
When I run :open -t chrome://crash
|
When I run :open -t chrome://crash
|
||||||
Then the error "Renderer process crashed" should be shown
|
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
|
Feature: Using :navigate
|
||||||
|
|
||||||
Scenario: :navigate with invalid argument
|
Scenario: :navigate with invalid argument
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
|
||||||
|
|
||||||
Feature: Opening pages
|
Feature: Opening pages
|
||||||
|
|
||||||
Scenario: :open with URL
|
Scenario: :open with URL
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
|
||||||
|
|
||||||
Feature: Prompts
|
Feature: Prompts
|
||||||
Various prompts (javascript, SSL errors, authentification, etc.)
|
Various prompts (javascript, SSL errors, authentification, etc.)
|
||||||
|
|
||||||
@ -150,12 +152,13 @@ Feature: Prompts
|
|||||||
Scenario: Pasting via shift-insert without it being supported
|
Scenario: Pasting via shift-insert without it being supported
|
||||||
When selection is not supported
|
When selection is not supported
|
||||||
And I put "insert test" into the primary selection
|
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 open data/prompt/jsprompt.html
|
||||||
And I run :click-element id button
|
And I run :click-element id button
|
||||||
And I wait for a prompt
|
And I wait for a prompt
|
||||||
And I press the keys "<Shift-Insert>"
|
And I press the keys "<Shift-Insert>"
|
||||||
And I run :prompt-accept
|
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
|
@js_prompt
|
||||||
Scenario: Using content -> ignore-javascript-prompt
|
Scenario: Using content -> ignore-javascript-prompt
|
||||||
@ -174,6 +177,7 @@ Feature: Prompts
|
|||||||
Then the error "Certificate error: *" should be shown
|
Then the error "Certificate error: *" should be shown
|
||||||
And the page should contain the plaintext "Hello World via SSL!"
|
And the page should contain the plaintext "Hello World via SSL!"
|
||||||
|
|
||||||
|
@issue2478
|
||||||
Scenario: SSL error with ssl-strict = true
|
Scenario: SSL error with ssl-strict = true
|
||||||
When I clear SSL errors
|
When I clear SSL errors
|
||||||
And I set network -> ssl-strict to true
|
And I set network -> ssl-strict to true
|
||||||
@ -189,6 +193,7 @@ Feature: Prompts
|
|||||||
And I wait until the SSL page finished loading
|
And I wait until the SSL page finished loading
|
||||||
Then the page should contain the plaintext "Hello World via SSL!"
|
Then the page should contain the plaintext "Hello World via SSL!"
|
||||||
|
|
||||||
|
@issue2478
|
||||||
Scenario: SSL error with ssl-strict = ask -> no
|
Scenario: SSL error with ssl-strict = ask -> no
|
||||||
When I clear SSL errors
|
When I clear SSL errors
|
||||||
And I set network -> ssl-strict to ask
|
And I set network -> ssl-strict to ask
|
||||||
@ -197,6 +202,7 @@ Feature: Prompts
|
|||||||
And I run :prompt-accept no
|
And I run :prompt-accept no
|
||||||
Then a SSL error page should be shown
|
Then a SSL error page should be shown
|
||||||
|
|
||||||
|
@issue2478
|
||||||
Scenario: SSL error with ssl-strict = ask -> abort
|
Scenario: SSL error with ssl-strict = ask -> abort
|
||||||
When I clear SSL errors
|
When I clear SSL errors
|
||||||
And I set network -> ssl-strict to ask
|
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
|
Feature: Scrolling
|
||||||
Tests the various scroll commands.
|
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
|
Feature: Searching on a page
|
||||||
Searching text on the page (like /foo) with different options.
|
Searching text on the page (like /foo) with different options.
|
||||||
|
|
||||||
@ -110,6 +112,15 @@ Feature: Searching on a page
|
|||||||
And I run :yank selection
|
And I run :yank selection
|
||||||
Then the clipboard should contain "foo"
|
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
|
## :search-prev
|
||||||
|
|
||||||
Scenario: Jumping to previous match
|
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
|
Feature: Saving and loading sessions
|
||||||
|
|
||||||
Background:
|
Background:
|
||||||
@ -198,7 +200,7 @@ Feature: Saving and loading sessions
|
|||||||
|
|
||||||
Scenario: Saving a session with a page using history.replaceState()
|
Scenario: Saving a session with a page using history.replaceState()
|
||||||
When I open data/sessions/history_replace_state.html without waiting
|
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:
|
And the session should look like:
|
||||||
windows:
|
windows:
|
||||||
- tabs:
|
- tabs:
|
||||||
@ -212,7 +214,7 @@ Feature: Saving and loading sessions
|
|||||||
Scenario: Saving a session with a page using history.replaceState() and navigating away (qtwebkit)
|
Scenario: Saving a session with a page using history.replaceState() and navigating away (qtwebkit)
|
||||||
When I open data/sessions/history_replace_state.html
|
When I open data/sessions/history_replace_state.html
|
||||||
And I open data/hello.txt
|
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:
|
And the session should look like:
|
||||||
windows:
|
windows:
|
||||||
- tabs:
|
- tabs:
|
||||||
@ -229,7 +231,7 @@ Feature: Saving and loading sessions
|
|||||||
@qtwebkit_skip
|
@qtwebkit_skip
|
||||||
Scenario: Saving a session with a page using history.replaceState() and navigating away
|
Scenario: Saving a session with a page using history.replaceState() and navigating away
|
||||||
When I open data/sessions/history_replace_state.html without waiting
|
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
|
And I open data/hello.txt
|
||||||
Then the session should look like:
|
Then the session should look like:
|
||||||
windows:
|
windows:
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
|
||||||
|
|
||||||
Feature: Setting settings.
|
Feature: Setting settings.
|
||||||
|
|
||||||
Background:
|
Background:
|
||||||
@ -76,15 +78,15 @@ Feature: Setting settings.
|
|||||||
When I run :set -t colors statusbar.bg green
|
When I run :set -t colors statusbar.bg green
|
||||||
Then colors -> statusbar.bg should be 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)
|
# (and displays a page saying it's not available)
|
||||||
Scenario: Opening qute:settings
|
Scenario: Opening qute://settings
|
||||||
When I run :set
|
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:
|
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
|
Scenario: Focusing input fields in qute://settings and entering valid value
|
||||||
When I set general -> ignore-case to false
|
When I set general -> ignore-case to false
|
||||||
And I open qute://settings
|
And I open qute://settings
|
||||||
@ -99,7 +101,7 @@ Feature: Setting settings.
|
|||||||
And I press the key "<Tab>"
|
And I press the key "<Tab>"
|
||||||
Then general -> ignore-case should be true
|
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
|
Scenario: Focusing input fields in qute://settings and entering invalid value
|
||||||
When I open qute://settings
|
When I open qute://settings
|
||||||
# scroll to the right - the table does not fit in the default screen
|
# 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
|
Feature: :spawn
|
||||||
|
|
||||||
Scenario: Running :spawn
|
Scenario: Running :spawn
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
|
||||||
|
|
||||||
Feature: Tab management
|
Feature: Tab management
|
||||||
Tests for various :tab-* commands.
|
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):
|
def wait_for_download_prompt(tmpdir, quteproc, path):
|
||||||
full_path = path.replace('(tmpdir)', str(tmpdir)).replace('/', os.sep)
|
full_path = path.replace('(tmpdir)', str(tmpdir)).replace('/', os.sep)
|
||||||
quteproc.wait_for(message=PROMPT_MSG.format(full_path))
|
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")
|
@bdd.when("I download an SSL page")
|
||||||
|
@ -17,5 +17,18 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
import pytest_bdd as bdd
|
import pytest_bdd as bdd
|
||||||
bdd.scenarios('open.feature')
|
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
|
Feature: quickmarks and bookmarks
|
||||||
|
|
||||||
## bookmarks
|
## bookmarks
|
||||||
@ -223,12 +225,12 @@ Feature: quickmarks and bookmarks
|
|||||||
Scenario: Listing quickmarks
|
Scenario: Listing quickmarks
|
||||||
When I run :quickmark-add http://localhost:(port)/data/numbers/20.txt twenty
|
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 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"
|
Then the page should contain the plaintext "twenty"
|
||||||
And the page should contain the plaintext "twentyone"
|
And the page should contain the plaintext "twentyone"
|
||||||
|
|
||||||
Scenario: Listing bookmarks
|
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 run :bookmark-add
|
||||||
And I open qute:bookmarks
|
And I open qute://bookmarks
|
||||||
Then the page should contain the plaintext "Test title"
|
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.
|
Feature: Miscellaneous utility commands exposed to the user.
|
||||||
|
|
||||||
Background:
|
Background:
|
||||||
@ -129,6 +131,7 @@ Feature: Miscellaneous utility commands exposed to the user.
|
|||||||
And I hint with args "all tab-fg"
|
And I hint with args "all tab-fg"
|
||||||
And I run :leave-mode
|
And I run :leave-mode
|
||||||
And I run :repeat-command
|
And I run :repeat-command
|
||||||
|
And I wait for "hints: *" in the log
|
||||||
And I run :follow-hint a
|
And I run :follow-hint a
|
||||||
And I wait until data/hello.txt is loaded
|
And I wait until data/hello.txt is loaded
|
||||||
Then the following tabs should be open:
|
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 :message-info oldstuff
|
||||||
And I run :repeat 20 message-info otherstuff
|
And I run :repeat 20 message-info otherstuff
|
||||||
And I run :message-info newstuff
|
And I run :message-info newstuff
|
||||||
And I open qute:log
|
And I open qute://log
|
||||||
Then the page should contain the plaintext "newstuff"
|
Then the page should contain the plaintext "newstuff"
|
||||||
And the page should not contain the plaintext "oldstuff"
|
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
|
Scenario: Using debug-log-filter with invalid filter
|
||||||
When I run :debug-log-filter blah
|
When I run :debug-log-filter blah
|
||||||
Then the error "filters: Invalid value blah - expected one of: statusbar, *" should be shown
|
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.
|
Feature: Yanking and pasting.
|
||||||
:yank, {clipboard} and {primary} can be used to copy/paste the URL or title
|
:yank, {clipboard} and {primary} can be used to copy/paste the URL or title
|
||||||
from/to the clipboard and primary selection.
|
from/to the clipboard and primary selection.
|
||||||
@ -99,6 +101,11 @@ Feature: Yanking and pasting.
|
|||||||
And I run :open {primary} (invalid command)
|
And I run :open {primary} (invalid command)
|
||||||
Then the error "Primary selection is empty." should be shown
|
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
|
Scenario: Pasting with a space in clipboard
|
||||||
When I put " " into the clipboard
|
When I put " " into the clipboard
|
||||||
And I run :open {clipboard} (invalid command)
|
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
|
Feature: Zooming in and out
|
||||||
|
|
||||||
Background:
|
Background:
|
||||||
|
@ -75,6 +75,10 @@ def is_ignored_lowlevel_message(message):
|
|||||||
"not supported by protocol" in message):
|
"not supported by protocol" in message):
|
||||||
# Makes tests fail on Quantumcross' machine
|
# Makes tests fail on Quantumcross' machine
|
||||||
return True
|
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
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@ -149,11 +149,11 @@ def test_quteprocess_quitting(qtbot, quteproc_process):
|
|||||||
@pytest.mark.parametrize('data, attrs', [
|
@pytest.mark.parametrize('data, attrs', [
|
||||||
(
|
(
|
||||||
# Normal message
|
# 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, '
|
'"module": "earlyinit", "funcName": "init_log", "lineno": 280, '
|
||||||
'"levelno": 10, "message": "Log initialized."}',
|
'"levelno": 10, "message": "Log initialized."}',
|
||||||
{
|
{
|
||||||
'timestamp': datetime.datetime.fromtimestamp(0),
|
'timestamp': datetime.datetime.fromtimestamp(86400),
|
||||||
'loglevel': logging.DEBUG,
|
'loglevel': logging.DEBUG,
|
||||||
'category': 'init',
|
'category': 'init',
|
||||||
'module': 'earlyinit',
|
'module': 'earlyinit',
|
||||||
@ -165,28 +165,28 @@ def test_quteprocess_quitting(qtbot, quteproc_process):
|
|||||||
),
|
),
|
||||||
(
|
(
|
||||||
# VDEBUG
|
# 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, '
|
'"module": "foo", "funcName": "foo", "lineno": 0, "levelno": 9, '
|
||||||
'"message": ""}',
|
'"message": ""}',
|
||||||
{'loglevel': log.VDEBUG_LEVEL}
|
{'loglevel': log.VDEBUG_LEVEL}
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
# Unknown module
|
# 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, '
|
'"module": null, "funcName": null, "lineno": 0, "levelno": 10, '
|
||||||
'"message": "test"}',
|
'"message": "test"}',
|
||||||
{'module': None, 'function': None, 'line': None},
|
{'module': None, 'function': None, 'line': None},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
# Expected message
|
# 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, '
|
'"module": "foo", "funcName": "foo", "lineno": 0, "levelno": 9, '
|
||||||
'"message": "SpellCheck: test"}',
|
'"message": "SpellCheck: test"}',
|
||||||
{'expected': True},
|
{'expected': True},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
# Weird Qt location
|
# Weird Qt location
|
||||||
'{"created": 0, "msecs": 0, "levelname": "DEBUG", "name": "qt", '
|
'{"created": 86400, "msecs": 0, "levelname": "DEBUG", "name": "qt", '
|
||||||
'"module": "qnetworkreplyhttpimpl", "funcName": '
|
'"module": "qnetworkreplyhttpimpl", "funcName": '
|
||||||
'"void QNetworkReplyHttpImplPrivate::error('
|
'"void QNetworkReplyHttpImplPrivate::error('
|
||||||
'QNetworkReply::NetworkError, const QString&)", "lineno": 1929, '
|
'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('
|
'"module": "qxcbxsettings", "funcName": "QXcbXSettings::QXcbXSettings('
|
||||||
'QXcbScreen*)", "lineno": 233, "levelno": 10, "message": '
|
'QXcbScreen*)", "lineno": 233, "levelno": 10, "message": '
|
||||||
'"QXcbXSettings::QXcbXSettings(QXcbScreen*) Failed to get selection '
|
'"QXcbXSettings::QXcbXSettings(QXcbScreen*) Failed to get selection '
|
||||||
@ -213,7 +213,7 @@ def test_quteprocess_quitting(qtbot, quteproc_process):
|
|||||||
),
|
),
|
||||||
(
|
(
|
||||||
# ResourceWarning
|
# ResourceWarning
|
||||||
'{"created": 0, "msecs": 0, "levelname": "WARNING", '
|
'{"created": 86400, "msecs": 0, "levelname": "WARNING", '
|
||||||
'"name": "py.warnings", "module": "app", "funcName": "qt_mainloop", '
|
'"name": "py.warnings", "module": "app", "funcName": "qt_mainloop", '
|
||||||
'"lineno": 121, "levelno": 30, "message": '
|
'"lineno": 121, "levelno": 30, "message": '
|
||||||
'".../app.py:121: ResourceWarning: unclosed file <_io.TextIOWrapper '
|
'".../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', [
|
@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,
|
'module': 'bar', 'funcName': 'qux', 'lineno': 10, 'levelno': 10,
|
||||||
'message': 'quux'},
|
'message': 'quux'},
|
||||||
False, False,
|
False, False,
|
||||||
@ -239,7 +239,7 @@ def test_log_line_parse(data, attrs):
|
|||||||
),
|
),
|
||||||
# Traceback attached
|
# 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,
|
'module': 'bar', 'funcName': 'qux', 'lineno': 10, 'levelno': 10,
|
||||||
'message': 'quux', 'traceback': 'Traceback (most recent call '
|
'message': 'quux', 'traceback': 'Traceback (most recent call '
|
||||||
'last):\n here be dragons'},
|
'last):\n here be dragons'},
|
||||||
@ -250,7 +250,7 @@ def test_log_line_parse(data, attrs):
|
|||||||
),
|
),
|
||||||
# Colorized
|
# 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,
|
'module': 'bar', 'funcName': 'qux', 'lineno': 10, 'levelno': 10,
|
||||||
'message': 'quux'},
|
'message': 'quux'},
|
||||||
True, False,
|
True, False,
|
||||||
@ -259,7 +259,7 @@ def test_log_line_parse(data, attrs):
|
|||||||
),
|
),
|
||||||
# Expected error
|
# 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,
|
'module': 'bar', 'funcName': 'qux', 'lineno': 10, 'levelno': 40,
|
||||||
'message': 'quux'},
|
'message': 'quux'},
|
||||||
False, True,
|
False, True,
|
||||||
@ -267,7 +267,7 @@ def test_log_line_parse(data, attrs):
|
|||||||
),
|
),
|
||||||
# Expected other message (i.e. should make no difference)
|
# 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,
|
'module': 'bar', 'funcName': 'qux', 'lineno': 10, 'levelno': 10,
|
||||||
'message': 'quux'},
|
'message': 'quux'},
|
||||||
False, True,
|
False, True,
|
||||||
@ -275,7 +275,7 @@ def test_log_line_parse(data, attrs):
|
|||||||
),
|
),
|
||||||
# Expected error colorized (shouldn't be red)
|
# 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,
|
'module': 'bar', 'funcName': 'qux', 'lineno': 10, 'levelno': 40,
|
||||||
'message': 'quux'},
|
'message': 'quux'},
|
||||||
True, True,
|
True, True,
|
||||||
|
@ -74,6 +74,8 @@ class Request(testprocess.Line):
|
|||||||
'/redirect-to': [http.client.FOUND],
|
'/redirect-to': [http.client.FOUND],
|
||||||
|
|
||||||
'/cookies/set': [http.client.FOUND],
|
'/cookies/set': [http.client.FOUND],
|
||||||
|
|
||||||
|
'/custom/500-inline': [http.client.INTERNAL_SERVER_ERROR],
|
||||||
}
|
}
|
||||||
for i in range(15):
|
for i in range(15):
|
||||||
path_to_statuses['/redirect/{}'.format(i)] = [http.client.FOUND]
|
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')))
|
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
|
@app.after_request
|
||||||
def log_request(response):
|
def log_request(response):
|
||||||
"""Log a webserver request."""
|
"""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/908
|
||||||
https://github.com/qutebrowser/qutebrowser/issues/1726
|
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)
|
args = ['--temp-basedir'] + _base_args(request.config)
|
||||||
quteproc_new.start(args, env={'LC_ALL': 'C'})
|
quteproc_new.start(args, env={'LC_ALL': 'C'})
|
||||||
quteproc_new.set_setting('storage', 'download-directory', str(tmpdir))
|
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/866
|
||||||
https://github.com/qutebrowser/qutebrowser/issues/1269
|
https://github.com/qutebrowser/qutebrowser/issues/1269
|
||||||
"""
|
"""
|
||||||
if request.config.webengine:
|
|
||||||
pytest.skip("Downloads are not implemented with QtWebEngine yet")
|
|
||||||
|
|
||||||
home = tmpdir / 'home'
|
home = tmpdir / 'home'
|
||||||
home.ensure(dir=True)
|
home.ensure(dir=True)
|
||||||
temp_basedir_env['HOME'] = str(home)
|
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):
|
def test_no_loglines(request, quteproc_new):
|
||||||
"""Test qute:log with --loglines=0."""
|
"""Test qute://log with --loglines=0."""
|
||||||
if request.config.webengine:
|
|
||||||
pytest.skip("qute:log is not implemented with QtWebEngine yet")
|
|
||||||
quteproc_new.start(args=['--temp-basedir', '--loglines=0'] +
|
quteproc_new.start(args=['--temp-basedir', '--loglines=0'] +
|
||||||
_base_args(request.config))
|
_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.'
|
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
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import datetime
|
import json
|
||||||
import collections
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
from PyQt5.QtCore import QUrl
|
from PyQt5.QtCore import QUrl
|
||||||
import pytest
|
import pytest
|
||||||
@ -27,30 +28,72 @@ from qutebrowser.browser import history, qutescheme
|
|||||||
from qutebrowser.utils import objreg
|
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:
|
class TestHistoryHandler:
|
||||||
|
|
||||||
"""Test the qute://history endpoint."""
|
"""Test the qute://history endpoint."""
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture(scope="module")
|
||||||
def dates(self):
|
def now(self):
|
||||||
one_day = datetime.timedelta(days=1)
|
return int(time.time())
|
||||||
today = datetime.datetime.today()
|
|
||||||
tomorrow = today + one_day
|
|
||||||
yesterday = today - one_day
|
|
||||||
return Dates(yesterday, today, tomorrow)
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def entries(self, dates):
|
def entries(self, now):
|
||||||
today = history.Entry(atime=str(dates.today.timestamp()),
|
"""Create fake history entries."""
|
||||||
url=QUrl('www.today.com'), title='today')
|
# create 12 history items spaced 6 hours apart, starting from now
|
||||||
tomorrow = history.Entry(atime=str(dates.tomorrow.timestamp()),
|
entry_count = 12
|
||||||
url=QUrl('www.tomorrow.com'), title='tomorrow')
|
interval = 6 * 60 * 60
|
||||||
yesterday = history.Entry(atime=str(dates.yesterday.timestamp()),
|
|
||||||
url=QUrl('www.yesterday.com'), title='yesterday')
|
items = []
|
||||||
return Dates(yesterday, today, tomorrow)
|
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
|
@pytest.fixture
|
||||||
def fake_web_history(self, fake_save_manager, tmpdir):
|
def fake_web_history(self, fake_save_manager, tmpdir):
|
||||||
@ -62,78 +105,61 @@ class TestHistoryHandler:
|
|||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def fake_history(self, fake_web_history, entries):
|
def fake_history(self, fake_web_history, entries):
|
||||||
"""Create fake history for three different days."""
|
"""Create fake history."""
|
||||||
fake_web_history._add_entry(entries.yesterday)
|
for item in entries:
|
||||||
fake_web_history._add_entry(entries.today)
|
fake_web_history._add_entry(item)
|
||||||
fake_web_history._add_entry(entries.tomorrow)
|
|
||||||
fake_web_history.save()
|
fake_web_history.save()
|
||||||
|
|
||||||
def test_history_without_query(self):
|
@pytest.mark.parametrize("start_time_offset, expected_item_count", [
|
||||||
"""Ensure qute://history shows today's history without any query."""
|
(0, 4),
|
||||||
_mimetype, data = qutescheme.qute_history(QUrl("qute://history"))
|
(24*60*60, 4),
|
||||||
key = "<span class=\"date\">{}</span>".format(
|
(48*60*60, 4),
|
||||||
datetime.date.today().strftime("%a, %d %B %Y"))
|
(72*60*60, 0)
|
||||||
assert key in data
|
])
|
||||||
|
def test_qutehistory_data(self, start_time_offset, expected_item_count,
|
||||||
def test_history_with_bad_query(self):
|
now):
|
||||||
"""Ensure qute://history shows today's history with bad query."""
|
"""Ensure qute://history/data returns correct items."""
|
||||||
url = QUrl("qute://history?date=204-blaah")
|
start_time = now - start_time_offset
|
||||||
|
url = QUrl("qute://history/data?start_time=" + str(start_time))
|
||||||
_mimetype, data = qutescheme.qute_history(url)
|
_mimetype, data = qutescheme.qute_history(url)
|
||||||
key = "<span class=\"date\">{}</span>".format(
|
items = json.loads(data)
|
||||||
datetime.date.today().strftime("%a, %d %B %Y"))
|
items = [item for item in items if 'time' in item] # skip 'next' item
|
||||||
assert key in data
|
|
||||||
|
|
||||||
def test_history_today(self):
|
assert len(items) == expected_item_count
|
||||||
"""Ensure qute://history shows history for today."""
|
|
||||||
url = QUrl("qute://history")
|
# 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)
|
_mimetype, data = qutescheme.qute_history(url)
|
||||||
assert "today" in data
|
items = json.loads(data)
|
||||||
assert "tomorrow" not in data
|
items = [item for item in items if 'next' in item] # 'next' items
|
||||||
assert "yesterday" not in data
|
assert len(items) == 1
|
||||||
|
|
||||||
def test_history_yesterday(self, dates):
|
if next_time == -1:
|
||||||
"""Ensure qute://history shows history for yesterday."""
|
assert items[0]["next"] == -1
|
||||||
url = QUrl("qute://history?date=" +
|
else:
|
||||||
dates.yesterday.strftime("%Y-%m-%d"))
|
assert items[0]["next"] == now - next_time
|
||||||
_mimetype, data = qutescheme.qute_history(url)
|
|
||||||
assert "today" not in data
|
|
||||||
assert "tomorrow" not in data
|
|
||||||
assert "yesterday" in data
|
|
||||||
|
|
||||||
def test_history_tomorrow(self, dates):
|
def test_qute_history_benchmark(self, fake_web_history, benchmark, now):
|
||||||
"""Ensure qute://history shows history for tomorrow."""
|
for t in range(100000): # one history per second
|
||||||
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):
|
|
||||||
entry = history.Entry(
|
entry = history.Entry(
|
||||||
atime=str(dates.yesterday.timestamp()),
|
atime=str(now - t),
|
||||||
url=QUrl('www.yesterday.com/{}'.format(i)),
|
url=QUrl('www.x.com/{}'.format(t)),
|
||||||
title='yesterday')
|
title='x at {}'.format(t))
|
||||||
fake_web_history._add_entry(entry)
|
fake_web_history._add_entry(entry)
|
||||||
fake_web_history._add_entry(entries.today)
|
|
||||||
fake_web_history._add_entry(entries.tomorrow)
|
|
||||||
|
|
||||||
url = QUrl("qute://history")
|
url = QUrl("qute://history/data?start_time={}".format(now))
|
||||||
_mimetype, data = benchmark(qutescheme.qute_history, url)
|
_mimetype, _data = benchmark(qutescheme.qute_history, url)
|
||||||
|
|
||||||
assert "today" in data
|
|
||||||
assert "tomorrow" not in data
|
|
||||||
assert "yesterday" not in data
|
|
||||||
|
@ -91,3 +91,10 @@ def test_error_network_reply(qtbot, req):
|
|||||||
assert reply.readData(1) == b''
|
assert reply.readData(1) == b''
|
||||||
assert reply.error() == QNetworkReply.UnknownNetworkError
|
assert reply.error() == QNetworkReply.UnknownNetworkError
|
||||||
assert reply.errorString() == "This is an error"
|
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
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import pytest
|
||||||
from PyQt5.QtCore import QUrl, QDateTime
|
from PyQt5.QtCore import QUrl, QDateTime
|
||||||
from PyQt5.QtNetwork import QNetworkDiskCache, QNetworkCacheMetaData
|
from PyQt5.QtNetwork import QNetworkDiskCache, QNetworkCacheMetaData
|
||||||
|
|
||||||
from qutebrowser.browser.webkit import cache
|
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'):
|
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/>.
|
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from PyQt5.QtCore import Qt
|
||||||
|
|
||||||
from qutebrowser.mainwindow import messageview
|
from qutebrowser.mainwindow import messageview
|
||||||
from qutebrowser.utils import usertypes
|
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', replace=replace1)
|
||||||
view.show_message(usertypes.MessageLevel.info, 'test 2', replace=replace2)
|
view.show_message(usertypes.MessageLevel.info, 'test 2', replace=replace2)
|
||||||
assert len(view._messages) == length
|
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()
|
p._proc.terminate()
|
||||||
if not blocker.signal_triggered:
|
if not blocker.signal_triggered:
|
||||||
p._proc.kill()
|
p._proc.kill()
|
||||||
|
p._proc.waitForFinished()
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
|
@ -587,22 +587,20 @@ def test_timeout(qtbot, caplog, qlocalsocket, ipc_server):
|
|||||||
assert caplog.records[-1].message.startswith("IPC connection timed out")
|
assert caplog.records[-1].message.startswith("IPC connection timed out")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('method, args, is_warning', [
|
def test_ipcserver_socket_none_readyread(ipc_server, caplog):
|
||||||
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)
|
|
||||||
assert ipc_server._socket is None
|
assert ipc_server._socket is None
|
||||||
|
assert ipc_server._old_socket is None
|
||||||
if is_warning:
|
|
||||||
with caplog.at_level(logging.WARNING):
|
with caplog.at_level(logging.WARNING):
|
||||||
func(*args)
|
ipc_server.on_ready_read()
|
||||||
else:
|
msg = "In on_ready_read with None socket and old_socket!"
|
||||||
func(*args)
|
assert msg in [r.message for r in caplog.records]
|
||||||
|
|
||||||
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]
|
assert msg in [r.message for r in caplog.records]
|
||||||
|
|
||||||
|
|
||||||
|
@ -44,8 +44,8 @@ def expected_text(*args):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def keyhint(qtbot, config_stub, key_config_stub):
|
def keyhint_config(config_stub):
|
||||||
"""Fixture to initialize a KeyHintView."""
|
"""Fixture providing the necessary config settings for the KeyHintView."""
|
||||||
config_stub.data = {
|
config_stub.data = {
|
||||||
'colors': {
|
'colors': {
|
||||||
'keyhint.fg': 'white',
|
'keyhint.fg': 'white',
|
||||||
@ -55,9 +55,16 @@ def keyhint(qtbot, config_stub, key_config_stub):
|
|||||||
'fonts': {'keyhint': 'Comic Sans'},
|
'fonts': {'keyhint': 'Comic Sans'},
|
||||||
'ui': {
|
'ui': {
|
||||||
'keyhint-blacklist': '',
|
'keyhint-blacklist': '',
|
||||||
|
'keyhint-delay': 500,
|
||||||
'status-position': 'bottom',
|
'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)
|
keyhint = KeyHintView(0, None)
|
||||||
qtbot.add_widget(keyhint)
|
qtbot.add_widget(keyhint)
|
||||||
assert keyhint.text() == ''
|
assert keyhint.text() == ''
|
||||||
@ -161,3 +168,16 @@ def test_blacklist_all(keyhint, config_stub, key_config_stub):
|
|||||||
|
|
||||||
keyhint.update_keyhint('normal', 'a')
|
keyhint.update_keyhint('normal', 'a')
|
||||||
assert not keyhint.text()
|
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