Merge branch 'master' into more-color-settings
This commit is contained in:
commit
b59dc8e89b
@ -3,6 +3,7 @@ branch = true
|
|||||||
omit =
|
omit =
|
||||||
qutebrowser/__main__.py
|
qutebrowser/__main__.py
|
||||||
*/__init__.py
|
*/__init__.py
|
||||||
|
qutebrowser/resources.py
|
||||||
|
|
||||||
[report]
|
[report]
|
||||||
exclude_lines =
|
exclude_lines =
|
||||||
|
47
.eslintrc
Normal file
47
.eslintrc
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
# vim: ft=yaml
|
||||||
|
|
||||||
|
env:
|
||||||
|
browser: true
|
||||||
|
|
||||||
|
rules:
|
||||||
|
block-scoped-var: 2
|
||||||
|
dot-location: 2
|
||||||
|
default-case: 2
|
||||||
|
guard-for-in: 2
|
||||||
|
no-div-regex: 2
|
||||||
|
no-param-reassign: 2
|
||||||
|
no-eq-null: 2
|
||||||
|
no-floating-decimal: 2
|
||||||
|
no-self-compare: 2
|
||||||
|
no-throw-literal: 2
|
||||||
|
no-void: 2
|
||||||
|
radix: 2
|
||||||
|
wrap-iife: [2, "inside"]
|
||||||
|
brace-style: [2, "1tbs", {"allowSingleLine": true}]
|
||||||
|
comma-style: [2, "last"]
|
||||||
|
consistent-this: [2, "self"]
|
||||||
|
func-style: [2, "declaration"]
|
||||||
|
indent: [2, 4, {"indentSwitchCase": true}]
|
||||||
|
linebreak-style: [2, "unix"]
|
||||||
|
max-nested-callbacks: [2, 3]
|
||||||
|
no-lonely-if: 2
|
||||||
|
no-multiple-empty-lines: [2, {"max": 2}]
|
||||||
|
no-nested-ternary: 2
|
||||||
|
no-unneeded-ternary: 2
|
||||||
|
operator-assignment: [2, "always"]
|
||||||
|
operator-linebreak: [2, "after"]
|
||||||
|
space-after-keywords: [2, "always"]
|
||||||
|
space-before-blocks: [2, "always"]
|
||||||
|
space-before-function-paren: [2, {"anonymous": "never", "named": "never"}]
|
||||||
|
space-in-brackets: [2, "never"]
|
||||||
|
space-in-parens: [2, "never"]
|
||||||
|
space-unary-ops: [2, {"words": true, "nonwords": false}]
|
||||||
|
spaced-line-comment: [2, "always"]
|
||||||
|
max-depth: [2, 5]
|
||||||
|
max-len: [2, 79, 4]
|
||||||
|
max-params: [2, 5]
|
||||||
|
max-statements: [2, 30]
|
||||||
|
no-bitwise: 2
|
||||||
|
no-reserved-keys: 2
|
||||||
|
global-strict: 0
|
||||||
|
quotes: 0
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -21,3 +21,4 @@ __pycache__
|
|||||||
/.coverage
|
/.coverage
|
||||||
/htmlcov
|
/htmlcov
|
||||||
/.tox
|
/.tox
|
||||||
|
/testresults.html
|
||||||
|
@ -21,6 +21,18 @@ Added
|
|||||||
~~~~~
|
~~~~~
|
||||||
|
|
||||||
- New commands `:message-info`, `:message-error` and `:message-warning` to show messages in the statusbar, e.g. from an userscript.
|
- New commands `:message-info`, `:message-error` and `:message-warning` to show messages in the statusbar, e.g. from an userscript.
|
||||||
|
- There are now some example userscripts in `misc/userscripts`.
|
||||||
|
- New command `:scroll-px` which replaces `:scroll` for pixel-exact scrolling.
|
||||||
|
- New setting `ui -> smooth-scrolling`.
|
||||||
|
- New setting `content -> webgl` to enable/disable https://www.khronos.org/webgl/[WebGL].
|
||||||
|
- New setting `content -> css-regions` to enable/disable support for http://dev.w3.org/csswg/css-regions/[CSS Regions].
|
||||||
|
- New setting `content -> hyperlink-auditing` to enable/disable support for https://html.spec.whatwg.org/multipage/semantics.html#hyperlink-auditing[hyperlink auditing].
|
||||||
|
- Support for Qt 5.5 and tox 2.0
|
||||||
|
- New arguments `--datadir` and `--cachedir` to set the data/cache location.
|
||||||
|
- New arguments `--basedir` and `--temp-basedir` (intended for debugging) to set a different base directory for all data, which allows multiple invocations.
|
||||||
|
- New argument `--no-err-windows` to suppress all error windows.
|
||||||
|
- New visual/caret mode (bound to `v`) to select text by keyboard.
|
||||||
|
- New setting `tabs -> mousewheel-tab-switching` to control mousewheel behavior on the tab bar.
|
||||||
|
|
||||||
Changed
|
Changed
|
||||||
~~~~~~~
|
~~~~~~~
|
||||||
@ -31,6 +43,23 @@ Changed
|
|||||||
- New bindings `<Ctrl-R>` (rapid), `<Ctrl-F>` (foreground) and `<Ctrl-B>` (background) to switch hint modes while hinting.
|
- New bindings `<Ctrl-R>` (rapid), `<Ctrl-F>` (foreground) and `<Ctrl-B>` (background) to switch hint modes while hinting.
|
||||||
- `<Ctrl-M>` is now accepted as an additional alias for `<Return>`/`<Ctrl-J>`
|
- `<Ctrl-M>` is now accepted as an additional alias for `<Return>`/`<Ctrl-J>`
|
||||||
- `:hint tab` and `F` now respect the `background-tabs` setting. To enforce a foreground tab (what `F` did before), use `:hint tab-fg` or `;f`.
|
- `:hint tab` and `F` now respect the `background-tabs` setting. To enforce a foreground tab (what `F` did before), use `:hint tab-fg` or `;f`.
|
||||||
|
- `:scroll` now takes a direction argument (`up`/`down`/`left`/`right`/`top`/`bottom`/`page-up`/`page-down`) instead of two pixel arguments (`dx`/`dy`). The old form still works but is deprecated.
|
||||||
|
|
||||||
|
Deprecated
|
||||||
|
~~~~~~~~~~
|
||||||
|
|
||||||
|
- `:scroll` with two pixel-arguments is now deprecated - `:scroll-px` should be used instead.
|
||||||
|
|
||||||
|
Removed
|
||||||
|
~~~~~~~
|
||||||
|
|
||||||
|
- The `--no-crash-dialog` argument which was intended for debugging only was removed as it's replaced by `--no-err-windows` which suppresses all error windows.
|
||||||
|
|
||||||
|
Fixed
|
||||||
|
~~~~~
|
||||||
|
|
||||||
|
- Scrolling should now work more reliably on some pages where arrow keys worked but `hjkl` didn't.
|
||||||
|
- Small improvements when checking if an input is an URL or not.
|
||||||
|
|
||||||
v0.2.2 (unreleased)
|
v0.2.2 (unreleased)
|
||||||
-------------------
|
-------------------
|
||||||
@ -40,6 +69,12 @@ Fixed
|
|||||||
|
|
||||||
- Fixed searching for terms starting with a hyphen (e.g. `/-foo`)
|
- Fixed searching for terms starting with a hyphen (e.g. `/-foo`)
|
||||||
- Proxy authentication credentials are now remembered between different tabs.
|
- Proxy authentication credentials are now remembered between different tabs.
|
||||||
|
- Fixed updating of the tab title on pages without title.
|
||||||
|
- Fixed AssertionError when closing many windows quickly.
|
||||||
|
- Various fixes for deprecated key bindings and auto-migrations.
|
||||||
|
- Workaround for qutebrowser not starting when there are NUL-bytes in the history (because of a currently unknown bug)
|
||||||
|
- Fixed handling of keybindings containing Ctrl/Meta on OS X.
|
||||||
|
- Fixed crash when downloading an URL without filename (e.g. magnet links) via "Save as...".
|
||||||
|
|
||||||
https://github.com/The-Compiler/qutebrowser/releases/tag/v0.2.1[v0.2.1]
|
https://github.com/The-Compiler/qutebrowser/releases/tag/v0.2.1[v0.2.1]
|
||||||
-----------------------------------------------------------------------
|
-----------------------------------------------------------------------
|
||||||
|
@ -86,7 +86,7 @@ Useful utilities
|
|||||||
Checkers
|
Checkers
|
||||||
~~~~~~~~
|
~~~~~~~~
|
||||||
|
|
||||||
qutbebrowser uses http://tox.readthedocs.org/en/latest/[tox] to run its
|
qutebrowser uses http://tox.readthedocs.org/en/latest/[tox] to run its
|
||||||
unittests and several linters/checkers.
|
unittests and several linters/checkers.
|
||||||
|
|
||||||
Currently, the following tools will be invoked when you run `tox`:
|
Currently, the following tools will be invoked when you run `tox`:
|
||||||
|
@ -84,13 +84,22 @@ There are two Archlinux packages available in the AUR:
|
|||||||
https://aur.archlinux.org/packages/qutebrowser/[qutebrowser] and
|
https://aur.archlinux.org/packages/qutebrowser/[qutebrowser] and
|
||||||
https://aur.archlinux.org/packages/qutebrowser-git/[qutebrowser-git].
|
https://aur.archlinux.org/packages/qutebrowser-git/[qutebrowser-git].
|
||||||
|
|
||||||
You can install them like this:
|
You can install them (and the needed pypeg2 dependency) like this:
|
||||||
|
|
||||||
----
|
----
|
||||||
$ mkdir qutebrowser
|
$ wget https://aur.archlinux.org/packages/py/python-pypeg2/python-pypeg2.tar.gz
|
||||||
$ cd qutebrowser
|
$ tar xzf python-pypeg2.tar.gz
|
||||||
$ wget https://aur.archlinux.org/packages/qu/qutebrowser-git/PKGBUILD
|
$ cd python-pypeg2
|
||||||
$ makepkg -si
|
$ makepkg -si
|
||||||
|
$ cd ..
|
||||||
|
$ rm -r python-pypeg2 python-pypeg2.tar.gz
|
||||||
|
|
||||||
|
$ wget https://aur.archlinux.org/packages/qu/qutebrowser/qutebrowser.tar.gz
|
||||||
|
$ tar xzf qutebrowser.tar.gz
|
||||||
|
$ cd qutebrowser
|
||||||
|
$ makepkg -si
|
||||||
|
$ cd ..
|
||||||
|
$ rm -r qutebrowser qutebrowser.tar.gz
|
||||||
----
|
----
|
||||||
|
|
||||||
or you could use an AUR helper, e.g. `yaourt -S qutebrowser-git`.
|
or you could use an AUR helper, e.g. `yaourt -S qutebrowser-git`.
|
||||||
|
@ -2,6 +2,7 @@ global-exclude __pycache__ *.pyc *.pyo
|
|||||||
|
|
||||||
recursive-include qutebrowser/html *.html
|
recursive-include qutebrowser/html *.html
|
||||||
recursive-include qutebrowser/test *.py
|
recursive-include qutebrowser/test *.py
|
||||||
|
recursive-include qutebrowser/javascript *.js
|
||||||
graft icons
|
graft icons
|
||||||
graft scripts/pylint_checkers
|
graft scripts/pylint_checkers
|
||||||
graft doc/img
|
graft doc/img
|
||||||
@ -29,4 +30,5 @@ exclude qutebrowser.rcc
|
|||||||
exclude .coveragerc
|
exclude .coveragerc
|
||||||
exclude .flake8
|
exclude .flake8
|
||||||
exclude .pylintrc
|
exclude .pylintrc
|
||||||
|
exclude .eslintrc
|
||||||
exclude doc/help
|
exclude doc/help
|
||||||
|
@ -138,16 +138,20 @@ Contributors, sorted by the number of commits in descending order:
|
|||||||
* Raphael Pierzina
|
* Raphael Pierzina
|
||||||
* Joel Torstensson
|
* Joel Torstensson
|
||||||
* Claude
|
* Claude
|
||||||
|
* Artur Shaik
|
||||||
* ZDarian
|
* ZDarian
|
||||||
* Peter Vilim
|
* Peter Vilim
|
||||||
* John ShaggyTwoDope Jenkins
|
* John ShaggyTwoDope Jenkins
|
||||||
* Jimmy
|
* Jimmy
|
||||||
|
* Zach-Button
|
||||||
* rikn00
|
* rikn00
|
||||||
* Patric Schmitz
|
* Patric Schmitz
|
||||||
* Martin Zimmermann
|
* Martin Zimmermann
|
||||||
|
* Martin Tournoij
|
||||||
* Error 800
|
* Error 800
|
||||||
* Brian Jackson
|
* Brian Jackson
|
||||||
* sbinix
|
* sbinix
|
||||||
|
* Tobias Patzl
|
||||||
* Johannes Altmanninger
|
* Johannes Altmanninger
|
||||||
* Samir Benmendil
|
* Samir Benmendil
|
||||||
* Regina Hug
|
* Regina Hug
|
||||||
|
@ -689,12 +689,28 @@ How many steps to zoom out.
|
|||||||
|<<command-history-prev,command-history-prev>>|Go back in the commandline history.
|
|<<command-history-prev,command-history-prev>>|Go back in the commandline history.
|
||||||
|<<completion-item-next,completion-item-next>>|Select the next completion item.
|
|<<completion-item-next,completion-item-next>>|Select the next completion item.
|
||||||
|<<completion-item-prev,completion-item-prev>>|Select the previous completion item.
|
|<<completion-item-prev,completion-item-prev>>|Select the previous completion item.
|
||||||
|
|<<drop-selection,drop-selection>>|Drop selection and keep selection mode enabled.
|
||||||
|<<enter-mode,enter-mode>>|Enter a key mode.
|
|<<enter-mode,enter-mode>>|Enter a key mode.
|
||||||
|<<follow-hint,follow-hint>>|Follow the currently selected hint.
|
|<<follow-hint,follow-hint>>|Follow the currently selected hint.
|
||||||
|<<leave-mode,leave-mode>>|Leave the mode we're currently in.
|
|<<leave-mode,leave-mode>>|Leave the mode we're currently in.
|
||||||
|<<message-error,message-error>>|Show an error message in the statusbar.
|
|<<message-error,message-error>>|Show an error message in the statusbar.
|
||||||
|<<message-info,message-info>>|Show an info message in the statusbar.
|
|<<message-info,message-info>>|Show an info message in the statusbar.
|
||||||
|<<message-warning,message-warning>>|Show a warning message in the statusbar.
|
|<<message-warning,message-warning>>|Show a warning message in the statusbar.
|
||||||
|
|<<move-to-end-of-document,move-to-end-of-document>>|Move the cursor or selection to the end of the document.
|
||||||
|
|<<move-to-end-of-line,move-to-end-of-line>>|Move the cursor or selection to the end of line.
|
||||||
|
|<<move-to-end-of-next-block,move-to-end-of-next-block>>|Move the cursor or selection to the end of next block.
|
||||||
|
|<<move-to-end-of-prev-block,move-to-end-of-prev-block>>|Move the cursor or selection to the end of previous block.
|
||||||
|
|<<move-to-end-of-word,move-to-end-of-word>>|Move the cursor or selection to the end of the word.
|
||||||
|
|<<move-to-next-char,move-to-next-char>>|Move the cursor or selection to the next char.
|
||||||
|
|<<move-to-next-line,move-to-next-line>>|Move the cursor or selection to the next line.
|
||||||
|
|<<move-to-next-word,move-to-next-word>>|Move the cursor or selection to the next word.
|
||||||
|
|<<move-to-prev-char,move-to-prev-char>>|Move the cursor or selection to the previous char.
|
||||||
|
|<<move-to-prev-line,move-to-prev-line>>|Move the cursor or selection to the prev line.
|
||||||
|
|<<move-to-prev-word,move-to-prev-word>>|Move the cursor or selection to the previous word.
|
||||||
|
|<<move-to-start-of-document,move-to-start-of-document>>|Move the cursor or selection to the start of the document.
|
||||||
|
|<<move-to-start-of-line,move-to-start-of-line>>|Move the cursor or selection to the start of the line.
|
||||||
|
|<<move-to-start-of-next-block,move-to-start-of-next-block>>|Move the cursor or selection to the start of next block.
|
||||||
|
|<<move-to-start-of-prev-block,move-to-start-of-prev-block>>|Move the cursor or selection to the start of previous block.
|
||||||
|<<open-editor,open-editor>>|Open an external editor with the currently selected form field.
|
|<<open-editor,open-editor>>|Open an external editor with the currently selected form field.
|
||||||
|<<prompt-accept,prompt-accept>>|Accept the current prompt.
|
|<<prompt-accept,prompt-accept>>|Accept the current prompt.
|
||||||
|<<prompt-no,prompt-no>>|Answer no to a yes/no prompt.
|
|<<prompt-no,prompt-no>>|Answer no to a yes/no prompt.
|
||||||
@ -712,11 +728,14 @@ How many steps to zoom out.
|
|||||||
|<<rl-unix-line-discard,rl-unix-line-discard>>|Remove chars backward from the cursor to the beginning of the line.
|
|<<rl-unix-line-discard,rl-unix-line-discard>>|Remove chars backward from the cursor to the beginning of the line.
|
||||||
|<<rl-unix-word-rubout,rl-unix-word-rubout>>|Remove chars from the cursor to the beginning of the word.
|
|<<rl-unix-word-rubout,rl-unix-word-rubout>>|Remove chars from the cursor to the beginning of the word.
|
||||||
|<<rl-yank,rl-yank>>|Paste the most recently deleted text.
|
|<<rl-yank,rl-yank>>|Paste the most recently deleted text.
|
||||||
|<<scroll,scroll>>|Scroll the current tab by 'count * dx/dy'.
|
|<<scroll,scroll>>|Scroll the current tab in the given direction.
|
||||||
|<<scroll-page,scroll-page>>|Scroll the frame page-wise.
|
|<<scroll-page,scroll-page>>|Scroll the frame page-wise.
|
||||||
|<<scroll-perc,scroll-perc>>|Scroll to a specific percentage of the page.
|
|<<scroll-perc,scroll-perc>>|Scroll to a specific percentage of the page.
|
||||||
|
|<<scroll-px,scroll-px>>|Scroll the current tab by 'count * dx/dy' pixels.
|
||||||
|<<search-next,search-next>>|Continue the search to the ([count]th) next term.
|
|<<search-next,search-next>>|Continue the search to the ([count]th) next term.
|
||||||
|<<search-prev,search-prev>>|Continue the search to the ([count]th) previous term.
|
|<<search-prev,search-prev>>|Continue the search to the ([count]th) previous term.
|
||||||
|
|<<toggle-selection,toggle-selection>>|Toggle caret selection mode.
|
||||||
|
|<<yank-selected,yank-selected>>|Yank the selected text to the clipboard or primary selection.
|
||||||
|==============
|
|==============
|
||||||
[[command-accept]]
|
[[command-accept]]
|
||||||
=== command-accept
|
=== command-accept
|
||||||
@ -738,6 +757,10 @@ Select the next completion item.
|
|||||||
=== completion-item-prev
|
=== completion-item-prev
|
||||||
Select the previous completion item.
|
Select the previous completion item.
|
||||||
|
|
||||||
|
[[drop-selection]]
|
||||||
|
=== drop-selection
|
||||||
|
Drop selection and keep selection mode enabled.
|
||||||
|
|
||||||
[[enter-mode]]
|
[[enter-mode]]
|
||||||
=== enter-mode
|
=== enter-mode
|
||||||
Syntax: +:enter-mode 'mode'+
|
Syntax: +:enter-mode 'mode'+
|
||||||
@ -782,6 +805,99 @@ Show a warning message in the statusbar.
|
|||||||
==== positional arguments
|
==== positional arguments
|
||||||
* +'text'+: The text to show.
|
* +'text'+: The text to show.
|
||||||
|
|
||||||
|
[[move-to-end-of-document]]
|
||||||
|
=== move-to-end-of-document
|
||||||
|
Move the cursor or selection to the end of the document.
|
||||||
|
|
||||||
|
[[move-to-end-of-line]]
|
||||||
|
=== move-to-end-of-line
|
||||||
|
Move the cursor or selection to the end of line.
|
||||||
|
|
||||||
|
[[move-to-end-of-next-block]]
|
||||||
|
=== move-to-end-of-next-block
|
||||||
|
Move the cursor or selection to the end of next block.
|
||||||
|
|
||||||
|
==== count
|
||||||
|
How many blocks to move.
|
||||||
|
|
||||||
|
[[move-to-end-of-prev-block]]
|
||||||
|
=== move-to-end-of-prev-block
|
||||||
|
Move the cursor or selection to the end of previous block.
|
||||||
|
|
||||||
|
==== count
|
||||||
|
How many blocks to move.
|
||||||
|
|
||||||
|
[[move-to-end-of-word]]
|
||||||
|
=== move-to-end-of-word
|
||||||
|
Move the cursor or selection to the end of the word.
|
||||||
|
|
||||||
|
==== count
|
||||||
|
How many words to move.
|
||||||
|
|
||||||
|
[[move-to-next-char]]
|
||||||
|
=== move-to-next-char
|
||||||
|
Move the cursor or selection to the next char.
|
||||||
|
|
||||||
|
==== count
|
||||||
|
How many lines to move.
|
||||||
|
|
||||||
|
[[move-to-next-line]]
|
||||||
|
=== move-to-next-line
|
||||||
|
Move the cursor or selection to the next line.
|
||||||
|
|
||||||
|
==== count
|
||||||
|
How many lines to move.
|
||||||
|
|
||||||
|
[[move-to-next-word]]
|
||||||
|
=== move-to-next-word
|
||||||
|
Move the cursor or selection to the next word.
|
||||||
|
|
||||||
|
==== count
|
||||||
|
How many words to move.
|
||||||
|
|
||||||
|
[[move-to-prev-char]]
|
||||||
|
=== move-to-prev-char
|
||||||
|
Move the cursor or selection to the previous char.
|
||||||
|
|
||||||
|
==== count
|
||||||
|
How many chars to move.
|
||||||
|
|
||||||
|
[[move-to-prev-line]]
|
||||||
|
=== move-to-prev-line
|
||||||
|
Move the cursor or selection to the prev line.
|
||||||
|
|
||||||
|
==== count
|
||||||
|
How many lines to move.
|
||||||
|
|
||||||
|
[[move-to-prev-word]]
|
||||||
|
=== move-to-prev-word
|
||||||
|
Move the cursor or selection to the previous word.
|
||||||
|
|
||||||
|
==== count
|
||||||
|
How many words to move.
|
||||||
|
|
||||||
|
[[move-to-start-of-document]]
|
||||||
|
=== move-to-start-of-document
|
||||||
|
Move the cursor or selection to the start of the document.
|
||||||
|
|
||||||
|
[[move-to-start-of-line]]
|
||||||
|
=== move-to-start-of-line
|
||||||
|
Move the cursor or selection to the start of the line.
|
||||||
|
|
||||||
|
[[move-to-start-of-next-block]]
|
||||||
|
=== move-to-start-of-next-block
|
||||||
|
Move the cursor or selection to the start of next block.
|
||||||
|
|
||||||
|
==== count
|
||||||
|
How many blocks to move.
|
||||||
|
|
||||||
|
[[move-to-start-of-prev-block]]
|
||||||
|
=== move-to-start-of-prev-block
|
||||||
|
Move the cursor or selection to the start of previous block.
|
||||||
|
|
||||||
|
==== count
|
||||||
|
How many blocks to move.
|
||||||
|
|
||||||
[[open-editor]]
|
[[open-editor]]
|
||||||
=== open-editor
|
=== open-editor
|
||||||
Open an external editor with the currently selected form field.
|
Open an external editor with the currently selected form field.
|
||||||
@ -880,13 +996,13 @@ This acts like readline's yank.
|
|||||||
|
|
||||||
[[scroll]]
|
[[scroll]]
|
||||||
=== scroll
|
=== scroll
|
||||||
Syntax: +:scroll 'dx' 'dy'+
|
Syntax: +:scroll 'direction' ['dy']+
|
||||||
|
|
||||||
Scroll the current tab by 'count * dx/dy'.
|
Scroll the current tab in the given direction.
|
||||||
|
|
||||||
==== positional arguments
|
==== positional arguments
|
||||||
* +'dx'+: How much to scroll in x-direction.
|
* +'direction'+: In which direction to scroll (up/down/left/right/top/bottom).
|
||||||
* +'dy'+: How much to scroll in x-direction.
|
|
||||||
|
|
||||||
==== count
|
==== count
|
||||||
multiplier
|
multiplier
|
||||||
@ -921,6 +1037,19 @@ The percentage can be given either as argument or as count. If no percentage is
|
|||||||
==== count
|
==== count
|
||||||
Percentage to scroll.
|
Percentage to scroll.
|
||||||
|
|
||||||
|
[[scroll-px]]
|
||||||
|
=== scroll-px
|
||||||
|
Syntax: +:scroll-px 'dx' 'dy'+
|
||||||
|
|
||||||
|
Scroll the current tab by 'count * dx/dy' pixels.
|
||||||
|
|
||||||
|
==== positional arguments
|
||||||
|
* +'dx'+: How much to scroll in x-direction.
|
||||||
|
* +'dy'+: How much to scroll in x-direction.
|
||||||
|
|
||||||
|
==== count
|
||||||
|
multiplier
|
||||||
|
|
||||||
[[search-next]]
|
[[search-next]]
|
||||||
=== search-next
|
=== search-next
|
||||||
Continue the search to the ([count]th) next term.
|
Continue the search to the ([count]th) next term.
|
||||||
@ -935,6 +1064,20 @@ Continue the search to the ([count]th) previous term.
|
|||||||
==== count
|
==== count
|
||||||
How many elements to ignore.
|
How many elements to ignore.
|
||||||
|
|
||||||
|
[[toggle-selection]]
|
||||||
|
=== toggle-selection
|
||||||
|
Toggle caret selection mode.
|
||||||
|
|
||||||
|
[[yank-selected]]
|
||||||
|
=== yank-selected
|
||||||
|
Syntax: +:yank-selected [*--sel*] [*--keep*]+
|
||||||
|
|
||||||
|
Yank the selected text to the clipboard or primary selection.
|
||||||
|
|
||||||
|
==== optional arguments
|
||||||
|
* +*-s*+, +*--sel*+: Use the primary selection instead of the clipboard.
|
||||||
|
* +*-k*+, +*--keep*+: If given, stay in visual mode after yanking.
|
||||||
|
|
||||||
|
|
||||||
== Debugging commands
|
== Debugging commands
|
||||||
These commands are mainly intended for debugging. They are hidden if qutebrowser was started without the `--debug`-flag.
|
These commands are mainly intended for debugging. They are hidden if qutebrowser was started without the `--debug`-flag.
|
||||||
|
@ -40,6 +40,7 @@
|
|||||||
|<<ui-frame-flattening,frame-flattening>>|Whether to expand each subframe to its contents.
|
|<<ui-frame-flattening,frame-flattening>>|Whether to expand each subframe to its contents.
|
||||||
|<<ui-user-stylesheet,user-stylesheet>>|User stylesheet to use (absolute filename or CSS string). Will expand environment variables.
|
|<<ui-user-stylesheet,user-stylesheet>>|User stylesheet to use (absolute filename or CSS string). Will expand environment variables.
|
||||||
|<<ui-css-media-type,css-media-type>>|Set the CSS media type.
|
|<<ui-css-media-type,css-media-type>>|Set the CSS media type.
|
||||||
|
|<<ui-smooth-scrolling,smooth-scrolling>>|Whether to enable smooth scrolling for webpages.
|
||||||
|<<ui-remove-finished-downloads,remove-finished-downloads>>|Whether to remove finished downloads automatically.
|
|<<ui-remove-finished-downloads,remove-finished-downloads>>|Whether to remove finished downloads automatically.
|
||||||
|<<ui-hide-statusbar,hide-statusbar>>|Whether to hide the statusbar unless a message is shown.
|
|<<ui-hide-statusbar,hide-statusbar>>|Whether to hide the statusbar unless a message is shown.
|
||||||
|<<ui-window-title-format,window-title-format>>|The format to use for the window title. The following placeholders are defined:
|
|<<ui-window-title-format,window-title-format>>|The format to use for the window title. The following placeholders are defined:
|
||||||
@ -110,6 +111,7 @@
|
|||||||
|<<tabs-indicator-space,indicator-space>>|Spacing between tab edge and indicator.
|
|<<tabs-indicator-space,indicator-space>>|Spacing between tab edge and indicator.
|
||||||
|<<tabs-tabs-are-windows,tabs-are-windows>>|Whether to open windows instead of tabs.
|
|<<tabs-tabs-are-windows,tabs-are-windows>>|Whether to open windows instead of tabs.
|
||||||
|<<tabs-title-format,title-format>>|The format to use for the tab title. The following placeholders are defined:
|
|<<tabs-title-format,title-format>>|The format to use for the tab title. The following placeholders are defined:
|
||||||
|
|<<tabs-mousewheel-tab-switching,mousewheel-tab-switching>>|Switch between tabs using the mouse wheel.
|
||||||
|==============
|
|==============
|
||||||
|
|
||||||
.Quick reference for section ``storage''
|
.Quick reference for section ``storage''
|
||||||
@ -134,6 +136,9 @@
|
|||||||
|<<content-allow-images,allow-images>>|Whether images are automatically loaded in web pages.
|
|<<content-allow-images,allow-images>>|Whether images are automatically loaded in web pages.
|
||||||
|<<content-allow-javascript,allow-javascript>>|Enables or disables the running of JavaScript programs.
|
|<<content-allow-javascript,allow-javascript>>|Enables or disables the running of JavaScript programs.
|
||||||
|<<content-allow-plugins,allow-plugins>>|Enables or disables plugins in Web pages.
|
|<<content-allow-plugins,allow-plugins>>|Enables or disables plugins in Web pages.
|
||||||
|
|<<content-webgl,webgl>>|Enables or disables WebGL.
|
||||||
|
|<<content-css-regions,css-regions>>|Enable or disable support for CSS regions.
|
||||||
|
|<<content-hyperlink-auditing,hyperlink-auditing>>|Enable or disable hyperlink auditing (<a ping>).
|
||||||
|<<content-geolocation,geolocation>>|Allow websites to request geolocations.
|
|<<content-geolocation,geolocation>>|Allow websites to request geolocations.
|
||||||
|<<content-notifications,notifications>>|Allow websites to show notifications.
|
|<<content-notifications,notifications>>|Allow websites to show notifications.
|
||||||
|<<content-javascript-can-open-windows,javascript-can-open-windows>>|Whether JavaScript programs can open new windows.
|
|<<content-javascript-can-open-windows,javascript-can-open-windows>>|Whether JavaScript programs can open new windows.
|
||||||
@ -187,6 +192,8 @@
|
|||||||
|<<colors-statusbar.bg.warning,statusbar.bg.warning>>|Background color of the statusbar if there is a warning.
|
|<<colors-statusbar.bg.warning,statusbar.bg.warning>>|Background color of the statusbar if there is a warning.
|
||||||
|<<colors-statusbar.bg.prompt,statusbar.bg.prompt>>|Background color of the statusbar if there is a prompt.
|
|<<colors-statusbar.bg.prompt,statusbar.bg.prompt>>|Background color of the statusbar if there is a prompt.
|
||||||
|<<colors-statusbar.bg.insert,statusbar.bg.insert>>|Background color of the statusbar in insert mode.
|
|<<colors-statusbar.bg.insert,statusbar.bg.insert>>|Background color of the statusbar in insert mode.
|
||||||
|
|<<colors-statusbar.bg.caret,statusbar.bg.caret>>|Background color of the statusbar in caret mode.
|
||||||
|
|<<colors-statusbar.bg.caret-selection,statusbar.bg.caret-selection>>|Background color of the statusbar in caret mode with a selection
|
||||||
|<<colors-statusbar.progress.bg,statusbar.progress.bg>>|Background color of the progress bar.
|
|<<colors-statusbar.progress.bg,statusbar.progress.bg>>|Background color of the progress bar.
|
||||||
|<<colors-statusbar.url.fg,statusbar.url.fg>>|Default foreground color of the URL in the statusbar.
|
|<<colors-statusbar.url.fg,statusbar.url.fg>>|Default foreground color of the URL in the statusbar.
|
||||||
|<<colors-statusbar.url.fg.success,statusbar.url.fg.success>>|Foreground color of the URL in the statusbar on successful load.
|
|<<colors-statusbar.url.fg.success,statusbar.url.fg.success>>|Foreground color of the URL in the statusbar on successful load.
|
||||||
@ -392,10 +399,10 @@ How to open links in an existing instance if a new one is launched.
|
|||||||
|
|
||||||
Valid values:
|
Valid values:
|
||||||
|
|
||||||
* +tab+: Open a new tab in the existing window and activate it.
|
* +tab+: Open a new tab in the existing window and activate the window.
|
||||||
* +tab-bg+: Open a new background tab in the existing window and activate it.
|
* +tab-bg+: Open a new background tab in the existing window and activate the window.
|
||||||
* +tab-silent+: Open a new tab in the existing window without activating it.
|
* +tab-silent+: Open a new tab in the existing window without activating the window.
|
||||||
* +tab-bg-silent+: Open a new background tab in the existing window without activating it.
|
* +tab-bg-silent+: Open a new background tab in the existing window without activating the window.
|
||||||
* +window+: Open in a new window.
|
* +window+: Open in a new window.
|
||||||
|
|
||||||
Default: +pass:[window]+
|
Default: +pass:[window]+
|
||||||
@ -531,6 +538,17 @@ Set the CSS media type.
|
|||||||
|
|
||||||
Default: empty
|
Default: empty
|
||||||
|
|
||||||
|
[[ui-smooth-scrolling]]
|
||||||
|
=== smooth-scrolling
|
||||||
|
Whether to enable smooth scrolling for webpages.
|
||||||
|
|
||||||
|
Valid values:
|
||||||
|
|
||||||
|
* +true+
|
||||||
|
* +false+
|
||||||
|
|
||||||
|
Default: +pass:[false]+
|
||||||
|
|
||||||
[[ui-remove-finished-downloads]]
|
[[ui-remove-finished-downloads]]
|
||||||
=== remove-finished-downloads
|
=== remove-finished-downloads
|
||||||
Whether to remove finished downloads automatically.
|
Whether to remove finished downloads automatically.
|
||||||
@ -1014,6 +1032,17 @@ The format to use for the tab title. The following placeholders are defined:
|
|||||||
|
|
||||||
Default: +pass:[{index}: {title}]+
|
Default: +pass:[{index}: {title}]+
|
||||||
|
|
||||||
|
[[tabs-mousewheel-tab-switching]]
|
||||||
|
=== mousewheel-tab-switching
|
||||||
|
Switch between tabs using the mouse wheel.
|
||||||
|
|
||||||
|
Valid values:
|
||||||
|
|
||||||
|
* +true+
|
||||||
|
* +false+
|
||||||
|
|
||||||
|
Default: +pass:[true]+
|
||||||
|
|
||||||
== storage
|
== storage
|
||||||
Settings related to cache and storage.
|
Settings related to cache and storage.
|
||||||
|
|
||||||
@ -1138,6 +1167,39 @@ Valid values:
|
|||||||
|
|
||||||
Default: +pass:[false]+
|
Default: +pass:[false]+
|
||||||
|
|
||||||
|
[[content-webgl]]
|
||||||
|
=== webgl
|
||||||
|
Enables or disables WebGL.
|
||||||
|
|
||||||
|
Valid values:
|
||||||
|
|
||||||
|
* +true+
|
||||||
|
* +false+
|
||||||
|
|
||||||
|
Default: +pass:[true]+
|
||||||
|
|
||||||
|
[[content-css-regions]]
|
||||||
|
=== css-regions
|
||||||
|
Enable or disable support for CSS regions.
|
||||||
|
|
||||||
|
Valid values:
|
||||||
|
|
||||||
|
* +true+
|
||||||
|
* +false+
|
||||||
|
|
||||||
|
Default: +pass:[true]+
|
||||||
|
|
||||||
|
[[content-hyperlink-auditing]]
|
||||||
|
=== hyperlink-auditing
|
||||||
|
Enable or disable hyperlink auditing (<a ping>).
|
||||||
|
|
||||||
|
Valid values:
|
||||||
|
|
||||||
|
* +true+
|
||||||
|
* +false+
|
||||||
|
|
||||||
|
Default: +pass:[false]+
|
||||||
|
|
||||||
[[content-geolocation]]
|
[[content-geolocation]]
|
||||||
=== geolocation
|
=== geolocation
|
||||||
Allow websites to request geolocations.
|
Allow websites to request geolocations.
|
||||||
@ -1498,6 +1560,18 @@ Background color of the statusbar in insert mode.
|
|||||||
|
|
||||||
Default: +pass:[darkgreen]+
|
Default: +pass:[darkgreen]+
|
||||||
|
|
||||||
|
[[colors-statusbar.bg.caret]]
|
||||||
|
=== statusbar.bg.caret
|
||||||
|
Background color of the statusbar in caret mode.
|
||||||
|
|
||||||
|
Default: +pass:[purple]+
|
||||||
|
|
||||||
|
[[colors-statusbar.bg.caret-selection]]
|
||||||
|
=== statusbar.bg.caret-selection
|
||||||
|
Background color of the statusbar in caret mode with a selection
|
||||||
|
|
||||||
|
Default: +pass:[#a12dff]+
|
||||||
|
|
||||||
[[colors-statusbar.progress.bg]]
|
[[colors-statusbar.progress.bg]]
|
||||||
=== statusbar.progress.bg
|
=== statusbar.progress.bg
|
||||||
Background color of the progress bar.
|
Background color of the progress bar.
|
||||||
|
@ -11,6 +11,7 @@ What to do now
|
|||||||
* View the http://qutebrowser.org/img/cheatsheet-big.png[key binding cheatsheet]
|
* View the http://qutebrowser.org/img/cheatsheet-big.png[key binding cheatsheet]
|
||||||
to make yourself familiar with the key bindings: +
|
to make yourself familiar with the key bindings: +
|
||||||
image:http://qutebrowser.org/img/cheatsheet-small.png["qutebrowser key binding cheatsheet",link="http://qutebrowser.org/img/cheatsheet-big.png"]
|
image:http://qutebrowser.org/img/cheatsheet-small.png["qutebrowser key binding cheatsheet",link="http://qutebrowser.org/img/cheatsheet-big.png"]
|
||||||
|
* Run `:adblock-update` to download adblock lists and activate adblocking.
|
||||||
* If you just cloned the repository, you'll need to run
|
* If you just cloned the repository, you'll need to run
|
||||||
`scripts/asciidoc2html.py` to generate the documentation.
|
`scripts/asciidoc2html.py` to generate the documentation.
|
||||||
* Go to the link:qute://settings[settings page] to set up qutebrowser the way you want it.
|
* Go to the link:qute://settings[settings page] to set up qutebrowser the way you want it.
|
||||||
|
@ -41,6 +41,15 @@ show it.
|
|||||||
*-c* 'CONFDIR', *--confdir* 'CONFDIR'::
|
*-c* 'CONFDIR', *--confdir* 'CONFDIR'::
|
||||||
Set config directory (empty for no config storage).
|
Set config directory (empty for no config storage).
|
||||||
|
|
||||||
|
*--datadir* 'DATADIR'::
|
||||||
|
Set data directory (empty for no data storage).
|
||||||
|
|
||||||
|
*--cachedir* 'CACHEDIR'::
|
||||||
|
Set cache directory (empty for no cache storage).
|
||||||
|
|
||||||
|
*--basedir* 'BASEDIR'::
|
||||||
|
Base directory for all storage. Other --*dir arguments are ignored if this is given.
|
||||||
|
|
||||||
*-V*, *--version*::
|
*-V*, *--version*::
|
||||||
Show version and quit.
|
Show version and quit.
|
||||||
|
|
||||||
@ -81,12 +90,15 @@ show it.
|
|||||||
*--debug-exit*::
|
*--debug-exit*::
|
||||||
Turn on debugging of late exit.
|
Turn on debugging of late exit.
|
||||||
|
|
||||||
*--no-crash-dialog*::
|
|
||||||
Don't show a crash dialog.
|
|
||||||
|
|
||||||
*--pdb-postmortem*::
|
*--pdb-postmortem*::
|
||||||
Drop into pdb on exceptions.
|
Drop into pdb on exceptions.
|
||||||
|
|
||||||
|
*--temp-basedir*::
|
||||||
|
Use a temporary basedir.
|
||||||
|
|
||||||
|
*--no-err-windows*::
|
||||||
|
Don't show any error windows (used for tests/smoke.py).
|
||||||
|
|
||||||
*--qt-name* 'NAME'::
|
*--qt-name* 'NAME'::
|
||||||
Set the window name.
|
Set the window name.
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
height="640"
|
height="640"
|
||||||
id="svg2"
|
id="svg2"
|
||||||
sodipodi:version="0.32"
|
sodipodi:version="0.32"
|
||||||
inkscape:version="0.91 r13725"
|
inkscape:version="0.48.5 r10040"
|
||||||
version="1.0"
|
version="1.0"
|
||||||
sodipodi:docname="cheatsheet.svg"
|
sodipodi:docname="cheatsheet.svg"
|
||||||
inkscape:output_extension="org.inkscape.output.svg.inkscape"
|
inkscape:output_extension="org.inkscape.output.svg.inkscape"
|
||||||
@ -33,16 +33,16 @@
|
|||||||
inkscape:pageopacity="0.0"
|
inkscape:pageopacity="0.0"
|
||||||
inkscape:pageshadow="2"
|
inkscape:pageshadow="2"
|
||||||
inkscape:zoom="0.8791156"
|
inkscape:zoom="0.8791156"
|
||||||
inkscape:cx="641.54005"
|
inkscape:cx="768.67127"
|
||||||
inkscape:cy="233.0095"
|
inkscape:cy="133.80749"
|
||||||
inkscape:document-units="px"
|
inkscape:document-units="px"
|
||||||
inkscape:current-layer="layer1"
|
inkscape:current-layer="layer1"
|
||||||
width="1024px"
|
width="1024px"
|
||||||
height="640px"
|
height="640px"
|
||||||
showgrid="false"
|
showgrid="false"
|
||||||
inkscape:window-width="1366"
|
inkscape:window-width="636"
|
||||||
inkscape:window-height="768"
|
inkscape:window-height="536"
|
||||||
inkscape:window-x="0"
|
inkscape:window-x="2560"
|
||||||
inkscape:window-y="0"
|
inkscape:window-y="0"
|
||||||
showguides="true"
|
showguides="true"
|
||||||
inkscape:guide-bbox="true"
|
inkscape:guide-bbox="true"
|
||||||
@ -1939,7 +1939,7 @@
|
|||||||
x="542.06946"
|
x="542.06946"
|
||||||
sodipodi:role="line"
|
sodipodi:role="line"
|
||||||
id="tspan4938"
|
id="tspan4938"
|
||||||
style="font-size:8px">scoll</tspan><tspan
|
style="font-size:8px">scroll</tspan><tspan
|
||||||
y="276.1955"
|
y="276.1955"
|
||||||
x="542.06946"
|
x="542.06946"
|
||||||
sodipodi:role="line"
|
sodipodi:role="line"
|
||||||
@ -3326,27 +3326,15 @@
|
|||||||
style="font-size:8px">tab</tspan></text>
|
style="font-size:8px">tab</tspan></text>
|
||||||
<text
|
<text
|
||||||
xml:space="preserve"
|
xml:space="preserve"
|
||||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:8px;line-height:89.99999762%;font-family:TlwgTypewriter;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none"
|
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:8px;line-height:89.99999762%;font-family:TlwgTypewriter;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#ff0000;fill-opacity:1;stroke:none"
|
||||||
x="267.67316"
|
x="274.21381"
|
||||||
y="326.20523"
|
y="343.17578"
|
||||||
id="text10547-23-6-7"
|
id="text10547-23-6-7"
|
||||||
sodipodi:linespacing="89.999998%"><tspan
|
sodipodi:linespacing="89.999998%"><tspan
|
||||||
sodipodi:role="line"
|
sodipodi:role="line"
|
||||||
x="267.67316"
|
x="274.21381"
|
||||||
y="326.20523"
|
y="343.17578"
|
||||||
id="tspan10560-1-3-1" /><tspan
|
id="tspan4052">(10)</tspan></text>
|
||||||
sodipodi:role="line"
|
|
||||||
x="267.67316"
|
|
||||||
y="333.40524"
|
|
||||||
id="tspan5325">co: close</tspan><tspan
|
|
||||||
sodipodi:role="line"
|
|
||||||
x="267.67316"
|
|
||||||
y="340.60522"
|
|
||||||
id="tspan10562-12-5-98">other tabs</tspan><tspan
|
|
||||||
sodipodi:role="line"
|
|
||||||
x="267.67316"
|
|
||||||
y="347.80524"
|
|
||||||
id="tspan4045">cd: clea</tspan></text>
|
|
||||||
<text
|
<text
|
||||||
sodipodi:linespacing="89.999998%"
|
sodipodi:linespacing="89.999998%"
|
||||||
id="text10564-6-7-8-0"
|
id="text10564-6-7-8-0"
|
||||||
@ -3471,5 +3459,20 @@
|
|||||||
y="177.63554"
|
y="177.63554"
|
||||||
style="font-size:8px"
|
style="font-size:8px"
|
||||||
id="tspan3719">cache)</tspan></text>
|
id="tspan3719">cache)</tspan></text>
|
||||||
|
<text
|
||||||
|
sodipodi:linespacing="89.999998%"
|
||||||
|
id="text9514-60-7-7-0-8"
|
||||||
|
y="338.04874"
|
||||||
|
x="342.42523"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:8px;line-height:89.99999762%;font-family:TlwgTypewriter;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none"
|
||||||
|
xml:space="preserve"><tspan
|
||||||
|
y="338.04874"
|
||||||
|
x="342.42523"
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan5689-6">visual</tspan><tspan
|
||||||
|
y="345.24875"
|
||||||
|
x="342.42523"
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan4112">mode</tspan></text>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 137 KiB |
48
misc/userscripts/dmenu_qutebrowser
Executable file
48
misc/userscripts/dmenu_qutebrowser
Executable file
@ -0,0 +1,48 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Copyright 2015 Zach-Button <zachrey.button@gmail.com>
|
||||||
|
#
|
||||||
|
# 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/>.
|
||||||
|
|
||||||
|
# Pipes history, quickmarks, and URL into dmenu.
|
||||||
|
#
|
||||||
|
# If run from qutebrowser as a userscript, it runs :open on the URL
|
||||||
|
# If not, it opens a new qutebrowser window at the URL
|
||||||
|
#
|
||||||
|
# Ideal for use with tabs-are-windows. Set a hotkey to launch this script, then:
|
||||||
|
# :bind o spawn --userscript dmenu_qutebrowser
|
||||||
|
#
|
||||||
|
# Use the hotkey to open in new tab/window, press 'o' to open URL in current tab/window
|
||||||
|
# You can simulate "go" by pressing "o<tab>", as the current URL is always first in the list
|
||||||
|
#
|
||||||
|
# I personally use "<Mod4>o" to launch this script. For me, my workflow is:
|
||||||
|
# Default keys Keys with this script
|
||||||
|
# O <Mod4>o
|
||||||
|
# o o
|
||||||
|
# go o<Tab>
|
||||||
|
# gO gC, then o<Tab>
|
||||||
|
# (This is unnecessarily long. I use this rarely, feel free to make this script accept parameters.)
|
||||||
|
#
|
||||||
|
|
||||||
|
[ -z "$QUTE_URL" ] && QUTE_URL='http://google.com'
|
||||||
|
|
||||||
|
url=$(echo "$QUTE_URL" | cat - ~/.config/qutebrowser/quickmarks ~/.local/share/qutebrowser/history | dmenu -l 15 -p qutebrowser)
|
||||||
|
url=$(echo $url | sed -E 's/[^ ]+ +//g' | egrep "https?:" || echo $url)
|
||||||
|
|
||||||
|
[ -z "${url// }" ] && exit
|
||||||
|
|
||||||
|
echo "open $url" >> "$QUTE_FIFO" || qutebrowser "$url"
|
||||||
|
|
32
misc/userscripts/qutebrowser_viewsource
Executable file
32
misc/userscripts/qutebrowser_viewsource
Executable file
@ -0,0 +1,32 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Copyright 2015 Zach-Button <zachrey.button@gmail.com>
|
||||||
|
#
|
||||||
|
# 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/>.
|
||||||
|
|
||||||
|
#
|
||||||
|
# This script fetches the unprocessed HTML source for a page and opens it in vim.
|
||||||
|
# :bind gf spawn --userscript qutebrowser_viewsource
|
||||||
|
#
|
||||||
|
# Caveat: Does not use authentication of any kind. Add it in if you want it to.
|
||||||
|
#
|
||||||
|
|
||||||
|
path=/tmp/qutebrowser_$(mktemp XXXXXXXX).html
|
||||||
|
|
||||||
|
curl "$QUTE_URL" > $path
|
||||||
|
urxvt -e vim "$path"
|
||||||
|
|
||||||
|
rm "$path"
|
@ -26,8 +26,11 @@ import configparser
|
|||||||
import functools
|
import functools
|
||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
|
import shutil
|
||||||
|
import tempfile
|
||||||
|
import atexit
|
||||||
|
|
||||||
from PyQt5.QtWidgets import QApplication, QMessageBox
|
from PyQt5.QtWidgets import QApplication
|
||||||
from PyQt5.QtGui import QDesktopServices, QPixmap, QIcon, QCursor, QWindow
|
from PyQt5.QtGui import QDesktopServices, QPixmap, QIcon, QCursor, QWindow
|
||||||
from PyQt5.QtCore import (pyqtSlot, qInstallMessageHandler, QTimer, QUrl,
|
from PyQt5.QtCore import (pyqtSlot, qInstallMessageHandler, QTimer, QUrl,
|
||||||
QObject, Qt, QEvent)
|
QObject, Qt, QEvent)
|
||||||
@ -47,7 +50,7 @@ from qutebrowser.mainwindow import mainwindow
|
|||||||
from qutebrowser.misc import readline, ipc, savemanager, sessions, crashsignal
|
from qutebrowser.misc import readline, ipc, savemanager, sessions, crashsignal
|
||||||
from qutebrowser.misc import utilcmds # pylint: disable=unused-import
|
from qutebrowser.misc import utilcmds # pylint: disable=unused-import
|
||||||
from qutebrowser.utils import (log, version, message, utils, qtutils, urlutils,
|
from qutebrowser.utils import (log, version, message, utils, qtutils, urlutils,
|
||||||
objreg, usertypes, standarddir)
|
objreg, usertypes, standarddir, error)
|
||||||
# We import utilcmds to run the cmdutils.register decorators.
|
# We import utilcmds to run the cmdutils.register decorators.
|
||||||
|
|
||||||
|
|
||||||
@ -64,7 +67,10 @@ def run(args):
|
|||||||
print(qutebrowser.__copyright__)
|
print(qutebrowser.__copyright__)
|
||||||
print()
|
print()
|
||||||
print(version.GPL_BOILERPLATE.strip())
|
print(version.GPL_BOILERPLATE.strip())
|
||||||
sys.exit(0)
|
sys.exit(usertypes.Exit.ok)
|
||||||
|
|
||||||
|
if args.temp_basedir:
|
||||||
|
args.basedir = tempfile.mkdtemp(prefix='qutebrowser-basedir-')
|
||||||
|
|
||||||
quitter = Quitter(args)
|
quitter = Quitter(args)
|
||||||
objreg.register('quitter', quitter)
|
objreg.register('quitter', quitter)
|
||||||
@ -84,11 +90,11 @@ def run(args):
|
|||||||
objreg.register('signal-handler', signal_handler)
|
objreg.register('signal-handler', signal_handler)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
sent = ipc.send_to_running_instance(args.command)
|
sent = ipc.send_to_running_instance(args)
|
||||||
if sent:
|
if sent:
|
||||||
sys.exit(0)
|
sys.exit(usertypes.Exit.ok)
|
||||||
log.init.debug("Starting IPC server...")
|
log.init.debug("Starting IPC server...")
|
||||||
server = ipc.IPCServer(qApp)
|
server = ipc.IPCServer(args, qApp)
|
||||||
objreg.register('ipc-server', server)
|
objreg.register('ipc-server', server)
|
||||||
server.got_args.connect(lambda args, cwd:
|
server.got_args.connect(lambda args, cwd:
|
||||||
process_pos_args(args, cwd=cwd, via_ipc=True))
|
process_pos_args(args, cwd=cwd, via_ipc=True))
|
||||||
@ -96,16 +102,16 @@ def run(args):
|
|||||||
# This could be a race condition...
|
# This could be a race condition...
|
||||||
log.init.debug("Got AddressInUseError, trying again.")
|
log.init.debug("Got AddressInUseError, trying again.")
|
||||||
time.sleep(500)
|
time.sleep(500)
|
||||||
sent = ipc.send_to_running_instance(args.command)
|
sent = ipc.send_to_running_instance(args)
|
||||||
if sent:
|
if sent:
|
||||||
sys.exit(0)
|
sys.exit(usertypes.Exit.ok)
|
||||||
else:
|
else:
|
||||||
ipc.display_error(e)
|
ipc.display_error(e, args)
|
||||||
sys.exit(1)
|
sys.exit(usertypes.Exit.err_ipc)
|
||||||
except ipc.Error as e:
|
except ipc.Error as e:
|
||||||
ipc.display_error(e)
|
ipc.display_error(e, args)
|
||||||
# We didn't really initialize much so far, so we just quit hard.
|
# We didn't really initialize much so far, so we just quit hard.
|
||||||
sys.exit(1)
|
sys.exit(usertypes.Exit.err_ipc)
|
||||||
|
|
||||||
init(args, crash_handler)
|
init(args, crash_handler)
|
||||||
ret = qt_mainloop()
|
ret = qt_mainloop()
|
||||||
@ -139,11 +145,9 @@ def init(args, crash_handler):
|
|||||||
try:
|
try:
|
||||||
_init_modules(args, crash_handler)
|
_init_modules(args, crash_handler)
|
||||||
except (OSError, UnicodeDecodeError) as e:
|
except (OSError, UnicodeDecodeError) as e:
|
||||||
msgbox = QMessageBox(
|
error.handle_fatal_exc(e, args, "Error while initializing!",
|
||||||
QMessageBox.Critical, "Error while initializing!",
|
pre_text="Error while initializing")
|
||||||
"Error while initializing: {}".format(e))
|
sys.exit(usertypes.Exit.err_init)
|
||||||
msgbox.exec_()
|
|
||||||
sys.exit(1)
|
|
||||||
QTimer.singleShot(0, functools.partial(_process_args, args))
|
QTimer.singleShot(0, functools.partial(_process_args, args))
|
||||||
|
|
||||||
log.init.debug("Initializing eventfilter...")
|
log.init.debug("Initializing eventfilter...")
|
||||||
@ -199,7 +203,7 @@ def _process_args(args):
|
|||||||
|
|
||||||
process_pos_args(args.command)
|
process_pos_args(args.command)
|
||||||
_open_startpage()
|
_open_startpage()
|
||||||
_open_quickstart()
|
_open_quickstart(args)
|
||||||
|
|
||||||
|
|
||||||
def _load_session(name):
|
def _load_session(name):
|
||||||
@ -303,8 +307,15 @@ def _open_startpage(win_id=None):
|
|||||||
tabbed_browser.tabopen(url)
|
tabbed_browser.tabopen(url)
|
||||||
|
|
||||||
|
|
||||||
def _open_quickstart():
|
def _open_quickstart(args):
|
||||||
"""Open quickstart if it's the first start."""
|
"""Open quickstart if it's the first start.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args: The argparse namespace.
|
||||||
|
"""
|
||||||
|
if args.datadir is not None or args.basedir is not None:
|
||||||
|
# With --datadir or --basedir given, don't open quickstart.
|
||||||
|
return
|
||||||
state_config = objreg.get('state-config')
|
state_config = objreg.get('state-config')
|
||||||
try:
|
try:
|
||||||
quickstart_done = state_config['general']['quickstart-done'] == '1'
|
quickstart_done = state_config['general']['quickstart-done'] == '1'
|
||||||
@ -386,7 +397,8 @@ def _init_modules(args, crash_handler):
|
|||||||
log.init.debug("Initializing web history...")
|
log.init.debug("Initializing web history...")
|
||||||
history.init(qApp)
|
history.init(qApp)
|
||||||
log.init.debug("Initializing crashlog...")
|
log.init.debug("Initializing crashlog...")
|
||||||
crash_handler.handle_segfault()
|
if not args.no_err_windows:
|
||||||
|
crash_handler.handle_segfault()
|
||||||
log.init.debug("Initializing sessions...")
|
log.init.debug("Initializing sessions...")
|
||||||
sessions.init(qApp)
|
sessions.init(qApp)
|
||||||
log.init.debug("Initializing js-bridge...")
|
log.init.debug("Initializing js-bridge...")
|
||||||
@ -624,13 +636,15 @@ class Quitter:
|
|||||||
try:
|
try:
|
||||||
save_manager.save(key, is_exit=True)
|
save_manager.save(key, is_exit=True)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
msgbox = QMessageBox(
|
error.handle_fatal_exc(
|
||||||
QMessageBox.Critical, "Error while saving!",
|
e, self._args, "Error while saving!",
|
||||||
"Error while saving {}: {}".format(key, e))
|
pre_text="Error while saving {}".format(key))
|
||||||
msgbox.exec_()
|
|
||||||
# Re-enable faulthandler to stdout, then remove crash log
|
# Re-enable faulthandler to stdout, then remove crash log
|
||||||
log.destroy.debug("Deactivating crash log...")
|
log.destroy.debug("Deactivating crash log...")
|
||||||
objreg.get('crash-handler').destroy_crashlogfile()
|
objreg.get('crash-handler').destroy_crashlogfile()
|
||||||
|
# Delete temp basedir
|
||||||
|
if self._args.temp_basedir:
|
||||||
|
atexit.register(shutil.rmtree, self._args.basedir)
|
||||||
# If we don't kill our custom handler here we might get segfaults
|
# If we don't kill our custom handler here we might get segfaults
|
||||||
log.destroy.debug("Deactiving message handler...")
|
log.destroy.debug("Deactiving message handler...")
|
||||||
qInstallMessageHandler(None)
|
qInstallMessageHandler(None)
|
||||||
|
@ -27,7 +27,7 @@ import zipfile
|
|||||||
|
|
||||||
from qutebrowser.config import config
|
from qutebrowser.config import config
|
||||||
from qutebrowser.utils import objreg, standarddir, log, message
|
from qutebrowser.utils import objreg, standarddir, log, message
|
||||||
from qutebrowser.commands import cmdutils
|
from qutebrowser.commands import cmdutils, cmdexc
|
||||||
|
|
||||||
|
|
||||||
def guess_zip_filename(zf):
|
def guess_zip_filename(zf):
|
||||||
@ -90,12 +90,18 @@ class HostBlocker:
|
|||||||
self.blocked_hosts = set()
|
self.blocked_hosts = set()
|
||||||
self._in_progress = []
|
self._in_progress = []
|
||||||
self._done_count = 0
|
self._done_count = 0
|
||||||
self._hosts_file = os.path.join(standarddir.data(), 'blocked-hosts')
|
data_dir = standarddir.data()
|
||||||
|
if data_dir is None:
|
||||||
|
self._hosts_file = None
|
||||||
|
else:
|
||||||
|
self._hosts_file = os.path.join(data_dir, 'blocked-hosts')
|
||||||
objreg.get('config').changed.connect(self.on_config_changed)
|
objreg.get('config').changed.connect(self.on_config_changed)
|
||||||
|
|
||||||
def read_hosts(self):
|
def read_hosts(self):
|
||||||
"""Read hosts from the existing blocked-hosts file."""
|
"""Read hosts from the existing blocked-hosts file."""
|
||||||
self.blocked_hosts = set()
|
self.blocked_hosts = set()
|
||||||
|
if self._hosts_file is None:
|
||||||
|
return
|
||||||
if os.path.exists(self._hosts_file):
|
if os.path.exists(self._hosts_file):
|
||||||
try:
|
try:
|
||||||
with open(self._hosts_file, 'r', encoding='utf-8') as f:
|
with open(self._hosts_file, 'r', encoding='utf-8') as f:
|
||||||
@ -104,13 +110,17 @@ class HostBlocker:
|
|||||||
except OSError:
|
except OSError:
|
||||||
log.misc.exception("Failed to read host blocklist!")
|
log.misc.exception("Failed to read host blocklist!")
|
||||||
else:
|
else:
|
||||||
if config.get('content', 'host-block-lists') is not None:
|
args = objreg.get('args')
|
||||||
|
if (config.get('content', 'host-block-lists') is not None and
|
||||||
|
args.basedir is None):
|
||||||
message.info('current',
|
message.info('current',
|
||||||
"Run :adblock-update to get adblock lists.")
|
"Run :adblock-update to get adblock lists.")
|
||||||
|
|
||||||
@cmdutils.register(instance='host-blocker', win_id='win_id')
|
@cmdutils.register(instance='host-blocker', win_id='win_id')
|
||||||
def adblock_update(self, win_id):
|
def adblock_update(self, win_id):
|
||||||
"""Update the adblock block lists."""
|
"""Update the adblock block lists."""
|
||||||
|
if self._hosts_file is None:
|
||||||
|
raise cmdexc.CommandError("No data storage is configured!")
|
||||||
self.blocked_hosts = set()
|
self.blocked_hosts = set()
|
||||||
self._done_count = 0
|
self._done_count = 0
|
||||||
urls = config.get('content', 'host-block-lists')
|
urls = config.get('content', 'host-block-lists')
|
||||||
|
@ -21,6 +21,7 @@
|
|||||||
|
|
||||||
import os.path
|
import os.path
|
||||||
|
|
||||||
|
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
|
||||||
@ -29,23 +30,41 @@ from qutebrowser.utils import utils, standarddir, objreg
|
|||||||
|
|
||||||
class DiskCache(QNetworkDiskCache):
|
class DiskCache(QNetworkDiskCache):
|
||||||
|
|
||||||
"""Disk cache which sets correct cache dir and size."""
|
"""Disk cache which sets correct cache dir and size.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
_activated: Whether the cache should be used.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.setCacheDirectory(os.path.join(standarddir.cache(), 'http'))
|
cache_dir = standarddir.cache()
|
||||||
|
if config.get('general', 'private-browsing') or cache_dir is None:
|
||||||
|
self._activated = False
|
||||||
|
else:
|
||||||
|
self._activated = True
|
||||||
|
self.setCacheDirectory(os.path.join(standarddir.cache(), 'http'))
|
||||||
self.setMaximumCacheSize(config.get('storage', 'cache-size'))
|
self.setMaximumCacheSize(config.get('storage', 'cache-size'))
|
||||||
objreg.get('config').changed.connect(self.cache_size_changed)
|
objreg.get('config').changed.connect(self.on_config_changed)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return utils.get_repr(self, size=self.cacheSize(),
|
return utils.get_repr(self, size=self.cacheSize(),
|
||||||
maxsize=self.maximumCacheSize(),
|
maxsize=self.maximumCacheSize(),
|
||||||
path=self.cacheDirectory())
|
path=self.cacheDirectory())
|
||||||
|
|
||||||
@config.change_filter('storage', 'cache-size')
|
@pyqtSlot(str, str)
|
||||||
def cache_size_changed(self):
|
def on_config_changed(self, section, option):
|
||||||
"""Update cache size if the config was changed."""
|
"""Update cache size/activated if the config was changed."""
|
||||||
self.setMaximumCacheSize(config.get('storage', 'cache-size'))
|
if (section, option) == ('storage', 'cache-size'):
|
||||||
|
self.setMaximumCacheSize(config.get('storage', 'cache-size'))
|
||||||
|
elif (section, option) == ('general', 'private-browsing'):
|
||||||
|
if (config.get('general', 'private-browsing') or
|
||||||
|
standarddir.cache() is None):
|
||||||
|
self._activated = False
|
||||||
|
else:
|
||||||
|
self._activated = True
|
||||||
|
self.setCacheDirectory(
|
||||||
|
os.path.join(standarddir.cache(), 'http'))
|
||||||
|
|
||||||
def cacheSize(self):
|
def cacheSize(self):
|
||||||
"""Return the current size taken up by the cache.
|
"""Return the current size taken up by the cache.
|
||||||
@ -53,10 +72,10 @@ class DiskCache(QNetworkDiskCache):
|
|||||||
Return:
|
Return:
|
||||||
An int.
|
An int.
|
||||||
"""
|
"""
|
||||||
if config.get('general', 'private-browsing'):
|
if self._activated:
|
||||||
return 0
|
|
||||||
else:
|
|
||||||
return super().cacheSize()
|
return super().cacheSize()
|
||||||
|
else:
|
||||||
|
return 0
|
||||||
|
|
||||||
def fileMetaData(self, filename):
|
def fileMetaData(self, filename):
|
||||||
"""Return the QNetworkCacheMetaData for the cache file filename.
|
"""Return the QNetworkCacheMetaData for the cache file filename.
|
||||||
@ -67,10 +86,10 @@ class DiskCache(QNetworkDiskCache):
|
|||||||
Return:
|
Return:
|
||||||
A QNetworkCacheMetaData object.
|
A QNetworkCacheMetaData object.
|
||||||
"""
|
"""
|
||||||
if config.get('general', 'private-browsing'):
|
if self._activated:
|
||||||
return QNetworkCacheMetaData()
|
|
||||||
else:
|
|
||||||
return super().fileMetaData(filename)
|
return super().fileMetaData(filename)
|
||||||
|
else:
|
||||||
|
return QNetworkCacheMetaData()
|
||||||
|
|
||||||
def data(self, url):
|
def data(self, url):
|
||||||
"""Return the data associated with url.
|
"""Return the data associated with url.
|
||||||
@ -81,10 +100,10 @@ class DiskCache(QNetworkDiskCache):
|
|||||||
return:
|
return:
|
||||||
A QIODevice or None.
|
A QIODevice or None.
|
||||||
"""
|
"""
|
||||||
if config.get('general', 'private-browsing'):
|
if self._activated:
|
||||||
return None
|
|
||||||
else:
|
|
||||||
return super().data(url)
|
return super().data(url)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
def insert(self, device):
|
def insert(self, device):
|
||||||
"""Insert the data in device and the prepared meta data into the cache.
|
"""Insert the data in device and the prepared meta data into the cache.
|
||||||
@ -92,10 +111,10 @@ class DiskCache(QNetworkDiskCache):
|
|||||||
Args:
|
Args:
|
||||||
device: A QIODevice.
|
device: A QIODevice.
|
||||||
"""
|
"""
|
||||||
if config.get('general', 'private-browsing'):
|
if self._activated:
|
||||||
return
|
|
||||||
else:
|
|
||||||
super().insert(device)
|
super().insert(device)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
def metaData(self, url):
|
def metaData(self, url):
|
||||||
"""Return the meta data for the url url.
|
"""Return the meta data for the url url.
|
||||||
@ -106,10 +125,10 @@ class DiskCache(QNetworkDiskCache):
|
|||||||
Return:
|
Return:
|
||||||
A QNetworkCacheMetaData object.
|
A QNetworkCacheMetaData object.
|
||||||
"""
|
"""
|
||||||
if config.get('general', 'private-browsing'):
|
if self._activated:
|
||||||
return QNetworkCacheMetaData()
|
|
||||||
else:
|
|
||||||
return super().metaData(url)
|
return super().metaData(url)
|
||||||
|
else:
|
||||||
|
return QNetworkCacheMetaData()
|
||||||
|
|
||||||
def prepare(self, meta_data):
|
def prepare(self, meta_data):
|
||||||
"""Return the device that should be populated with the data.
|
"""Return the device that should be populated with the data.
|
||||||
@ -120,10 +139,10 @@ class DiskCache(QNetworkDiskCache):
|
|||||||
Return:
|
Return:
|
||||||
A QIODevice or None.
|
A QIODevice or None.
|
||||||
"""
|
"""
|
||||||
if config.get('general', 'private-browsing'):
|
if self._activated:
|
||||||
return None
|
|
||||||
else:
|
|
||||||
return super().prepare(meta_data)
|
return super().prepare(meta_data)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
def remove(self, url):
|
def remove(self, url):
|
||||||
"""Remove the cache entry for url.
|
"""Remove the cache entry for url.
|
||||||
@ -131,10 +150,10 @@ class DiskCache(QNetworkDiskCache):
|
|||||||
Return:
|
Return:
|
||||||
True on success, False otherwise.
|
True on success, False otherwise.
|
||||||
"""
|
"""
|
||||||
if config.get('general', 'private-browsing'):
|
if self._activated:
|
||||||
return False
|
|
||||||
else:
|
|
||||||
return super().remove(url)
|
return super().remove(url)
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
def updateMetaData(self, meta_data):
|
def updateMetaData(self, meta_data):
|
||||||
"""Update the cache meta date for the meta_data's url to meta_data.
|
"""Update the cache meta date for the meta_data's url to meta_data.
|
||||||
@ -142,14 +161,14 @@ class DiskCache(QNetworkDiskCache):
|
|||||||
Args:
|
Args:
|
||||||
meta_data: A QNetworkCacheMetaData object.
|
meta_data: A QNetworkCacheMetaData object.
|
||||||
"""
|
"""
|
||||||
if config.get('general', 'private-browsing'):
|
if self._activated:
|
||||||
return
|
|
||||||
else:
|
|
||||||
super().updateMetaData(meta_data)
|
super().updateMetaData(meta_data)
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
def clear(self):
|
def clear(self):
|
||||||
"""Remove all items from the cache."""
|
"""Remove all items from the cache."""
|
||||||
if config.get('general', 'private-browsing'):
|
if self._activated:
|
||||||
return
|
|
||||||
else:
|
|
||||||
super().clear()
|
super().clear()
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
@ -27,8 +27,8 @@ import posixpath
|
|||||||
import functools
|
import functools
|
||||||
|
|
||||||
from PyQt5.QtWidgets import QApplication, QTabBar
|
from PyQt5.QtWidgets import QApplication, QTabBar
|
||||||
from PyQt5.QtCore import Qt, QUrl
|
from PyQt5.QtCore import Qt, QUrl, QEvent
|
||||||
from PyQt5.QtGui import QClipboard
|
from PyQt5.QtGui import QClipboard, QKeyEvent
|
||||||
from PyQt5.QtPrintSupport import QPrintDialog, QPrintPreviewDialog
|
from PyQt5.QtPrintSupport import QPrintDialog, QPrintPreviewDialog
|
||||||
from PyQt5.QtWebKitWidgets import QWebPage
|
from PyQt5.QtWebKitWidgets import QWebPage
|
||||||
import pygments
|
import pygments
|
||||||
@ -38,8 +38,10 @@ import pygments.formatters
|
|||||||
from qutebrowser.commands import userscripts, cmdexc, cmdutils
|
from qutebrowser.commands import userscripts, cmdexc, cmdutils
|
||||||
from qutebrowser.config import config, configexc
|
from qutebrowser.config import config, configexc
|
||||||
from qutebrowser.browser import webelem, inspector
|
from qutebrowser.browser import webelem, inspector
|
||||||
|
from qutebrowser.keyinput import modeman
|
||||||
from qutebrowser.utils import (message, usertypes, log, qtutils, urlutils,
|
from qutebrowser.utils import (message, usertypes, log, qtutils, urlutils,
|
||||||
objreg, utils)
|
objreg, utils)
|
||||||
|
from qutebrowser.utils.usertypes import KeyMode
|
||||||
from qutebrowser.misc import editor
|
from qutebrowser.misc import editor
|
||||||
|
|
||||||
|
|
||||||
@ -56,46 +58,40 @@ class CommandDispatcher:
|
|||||||
Attributes:
|
Attributes:
|
||||||
_editor: The ExternalEditor object.
|
_editor: The ExternalEditor object.
|
||||||
_win_id: The window ID the CommandDispatcher is associated with.
|
_win_id: The window ID the CommandDispatcher is associated with.
|
||||||
|
_tabbed_browser: The TabbedBrowser used.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, win_id):
|
def __init__(self, win_id, tabbed_browser):
|
||||||
self._editor = None
|
self._editor = None
|
||||||
self._win_id = win_id
|
self._win_id = win_id
|
||||||
|
self._tabbed_browser = tabbed_browser
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return utils.get_repr(self)
|
return utils.get_repr(self)
|
||||||
|
|
||||||
def _tabbed_browser(self, window=False):
|
def _new_tabbed_browser(self):
|
||||||
"""Convienence method to get the right tabbed-browser.
|
"""Get a tabbed-browser from a new window."""
|
||||||
|
|
||||||
Args:
|
|
||||||
window: If True, open a new window.
|
|
||||||
"""
|
|
||||||
from qutebrowser.mainwindow import mainwindow
|
from qutebrowser.mainwindow import mainwindow
|
||||||
if window:
|
new_window = mainwindow.MainWindow()
|
||||||
new_window = mainwindow.MainWindow()
|
new_window.show()
|
||||||
new_window.show()
|
return new_window.tabbed_browser
|
||||||
win_id = new_window.win_id
|
|
||||||
else:
|
|
||||||
win_id = self._win_id
|
|
||||||
return objreg.get('tabbed-browser', scope='window', window=win_id)
|
|
||||||
|
|
||||||
def _count(self):
|
def _count(self):
|
||||||
"""Convenience method to get the widget count."""
|
"""Convenience method to get the widget count."""
|
||||||
return self._tabbed_browser().count()
|
return self._tabbed_browser.count()
|
||||||
|
|
||||||
def _set_current_index(self, idx):
|
def _set_current_index(self, idx):
|
||||||
"""Convenience method to set the current widget index."""
|
"""Convenience method to set the current widget index."""
|
||||||
return self._tabbed_browser().setCurrentIndex(idx)
|
return self._tabbed_browser.setCurrentIndex(idx)
|
||||||
|
|
||||||
def _current_index(self):
|
def _current_index(self):
|
||||||
"""Convenience method to get the current widget index."""
|
"""Convenience method to get the current widget index."""
|
||||||
return self._tabbed_browser().currentIndex()
|
return self._tabbed_browser.currentIndex()
|
||||||
|
|
||||||
def _current_url(self):
|
def _current_url(self):
|
||||||
"""Convenience method to get the current url."""
|
"""Convenience method to get the current url."""
|
||||||
try:
|
try:
|
||||||
return self._tabbed_browser().current_url()
|
return self._tabbed_browser.current_url()
|
||||||
except qtutils.QtValueError as e:
|
except qtutils.QtValueError as e:
|
||||||
msg = "Current URL is invalid"
|
msg = "Current URL is invalid"
|
||||||
if e.reason:
|
if e.reason:
|
||||||
@ -105,7 +101,7 @@ class CommandDispatcher:
|
|||||||
|
|
||||||
def _current_widget(self):
|
def _current_widget(self):
|
||||||
"""Get the currently active widget from a command."""
|
"""Get the currently active widget from a command."""
|
||||||
widget = self._tabbed_browser().currentWidget()
|
widget = self._tabbed_browser.currentWidget()
|
||||||
if widget is None:
|
if widget is None:
|
||||||
raise cmdexc.CommandError("No WebView available yet!")
|
raise cmdexc.CommandError("No WebView available yet!")
|
||||||
return widget
|
return widget
|
||||||
@ -120,10 +116,10 @@ class CommandDispatcher:
|
|||||||
window: Whether to open in a new window
|
window: Whether to open in a new window
|
||||||
"""
|
"""
|
||||||
urlutils.raise_cmdexc_if_invalid(url)
|
urlutils.raise_cmdexc_if_invalid(url)
|
||||||
tabbed_browser = self._tabbed_browser()
|
tabbed_browser = self._tabbed_browser
|
||||||
cmdutils.check_exclusive((tab, background, window), 'tbw')
|
cmdutils.check_exclusive((tab, background, window), 'tbw')
|
||||||
if window:
|
if window:
|
||||||
tabbed_browser = self._tabbed_browser(window=True)
|
tabbed_browser = self._new_tabbed_browser()
|
||||||
tabbed_browser.tabopen(url)
|
tabbed_browser.tabopen(url)
|
||||||
elif tab:
|
elif tab:
|
||||||
tabbed_browser.tabopen(url, background=False, explicit=True)
|
tabbed_browser.tabopen(url, background=False, explicit=True)
|
||||||
@ -144,12 +140,11 @@ class CommandDispatcher:
|
|||||||
The widget with the given tab ID if count is given.
|
The widget with the given tab ID if count is given.
|
||||||
None if no widget was found.
|
None if no widget was found.
|
||||||
"""
|
"""
|
||||||
tabbed_browser = self._tabbed_browser()
|
|
||||||
if count is None:
|
if count is None:
|
||||||
return tabbed_browser.currentWidget()
|
return self._tabbed_browser.currentWidget()
|
||||||
elif 1 <= count <= self._count():
|
elif 1 <= count <= self._count():
|
||||||
cmdutils.check_overflow(count + 1, 'int')
|
cmdutils.check_overflow(count + 1, 'int')
|
||||||
return tabbed_browser.widget(count - 1)
|
return self._tabbed_browser.widget(count - 1)
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@ -165,12 +160,17 @@ class CommandDispatcher:
|
|||||||
perc = 100
|
perc = 100
|
||||||
elif perc is None:
|
elif perc is None:
|
||||||
perc = count
|
perc = count
|
||||||
perc = qtutils.check_overflow(perc, 'int', fatal=False)
|
if perc == 0:
|
||||||
frame = self._current_widget().page().currentFrame()
|
self.scroll('top')
|
||||||
m = frame.scrollBarMaximum(orientation)
|
elif perc == 100:
|
||||||
if m == 0:
|
self.scroll('bottom')
|
||||||
return
|
else:
|
||||||
frame.setScrollBarValue(orientation, int(m * perc / 100))
|
perc = qtutils.check_overflow(perc, 'int', fatal=False)
|
||||||
|
frame = self._current_widget().page().currentFrame()
|
||||||
|
m = frame.scrollBarMaximum(orientation)
|
||||||
|
if m == 0:
|
||||||
|
return
|
||||||
|
frame.setScrollBarValue(orientation, int(m * perc / 100))
|
||||||
|
|
||||||
def _tab_move_absolute(self, idx):
|
def _tab_move_absolute(self, idx):
|
||||||
"""Get an index for moving a tab absolutely.
|
"""Get an index for moving a tab absolutely.
|
||||||
@ -208,7 +208,7 @@ class CommandDispatcher:
|
|||||||
window=self._win_id)
|
window=self._win_id)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise cmdexc.CommandError("No last focused tab!")
|
raise cmdexc.CommandError("No last focused tab!")
|
||||||
idx = self._tabbed_browser().indexOf(tab)
|
idx = self._tabbed_browser.indexOf(tab)
|
||||||
if idx == -1:
|
if idx == -1:
|
||||||
raise cmdexc.CommandError("Last focused tab vanished!")
|
raise cmdexc.CommandError("Last focused tab vanished!")
|
||||||
self._set_current_index(idx)
|
self._set_current_index(idx)
|
||||||
@ -266,16 +266,15 @@ class CommandDispatcher:
|
|||||||
tab = self._cntwidget(count)
|
tab = self._cntwidget(count)
|
||||||
if tab is None:
|
if tab is None:
|
||||||
return
|
return
|
||||||
tabbed_browser = self._tabbed_browser()
|
tabbar = self._tabbed_browser.tabBar()
|
||||||
tabbar = tabbed_browser.tabBar()
|
|
||||||
selection_override = self._get_selection_override(left, right,
|
selection_override = self._get_selection_override(left, right,
|
||||||
opposite)
|
opposite)
|
||||||
if selection_override is None:
|
if selection_override is None:
|
||||||
tabbed_browser.close_tab(tab)
|
self._tabbed_browser.close_tab(tab)
|
||||||
else:
|
else:
|
||||||
old_selection_behavior = tabbar.selectionBehaviorOnRemove()
|
old_selection_behavior = tabbar.selectionBehaviorOnRemove()
|
||||||
tabbar.setSelectionBehaviorOnRemove(selection_override)
|
tabbar.setSelectionBehaviorOnRemove(selection_override)
|
||||||
tabbed_browser.close_tab(tab)
|
self._tabbed_browser.close_tab(tab)
|
||||||
tabbar.setSelectionBehaviorOnRemove(old_selection_behavior)
|
tabbar.setSelectionBehaviorOnRemove(old_selection_behavior)
|
||||||
|
|
||||||
@cmdutils.register(instance='command-dispatcher', name='open',
|
@cmdutils.register(instance='command-dispatcher', name='open',
|
||||||
@ -310,7 +309,7 @@ class CommandDispatcher:
|
|||||||
if count is None:
|
if count is None:
|
||||||
# We want to open a URL in the current tab, but none exists
|
# We want to open a URL in the current tab, but none exists
|
||||||
# yet.
|
# yet.
|
||||||
self._tabbed_browser().tabopen(url)
|
self._tabbed_browser.tabopen(url)
|
||||||
else:
|
else:
|
||||||
# Explicit count with a tab that doesn't exist.
|
# Explicit count with a tab that doesn't exist.
|
||||||
return
|
return
|
||||||
@ -386,12 +385,14 @@ class CommandDispatcher:
|
|||||||
"""
|
"""
|
||||||
if bg and window:
|
if bg and window:
|
||||||
raise cmdexc.CommandError("Only one of -b/-w can be given!")
|
raise cmdexc.CommandError("Only one of -b/-w can be given!")
|
||||||
cur_tabbed_browser = self._tabbed_browser()
|
|
||||||
curtab = self._current_widget()
|
curtab = self._current_widget()
|
||||||
cur_title = cur_tabbed_browser.page_title(self._current_index())
|
cur_title = self._tabbed_browser.page_title(self._current_index())
|
||||||
# The new tab could be in a new tabbed_browser (e.g. because of
|
# The new tab could be in a new tabbed_browser (e.g. because of
|
||||||
# tabs-are-windows being set)
|
# tabs-are-windows being set)
|
||||||
new_tabbed_browser = self._tabbed_browser(window)
|
if window:
|
||||||
|
new_tabbed_browser = self._new_tabbed_browser()
|
||||||
|
else:
|
||||||
|
new_tabbed_browser = self._tabbed_browser
|
||||||
newtab = new_tabbed_browser.tabopen(background=bg, explicit=True)
|
newtab = new_tabbed_browser.tabopen(background=bg, explicit=True)
|
||||||
new_tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
new_tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||||
window=newtab.win_id)
|
window=newtab.win_id)
|
||||||
@ -409,9 +410,8 @@ class CommandDispatcher:
|
|||||||
"""Detach the current tab to its own window."""
|
"""Detach the current tab to its own window."""
|
||||||
url = self._current_url()
|
url = self._current_url()
|
||||||
self._open(url, window=True)
|
self._open(url, window=True)
|
||||||
tabbed_browser = self._tabbed_browser()
|
|
||||||
cur_widget = self._current_widget()
|
cur_widget = self._current_widget()
|
||||||
tabbed_browser.close_tab(cur_widget)
|
self._tabbed_browser.close_tab(cur_widget)
|
||||||
|
|
||||||
def _back_forward(self, tab, bg, window, count, forward):
|
def _back_forward(self, tab, bg, window, count, forward):
|
||||||
"""Helper function for :back/:forward."""
|
"""Helper function for :back/:forward."""
|
||||||
@ -555,8 +555,8 @@ class CommandDispatcher:
|
|||||||
|
|
||||||
@cmdutils.register(instance='command-dispatcher', hide=True,
|
@cmdutils.register(instance='command-dispatcher', hide=True,
|
||||||
scope='window', count='count')
|
scope='window', count='count')
|
||||||
def scroll(self, dx: {'type': float}, dy: {'type': float}, count=1):
|
def scroll_px(self, dx: {'type': float}, dy: {'type': float}, count=1):
|
||||||
"""Scroll the current tab by 'count * dx/dy'.
|
"""Scroll the current tab by 'count * dx/dy' pixels.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
dx: How much to scroll in x-direction.
|
dx: How much to scroll in x-direction.
|
||||||
@ -569,6 +569,61 @@ class CommandDispatcher:
|
|||||||
cmdutils.check_overflow(dy, 'int')
|
cmdutils.check_overflow(dy, 'int')
|
||||||
self._current_widget().page().currentFrame().scroll(dx, dy)
|
self._current_widget().page().currentFrame().scroll(dx, dy)
|
||||||
|
|
||||||
|
@cmdutils.register(instance='command-dispatcher', hide=True,
|
||||||
|
scope='window', count='count')
|
||||||
|
def scroll(self,
|
||||||
|
direction: {'type': (str, float)},
|
||||||
|
dy: {'type': float, 'hide': True}=None,
|
||||||
|
count=1):
|
||||||
|
"""Scroll the current tab in the given direction.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
direction: In which direction to scroll
|
||||||
|
(up/down/left/right/top/bottom).
|
||||||
|
dy: Deprecated argument to support the old dx/dy form.
|
||||||
|
count: multiplier
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Check for deprecated dx/dy form (like with scroll-px).
|
||||||
|
dx = float(direction)
|
||||||
|
dy = float(dy)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
# Invalid values will get handled later.
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
message.warning(self._win_id, ":scroll with dx/dy arguments is "
|
||||||
|
"deprecated - use :scroll-px instead!")
|
||||||
|
self.scroll_px(dx, dy, count=count)
|
||||||
|
return
|
||||||
|
|
||||||
|
fake_keys = {
|
||||||
|
'up': Qt.Key_Up,
|
||||||
|
'down': Qt.Key_Down,
|
||||||
|
'left': Qt.Key_Left,
|
||||||
|
'right': Qt.Key_Right,
|
||||||
|
'top': Qt.Key_Home,
|
||||||
|
'bottom': Qt.Key_End,
|
||||||
|
'page-up': Qt.Key_PageUp,
|
||||||
|
'page-down': Qt.Key_PageDown,
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
key = fake_keys[direction]
|
||||||
|
except KeyError:
|
||||||
|
raise cmdexc.CommandError("Invalid value {!r} for direction - "
|
||||||
|
"expected one of: {}".format(
|
||||||
|
direction, ', '.join(fake_keys)))
|
||||||
|
widget = self._current_widget()
|
||||||
|
press_evt = QKeyEvent(QEvent.KeyPress, key, Qt.NoModifier, 0, 0, 0)
|
||||||
|
release_evt = QKeyEvent(QEvent.KeyRelease, key, Qt.NoModifier, 0, 0, 0)
|
||||||
|
|
||||||
|
# Count doesn't make sense with top/bottom
|
||||||
|
if direction in ('top', 'bottom'):
|
||||||
|
count = 1
|
||||||
|
|
||||||
|
for _ in range(count):
|
||||||
|
widget.keyPressEvent(press_evt)
|
||||||
|
widget.keyReleaseEvent(release_evt)
|
||||||
|
|
||||||
@cmdutils.register(instance='command-dispatcher', hide=True,
|
@cmdutils.register(instance='command-dispatcher', hide=True,
|
||||||
scope='window', count='count')
|
scope='window', count='count')
|
||||||
def scroll_perc(self, perc: {'type': float}=None,
|
def scroll_perc(self, perc: {'type': float}=None,
|
||||||
@ -596,10 +651,22 @@ class CommandDispatcher:
|
|||||||
y: How many pages to scroll down.
|
y: How many pages to scroll down.
|
||||||
count: multiplier
|
count: multiplier
|
||||||
"""
|
"""
|
||||||
|
mult_x = count * x
|
||||||
|
mult_y = count * y
|
||||||
|
if mult_y.is_integer():
|
||||||
|
if mult_y == 0:
|
||||||
|
pass
|
||||||
|
elif mult_y < 0:
|
||||||
|
self.scroll('page-up', count=-int(mult_y))
|
||||||
|
elif mult_y > 0:
|
||||||
|
self.scroll('page-down', count=int(mult_y))
|
||||||
|
mult_y = 0
|
||||||
|
if mult_x == 0 and mult_y == 0:
|
||||||
|
return
|
||||||
frame = self._current_widget().page().currentFrame()
|
frame = self._current_widget().page().currentFrame()
|
||||||
size = frame.geometry()
|
size = frame.geometry()
|
||||||
dx = count * x * size.width()
|
dx = mult_x * size.width()
|
||||||
dy = count * y * size.height()
|
dy = mult_y * size.height()
|
||||||
cmdutils.check_overflow(dx, 'int')
|
cmdutils.check_overflow(dx, 'int')
|
||||||
cmdutils.check_overflow(dy, 'int')
|
cmdutils.check_overflow(dy, 'int')
|
||||||
frame.scroll(dx, dy)
|
frame.scroll(dx, dy)
|
||||||
@ -614,7 +681,7 @@ class CommandDispatcher:
|
|||||||
"""
|
"""
|
||||||
clipboard = QApplication.clipboard()
|
clipboard = QApplication.clipboard()
|
||||||
if title:
|
if title:
|
||||||
s = self._tabbed_browser().page_title(self._current_index())
|
s = self._tabbed_browser.page_title(self._current_index())
|
||||||
else:
|
else:
|
||||||
s = self._current_url().toString(
|
s = self._current_url().toString(
|
||||||
QUrl.FullyEncoded | QUrl.RemovePassword)
|
QUrl.FullyEncoded | QUrl.RemovePassword)
|
||||||
@ -680,22 +747,21 @@ class CommandDispatcher:
|
|||||||
right: Keep tabs to the right of the current.
|
right: Keep tabs to the right of the current.
|
||||||
"""
|
"""
|
||||||
cmdutils.check_exclusive((left, right), 'lr')
|
cmdutils.check_exclusive((left, right), 'lr')
|
||||||
tabbed_browser = self._tabbed_browser()
|
cur_idx = self._tabbed_browser.currentIndex()
|
||||||
cur_idx = tabbed_browser.currentIndex()
|
|
||||||
assert cur_idx != -1
|
assert cur_idx != -1
|
||||||
|
|
||||||
for i, tab in enumerate(tabbed_browser.widgets()):
|
for i, tab in enumerate(self._tabbed_browser.widgets()):
|
||||||
if (i == cur_idx or (left and i < cur_idx) or
|
if (i == cur_idx or (left and i < cur_idx) or
|
||||||
(right and i > cur_idx)):
|
(right and i > cur_idx)):
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
tabbed_browser.close_tab(tab)
|
self._tabbed_browser.close_tab(tab)
|
||||||
|
|
||||||
@cmdutils.register(instance='command-dispatcher', scope='window')
|
@cmdutils.register(instance='command-dispatcher', scope='window')
|
||||||
def undo(self):
|
def undo(self):
|
||||||
"""Re-open a closed tab (optionally skipping [count] closed tabs)."""
|
"""Re-open a closed tab (optionally skipping [count] closed tabs)."""
|
||||||
try:
|
try:
|
||||||
self._tabbed_browser().undo()
|
self._tabbed_browser.undo()
|
||||||
except IndexError:
|
except IndexError:
|
||||||
raise cmdexc.CommandError("Nothing to undo!")
|
raise cmdexc.CommandError("Nothing to undo!")
|
||||||
|
|
||||||
@ -808,20 +874,19 @@ class CommandDispatcher:
|
|||||||
if not 0 <= new_idx < self._count():
|
if not 0 <= new_idx < self._count():
|
||||||
raise cmdexc.CommandError("Can't move tab to position {}!".format(
|
raise cmdexc.CommandError("Can't move tab to position {}!".format(
|
||||||
new_idx))
|
new_idx))
|
||||||
tabbed_browser = self._tabbed_browser()
|
|
||||||
tab = self._current_widget()
|
tab = self._current_widget()
|
||||||
cur_idx = self._current_index()
|
cur_idx = self._current_index()
|
||||||
icon = tabbed_browser.tabIcon(cur_idx)
|
icon = self._tabbed_browser.tabIcon(cur_idx)
|
||||||
label = tabbed_browser.page_title(cur_idx)
|
label = self._tabbed_browser.page_title(cur_idx)
|
||||||
cmdutils.check_overflow(cur_idx, 'int')
|
cmdutils.check_overflow(cur_idx, 'int')
|
||||||
cmdutils.check_overflow(new_idx, 'int')
|
cmdutils.check_overflow(new_idx, 'int')
|
||||||
tabbed_browser.setUpdatesEnabled(False)
|
self._tabbed_browser.setUpdatesEnabled(False)
|
||||||
try:
|
try:
|
||||||
tabbed_browser.removeTab(cur_idx)
|
self._tabbed_browser.removeTab(cur_idx)
|
||||||
tabbed_browser.insertTab(new_idx, tab, icon, label)
|
self._tabbed_browser.insertTab(new_idx, tab, icon, label)
|
||||||
self._set_current_index(new_idx)
|
self._set_current_index(new_idx)
|
||||||
finally:
|
finally:
|
||||||
tabbed_browser.setUpdatesEnabled(True)
|
self._tabbed_browser.setUpdatesEnabled(True)
|
||||||
|
|
||||||
@cmdutils.register(instance='command-dispatcher', scope='window',
|
@cmdutils.register(instance='command-dispatcher', scope='window',
|
||||||
win_id='win_id')
|
win_id='win_id')
|
||||||
@ -877,11 +942,10 @@ class CommandDispatcher:
|
|||||||
}
|
}
|
||||||
|
|
||||||
idx = self._current_index()
|
idx = self._current_index()
|
||||||
tabbed_browser = self._tabbed_browser()
|
|
||||||
if idx != -1:
|
if idx != -1:
|
||||||
env['QUTE_TITLE'] = tabbed_browser.page_title(idx)
|
env['QUTE_TITLE'] = self._tabbed_browser.page_title(idx)
|
||||||
|
|
||||||
webview = tabbed_browser.currentWidget()
|
webview = self._tabbed_browser.currentWidget()
|
||||||
if webview is None:
|
if webview is None:
|
||||||
mainframe = None
|
mainframe = None
|
||||||
else:
|
else:
|
||||||
@ -891,7 +955,7 @@ class CommandDispatcher:
|
|||||||
mainframe = webview.page().mainFrame()
|
mainframe = webview.page().mainFrame()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
url = tabbed_browser.current_url()
|
url = self._tabbed_browser.current_url()
|
||||||
except qtutils.QtValueError:
|
except qtutils.QtValueError:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
@ -983,7 +1047,7 @@ class CommandDispatcher:
|
|||||||
full=True, linenos='table')
|
full=True, linenos='table')
|
||||||
highlighted = pygments.highlight(html, lexer, formatter)
|
highlighted = pygments.highlight(html, lexer, formatter)
|
||||||
current_url = self._current_url()
|
current_url = self._current_url()
|
||||||
tab = self._tabbed_browser().tabopen(explicit=True)
|
tab = self._tabbed_browser.tabopen(explicit=True)
|
||||||
tab.setHtml(highlighted, current_url)
|
tab.setHtml(highlighted, current_url)
|
||||||
tab.viewing_source = True
|
tab.viewing_source = True
|
||||||
|
|
||||||
@ -1030,8 +1094,7 @@ class CommandDispatcher:
|
|||||||
self._open(url, tab, bg, window)
|
self._open(url, tab, bg, window)
|
||||||
|
|
||||||
@cmdutils.register(instance='command-dispatcher',
|
@cmdutils.register(instance='command-dispatcher',
|
||||||
modes=[usertypes.KeyMode.insert],
|
modes=[KeyMode.insert], hide=True, scope='window')
|
||||||
hide=True, scope='window')
|
|
||||||
def open_editor(self):
|
def open_editor(self):
|
||||||
"""Open an external editor with the currently selected form field.
|
"""Open an external editor with the currently selected form field.
|
||||||
|
|
||||||
@ -1056,7 +1119,7 @@ class CommandDispatcher:
|
|||||||
else:
|
else:
|
||||||
text = elem.evaluateJavaScript('this.value')
|
text = elem.evaluateJavaScript('this.value')
|
||||||
self._editor = editor.ExternalEditor(
|
self._editor = editor.ExternalEditor(
|
||||||
self._win_id, self._tabbed_browser())
|
self._win_id, self._tabbed_browser)
|
||||||
self._editor.editing_finished.connect(
|
self._editor.editing_finished.connect(
|
||||||
functools.partial(self.on_editing_finished, elem))
|
functools.partial(self.on_editing_finished, elem))
|
||||||
self._editor.edit(text)
|
self._editor.edit(text)
|
||||||
@ -1151,6 +1214,283 @@ class CommandDispatcher:
|
|||||||
for _ in range(count):
|
for _ in range(count):
|
||||||
view.search(view.search_text, flags)
|
view.search(view.search_text, flags)
|
||||||
|
|
||||||
|
@cmdutils.register(instance='command-dispatcher', hide=True,
|
||||||
|
modes=[KeyMode.caret], scope='window', count='count')
|
||||||
|
def move_to_next_line(self, count=1):
|
||||||
|
"""Move the cursor or selection to the next line.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
count: How many lines to move.
|
||||||
|
"""
|
||||||
|
webview = self._current_widget()
|
||||||
|
if not webview.selection_enabled:
|
||||||
|
act = QWebPage.MoveToNextLine
|
||||||
|
else:
|
||||||
|
act = QWebPage.SelectNextLine
|
||||||
|
for _ in range(count):
|
||||||
|
webview.triggerPageAction(act)
|
||||||
|
|
||||||
|
@cmdutils.register(instance='command-dispatcher', hide=True,
|
||||||
|
modes=[KeyMode.caret], scope='window', count='count')
|
||||||
|
def move_to_prev_line(self, count=1):
|
||||||
|
"""Move the cursor or selection to the prev line.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
count: How many lines to move.
|
||||||
|
"""
|
||||||
|
webview = self._current_widget()
|
||||||
|
if not webview.selection_enabled:
|
||||||
|
act = QWebPage.MoveToPreviousLine
|
||||||
|
else:
|
||||||
|
act = QWebPage.SelectPreviousLine
|
||||||
|
for _ in range(count):
|
||||||
|
webview.triggerPageAction(act)
|
||||||
|
|
||||||
|
@cmdutils.register(instance='command-dispatcher', hide=True,
|
||||||
|
modes=[KeyMode.caret], scope='window', count='count')
|
||||||
|
def move_to_next_char(self, count=1):
|
||||||
|
"""Move the cursor or selection to the next char.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
count: How many lines to move.
|
||||||
|
"""
|
||||||
|
webview = self._current_widget()
|
||||||
|
if not webview.selection_enabled:
|
||||||
|
act = QWebPage.MoveToNextChar
|
||||||
|
else:
|
||||||
|
act = QWebPage.SelectNextChar
|
||||||
|
for _ in range(count):
|
||||||
|
webview.triggerPageAction(act)
|
||||||
|
|
||||||
|
@cmdutils.register(instance='command-dispatcher', hide=True,
|
||||||
|
modes=[KeyMode.caret], scope='window', count='count')
|
||||||
|
def move_to_prev_char(self, count=1):
|
||||||
|
"""Move the cursor or selection to the previous char.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
count: How many chars to move.
|
||||||
|
"""
|
||||||
|
webview = self._current_widget()
|
||||||
|
if not webview.selection_enabled:
|
||||||
|
act = QWebPage.MoveToPreviousChar
|
||||||
|
else:
|
||||||
|
act = QWebPage.SelectPreviousChar
|
||||||
|
for _ in range(count):
|
||||||
|
webview.triggerPageAction(act)
|
||||||
|
|
||||||
|
@cmdutils.register(instance='command-dispatcher', hide=True,
|
||||||
|
modes=[KeyMode.caret], scope='window', count='count')
|
||||||
|
def move_to_end_of_word(self, count=1):
|
||||||
|
"""Move the cursor or selection to the end of the word.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
count: How many words to move.
|
||||||
|
"""
|
||||||
|
webview = self._current_widget()
|
||||||
|
if not webview.selection_enabled:
|
||||||
|
act = QWebPage.MoveToNextWord
|
||||||
|
else:
|
||||||
|
act = QWebPage.SelectNextWord
|
||||||
|
for _ in range(count):
|
||||||
|
webview.triggerPageAction(act)
|
||||||
|
|
||||||
|
@cmdutils.register(instance='command-dispatcher', hide=True,
|
||||||
|
modes=[KeyMode.caret], scope='window', count='count')
|
||||||
|
def move_to_next_word(self, count=1):
|
||||||
|
"""Move the cursor or selection to the next word.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
count: How many words to move.
|
||||||
|
"""
|
||||||
|
webview = self._current_widget()
|
||||||
|
if not webview.selection_enabled:
|
||||||
|
act = [QWebPage.MoveToNextWord, QWebPage.MoveToNextChar]
|
||||||
|
else:
|
||||||
|
act = [QWebPage.SelectNextWord, QWebPage.SelectNextChar]
|
||||||
|
for _ in range(count):
|
||||||
|
for a in act:
|
||||||
|
webview.triggerPageAction(a)
|
||||||
|
|
||||||
|
@cmdutils.register(instance='command-dispatcher', hide=True,
|
||||||
|
modes=[KeyMode.caret], scope='window', count='count')
|
||||||
|
def move_to_prev_word(self, count=1):
|
||||||
|
"""Move the cursor or selection to the previous word.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
count: How many words to move.
|
||||||
|
"""
|
||||||
|
webview = self._current_widget()
|
||||||
|
if not webview.selection_enabled:
|
||||||
|
act = QWebPage.MoveToPreviousWord
|
||||||
|
else:
|
||||||
|
act = QWebPage.SelectPreviousWord
|
||||||
|
for _ in range(count):
|
||||||
|
webview.triggerPageAction(act)
|
||||||
|
|
||||||
|
@cmdutils.register(instance='command-dispatcher', hide=True,
|
||||||
|
modes=[KeyMode.caret], scope='window')
|
||||||
|
def move_to_start_of_line(self):
|
||||||
|
"""Move the cursor or selection to the start of the line."""
|
||||||
|
webview = self._current_widget()
|
||||||
|
if not webview.selection_enabled:
|
||||||
|
act = QWebPage.MoveToStartOfLine
|
||||||
|
else:
|
||||||
|
act = QWebPage.SelectStartOfLine
|
||||||
|
webview.triggerPageAction(act)
|
||||||
|
|
||||||
|
@cmdutils.register(instance='command-dispatcher', hide=True,
|
||||||
|
modes=[KeyMode.caret], scope='window')
|
||||||
|
def move_to_end_of_line(self):
|
||||||
|
"""Move the cursor or selection to the end of line."""
|
||||||
|
webview = self._current_widget()
|
||||||
|
if not webview.selection_enabled:
|
||||||
|
act = QWebPage.MoveToEndOfLine
|
||||||
|
else:
|
||||||
|
act = QWebPage.SelectEndOfLine
|
||||||
|
webview.triggerPageAction(act)
|
||||||
|
|
||||||
|
@cmdutils.register(instance='command-dispatcher', hide=True,
|
||||||
|
modes=[KeyMode.caret], scope='window', count='count')
|
||||||
|
def move_to_start_of_next_block(self, count=1):
|
||||||
|
"""Move the cursor or selection to the start of next block.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
count: How many blocks to move.
|
||||||
|
"""
|
||||||
|
webview = self._current_widget()
|
||||||
|
if not webview.selection_enabled:
|
||||||
|
act = [QWebPage.MoveToEndOfBlock, QWebPage.MoveToNextLine,
|
||||||
|
QWebPage.MoveToStartOfBlock]
|
||||||
|
else:
|
||||||
|
act = [QWebPage.SelectEndOfBlock, QWebPage.SelectNextLine,
|
||||||
|
QWebPage.SelectStartOfBlock]
|
||||||
|
for _ in range(count):
|
||||||
|
for a in act:
|
||||||
|
webview.triggerPageAction(a)
|
||||||
|
|
||||||
|
@cmdutils.register(instance='command-dispatcher', hide=True,
|
||||||
|
modes=[KeyMode.caret], scope='window', count='count')
|
||||||
|
def move_to_start_of_prev_block(self, count=1):
|
||||||
|
"""Move the cursor or selection to the start of previous block.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
count: How many blocks to move.
|
||||||
|
"""
|
||||||
|
webview = self._current_widget()
|
||||||
|
if not webview.selection_enabled:
|
||||||
|
act = [QWebPage.MoveToStartOfBlock, QWebPage.MoveToPreviousLine,
|
||||||
|
QWebPage.MoveToStartOfBlock]
|
||||||
|
else:
|
||||||
|
act = [QWebPage.SelectStartOfBlock, QWebPage.SelectPreviousLine,
|
||||||
|
QWebPage.SelectStartOfBlock]
|
||||||
|
for _ in range(count):
|
||||||
|
for a in act:
|
||||||
|
webview.triggerPageAction(a)
|
||||||
|
|
||||||
|
@cmdutils.register(instance='command-dispatcher', hide=True,
|
||||||
|
modes=[KeyMode.caret], scope='window', count='count')
|
||||||
|
def move_to_end_of_next_block(self, count=1):
|
||||||
|
"""Move the cursor or selection to the end of next block.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
count: How many blocks to move.
|
||||||
|
"""
|
||||||
|
webview = self._current_widget()
|
||||||
|
if not webview.selection_enabled:
|
||||||
|
act = [QWebPage.MoveToEndOfBlock, QWebPage.MoveToNextLine,
|
||||||
|
QWebPage.MoveToEndOfBlock]
|
||||||
|
else:
|
||||||
|
act = [QWebPage.SelectEndOfBlock, QWebPage.SelectNextLine,
|
||||||
|
QWebPage.SelectEndOfBlock]
|
||||||
|
for _ in range(count):
|
||||||
|
for a in act:
|
||||||
|
webview.triggerPageAction(a)
|
||||||
|
|
||||||
|
@cmdutils.register(instance='command-dispatcher', hide=True,
|
||||||
|
modes=[KeyMode.caret], scope='window', count='count')
|
||||||
|
def move_to_end_of_prev_block(self, count=1):
|
||||||
|
"""Move the cursor or selection to the end of previous block.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
count: How many blocks to move.
|
||||||
|
"""
|
||||||
|
webview = self._current_widget()
|
||||||
|
if not webview.selection_enabled:
|
||||||
|
act = [QWebPage.MoveToStartOfBlock, QWebPage.MoveToPreviousLine,
|
||||||
|
QWebPage.MoveToEndOfBlock]
|
||||||
|
else:
|
||||||
|
act = [QWebPage.SelectStartOfBlock, QWebPage.SelectPreviousLine,
|
||||||
|
QWebPage.SelectEndOfBlock]
|
||||||
|
for _ in range(count):
|
||||||
|
for a in act:
|
||||||
|
webview.triggerPageAction(a)
|
||||||
|
|
||||||
|
@cmdutils.register(instance='command-dispatcher', hide=True,
|
||||||
|
modes=[KeyMode.caret], scope='window')
|
||||||
|
def move_to_start_of_document(self):
|
||||||
|
"""Move the cursor or selection to the start of the document."""
|
||||||
|
webview = self._current_widget()
|
||||||
|
if not webview.selection_enabled:
|
||||||
|
act = QWebPage.MoveToStartOfDocument
|
||||||
|
else:
|
||||||
|
act = QWebPage.SelectStartOfDocument
|
||||||
|
webview.triggerPageAction(act)
|
||||||
|
|
||||||
|
@cmdutils.register(instance='command-dispatcher', hide=True,
|
||||||
|
modes=[KeyMode.caret], scope='window')
|
||||||
|
def move_to_end_of_document(self):
|
||||||
|
"""Move the cursor or selection to the end of the document."""
|
||||||
|
webview = self._current_widget()
|
||||||
|
if not webview.selection_enabled:
|
||||||
|
act = QWebPage.MoveToEndOfDocument
|
||||||
|
else:
|
||||||
|
act = QWebPage.SelectEndOfDocument
|
||||||
|
webview.triggerPageAction(act)
|
||||||
|
|
||||||
|
@cmdutils.register(instance='command-dispatcher', hide=True,
|
||||||
|
modes=[KeyMode.caret], scope='window')
|
||||||
|
def yank_selected(self, sel=False, keep=False):
|
||||||
|
"""Yank the selected text to the clipboard or primary selection.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sel: Use the primary selection instead of the clipboard.
|
||||||
|
keep: If given, stay in visual mode after yanking.
|
||||||
|
"""
|
||||||
|
s = self._current_widget().selectedText()
|
||||||
|
if not self._current_widget().hasSelection() or len(s) == 0:
|
||||||
|
message.info(self._win_id, "Nothing to yank")
|
||||||
|
return
|
||||||
|
|
||||||
|
clipboard = QApplication.clipboard()
|
||||||
|
if sel and clipboard.supportsSelection():
|
||||||
|
mode = QClipboard.Selection
|
||||||
|
target = "primary selection"
|
||||||
|
else:
|
||||||
|
mode = QClipboard.Clipboard
|
||||||
|
target = "clipboard"
|
||||||
|
log.misc.debug("Yanking to {}: '{}'".format(target, s))
|
||||||
|
clipboard.setText(s, mode)
|
||||||
|
message.info(self._win_id, "{} {} yanked to {}".format(
|
||||||
|
len(s), "char" if len(s) == 1 else "chars", target))
|
||||||
|
if not keep:
|
||||||
|
modeman.leave(self._win_id, KeyMode.caret, "yank selected")
|
||||||
|
|
||||||
|
@cmdutils.register(instance='command-dispatcher', hide=True,
|
||||||
|
modes=[KeyMode.caret], scope='window')
|
||||||
|
def toggle_selection(self):
|
||||||
|
"""Toggle caret selection mode."""
|
||||||
|
widget = self._current_widget()
|
||||||
|
widget.selection_enabled = not widget.selection_enabled
|
||||||
|
mainwindow = objreg.get('main-window', scope='window',
|
||||||
|
window=self._win_id)
|
||||||
|
mainwindow.status.set_mode_active(usertypes.KeyMode.caret, True)
|
||||||
|
|
||||||
|
@cmdutils.register(instance='command-dispatcher', hide=True,
|
||||||
|
modes=[KeyMode.caret], scope='window')
|
||||||
|
def drop_selection(self):
|
||||||
|
"""Drop selection and keep selection mode enabled."""
|
||||||
|
self._current_widget().triggerPageAction(QWebPage.MoveToNextChar)
|
||||||
|
|
||||||
@cmdutils.register(instance='command-dispatcher', scope='window',
|
@cmdutils.register(instance='command-dispatcher', scope='window',
|
||||||
count='count', debug=True)
|
count='count', debug=True)
|
||||||
def debug_webaction(self, action, count=1):
|
def debug_webaction(self, action, count=1):
|
||||||
|
@ -691,8 +691,11 @@ class DownloadManager(QAbstractListModel):
|
|||||||
if fileobj is not None or filename is not None:
|
if fileobj is not None or filename is not None:
|
||||||
return self.fetch_request(request, page, fileobj, filename,
|
return self.fetch_request(request, page, fileobj, filename,
|
||||||
auto_remove, suggested_fn)
|
auto_remove, suggested_fn)
|
||||||
encoding = sys.getfilesystemencoding()
|
if suggested_fn is None:
|
||||||
suggested_fn = utils.force_encoding(suggested_fn, encoding)
|
suggested_fn = 'qutebrowser-download'
|
||||||
|
else:
|
||||||
|
encoding = sys.getfilesystemencoding()
|
||||||
|
suggested_fn = utils.force_encoding(suggested_fn, encoding)
|
||||||
q = self._prepare_question()
|
q = self._prepare_question()
|
||||||
q.default = _path_suggestion(suggested_fn)
|
q.default = _path_suggestion(suggested_fn)
|
||||||
message_bridge = objreg.get('message-bridge', scope='window',
|
message_bridge = objreg.get('message-bridge', scope='window',
|
||||||
@ -1043,7 +1046,7 @@ class DownloadManager(QAbstractListModel):
|
|||||||
"""Override flags so items aren't selectable.
|
"""Override flags so items aren't selectable.
|
||||||
|
|
||||||
The default would be Qt.ItemIsEnabled | Qt.ItemIsSelectable."""
|
The default would be Qt.ItemIsEnabled | Qt.ItemIsSelectable."""
|
||||||
return Qt.ItemIsEnabled
|
return Qt.ItemIsEnabled | Qt.ItemNeverHasChildren
|
||||||
|
|
||||||
def rowCount(self, parent=QModelIndex()):
|
def rowCount(self, parent=QModelIndex()):
|
||||||
"""Get count of active downloads."""
|
"""Get count of active downloads."""
|
||||||
|
@ -83,25 +83,7 @@ class WebHistory(QWebHistoryInterface):
|
|||||||
self._lineparser = lineparser.AppendLineParser(
|
self._lineparser = lineparser.AppendLineParser(
|
||||||
standarddir.data(), 'history', parent=self)
|
standarddir.data(), 'history', parent=self)
|
||||||
self._history_dict = collections.OrderedDict()
|
self._history_dict = collections.OrderedDict()
|
||||||
with self._lineparser.open():
|
self._read_history()
|
||||||
for line in self._lineparser:
|
|
||||||
data = line.rstrip().split(maxsplit=1)
|
|
||||||
if not data:
|
|
||||||
# empty line
|
|
||||||
continue
|
|
||||||
elif len(data) != 2:
|
|
||||||
# other malformed line
|
|
||||||
log.init.warning("Invalid history entry {!r}!".format(
|
|
||||||
line))
|
|
||||||
continue
|
|
||||||
atime, url = data
|
|
||||||
# This de-duplicates history entries; only the latest
|
|
||||||
# entry for each URL is kept. If you want to keep
|
|
||||||
# information about previous hits change the items in
|
|
||||||
# old_urls to be lists or change HistoryEntry to have a
|
|
||||||
# list of atimes.
|
|
||||||
self._history_dict[url] = HistoryEntry(atime, url)
|
|
||||||
self._history_dict.move_to_end(url)
|
|
||||||
self._new_history = []
|
self._new_history = []
|
||||||
self._saved_count = 0
|
self._saved_count = 0
|
||||||
objreg.get('save-manager').add_saveable(
|
objreg.get('save-manager').add_saveable(
|
||||||
@ -119,6 +101,36 @@ class WebHistory(QWebHistoryInterface):
|
|||||||
def __len__(self):
|
def __len__(self):
|
||||||
return len(self._history_dict)
|
return len(self._history_dict)
|
||||||
|
|
||||||
|
def _read_history(self):
|
||||||
|
"""Read the initial history."""
|
||||||
|
if standarddir.data() is None:
|
||||||
|
return
|
||||||
|
with self._lineparser.open():
|
||||||
|
for line in self._lineparser:
|
||||||
|
data = line.rstrip().split(maxsplit=1)
|
||||||
|
if not data:
|
||||||
|
# empty line
|
||||||
|
continue
|
||||||
|
elif len(data) != 2:
|
||||||
|
# other malformed line
|
||||||
|
log.init.warning("Invalid history entry {!r}!".format(
|
||||||
|
line))
|
||||||
|
continue
|
||||||
|
atime, url = data
|
||||||
|
if atime.startswith('\0'):
|
||||||
|
log.init.warning(
|
||||||
|
"Removing NUL bytes from entry {!r} - see "
|
||||||
|
"https://github.com/The-Compiler/qutebrowser/issues/"
|
||||||
|
"670".format(data))
|
||||||
|
atime = atime.lstrip('\0')
|
||||||
|
# This de-duplicates history entries; only the latest
|
||||||
|
# entry for each URL is kept. If you want to keep
|
||||||
|
# information about previous hits change the items in
|
||||||
|
# old_urls to be lists or change HistoryEntry to have a
|
||||||
|
# list of atimes.
|
||||||
|
self._history_dict[url] = HistoryEntry(atime, url)
|
||||||
|
self._history_dict.move_to_end(url)
|
||||||
|
|
||||||
def get_recent(self):
|
def get_recent(self):
|
||||||
"""Get the most recent history entries."""
|
"""Get the most recent history entries."""
|
||||||
old = self._lineparser.get_recent()
|
old = self._lineparser.get_recent()
|
||||||
|
@ -195,10 +195,20 @@ class NetworkManager(QNetworkAccessManager):
|
|||||||
errors = [SslError(e) for e in errors]
|
errors = [SslError(e) for e in errors]
|
||||||
ssl_strict = config.get('network', 'ssl-strict')
|
ssl_strict = config.get('network', 'ssl-strict')
|
||||||
if ssl_strict == 'ask':
|
if ssl_strict == 'ask':
|
||||||
host_tpl = urlutils.host_tuple(reply.url())
|
try:
|
||||||
if set(errors).issubset(self._accepted_ssl_errors[host_tpl]):
|
host_tpl = urlutils.host_tuple(reply.url())
|
||||||
|
except ValueError:
|
||||||
|
host_tpl = None
|
||||||
|
is_accepted = False
|
||||||
|
is_rejected = False
|
||||||
|
else:
|
||||||
|
is_accepted = set(errors).issubset(
|
||||||
|
self._accepted_ssl_errors[host_tpl])
|
||||||
|
is_rejected = set(errors).issubset(
|
||||||
|
self._rejected_ssl_errors[host_tpl])
|
||||||
|
if is_accepted:
|
||||||
reply.ignoreSslErrors()
|
reply.ignoreSslErrors()
|
||||||
elif set(errors).issubset(self._rejected_ssl_errors[host_tpl]):
|
elif is_rejected:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
err_string = '\n'.join('- ' + err.errorString() for err in
|
err_string = '\n'.join('- ' + err.errorString() for err in
|
||||||
@ -208,9 +218,11 @@ class NetworkManager(QNetworkAccessManager):
|
|||||||
owner=reply)
|
owner=reply)
|
||||||
if answer:
|
if answer:
|
||||||
reply.ignoreSslErrors()
|
reply.ignoreSslErrors()
|
||||||
self._accepted_ssl_errors[host_tpl] += errors
|
d = self._accepted_ssl_errors
|
||||||
else:
|
else:
|
||||||
self._rejected_ssl_errors[host_tpl] += errors
|
d = self._rejected_ssl_errors
|
||||||
|
if host_tpl is not None:
|
||||||
|
d[host_tpl] += errors
|
||||||
elif ssl_strict:
|
elif ssl_strict:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
|
@ -106,6 +106,7 @@ class WebView(QWebView):
|
|||||||
self.keep_icon = False
|
self.keep_icon = False
|
||||||
self.search_text = None
|
self.search_text = None
|
||||||
self.search_flags = 0
|
self.search_flags = 0
|
||||||
|
self.selection_enabled = False
|
||||||
self.init_neighborlist()
|
self.init_neighborlist()
|
||||||
cfg = objreg.get('config')
|
cfg = objreg.get('config')
|
||||||
cfg.changed.connect(self.init_neighborlist)
|
cfg.changed.connect(self.init_neighborlist)
|
||||||
@ -378,6 +379,8 @@ class WebView(QWebView):
|
|||||||
if url.isValid():
|
if url.isValid():
|
||||||
self.cur_url = url
|
self.cur_url = url
|
||||||
self.url_text_changed.emit(url.toDisplayString())
|
self.url_text_changed.emit(url.toDisplayString())
|
||||||
|
if not self.title():
|
||||||
|
self.titleChanged.emit(self.url().toDisplayString())
|
||||||
|
|
||||||
@pyqtSlot('QMouseEvent')
|
@pyqtSlot('QMouseEvent')
|
||||||
def on_mouse_event(self, evt):
|
def on_mouse_event(self, evt):
|
||||||
@ -396,7 +399,7 @@ class WebView(QWebView):
|
|||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def on_load_finished(self):
|
def on_load_finished(self):
|
||||||
"""Handle auto-insert-mode after loading finished.
|
"""Handle a finished page load.
|
||||||
|
|
||||||
We don't take loadFinished's ok argument here as it always seems to be
|
We don't take loadFinished's ok argument here as it always seems to be
|
||||||
true when the QWebPage has an ErrorPageExtension implemented.
|
true when the QWebPage has an ErrorPageExtension implemented.
|
||||||
@ -409,6 +412,12 @@ class WebView(QWebView):
|
|||||||
self._set_load_status(LoadStatus.warn)
|
self._set_load_status(LoadStatus.warn)
|
||||||
else:
|
else:
|
||||||
self._set_load_status(LoadStatus.error)
|
self._set_load_status(LoadStatus.error)
|
||||||
|
if not self.title():
|
||||||
|
self.titleChanged.emit(self.url().toDisplayString())
|
||||||
|
self._handle_auto_insert_mode(ok)
|
||||||
|
|
||||||
|
def _handle_auto_insert_mode(self, ok):
|
||||||
|
"""Handle auto-insert-mode after loading finished."""
|
||||||
if not config.get('input', 'auto-insert-mode'):
|
if not config.get('input', 'auto-insert-mode'):
|
||||||
return
|
return
|
||||||
mode_manager = objreg.get('mode-manager', scope='window',
|
mode_manager = objreg.get('mode-manager', scope='window',
|
||||||
@ -435,6 +444,18 @@ class WebView(QWebView):
|
|||||||
log.webview.debug("Ignoring focus because mode {} was "
|
log.webview.debug("Ignoring focus because mode {} was "
|
||||||
"entered.".format(mode))
|
"entered.".format(mode))
|
||||||
self.setFocusPolicy(Qt.NoFocus)
|
self.setFocusPolicy(Qt.NoFocus)
|
||||||
|
elif mode == usertypes.KeyMode.caret:
|
||||||
|
settings = self.settings()
|
||||||
|
settings.setAttribute(QWebSettings.CaretBrowsingEnabled, True)
|
||||||
|
self.selection_enabled = False
|
||||||
|
|
||||||
|
if self.isVisible():
|
||||||
|
# Sometimes the caret isn't immediately visible, but unfocusing
|
||||||
|
# and refocusing it fixes that.
|
||||||
|
self.clearFocus()
|
||||||
|
self.setFocus(Qt.OtherFocusReason)
|
||||||
|
self.page().currentFrame().evaluateJavaScript(
|
||||||
|
utils.read_file('javascript/position_caret.js'))
|
||||||
|
|
||||||
@pyqtSlot(usertypes.KeyMode)
|
@pyqtSlot(usertypes.KeyMode)
|
||||||
def on_mode_left(self, mode):
|
def on_mode_left(self, mode):
|
||||||
@ -443,6 +464,15 @@ class WebView(QWebView):
|
|||||||
usertypes.KeyMode.yesno):
|
usertypes.KeyMode.yesno):
|
||||||
log.webview.debug("Restoring focus policy because mode {} was "
|
log.webview.debug("Restoring focus policy because mode {} was "
|
||||||
"left.".format(mode))
|
"left.".format(mode))
|
||||||
|
elif mode == usertypes.KeyMode.caret:
|
||||||
|
settings = self.settings()
|
||||||
|
if settings.testAttribute(QWebSettings.CaretBrowsingEnabled):
|
||||||
|
if self.selection_enabled and self.hasSelection():
|
||||||
|
# Remove selection if it exists
|
||||||
|
self.triggerPageAction(QWebPage.MoveToNextChar)
|
||||||
|
settings.setAttribute(QWebSettings.CaretBrowsingEnabled, False)
|
||||||
|
self.selection_enabled = False
|
||||||
|
|
||||||
self.setFocusPolicy(Qt.WheelFocus)
|
self.setFocusPolicy(Qt.WheelFocus)
|
||||||
|
|
||||||
def search(self, text, flags):
|
def search(self, text, flags):
|
||||||
|
@ -61,7 +61,7 @@ class Command:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
AnnotationInfo = collections.namedtuple('AnnotationInfo',
|
AnnotationInfo = collections.namedtuple('AnnotationInfo',
|
||||||
['kwargs', 'type', 'flag'])
|
['kwargs', 'type', 'flag', 'hide'])
|
||||||
|
|
||||||
def __init__(self, *, handler, name, instance=None, maxsplit=None,
|
def __init__(self, *, handler, name, instance=None, maxsplit=None,
|
||||||
hide=False, completion=None, modes=None, not_modes=None,
|
hide=False, completion=None, modes=None, not_modes=None,
|
||||||
@ -304,7 +304,8 @@ class Command:
|
|||||||
self.flags_with_args += [short_flag, long_flag]
|
self.flags_with_args += [short_flag, long_flag]
|
||||||
else:
|
else:
|
||||||
args.append(name)
|
args.append(name)
|
||||||
self.pos_args.append((param.name, name))
|
if not annotation_info.hide:
|
||||||
|
self.pos_args.append((param.name, name))
|
||||||
return args
|
return args
|
||||||
|
|
||||||
def _parse_annotation(self, param):
|
def _parse_annotation(self, param):
|
||||||
@ -321,11 +322,11 @@ class Command:
|
|||||||
flag: The short name/flag if overridden.
|
flag: The short name/flag if overridden.
|
||||||
name: The long name if overridden.
|
name: The long name if overridden.
|
||||||
"""
|
"""
|
||||||
info = {'kwargs': {}, 'type': None, 'flag': None}
|
info = {'kwargs': {}, 'type': None, 'flag': None, 'hide': False}
|
||||||
if param.annotation is not inspect.Parameter.empty:
|
if param.annotation is not inspect.Parameter.empty:
|
||||||
log.commands.vdebug("Parsing annotation {}".format(
|
log.commands.vdebug("Parsing annotation {}".format(
|
||||||
param.annotation))
|
param.annotation))
|
||||||
for field in ('type', 'flag', 'name'):
|
for field in ('type', 'flag', 'name', 'hide'):
|
||||||
if field in param.annotation:
|
if field in param.annotation:
|
||||||
info[field] = param.annotation[field]
|
info[field] = param.annotation[field]
|
||||||
if 'nargs' in param.annotation:
|
if 'nargs' in param.annotation:
|
||||||
|
@ -109,7 +109,8 @@ class BaseCompletionModel(QStandardItemModel):
|
|||||||
qtutils.ensure_valid(index)
|
qtutils.ensure_valid(index)
|
||||||
if index.parent().isValid():
|
if index.parent().isValid():
|
||||||
# item
|
# item
|
||||||
return Qt.ItemIsEnabled | Qt.ItemIsSelectable
|
return (Qt.ItemIsEnabled | Qt.ItemIsSelectable |
|
||||||
|
Qt.ItemNeverHasChildren)
|
||||||
else:
|
else:
|
||||||
# category
|
# category
|
||||||
return Qt.NoItemFlags
|
return Qt.NoItemFlags
|
||||||
|
@ -33,12 +33,12 @@ import collections
|
|||||||
import collections.abc
|
import collections.abc
|
||||||
|
|
||||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QUrl, QSettings
|
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QUrl, QSettings
|
||||||
from PyQt5.QtWidgets import QMessageBox
|
|
||||||
|
|
||||||
from qutebrowser.config import configdata, configexc, textwrapper
|
from qutebrowser.config import configdata, configexc, textwrapper
|
||||||
from qutebrowser.config.parsers import ini, keyconf
|
from qutebrowser.config.parsers import ini, keyconf
|
||||||
from qutebrowser.commands import cmdexc, cmdutils
|
from qutebrowser.commands import cmdexc, cmdutils
|
||||||
from qutebrowser.utils import message, objreg, utils, standarddir, log, qtutils
|
from qutebrowser.utils import (message, objreg, utils, standarddir, log,
|
||||||
|
qtutils, error, usertypes)
|
||||||
from qutebrowser.utils.usertypes import Completion
|
from qutebrowser.utils.usertypes import Completion
|
||||||
|
|
||||||
|
|
||||||
@ -137,8 +137,8 @@ def _init_main_config(parent=None):
|
|||||||
Args:
|
Args:
|
||||||
parent: The parent to pass to ConfigManager.
|
parent: The parent to pass to ConfigManager.
|
||||||
"""
|
"""
|
||||||
|
args = objreg.get('args')
|
||||||
try:
|
try:
|
||||||
args = objreg.get('args')
|
|
||||||
config_obj = ConfigManager(standarddir.config(), 'qutebrowser.conf',
|
config_obj = ConfigManager(standarddir.config(), 'qutebrowser.conf',
|
||||||
args.relaxed_config, parent=parent)
|
args.relaxed_config, parent=parent)
|
||||||
except (configexc.Error, configparser.Error, UnicodeDecodeError) as e:
|
except (configexc.Error, configparser.Error, UnicodeDecodeError) as e:
|
||||||
@ -149,12 +149,11 @@ def _init_main_config(parent=None):
|
|||||||
e.section, e.option) # pylint: disable=no-member
|
e.section, e.option) # pylint: disable=no-member
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
errstr += "\n{}".format(e)
|
errstr += "\n"
|
||||||
msgbox = QMessageBox(QMessageBox.Critical,
|
error.handle_fatal_exc(e, args, "Error while reading config!",
|
||||||
"Error while reading config!", errstr)
|
pre_text=errstr)
|
||||||
msgbox.exec_()
|
|
||||||
# We didn't really initialize much so far, so we just quit hard.
|
# We didn't really initialize much so far, so we just quit hard.
|
||||||
sys.exit(1)
|
sys.exit(usertypes.Exit.err_config)
|
||||||
else:
|
else:
|
||||||
objreg.register('config', config_obj)
|
objreg.register('config', config_obj)
|
||||||
if standarddir.config() is not None:
|
if standarddir.config() is not None:
|
||||||
@ -178,8 +177,8 @@ def _init_key_config(parent):
|
|||||||
Args:
|
Args:
|
||||||
parent: The parent to use for the KeyConfigParser.
|
parent: The parent to use for the KeyConfigParser.
|
||||||
"""
|
"""
|
||||||
|
args = objreg.get('args')
|
||||||
try:
|
try:
|
||||||
args = objreg.get('args')
|
|
||||||
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)
|
||||||
@ -188,12 +187,10 @@ def _init_key_config(parent):
|
|||||||
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:
|
||||||
errstr += "In line {}: ".format(e.lineno)
|
errstr += "In line {}: ".format(e.lineno)
|
||||||
errstr += str(e)
|
error.handle_fatal_exc(e, args, "Error while reading key config!",
|
||||||
msgbox = QMessageBox(QMessageBox.Critical,
|
pre_text=errstr)
|
||||||
"Error while reading key config!", errstr)
|
|
||||||
msgbox.exec_()
|
|
||||||
# We didn't really initialize much so far, so we just quit hard.
|
# We didn't really initialize much so far, so we just quit hard.
|
||||||
sys.exit(1)
|
sys.exit(usertypes.Exit.err_key_config)
|
||||||
else:
|
else:
|
||||||
objreg.register('key-config', key_config)
|
objreg.register('key-config', key_config)
|
||||||
if standarddir.config() is not None:
|
if standarddir.config() is not None:
|
||||||
|
@ -280,6 +280,10 @@ def data(readonly=False):
|
|||||||
SettingValue(typ.String(none_ok=True), ''),
|
SettingValue(typ.String(none_ok=True), ''),
|
||||||
"Set the CSS media type."),
|
"Set the CSS media type."),
|
||||||
|
|
||||||
|
('smooth-scrolling',
|
||||||
|
SettingValue(typ.Bool(), 'false'),
|
||||||
|
"Whether to enable smooth scrolling for webpages."),
|
||||||
|
|
||||||
('remove-finished-downloads',
|
('remove-finished-downloads',
|
||||||
SettingValue(typ.Bool(), 'false'),
|
SettingValue(typ.Bool(), 'false'),
|
||||||
"Whether to remove finished downloads automatically."),
|
"Whether to remove finished downloads automatically."),
|
||||||
@ -522,6 +526,10 @@ def data(readonly=False):
|
|||||||
"* `{index}`: The index of this tab.\n"
|
"* `{index}`: The index of this tab.\n"
|
||||||
"* `{id}`: The internal tab ID of this tab."),
|
"* `{id}`: The internal tab ID of this tab."),
|
||||||
|
|
||||||
|
('mousewheel-tab-switching',
|
||||||
|
SettingValue(typ.Bool(), 'true'),
|
||||||
|
"Switch between tabs using the mouse wheel."),
|
||||||
|
|
||||||
readonly=readonly
|
readonly=readonly
|
||||||
)),
|
)),
|
||||||
|
|
||||||
@ -609,6 +617,18 @@ def data(readonly=False):
|
|||||||
'Qt plugins with a mimetype such as "application/x-qt-plugin" '
|
'Qt plugins with a mimetype such as "application/x-qt-plugin" '
|
||||||
"are not affected by this setting."),
|
"are not affected by this setting."),
|
||||||
|
|
||||||
|
('webgl',
|
||||||
|
SettingValue(typ.Bool(), 'true'),
|
||||||
|
"Enables or disables WebGL."),
|
||||||
|
|
||||||
|
('css-regions',
|
||||||
|
SettingValue(typ.Bool(), 'true'),
|
||||||
|
"Enable or disable support for CSS regions."),
|
||||||
|
|
||||||
|
('hyperlink-auditing',
|
||||||
|
SettingValue(typ.Bool(), 'false'),
|
||||||
|
"Enable or disable hyperlink auditing (<a ping>)."),
|
||||||
|
|
||||||
('geolocation',
|
('geolocation',
|
||||||
SettingValue(typ.BoolAsk(), 'ask'),
|
SettingValue(typ.BoolAsk(), 'ask'),
|
||||||
"Allow websites to request geolocations."),
|
"Allow websites to request geolocations."),
|
||||||
@ -844,6 +864,24 @@ def data(readonly=False):
|
|||||||
SettingValue(typ.QssColor(), '${statusbar.bg}'),
|
SettingValue(typ.QssColor(), '${statusbar.bg}'),
|
||||||
"Background color of the statusbar in command mode."),
|
"Background color of the statusbar in command mode."),
|
||||||
|
|
||||||
|
('statusbar.fg.caret',
|
||||||
|
SettingValue(typ.QssColor(), '${statusbar.fg}'),
|
||||||
|
"Foreground color of the statusbar in caret mode."),
|
||||||
|
|
||||||
|
('statusbar.bg.caret',
|
||||||
|
SettingValue(typ.QssColor(), 'purple'),
|
||||||
|
"Background color of the statusbar in caret mode."),
|
||||||
|
|
||||||
|
('statusbar.fg.caret-selection',
|
||||||
|
SettingValue(typ.QssColor(), '${statusbar.fg}'),
|
||||||
|
"Foreground color of the statusbar in caret mode with a "
|
||||||
|
"selection"),
|
||||||
|
|
||||||
|
('statusbar.bg.caret-selection',
|
||||||
|
SettingValue(typ.QssColor(), '#a12dff'),
|
||||||
|
"Background color of the statusbar in caret mode with a "
|
||||||
|
"selection"),
|
||||||
|
|
||||||
('statusbar.progress.bg',
|
('statusbar.progress.bg',
|
||||||
SettingValue(typ.QssColor(), 'white'),
|
SettingValue(typ.QssColor(), 'white'),
|
||||||
"Background color of the progress bar."),
|
"Background color of the progress bar."),
|
||||||
@ -1129,6 +1167,8 @@ KEY_SECTION_DESC = {
|
|||||||
" * `prompt-accept`: Confirm the entered value.\n"
|
" * `prompt-accept`: Confirm the entered value.\n"
|
||||||
" * `prompt-yes`: Answer yes to a yes/no question.\n"
|
" * `prompt-yes`: Answer yes to a yes/no question.\n"
|
||||||
" * `prompt-no`: Answer no to a yes/no question."),
|
" * `prompt-no`: Answer no to a yes/no question."),
|
||||||
|
'caret': (
|
||||||
|
""),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -1138,7 +1178,7 @@ KEY_DATA = collections.OrderedDict([
|
|||||||
])),
|
])),
|
||||||
|
|
||||||
('normal', collections.OrderedDict([
|
('normal', collections.OrderedDict([
|
||||||
('search ""', ['<Escape>']),
|
('search', ['<Escape>']),
|
||||||
('set-cmd-text -s :open', ['o']),
|
('set-cmd-text -s :open', ['o']),
|
||||||
('set-cmd-text :open {url}', ['go']),
|
('set-cmd-text :open {url}', ['go']),
|
||||||
('set-cmd-text -s :open -t', ['O']),
|
('set-cmd-text -s :open -t', ['O']),
|
||||||
@ -1181,19 +1221,20 @@ KEY_DATA = collections.OrderedDict([
|
|||||||
('hint links fill ":open -b {hint-url}"', ['.o']),
|
('hint links fill ":open -b {hint-url}"', ['.o']),
|
||||||
('hint links yank', [';y']),
|
('hint links yank', [';y']),
|
||||||
('hint links yank-primary', [';Y']),
|
('hint links yank-primary', [';Y']),
|
||||||
('hint links rapid', [';r']),
|
('hint --rapid links tab-bg', [';r']),
|
||||||
('hint links rapid-win', [';R']),
|
('hint --rapid links window', [';R']),
|
||||||
('hint links download', [';d']),
|
('hint links download', [';d']),
|
||||||
('scroll -50 0', ['h']),
|
('scroll left', ['h']),
|
||||||
('scroll 0 50', ['j']),
|
('scroll down', ['j']),
|
||||||
('scroll 0 -50', ['k']),
|
('scroll up', ['k']),
|
||||||
('scroll 50 0', ['l']),
|
('scroll right', ['l']),
|
||||||
('undo', ['u', '<Ctrl-Shift-T>']),
|
('undo', ['u', '<Ctrl-Shift-T>']),
|
||||||
('scroll-perc 0', ['gg']),
|
('scroll-perc 0', ['gg']),
|
||||||
('scroll-perc', ['G']),
|
('scroll-perc', ['G']),
|
||||||
('search-next', ['n']),
|
('search-next', ['n']),
|
||||||
('search-prev', ['N']),
|
('search-prev', ['N']),
|
||||||
('enter-mode insert', ['i']),
|
('enter-mode insert', ['i']),
|
||||||
|
('enter-mode caret', ['v']),
|
||||||
('yank', ['yy']),
|
('yank', ['yy']),
|
||||||
('yank -s', ['yY']),
|
('yank -s', ['yY']),
|
||||||
('yank -t', ['yt']),
|
('yank -t', ['yt']),
|
||||||
@ -1294,6 +1335,33 @@ KEY_DATA = collections.OrderedDict([
|
|||||||
('rl-delete-char', ['<Ctrl-?>']),
|
('rl-delete-char', ['<Ctrl-?>']),
|
||||||
('rl-backward-delete-char', ['<Ctrl-H>']),
|
('rl-backward-delete-char', ['<Ctrl-H>']),
|
||||||
])),
|
])),
|
||||||
|
|
||||||
|
('caret', collections.OrderedDict([
|
||||||
|
('toggle-selection', ['v', '<Space>']),
|
||||||
|
('drop-selection', ['<Ctrl-Space>']),
|
||||||
|
('enter-mode normal', ['c']),
|
||||||
|
('move-to-next-line', ['j']),
|
||||||
|
('move-to-prev-line', ['k']),
|
||||||
|
('move-to-next-char', ['l']),
|
||||||
|
('move-to-prev-char', ['h']),
|
||||||
|
('move-to-end-of-word', ['e']),
|
||||||
|
('move-to-next-word', ['w']),
|
||||||
|
('move-to-prev-word', ['b']),
|
||||||
|
('move-to-start-of-next-block', [']']),
|
||||||
|
('move-to-start-of-prev-block', ['[']),
|
||||||
|
('move-to-end-of-next-block', ['}']),
|
||||||
|
('move-to-end-of-prev-block', ['{']),
|
||||||
|
('move-to-start-of-line', ['0']),
|
||||||
|
('move-to-end-of-line', ['$']),
|
||||||
|
('move-to-start-of-document', ['gg']),
|
||||||
|
('move-to-end-of-document', ['G']),
|
||||||
|
('yank-selected -p', ['Y']),
|
||||||
|
('yank-selected', ['y', '<Return>', '<Ctrl-J>']),
|
||||||
|
('scroll left', ['H']),
|
||||||
|
('scroll down', ['J']),
|
||||||
|
('scroll up', ['K']),
|
||||||
|
('scroll right', ['L']),
|
||||||
|
])),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
@ -1301,10 +1369,22 @@ KEY_DATA = collections.OrderedDict([
|
|||||||
|
|
||||||
CHANGED_KEY_COMMANDS = [
|
CHANGED_KEY_COMMANDS = [
|
||||||
(re.compile(r'^open -([twb]) about:blank$'), r'open -\1'),
|
(re.compile(r'^open -([twb]) about:blank$'), r'open -\1'),
|
||||||
|
|
||||||
(re.compile(r'^download-page$'), r'download'),
|
(re.compile(r'^download-page$'), r'download'),
|
||||||
(re.compile(r'^cancel-download$'), r'download-cancel'),
|
(re.compile(r'^cancel-download$'), r'download-cancel'),
|
||||||
|
|
||||||
(re.compile(r'^search ""$'), r'search'),
|
(re.compile(r'^search ""$'), r'search'),
|
||||||
(re.compile(r"^search ''$"), r'search'),
|
(re.compile(r"^search ''$"), r'search'),
|
||||||
|
|
||||||
(re.compile(r"""^set-cmd-text ['"](.*) ['"]$"""), r'set-cmd-text -s \1'),
|
(re.compile(r"""^set-cmd-text ['"](.*) ['"]$"""), r'set-cmd-text -s \1'),
|
||||||
(re.compile(r"""^set-cmd-text ['"](.*)['"]$"""), r'set-cmd-text \1'),
|
(re.compile(r"""^set-cmd-text ['"](.*)['"]$"""), r'set-cmd-text \1'),
|
||||||
|
|
||||||
|
(re.compile(r"^hint links rapid$"), r'hint --rapid links tab-bg'),
|
||||||
|
(re.compile(r"^hint links rapid-win$"), r'hint --rapid links window'),
|
||||||
|
|
||||||
|
(re.compile(r'^scroll -50 0$'), r'scroll left'),
|
||||||
|
(re.compile(r'^scroll 0 50$'), r'scroll down'),
|
||||||
|
(re.compile(r'^scroll 0 -50$'), r'scroll up'),
|
||||||
|
(re.compile(r'^scroll 50 0$'), r'scroll right'),
|
||||||
|
(re.compile(r'^scroll ([-\d]+ [-\d]+)$'), r'scroll-px \1'),
|
||||||
]
|
]
|
||||||
|
@ -1436,15 +1436,17 @@ class NewInstanceOpenTarget(BaseType):
|
|||||||
"""How to open links in an existing instance if a new one is launched."""
|
"""How to open links in an existing instance if a new one is launched."""
|
||||||
|
|
||||||
valid_values = ValidValues(('tab', "Open a new tab in the existing "
|
valid_values = ValidValues(('tab', "Open a new tab in the existing "
|
||||||
"window and activate it."),
|
"window and activate the window."),
|
||||||
('tab-bg', "Open a new background tab in the "
|
('tab-bg', "Open a new background tab in the "
|
||||||
"existing window and activate it."),
|
"existing window and activate the "
|
||||||
|
"window."),
|
||||||
('tab-silent', "Open a new tab in the existing "
|
('tab-silent', "Open a new tab in the existing "
|
||||||
"window without activating "
|
"window without activating "
|
||||||
"it."),
|
"the window."),
|
||||||
('tab-bg-silent', "Open a new background tab "
|
('tab-bg-silent', "Open a new background tab "
|
||||||
"in the existing window "
|
"in the existing window "
|
||||||
"without activating it."),
|
"without activating the "
|
||||||
|
"window."),
|
||||||
('window', "Open in a new window."))
|
('window', "Open in a new window."))
|
||||||
|
|
||||||
|
|
||||||
|
@ -47,11 +47,15 @@ class ReadConfigParser(configparser.ConfigParser):
|
|||||||
self.optionxform = lambda opt: opt # be case-insensitive
|
self.optionxform = lambda opt: opt # be case-insensitive
|
||||||
self._configdir = configdir
|
self._configdir = configdir
|
||||||
self._fname = fname
|
self._fname = fname
|
||||||
|
if self._configdir is None:
|
||||||
|
self._configfile = None
|
||||||
|
return
|
||||||
self._configfile = os.path.join(self._configdir, fname)
|
self._configfile = os.path.join(self._configdir, fname)
|
||||||
if not os.path.isfile(self._configfile):
|
if not os.path.isfile(self._configfile):
|
||||||
return
|
return
|
||||||
log.init.debug("Reading config from {}".format(self._configfile))
|
log.init.debug("Reading config from {}".format(self._configfile))
|
||||||
self.read(self._configfile, encoding='utf-8')
|
if self._configfile is not None:
|
||||||
|
self.read(self._configfile, encoding='utf-8')
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return utils.get_repr(self, constructor=True,
|
return utils.get_repr(self, constructor=True,
|
||||||
@ -64,6 +68,8 @@ class ReadWriteConfigParser(ReadConfigParser):
|
|||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
"""Save the config file."""
|
"""Save the config file."""
|
||||||
|
if self._configdir is None:
|
||||||
|
return
|
||||||
if not os.path.exists(self._configdir):
|
if not os.path.exists(self._configdir):
|
||||||
os.makedirs(self._configdir, 0o755)
|
os.makedirs(self._configdir, 0o755)
|
||||||
log.destroy.debug("Saving config to {}".format(self._configfile))
|
log.destroy.debug("Saving config to {}".format(self._configfile))
|
||||||
|
@ -237,20 +237,30 @@ class KeyConfigParser(QObject):
|
|||||||
only_new: If set, only keybindings which are completely unused
|
only_new: If set, only keybindings which are completely unused
|
||||||
(same command/key not bound) are added.
|
(same command/key not bound) are added.
|
||||||
"""
|
"""
|
||||||
|
# {'sectname': {'keychain1': 'command', 'keychain2': 'command'}, ...}
|
||||||
|
bindings_to_add = collections.OrderedDict()
|
||||||
|
|
||||||
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)
|
||||||
|
bindings_to_add[sectname] = collections.OrderedDict()
|
||||||
|
for command, keychains in sect.items():
|
||||||
|
for e in keychains:
|
||||||
|
if not only_new or self._is_new(sectname, command, e):
|
||||||
|
assert e not in bindings_to_add[sectname]
|
||||||
|
bindings_to_add[sectname][e] = command
|
||||||
|
|
||||||
|
for sectname, sect in bindings_to_add.items():
|
||||||
if not sect:
|
if not sect:
|
||||||
if not only_new:
|
if not only_new:
|
||||||
self.keybindings[sectname] = collections.OrderedDict()
|
self.keybindings[sectname] = collections.OrderedDict()
|
||||||
self._mark_config_dirty()
|
|
||||||
else:
|
else:
|
||||||
for command, keychains in sect.items():
|
for keychain, command in sect.items():
|
||||||
for e in keychains:
|
self._add_binding(sectname, keychain, command)
|
||||||
if not only_new or self._is_new(sectname, command, e):
|
|
||||||
self._add_binding(sectname, e, command)
|
|
||||||
self._mark_config_dirty()
|
|
||||||
self.changed.emit(sectname)
|
self.changed.emit(sectname)
|
||||||
|
|
||||||
|
if bindings_to_add:
|
||||||
|
self._mark_config_dirty()
|
||||||
|
|
||||||
def _is_new(self, sectname, command, keychain):
|
def _is_new(self, sectname, command, keychain):
|
||||||
"""Check if a given binding is new.
|
"""Check if a given binding is new.
|
||||||
|
|
||||||
|
@ -84,8 +84,8 @@ class Base:
|
|||||||
qws: The QWebSettings instance to use, or None to use the global
|
qws: The QWebSettings instance to use, or None to use the global
|
||||||
instance.
|
instance.
|
||||||
"""
|
"""
|
||||||
log.config.vdebug("Restoring default {!r}.".format(self._default))
|
|
||||||
if self._default is not UNSET:
|
if self._default is not UNSET:
|
||||||
|
log.config.vdebug("Restoring default {!r}.".format(self._default))
|
||||||
self._set(self._default, qws=qws)
|
self._set(self._default, qws=qws)
|
||||||
|
|
||||||
def get(self, qws=None):
|
def get(self, qws=None):
|
||||||
@ -254,6 +254,12 @@ MAPPINGS = {
|
|||||||
# Attribute(QWebSettings.JavaEnabled),
|
# Attribute(QWebSettings.JavaEnabled),
|
||||||
'allow-plugins':
|
'allow-plugins':
|
||||||
Attribute(QWebSettings.PluginsEnabled),
|
Attribute(QWebSettings.PluginsEnabled),
|
||||||
|
'webgl':
|
||||||
|
Attribute(QWebSettings.WebGLEnabled),
|
||||||
|
'css-regions':
|
||||||
|
Attribute(QWebSettings.CSSRegionsEnabled),
|
||||||
|
'hyperlink-auditing':
|
||||||
|
Attribute(QWebSettings.HyperlinkAuditingEnabled),
|
||||||
'local-content-can-access-remote-urls':
|
'local-content-can-access-remote-urls':
|
||||||
Attribute(QWebSettings.LocalContentCanAccessRemoteUrls),
|
Attribute(QWebSettings.LocalContentCanAccessRemoteUrls),
|
||||||
'local-content-can-access-file-urls':
|
'local-content-can-access-file-urls':
|
||||||
@ -322,6 +328,8 @@ MAPPINGS = {
|
|||||||
'css-media-type':
|
'css-media-type':
|
||||||
NullStringSetter(getter=QWebSettings.cssMediaType,
|
NullStringSetter(getter=QWebSettings.cssMediaType,
|
||||||
setter=QWebSettings.setCSSMediaType),
|
setter=QWebSettings.setCSSMediaType),
|
||||||
|
'smooth-scrolling':
|
||||||
|
Attribute(QWebSettings.ScrollAnimatorEnabled),
|
||||||
#'accelerated-compositing':
|
#'accelerated-compositing':
|
||||||
# Attribute(QWebSettings.AcceleratedCompositingEnabled),
|
# Attribute(QWebSettings.AcceleratedCompositingEnabled),
|
||||||
#'tiled-backing-store':
|
#'tiled-backing-store':
|
||||||
@ -369,16 +377,20 @@ MAPPINGS = {
|
|||||||
|
|
||||||
def init():
|
def init():
|
||||||
"""Initialize the global QWebSettings."""
|
"""Initialize the global QWebSettings."""
|
||||||
if config.get('general', 'private-browsing'):
|
cache_path = standarddir.cache()
|
||||||
|
data_path = standarddir.data()
|
||||||
|
if config.get('general', 'private-browsing') or cache_path is None:
|
||||||
QWebSettings.setIconDatabasePath('')
|
QWebSettings.setIconDatabasePath('')
|
||||||
else:
|
else:
|
||||||
QWebSettings.setIconDatabasePath(standarddir.cache())
|
QWebSettings.setIconDatabasePath(cache_path)
|
||||||
QWebSettings.setOfflineWebApplicationCachePath(
|
if cache_path is not None:
|
||||||
os.path.join(standarddir.cache(), 'application-cache'))
|
QWebSettings.setOfflineWebApplicationCachePath(
|
||||||
QWebSettings.globalSettings().setLocalStoragePath(
|
os.path.join(cache_path, 'application-cache'))
|
||||||
os.path.join(standarddir.data(), 'local-storage'))
|
if data_path is not None:
|
||||||
QWebSettings.setOfflineStoragePath(
|
QWebSettings.globalSettings().setLocalStoragePath(
|
||||||
os.path.join(standarddir.data(), 'offline-storage'))
|
os.path.join(data_path, 'local-storage'))
|
||||||
|
QWebSettings.setOfflineStoragePath(
|
||||||
|
os.path.join(data_path, 'offline-storage'))
|
||||||
|
|
||||||
for sectname, section in MAPPINGS.items():
|
for sectname, section in MAPPINGS.items():
|
||||||
for optname, mapping in section.items():
|
for optname, mapping in section.items():
|
||||||
@ -394,11 +406,12 @@ def init():
|
|||||||
|
|
||||||
def update_settings(section, option):
|
def update_settings(section, option):
|
||||||
"""Update global settings when qwebsettings changed."""
|
"""Update global settings when qwebsettings changed."""
|
||||||
|
cache_path = standarddir.cache()
|
||||||
if (section, option) == ('general', 'private-browsing'):
|
if (section, option) == ('general', 'private-browsing'):
|
||||||
if config.get('general', 'private-browsing'):
|
if config.get('general', 'private-browsing') or cache_path is None:
|
||||||
QWebSettings.setIconDatabasePath('')
|
QWebSettings.setIconDatabasePath('')
|
||||||
else:
|
else:
|
||||||
QWebSettings.setIconDatabasePath(standarddir.cache())
|
QWebSettings.setIconDatabasePath(cache_path)
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
mapping = MAPPINGS[section][option]
|
mapping = MAPPINGS[section][option]
|
||||||
|
110
qutebrowser/javascript/position_caret.js
Normal file
110
qutebrowser/javascript/position_caret.js
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 Artur Shaik <ashaihullin@gmail.com>
|
||||||
|
* Copyright 2015 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable max-len */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Snippet to position caret at top of the page when caret mode is enabled.
|
||||||
|
* Some code was borrowed from:
|
||||||
|
*
|
||||||
|
* https://github.com/1995eaton/chromium-vim/blob/master/content_scripts/dom.js
|
||||||
|
* https://github.com/1995eaton/chromium-vim/blob/master/content_scripts/visual.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint-enable max-len */
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
function isElementInViewport(node) {
|
||||||
|
var i;
|
||||||
|
var boundingRect = (node.getClientRects()[0] ||
|
||||||
|
node.getBoundingClientRect());
|
||||||
|
if (boundingRect.width <= 1 && boundingRect.height <= 1) {
|
||||||
|
var rects = node.getClientRects();
|
||||||
|
for (i = 0; i < rects.length; i++) {
|
||||||
|
if (rects[i].width > rects[0].height &&
|
||||||
|
rects[i].height > rects[0].height) {
|
||||||
|
boundingRect = rects[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (boundingRect === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (boundingRect.top > innerHeight || boundingRect.left > innerWidth) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (boundingRect.width <= 1 || boundingRect.height <= 1) {
|
||||||
|
var children = node.children;
|
||||||
|
var visibleChildNode = false;
|
||||||
|
var l = children.length;
|
||||||
|
for (i = 0; i < l; ++i) {
|
||||||
|
boundingRect = (children[i].getClientRects()[0] ||
|
||||||
|
children[i].getBoundingClientRect());
|
||||||
|
if (boundingRect.width > 1 && boundingRect.height > 1) {
|
||||||
|
visibleChildNode = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (visibleChildNode === false) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (boundingRect.top + boundingRect.height < 10 ||
|
||||||
|
boundingRect.left + boundingRect.width < -10) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
var computedStyle = window.getComputedStyle(node, null);
|
||||||
|
if (computedStyle.visibility !== 'visible' ||
|
||||||
|
computedStyle.display === 'none' ||
|
||||||
|
node.hasAttribute('disabled') ||
|
||||||
|
parseInt(computedStyle.width, 10) === 0 ||
|
||||||
|
parseInt(computedStyle.height, 10) === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return boundingRect.top >= -20;
|
||||||
|
}
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
var walker = document.createTreeWalker(document.body, 4, null);
|
||||||
|
var node;
|
||||||
|
var textNodes = [];
|
||||||
|
var el;
|
||||||
|
while ((node = walker.nextNode())) {
|
||||||
|
if (node.nodeType === 3 && node.data.trim() !== '') {
|
||||||
|
textNodes.push(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (var i = 0; i < textNodes.length; i++) {
|
||||||
|
var element = textNodes[i].parentElement;
|
||||||
|
if (isElementInViewport(element.parentElement)) {
|
||||||
|
el = element;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (el !== undefined) {
|
||||||
|
var range = document.createRange();
|
||||||
|
range.setStart(el, 0);
|
||||||
|
range.setEnd(el, 0);
|
||||||
|
var sel = window.getSelection();
|
||||||
|
sel.removeAllRanges();
|
||||||
|
sel.addRange(range);
|
||||||
|
}
|
||||||
|
})();
|
@ -137,6 +137,9 @@ class BaseKeyParser(QObject):
|
|||||||
(countstr, cmd_input) = re.match(r'^(\d*)(.*)',
|
(countstr, cmd_input) = re.match(r'^(\d*)(.*)',
|
||||||
self._keystring).groups()
|
self._keystring).groups()
|
||||||
count = int(countstr) if countstr else None
|
count = int(countstr) if countstr else None
|
||||||
|
if count == 0 and not cmd_input:
|
||||||
|
cmd_input = self._keystring
|
||||||
|
count = None
|
||||||
else:
|
else:
|
||||||
cmd_input = self._keystring
|
cmd_input = self._keystring
|
||||||
count = None
|
count = None
|
||||||
|
@ -78,6 +78,7 @@ def init(win_id, parent):
|
|||||||
KM.prompt: keyparser.PassthroughKeyParser(win_id, 'prompt', modeman,
|
KM.prompt: keyparser.PassthroughKeyParser(win_id, 'prompt', modeman,
|
||||||
warn=False),
|
warn=False),
|
||||||
KM.yesno: modeparsers.PromptKeyParser(win_id, modeman),
|
KM.yesno: modeparsers.PromptKeyParser(win_id, modeman),
|
||||||
|
KM.caret: modeparsers.CaretKeyParser(win_id, modeman),
|
||||||
}
|
}
|
||||||
objreg.register('keyparsers', keyparsers, scope='window', window=win_id)
|
objreg.register('keyparsers', keyparsers, scope='window', window=win_id)
|
||||||
modeman.destroyed.connect(
|
modeman.destroyed.connect(
|
||||||
@ -92,6 +93,7 @@ def init(win_id, parent):
|
|||||||
passthrough=True)
|
passthrough=True)
|
||||||
modeman.register(KM.prompt, keyparsers[KM.prompt].handle, passthrough=True)
|
modeman.register(KM.prompt, keyparsers[KM.prompt].handle, passthrough=True)
|
||||||
modeman.register(KM.yesno, keyparsers[KM.yesno].handle)
|
modeman.register(KM.yesno, keyparsers[KM.yesno].handle)
|
||||||
|
modeman.register(KM.caret, keyparsers[KM.caret].handle, passthrough=True)
|
||||||
return modeman
|
return modeman
|
||||||
|
|
||||||
|
|
||||||
|
@ -218,3 +218,13 @@ class HintKeyParser(keyparser.CommandKeyParser):
|
|||||||
hintmanager = objreg.get('hintmanager', scope='tab',
|
hintmanager = objreg.get('hintmanager', scope='tab',
|
||||||
window=self._win_id, tab='current')
|
window=self._win_id, tab='current')
|
||||||
hintmanager.handle_partial_key(keystr)
|
hintmanager.handle_partial_key(keystr)
|
||||||
|
|
||||||
|
|
||||||
|
class CaretKeyParser(keyparser.CommandKeyParser):
|
||||||
|
|
||||||
|
"""KeyParser for caret mode."""
|
||||||
|
|
||||||
|
def __init__(self, win_id, parent=None):
|
||||||
|
super().__init__(win_id, parent, supports_count=True,
|
||||||
|
supports_chains=True)
|
||||||
|
self.read_config('caret')
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
import binascii
|
import binascii
|
||||||
import base64
|
import base64
|
||||||
import itertools
|
import itertools
|
||||||
|
import functools
|
||||||
|
|
||||||
from PyQt5.QtCore import pyqtSlot, QRect, QPoint, QTimer, Qt
|
from PyQt5.QtCore import pyqtSlot, QRect, QPoint, QTimer, Qt
|
||||||
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication
|
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication
|
||||||
@ -33,7 +34,7 @@ from qutebrowser.mainwindow import tabbedbrowser
|
|||||||
from qutebrowser.mainwindow.statusbar import bar
|
from qutebrowser.mainwindow.statusbar import bar
|
||||||
from qutebrowser.completion import completionwidget
|
from qutebrowser.completion import completionwidget
|
||||||
from qutebrowser.keyinput import modeman
|
from qutebrowser.keyinput import modeman
|
||||||
from qutebrowser.browser import hints, downloads, downloadview
|
from qutebrowser.browser import hints, downloads, downloadview, commands
|
||||||
|
|
||||||
|
|
||||||
win_id_gen = itertools.count(0)
|
win_id_gen = itertools.count(0)
|
||||||
@ -89,8 +90,8 @@ class MainWindow(QWidget):
|
|||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
status: The StatusBar widget.
|
status: The StatusBar widget.
|
||||||
|
tabbed_browser: The TabbedBrowser widget.
|
||||||
_downloadview: The DownloadView widget.
|
_downloadview: The DownloadView widget.
|
||||||
_tabbed_browser: The TabbedBrowser widget.
|
|
||||||
_vbox: The main QVBoxLayout.
|
_vbox: The main QVBoxLayout.
|
||||||
_commandrunner: The main CommandRunner instance.
|
_commandrunner: The main CommandRunner instance.
|
||||||
"""
|
"""
|
||||||
@ -138,9 +139,16 @@ class MainWindow(QWidget):
|
|||||||
|
|
||||||
self._downloadview = downloadview.DownloadView(self.win_id)
|
self._downloadview = downloadview.DownloadView(self.win_id)
|
||||||
|
|
||||||
self._tabbed_browser = tabbedbrowser.TabbedBrowser(self.win_id)
|
self.tabbed_browser = tabbedbrowser.TabbedBrowser(self.win_id)
|
||||||
objreg.register('tabbed-browser', self._tabbed_browser, scope='window',
|
objreg.register('tabbed-browser', self.tabbed_browser, scope='window',
|
||||||
window=self.win_id)
|
window=self.win_id)
|
||||||
|
dispatcher = commands.CommandDispatcher(self.win_id,
|
||||||
|
self.tabbed_browser)
|
||||||
|
objreg.register('command-dispatcher', dispatcher, scope='window',
|
||||||
|
window=self.win_id)
|
||||||
|
self.tabbed_browser.destroyed.connect(
|
||||||
|
functools.partial(objreg.delete, 'command-dispatcher',
|
||||||
|
scope='window', window=self.win_id))
|
||||||
|
|
||||||
# We need to set an explicit parent for StatusBar because it does some
|
# We need to set an explicit parent for StatusBar because it does some
|
||||||
# show/hide magic immediately which would mean it'd show up as a
|
# show/hide magic immediately which would mean it'd show up as a
|
||||||
@ -185,15 +193,15 @@ class MainWindow(QWidget):
|
|||||||
|
|
||||||
def _add_widgets(self):
|
def _add_widgets(self):
|
||||||
"""Add or readd all widgets to the VBox."""
|
"""Add or readd all widgets to the VBox."""
|
||||||
self._vbox.removeWidget(self._tabbed_browser)
|
self._vbox.removeWidget(self.tabbed_browser)
|
||||||
self._vbox.removeWidget(self._downloadview)
|
self._vbox.removeWidget(self._downloadview)
|
||||||
self._vbox.removeWidget(self.status)
|
self._vbox.removeWidget(self.status)
|
||||||
position = config.get('ui', 'downloads-position')
|
position = config.get('ui', 'downloads-position')
|
||||||
if position == 'north':
|
if position == 'north':
|
||||||
self._vbox.addWidget(self._downloadview)
|
self._vbox.addWidget(self._downloadview)
|
||||||
self._vbox.addWidget(self._tabbed_browser)
|
self._vbox.addWidget(self.tabbed_browser)
|
||||||
elif position == 'south':
|
elif position == 'south':
|
||||||
self._vbox.addWidget(self._tabbed_browser)
|
self._vbox.addWidget(self.tabbed_browser)
|
||||||
self._vbox.addWidget(self._downloadview)
|
self._vbox.addWidget(self._downloadview)
|
||||||
else:
|
else:
|
||||||
raise ValueError("Invalid position {}!".format(position))
|
raise ValueError("Invalid position {}!".format(position))
|
||||||
@ -260,7 +268,7 @@ class MainWindow(QWidget):
|
|||||||
prompter = self._get_object('prompter')
|
prompter = self._get_object('prompter')
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
self._tabbed_browser.close_window.connect(self.close)
|
self.tabbed_browser.close_window.connect(self.close)
|
||||||
mode_manager.entered.connect(hints.on_mode_entered)
|
mode_manager.entered.connect(hints.on_mode_entered)
|
||||||
|
|
||||||
# status bar
|
# status bar
|
||||||
@ -381,12 +389,12 @@ class MainWindow(QWidget):
|
|||||||
super().resizeEvent(e)
|
super().resizeEvent(e)
|
||||||
self.resize_completion()
|
self.resize_completion()
|
||||||
self._downloadview.updateGeometry()
|
self._downloadview.updateGeometry()
|
||||||
self._tabbed_browser.tabBar().refresh()
|
self.tabbed_browser.tabBar().refresh()
|
||||||
|
|
||||||
def closeEvent(self, e):
|
def closeEvent(self, e):
|
||||||
"""Override closeEvent to display a confirmation if needed."""
|
"""Override closeEvent to display a confirmation if needed."""
|
||||||
confirm_quit = config.get('ui', 'confirm-quit')
|
confirm_quit = config.get('ui', 'confirm-quit')
|
||||||
tab_count = self._tabbed_browser.count()
|
tab_count = self.tabbed_browser.count()
|
||||||
download_manager = objreg.get('download-manager', scope='window',
|
download_manager = objreg.get('download-manager', scope='window',
|
||||||
window=self.win_id)
|
window=self.win_id)
|
||||||
download_count = download_manager.rowCount()
|
download_count = download_manager.rowCount()
|
||||||
@ -416,8 +424,7 @@ class MainWindow(QWidget):
|
|||||||
e.ignore()
|
e.ignore()
|
||||||
return
|
return
|
||||||
e.accept()
|
e.accept()
|
||||||
if len(objreg.window_registry) == 1:
|
objreg.get('session-manager').save_last_window_session()
|
||||||
objreg.get('session-manager').save_last_window_session()
|
|
||||||
self._save_geometry()
|
self._save_geometry()
|
||||||
log.destroy.debug("Closing window {}".format(self.win_id))
|
log.destroy.debug("Closing window {}".format(self.win_id))
|
||||||
self._tabbed_browser.shutdown()
|
self.tabbed_browser.shutdown()
|
||||||
|
@ -36,6 +36,7 @@ from qutebrowser.mainwindow.statusbar import text as textwidget
|
|||||||
PreviousWidget = usertypes.enum('PreviousWidget', ['none', 'prompt',
|
PreviousWidget = usertypes.enum('PreviousWidget', ['none', 'prompt',
|
||||||
'command'])
|
'command'])
|
||||||
Severity = usertypes.enum('Severity', ['normal', 'warning', 'error'])
|
Severity = usertypes.enum('Severity', ['normal', 'warning', 'error'])
|
||||||
|
CaretMode = usertypes.enum('CaretMode', ['off', 'on', 'selection'])
|
||||||
|
|
||||||
|
|
||||||
class StatusBar(QWidget):
|
class StatusBar(QWidget):
|
||||||
@ -79,8 +80,14 @@ class StatusBar(QWidget):
|
|||||||
|
|
||||||
_command_active: If we're currently in command mode.
|
_command_active: If we're currently in command mode.
|
||||||
|
|
||||||
For some reason we need to have this as class attribute
|
For some reason we need to have this as class attribute
|
||||||
so pyqtProperty works correctly.
|
so pyqtProperty works correctly.
|
||||||
|
|
||||||
|
_caret_mode: The current caret mode (off/on/selection).
|
||||||
|
|
||||||
|
For some reason we need to have this as class attribute
|
||||||
|
so pyqtProperty works correctly.
|
||||||
|
|
||||||
Signals:
|
Signals:
|
||||||
resized: Emitted when the statusbar has resized, so the completion
|
resized: Emitted when the statusbar has resized, so the completion
|
||||||
widget can adjust its size to it.
|
widget can adjust its size to it.
|
||||||
@ -96,6 +103,7 @@ class StatusBar(QWidget):
|
|||||||
_prompt_active = False
|
_prompt_active = False
|
||||||
_insert_active = False
|
_insert_active = False
|
||||||
_command_active = False
|
_command_active = False
|
||||||
|
_caret_mode = CaretMode.off
|
||||||
|
|
||||||
STYLESHEET = """
|
STYLESHEET = """
|
||||||
QWidget#StatusBar {
|
QWidget#StatusBar {
|
||||||
@ -110,6 +118,26 @@ class StatusBar(QWidget):
|
|||||||
{{ color['statusbar.fg'] }}
|
{{ color['statusbar.fg'] }}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QWidget#StatusBar[caret_mode="on"] QLabel {
|
||||||
|
{{ color['statusbar.fg.caret'] }}
|
||||||
|
}
|
||||||
|
|
||||||
|
QWidget#StatusBar[caret_mode="on"] {
|
||||||
|
{{ color['statusbar.bg.caret'] }}
|
||||||
|
}
|
||||||
|
|
||||||
|
QWidget#StatusBar[caret_mode="selection"] QLabel {
|
||||||
|
{{ color['statusbar.fg.caret-selection'] }}
|
||||||
|
}
|
||||||
|
|
||||||
|
QWidget#StatusBar[caret_mode="selection"] {
|
||||||
|
{{ color['statusbar.bg.caret-selection'] }}
|
||||||
|
}
|
||||||
|
|
||||||
|
QWidget#StatusBar[prompt_active="true"] {
|
||||||
|
{{ color['statusbar.bg.prompt'] }}
|
||||||
|
}
|
||||||
|
|
||||||
QWidget#StatusBar[severity="error"] {
|
QWidget#StatusBar[severity="error"] {
|
||||||
{{ color['statusbar.bg.error'] }}
|
{{ color['statusbar.bg.error'] }}
|
||||||
}
|
}
|
||||||
@ -313,14 +341,37 @@ class StatusBar(QWidget):
|
|||||||
"""Getter for self.insert_active, so it can be used as Qt property."""
|
"""Getter for self.insert_active, so it can be used as Qt property."""
|
||||||
return self._insert_active
|
return self._insert_active
|
||||||
|
|
||||||
def _set_insert_active(self, val):
|
@pyqtProperty(str)
|
||||||
"""Setter for self.insert_active.
|
def caret_mode(self):
|
||||||
|
"""Getter for self._caret_mode, so it can be used as Qt property."""
|
||||||
|
return self._caret_mode.name
|
||||||
|
|
||||||
|
def set_mode_active(self, mode, val):
|
||||||
|
"""Setter for self.{insert,command,caret}_active.
|
||||||
|
|
||||||
Re-set the stylesheet after setting the value, so everything gets
|
Re-set the stylesheet after setting the value, so everything gets
|
||||||
updated by Qt properly.
|
updated by Qt properly.
|
||||||
"""
|
"""
|
||||||
log.statusbar.debug("Setting insert_active to {}".format(val))
|
if mode == usertypes.KeyMode.insert:
|
||||||
self._insert_active = val
|
log.statusbar.debug("Setting insert_active to {}".format(val))
|
||||||
|
self._insert_active = val
|
||||||
|
if mode == usertypes.KeyMode.command:
|
||||||
|
log.statusbar.debug("Setting command_active to {}".format(val))
|
||||||
|
self._command_active = val
|
||||||
|
elif mode == usertypes.KeyMode.caret:
|
||||||
|
webview = objreg.get('tabbed-browser', scope='window',
|
||||||
|
window=self._win_id).currentWidget()
|
||||||
|
log.statusbar.debug("Setting caret_mode - val {}, selection "
|
||||||
|
"{}".format(val, webview.selection_enabled))
|
||||||
|
if val:
|
||||||
|
if webview.selection_enabled:
|
||||||
|
self._set_mode_text("{} selection".format(mode.name))
|
||||||
|
self._caret_mode = CaretMode.selection
|
||||||
|
else:
|
||||||
|
self._set_mode_text(mode.name)
|
||||||
|
self._caret_mode = CaretMode.on
|
||||||
|
else:
|
||||||
|
self._caret_mode = CaretMode.off
|
||||||
self.setStyleSheet(style.get_stylesheet(self.STYLESHEET))
|
self.setStyleSheet(style.get_stylesheet(self.STYLESHEET))
|
||||||
|
|
||||||
def _set_mode_text(self, mode):
|
def _set_mode_text(self, mode):
|
||||||
@ -498,10 +549,10 @@ class StatusBar(QWidget):
|
|||||||
window=self._win_id)
|
window=self._win_id)
|
||||||
if mode in mode_manager.passthrough:
|
if mode in mode_manager.passthrough:
|
||||||
self._set_mode_text(mode.name)
|
self._set_mode_text(mode.name)
|
||||||
if mode == usertypes.KeyMode.insert:
|
if mode in (usertypes.KeyMode.insert,
|
||||||
self._set_insert_active(True)
|
usertypes.KeyMode.command,
|
||||||
if mode == usertypes.KeyMode.command:
|
usertypes.KeyMode.caret):
|
||||||
self._set_command_active(True)
|
self.set_mode_active(mode, True)
|
||||||
|
|
||||||
@pyqtSlot(usertypes.KeyMode, usertypes.KeyMode)
|
@pyqtSlot(usertypes.KeyMode, usertypes.KeyMode)
|
||||||
def on_mode_left(self, old_mode, new_mode):
|
def on_mode_left(self, old_mode, new_mode):
|
||||||
@ -513,10 +564,10 @@ class StatusBar(QWidget):
|
|||||||
self._set_mode_text(new_mode.name)
|
self._set_mode_text(new_mode.name)
|
||||||
else:
|
else:
|
||||||
self.txt.set_text(self.txt.Text.normal, '')
|
self.txt.set_text(self.txt.Text.normal, '')
|
||||||
if old_mode == usertypes.KeyMode.insert:
|
if old_mode in (usertypes.KeyMode.insert,
|
||||||
self._set_insert_active(False)
|
usertypes.KeyMode.command,
|
||||||
if old_mode == usertypes.KeyMode.command:
|
usertypes.KeyMode.caret):
|
||||||
self._set_command_active(False)
|
self.set_mode_active(old_mode, False)
|
||||||
|
|
||||||
@config.change_filter('ui', 'message-timeout')
|
@config.change_filter('ui', 'message-timeout')
|
||||||
def set_pop_timer_interval(self):
|
def set_pop_timer_interval(self):
|
||||||
|
@ -29,7 +29,7 @@ from PyQt5.QtGui import QIcon
|
|||||||
from qutebrowser.config import config
|
from qutebrowser.config import config
|
||||||
from qutebrowser.keyinput import modeman
|
from qutebrowser.keyinput import modeman
|
||||||
from qutebrowser.mainwindow import tabwidget
|
from qutebrowser.mainwindow import tabwidget
|
||||||
from qutebrowser.browser import signalfilter, commands, webview
|
from qutebrowser.browser import signalfilter, webview
|
||||||
from qutebrowser.utils import log, usertypes, utils, qtutils, objreg, urlutils
|
from qutebrowser.utils import log, usertypes, utils, qtutils, objreg, urlutils
|
||||||
|
|
||||||
|
|
||||||
@ -107,12 +107,6 @@ class TabbedBrowser(tabwidget.TabWidget):
|
|||||||
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||||
self._undo_stack = []
|
self._undo_stack = []
|
||||||
self._filter = signalfilter.SignalFilter(win_id, self)
|
self._filter = signalfilter.SignalFilter(win_id, self)
|
||||||
dispatcher = commands.CommandDispatcher(win_id)
|
|
||||||
objreg.register('command-dispatcher', dispatcher, scope='window',
|
|
||||||
window=win_id)
|
|
||||||
self.destroyed.connect(
|
|
||||||
functools.partial(objreg.delete, 'command-dispatcher',
|
|
||||||
scope='window', window=win_id))
|
|
||||||
self._now_focused = None
|
self._now_focused = None
|
||||||
# FIXME adjust this to font size
|
# FIXME adjust this to font size
|
||||||
# https://github.com/The-Compiler/qutebrowser/issues/119
|
# https://github.com/The-Compiler/qutebrowser/issues/119
|
||||||
@ -518,7 +512,8 @@ class TabbedBrowser(tabwidget.TabWidget):
|
|||||||
tab = self.widget(idx)
|
tab = self.widget(idx)
|
||||||
log.modes.debug("Current tab changed, focusing {!r}".format(tab))
|
log.modes.debug("Current tab changed, focusing {!r}".format(tab))
|
||||||
tab.setFocus()
|
tab.setFocus()
|
||||||
for mode in (usertypes.KeyMode.hint, usertypes.KeyMode.insert):
|
for mode in (usertypes.KeyMode.hint, usertypes.KeyMode.insert,
|
||||||
|
usertypes.KeyMode.caret):
|
||||||
modeman.maybe_leave(self._win_id, mode, 'tab changed')
|
modeman.maybe_leave(self._win_id, mode, 'tab changed')
|
||||||
if self._now_focused is not None:
|
if self._now_focused is not None:
|
||||||
objreg.register('last-focused-tab', self._now_focused, update=True,
|
objreg.register('last-focused-tab', self._now_focused, update=True,
|
||||||
@ -582,3 +577,14 @@ class TabbedBrowser(tabwidget.TabWidget):
|
|||||||
"""
|
"""
|
||||||
super().resizeEvent(e)
|
super().resizeEvent(e)
|
||||||
self.resized.emit(self.geometry())
|
self.resized.emit(self.geometry())
|
||||||
|
|
||||||
|
def wheelEvent(self, e):
|
||||||
|
"""Override wheelEvent of QWidget to forward it to the focused tab.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
e: The QWheelEvent
|
||||||
|
"""
|
||||||
|
if self._now_focused is not None:
|
||||||
|
self._now_focused.wheelEvent(e)
|
||||||
|
else:
|
||||||
|
e.ignore()
|
||||||
|
@ -480,6 +480,19 @@ class TabBar(QTabBar):
|
|||||||
new_idx = super().insertTab(idx, icon, '')
|
new_idx = super().insertTab(idx, icon, '')
|
||||||
self.set_page_title(new_idx, text)
|
self.set_page_title(new_idx, text)
|
||||||
|
|
||||||
|
def wheelEvent(self, e):
|
||||||
|
"""Override wheelEvent to make the action configurable.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
e: The QWheelEvent
|
||||||
|
"""
|
||||||
|
if config.get('tabs', 'mousewheel-tab-switching'):
|
||||||
|
super().wheelEvent(e)
|
||||||
|
else:
|
||||||
|
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||||
|
window=self._win_id)
|
||||||
|
tabbed_browser.wheelEvent(e)
|
||||||
|
|
||||||
|
|
||||||
class TabBarStyle(QCommonStyle):
|
class TabBarStyle(QCommonStyle):
|
||||||
|
|
||||||
|
@ -47,7 +47,7 @@ def check_python_version():
|
|||||||
version_str = '.'.join(map(str, sys.version_info[:3]))
|
version_str = '.'.join(map(str, sys.version_info[:3]))
|
||||||
text = ("At least Python 3.4 is required to run qutebrowser, but " +
|
text = ("At least Python 3.4 is required to run qutebrowser, but " +
|
||||||
version_str + " is installed!\n")
|
version_str + " is installed!\n")
|
||||||
if Tk:
|
if Tk and '--no-err-windows' not in sys.argv:
|
||||||
root = Tk()
|
root = Tk()
|
||||||
root.withdraw()
|
root.withdraw()
|
||||||
messagebox.showerror("qutebrowser: Fatal error!", text)
|
messagebox.showerror("qutebrowser: Fatal error!", text)
|
||||||
|
@ -584,3 +584,38 @@ class ReportErrorDialog(QDialog):
|
|||||||
btn.clicked.connect(self.close)
|
btn.clicked.connect(self.close)
|
||||||
hbox.addWidget(btn)
|
hbox.addWidget(btn)
|
||||||
vbox.addLayout(hbox)
|
vbox.addLayout(hbox)
|
||||||
|
|
||||||
|
|
||||||
|
def dump_exception_info(exc, pages, cmdhist, objects):
|
||||||
|
"""Dump exception info to stderr.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
exc: An exception tuple (type, value, traceback)
|
||||||
|
pages: A list of lists of the open pages (URLs as strings)
|
||||||
|
cmdhist: A list with the command history (as strings)
|
||||||
|
objects: A list of all QObjects as string.
|
||||||
|
"""
|
||||||
|
print(file=sys.stderr)
|
||||||
|
print("\n\n===== Handling exception with --no-err-windows... =====\n\n",
|
||||||
|
file=sys.stderr)
|
||||||
|
print("\n---- Exceptions ----", file=sys.stderr)
|
||||||
|
print(''.join(traceback.format_exception(*exc)), file=sys.stderr)
|
||||||
|
print("\n---- Version info ----", file=sys.stderr)
|
||||||
|
try:
|
||||||
|
print(version.version(), file=sys.stderr)
|
||||||
|
except Exception:
|
||||||
|
traceback.print_exc()
|
||||||
|
print("\n---- Config ----", file=sys.stderr)
|
||||||
|
try:
|
||||||
|
conf = objreg.get('config')
|
||||||
|
print(conf.dump_userconfig(), file=sys.stderr)
|
||||||
|
except Exception:
|
||||||
|
traceback.print_exc()
|
||||||
|
print("\n---- Commandline args ----", file=sys.stderr)
|
||||||
|
print(' '.join(sys.argv[1:]), file=sys.stderr)
|
||||||
|
print("\n---- Open pages ----", file=sys.stderr)
|
||||||
|
print('\n\n'.join('\n'.join(e) for e in pages), file=sys.stderr)
|
||||||
|
print("\n---- Command history ----", file=sys.stderr)
|
||||||
|
print('\n'.join(cmdhist), file=sys.stderr)
|
||||||
|
print("\n---- Objects ----", file=sys.stderr)
|
||||||
|
print(objects, file=sys.stderr)
|
||||||
|
@ -27,6 +27,7 @@ import signal
|
|||||||
import functools
|
import functools
|
||||||
import faulthandler
|
import faulthandler
|
||||||
import os.path
|
import os.path
|
||||||
|
import collections
|
||||||
|
|
||||||
from PyQt5.QtCore import (pyqtSlot, qInstallMessageHandler, QObject,
|
from PyQt5.QtCore import (pyqtSlot, qInstallMessageHandler, QObject,
|
||||||
QSocketNotifier, QTimer, QUrl)
|
QSocketNotifier, QTimer, QUrl)
|
||||||
@ -37,6 +38,10 @@ from qutebrowser.misc import earlyinit, crashdialog
|
|||||||
from qutebrowser.utils import usertypes, standarddir, log, objreg, debug
|
from qutebrowser.utils import usertypes, standarddir, log, objreg, debug
|
||||||
|
|
||||||
|
|
||||||
|
ExceptionInfo = collections.namedtuple('ExceptionInfo',
|
||||||
|
'pages, cmd_history, objects')
|
||||||
|
|
||||||
|
|
||||||
class CrashHandler(QObject):
|
class CrashHandler(QObject):
|
||||||
|
|
||||||
"""Handler for crashes, reports and exceptions.
|
"""Handler for crashes, reports and exceptions.
|
||||||
@ -63,7 +68,10 @@ class CrashHandler(QObject):
|
|||||||
|
|
||||||
def handle_segfault(self):
|
def handle_segfault(self):
|
||||||
"""Handle a segfault from a previous run."""
|
"""Handle a segfault from a previous run."""
|
||||||
logname = os.path.join(standarddir.data(), 'crash.log')
|
data_dir = None
|
||||||
|
if data_dir is None:
|
||||||
|
return
|
||||||
|
logname = os.path.join(data_dir, 'crash.log')
|
||||||
try:
|
try:
|
||||||
# First check if an old logfile exists.
|
# First check if an old logfile exists.
|
||||||
if os.path.exists(logname):
|
if os.path.exists(logname):
|
||||||
@ -118,7 +126,11 @@ class CrashHandler(QObject):
|
|||||||
|
|
||||||
def _init_crashlogfile(self):
|
def _init_crashlogfile(self):
|
||||||
"""Start a new logfile and redirect faulthandler to it."""
|
"""Start a new logfile and redirect faulthandler to it."""
|
||||||
logname = os.path.join(standarddir.data(), 'crash.log')
|
assert not self._args.no_err_windows
|
||||||
|
data_dir = standarddir.data()
|
||||||
|
if data_dir is None:
|
||||||
|
return
|
||||||
|
logname = os.path.join(data_dir, 'crash.log')
|
||||||
try:
|
try:
|
||||||
self._crash_log_file = open(logname, 'w', encoding='ascii')
|
self._crash_log_file = open(logname, 'w', encoding='ascii')
|
||||||
except OSError:
|
except OSError:
|
||||||
@ -153,6 +165,31 @@ class CrashHandler(QObject):
|
|||||||
except OSError:
|
except OSError:
|
||||||
log.destroy.exception("Could not remove crash log!")
|
log.destroy.exception("Could not remove crash log!")
|
||||||
|
|
||||||
|
def _get_exception_info(self):
|
||||||
|
"""Get info needed for the exception hook/dialog.
|
||||||
|
|
||||||
|
Return:
|
||||||
|
An ExceptionInfo namedtuple.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
pages = self._recover_pages(forgiving=True)
|
||||||
|
except Exception:
|
||||||
|
log.destroy.exception("Error while recovering pages")
|
||||||
|
pages = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
cmd_history = objreg.get('command-history')[-5:]
|
||||||
|
except Exception:
|
||||||
|
log.destroy.exception("Error while getting history: {}")
|
||||||
|
cmd_history = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
objects = debug.get_all_objects()
|
||||||
|
except Exception:
|
||||||
|
log.destroy.exception("Error while getting objects")
|
||||||
|
objects = ""
|
||||||
|
return ExceptionInfo(pages, cmd_history, objects)
|
||||||
|
|
||||||
def exception_hook(self, exctype, excvalue, tb): # noqa
|
def exception_hook(self, exctype, excvalue, tb): # noqa
|
||||||
"""Handle uncaught python exceptions.
|
"""Handle uncaught python exceptions.
|
||||||
|
|
||||||
@ -175,12 +212,11 @@ class CrashHandler(QObject):
|
|||||||
if self._args.pdb_postmortem:
|
if self._args.pdb_postmortem:
|
||||||
pdb.post_mortem(tb)
|
pdb.post_mortem(tb)
|
||||||
|
|
||||||
if (is_ignored_exception or self._args.no_crash_dialog or
|
if is_ignored_exception or self._args.pdb_postmortem:
|
||||||
self._args.pdb_postmortem):
|
|
||||||
# pdb exit, KeyboardInterrupt, ...
|
# pdb exit, KeyboardInterrupt, ...
|
||||||
status = 0 if is_ignored_exception else 2
|
status = 0 if is_ignored_exception else 2
|
||||||
try:
|
try:
|
||||||
qapp.shutdown(status)
|
self._quitter.shutdown(status)
|
||||||
return
|
return
|
||||||
except Exception:
|
except Exception:
|
||||||
log.init.exception("Error while shutting down")
|
log.init.exception("Error while shutting down")
|
||||||
@ -188,24 +224,7 @@ class CrashHandler(QObject):
|
|||||||
return
|
return
|
||||||
|
|
||||||
self._quitter.quit_status['crash'] = False
|
self._quitter.quit_status['crash'] = False
|
||||||
|
info = self._get_exception_info()
|
||||||
try:
|
|
||||||
pages = self._recover_pages(forgiving=True)
|
|
||||||
except Exception:
|
|
||||||
log.destroy.exception("Error while recovering pages")
|
|
||||||
pages = []
|
|
||||||
|
|
||||||
try:
|
|
||||||
cmd_history = objreg.get('command-history')[-5:]
|
|
||||||
except Exception:
|
|
||||||
log.destroy.exception("Error while getting history: {}")
|
|
||||||
cmd_history = []
|
|
||||||
|
|
||||||
try:
|
|
||||||
objects = debug.get_all_objects()
|
|
||||||
except Exception:
|
|
||||||
log.destroy.exception("Error while getting objects")
|
|
||||||
objects = ""
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
objreg.get('ipc-server').ignored = True
|
objreg.get('ipc-server').ignored = True
|
||||||
@ -218,18 +237,23 @@ class CrashHandler(QObject):
|
|||||||
except TypeError:
|
except TypeError:
|
||||||
log.destroy.exception("Error while preventing shutdown")
|
log.destroy.exception("Error while preventing shutdown")
|
||||||
self._app.closeAllWindows()
|
self._app.closeAllWindows()
|
||||||
self._crash_dialog = crashdialog.ExceptionCrashDialog(
|
if self._args.no_err_windows:
|
||||||
self._args.debug, pages, cmd_history, exc, objects)
|
crashdialog.dump_exception_info(exc, info.pages, info.cmd_history,
|
||||||
ret = self._crash_dialog.exec_()
|
info.objects)
|
||||||
if ret == QDialog.Accepted: # restore
|
else:
|
||||||
self._quitter.restart(pages)
|
self._crash_dialog = crashdialog.ExceptionCrashDialog(
|
||||||
|
self._args.debug, info.pages, info.cmd_history, exc,
|
||||||
|
info.objects)
|
||||||
|
ret = self._crash_dialog.exec_()
|
||||||
|
if ret == QDialog.Accepted: # restore
|
||||||
|
self._quitter.restart(info.pages)
|
||||||
|
|
||||||
# We might risk a segfault here, but that's better than continuing to
|
# We might risk a segfault here, but that's better than continuing to
|
||||||
# run in some undefined state, so we only do the most needed shutdown
|
# run in some undefined state, so we only do the most needed shutdown
|
||||||
# here.
|
# here.
|
||||||
qInstallMessageHandler(None)
|
qInstallMessageHandler(None)
|
||||||
self.destroy_crashlogfile()
|
self.destroy_crashlogfile()
|
||||||
sys.exit(1)
|
sys.exit(usertypes.Exit.exception)
|
||||||
|
|
||||||
def raise_crashdlg(self):
|
def raise_crashdlg(self):
|
||||||
"""Raise the crash dialog if one exists."""
|
"""Raise the crash dialog if one exists."""
|
||||||
|
@ -80,16 +80,21 @@ def _die(message, exception=None):
|
|||||||
"""
|
"""
|
||||||
from PyQt5.QtWidgets import QApplication, QMessageBox
|
from PyQt5.QtWidgets import QApplication, QMessageBox
|
||||||
from PyQt5.QtCore import Qt
|
from PyQt5.QtCore import Qt
|
||||||
if '--debug' in sys.argv and exception is not None:
|
if (('--debug' in sys.argv or '--no-err-windows' in sys.argv) and
|
||||||
|
exception is not None):
|
||||||
print(file=sys.stderr)
|
print(file=sys.stderr)
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
message += '<br/><br/><br/><b>Error:</b><br/>{}'.format(exception)
|
if '--no-err-windows' in sys.argv:
|
||||||
msgbox = QMessageBox(QMessageBox.Critical, "qutebrowser: Fatal error!",
|
print(message, file=sys.stderr)
|
||||||
message)
|
print("Exiting because of --no-err-windows.", file=sys.stderr)
|
||||||
msgbox.setTextFormat(Qt.RichText)
|
else:
|
||||||
msgbox.resize(msgbox.sizeHint())
|
message += '<br/><br/><br/><b>Error:</b><br/>{}'.format(exception)
|
||||||
msgbox.exec_()
|
msgbox = QMessageBox(QMessageBox.Critical, "qutebrowser: Fatal error!",
|
||||||
|
message)
|
||||||
|
msgbox.setTextFormat(Qt.RichText)
|
||||||
|
msgbox.resize(msgbox.sizeHint())
|
||||||
|
msgbox.exec_()
|
||||||
app.quit()
|
app.quit()
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
@ -186,13 +191,13 @@ def check_pyqt_core():
|
|||||||
text = text.replace('</b>', '')
|
text = text.replace('</b>', '')
|
||||||
text = text.replace('<br />', '\n')
|
text = text.replace('<br />', '\n')
|
||||||
text += '\n\nError: {}'.format(e)
|
text += '\n\nError: {}'.format(e)
|
||||||
if tkinter:
|
if tkinter and '--no-err-windows' not in sys.argv:
|
||||||
root = tkinter.Tk()
|
root = tkinter.Tk()
|
||||||
root.withdraw()
|
root.withdraw()
|
||||||
tkinter.messagebox.showerror("qutebrowser: Fatal error!", text)
|
tkinter.messagebox.showerror("qutebrowser: Fatal error!", text)
|
||||||
else:
|
else:
|
||||||
print(text, file=sys.stderr)
|
print(text, file=sys.stderr)
|
||||||
if '--debug' in sys.argv:
|
if '--debug' in sys.argv or '--no-err-windows' in sys.argv:
|
||||||
print(file=sys.stderr)
|
print(file=sys.stderr)
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
@ -122,7 +122,8 @@ class ExternalEditor(QObject):
|
|||||||
raise ValueError("Already editing a file!")
|
raise ValueError("Already editing a file!")
|
||||||
self._text = text
|
self._text = text
|
||||||
try:
|
try:
|
||||||
self._oshandle, self._filename = tempfile.mkstemp(text=True)
|
self._oshandle, self._filename = tempfile.mkstemp(
|
||||||
|
text=True, prefix='qutebrowser-editor-')
|
||||||
if text:
|
if text:
|
||||||
encoding = config.get('general', 'editor-encoding')
|
encoding = config.get('general', 'editor-encoding')
|
||||||
with open(self._filename, 'w', encoding=encoding) as f:
|
with open(self._filename, 'w', encoding=encoding) as f:
|
||||||
|
@ -23,20 +23,28 @@ import os
|
|||||||
import json
|
import json
|
||||||
import getpass
|
import getpass
|
||||||
import binascii
|
import binascii
|
||||||
|
import hashlib
|
||||||
|
|
||||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject
|
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject
|
||||||
from PyQt5.QtNetwork import QLocalSocket, QLocalServer, QAbstractSocket
|
from PyQt5.QtNetwork import QLocalSocket, QLocalServer, QAbstractSocket
|
||||||
from PyQt5.QtWidgets import QMessageBox
|
|
||||||
|
|
||||||
from qutebrowser.utils import log, usertypes
|
from qutebrowser.utils import log, usertypes, error
|
||||||
|
|
||||||
|
|
||||||
SOCKETNAME = 'qutebrowser-{}'.format(getpass.getuser())
|
|
||||||
CONNECT_TIMEOUT = 100
|
CONNECT_TIMEOUT = 100
|
||||||
WRITE_TIMEOUT = 1000
|
WRITE_TIMEOUT = 1000
|
||||||
READ_TIMEOUT = 5000
|
READ_TIMEOUT = 5000
|
||||||
|
|
||||||
|
|
||||||
|
def _get_socketname(args):
|
||||||
|
"""Get a socketname to use."""
|
||||||
|
parts = ['qutebrowser', getpass.getuser()]
|
||||||
|
if args.basedir is not None:
|
||||||
|
md5 = hashlib.md5(args.basedir.encode('utf-8'))
|
||||||
|
parts.append(md5.hexdigest())
|
||||||
|
return '-'.join(parts)
|
||||||
|
|
||||||
|
|
||||||
class Error(Exception):
|
class Error(Exception):
|
||||||
|
|
||||||
"""Exception raised when there was a problem with IPC."""
|
"""Exception raised when there was a problem with IPC."""
|
||||||
@ -80,6 +88,7 @@ class IPCServer(QObject):
|
|||||||
_timer: A timer to handle timeouts.
|
_timer: A timer to handle timeouts.
|
||||||
_server: A QLocalServer to accept new connections.
|
_server: A QLocalServer to accept new connections.
|
||||||
_socket: The QLocalSocket we're currently connected to.
|
_socket: The QLocalSocket we're currently connected to.
|
||||||
|
_socketname: The socketname to use.
|
||||||
|
|
||||||
Signals:
|
Signals:
|
||||||
got_args: Emitted when there was an IPC connection and arguments were
|
got_args: Emitted when there was an IPC connection and arguments were
|
||||||
@ -88,16 +97,22 @@ class IPCServer(QObject):
|
|||||||
|
|
||||||
got_args = pyqtSignal(list, str)
|
got_args = pyqtSignal(list, str)
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, args, parent=None):
|
||||||
"""Start the IPC server and listen to commands."""
|
"""Start the IPC server and listen to commands.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args: The argparse namespace.
|
||||||
|
parent: The parent to be used.
|
||||||
|
"""
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.ignored = False
|
self.ignored = False
|
||||||
|
self._socketname = _get_socketname(args)
|
||||||
self._remove_server()
|
self._remove_server()
|
||||||
self._timer = usertypes.Timer(self, 'ipc-timeout')
|
self._timer = usertypes.Timer(self, 'ipc-timeout')
|
||||||
self._timer.setInterval(READ_TIMEOUT)
|
self._timer.setInterval(READ_TIMEOUT)
|
||||||
self._timer.timeout.connect(self.on_timeout)
|
self._timer.timeout.connect(self.on_timeout)
|
||||||
self._server = QLocalServer(self)
|
self._server = QLocalServer(self)
|
||||||
ok = self._server.listen(SOCKETNAME)
|
ok = self._server.listen(self._socketname)
|
||||||
if not ok:
|
if not ok:
|
||||||
if self._server.serverError() == QAbstractSocket.AddressInUseError:
|
if self._server.serverError() == QAbstractSocket.AddressInUseError:
|
||||||
raise AddressInUseError(self._server)
|
raise AddressInUseError(self._server)
|
||||||
@ -108,17 +123,18 @@ class IPCServer(QObject):
|
|||||||
|
|
||||||
def _remove_server(self):
|
def _remove_server(self):
|
||||||
"""Remove an existing server."""
|
"""Remove an existing server."""
|
||||||
ok = QLocalServer.removeServer(SOCKETNAME)
|
ok = QLocalServer.removeServer(self._socketname)
|
||||||
if not ok:
|
if not ok:
|
||||||
raise Error("Error while removing server {}!".format(SOCKETNAME))
|
raise Error("Error while removing server {}!".format(
|
||||||
|
self._socketname))
|
||||||
|
|
||||||
@pyqtSlot(int)
|
@pyqtSlot(int)
|
||||||
def on_error(self, error):
|
def on_error(self, err):
|
||||||
"""Convenience method which calls _socket_error on an error."""
|
"""Convenience method which calls _socket_error on an error."""
|
||||||
self._timer.stop()
|
self._timer.stop()
|
||||||
log.ipc.debug("Socket error {}: {}".format(
|
log.ipc.debug("Socket error {}: {}".format(
|
||||||
self._socket.error(), self._socket.errorString()))
|
self._socket.error(), self._socket.errorString()))
|
||||||
if error != QLocalSocket.PeerClosedError:
|
if err != QLocalSocket.PeerClosedError:
|
||||||
_socket_error("handling IPC connection", self._socket)
|
_socket_error("handling IPC connection", self._socket)
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
@ -223,23 +239,23 @@ def _socket_error(action, socket):
|
|||||||
action, socket.errorString(), socket.error()))
|
action, socket.errorString(), socket.error()))
|
||||||
|
|
||||||
|
|
||||||
def send_to_running_instance(cmdlist):
|
def send_to_running_instance(args):
|
||||||
"""Try to send a commandline to a running instance.
|
"""Try to send a commandline to a running instance.
|
||||||
|
|
||||||
Blocks for CONNECT_TIMEOUT ms.
|
Blocks for CONNECT_TIMEOUT ms.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
cmdlist: A list to send (URLs/commands)
|
args: The argparse namespace.
|
||||||
|
|
||||||
Return:
|
Return:
|
||||||
True if connecting was successful, False if no connection was made.
|
True if connecting was successful, False if no connection was made.
|
||||||
"""
|
"""
|
||||||
socket = QLocalSocket()
|
socket = QLocalSocket()
|
||||||
socket.connectToServer(SOCKETNAME)
|
socket.connectToServer(_get_socketname(args))
|
||||||
connected = socket.waitForConnected(100)
|
connected = socket.waitForConnected(100)
|
||||||
if connected:
|
if connected:
|
||||||
log.ipc.info("Opening in existing instance")
|
log.ipc.info("Opening in existing instance")
|
||||||
json_data = {'args': cmdlist}
|
json_data = {'args': args.command}
|
||||||
try:
|
try:
|
||||||
cwd = os.getcwd()
|
cwd = os.getcwd()
|
||||||
except OSError:
|
except OSError:
|
||||||
@ -265,9 +281,8 @@ def send_to_running_instance(cmdlist):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def display_error(exc):
|
def display_error(exc, args):
|
||||||
"""Display a message box with an IPC error."""
|
"""Display a message box with an IPC error."""
|
||||||
text = '{}\n\nMaybe another instance is running but frozen?'.format(exc)
|
error.handle_fatal_exc(
|
||||||
msgbox = QMessageBox(QMessageBox.Critical, "Error while connecting to "
|
exc, args, "Error while connecting to running instance!",
|
||||||
"running instance!", text)
|
post_text="Maybe another instance is running but frozen?")
|
||||||
msgbox.exec_()
|
|
||||||
|
@ -35,7 +35,7 @@ class BaseLineParser(QObject):
|
|||||||
"""A LineParser without any real data.
|
"""A LineParser without any real data.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
_configdir: The directory to read the config from.
|
_configdir: Directory to read the config from, or None.
|
||||||
_configfile: The config file path.
|
_configfile: The config file path.
|
||||||
_fname: Filename of the config.
|
_fname: Filename of the config.
|
||||||
_binary: Whether to open the file in binary mode.
|
_binary: Whether to open the file in binary mode.
|
||||||
@ -53,12 +53,17 @@ class BaseLineParser(QObject):
|
|||||||
configdir: Directory to read the config from.
|
configdir: Directory to read the config from.
|
||||||
fname: Filename of the config file.
|
fname: Filename of the config file.
|
||||||
binary: Whether to open the file in binary mode.
|
binary: Whether to open the file in binary mode.
|
||||||
|
_opened: Whether the underlying file is open
|
||||||
"""
|
"""
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self._configdir = configdir
|
self._configdir = configdir
|
||||||
self._configfile = os.path.join(self._configdir, fname)
|
if self._configdir is None:
|
||||||
|
self._configfile = None
|
||||||
|
else:
|
||||||
|
self._configfile = os.path.join(self._configdir, fname)
|
||||||
self._fname = fname
|
self._fname = fname
|
||||||
self._binary = binary
|
self._binary = binary
|
||||||
|
self._opened = False
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return utils.get_repr(self, constructor=True,
|
return utils.get_repr(self, constructor=True,
|
||||||
@ -66,21 +71,38 @@ class BaseLineParser(QObject):
|
|||||||
binary=self._binary)
|
binary=self._binary)
|
||||||
|
|
||||||
def _prepare_save(self):
|
def _prepare_save(self):
|
||||||
"""Prepare saving of the file."""
|
"""Prepare saving of the file.
|
||||||
|
|
||||||
|
Return:
|
||||||
|
True if the file should be saved, False otherwise.
|
||||||
|
"""
|
||||||
|
if self._configdir is None:
|
||||||
|
return False
|
||||||
log.destroy.debug("Saving to {}".format(self._configfile))
|
log.destroy.debug("Saving to {}".format(self._configfile))
|
||||||
if not os.path.exists(self._configdir):
|
if not os.path.exists(self._configdir):
|
||||||
os.makedirs(self._configdir, 0o755)
|
os.makedirs(self._configdir, 0o755)
|
||||||
|
return True
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
def _open(self, mode):
|
def _open(self, mode):
|
||||||
"""Open self._configfile for reading.
|
"""Open self._configfile for reading.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
mode: The mode to use ('a'/'r'/'w')
|
mode: The mode to use ('a'/'r'/'w')
|
||||||
"""
|
"""
|
||||||
if self._binary:
|
assert self._configfile is not None
|
||||||
return open(self._configfile, mode + 'b')
|
if self._opened:
|
||||||
else:
|
raise IOError("Refusing to double-open AppendLineParser.")
|
||||||
return open(self._configfile, mode, encoding='utf-8')
|
self._opened = True
|
||||||
|
try:
|
||||||
|
if self._binary:
|
||||||
|
with open(self._configfile, mode + 'b') as f:
|
||||||
|
yield f
|
||||||
|
else:
|
||||||
|
with open(self._configfile, mode, encoding='utf-8') as f:
|
||||||
|
yield f
|
||||||
|
finally:
|
||||||
|
self._opened = False
|
||||||
|
|
||||||
def _write(self, fp, data):
|
def _write(self, fp, data):
|
||||||
"""Write the data to a file.
|
"""Write the data to a file.
|
||||||
@ -150,7 +172,9 @@ class AppendLineParser(BaseLineParser):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
self._prepare_save()
|
do_save = self._prepare_save()
|
||||||
|
if not do_save:
|
||||||
|
return
|
||||||
with self._open('a') as f:
|
with self._open('a') as f:
|
||||||
self._write(f, self.new_data)
|
self._write(f, self.new_data)
|
||||||
self.new_data = []
|
self.new_data = []
|
||||||
@ -173,7 +197,7 @@ class LineParser(BaseLineParser):
|
|||||||
binary: Whether to open the file in binary mode.
|
binary: Whether to open the file in binary mode.
|
||||||
"""
|
"""
|
||||||
super().__init__(configdir, fname, binary=binary, parent=parent)
|
super().__init__(configdir, fname, binary=binary, parent=parent)
|
||||||
if not os.path.isfile(self._configfile):
|
if configdir is None or not os.path.isfile(self._configfile):
|
||||||
self.data = []
|
self.data = []
|
||||||
else:
|
else:
|
||||||
log.init.debug("Reading {}".format(self._configfile))
|
log.init.debug("Reading {}".format(self._configfile))
|
||||||
@ -195,9 +219,18 @@ class LineParser(BaseLineParser):
|
|||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
"""Save the config file."""
|
"""Save the config file."""
|
||||||
self._prepare_save()
|
if self._opened:
|
||||||
with qtutils.savefile_open(self._configfile, self._binary) as f:
|
raise IOError("Refusing to double-open AppendLineParser.")
|
||||||
self._write(f, self.data)
|
do_save = self._prepare_save()
|
||||||
|
if not do_save:
|
||||||
|
return
|
||||||
|
self._opened = True
|
||||||
|
try:
|
||||||
|
assert self._configfile is not None
|
||||||
|
with qtutils.savefile_open(self._configfile, self._binary) as f:
|
||||||
|
self._write(f, self.data)
|
||||||
|
finally:
|
||||||
|
self._opened = False
|
||||||
|
|
||||||
|
|
||||||
class LimitLineParser(LineParser):
|
class LimitLineParser(LineParser):
|
||||||
@ -213,14 +246,14 @@ class LimitLineParser(LineParser):
|
|||||||
"""Constructor.
|
"""Constructor.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
configdir: Directory to read the config from.
|
configdir: Directory to read the config from, or None.
|
||||||
fname: Filename of the config file.
|
fname: Filename of the config file.
|
||||||
limit: Config tuple (section, option) which contains a limit.
|
limit: Config tuple (section, option) which contains a limit.
|
||||||
binary: Whether to open the file in binary mode.
|
binary: Whether to open the file in binary mode.
|
||||||
"""
|
"""
|
||||||
super().__init__(configdir, fname, binary=binary, parent=parent)
|
super().__init__(configdir, fname, binary=binary, parent=parent)
|
||||||
self._limit = limit
|
self._limit = limit
|
||||||
if limit is not None:
|
if limit is not None and configdir is not None:
|
||||||
objreg.get('config').changed.connect(self.cleanup_file)
|
objreg.get('config').changed.connect(self.cleanup_file)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
@ -231,6 +264,7 @@ class LimitLineParser(LineParser):
|
|||||||
@pyqtSlot(str, str)
|
@pyqtSlot(str, str)
|
||||||
def cleanup_file(self, section, option):
|
def cleanup_file(self, section, option):
|
||||||
"""Delete the file if the limit was changed to 0."""
|
"""Delete the file if the limit was changed to 0."""
|
||||||
|
assert self._configfile is not None
|
||||||
if (section, option) != self._limit:
|
if (section, option) != self._limit:
|
||||||
return
|
return
|
||||||
value = config.get(section, option)
|
value = config.get(section, option)
|
||||||
@ -243,6 +277,9 @@ class LimitLineParser(LineParser):
|
|||||||
limit = config.get(*self._limit)
|
limit = config.get(*self._limit)
|
||||||
if limit == 0:
|
if limit == 0:
|
||||||
return
|
return
|
||||||
self._prepare_save()
|
do_save = self._prepare_save()
|
||||||
|
if not do_save:
|
||||||
|
return
|
||||||
|
assert self._configfile is not None
|
||||||
with qtutils.savefile_open(self._configfile, self._binary) as f:
|
with qtutils.savefile_open(self._configfile, self._binary) as f:
|
||||||
self._write(f, self.data[-limit:])
|
self._write(f, self.data[-limit:])
|
||||||
|
@ -82,10 +82,14 @@ class SessionManager(QObject):
|
|||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self._current = None
|
self._current = None
|
||||||
self._base_path = os.path.join(standarddir.data(), 'sessions')
|
data_dir = standarddir.data()
|
||||||
|
if data_dir is None:
|
||||||
|
self._base_path = None
|
||||||
|
else:
|
||||||
|
self._base_path = os.path.join(standarddir.data(), 'sessions')
|
||||||
self._last_window_session = None
|
self._last_window_session = None
|
||||||
self.did_load = False
|
self.did_load = False
|
||||||
if not os.path.exists(self._base_path):
|
if self._base_path is not None and not os.path.exists(self._base_path):
|
||||||
os.mkdir(self._base_path)
|
os.mkdir(self._base_path)
|
||||||
|
|
||||||
def _get_session_path(self, name, check_exists=False):
|
def _get_session_path(self, name, check_exists=False):
|
||||||
@ -100,6 +104,11 @@ class SessionManager(QObject):
|
|||||||
if os.path.isabs(path) and ((not check_exists) or
|
if os.path.isabs(path) and ((not check_exists) or
|
||||||
os.path.exists(path)):
|
os.path.exists(path)):
|
||||||
return path
|
return path
|
||||||
|
elif self._base_path is None:
|
||||||
|
if check_exists:
|
||||||
|
raise SessionNotFoundError(name)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
else:
|
else:
|
||||||
path = os.path.join(self._base_path, name + '.yml')
|
path = os.path.join(self._base_path, name + '.yml')
|
||||||
if check_exists and not os.path.exists(path):
|
if check_exists and not os.path.exists(path):
|
||||||
@ -194,6 +203,8 @@ class SessionManager(QObject):
|
|||||||
else:
|
else:
|
||||||
name = 'default'
|
name = 'default'
|
||||||
path = self._get_session_path(name)
|
path = self._get_session_path(name)
|
||||||
|
if path is None:
|
||||||
|
raise SessionError("No data storage configured.")
|
||||||
|
|
||||||
log.sessions.debug("Saving session {} to {}...".format(name, path))
|
log.sessions.debug("Saving session {} to {}...".format(name, path))
|
||||||
if last_window:
|
if last_window:
|
||||||
@ -289,6 +300,8 @@ class SessionManager(QObject):
|
|||||||
def list_sessions(self):
|
def list_sessions(self):
|
||||||
"""Get a list of all session names."""
|
"""Get a list of all session names."""
|
||||||
sessions = []
|
sessions = []
|
||||||
|
if self._base_path is None:
|
||||||
|
return sessions
|
||||||
for filename in os.listdir(self._base_path):
|
for filename in os.listdir(self._base_path):
|
||||||
base, ext = os.path.splitext(filename)
|
base, ext = os.path.splitext(filename)
|
||||||
if ext == '.yml':
|
if ext == '.yml':
|
||||||
|
@ -48,6 +48,12 @@ def get_argparser():
|
|||||||
description=qutebrowser.__description__)
|
description=qutebrowser.__description__)
|
||||||
parser.add_argument('-c', '--confdir', help="Set config directory (empty "
|
parser.add_argument('-c', '--confdir', help="Set config directory (empty "
|
||||||
"for no config storage).")
|
"for no config storage).")
|
||||||
|
parser.add_argument('--datadir', help="Set data directory (empty for "
|
||||||
|
"no data storage).")
|
||||||
|
parser.add_argument('--cachedir', help="Set cache directory (empty for "
|
||||||
|
"no cache storage).")
|
||||||
|
parser.add_argument('--basedir', help="Base directory for all storage. "
|
||||||
|
"Other --*dir arguments are ignored if this is given.")
|
||||||
parser.add_argument('-V', '--version', help="Show version and quit.",
|
parser.add_argument('-V', '--version', help="Show version and quit.",
|
||||||
action='store_true')
|
action='store_true')
|
||||||
parser.add_argument('-s', '--set', help="Set a temporary setting for "
|
parser.add_argument('-s', '--set', help="Set a temporary setting for "
|
||||||
@ -84,10 +90,12 @@ def get_argparser():
|
|||||||
"the main window.")
|
"the main window.")
|
||||||
debug.add_argument('--debug-exit', help="Turn on debugging of late exit.",
|
debug.add_argument('--debug-exit', help="Turn on debugging of late exit.",
|
||||||
action='store_true')
|
action='store_true')
|
||||||
debug.add_argument('--no-crash-dialog', action='store_true', help="Don't "
|
|
||||||
"show a crash dialog.")
|
|
||||||
debug.add_argument('--pdb-postmortem', action='store_true',
|
debug.add_argument('--pdb-postmortem', action='store_true',
|
||||||
help="Drop into pdb on exceptions.")
|
help="Drop into pdb on exceptions.")
|
||||||
|
debug.add_argument('--temp-basedir', action='store_true', help="Use a "
|
||||||
|
"temporary basedir.")
|
||||||
|
debug.add_argument('--no-err-windows', action='store_true', help="Don't "
|
||||||
|
"show any error windows (used for tests/smoke.py).")
|
||||||
# For the Qt args, we use store_const with const=True rather than
|
# For the Qt args, we use store_const with const=True rather than
|
||||||
# store_true because we want the default to be None, to make
|
# store_true because we want the default to be None, to make
|
||||||
# utils.qt:get_args easier.
|
# utils.qt:get_args easier.
|
||||||
|
54
qutebrowser/utils/error.py
Normal file
54
qutebrowser/utils/error.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||||
|
|
||||||
|
# Copyright 2015 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/>.
|
||||||
|
|
||||||
|
"""Tools related to error printing/displaying."""
|
||||||
|
|
||||||
|
from PyQt5.QtWidgets import QMessageBox
|
||||||
|
|
||||||
|
from qutebrowser.utils import log
|
||||||
|
|
||||||
|
|
||||||
|
def handle_fatal_exc(exc, args, title, *, pre_text='', post_text=''):
|
||||||
|
"""Handle a fatal "expected" exception by displaying an error box.
|
||||||
|
|
||||||
|
If --no-err-windows is given as argument, the text is logged to the error
|
||||||
|
logger instead.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
exc: The Exception object being handled.
|
||||||
|
args: The argparser namespace.
|
||||||
|
title: The title to be used for the error message.
|
||||||
|
pre_text: The text to be displayed before the exception text.
|
||||||
|
post_text: The text to be displayed after the exception text.
|
||||||
|
"""
|
||||||
|
if args.no_err_windows:
|
||||||
|
log.misc.exception("Handling fatal {} with --no-err-windows!".format(
|
||||||
|
exc.__class__.__name__))
|
||||||
|
log.misc.error("title: {}".format(title))
|
||||||
|
log.misc.error("pre_text: {}".format(pre_text))
|
||||||
|
log.misc.error("post_text: {}".format(post_text))
|
||||||
|
else:
|
||||||
|
if pre_text:
|
||||||
|
msg_text = '{}: {}'.format(pre_text, exc)
|
||||||
|
else:
|
||||||
|
msg_text = str(exc)
|
||||||
|
if post_text:
|
||||||
|
msg_text += '\n\n{}'.format(post_text)
|
||||||
|
msgbox = QMessageBox(QMessageBox.Critical, title, msg_text)
|
||||||
|
msgbox.exec_()
|
@ -29,8 +29,7 @@ import faulthandler
|
|||||||
import traceback
|
import traceback
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
from PyQt5.QtCore import (QtDebugMsg, QtWarningMsg, QtCriticalMsg, QtFatalMsg,
|
from PyQt5 import QtCore
|
||||||
qInstallMessageHandler)
|
|
||||||
# Optional imports
|
# Optional imports
|
||||||
try:
|
try:
|
||||||
import colorama
|
import colorama
|
||||||
@ -153,15 +152,15 @@ def init_log(args):
|
|||||||
root.setLevel(logging.NOTSET)
|
root.setLevel(logging.NOTSET)
|
||||||
logging.captureWarnings(True)
|
logging.captureWarnings(True)
|
||||||
warnings.simplefilter('default')
|
warnings.simplefilter('default')
|
||||||
qInstallMessageHandler(qt_message_handler)
|
QtCore.qInstallMessageHandler(qt_message_handler)
|
||||||
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def disable_qt_msghandler():
|
def disable_qt_msghandler():
|
||||||
"""Contextmanager which temporarily disables the Qt message handler."""
|
"""Contextmanager which temporarily disables the Qt message handler."""
|
||||||
old_handler = qInstallMessageHandler(None)
|
old_handler = QtCore.qInstallMessageHandler(None)
|
||||||
yield
|
yield
|
||||||
qInstallMessageHandler(old_handler)
|
QtCore.qInstallMessageHandler(old_handler)
|
||||||
|
|
||||||
|
|
||||||
def _init_handlers(level, color, ram_capacity):
|
def _init_handlers(level, color, ram_capacity):
|
||||||
@ -244,11 +243,17 @@ def qt_message_handler(msg_type, context, msg):
|
|||||||
# Note we map critical to ERROR as it's actually "just" an error, and fatal
|
# Note we map critical to ERROR as it's actually "just" an error, and fatal
|
||||||
# to critical.
|
# to critical.
|
||||||
qt_to_logging = {
|
qt_to_logging = {
|
||||||
QtDebugMsg: logging.DEBUG,
|
QtCore.QtDebugMsg: logging.DEBUG,
|
||||||
QtWarningMsg: logging.WARNING,
|
QtCore.QtWarningMsg: logging.WARNING,
|
||||||
QtCriticalMsg: logging.ERROR,
|
QtCore.QtCriticalMsg: logging.ERROR,
|
||||||
QtFatalMsg: logging.CRITICAL,
|
QtCore.QtFatalMsg: logging.CRITICAL,
|
||||||
}
|
}
|
||||||
|
try:
|
||||||
|
# pylint: disable=no-member
|
||||||
|
qt_to_logging[QtCore.QtInfoMsg] = logging.INFO
|
||||||
|
except AttributeError:
|
||||||
|
# Qt < 5.5
|
||||||
|
pass
|
||||||
# Change levels of some well-known messages to debug so they don't get
|
# Change levels of some well-known messages to debug so they don't get
|
||||||
# shown to the user.
|
# shown to the user.
|
||||||
# suppressed_msgs is a list of regexes matching the message texts to hide.
|
# suppressed_msgs is a list of regexes matching the message texts to hide.
|
||||||
|
@ -131,7 +131,7 @@ def ensure_valid(obj):
|
|||||||
def ensure_not_null(obj):
|
def ensure_not_null(obj):
|
||||||
"""Ensure a Qt object with an .isNull() method is not null."""
|
"""Ensure a Qt object with an .isNull() method is not null."""
|
||||||
if obj.isNull():
|
if obj.isNull():
|
||||||
raise QtValueError(obj)
|
raise QtValueError(obj, null=True)
|
||||||
|
|
||||||
|
|
||||||
def check_qdatastream(stream):
|
def check_qdatastream(stream):
|
||||||
@ -180,7 +180,7 @@ def deserialize_stream(stream, obj):
|
|||||||
def savefile_open(filename, binary=False, encoding='utf-8'):
|
def savefile_open(filename, binary=False, encoding='utf-8'):
|
||||||
"""Context manager to easily use a QSaveFile."""
|
"""Context manager to easily use a QSaveFile."""
|
||||||
f = QSaveFile(filename)
|
f = QSaveFile(filename)
|
||||||
new_f = None
|
cancelled = False
|
||||||
try:
|
try:
|
||||||
ok = f.open(QIODevice.WriteOnly)
|
ok = f.open(QIODevice.WriteOnly)
|
||||||
if not ok:
|
if not ok:
|
||||||
@ -192,13 +192,14 @@ def savefile_open(filename, binary=False, encoding='utf-8'):
|
|||||||
yield new_f
|
yield new_f
|
||||||
except:
|
except:
|
||||||
f.cancelWriting()
|
f.cancelWriting()
|
||||||
|
cancelled = True
|
||||||
raise
|
raise
|
||||||
|
else:
|
||||||
|
new_f.flush()
|
||||||
finally:
|
finally:
|
||||||
if new_f is not None:
|
|
||||||
new_f.flush()
|
|
||||||
commit_ok = f.commit()
|
commit_ok = f.commit()
|
||||||
if not commit_ok:
|
if not commit_ok and not cancelled:
|
||||||
raise OSError(f.errorString())
|
raise OSError("Commit failed!")
|
||||||
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
@ -221,27 +222,58 @@ class PyQIODevice(io.BufferedIOBase):
|
|||||||
"""Wrapper for a QIODevice which provides a python interface.
|
"""Wrapper for a QIODevice which provides a python interface.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
_dev: The underlying QIODevice.
|
dev: The underlying QIODevice.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# pylint: disable=missing-docstring
|
# pylint: disable=missing-docstring
|
||||||
|
|
||||||
def __init__(self, dev):
|
def __init__(self, dev):
|
||||||
self._dev = dev
|
self.dev = dev
|
||||||
|
|
||||||
def __len__(self):
|
def __len__(self):
|
||||||
return self._dev.size()
|
return self.dev.size()
|
||||||
|
|
||||||
def _check_open(self):
|
def _check_open(self):
|
||||||
"""Check if the device is open, raise OSError if not."""
|
"""Check if the device is open, raise ValueError if not."""
|
||||||
if not self._dev.isOpen():
|
if not self.dev.isOpen():
|
||||||
raise OSError("IO operation on closed device!")
|
raise ValueError("IO operation on closed device!")
|
||||||
|
|
||||||
def _check_random(self):
|
def _check_random(self):
|
||||||
"""Check if the device supports random access, raise OSError if not."""
|
"""Check if the device supports random access, raise OSError if not."""
|
||||||
if not self.seekable():
|
if not self.seekable():
|
||||||
raise OSError("Random access not allowed!")
|
raise OSError("Random access not allowed!")
|
||||||
|
|
||||||
|
def _check_readable(self):
|
||||||
|
"""Check if the device is readable, raise OSError if not."""
|
||||||
|
if not self.dev.isReadable():
|
||||||
|
raise OSError("Trying to read unreadable file!")
|
||||||
|
|
||||||
|
def _check_writable(self):
|
||||||
|
"""Check if the device is writable, raise OSError if not."""
|
||||||
|
if not self.writable():
|
||||||
|
raise OSError("Trying to write to unwritable file!")
|
||||||
|
|
||||||
|
def open(self, mode):
|
||||||
|
"""Open the underlying device and ensure opening succeeded.
|
||||||
|
|
||||||
|
Raises OSError if opening failed.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mode: QIODevice::OpenMode flags.
|
||||||
|
|
||||||
|
Return:
|
||||||
|
A contextlib.closing() object so this can be used as
|
||||||
|
contextmanager.
|
||||||
|
"""
|
||||||
|
ok = self.dev.open(mode)
|
||||||
|
if not ok:
|
||||||
|
raise OSError(self.dev.errorString())
|
||||||
|
return contextlib.closing(self)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""Close the underlying device."""
|
||||||
|
self.dev.close()
|
||||||
|
|
||||||
def fileno(self):
|
def fileno(self):
|
||||||
raise io.UnsupportedOperation
|
raise io.UnsupportedOperation
|
||||||
|
|
||||||
@ -249,85 +281,102 @@ class PyQIODevice(io.BufferedIOBase):
|
|||||||
self._check_open()
|
self._check_open()
|
||||||
self._check_random()
|
self._check_random()
|
||||||
if whence == io.SEEK_SET:
|
if whence == io.SEEK_SET:
|
||||||
ok = self._dev.seek(offset)
|
ok = self.dev.seek(offset)
|
||||||
elif whence == io.SEEK_CUR:
|
elif whence == io.SEEK_CUR:
|
||||||
ok = self._dev.seek(self.tell() + offset)
|
ok = self.dev.seek(self.tell() + offset)
|
||||||
elif whence == io.SEEK_END:
|
elif whence == io.SEEK_END:
|
||||||
ok = self._dev.seek(len(self) + offset)
|
ok = self.dev.seek(len(self) + offset)
|
||||||
else:
|
else:
|
||||||
raise io.UnsupportedOperation("whence = {} is not "
|
raise io.UnsupportedOperation("whence = {} is not "
|
||||||
"supported!".format(whence))
|
"supported!".format(whence))
|
||||||
if not ok:
|
if not ok:
|
||||||
raise OSError(self._dev.errorString())
|
raise OSError("seek failed!")
|
||||||
|
|
||||||
def truncate(self, size=None): # pylint: disable=unused-argument
|
def truncate(self, size=None): # pylint: disable=unused-argument
|
||||||
raise io.UnsupportedOperation
|
raise io.UnsupportedOperation
|
||||||
|
|
||||||
def close(self):
|
|
||||||
self._dev.close()
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def closed(self):
|
def closed(self):
|
||||||
return not self._dev.isOpen()
|
return not self.dev.isOpen()
|
||||||
|
|
||||||
def flush(self):
|
def flush(self):
|
||||||
self._check_open()
|
self._check_open()
|
||||||
self._dev.waitForBytesWritten(-1)
|
self.dev.waitForBytesWritten(-1)
|
||||||
|
|
||||||
def isatty(self):
|
def isatty(self):
|
||||||
self._check_open()
|
self._check_open()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def readable(self):
|
def readable(self):
|
||||||
return self._dev.isReadable()
|
return self.dev.isReadable()
|
||||||
|
|
||||||
def readline(self, size=-1):
|
def readline(self, size=-1):
|
||||||
self._check_open()
|
self._check_open()
|
||||||
if size == -1:
|
self._check_readable()
|
||||||
size = 0
|
|
||||||
return self._dev.readLine(size)
|
if size < 0:
|
||||||
|
qt_size = 0 # no maximum size
|
||||||
|
elif size == 0:
|
||||||
|
return QByteArray()
|
||||||
|
else:
|
||||||
|
qt_size = size + 1 # Qt also counts the NUL byte
|
||||||
|
|
||||||
|
if self.dev.canReadLine():
|
||||||
|
buf = self.dev.readLine(qt_size)
|
||||||
|
else:
|
||||||
|
if size < 0:
|
||||||
|
buf = self.dev.readAll()
|
||||||
|
else:
|
||||||
|
buf = self.dev.read(size)
|
||||||
|
|
||||||
|
if buf is None:
|
||||||
|
raise OSError(self.dev.errorString())
|
||||||
|
return buf
|
||||||
|
|
||||||
def seekable(self):
|
def seekable(self):
|
||||||
return not self._dev.isSequential()
|
return not self.dev.isSequential()
|
||||||
|
|
||||||
def tell(self):
|
def tell(self):
|
||||||
self._check_open()
|
self._check_open()
|
||||||
self._check_random()
|
self._check_random()
|
||||||
return self._dev.pos()
|
return self.dev.pos()
|
||||||
|
|
||||||
def writable(self):
|
def writable(self):
|
||||||
return self._dev.isWritable()
|
return self.dev.isWritable()
|
||||||
|
|
||||||
def readinto(self, b):
|
|
||||||
self._check_open()
|
|
||||||
return self._dev.read(b, len(b))
|
|
||||||
|
|
||||||
def write(self, b):
|
def write(self, b):
|
||||||
self._check_open()
|
self._check_open()
|
||||||
num = self._dev.write(b)
|
self._check_writable()
|
||||||
|
num = self.dev.write(b)
|
||||||
if num == -1 or num < len(b):
|
if num == -1 or num < len(b):
|
||||||
raise OSError(self._dev.errorString())
|
raise OSError(self.dev.errorString())
|
||||||
return num
|
return num
|
||||||
|
|
||||||
def read(self, size):
|
def read(self, size=-1):
|
||||||
self._check_open()
|
self._check_open()
|
||||||
buf = bytes()
|
self._check_readable()
|
||||||
num = self._dev.read(buf, size)
|
if size < 0:
|
||||||
if num == -1:
|
buf = self.dev.readAll()
|
||||||
raise OSError(self._dev.errorString())
|
else:
|
||||||
return num
|
buf = self.dev.read(size)
|
||||||
|
if buf is None:
|
||||||
|
raise OSError(self.dev.errorString())
|
||||||
|
return buf
|
||||||
|
|
||||||
|
|
||||||
class QtValueError(ValueError):
|
class QtValueError(ValueError):
|
||||||
|
|
||||||
"""Exception which gets raised by ensure_valid."""
|
"""Exception which gets raised by ensure_valid."""
|
||||||
|
|
||||||
def __init__(self, obj):
|
def __init__(self, obj, null=False):
|
||||||
try:
|
try:
|
||||||
self.reason = obj.errorString()
|
self.reason = obj.errorString()
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
self.reason = None
|
self.reason = None
|
||||||
err = "{} is not valid".format(obj)
|
if null:
|
||||||
|
err = "{} is null".format(obj)
|
||||||
|
else:
|
||||||
|
err = "{} is not valid".format(obj)
|
||||||
if self.reason:
|
if self.reason:
|
||||||
err += ": {}".format(self.reason)
|
err += ": {}".format(self.reason)
|
||||||
super().__init__(err)
|
super().__init__(err)
|
||||||
|
@ -80,10 +80,26 @@ def _from_args(typ, args):
|
|||||||
path: The overridden path, or None to turn off storage.
|
path: The overridden path, or None to turn off storage.
|
||||||
"""
|
"""
|
||||||
typ_to_argparse_arg = {
|
typ_to_argparse_arg = {
|
||||||
QStandardPaths.ConfigLocation: 'confdir'
|
QStandardPaths.ConfigLocation: 'confdir',
|
||||||
|
QStandardPaths.DataLocation: 'datadir',
|
||||||
|
QStandardPaths.CacheLocation: 'cachedir',
|
||||||
}
|
}
|
||||||
|
basedir_suffix = {
|
||||||
|
QStandardPaths.ConfigLocation: 'config',
|
||||||
|
QStandardPaths.DataLocation: 'data',
|
||||||
|
QStandardPaths.CacheLocation: 'cache',
|
||||||
|
QStandardPaths.DownloadLocation: 'download',
|
||||||
|
QStandardPaths.RuntimeLocation: 'runtime',
|
||||||
|
}
|
||||||
|
|
||||||
if args is None:
|
if args is None:
|
||||||
return (False, None)
|
return (False, None)
|
||||||
|
|
||||||
|
if getattr(args, 'basedir', None) is not None:
|
||||||
|
basedir = args.basedir
|
||||||
|
suffix = basedir_suffix[typ]
|
||||||
|
return (True, os.path.join(basedir, suffix))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
argname = typ_to_argparse_arg[typ]
|
argname = typ_to_argparse_arg[typ]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
@ -135,8 +151,18 @@ def init(args):
|
|||||||
"""Initialize all standard dirs."""
|
"""Initialize all standard dirs."""
|
||||||
global _args
|
global _args
|
||||||
_args = args
|
_args = args
|
||||||
# http://www.brynosaurus.com/cachedir/spec.html
|
_init_cachedir_tag()
|
||||||
cachedir_tag = os.path.join(cache(), 'CACHEDIR.TAG')
|
|
||||||
|
|
||||||
|
def _init_cachedir_tag():
|
||||||
|
"""Create CACHEDIR.TAG if it doesn't exist.
|
||||||
|
|
||||||
|
See http://www.brynosaurus.com/cachedir/spec.html
|
||||||
|
"""
|
||||||
|
cache_dir = cache()
|
||||||
|
if cache_dir is None:
|
||||||
|
return
|
||||||
|
cachedir_tag = os.path.join(cache_dir, 'CACHEDIR.TAG')
|
||||||
if not os.path.exists(cachedir_tag):
|
if not os.path.exists(cachedir_tag):
|
||||||
try:
|
try:
|
||||||
with open(cachedir_tag, 'w', encoding='utf-8') as f:
|
with open(cachedir_tag, 'w', encoding='utf-8') as f:
|
||||||
|
@ -29,7 +29,7 @@ from PyQt5.QtCore import QUrl
|
|||||||
from PyQt5.QtNetwork import QHostInfo, QHostAddress
|
from PyQt5.QtNetwork import QHostInfo, QHostAddress
|
||||||
|
|
||||||
from qutebrowser.config import config, configexc
|
from qutebrowser.config import config, configexc
|
||||||
from qutebrowser.utils import log, qtutils, message
|
from qutebrowser.utils import log, qtutils, message, utils
|
||||||
from qutebrowser.commands import cmdexc
|
from qutebrowser.commands import cmdexc
|
||||||
|
|
||||||
|
|
||||||
@ -74,8 +74,7 @@ def _get_search_url(txt):
|
|||||||
"""
|
"""
|
||||||
log.url.debug("Finding search engine for '{}'".format(txt))
|
log.url.debug("Finding search engine for '{}'".format(txt))
|
||||||
engine, term = _parse_search_term(txt)
|
engine, term = _parse_search_term(txt)
|
||||||
if not term:
|
assert term
|
||||||
raise FuzzyUrlError("No search term given")
|
|
||||||
if engine is None:
|
if engine is None:
|
||||||
template = config.get('searchengines', 'DEFAULT')
|
template = config.get('searchengines', 'DEFAULT')
|
||||||
else:
|
else:
|
||||||
@ -95,11 +94,9 @@ def _is_url_naive(urlstr):
|
|||||||
True if the URL really is a URL, False otherwise.
|
True if the URL really is a URL, False otherwise.
|
||||||
"""
|
"""
|
||||||
url = qurl_from_user_input(urlstr)
|
url = qurl_from_user_input(urlstr)
|
||||||
try:
|
assert url.isValid()
|
||||||
ipaddress.ip_address(urlstr)
|
|
||||||
except ValueError:
|
if not utils.raises(ValueError, ipaddress.ip_address, urlstr):
|
||||||
pass
|
|
||||||
else:
|
|
||||||
# Valid IPv4/IPv6 address
|
# Valid IPv4/IPv6 address
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -109,31 +106,36 @@ def _is_url_naive(urlstr):
|
|||||||
if not QHostAddress(urlstr).isNull():
|
if not QHostAddress(urlstr).isNull():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if not url.isValid():
|
if '.' in url.host():
|
||||||
return False
|
|
||||||
elif '.' in url.host():
|
|
||||||
return True
|
|
||||||
elif url.host() == 'localhost':
|
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _is_url_dns(url):
|
def _is_url_dns(urlstr):
|
||||||
"""Check if a URL is really a URL via DNS.
|
"""Check if a URL is really a URL via DNS.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
url: The URL to check for as QUrl, ideally via qurl_from_user_input.
|
url: The URL to check for as a string.
|
||||||
|
|
||||||
Return:
|
Return:
|
||||||
True if the URL really is a URL, False otherwise.
|
True if the URL really is a URL, False otherwise.
|
||||||
"""
|
"""
|
||||||
if not url.isValid():
|
url = qurl_from_user_input(urlstr)
|
||||||
|
assert url.isValid()
|
||||||
|
|
||||||
|
if (utils.raises(ValueError, ipaddress.ip_address, urlstr) and
|
||||||
|
not QHostAddress(urlstr).isNull()):
|
||||||
|
log.url.debug("Bogus IP URL -> False")
|
||||||
|
# Qt treats things like "23.42" or "1337" or "0xDEAD" as valid URLs
|
||||||
|
# which we don't want to.
|
||||||
return False
|
return False
|
||||||
|
|
||||||
host = url.host()
|
host = url.host()
|
||||||
log.url.debug("DNS request for {}".format(host))
|
|
||||||
if not host:
|
if not host:
|
||||||
|
log.url.debug("URL has no host -> False")
|
||||||
return False
|
return False
|
||||||
|
log.url.debug("Doing DNS request for {}".format(host))
|
||||||
info = QHostInfo.fromName(host)
|
info = QHostInfo.fromName(host)
|
||||||
return not info.error()
|
return not info.error()
|
||||||
|
|
||||||
@ -230,6 +232,7 @@ def is_url(urlstr):
|
|||||||
|
|
||||||
urlstr = urlstr.strip()
|
urlstr = urlstr.strip()
|
||||||
qurl = QUrl(urlstr)
|
qurl = QUrl(urlstr)
|
||||||
|
qurl_userinput = qurl_from_user_input(urlstr)
|
||||||
|
|
||||||
if not autosearch:
|
if not autosearch:
|
||||||
# no autosearch, so everything is a URL unless it has an explicit
|
# no autosearch, so everything is a URL unless it has an explicit
|
||||||
@ -240,29 +243,33 @@ def is_url(urlstr):
|
|||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
if not qurl_userinput.isValid():
|
||||||
|
# This will also catch URLs containing spaces.
|
||||||
|
return False
|
||||||
|
|
||||||
if _has_explicit_scheme(qurl):
|
if _has_explicit_scheme(qurl):
|
||||||
# URLs with explicit schemes are always URLs
|
# URLs with explicit schemes are always URLs
|
||||||
log.url.debug("Contains explicit scheme")
|
log.url.debug("Contains explicit scheme")
|
||||||
url = True
|
url = True
|
||||||
elif ' ' in urlstr:
|
elif qurl_userinput.host() in ('localhost', '127.0.0.1', '::1'):
|
||||||
# A URL will never contain a space
|
log.url.debug("Is localhost.")
|
||||||
log.url.debug("Contains space -> no URL")
|
url = True
|
||||||
url = False
|
|
||||||
elif is_special_url(qurl):
|
elif is_special_url(qurl):
|
||||||
# Special URLs are always URLs, even with autosearch=False
|
# Special URLs are always URLs, even with autosearch=False
|
||||||
log.url.debug("Is an special URL.")
|
log.url.debug("Is an special URL.")
|
||||||
url = True
|
url = True
|
||||||
elif autosearch == 'dns':
|
elif autosearch == 'dns':
|
||||||
log.url.debug("Checking via DNS")
|
log.url.debug("Checking via DNS check")
|
||||||
# We want to use qurl_from_user_input here, as the user might enter
|
# We want to use qurl_from_user_input here, as the user might enter
|
||||||
# "foo.de" and that should be treated as URL here.
|
# "foo.de" and that should be treated as URL here.
|
||||||
url = _is_url_dns(qurl_from_user_input(urlstr))
|
url = _is_url_dns(urlstr)
|
||||||
elif autosearch == 'naive':
|
elif autosearch == 'naive':
|
||||||
log.url.debug("Checking via naive check")
|
log.url.debug("Checking via naive check")
|
||||||
url = _is_url_naive(urlstr)
|
url = _is_url_naive(urlstr)
|
||||||
else:
|
else:
|
||||||
raise ValueError("Invalid autosearch value")
|
raise ValueError("Invalid autosearch value")
|
||||||
return url and qurl_from_user_input(urlstr).isValid()
|
log.url.debug("url = {}".format(url))
|
||||||
|
return url
|
||||||
|
|
||||||
|
|
||||||
def qurl_from_user_input(urlstr):
|
def qurl_from_user_input(urlstr):
|
||||||
@ -311,20 +318,15 @@ def invalid_url_error(win_id, url, action):
|
|||||||
if url.isValid():
|
if url.isValid():
|
||||||
raise ValueError("Calling invalid_url_error with valid URL {}".format(
|
raise ValueError("Calling invalid_url_error with valid URL {}".format(
|
||||||
url.toDisplayString()))
|
url.toDisplayString()))
|
||||||
errstring = "Trying to {} with invalid URL".format(action)
|
errstring = get_errstring(
|
||||||
if url.errorString():
|
url, "Trying to {} with invalid URL".format(action))
|
||||||
errstring += " - {}".format(url.errorString())
|
|
||||||
message.error(win_id, errstring)
|
message.error(win_id, errstring)
|
||||||
|
|
||||||
|
|
||||||
def raise_cmdexc_if_invalid(url):
|
def raise_cmdexc_if_invalid(url):
|
||||||
"""Check if the given QUrl is invalid, and if so, raise a CommandError."""
|
"""Check if the given QUrl is invalid, and if so, raise a CommandError."""
|
||||||
if not url.isValid():
|
if not url.isValid():
|
||||||
errstr = "Invalid URL {}".format(url.toDisplayString())
|
raise cmdexc.CommandError(get_errstring(url))
|
||||||
url_error = url.errorString()
|
|
||||||
if url_error:
|
|
||||||
errstr += " - {}".format(url_error)
|
|
||||||
raise cmdexc.CommandError(errstr)
|
|
||||||
|
|
||||||
|
|
||||||
def filename_from_url(url):
|
def filename_from_url(url):
|
||||||
@ -348,11 +350,46 @@ def filename_from_url(url):
|
|||||||
|
|
||||||
|
|
||||||
def host_tuple(url):
|
def host_tuple(url):
|
||||||
"""Get a (scheme, host, port) tuple.
|
"""Get a (scheme, host, port) tuple from a QUrl.
|
||||||
|
|
||||||
This is suitable to identify a connection, e.g. for SSL errors.
|
This is suitable to identify a connection, e.g. for SSL errors.
|
||||||
"""
|
"""
|
||||||
return (url.scheme(), url.host(), url.port())
|
if not url.isValid():
|
||||||
|
raise ValueError(get_errstring(url))
|
||||||
|
scheme, host, port = url.scheme(), url.host(), url.port()
|
||||||
|
assert scheme
|
||||||
|
if not host:
|
||||||
|
raise ValueError("Got URL {} without host.".format(
|
||||||
|
url.toDisplayString()))
|
||||||
|
if port == -1:
|
||||||
|
port_mapping = {
|
||||||
|
'http': 80,
|
||||||
|
'https': 443,
|
||||||
|
'ftp': 21,
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
port = port_mapping[scheme]
|
||||||
|
except KeyError:
|
||||||
|
raise ValueError("Got URL {} with unknown port.".format(
|
||||||
|
url.toDisplayString()))
|
||||||
|
return scheme, host, port
|
||||||
|
|
||||||
|
|
||||||
|
def get_errstring(url, base="Invalid URL"):
|
||||||
|
"""Get an error string for an URL.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
url: The URL as a QUrl.
|
||||||
|
base: The base error string.
|
||||||
|
|
||||||
|
Return:
|
||||||
|
A new string with url.errorString() is appended if available.
|
||||||
|
"""
|
||||||
|
url_error = url.errorString()
|
||||||
|
if url_error:
|
||||||
|
return base + " - {}".format(url_error)
|
||||||
|
else:
|
||||||
|
return base
|
||||||
|
|
||||||
|
|
||||||
class FuzzyUrlError(Exception):
|
class FuzzyUrlError(Exception):
|
||||||
@ -360,17 +397,19 @@ class FuzzyUrlError(Exception):
|
|||||||
"""Exception raised by fuzzy_url on problems.
|
"""Exception raised by fuzzy_url on problems.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
|
msg: The error message to use.
|
||||||
url: The QUrl which caused the error.
|
url: The QUrl which caused the error.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, msg, url=None):
|
def __init__(self, msg, url=None):
|
||||||
super().__init__(msg)
|
super().__init__(msg)
|
||||||
if url is not None:
|
if url is not None and url.isValid():
|
||||||
assert not url.isValid()
|
raise ValueError("Got valid URL {}!".format(url.toDisplayString()))
|
||||||
self.url = url
|
self.url = url
|
||||||
|
self.msg = msg
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
if self.url is None or not self.url.errorString():
|
if self.url is None or not self.url.errorString():
|
||||||
return str(super())
|
return self.msg
|
||||||
else:
|
else:
|
||||||
return '{}: {}'.format(str(super()), self.url.errorString())
|
return '{}: {}'.format(self.msg, self.url.errorString())
|
||||||
|
@ -231,7 +231,7 @@ ClickTarget = enum('ClickTarget', ['normal', 'tab', 'tab_bg', 'window'])
|
|||||||
|
|
||||||
# Key input modes
|
# Key input modes
|
||||||
KeyMode = enum('KeyMode', ['normal', 'hint', 'command', 'yesno', 'prompt',
|
KeyMode = enum('KeyMode', ['normal', 'hint', 'command', 'yesno', 'prompt',
|
||||||
'insert', 'passthrough'])
|
'insert', 'passthrough', 'caret'])
|
||||||
|
|
||||||
|
|
||||||
# Available command completions
|
# Available command completions
|
||||||
@ -240,6 +240,11 @@ Completion = enum('Completion', ['command', 'section', 'option', 'value',
|
|||||||
'quickmark_by_name', 'url', 'sessions'])
|
'quickmark_by_name', 'url', 'sessions'])
|
||||||
|
|
||||||
|
|
||||||
|
# Exit statuses for errors. Needs to be an int for sys.exit.
|
||||||
|
Exit = enum('Exit', ['ok', 'reserved', 'exception', 'err_ipc', 'err_init',
|
||||||
|
'err_config', 'err_key_config'], is_int=True)
|
||||||
|
|
||||||
|
|
||||||
class Question(QObject):
|
class Question(QObject):
|
||||||
|
|
||||||
"""A question asked to the user, e.g. via the status bar.
|
"""A question asked to the user, e.g. via the status bar.
|
||||||
|
@ -326,17 +326,24 @@ def keyevent_to_string(e):
|
|||||||
A name of the key (combination) as a string or
|
A name of the key (combination) as a string or
|
||||||
None if only modifiers are pressed..
|
None if only modifiers are pressed..
|
||||||
"""
|
"""
|
||||||
modmask2str = collections.OrderedDict([
|
|
||||||
(Qt.ControlModifier, 'Ctrl'),
|
|
||||||
(Qt.AltModifier, 'Alt'),
|
|
||||||
(Qt.MetaModifier, 'Meta'),
|
|
||||||
(Qt.ShiftModifier, 'Shift'),
|
|
||||||
])
|
|
||||||
if sys.platform == 'darwin':
|
if sys.platform == 'darwin':
|
||||||
# FIXME verify this feels right on a real Mac as well.
|
# Qt swaps Ctrl/Meta on OS X, so we switch it back here so the user can
|
||||||
# In my Virtualbox VM, the Ctrl key shows up as meta.
|
# use it in the config as expected. See:
|
||||||
# https://github.com/The-Compiler/qutebrowser/issues/110
|
# https://github.com/The-Compiler/qutebrowser/issues/110
|
||||||
modmask2str[Qt.MetaModifier] = 'Ctrl'
|
# http://doc.qt.io/qt-5.4/osx-issues.html#special-keys
|
||||||
|
modmask2str = collections.OrderedDict([
|
||||||
|
(Qt.MetaModifier, 'Ctrl'),
|
||||||
|
(Qt.AltModifier, 'Alt'),
|
||||||
|
(Qt.ControlModifier, 'Meta'),
|
||||||
|
(Qt.ShiftModifier, 'Shift'),
|
||||||
|
])
|
||||||
|
else:
|
||||||
|
modmask2str = collections.OrderedDict([
|
||||||
|
(Qt.ControlModifier, 'Ctrl'),
|
||||||
|
(Qt.AltModifier, 'Alt'),
|
||||||
|
(Qt.MetaModifier, 'Meta'),
|
||||||
|
(Qt.ShiftModifier, 'Shift'),
|
||||||
|
])
|
||||||
modifiers = (Qt.Key_Control, Qt.Key_Alt, Qt.Key_Shift, Qt.Key_Meta,
|
modifiers = (Qt.Key_Control, Qt.Key_Alt, Qt.Key_Shift, Qt.Key_Meta,
|
||||||
Qt.Key_AltGr, Qt.Key_Super_L, Qt.Key_Super_R,
|
Qt.Key_AltGr, Qt.Key_Super_L, Qt.Key_Super_R,
|
||||||
Qt.Key_Hyper_L, Qt.Key_Hyper_R, Qt.Key_Direction_L,
|
Qt.Key_Hyper_L, Qt.Key_Hyper_R, Qt.Key_Direction_L,
|
||||||
@ -503,7 +510,8 @@ def get_repr(obj, constructor=False, **attrs):
|
|||||||
"""
|
"""
|
||||||
cls = qualname(obj.__class__)
|
cls = qualname(obj.__class__)
|
||||||
parts = []
|
parts = []
|
||||||
for name, val in attrs.items():
|
items = sorted(attrs.items())
|
||||||
|
for name, val in items:
|
||||||
parts.append('{}={!r}'.format(name, val))
|
parts.append('{}={!r}'.format(name, val))
|
||||||
if constructor:
|
if constructor:
|
||||||
return '{}({})'.format(cls, ', '.join(parts))
|
return '{}({})'.format(cls, ', '.join(parts))
|
||||||
|
@ -130,7 +130,7 @@ def _module_versions():
|
|||||||
try:
|
try:
|
||||||
import sipconfig # pylint: disable=import-error,unused-variable
|
import sipconfig # pylint: disable=import-error,unused-variable
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
lines.append('SIP: ?')
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
lines.append('SIP: {}'.format(
|
lines.append('SIP: {}'.format(
|
||||||
|
56
scripts/keytester.py
Normal file
56
scripts/keytester.py
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||||
|
|
||||||
|
# Copyright 2014-2015 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/>.
|
||||||
|
|
||||||
|
"""Small test script to show key presses.
|
||||||
|
|
||||||
|
Use python3 -m scripts.keytester to launch it.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QHBoxLayout
|
||||||
|
|
||||||
|
from qutebrowser.utils import utils
|
||||||
|
|
||||||
|
|
||||||
|
class KeyWidget(QWidget):
|
||||||
|
|
||||||
|
"""Widget displaying key presses."""
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self._layout = QHBoxLayout(self)
|
||||||
|
self._label = QLabel(text="Waiting for keypress...")
|
||||||
|
self._layout.addWidget(self._label)
|
||||||
|
|
||||||
|
def keyPressEvent(self, e):
|
||||||
|
"""Show pressed keys."""
|
||||||
|
lines = [
|
||||||
|
str(utils.keyevent_to_string(e)),
|
||||||
|
'',
|
||||||
|
'key: 0x{:x}'.format(int(e.key())),
|
||||||
|
'modifiers: 0x{:x}'.format(int(e.modifiers())),
|
||||||
|
'text: {!r}'.format(e.text()),
|
||||||
|
]
|
||||||
|
self._label.setText('\n'.join(lines))
|
||||||
|
|
||||||
|
|
||||||
|
app = QApplication([])
|
||||||
|
w = KeyWidget()
|
||||||
|
w.show()
|
||||||
|
app.exec_()
|
@ -70,13 +70,16 @@ def link_pyqt(sys_path, venv_path):
|
|||||||
if not globbed_sip:
|
if not globbed_sip:
|
||||||
raise Error("Did not find sip in {}!".format(sys_path))
|
raise Error("Did not find sip in {}!".format(sys_path))
|
||||||
|
|
||||||
files = ['PyQt5']
|
files = [('PyQt5', True), ('sipconfig.py', False)]
|
||||||
files += [os.path.basename(e) for e in globbed_sip]
|
files += [(os.path.basename(e), True) for e in globbed_sip]
|
||||||
for fn in files:
|
for fn, required in files:
|
||||||
source = os.path.join(sys_path, fn)
|
source = os.path.join(sys_path, fn)
|
||||||
dest = os.path.join(venv_path, fn)
|
dest = os.path.join(venv_path, fn)
|
||||||
if not os.path.exists(source):
|
if not os.path.exists(source):
|
||||||
raise FileNotFoundError(source)
|
if required:
|
||||||
|
raise FileNotFoundError(source)
|
||||||
|
else:
|
||||||
|
continue
|
||||||
if os.path.exists(dest):
|
if os.path.exists(dest):
|
||||||
if os.path.isdir(dest) and not os.path.islink(dest):
|
if os.path.isdir(dest) and not os.path.islink(dest):
|
||||||
shutil.rmtree(dest)
|
shutil.rmtree(dest)
|
||||||
|
@ -39,18 +39,29 @@ if '--profile-keep' in sys.argv:
|
|||||||
profilefile = os.path.join(os.getcwd(), 'profile')
|
profilefile = os.path.join(os.getcwd(), 'profile')
|
||||||
else:
|
else:
|
||||||
profilefile = os.path.join(tempdir, 'profile')
|
profilefile = os.path.join(tempdir, 'profile')
|
||||||
|
|
||||||
if '--profile-noconv' in sys.argv:
|
if '--profile-noconv' in sys.argv:
|
||||||
sys.argv.remove('--profile-noconv')
|
sys.argv.remove('--profile-noconv')
|
||||||
noconv = True
|
noconv = True
|
||||||
else:
|
else:
|
||||||
noconv = False
|
noconv = False
|
||||||
|
|
||||||
|
if '--profile-dot' in sys.argv:
|
||||||
|
sys.argv.remove('--profile-dot')
|
||||||
|
dot = True
|
||||||
|
else:
|
||||||
|
dot = False
|
||||||
|
|
||||||
callgraphfile = os.path.join(tempdir, 'callgraph')
|
callgraphfile = os.path.join(tempdir, 'callgraph')
|
||||||
profiler = cProfile.Profile()
|
profiler = cProfile.Profile()
|
||||||
profiler.run('qutebrowser.qutebrowser.main()')
|
profiler.run('qutebrowser.qutebrowser.main()')
|
||||||
profiler.dump_stats(profilefile)
|
profiler.dump_stats(profilefile)
|
||||||
|
|
||||||
if not noconv:
|
if not noconv:
|
||||||
subprocess.call(['pyprof2calltree', '-k', '-i', profilefile,
|
if dot:
|
||||||
'-o', callgraphfile])
|
subprocess.call('gprof2dot -f pstats profile | dot -Tpng | feh -F -',
|
||||||
|
shell=True) # yep, shell=True. I know what I'm doing.
|
||||||
|
else:
|
||||||
|
subprocess.call(['pyprof2calltree', '-k', '-i', profilefile,
|
||||||
|
'-o', callgraphfile])
|
||||||
shutil.rmtree(tempdir)
|
shutil.rmtree(tempdir)
|
||||||
|
@ -211,7 +211,11 @@ def _get_command_doc_count(cmd, parser):
|
|||||||
if cmd.count_arg is not None:
|
if cmd.count_arg is not None:
|
||||||
yield ""
|
yield ""
|
||||||
yield "==== count"
|
yield "==== count"
|
||||||
yield parser.arg_descs[cmd.count_arg]
|
try:
|
||||||
|
yield parser.arg_descs[cmd.count_arg]
|
||||||
|
except KeyError:
|
||||||
|
raise KeyError("No description for count arg {!r} of command "
|
||||||
|
"{!r}!".format(cmd.count_arg, cmd.name))
|
||||||
|
|
||||||
|
|
||||||
def _get_command_doc_notes(cmd):
|
def _get_command_doc_notes(cmd):
|
||||||
|
@ -378,10 +378,10 @@ class TestIsEditable:
|
|||||||
webelem.config = old_config
|
webelem.config = old_config
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def stubbed_config(self, config_stub, mocker):
|
def stubbed_config(self, config_stub, monkeypatch):
|
||||||
"""Fixture to create a config stub with an input section."""
|
"""Fixture to create a config stub with an input section."""
|
||||||
config_stub.data = {'input': {}}
|
config_stub.data = {'input': {}}
|
||||||
mocker.patch('qutebrowser.browser.webelem.config', new=config_stub)
|
monkeypatch.setattr('qutebrowser.browser.webelem.config', config_stub)
|
||||||
return config_stub
|
return config_stub
|
||||||
|
|
||||||
def test_input_plain(self):
|
def test_input_plain(self):
|
||||||
|
@ -177,6 +177,60 @@ class TestKeyConfigParser:
|
|||||||
with pytest.raises(keyconf.KeyConfigError):
|
with pytest.raises(keyconf.KeyConfigError):
|
||||||
kcp._read_command(cmdline_test.cmd)
|
kcp._read_command(cmdline_test.cmd)
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('rgx', [rgx for rgx, _repl
|
||||||
|
in configdata.CHANGED_KEY_COMMANDS])
|
||||||
|
def test_default_config_no_deprecated(self, rgx):
|
||||||
|
"""Make sure the default config contains no deprecated commands."""
|
||||||
|
for sect in configdata.KEY_DATA.values():
|
||||||
|
for command in sect:
|
||||||
|
assert rgx.match(command) is None
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
'old, new_expected',
|
||||||
|
[
|
||||||
|
('open -t about:blank', 'open -t'),
|
||||||
|
('open -w about:blank', 'open -w'),
|
||||||
|
('open -b about:blank', 'open -b'),
|
||||||
|
('open about:blank', None),
|
||||||
|
('open -t example.com', None),
|
||||||
|
|
||||||
|
('download-page', 'download'),
|
||||||
|
('cancel-download', 'download-cancel'),
|
||||||
|
|
||||||
|
('search ""', 'search'),
|
||||||
|
("search ''", 'search'),
|
||||||
|
('search "foo"', None),
|
||||||
|
|
||||||
|
('set-cmd-text "foo bar"', 'set-cmd-text foo bar'),
|
||||||
|
("set-cmd-text 'foo bar'", 'set-cmd-text foo bar'),
|
||||||
|
('set-cmd-text foo bar', None),
|
||||||
|
('set-cmd-text "foo bar "', 'set-cmd-text -s foo bar'),
|
||||||
|
("set-cmd-text 'foo bar '", 'set-cmd-text -s foo bar'),
|
||||||
|
|
||||||
|
('hint links rapid', 'hint --rapid links tab-bg'),
|
||||||
|
('hint links rapid-win', 'hint --rapid links window'),
|
||||||
|
|
||||||
|
('scroll -50 0', 'scroll left'),
|
||||||
|
('scroll 0 50', 'scroll down'),
|
||||||
|
('scroll 0 -50', 'scroll up'),
|
||||||
|
('scroll 50 0', 'scroll right'),
|
||||||
|
('scroll -50 10', 'scroll-px -50 10'),
|
||||||
|
('scroll 50 50', 'scroll-px 50 50'),
|
||||||
|
('scroll 0 0', 'scroll-px 0 0'),
|
||||||
|
('scroll 23 42', 'scroll-px 23 42'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
def test_migrations(self, old, new_expected):
|
||||||
|
"""Make sure deprecated commands get migrated correctly."""
|
||||||
|
if new_expected is None:
|
||||||
|
new_expected = old
|
||||||
|
new = old
|
||||||
|
for rgx, repl in configdata.CHANGED_KEY_COMMANDS:
|
||||||
|
if rgx.match(new):
|
||||||
|
new = rgx.sub(repl, new)
|
||||||
|
break
|
||||||
|
assert new == new_expected
|
||||||
|
|
||||||
|
|
||||||
class TestDefaultConfig:
|
class TestDefaultConfig:
|
||||||
|
|
||||||
@ -221,7 +275,7 @@ class TestConfigInit:
|
|||||||
|
|
||||||
def test_config_none(self, monkeypatch):
|
def test_config_none(self, monkeypatch):
|
||||||
"""Test initializing with config path set to None."""
|
"""Test initializing with config path set to None."""
|
||||||
args = types.SimpleNamespace(confdir='')
|
args = types.SimpleNamespace(confdir='', datadir='', cachedir='')
|
||||||
for k, v in self.env.items():
|
for k, v in self.env.items():
|
||||||
monkeypatch.setenv(k, v)
|
monkeypatch.setenv(k, v)
|
||||||
standarddir.init(args)
|
standarddir.init(args)
|
||||||
|
@ -883,11 +883,12 @@ class TestCommand:
|
|||||||
"""Test Command."""
|
"""Test Command."""
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def setup(self, mocker, stubs):
|
def setup(self, monkeypatch, stubs):
|
||||||
self.t = configtypes.Command()
|
self.t = configtypes.Command()
|
||||||
cmd_utils = stubs.FakeCmdUtils({'cmd1': stubs.FakeCommand("desc 1"),
|
cmd_utils = stubs.FakeCmdUtils({'cmd1': stubs.FakeCommand("desc 1"),
|
||||||
'cmd2': stubs.FakeCommand("desc 2")})
|
'cmd2': stubs.FakeCommand("desc 2")})
|
||||||
mocker.patch('qutebrowser.config.configtypes.cmdutils', new=cmd_utils)
|
monkeypatch.setattr('qutebrowser.config.configtypes.cmdutils',
|
||||||
|
cmd_utils)
|
||||||
|
|
||||||
def test_validate_empty(self):
|
def test_validate_empty(self):
|
||||||
"""Test validate with an empty string."""
|
"""Test validate with an empty string."""
|
||||||
|
23
tests/javascript/base.html
Normal file
23
tests/javascript/base.html
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<!--
|
||||||
|
vim: ft=html fileencoding=utf-8 sts=4 sw=4 et:
|
||||||
|
-->
|
||||||
|
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>qutebrowser javascript test</title>
|
||||||
|
<style type="text/css">
|
||||||
|
{% block style %}
|
||||||
|
body {
|
||||||
|
font-size: 50px;
|
||||||
|
line-height: 70px;
|
||||||
|
}
|
||||||
|
{% endblock %}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{% block content %}
|
||||||
|
{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
141
tests/javascript/conftest.py
Normal file
141
tests/javascript/conftest.py
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||||
|
|
||||||
|
# Copyright 2015 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/>.
|
||||||
|
|
||||||
|
"""pylint conftest file for javascript test."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import os.path
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import jinja2
|
||||||
|
from PyQt5.QtWebKit import QWebSettings
|
||||||
|
from PyQt5.QtWebKitWidgets import QWebView, QWebPage
|
||||||
|
|
||||||
|
import qutebrowser
|
||||||
|
|
||||||
|
|
||||||
|
class TestWebPage(QWebPage):
|
||||||
|
|
||||||
|
"""QWebPage subclass which overrides some test methods.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
_logger: The logger used for alerts.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self._logger = logging.getLogger('js-tests')
|
||||||
|
|
||||||
|
def javaScriptAlert(self, _frame, msg):
|
||||||
|
"""Log javascript alerts."""
|
||||||
|
self._logger.info("js alert: {}".format(msg))
|
||||||
|
|
||||||
|
def javaScriptConfirm(self, _frame, msg):
|
||||||
|
"""Fail tests on js confirm() as that should never happen."""
|
||||||
|
pytest.fail("js confirm: {}".format(msg))
|
||||||
|
|
||||||
|
def javaScriptPrompt(self, _frame, msg, _default):
|
||||||
|
"""Fail tests on js prompt() as that should never happen."""
|
||||||
|
pytest.fail("js prompt: {}".format(msg))
|
||||||
|
|
||||||
|
def javaScriptConsoleMessage(self, msg, line, source):
|
||||||
|
"""Fail tests on js console messages as they're used for errors."""
|
||||||
|
pytest.fail("js console ({}:{}): {}".format(source, line, msg))
|
||||||
|
|
||||||
|
|
||||||
|
class JSTester:
|
||||||
|
|
||||||
|
"""Object returned by js_tester which provides test data and a webview.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
webview: The webview which is used.
|
||||||
|
_qtbot: The QtBot fixture from pytest-qt.
|
||||||
|
_jinja_env: The jinja2 environment used to get templates.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, webview, qtbot):
|
||||||
|
self.webview = webview
|
||||||
|
self.webview.setPage(TestWebPage(self.webview))
|
||||||
|
self._qtbot = qtbot
|
||||||
|
loader = jinja2.FileSystemLoader(os.path.dirname(__file__))
|
||||||
|
self._jinja_env = jinja2.Environment(loader=loader, autoescape=True)
|
||||||
|
|
||||||
|
def scroll_anchor(self, name):
|
||||||
|
"""Scroll the main frame to the given anchor."""
|
||||||
|
page = self.webview.page()
|
||||||
|
with self._qtbot.waitSignal(page.scrollRequested):
|
||||||
|
page.mainFrame().scrollToAnchor(name)
|
||||||
|
|
||||||
|
def load(self, path, **kwargs):
|
||||||
|
"""Load and display the given test data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: The path to the test file, relative to the javascript/
|
||||||
|
folder.
|
||||||
|
**kwargs: Passed to jinja's template.render().
|
||||||
|
"""
|
||||||
|
template = self._jinja_env.get_template(path)
|
||||||
|
with self._qtbot.waitSignal(self.webview.loadFinished):
|
||||||
|
self.webview.setHtml(template.render(**kwargs))
|
||||||
|
|
||||||
|
def run_file(self, filename):
|
||||||
|
"""Run a javascript file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filename: The javascript filename, relative to
|
||||||
|
qutebrowser/javascript.
|
||||||
|
|
||||||
|
Return:
|
||||||
|
The javascript return value.
|
||||||
|
"""
|
||||||
|
base_path = os.path.join(os.path.dirname(qutebrowser.__file__),
|
||||||
|
'javascript')
|
||||||
|
full_path = os.path.join(base_path, filename)
|
||||||
|
with open(full_path, 'r', encoding='utf-8') as f:
|
||||||
|
source = f.read()
|
||||||
|
return self.run(source)
|
||||||
|
|
||||||
|
def run(self, source):
|
||||||
|
"""Run the given javascript source.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
source: The source to run as a string.
|
||||||
|
|
||||||
|
Return:
|
||||||
|
The javascript return value.
|
||||||
|
"""
|
||||||
|
assert self.webview.settings().testAttribute(
|
||||||
|
QWebSettings.JavascriptEnabled)
|
||||||
|
return self.webview.page().mainFrame().evaluateJavaScript(source)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def js_tester(qtbot):
|
||||||
|
"""Fixture to test javascript snippets.
|
||||||
|
|
||||||
|
Provides a QWebView with a 640x480px size and a JSTester instance.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
qtbot: pytestqt.plugin.QtBot fixture.
|
||||||
|
"""
|
||||||
|
webview = QWebView()
|
||||||
|
qtbot.add_widget(webview)
|
||||||
|
webview.resize(640, 480)
|
||||||
|
return JSTester(webview, qtbot)
|
5
tests/javascript/position_caret/invisible.html
Normal file
5
tests/javascript/position_caret/invisible.html
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<p style="{{style}}">This line is hidden.</p>
|
||||||
|
<p>MARKER this should be the paragraph the caret is on.</p>
|
||||||
|
{% endblock %}
|
8
tests/javascript/position_caret/scrolled_down.html
Normal file
8
tests/javascript/position_caret/scrolled_down.html
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer nec odio. Praesent libero. Sed cursus ante dapibus diam. Sed nisi. Nulla quis sem at nibh elementum imperdiet. Duis sagittis ipsum. Praesent mauris. Fusce nec tellus sed augue semper porta. Mauris massa. Vestibulum lacinia arcu eget nulla. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Curabitur sodales ligula in libero. </p>
|
||||||
|
|
||||||
|
<a id="anchor" /><p>MARKER this should be the paragraph the caret is on.</p>
|
||||||
|
|
||||||
|
<p>Some more text</p>
|
||||||
|
{% endblock %}
|
9
tests/javascript/position_caret/scrolled_down_img.html
Normal file
9
tests/javascript/position_caret/scrolled_down_img.html
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer nec odio. Praesent libero. Sed cursus ante dapibus diam. Sed nisi. Nulla quis sem at nibh elementum imperdiet. Duis sagittis ipsum. Praesent mauris. Fusce nec tellus sed augue semper porta. Mauris massa. Vestibulum lacinia arcu eget nulla. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Curabitur sodales ligula in libero. </p>
|
||||||
|
|
||||||
|
<a id="anchor" /><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3wUMEQsSoCNVVgAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAAFklEQVQoz2P8z0AaYGIY1TCqYdhqAABALgEfsZDCTQAAAABJRU5ErkJggg==" />
|
||||||
|
|
||||||
|
<p>MARKER this should be the paragraph the caret is on.</p>
|
||||||
|
<p>Some more text</p>
|
||||||
|
{% endblock %}
|
4
tests/javascript/position_caret/simple.html
Normal file
4
tests/javascript/position_caret/simple.html
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<p>MARKER this should be the paragraph the caret is on.</p>
|
||||||
|
{% endblock %}
|
96
tests/javascript/position_caret/test_position_caret.py
Normal file
96
tests/javascript/position_caret/test_position_caret.py
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||||
|
|
||||||
|
# Copyright 2015 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/>.
|
||||||
|
|
||||||
|
"""Tests for position_caret.js."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from PyQt5.QtCore import Qt
|
||||||
|
from PyQt5.QtWebKit import QWebSettings
|
||||||
|
from PyQt5.QtWebKitWidgets import QWebPage
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.yield_fixture(autouse=True)
|
||||||
|
def enable_caret_browsing():
|
||||||
|
"""Fixture to enable caret browsing globally."""
|
||||||
|
settings = QWebSettings.globalSettings()
|
||||||
|
old_value = settings.testAttribute(QWebSettings.CaretBrowsingEnabled)
|
||||||
|
settings.setAttribute(QWebSettings.CaretBrowsingEnabled, True)
|
||||||
|
yield
|
||||||
|
settings.setAttribute(QWebSettings.CaretBrowsingEnabled, old_value)
|
||||||
|
|
||||||
|
|
||||||
|
class CaretTester:
|
||||||
|
|
||||||
|
"""Helper class (for the caret_tester fixture) for asserts.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
js: The js_tester fixture.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, js_tester):
|
||||||
|
self.js = js_tester
|
||||||
|
|
||||||
|
def check(self):
|
||||||
|
"""Check whether the caret is before the MARKER text."""
|
||||||
|
self.js.run_file('position_caret.js')
|
||||||
|
self.js.webview.triggerPageAction(QWebPage.SelectNextWord)
|
||||||
|
assert self.js.webview.selectedText().rstrip() == "MARKER"
|
||||||
|
|
||||||
|
def check_scrolled(self):
|
||||||
|
"""Check if the page is scrolled down."""
|
||||||
|
frame = self.js.webview.page().mainFrame()
|
||||||
|
minimum = frame.scrollBarMinimum(Qt.Vertical)
|
||||||
|
value = frame.scrollBarValue(Qt.Vertical)
|
||||||
|
assert value > minimum
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def caret_tester(js_tester):
|
||||||
|
"""Helper fixture to test caret browsing positions."""
|
||||||
|
return CaretTester(js_tester)
|
||||||
|
|
||||||
|
|
||||||
|
def test_simple(caret_tester):
|
||||||
|
"""Test with a simple (one-line) HTML text."""
|
||||||
|
caret_tester.js.load('position_caret/simple.html')
|
||||||
|
caret_tester.check()
|
||||||
|
|
||||||
|
|
||||||
|
def test_scrolled_down(caret_tester):
|
||||||
|
"""Test with multiple text blocks with the viewport scrolled down."""
|
||||||
|
caret_tester.js.load('position_caret/scrolled_down.html')
|
||||||
|
caret_tester.js.scroll_anchor('anchor')
|
||||||
|
caret_tester.check_scrolled()
|
||||||
|
caret_tester.check()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('style', ['visibility: hidden', 'display: none'])
|
||||||
|
def test_invisible(caret_tester, style):
|
||||||
|
"""Test with hidden text elements."""
|
||||||
|
caret_tester.js.load('position_caret/invisible.html', style=style)
|
||||||
|
caret_tester.check()
|
||||||
|
|
||||||
|
|
||||||
|
def test_scrolled_down_img(caret_tester):
|
||||||
|
"""Test with an image at the top with the viewport scrolled down."""
|
||||||
|
caret_tester.js.load('position_caret/scrolled_down_img.html')
|
||||||
|
caret_tester.js.scroll_anchor('anchor')
|
||||||
|
caret_tester.check_scrolled()
|
||||||
|
caret_tester.check()
|
45
tests/keyinput/conftest.py
Normal file
45
tests/keyinput/conftest.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||||
|
|
||||||
|
# Copyright 2015 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/>.
|
||||||
|
|
||||||
|
"""pytest fixtures for tests.keyinput."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from unittest import mock
|
||||||
|
from qutebrowser.utils import objreg
|
||||||
|
|
||||||
|
|
||||||
|
BINDINGS = {'test': {'<Ctrl-a>': 'ctrla',
|
||||||
|
'a': 'a',
|
||||||
|
'ba': 'ba',
|
||||||
|
'ax': 'ax',
|
||||||
|
'ccc': 'ccc',
|
||||||
|
'0': '0'},
|
||||||
|
'test2': {'foo': 'bar', '<Ctrl+X>': 'ctrlx'},
|
||||||
|
'normal': {'a': 'a', 'ba': 'ba'}}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.yield_fixture
|
||||||
|
def fake_keyconfig():
|
||||||
|
"""Create a mock of a KeyConfiguration and register it into objreg."""
|
||||||
|
fake_keyconfig = mock.Mock(spec=['get_bindings_for'])
|
||||||
|
fake_keyconfig.get_bindings_for.side_effect = lambda s: BINDINGS[s]
|
||||||
|
objreg.register('key-config', fake_keyconfig)
|
||||||
|
yield
|
||||||
|
objreg.delete('key-config')
|
@ -21,6 +21,7 @@
|
|||||||
|
|
||||||
"""Tests for BaseKeyParser."""
|
"""Tests for BaseKeyParser."""
|
||||||
|
|
||||||
|
import sys
|
||||||
import logging
|
import logging
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
@ -28,35 +29,17 @@ from PyQt5.QtCore import Qt
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from qutebrowser.keyinput import basekeyparser
|
from qutebrowser.keyinput import basekeyparser
|
||||||
from qutebrowser.utils import objreg, log
|
from qutebrowser.utils import log
|
||||||
|
|
||||||
|
|
||||||
CONFIG = {'input': {'timeout': 100}}
|
CONFIG = {'input': {'timeout': 100}}
|
||||||
|
|
||||||
|
|
||||||
BINDINGS = {'test': {'<Ctrl-a>': 'ctrla',
|
|
||||||
'a': 'a',
|
|
||||||
'ba': 'ba',
|
|
||||||
'ax': 'ax',
|
|
||||||
'ccc': 'ccc'},
|
|
||||||
'test2': {'foo': 'bar', '<Ctrl+X>': 'ctrlx'}}
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.yield_fixture
|
|
||||||
def fake_keyconfig():
|
|
||||||
"""Create a mock of a KeyConfiguration and register it into objreg."""
|
|
||||||
fake_keyconfig = mock.Mock(spec=['get_bindings_for'])
|
|
||||||
fake_keyconfig.get_bindings_for.side_effect = lambda s: BINDINGS[s]
|
|
||||||
objreg.register('key-config', fake_keyconfig)
|
|
||||||
yield
|
|
||||||
objreg.delete('key-config')
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_timer(mocker, stubs):
|
def mock_timer(monkeypatch, stubs):
|
||||||
"""Mock the Timer class used by the usertypes module with a stub."""
|
"""Mock the Timer class used by the usertypes module with a stub."""
|
||||||
mocker.patch('qutebrowser.keyinput.basekeyparser.usertypes.Timer',
|
monkeypatch.setattr('qutebrowser.keyinput.basekeyparser.usertypes.Timer',
|
||||||
new=stubs.FakeTimer)
|
stubs.FakeTimer)
|
||||||
|
|
||||||
|
|
||||||
class TestSplitCount:
|
class TestSplitCount:
|
||||||
@ -131,8 +114,12 @@ class TestSpecialKeys:
|
|||||||
|
|
||||||
def test_valid_key(self, fake_keyevent_factory):
|
def test_valid_key(self, fake_keyevent_factory):
|
||||||
"""Test a valid special keyevent."""
|
"""Test a valid special keyevent."""
|
||||||
self.kp.handle(fake_keyevent_factory(Qt.Key_A, Qt.ControlModifier))
|
if sys.platform == 'darwin':
|
||||||
self.kp.handle(fake_keyevent_factory(Qt.Key_X, Qt.ControlModifier))
|
modifier = Qt.MetaModifier
|
||||||
|
else:
|
||||||
|
modifier = Qt.ControlModifier
|
||||||
|
self.kp.handle(fake_keyevent_factory(Qt.Key_A, modifier))
|
||||||
|
self.kp.handle(fake_keyevent_factory(Qt.Key_X, modifier))
|
||||||
self.kp.execute.assert_called_once_with('ctrla', self.kp.Type.special)
|
self.kp.execute.assert_called_once_with('ctrla', self.kp.Type.special)
|
||||||
|
|
||||||
def test_invalid_key(self, fake_keyevent_factory):
|
def test_invalid_key(self, fake_keyevent_factory):
|
||||||
@ -167,8 +154,12 @@ class TestKeyChain:
|
|||||||
|
|
||||||
def test_valid_special_key(self, fake_keyevent_factory):
|
def test_valid_special_key(self, fake_keyevent_factory):
|
||||||
"""Test valid special key."""
|
"""Test valid special key."""
|
||||||
self.kp.handle(fake_keyevent_factory(Qt.Key_A, Qt.ControlModifier))
|
if sys.platform == 'darwin':
|
||||||
self.kp.handle(fake_keyevent_factory(Qt.Key_X, Qt.ControlModifier))
|
modifier = Qt.MetaModifier
|
||||||
|
else:
|
||||||
|
modifier = Qt.ControlModifier
|
||||||
|
self.kp.handle(fake_keyevent_factory(Qt.Key_A, modifier))
|
||||||
|
self.kp.handle(fake_keyevent_factory(Qt.Key_X, modifier))
|
||||||
self.kp.execute.assert_called_once_with('ctrla', self.kp.Type.special)
|
self.kp.execute.assert_called_once_with('ctrla', self.kp.Type.special)
|
||||||
assert self.kp._keystring == ''
|
assert self.kp._keystring == ''
|
||||||
|
|
||||||
@ -189,12 +180,18 @@ class TestKeyChain:
|
|||||||
self.kp.execute.assert_called_once_with('ba', self.kp.Type.chain, None)
|
self.kp.execute.assert_called_once_with('ba', self.kp.Type.chain, None)
|
||||||
assert self.kp._keystring == ''
|
assert self.kp._keystring == ''
|
||||||
|
|
||||||
|
def test_0(self, fake_keyevent_factory):
|
||||||
|
"""Test with 0 keypress."""
|
||||||
|
self.kp.handle(fake_keyevent_factory(Qt.Key_0, text='0'))
|
||||||
|
self.kp.execute.assert_called_once_with('0', self.kp.Type.chain, None)
|
||||||
|
assert self.kp._keystring == ''
|
||||||
|
|
||||||
def test_ambiguous_keychain(self, fake_keyevent_factory, config_stub,
|
def test_ambiguous_keychain(self, fake_keyevent_factory, config_stub,
|
||||||
mocker):
|
monkeypatch):
|
||||||
"""Test ambiguous keychain."""
|
"""Test ambiguous keychain."""
|
||||||
config_stub.data = CONFIG
|
config_stub.data = CONFIG
|
||||||
mocker.patch('qutebrowser.keyinput.basekeyparser.config',
|
monkeypatch.setattr('qutebrowser.keyinput.basekeyparser.config',
|
||||||
new=config_stub)
|
config_stub)
|
||||||
timer = self.kp._ambiguous_timer
|
timer = self.kp._ambiguous_timer
|
||||||
assert not timer.isActive()
|
assert not timer.isActive()
|
||||||
# We start with 'a' where the keychain gives us an ambiguous result.
|
# We start with 'a' where the keychain gives us an ambiguous result.
|
||||||
@ -242,7 +239,9 @@ class TestCount:
|
|||||||
self.kp.handle(fake_keyevent_factory(Qt.Key_0, text='0'))
|
self.kp.handle(fake_keyevent_factory(Qt.Key_0, text='0'))
|
||||||
self.kp.handle(fake_keyevent_factory(Qt.Key_B, text='b'))
|
self.kp.handle(fake_keyevent_factory(Qt.Key_B, text='b'))
|
||||||
self.kp.handle(fake_keyevent_factory(Qt.Key_A, text='a'))
|
self.kp.handle(fake_keyevent_factory(Qt.Key_A, text='a'))
|
||||||
self.kp.execute.assert_called_once_with('ba', self.kp.Type.chain, 0)
|
calls = [mock.call('0', self.kp.Type.chain, None),
|
||||||
|
mock.call('ba', self.kp.Type.chain, None)]
|
||||||
|
self.kp.execute.assert_has_calls(calls)
|
||||||
assert self.kp._keystring == ''
|
assert self.kp._keystring == ''
|
||||||
|
|
||||||
def test_count_42(self, fake_keyevent_factory):
|
def test_count_42(self, fake_keyevent_factory):
|
||||||
|
@ -19,25 +19,18 @@
|
|||||||
|
|
||||||
"""Tests for mode parsers."""
|
"""Tests for mode parsers."""
|
||||||
|
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt
|
from PyQt5.QtCore import Qt
|
||||||
|
|
||||||
from unittest import mock
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from qutebrowser.keyinput import modeparsers
|
from qutebrowser.keyinput import modeparsers
|
||||||
from qutebrowser.utils import objreg
|
|
||||||
|
|
||||||
|
|
||||||
CONFIG = {'input': {'partial-timeout': 100}}
|
CONFIG = {'input': {'partial-timeout': 100}}
|
||||||
|
|
||||||
|
|
||||||
BINDINGS = {'normal': {'a': 'a', 'ba': 'ba'}}
|
|
||||||
|
|
||||||
|
|
||||||
fake_keyconfig = mock.Mock(spec=['get_bindings_for'])
|
|
||||||
fake_keyconfig.get_bindings_for.side_effect = lambda s: BINDINGS[s]
|
|
||||||
|
|
||||||
|
|
||||||
class TestsNormalKeyParser:
|
class TestsNormalKeyParser:
|
||||||
|
|
||||||
"""Tests for NormalKeyParser.
|
"""Tests for NormalKeyParser.
|
||||||
@ -49,19 +42,18 @@ class TestsNormalKeyParser:
|
|||||||
# pylint: disable=protected-access
|
# pylint: disable=protected-access
|
||||||
|
|
||||||
@pytest.yield_fixture(autouse=True)
|
@pytest.yield_fixture(autouse=True)
|
||||||
def setup(self, mocker, stubs, config_stub):
|
def setup(self, monkeypatch, stubs, config_stub, fake_keyconfig):
|
||||||
"""Set up mocks and read the test config."""
|
"""Set up mocks and read the test config."""
|
||||||
mocker.patch('qutebrowser.keyinput.basekeyparser.usertypes.Timer',
|
monkeypatch.setattr(
|
||||||
new=stubs.FakeTimer)
|
'qutebrowser.keyinput.basekeyparser.usertypes.Timer',
|
||||||
|
stubs.FakeTimer)
|
||||||
config_stub.data = CONFIG
|
config_stub.data = CONFIG
|
||||||
mocker.patch('qutebrowser.keyinput.modeparsers.config',
|
monkeypatch.setattr('qutebrowser.keyinput.modeparsers.config',
|
||||||
new=config_stub)
|
config_stub)
|
||||||
|
|
||||||
objreg.register('key-config', fake_keyconfig)
|
|
||||||
self.kp = modeparsers.NormalKeyParser(0)
|
self.kp = modeparsers.NormalKeyParser(0)
|
||||||
self.kp.execute = mock.Mock()
|
self.kp.execute = mock.Mock()
|
||||||
yield
|
yield
|
||||||
objreg.delete('key-config')
|
|
||||||
|
|
||||||
def test_keychain(self, fake_keyevent_factory):
|
def test_keychain(self, fake_keyevent_factory):
|
||||||
"""Test valid keychain."""
|
"""Test valid keychain."""
|
||||||
|
@ -19,8 +19,6 @@
|
|||||||
|
|
||||||
"""Tests for qutebrowser.misc.crashdialog."""
|
"""Tests for qutebrowser.misc.crashdialog."""
|
||||||
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
from qutebrowser.misc import crashdialog
|
from qutebrowser.misc import crashdialog
|
||||||
|
|
||||||
|
|
||||||
@ -52,7 +50,7 @@ Hello world!
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class ParseFatalStacktraceTests(unittest.TestCase):
|
class TestParseFatalStacktrace:
|
||||||
|
|
||||||
"""Tests for parse_fatal_stacktrace."""
|
"""Tests for parse_fatal_stacktrace."""
|
||||||
|
|
||||||
@ -60,30 +58,22 @@ class ParseFatalStacktraceTests(unittest.TestCase):
|
|||||||
"""Test parse_fatal_stacktrace with a valid text."""
|
"""Test parse_fatal_stacktrace with a valid text."""
|
||||||
text = VALID_CRASH_TEXT.strip().replace('_', ' ')
|
text = VALID_CRASH_TEXT.strip().replace('_', ' ')
|
||||||
typ, func = crashdialog.parse_fatal_stacktrace(text)
|
typ, func = crashdialog.parse_fatal_stacktrace(text)
|
||||||
self.assertEqual(typ, "Segmentation fault")
|
assert (typ, func) == ("Segmentation fault", 'testfunc')
|
||||||
self.assertEqual(func, 'testfunc')
|
|
||||||
|
|
||||||
def test_valid_text_thread(self):
|
def test_valid_text_thread(self):
|
||||||
"""Test parse_fatal_stacktrace with a valid text #2."""
|
"""Test parse_fatal_stacktrace with a valid text #2."""
|
||||||
text = VALID_CRASH_TEXT_THREAD.strip().replace('_', ' ')
|
text = VALID_CRASH_TEXT_THREAD.strip().replace('_', ' ')
|
||||||
typ, func = crashdialog.parse_fatal_stacktrace(text)
|
typ, func = crashdialog.parse_fatal_stacktrace(text)
|
||||||
self.assertEqual(typ, "Segmentation fault")
|
assert (typ, func) == ("Segmentation fault", 'testfunc')
|
||||||
self.assertEqual(func, 'testfunc')
|
|
||||||
|
|
||||||
def test_valid_text_empty(self):
|
def test_valid_text_empty(self):
|
||||||
"""Test parse_fatal_stacktrace with a valid text but empty function."""
|
"""Test parse_fatal_stacktrace with a valid text but empty function."""
|
||||||
text = VALID_CRASH_TEXT_EMPTY.strip().replace('_', ' ')
|
text = VALID_CRASH_TEXT_EMPTY.strip().replace('_', ' ')
|
||||||
typ, func = crashdialog.parse_fatal_stacktrace(text)
|
typ, func = crashdialog.parse_fatal_stacktrace(text)
|
||||||
self.assertEqual(typ, 'Aborted')
|
assert (typ, func) == ('Aborted', '')
|
||||||
self.assertEqual(func, '')
|
|
||||||
|
|
||||||
def test_invalid_text(self):
|
def test_invalid_text(self):
|
||||||
"""Test parse_fatal_stacktrace with an invalid text."""
|
"""Test parse_fatal_stacktrace with an invalid text."""
|
||||||
text = INVALID_CRASH_TEXT.strip().replace('_', ' ')
|
text = INVALID_CRASH_TEXT.strip().replace('_', ' ')
|
||||||
typ, func = crashdialog.parse_fatal_stacktrace(text)
|
typ, func = crashdialog.parse_fatal_stacktrace(text)
|
||||||
self.assertEqual(typ, '')
|
assert (typ, func) == ('', '')
|
||||||
self.assertEqual(func, '')
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
unittest.main()
|
|
||||||
|
@ -41,18 +41,18 @@ class TestArg:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
@pytest.yield_fixture(autouse=True)
|
@pytest.yield_fixture(autouse=True)
|
||||||
def setup(self, mocker, stubs):
|
def setup(self, monkeypatch, stubs):
|
||||||
mocker.patch('qutebrowser.misc.editor.QProcess',
|
monkeypatch.setattr('qutebrowser.misc.editor.QProcess',
|
||||||
new_callable=stubs.FakeQProcess)
|
stubs.FakeQProcess())
|
||||||
self.editor = editor.ExternalEditor(0)
|
self.editor = editor.ExternalEditor(0)
|
||||||
yield
|
yield
|
||||||
self.editor._cleanup() # pylint: disable=protected-access
|
self.editor._cleanup() # pylint: disable=protected-access
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def stubbed_config(self, config_stub, mocker):
|
def stubbed_config(self, config_stub, monkeypatch):
|
||||||
"""Fixture to create a config stub with an input section."""
|
"""Fixture to create a config stub with an input section."""
|
||||||
config_stub.data = {'input': {}}
|
config_stub.data = {'input': {}}
|
||||||
mocker.patch('qutebrowser.misc.editor.config', new=config_stub)
|
monkeypatch.setattr('qutebrowser.misc.editor.config', config_stub)
|
||||||
return config_stub
|
return config_stub
|
||||||
|
|
||||||
def test_simple_start_args(self, stubbed_config):
|
def test_simple_start_args(self, stubbed_config):
|
||||||
@ -98,14 +98,14 @@ class TestFileHandling:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def setup(self, mocker, stubs, config_stub):
|
def setup(self, monkeypatch, stubs, config_stub):
|
||||||
mocker.patch('qutebrowser.misc.editor.message',
|
monkeypatch.setattr('qutebrowser.misc.editor.message',
|
||||||
new=stubs.MessageModule())
|
stubs.MessageModule())
|
||||||
mocker.patch('qutebrowser.misc.editor.QProcess',
|
monkeypatch.setattr('qutebrowser.misc.editor.QProcess',
|
||||||
new_callable=stubs.FakeQProcess)
|
stubs.FakeQProcess())
|
||||||
config_stub.data = {'general': {'editor': [''],
|
config_stub.data = {'general': {'editor': [''],
|
||||||
'editor-encoding': 'utf-8'}}
|
'editor-encoding': 'utf-8'}}
|
||||||
mocker.patch('qutebrowser.misc.editor.config', config_stub)
|
monkeypatch.setattr('qutebrowser.misc.editor.config', config_stub)
|
||||||
self.editor = editor.ExternalEditor(0)
|
self.editor = editor.ExternalEditor(0)
|
||||||
|
|
||||||
def test_file_handling_closed_ok(self):
|
def test_file_handling_closed_ok(self):
|
||||||
@ -147,12 +147,12 @@ class TestModifyTests:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def setup(self, mocker, stubs, config_stub):
|
def setup(self, monkeypatch, stubs, config_stub):
|
||||||
mocker.patch('qutebrowser.misc.editor.QProcess',
|
monkeypatch.setattr('qutebrowser.misc.editor.QProcess',
|
||||||
new_callable=stubs.FakeQProcess)
|
stubs.FakeQProcess())
|
||||||
config_stub.data = {'general': {'editor': [''],
|
config_stub.data = {'general': {'editor': [''],
|
||||||
'editor-encoding': 'utf-8'}}
|
'editor-encoding': 'utf-8'}}
|
||||||
mocker.patch('qutebrowser.misc.editor.config', new=config_stub)
|
monkeypatch.setattr('qutebrowser.misc.editor.config', config_stub)
|
||||||
self.editor = editor.ExternalEditor(0)
|
self.editor = editor.ExternalEditor(0)
|
||||||
self.editor.editing_finished = mock.Mock()
|
self.editor.editing_finished = mock.Mock()
|
||||||
|
|
||||||
@ -219,14 +219,14 @@ class TestErrorMessage:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
@pytest.yield_fixture(autouse=True)
|
@pytest.yield_fixture(autouse=True)
|
||||||
def setup(self, mocker, stubs, config_stub):
|
def setup(self, monkeypatch, stubs, config_stub):
|
||||||
mocker.patch('qutebrowser.misc.editor.QProcess',
|
monkeypatch.setattr('qutebrowser.misc.editor.QProcess',
|
||||||
new_callable=stubs.FakeQProcess)
|
stubs.FakeQProcess())
|
||||||
mocker.patch('qutebrowser.misc.editor.message',
|
monkeypatch.setattr('qutebrowser.misc.editor.message',
|
||||||
new=stubs.MessageModule())
|
stubs.MessageModule())
|
||||||
config_stub.data = {'general': {'editor': [''],
|
config_stub.data = {'general': {'editor': [''],
|
||||||
'editor-encoding': 'utf-8'}}
|
'editor-encoding': 'utf-8'}}
|
||||||
mocker.patch('qutebrowser.misc.editor.config', new=config_stub)
|
monkeypatch.setattr('qutebrowser.misc.editor.config', config_stub)
|
||||||
self.editor = editor.ExternalEditor(0)
|
self.editor = editor.ExternalEditor(0)
|
||||||
yield
|
yield
|
||||||
self.editor._cleanup() # pylint: disable=protected-access
|
self.editor._cleanup() # pylint: disable=protected-access
|
||||||
|
@ -23,10 +23,10 @@
|
|||||||
|
|
||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
import unittest
|
|
||||||
from unittest import mock
|
|
||||||
|
|
||||||
from qutebrowser.misc import lineparser
|
import pytest
|
||||||
|
|
||||||
|
from qutebrowser.misc import lineparser as lineparsermod
|
||||||
|
|
||||||
|
|
||||||
class LineParserWrapper:
|
class LineParserWrapper:
|
||||||
@ -69,118 +69,129 @@ class LineParserWrapper:
|
|||||||
def _prepare_save(self):
|
def _prepare_save(self):
|
||||||
"""Keep track if _prepare_save has been called."""
|
"""Keep track if _prepare_save has been called."""
|
||||||
self._test_save_prepared = True
|
self._test_save_prepared = True
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
class TestableAppendLineParser(LineParserWrapper, lineparser.AppendLineParser):
|
class AppendLineParserTestable(LineParserWrapper,
|
||||||
|
lineparsermod.AppendLineParser):
|
||||||
|
|
||||||
"""Wrapper over AppendLineParser to make it testable."""
|
"""Wrapper over AppendLineParser to make it testable."""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class TestableLineParser(LineParserWrapper, lineparser.LineParser):
|
class LineParserTestable(LineParserWrapper, lineparsermod.LineParser):
|
||||||
|
|
||||||
"""Wrapper over LineParser to make it testable."""
|
"""Wrapper over LineParser to make it testable."""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class TestableLimitLineParser(LineParserWrapper, lineparser.LimitLineParser):
|
class LimitLineParserTestable(LineParserWrapper,
|
||||||
|
lineparsermod.LimitLineParser):
|
||||||
|
|
||||||
"""Wrapper over LimitLineParser to make it testable."""
|
"""Wrapper over LimitLineParser to make it testable."""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@mock.patch('qutebrowser.misc.lineparser.os.path')
|
class TestBaseLineParser:
|
||||||
@mock.patch('qutebrowser.misc.lineparser.os')
|
|
||||||
class BaseLineParserTests(unittest.TestCase):
|
|
||||||
|
|
||||||
"""Tests for BaseLineParser."""
|
"""Tests for BaseLineParser."""
|
||||||
|
|
||||||
def setUp(self):
|
CONFDIR = "this really doesn't matter"
|
||||||
self._confdir = "this really doesn't matter"
|
FILENAME = "and neither does this"
|
||||||
self._fname = "and neither does this"
|
|
||||||
self._lineparser = lineparser.BaseLineParser(
|
|
||||||
self._confdir, self._fname)
|
|
||||||
|
|
||||||
def test_prepare_save_existing(self, os_mock, os_path_mock):
|
@pytest.fixture
|
||||||
|
def lineparser(self):
|
||||||
|
"""Fixture providing a BaseLineParser."""
|
||||||
|
return lineparsermod.BaseLineParser(self.CONFDIR, self.FILENAME)
|
||||||
|
|
||||||
|
def test_prepare_save_existing(self, mocker, lineparser):
|
||||||
"""Test if _prepare_save does what it's supposed to do."""
|
"""Test if _prepare_save does what it's supposed to do."""
|
||||||
os_path_mock.exists.return_value = True
|
exists_mock = mocker.patch(
|
||||||
self._lineparser._prepare_save()
|
'qutebrowser.misc.lineparser.os.path.exists')
|
||||||
self.assertFalse(os_mock.makedirs.called)
|
makedirs_mock = mocker.patch('qutebrowser.misc.lineparser.os.makedirs')
|
||||||
|
exists_mock.return_value = True
|
||||||
|
|
||||||
def test_prepare_save_missing(self, os_mock, os_path_mock):
|
lineparser._prepare_save()
|
||||||
|
assert not makedirs_mock.called
|
||||||
|
|
||||||
|
def test_prepare_save_missing(self, mocker, lineparser):
|
||||||
"""Test if _prepare_save does what it's supposed to do."""
|
"""Test if _prepare_save does what it's supposed to do."""
|
||||||
os_path_mock.exists.return_value = False
|
exists_mock = mocker.patch(
|
||||||
self._lineparser._prepare_save()
|
'qutebrowser.misc.lineparser.os.path.exists')
|
||||||
os_mock.makedirs.assert_called_with(self._confdir, 0o755)
|
exists_mock.return_value = False
|
||||||
|
makedirs_mock = mocker.patch('qutebrowser.misc.lineparser.os.makedirs')
|
||||||
|
|
||||||
|
lineparser._prepare_save()
|
||||||
|
makedirs_mock.assert_called_with(self.CONFDIR, 0o755)
|
||||||
|
|
||||||
|
|
||||||
class AppendLineParserTests(unittest.TestCase):
|
class TestAppendLineParser:
|
||||||
|
|
||||||
"""Tests for AppendLineParser."""
|
"""Tests for AppendLineParser."""
|
||||||
|
|
||||||
def setUp(self):
|
BASE_DATA = ['old data 1', 'old data 2']
|
||||||
self._lineparser = TestableAppendLineParser('this really',
|
|
||||||
'does not matter')
|
|
||||||
self._lineparser.new_data = ['old data 1', 'old data 2']
|
|
||||||
self._expected_data = self._lineparser.new_data
|
|
||||||
self._lineparser.save()
|
|
||||||
|
|
||||||
def _get_expected(self):
|
@pytest.fixture
|
||||||
|
def lineparser(self):
|
||||||
|
"""Fixture to get an AppendLineParser for tests."""
|
||||||
|
lp = AppendLineParserTestable('this really', 'does not matter')
|
||||||
|
lp.new_data = self.BASE_DATA
|
||||||
|
lp.save()
|
||||||
|
return lp
|
||||||
|
|
||||||
|
def _get_expected(self, new_data):
|
||||||
"""Get the expected data with newlines."""
|
"""Get the expected data with newlines."""
|
||||||
return '\n'.join(self._expected_data) + '\n'
|
return '\n'.join(self.BASE_DATA + new_data) + '\n'
|
||||||
|
|
||||||
def test_save(self):
|
def test_save(self, lineparser):
|
||||||
"""Test save()."""
|
"""Test save()."""
|
||||||
self._lineparser.new_data = ['new data 1', 'new data 2']
|
new_data = ['new data 1', 'new data 2']
|
||||||
self._expected_data += self._lineparser.new_data
|
lineparser.new_data = new_data
|
||||||
self._lineparser.save()
|
lineparser.save()
|
||||||
self.assertEqual(self._lineparser._data, self._get_expected())
|
assert lineparser._data == self._get_expected(new_data)
|
||||||
|
|
||||||
def test_iter_without_open(self):
|
def test_iter_without_open(self, lineparser):
|
||||||
"""Test __iter__ without having called open()."""
|
"""Test __iter__ without having called open()."""
|
||||||
with self.assertRaises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
iter(self._lineparser)
|
iter(lineparser)
|
||||||
|
|
||||||
def test_iter(self):
|
def test_iter(self, lineparser):
|
||||||
"""Test __iter__."""
|
"""Test __iter__."""
|
||||||
self._lineparser.new_data = ['new data 1', 'new data 2']
|
new_data = ['new data 1', 'new data 2']
|
||||||
self._expected_data += self._lineparser.new_data
|
lineparser.new_data = new_data
|
||||||
with self._lineparser.open():
|
with lineparser.open():
|
||||||
self.assertEqual(list(self._lineparser), self._expected_data)
|
assert list(lineparser) == self.BASE_DATA + new_data
|
||||||
|
|
||||||
@mock.patch('qutebrowser.misc.lineparser.AppendLineParser._open')
|
def test_iter_not_found(self, mocker):
|
||||||
def test_iter_not_found(self, open_mock):
|
|
||||||
"""Test __iter__ with no file."""
|
"""Test __iter__ with no file."""
|
||||||
|
open_mock = mocker.patch(
|
||||||
|
'qutebrowser.misc.lineparser.AppendLineParser._open')
|
||||||
open_mock.side_effect = FileNotFoundError
|
open_mock.side_effect = FileNotFoundError
|
||||||
linep = lineparser.AppendLineParser('foo', 'bar')
|
new_data = ['new data 1', 'new data 2']
|
||||||
linep.new_data = ['new data 1', 'new data 2']
|
linep = lineparsermod.AppendLineParser('foo', 'bar')
|
||||||
expected_data = linep.new_data
|
linep.new_data = new_data
|
||||||
with linep.open():
|
with linep.open():
|
||||||
self.assertEqual(list(linep), expected_data)
|
assert list(linep) == new_data
|
||||||
|
|
||||||
def test_get_recent_none(self):
|
def test_get_recent_none(self):
|
||||||
"""Test get_recent with no data."""
|
"""Test get_recent with no data."""
|
||||||
linep = TestableAppendLineParser('this really', 'does not matter')
|
linep = AppendLineParserTestable('this really', 'does not matter')
|
||||||
self.assertEqual(linep.get_recent(), [])
|
assert linep.get_recent() == []
|
||||||
|
|
||||||
def test_get_recent_little(self):
|
def test_get_recent_little(self, lineparser):
|
||||||
"""Test get_recent with little data."""
|
"""Test get_recent with little data."""
|
||||||
data = [e + '\n' for e in self._expected_data]
|
data = [e + '\n' for e in self.BASE_DATA]
|
||||||
self.assertEqual(self._lineparser.get_recent(), data)
|
assert lineparser.get_recent() == data
|
||||||
|
|
||||||
def test_get_recent_much(self):
|
def test_get_recent_much(self, lineparser):
|
||||||
"""Test get_recent with much data."""
|
"""Test get_recent with much data."""
|
||||||
size = 64
|
size = 64
|
||||||
new_data = ['new data {}'.format(i) for i in range(size)]
|
new_data = ['new data {}'.format(i) for i in range(size)]
|
||||||
self._lineparser.new_data = new_data
|
lineparser.new_data = new_data
|
||||||
self._lineparser.save()
|
lineparser.save()
|
||||||
data = '\n'.join(self._expected_data + new_data)
|
data = '\n'.join(self.BASE_DATA + new_data)
|
||||||
data = [e + '\n' for e in data[-(size - 1):].splitlines()]
|
data = [e + '\n' for e in data[-(size - 1):].splitlines()]
|
||||||
self.assertEqual(self._lineparser.get_recent(size), data)
|
assert lineparser.get_recent(size) == data
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
unittest.main()
|
|
||||||
|
@ -31,10 +31,11 @@ from qutebrowser.misc import readline
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mocked_qapp(mocker, stubs):
|
def mocked_qapp(monkeypatch, stubs):
|
||||||
"""Fixture that mocks readline.QApplication and returns it."""
|
"""Fixture that mocks readline.QApplication and returns it."""
|
||||||
return mocker.patch('qutebrowser.misc.readline.QApplication',
|
stub = stubs.FakeQApplication()
|
||||||
new_callable=stubs.FakeQApplication)
|
monkeypatch.setattr('qutebrowser.misc.readline.QApplication', stub)
|
||||||
|
return stub
|
||||||
|
|
||||||
|
|
||||||
class TestNoneWidget:
|
class TestNoneWidget:
|
||||||
|
@ -22,231 +22,241 @@
|
|||||||
"""Tests for qutebrowser.utils.log."""
|
"""Tests for qutebrowser.utils.log."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import unittest
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import itertools
|
||||||
import sys
|
import sys
|
||||||
from unittest import mock
|
|
||||||
|
import pytest
|
||||||
|
from PyQt5.QtCore import qWarning
|
||||||
|
|
||||||
from qutebrowser.utils import log
|
from qutebrowser.utils import log
|
||||||
|
|
||||||
from PyQt5.QtCore import qWarning
|
|
||||||
|
|
||||||
|
@pytest.yield_fixture(autouse=True)
|
||||||
class BaseTest(unittest.TestCase):
|
def restore_loggers():
|
||||||
|
"""Fixture to save/restore the logging state.
|
||||||
"""Base class for logging tests.
|
|
||||||
|
|
||||||
Based on CPython's Lib/test/test_logging.py.
|
Based on CPython's Lib/test/test_logging.py.
|
||||||
"""
|
"""
|
||||||
|
logger_dict = logging.getLogger().manager.loggerDict
|
||||||
|
logging._acquireLock()
|
||||||
|
try:
|
||||||
|
saved_handlers = logging._handlers.copy()
|
||||||
|
saved_handler_list = logging._handlerList[:]
|
||||||
|
saved_loggers = saved_loggers = logger_dict.copy()
|
||||||
|
saved_name_to_level = logging._nameToLevel.copy()
|
||||||
|
saved_level_to_name = logging._levelToName.copy()
|
||||||
|
logger_states = {}
|
||||||
|
for name in saved_loggers:
|
||||||
|
logger_states[name] = getattr(saved_loggers[name], 'disabled',
|
||||||
|
None)
|
||||||
|
finally:
|
||||||
|
logging._releaseLock()
|
||||||
|
|
||||||
def setUp(self):
|
root_logger = logging.getLogger("")
|
||||||
"""Save the old logging configuration."""
|
root_handlers = root_logger.handlers[:]
|
||||||
|
original_logging_level = root_logger.getEffectiveLevel()
|
||||||
|
|
||||||
|
yield
|
||||||
|
|
||||||
|
while root_logger.handlers:
|
||||||
|
h = root_logger.handlers[0]
|
||||||
|
root_logger.removeHandler(h)
|
||||||
|
h.close()
|
||||||
|
root_logger.setLevel(original_logging_level)
|
||||||
|
for h in root_handlers:
|
||||||
|
root_logger.addHandler(h)
|
||||||
|
logging._acquireLock()
|
||||||
|
try:
|
||||||
|
logging._levelToName.clear()
|
||||||
|
logging._levelToName.update(saved_level_to_name)
|
||||||
|
logging._nameToLevel.clear()
|
||||||
|
logging._nameToLevel.update(saved_name_to_level)
|
||||||
|
logging._handlers.clear()
|
||||||
|
logging._handlers.update(saved_handlers)
|
||||||
|
logging._handlerList[:] = saved_handler_list
|
||||||
logger_dict = logging.getLogger().manager.loggerDict
|
logger_dict = logging.getLogger().manager.loggerDict
|
||||||
logging._acquireLock()
|
logger_dict.clear()
|
||||||
try:
|
logger_dict.update(saved_loggers)
|
||||||
self.saved_handlers = logging._handlers.copy()
|
logger_states = logger_states
|
||||||
self.saved_handler_list = logging._handlerList[:]
|
for name in logger_states:
|
||||||
self.saved_loggers = saved_loggers = logger_dict.copy()
|
if logger_states[name] is not None:
|
||||||
self.saved_name_to_level = logging._nameToLevel.copy()
|
saved_loggers[name].disabled = logger_states[name]
|
||||||
self.saved_level_to_name = logging._levelToName.copy()
|
finally:
|
||||||
self.logger_states = {}
|
logging._releaseLock()
|
||||||
for name in saved_loggers:
|
|
||||||
self.logger_states[name] = getattr(saved_loggers[name],
|
|
||||||
'disabled', None)
|
|
||||||
finally:
|
|
||||||
logging._releaseLock()
|
|
||||||
|
|
||||||
self.root_logger = logging.getLogger("")
|
|
||||||
self.root_handlers = self.root_logger.handlers[:]
|
|
||||||
self.original_logging_level = self.root_logger.getEffectiveLevel()
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
"""Restore the original logging configuration."""
|
|
||||||
while self.root_logger.handlers:
|
|
||||||
h = self.root_logger.handlers[0]
|
|
||||||
self.root_logger.removeHandler(h)
|
|
||||||
h.close()
|
|
||||||
self.root_logger.setLevel(self.original_logging_level)
|
|
||||||
for h in self.root_handlers:
|
|
||||||
self.root_logger.addHandler(h)
|
|
||||||
logging._acquireLock()
|
|
||||||
try:
|
|
||||||
logging._levelToName.clear()
|
|
||||||
logging._levelToName.update(self.saved_level_to_name)
|
|
||||||
logging._nameToLevel.clear()
|
|
||||||
logging._nameToLevel.update(self.saved_name_to_level)
|
|
||||||
logging._handlers.clear()
|
|
||||||
logging._handlers.update(self.saved_handlers)
|
|
||||||
logging._handlerList[:] = self.saved_handler_list
|
|
||||||
logger_dict = logging.getLogger().manager.loggerDict
|
|
||||||
logger_dict.clear()
|
|
||||||
logger_dict.update(self.saved_loggers)
|
|
||||||
logger_states = self.logger_states
|
|
||||||
for name in self.logger_states:
|
|
||||||
if logger_states[name] is not None:
|
|
||||||
self.saved_loggers[name].disabled = logger_states[name]
|
|
||||||
finally:
|
|
||||||
logging._releaseLock()
|
|
||||||
|
|
||||||
|
|
||||||
class LogFilterTests(unittest.TestCase):
|
@pytest.fixture(scope='session')
|
||||||
|
def log_counter():
|
||||||
|
"""Counter for logger fixture to get unique loggers."""
|
||||||
|
return itertools.count()
|
||||||
|
|
||||||
"""Tests for LogFilter.
|
|
||||||
|
|
||||||
Attributes:
|
@pytest.fixture
|
||||||
logger: The logger we use to create records.
|
def logger(log_counter):
|
||||||
|
"""Fixture which provides a logger for tests.
|
||||||
|
|
||||||
|
Unique throwaway loggers are used to make sure the tests don't influence
|
||||||
|
each other.
|
||||||
"""
|
"""
|
||||||
|
i = next(log_counter)
|
||||||
|
return logging.getLogger('qutebrowser-unittest-logger-{}'.format(i))
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.logger = logging.getLogger("foo")
|
|
||||||
|
|
||||||
def _make_record(self, name, level=logging.DEBUG):
|
class TestLogFilter:
|
||||||
|
|
||||||
|
"""Tests for LogFilter."""
|
||||||
|
|
||||||
|
def _make_record(self, logger, name, level=logging.DEBUG):
|
||||||
"""Create a bogus logging record with the supplied logger name."""
|
"""Create a bogus logging record with the supplied logger name."""
|
||||||
return self.logger.makeRecord(name, level=level, fn=None, lno=0,
|
return logger.makeRecord(name, level=level, fn=None, lno=0, msg="",
|
||||||
msg="", args=None, exc_info=None)
|
args=None, exc_info=None)
|
||||||
|
|
||||||
def test_empty(self):
|
def test_empty(self, logger):
|
||||||
"""Test if an empty filter lets all messages through."""
|
"""Test if an empty filter lets all messages through."""
|
||||||
logfilter = log.LogFilter(None)
|
logfilter = log.LogFilter(None)
|
||||||
record = self._make_record("eggs.bacon.spam")
|
record = self._make_record(logger, "eggs.bacon.spam")
|
||||||
self.assertTrue(logfilter.filter(record))
|
assert logfilter.filter(record)
|
||||||
record = self._make_record("eggs")
|
record = self._make_record(logger, "eggs")
|
||||||
self.assertTrue(logfilter.filter(record))
|
assert logfilter.filter(record)
|
||||||
|
|
||||||
def test_matching(self):
|
def test_matching(self, logger):
|
||||||
"""Test if a filter lets an exactly matching log record through."""
|
"""Test if a filter lets an exactly matching log record through."""
|
||||||
logfilter = log.LogFilter(["eggs", "bacon"])
|
logfilter = log.LogFilter(["eggs", "bacon"])
|
||||||
record = self._make_record("eggs")
|
record = self._make_record(logger, "eggs")
|
||||||
self.assertTrue(logfilter.filter(record))
|
assert logfilter.filter(record)
|
||||||
record = self._make_record("bacon")
|
record = self._make_record(logger, "bacon")
|
||||||
self.assertTrue(logfilter.filter(record))
|
assert logfilter.filter(record)
|
||||||
record = self._make_record("spam")
|
record = self._make_record(logger, "spam")
|
||||||
self.assertFalse(logfilter.filter(record))
|
assert not logfilter.filter(record)
|
||||||
logfilter = log.LogFilter(["eggs.bacon"])
|
logfilter = log.LogFilter(["eggs.bacon"])
|
||||||
record = self._make_record("eggs.bacon")
|
record = self._make_record(logger, "eggs.bacon")
|
||||||
self.assertTrue(logfilter.filter(record))
|
assert logfilter.filter(record)
|
||||||
|
|
||||||
def test_equal_start(self):
|
def test_equal_start(self, logger):
|
||||||
"""Test if a filter blocks a logger which looks equal but isn't."""
|
"""Test if a filter blocks a logger which looks equal but isn't."""
|
||||||
logfilter = log.LogFilter(["eggs"])
|
logfilter = log.LogFilter(["eggs"])
|
||||||
record = self._make_record("eggsauce")
|
record = self._make_record(logger, "eggsauce")
|
||||||
self.assertFalse(logfilter.filter(record))
|
assert not logfilter.filter(record)
|
||||||
logfilter = log.LogFilter("eggs.bacon")
|
logfilter = log.LogFilter("eggs.bacon")
|
||||||
record = self._make_record("eggs.baconstrips")
|
record = self._make_record(logger, "eggs.baconstrips")
|
||||||
self.assertFalse(logfilter.filter(record))
|
assert not logfilter.filter(record)
|
||||||
|
|
||||||
def test_child(self):
|
def test_child(self, logger):
|
||||||
"""Test if a filter lets through a logger which is a child."""
|
"""Test if a filter lets through a logger which is a child."""
|
||||||
logfilter = log.LogFilter(["eggs.bacon", "spam.ham"])
|
logfilter = log.LogFilter(["eggs.bacon", "spam.ham"])
|
||||||
record = self._make_record("eggs.bacon.spam")
|
record = self._make_record(logger, "eggs.bacon.spam")
|
||||||
self.assertTrue(logfilter.filter(record))
|
assert logfilter.filter(record)
|
||||||
record = self._make_record("spam.ham.salami")
|
record = self._make_record(logger, "spam.ham.salami")
|
||||||
self.assertTrue(logfilter.filter(record))
|
assert logfilter.filter(record)
|
||||||
|
|
||||||
def test_debug(self):
|
def test_debug(self, logger):
|
||||||
"""Test if messages more important than debug are never filtered."""
|
"""Test if messages more important than debug are never filtered."""
|
||||||
logfilter = log.LogFilter(["eggs"])
|
logfilter = log.LogFilter(["eggs"])
|
||||||
# First check if the filter works as intended with debug messages
|
# First check if the filter works as intended with debug messages
|
||||||
record = self._make_record("eggs")
|
record = self._make_record(logger, "eggs")
|
||||||
self.assertTrue(logfilter.filter(record))
|
assert logfilter.filter(record)
|
||||||
record = self._make_record("bacon")
|
record = self._make_record(logger, "bacon")
|
||||||
self.assertFalse(logfilter.filter(record))
|
assert not logfilter.filter(record)
|
||||||
# Then check if info is not filtered
|
# Then check if info is not filtered
|
||||||
record = self._make_record("eggs", level=logging.INFO)
|
record = self._make_record(logger, "eggs", level=logging.INFO)
|
||||||
self.assertTrue(logfilter.filter(record))
|
assert logfilter.filter(record)
|
||||||
record = self._make_record("bacon", level=logging.INFO)
|
record = self._make_record(logger, "bacon", level=logging.INFO)
|
||||||
self.assertTrue(logfilter.filter(record))
|
assert logfilter.filter(record)
|
||||||
|
|
||||||
|
|
||||||
class RAMHandlerTests(BaseTest):
|
class TestRAMHandler:
|
||||||
|
|
||||||
"""Tests for RAMHandler.
|
"""Tests for RAMHandler."""
|
||||||
|
|
||||||
Attributes:
|
@pytest.fixture
|
||||||
logger: The logger we use to log to the handler.
|
def handler(self, logger):
|
||||||
handler: The RAMHandler we're testing.
|
"""Fixture providing a RAMHandler."""
|
||||||
old_level: The level the root logger had before executing the test.
|
handler = log.RAMHandler(capacity=2)
|
||||||
old_handlers: The handlers the root logger had before executing the
|
handler.setLevel(logging.NOTSET)
|
||||||
test.
|
logger.addHandler(handler)
|
||||||
"""
|
return handler
|
||||||
|
|
||||||
def setUp(self):
|
def test_filled(self, handler, logger):
|
||||||
super().setUp()
|
|
||||||
self.logger = logging.getLogger()
|
|
||||||
self.logger.handlers = []
|
|
||||||
self.logger.setLevel(logging.NOTSET)
|
|
||||||
self.handler = log.RAMHandler(capacity=2)
|
|
||||||
self.handler.setLevel(logging.NOTSET)
|
|
||||||
self.logger.addHandler(self.handler)
|
|
||||||
|
|
||||||
def test_filled(self):
|
|
||||||
"""Test handler with exactly as much records as it can hold."""
|
"""Test handler with exactly as much records as it can hold."""
|
||||||
self.logger.debug("One")
|
logger.debug("One")
|
||||||
self.logger.debug("Two")
|
logger.debug("Two")
|
||||||
self.assertEqual(len(self.handler._data), 2)
|
assert len(handler._data) == 2
|
||||||
self.assertEqual(self.handler._data[0].msg, "One")
|
assert handler._data[0].msg == "One"
|
||||||
self.assertEqual(self.handler._data[1].msg, "Two")
|
assert handler._data[1].msg == "Two"
|
||||||
|
|
||||||
def test_overflow(self):
|
def test_overflow(self, handler, logger):
|
||||||
"""Test handler with more records as it can hold."""
|
"""Test handler with more records as it can hold."""
|
||||||
self.logger.debug("One")
|
logger.debug("One")
|
||||||
self.logger.debug("Two")
|
logger.debug("Two")
|
||||||
self.logger.debug("Three")
|
logger.debug("Three")
|
||||||
self.assertEqual(len(self.handler._data), 2)
|
assert len(handler._data) == 2
|
||||||
self.assertEqual(self.handler._data[0].msg, "Two")
|
assert handler._data[0].msg == "Two"
|
||||||
self.assertEqual(self.handler._data[1].msg, "Three")
|
assert handler._data[1].msg == "Three"
|
||||||
|
|
||||||
def test_dump_log(self):
|
def test_dump_log(self, handler, logger):
|
||||||
"""Test dump_log()."""
|
"""Test dump_log()."""
|
||||||
self.logger.debug("One")
|
logger.debug("One")
|
||||||
self.logger.debug("Two")
|
logger.debug("Two")
|
||||||
self.logger.debug("Three")
|
logger.debug("Three")
|
||||||
self.assertEqual(self.handler.dump_log(), "Two\nThree")
|
assert handler.dump_log() == "Two\nThree"
|
||||||
|
|
||||||
|
|
||||||
@mock.patch('qutebrowser.utils.log.qInstallMessageHandler', autospec=True)
|
class TestInitLog:
|
||||||
class InitLogTests(BaseTest):
|
|
||||||
|
|
||||||
"""Tests for init_log."""
|
"""Tests for init_log."""
|
||||||
|
|
||||||
def setUp(self):
|
@pytest.fixture(autouse=True)
|
||||||
super().setUp()
|
def setup(self, mocker):
|
||||||
self.args = argparse.Namespace(debug=True, loglevel=logging.DEBUG,
|
"""Mock out qInstallMessageHandler."""
|
||||||
color=True, loglines=10, logfilter="")
|
mocker.patch('qutebrowser.utils.log.QtCore.qInstallMessageHandler',
|
||||||
|
autospec=True)
|
||||||
|
|
||||||
def test_stderr_none(self, _mock):
|
@pytest.fixture
|
||||||
|
def args(self):
|
||||||
|
"""Fixture providing an argparse namespace."""
|
||||||
|
return argparse.Namespace(debug=True, loglevel=logging.DEBUG,
|
||||||
|
color=True, loglines=10, logfilter="")
|
||||||
|
|
||||||
|
def test_stderr_none(self, args):
|
||||||
"""Test init_log with sys.stderr = None."""
|
"""Test init_log with sys.stderr = None."""
|
||||||
old_stderr = sys.stderr
|
old_stderr = sys.stderr
|
||||||
sys.stderr = None
|
sys.stderr = None
|
||||||
log.init_log(self.args)
|
log.init_log(args)
|
||||||
sys.stderr = old_stderr
|
sys.stderr = old_stderr
|
||||||
|
|
||||||
|
|
||||||
class HideQtWarningTests(BaseTest):
|
class TestHideQtWarning:
|
||||||
|
|
||||||
"""Tests for hide_qt_warning/QtWarningFilter."""
|
"""Tests for hide_qt_warning/QtWarningFilter."""
|
||||||
|
|
||||||
def test_unfiltered(self):
|
def test_unfiltered(self, caplog):
|
||||||
"""Test a message which is not filtered."""
|
"""Test a message which is not filtered."""
|
||||||
with log.hide_qt_warning("World", logger='qt-tests'):
|
with log.hide_qt_warning("World", logger='qt-tests'):
|
||||||
with self.assertLogs('qt-tests', logging.WARNING):
|
with caplog.atLevel(logging.WARNING, logger='qt-tests'):
|
||||||
qWarning("Hello World")
|
qWarning("Hello World")
|
||||||
|
assert len(caplog.records()) == 1
|
||||||
|
record = caplog.records()[0]
|
||||||
|
assert record.levelname == 'WARNING'
|
||||||
|
assert record.message == "Hello World"
|
||||||
|
|
||||||
def test_filtered_exact(self):
|
def test_filtered_exact(self, caplog):
|
||||||
"""Test a message which is filtered (exact match)."""
|
"""Test a message which is filtered (exact match)."""
|
||||||
with log.hide_qt_warning("Hello", logger='qt-tests'):
|
with log.hide_qt_warning("Hello", logger='qt-tests'):
|
||||||
qWarning("Hello")
|
with caplog.atLevel(logging.WARNING, logger='qt-tests'):
|
||||||
|
qWarning("Hello")
|
||||||
|
assert not caplog.records()
|
||||||
|
|
||||||
def test_filtered_start(self):
|
def test_filtered_start(self, caplog):
|
||||||
"""Test a message which is filtered (match at line start)."""
|
"""Test a message which is filtered (match at line start)."""
|
||||||
with log.hide_qt_warning("Hello", logger='qt-tests'):
|
with log.hide_qt_warning("Hello", logger='qt-tests'):
|
||||||
qWarning("Hello World")
|
with caplog.atLevel(logging.WARNING, logger='qt-tests'):
|
||||||
|
qWarning("Hello World")
|
||||||
|
assert not caplog.records()
|
||||||
|
|
||||||
def test_filtered_whitespace(self):
|
def test_filtered_whitespace(self, caplog):
|
||||||
"""Test a message which is filtered (match with whitespace)."""
|
"""Test a message which is filtered (match with whitespace)."""
|
||||||
with log.hide_qt_warning("Hello", logger='qt-tests'):
|
with log.hide_qt_warning("Hello", logger='qt-tests'):
|
||||||
qWarning(" Hello World ")
|
with caplog.atLevel(logging.WARNING, logger='qt-tests'):
|
||||||
|
qWarning(" Hello World ")
|
||||||
|
assert not caplog.records()
|
||||||
if __name__ == '__main__':
|
|
||||||
unittest.main()
|
|
||||||
|
@ -22,6 +22,8 @@
|
|||||||
import os
|
import os
|
||||||
import os.path
|
import os.path
|
||||||
import sys
|
import sys
|
||||||
|
import types
|
||||||
|
import collections
|
||||||
|
|
||||||
from PyQt5.QtWidgets import QApplication
|
from PyQt5.QtWidgets import QApplication
|
||||||
import pytest
|
import pytest
|
||||||
@ -114,3 +116,51 @@ class TestGetStandardDirWindows:
|
|||||||
"""Test cache dir."""
|
"""Test cache dir."""
|
||||||
expected = ['qutebrowser_test', 'cache']
|
expected = ['qutebrowser_test', 'cache']
|
||||||
assert standarddir.cache().split(os.sep)[-2:] == expected
|
assert standarddir.cache().split(os.sep)[-2:] == expected
|
||||||
|
|
||||||
|
|
||||||
|
DirArgTest = collections.namedtuple('DirArgTest', 'arg, expected')
|
||||||
|
|
||||||
|
|
||||||
|
class TestArguments:
|
||||||
|
|
||||||
|
"""Tests with confdir/cachedir/datadir arguments."""
|
||||||
|
|
||||||
|
@pytest.fixture(params=[DirArgTest('', None), DirArgTest('foo', 'foo')])
|
||||||
|
def testcase(self, request, tmpdir):
|
||||||
|
"""Fixture providing testcases."""
|
||||||
|
if request.param.expected is None:
|
||||||
|
return request.param
|
||||||
|
else:
|
||||||
|
arg = str(tmpdir / request.param.arg)
|
||||||
|
return DirArgTest(arg, arg)
|
||||||
|
|
||||||
|
def test_confdir(self, testcase):
|
||||||
|
"""Test --confdir."""
|
||||||
|
args = types.SimpleNamespace(confdir=testcase.arg, cachedir=None,
|
||||||
|
datadir=None)
|
||||||
|
standarddir.init(args)
|
||||||
|
assert standarddir.config() == testcase.expected
|
||||||
|
|
||||||
|
def test_cachedir(self, testcase):
|
||||||
|
"""Test --cachedir."""
|
||||||
|
args = types.SimpleNamespace(confdir=None, cachedir=testcase.arg,
|
||||||
|
datadir=None)
|
||||||
|
standarddir.init(args)
|
||||||
|
assert standarddir.cache() == testcase.expected
|
||||||
|
|
||||||
|
def test_datadir(self, testcase):
|
||||||
|
"""Test --datadir."""
|
||||||
|
args = types.SimpleNamespace(confdir=None, cachedir=None,
|
||||||
|
datadir=testcase.arg)
|
||||||
|
standarddir.init(args)
|
||||||
|
assert standarddir.data() == testcase.expected
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('typ', ['config', 'data', 'cache', 'download',
|
||||||
|
'runtime'])
|
||||||
|
def test_basedir(self, tmpdir, typ):
|
||||||
|
"""Test --basedir."""
|
||||||
|
expected = str(tmpdir / typ)
|
||||||
|
args = types.SimpleNamespace(basedir=str(tmpdir))
|
||||||
|
standarddir.init(args)
|
||||||
|
func = getattr(standarddir, typ)
|
||||||
|
assert func() == expected
|
||||||
|
@ -81,10 +81,10 @@ class TestSearchUrl:
|
|||||||
"""Test _get_search_url."""
|
"""Test _get_search_url."""
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def mock_config(self, config_stub, mocker):
|
def mock_config(self, config_stub, monkeypatch):
|
||||||
"""Fixture to patch urlutils.config with a stub."""
|
"""Fixture to patch urlutils.config with a stub."""
|
||||||
init_config_stub(config_stub)
|
init_config_stub(config_stub)
|
||||||
mocker.patch('qutebrowser.utils.urlutils.config', config_stub)
|
monkeypatch.setattr('qutebrowser.utils.urlutils.config', config_stub)
|
||||||
|
|
||||||
def test_default_engine(self):
|
def test_default_engine(self):
|
||||||
"""Test default search engine."""
|
"""Test default search engine."""
|
||||||
@ -159,24 +159,24 @@ class TestIsUrl:
|
|||||||
)
|
)
|
||||||
|
|
||||||
@pytest.mark.parametrize('url', URLS)
|
@pytest.mark.parametrize('url', URLS)
|
||||||
def test_urls(self, mocker, config_stub, url):
|
def test_urls(self, monkeypatch, config_stub, url):
|
||||||
"""Test things which are URLs."""
|
"""Test things which are URLs."""
|
||||||
init_config_stub(config_stub, 'naive')
|
init_config_stub(config_stub, 'naive')
|
||||||
mocker.patch('qutebrowser.utils.urlutils.config', config_stub)
|
monkeypatch.setattr('qutebrowser.utils.urlutils.config', config_stub)
|
||||||
assert urlutils.is_url(url), url
|
assert urlutils.is_url(url), url
|
||||||
|
|
||||||
@pytest.mark.parametrize('url', NOT_URLS)
|
@pytest.mark.parametrize('url', NOT_URLS)
|
||||||
def test_not_urls(self, mocker, config_stub, url):
|
def test_not_urls(self, monkeypatch, config_stub, url):
|
||||||
"""Test things which are not URLs."""
|
"""Test things which are not URLs."""
|
||||||
init_config_stub(config_stub, 'naive')
|
init_config_stub(config_stub, 'naive')
|
||||||
mocker.patch('qutebrowser.utils.urlutils.config', config_stub)
|
monkeypatch.setattr('qutebrowser.utils.urlutils.config', config_stub)
|
||||||
assert not urlutils.is_url(url), url
|
assert not urlutils.is_url(url), url
|
||||||
|
|
||||||
@pytest.mark.parametrize('autosearch', [True, False])
|
@pytest.mark.parametrize('autosearch', [True, False])
|
||||||
def test_search_autosearch(self, mocker, config_stub, autosearch):
|
def test_search_autosearch(self, monkeypatch, config_stub, autosearch):
|
||||||
"""Test explicit search with auto-search=True."""
|
"""Test explicit search with auto-search=True."""
|
||||||
init_config_stub(config_stub, autosearch)
|
init_config_stub(config_stub, autosearch)
|
||||||
mocker.patch('qutebrowser.utils.urlutils.config', config_stub)
|
monkeypatch.setattr('qutebrowser.utils.urlutils.config', config_stub)
|
||||||
assert not urlutils.is_url('test foo')
|
assert not urlutils.is_url('test foo')
|
||||||
|
|
||||||
|
|
||||||
|
@ -313,17 +313,15 @@ class TestKeyEventToString:
|
|||||||
def test_key_and_modifier(self, fake_keyevent_factory):
|
def test_key_and_modifier(self, fake_keyevent_factory):
|
||||||
"""Test with key and modifier pressed."""
|
"""Test with key and modifier pressed."""
|
||||||
evt = fake_keyevent_factory(key=Qt.Key_A, modifiers=Qt.ControlModifier)
|
evt = fake_keyevent_factory(key=Qt.Key_A, modifiers=Qt.ControlModifier)
|
||||||
assert utils.keyevent_to_string(evt) == 'Ctrl+A'
|
expected = 'Meta+A' if sys.platform == 'darwin' else 'Ctrl+A'
|
||||||
|
assert utils.keyevent_to_string(evt) == expected
|
||||||
|
|
||||||
def test_key_and_modifiers(self, fake_keyevent_factory):
|
def test_key_and_modifiers(self, fake_keyevent_factory):
|
||||||
"""Test with key and multiple modifier pressed."""
|
"""Test with key and multiple modifiers pressed."""
|
||||||
evt = fake_keyevent_factory(
|
evt = fake_keyevent_factory(
|
||||||
key=Qt.Key_A, modifiers=(Qt.ControlModifier | Qt.AltModifier |
|
key=Qt.Key_A, modifiers=(Qt.ControlModifier | Qt.AltModifier |
|
||||||
Qt.MetaModifier | Qt.ShiftModifier))
|
Qt.MetaModifier | Qt.ShiftModifier))
|
||||||
if sys.platform == 'darwin':
|
assert utils.keyevent_to_string(evt) == 'Ctrl+Alt+Meta+Shift+A'
|
||||||
assert utils.keyevent_to_string(evt) == 'Ctrl+Alt+Shift+A'
|
|
||||||
else:
|
|
||||||
assert utils.keyevent_to_string(evt) == 'Ctrl+Alt+Meta+Shift+A'
|
|
||||||
|
|
||||||
|
|
||||||
class TestNormalize:
|
class TestNormalize:
|
||||||
|
@ -19,45 +19,38 @@
|
|||||||
|
|
||||||
"""Tests for the Enum class."""
|
"""Tests for the Enum class."""
|
||||||
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
from qutebrowser.utils import usertypes
|
from qutebrowser.utils import usertypes
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
# FIXME: Add some more tests, e.g. for is_int
|
# FIXME: Add some more tests, e.g. for is_int
|
||||||
|
|
||||||
|
|
||||||
class EnumTests(unittest.TestCase):
|
@pytest.fixture
|
||||||
|
def enum():
|
||||||
"""Test simple enums.
|
return usertypes.enum('Enum', ['one', 'two'])
|
||||||
|
|
||||||
Attributes:
|
|
||||||
enum: The enum we're testing.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.enum = usertypes.enum('Enum', ['one', 'two'])
|
|
||||||
|
|
||||||
def test_values(self):
|
|
||||||
"""Test if enum members resolve to the right values."""
|
|
||||||
self.assertEqual(self.enum.one.value, 1)
|
|
||||||
self.assertEqual(self.enum.two.value, 2)
|
|
||||||
|
|
||||||
def test_name(self):
|
|
||||||
"""Test .name mapping."""
|
|
||||||
self.assertEqual(self.enum.one.name, 'one')
|
|
||||||
self.assertEqual(self.enum.two.name, 'two')
|
|
||||||
|
|
||||||
def test_unknown(self):
|
|
||||||
"""Test invalid values which should raise an AttributeError."""
|
|
||||||
with self.assertRaises(AttributeError):
|
|
||||||
_ = self.enum.three
|
|
||||||
|
|
||||||
def test_start(self):
|
|
||||||
"""Test the start= argument."""
|
|
||||||
e = usertypes.enum('Enum', ['three', 'four'], start=3)
|
|
||||||
self.assertEqual(e.three.value, 3)
|
|
||||||
self.assertEqual(e.four.value, 4)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
def test_values(enum):
|
||||||
unittest.main()
|
"""Test if enum members resolve to the right values."""
|
||||||
|
assert enum.one.value == 1
|
||||||
|
assert enum.two.value == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_name(enum):
|
||||||
|
"""Test .name mapping."""
|
||||||
|
assert enum.one.name == 'one'
|
||||||
|
assert enum.two.name == 'two'
|
||||||
|
|
||||||
|
|
||||||
|
def test_unknown(enum):
|
||||||
|
"""Test invalid values which should raise an AttributeError."""
|
||||||
|
with pytest.raises(AttributeError):
|
||||||
|
_ = enum.three
|
||||||
|
|
||||||
|
|
||||||
|
def test_start():
|
||||||
|
"""Test the start= argument."""
|
||||||
|
e = usertypes.enum('Enum', ['three', 'four'], start=3)
|
||||||
|
assert e.three.value == 3
|
||||||
|
assert e.four.value == 4
|
||||||
|
@ -21,364 +21,331 @@
|
|||||||
|
|
||||||
"""Tests for the NeighborList class."""
|
"""Tests for the NeighborList class."""
|
||||||
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
from qutebrowser.utils import usertypes
|
from qutebrowser.utils import usertypes
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
class InitTests(unittest.TestCase):
|
|
||||||
|
|
||||||
"""Just try to init some neighborlists.
|
class TestInit:
|
||||||
|
|
||||||
Attributes:
|
"""Just try to init some neighborlists."""
|
||||||
nl: The NeighborList we're testing.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def test_empty(self):
|
def test_empty(self):
|
||||||
"""Test constructing an empty NeighborList."""
|
"""Test constructing an empty NeighborList."""
|
||||||
nl = usertypes.NeighborList()
|
nl = usertypes.NeighborList()
|
||||||
self.assertEqual(nl.items, [])
|
assert nl.items == []
|
||||||
|
|
||||||
def test_items(self):
|
def test_items(self):
|
||||||
"""Test constructing an NeighborList with items."""
|
"""Test constructing an NeighborList with items."""
|
||||||
nl = usertypes.NeighborList([1, 2, 3])
|
nl = usertypes.NeighborList([1, 2, 3])
|
||||||
self.assertEqual(nl.items, [1, 2, 3])
|
assert nl.items == [1, 2, 3]
|
||||||
|
|
||||||
def test_len(self):
|
def test_len(self):
|
||||||
"""Test len() on NeighborList."""
|
"""Test len() on NeighborList."""
|
||||||
nl = usertypes.NeighborList([1, 2, 3])
|
nl = usertypes.NeighborList([1, 2, 3])
|
||||||
self.assertEqual(len(nl), 3)
|
assert len(nl) == 3
|
||||||
|
|
||||||
def test_contains(self):
|
def test_contains(self):
|
||||||
"""Test 'in' on NeighborList."""
|
"""Test 'in' on NeighborList."""
|
||||||
nl = usertypes.NeighborList([1, 2, 3])
|
nl = usertypes.NeighborList([1, 2, 3])
|
||||||
self.assertIn(2, nl)
|
assert 2 in nl
|
||||||
self.assertNotIn(4, nl)
|
assert 4 not in nl
|
||||||
|
|
||||||
|
|
||||||
class DefaultTests(unittest.TestCase):
|
class TestDefaultArg:
|
||||||
|
|
||||||
"""Test the default argument.
|
"""Test the default argument."""
|
||||||
|
|
||||||
Attributes:
|
|
||||||
nl: The NeighborList we're testing.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def test_simple(self):
|
def test_simple(self):
|
||||||
"""Test default with a numeric argument."""
|
"""Test default with a numeric argument."""
|
||||||
nl = usertypes.NeighborList([1, 2, 3], default=2)
|
nl = usertypes.NeighborList([1, 2, 3], default=2)
|
||||||
self.assertEqual(nl._idx, 1)
|
assert nl._idx == 1
|
||||||
|
|
||||||
def test_none(self):
|
def test_none(self):
|
||||||
"""Test default 'None'."""
|
"""Test default 'None'."""
|
||||||
nl = usertypes.NeighborList([1, 2, None], default=None)
|
nl = usertypes.NeighborList([1, 2, None], default=None)
|
||||||
self.assertEqual(nl._idx, 2)
|
assert nl._idx == 2
|
||||||
|
|
||||||
def test_unset(self):
|
def test_unset(self):
|
||||||
"""Test unset default value."""
|
"""Test unset default value."""
|
||||||
nl = usertypes.NeighborList([1, 2, 3])
|
nl = usertypes.NeighborList([1, 2, 3])
|
||||||
self.assertIsNone(nl._idx)
|
assert nl._idx is None
|
||||||
|
|
||||||
|
|
||||||
class EmptyTests(unittest.TestCase):
|
class TestEmpty:
|
||||||
|
|
||||||
"""Tests with no items.
|
"""Tests with no items."""
|
||||||
|
|
||||||
Attributes:
|
@pytest.fixture
|
||||||
nl: The NeighborList we're testing.
|
def neighborlist(self):
|
||||||
"""
|
return usertypes.NeighborList()
|
||||||
|
|
||||||
def setUp(self):
|
def test_curitem(self, neighborlist):
|
||||||
self.nl = usertypes.NeighborList()
|
|
||||||
|
|
||||||
def test_curitem(self):
|
|
||||||
"""Test curitem with no item."""
|
"""Test curitem with no item."""
|
||||||
with self.assertRaises(IndexError):
|
with pytest.raises(IndexError):
|
||||||
self.nl.curitem()
|
neighborlist.curitem()
|
||||||
|
|
||||||
def test_firstitem(self):
|
def test_firstitem(self, neighborlist):
|
||||||
"""Test firstitem with no item."""
|
"""Test firstitem with no item."""
|
||||||
with self.assertRaises(IndexError):
|
with pytest.raises(IndexError):
|
||||||
self.nl.firstitem()
|
neighborlist.firstitem()
|
||||||
|
|
||||||
def test_lastitem(self):
|
def test_lastitem(self, neighborlist):
|
||||||
"""Test lastitem with no item."""
|
"""Test lastitem with no item."""
|
||||||
with self.assertRaises(IndexError):
|
with pytest.raises(IndexError):
|
||||||
self.nl.lastitem()
|
neighborlist.lastitem()
|
||||||
|
|
||||||
def test_getitem(self):
|
def test_getitem(self, neighborlist):
|
||||||
"""Test getitem with no item."""
|
"""Test getitem with no item."""
|
||||||
with self.assertRaises(IndexError):
|
with pytest.raises(IndexError):
|
||||||
self.nl.getitem(1)
|
neighborlist.getitem(1)
|
||||||
|
|
||||||
|
|
||||||
class ItemTests(unittest.TestCase):
|
class TestItems:
|
||||||
|
|
||||||
"""Tests with items.
|
"""Tests with items."""
|
||||||
|
|
||||||
Attributes:
|
@pytest.fixture
|
||||||
nl: The NeighborList we're testing.
|
def neighborlist(self):
|
||||||
"""
|
return usertypes.NeighborList([1, 2, 3, 4, 5], default=3)
|
||||||
|
|
||||||
def setUp(self):
|
def test_curitem(self, neighborlist):
|
||||||
self.nl = usertypes.NeighborList([1, 2, 3, 4, 5], default=3)
|
|
||||||
|
|
||||||
def test_curitem(self):
|
|
||||||
"""Test curitem()."""
|
"""Test curitem()."""
|
||||||
self.assertEqual(self.nl._idx, 2)
|
assert neighborlist._idx == 2
|
||||||
self.assertEqual(self.nl.curitem(), 3)
|
assert neighborlist.curitem() == 3
|
||||||
self.assertEqual(self.nl._idx, 2)
|
assert neighborlist._idx == 2
|
||||||
|
|
||||||
def test_nextitem(self):
|
def test_nextitem(self, neighborlist):
|
||||||
"""Test nextitem()."""
|
"""Test nextitem()."""
|
||||||
self.assertEqual(self.nl.nextitem(), 4)
|
assert neighborlist.nextitem() == 4
|
||||||
self.assertEqual(self.nl._idx, 3)
|
assert neighborlist._idx == 3
|
||||||
self.assertEqual(self.nl.nextitem(), 5)
|
assert neighborlist.nextitem() == 5
|
||||||
self.assertEqual(self.nl._idx, 4)
|
assert neighborlist._idx == 4
|
||||||
|
|
||||||
def test_previtem(self):
|
def test_previtem(self, neighborlist):
|
||||||
"""Test previtem()."""
|
"""Test previtem()."""
|
||||||
self.assertEqual(self.nl.previtem(), 2)
|
assert neighborlist.previtem() == 2
|
||||||
self.assertEqual(self.nl._idx, 1)
|
assert neighborlist._idx == 1
|
||||||
self.assertEqual(self.nl.previtem(), 1)
|
assert neighborlist.previtem() == 1
|
||||||
self.assertEqual(self.nl._idx, 0)
|
assert neighborlist._idx == 0
|
||||||
|
|
||||||
def test_firstitem(self):
|
def test_firstitem(self, neighborlist):
|
||||||
"""Test firstitem()."""
|
"""Test firstitem()."""
|
||||||
self.assertEqual(self.nl.firstitem(), 1)
|
assert neighborlist.firstitem() == 1
|
||||||
self.assertEqual(self.nl._idx, 0)
|
assert neighborlist._idx == 0
|
||||||
|
|
||||||
def test_lastitem(self):
|
def test_lastitem(self, neighborlist):
|
||||||
"""Test lastitem()."""
|
"""Test lastitem()."""
|
||||||
self.assertEqual(self.nl.lastitem(), 5)
|
assert neighborlist.lastitem() == 5
|
||||||
self.assertEqual(self.nl._idx, 4)
|
assert neighborlist._idx == 4
|
||||||
|
|
||||||
def test_reset(self):
|
def test_reset(self, neighborlist):
|
||||||
"""Test reset()."""
|
"""Test reset()."""
|
||||||
self.nl.nextitem()
|
neighborlist.nextitem()
|
||||||
self.assertEqual(self.nl._idx, 3)
|
assert neighborlist._idx == 3
|
||||||
self.nl.reset()
|
neighborlist.reset()
|
||||||
self.assertEqual(self.nl._idx, 2)
|
assert neighborlist._idx == 2
|
||||||
|
|
||||||
def test_getitem(self):
|
def test_getitem(self, neighborlist):
|
||||||
"""Test getitem()."""
|
"""Test getitem()."""
|
||||||
self.assertEqual(self.nl.getitem(2), 5)
|
assert neighborlist.getitem(2) == 5
|
||||||
self.assertEqual(self.nl._idx, 4)
|
assert neighborlist._idx == 4
|
||||||
self.nl.reset()
|
neighborlist.reset()
|
||||||
self.assertEqual(self.nl.getitem(-2), 1)
|
assert neighborlist.getitem(-2) == 1
|
||||||
self.assertEqual(self.nl._idx, 0)
|
assert neighborlist._idx == 0
|
||||||
|
|
||||||
|
|
||||||
class OneTests(unittest.TestCase):
|
class TestSingleItem:
|
||||||
|
|
||||||
"""Tests with a list with only one item.
|
"""Tests with a list with only one item."""
|
||||||
|
|
||||||
Attributes:
|
@pytest.fixture
|
||||||
nl: The NeighborList we're testing.
|
def neighborlist(self):
|
||||||
"""
|
return usertypes.NeighborList([1], default=1)
|
||||||
|
|
||||||
def setUp(self):
|
def test_first_wrap(self, neighborlist):
|
||||||
self.nl = usertypes.NeighborList([1], default=1)
|
|
||||||
|
|
||||||
def test_first_wrap(self):
|
|
||||||
"""Test out of bounds previtem() with mode=wrap."""
|
"""Test out of bounds previtem() with mode=wrap."""
|
||||||
self.nl._mode = usertypes.NeighborList.Modes.wrap
|
neighborlist._mode = usertypes.NeighborList.Modes.wrap
|
||||||
self.nl.firstitem()
|
neighborlist.firstitem()
|
||||||
self.assertEqual(self.nl._idx, 0)
|
assert neighborlist._idx == 0
|
||||||
self.assertEqual(self.nl.previtem(), 1)
|
assert neighborlist.previtem() == 1
|
||||||
self.assertEqual(self.nl._idx, 0)
|
assert neighborlist._idx == 0
|
||||||
|
|
||||||
def test_first_block(self):
|
def test_first_block(self, neighborlist):
|
||||||
"""Test out of bounds previtem() with mode=block."""
|
"""Test out of bounds previtem() with mode=block."""
|
||||||
self.nl._mode = usertypes.NeighborList.Modes.block
|
neighborlist._mode = usertypes.NeighborList.Modes.block
|
||||||
self.nl.firstitem()
|
neighborlist.firstitem()
|
||||||
self.assertEqual(self.nl._idx, 0)
|
assert neighborlist._idx == 0
|
||||||
self.assertEqual(self.nl.previtem(), 1)
|
assert neighborlist.previtem() == 1
|
||||||
self.assertEqual(self.nl._idx, 0)
|
assert neighborlist._idx == 0
|
||||||
|
|
||||||
def test_first_raise(self):
|
def test_first_raise(self, neighborlist):
|
||||||
"""Test out of bounds previtem() with mode=raise."""
|
"""Test out of bounds previtem() with mode=raise."""
|
||||||
self.nl._mode = usertypes.NeighborList.Modes.exception
|
neighborlist._mode = usertypes.NeighborList.Modes.exception
|
||||||
self.nl.firstitem()
|
neighborlist.firstitem()
|
||||||
self.assertEqual(self.nl._idx, 0)
|
assert neighborlist._idx == 0
|
||||||
with self.assertRaises(IndexError):
|
with pytest.raises(IndexError):
|
||||||
self.nl.previtem()
|
neighborlist.previtem()
|
||||||
self.assertEqual(self.nl._idx, 0)
|
assert neighborlist._idx == 0
|
||||||
|
|
||||||
def test_last_wrap(self):
|
def test_last_wrap(self, neighborlist):
|
||||||
"""Test out of bounds nextitem() with mode=wrap."""
|
"""Test out of bounds nextitem() with mode=wrap."""
|
||||||
self.nl._mode = usertypes.NeighborList.Modes.wrap
|
neighborlist._mode = usertypes.NeighborList.Modes.wrap
|
||||||
self.nl.lastitem()
|
neighborlist.lastitem()
|
||||||
self.assertEqual(self.nl._idx, 0)
|
assert neighborlist._idx == 0
|
||||||
self.assertEqual(self.nl.nextitem(), 1)
|
assert neighborlist.nextitem() == 1
|
||||||
self.assertEqual(self.nl._idx, 0)
|
assert neighborlist._idx == 0
|
||||||
|
|
||||||
def test_last_block(self):
|
def test_last_block(self, neighborlist):
|
||||||
"""Test out of bounds nextitem() with mode=block."""
|
"""Test out of bounds nextitem() with mode=block."""
|
||||||
self.nl._mode = usertypes.NeighborList.Modes.block
|
neighborlist._mode = usertypes.NeighborList.Modes.block
|
||||||
self.nl.lastitem()
|
neighborlist.lastitem()
|
||||||
self.assertEqual(self.nl._idx, 0)
|
assert neighborlist._idx == 0
|
||||||
self.assertEqual(self.nl.nextitem(), 1)
|
assert neighborlist.nextitem() == 1
|
||||||
self.assertEqual(self.nl._idx, 0)
|
assert neighborlist._idx == 0
|
||||||
|
|
||||||
def test_last_raise(self):
|
def test_last_raise(self, neighborlist):
|
||||||
"""Test out of bounds nextitem() with mode=raise."""
|
"""Test out of bounds nextitem() with mode=raise."""
|
||||||
self.nl._mode = usertypes.NeighborList.Modes.exception
|
neighborlist._mode = usertypes.NeighborList.Modes.exception
|
||||||
self.nl.lastitem()
|
neighborlist.lastitem()
|
||||||
self.assertEqual(self.nl._idx, 0)
|
assert neighborlist._idx == 0
|
||||||
with self.assertRaises(IndexError):
|
with pytest.raises(IndexError):
|
||||||
self.nl.nextitem()
|
neighborlist.nextitem()
|
||||||
self.assertEqual(self.nl._idx, 0)
|
assert neighborlist._idx == 0
|
||||||
|
|
||||||
|
|
||||||
class BlockTests(unittest.TestCase):
|
class TestBlockMode:
|
||||||
|
|
||||||
"""Tests with mode=block.
|
"""Tests with mode=block."""
|
||||||
|
|
||||||
Attributes:
|
@pytest.fixture
|
||||||
nl: The NeighborList we're testing.
|
def neighborlist(self):
|
||||||
"""
|
return usertypes.NeighborList(
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.nl = usertypes.NeighborList(
|
|
||||||
[1, 2, 3, 4, 5], default=3,
|
[1, 2, 3, 4, 5], default=3,
|
||||||
mode=usertypes.NeighborList.Modes.block)
|
mode=usertypes.NeighborList.Modes.block)
|
||||||
|
|
||||||
def test_first(self):
|
def test_first(self, neighborlist):
|
||||||
"""Test out of bounds previtem()."""
|
"""Test out of bounds previtem()."""
|
||||||
self.nl.firstitem()
|
neighborlist.firstitem()
|
||||||
self.assertEqual(self.nl._idx, 0)
|
assert neighborlist._idx == 0
|
||||||
self.assertEqual(self.nl.previtem(), 1)
|
assert neighborlist.previtem() == 1
|
||||||
self.assertEqual(self.nl._idx, 0)
|
assert neighborlist._idx == 0
|
||||||
|
|
||||||
def test_last(self):
|
def test_last(self, neighborlist):
|
||||||
"""Test out of bounds nextitem()."""
|
"""Test out of bounds nextitem()."""
|
||||||
self.nl.lastitem()
|
neighborlist.lastitem()
|
||||||
self.assertEqual(self.nl._idx, 4)
|
assert neighborlist._idx == 4
|
||||||
self.assertEqual(self.nl.nextitem(), 5)
|
assert neighborlist.nextitem() == 5
|
||||||
self.assertEqual(self.nl._idx, 4)
|
assert neighborlist._idx == 4
|
||||||
|
|
||||||
|
|
||||||
class WrapTests(unittest.TestCase):
|
class TestWrapMode:
|
||||||
|
|
||||||
"""Tests with mode=wrap.
|
"""Tests with mode=wrap."""
|
||||||
|
|
||||||
Attributes:
|
@pytest.fixture
|
||||||
nl: The NeighborList we're testing.
|
def neighborlist(self):
|
||||||
"""
|
return usertypes.NeighborList(
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.nl = usertypes.NeighborList(
|
|
||||||
[1, 2, 3, 4, 5], default=3, mode=usertypes.NeighborList.Modes.wrap)
|
[1, 2, 3, 4, 5], default=3, mode=usertypes.NeighborList.Modes.wrap)
|
||||||
|
|
||||||
def test_first(self):
|
def test_first(self, neighborlist):
|
||||||
"""Test out of bounds previtem()."""
|
"""Test out of bounds previtem()."""
|
||||||
self.nl.firstitem()
|
neighborlist.firstitem()
|
||||||
self.assertEqual(self.nl._idx, 0)
|
assert neighborlist._idx == 0
|
||||||
self.assertEqual(self.nl.previtem(), 5)
|
assert neighborlist.previtem() == 5
|
||||||
self.assertEqual(self.nl._idx, 4)
|
assert neighborlist._idx == 4
|
||||||
|
|
||||||
def test_last(self):
|
def test_last(self, neighborlist):
|
||||||
"""Test out of bounds nextitem()."""
|
"""Test out of bounds nextitem()."""
|
||||||
self.nl.lastitem()
|
neighborlist.lastitem()
|
||||||
self.assertEqual(self.nl._idx, 4)
|
assert neighborlist._idx == 4
|
||||||
self.assertEqual(self.nl.nextitem(), 1)
|
assert neighborlist.nextitem() == 1
|
||||||
self.assertEqual(self.nl._idx, 0)
|
assert neighborlist._idx == 0
|
||||||
|
|
||||||
|
|
||||||
class RaiseTests(unittest.TestCase):
|
class TestExceptionMode:
|
||||||
|
|
||||||
"""Tests with mode=exception.
|
"""Tests with mode=exception."""
|
||||||
|
|
||||||
Attributes:
|
@pytest.fixture
|
||||||
nl: The NeighborList we're testing.
|
def neighborlist(self):
|
||||||
"""
|
return usertypes.NeighborList(
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.nl = usertypes.NeighborList(
|
|
||||||
[1, 2, 3, 4, 5], default=3,
|
[1, 2, 3, 4, 5], default=3,
|
||||||
mode=usertypes.NeighborList.Modes.exception)
|
mode=usertypes.NeighborList.Modes.exception)
|
||||||
|
|
||||||
def test_first(self):
|
def test_first(self, neighborlist):
|
||||||
"""Test out of bounds previtem()."""
|
"""Test out of bounds previtem()."""
|
||||||
self.nl.firstitem()
|
neighborlist.firstitem()
|
||||||
self.assertEqual(self.nl._idx, 0)
|
assert neighborlist._idx == 0
|
||||||
with self.assertRaises(IndexError):
|
with pytest.raises(IndexError):
|
||||||
self.nl.previtem()
|
neighborlist.previtem()
|
||||||
self.assertEqual(self.nl._idx, 0)
|
assert neighborlist._idx == 0
|
||||||
|
|
||||||
def test_last(self):
|
def test_last(self, neighborlist):
|
||||||
"""Test out of bounds nextitem()."""
|
"""Test out of bounds nextitem()."""
|
||||||
self.nl.lastitem()
|
neighborlist.lastitem()
|
||||||
self.assertEqual(self.nl._idx, 4)
|
assert neighborlist._idx == 4
|
||||||
with self.assertRaises(IndexError):
|
with pytest.raises(IndexError):
|
||||||
self.nl.nextitem()
|
neighborlist.nextitem()
|
||||||
self.assertEqual(self.nl._idx, 4)
|
assert neighborlist._idx == 4
|
||||||
|
|
||||||
|
|
||||||
class SnapInTests(unittest.TestCase):
|
class TestSnapIn:
|
||||||
|
|
||||||
"""Tests for the fuzzyval/_snap_in features.
|
"""Tests for the fuzzyval/_snap_in features."""
|
||||||
|
|
||||||
Attributes:
|
@pytest.fixture
|
||||||
nl: The NeighborList we're testing.
|
def neighborlist(self):
|
||||||
"""
|
return usertypes.NeighborList([20, 9, 1, 5])
|
||||||
|
|
||||||
def setUp(self):
|
def test_bigger(self, neighborlist):
|
||||||
self.nl = usertypes.NeighborList([20, 9, 1, 5])
|
|
||||||
|
|
||||||
def test_bigger(self):
|
|
||||||
"""Test fuzzyval with snapping to a bigger value."""
|
"""Test fuzzyval with snapping to a bigger value."""
|
||||||
self.nl.fuzzyval = 7
|
neighborlist.fuzzyval = 7
|
||||||
self.assertEqual(self.nl.nextitem(), 9)
|
assert neighborlist.nextitem() == 9
|
||||||
self.assertEqual(self.nl._idx, 1)
|
assert neighborlist._idx == 1
|
||||||
self.assertEqual(self.nl.nextitem(), 1)
|
assert neighborlist.nextitem() == 1
|
||||||
self.assertEqual(self.nl._idx, 2)
|
assert neighborlist._idx == 2
|
||||||
|
|
||||||
def test_smaller(self):
|
def test_smaller(self, neighborlist):
|
||||||
"""Test fuzzyval with snapping to a smaller value."""
|
"""Test fuzzyval with snapping to a smaller value."""
|
||||||
self.nl.fuzzyval = 7
|
neighborlist.fuzzyval = 7
|
||||||
self.assertEqual(self.nl.previtem(), 5)
|
assert neighborlist.previtem() == 5
|
||||||
self.assertEqual(self.nl._idx, 3)
|
assert neighborlist._idx == 3
|
||||||
self.assertEqual(self.nl.previtem(), 1)
|
assert neighborlist.previtem() == 1
|
||||||
self.assertEqual(self.nl._idx, 2)
|
assert neighborlist._idx == 2
|
||||||
|
|
||||||
def test_equal_bigger(self):
|
def test_equal_bigger(self, neighborlist):
|
||||||
"""Test fuzzyval with matching value, snapping to a bigger value."""
|
"""Test fuzzyval with matching value, snapping to a bigger value."""
|
||||||
self.nl.fuzzyval = 20
|
neighborlist.fuzzyval = 20
|
||||||
self.assertEqual(self.nl.nextitem(), 9)
|
assert neighborlist.nextitem() == 9
|
||||||
self.assertEqual(self.nl._idx, 1)
|
assert neighborlist._idx == 1
|
||||||
|
|
||||||
def test_equal_smaller(self):
|
def test_equal_smaller(self, neighborlist):
|
||||||
"""Test fuzzyval with matching value, snapping to a smaller value."""
|
"""Test fuzzyval with matching value, snapping to a smaller value."""
|
||||||
self.nl.fuzzyval = 5
|
neighborlist.fuzzyval = 5
|
||||||
self.assertEqual(self.nl.previtem(), 1)
|
assert neighborlist.previtem() == 1
|
||||||
self.assertEqual(self.nl._idx, 2)
|
assert neighborlist._idx == 2
|
||||||
|
|
||||||
def test_too_big_next(self):
|
def test_too_big_next(self, neighborlist):
|
||||||
"""Test fuzzyval/next with a value bigger than any in the list."""
|
"""Test fuzzyval/next with a value bigger than any in the list."""
|
||||||
self.nl.fuzzyval = 30
|
neighborlist.fuzzyval = 30
|
||||||
self.assertEqual(self.nl.nextitem(), 20)
|
assert neighborlist.nextitem() == 20
|
||||||
self.assertEqual(self.nl._idx, 0)
|
assert neighborlist._idx == 0
|
||||||
|
|
||||||
def test_too_big_prev(self):
|
def test_too_big_prev(self, neighborlist):
|
||||||
"""Test fuzzyval/prev with a value bigger than any in the list."""
|
"""Test fuzzyval/prev with a value bigger than any in the list."""
|
||||||
self.nl.fuzzyval = 30
|
neighborlist.fuzzyval = 30
|
||||||
self.assertEqual(self.nl.previtem(), 20)
|
assert neighborlist.previtem() == 20
|
||||||
self.assertEqual(self.nl._idx, 0)
|
assert neighborlist._idx == 0
|
||||||
|
|
||||||
def test_too_small_next(self):
|
def test_too_small_next(self, neighborlist):
|
||||||
"""Test fuzzyval/next with a value smaller than any in the list."""
|
"""Test fuzzyval/next with a value smaller than any in the list."""
|
||||||
self.nl.fuzzyval = 0
|
neighborlist.fuzzyval = 0
|
||||||
self.assertEqual(self.nl.nextitem(), 1)
|
assert neighborlist.nextitem() == 1
|
||||||
self.assertEqual(self.nl._idx, 2)
|
assert neighborlist._idx == 2
|
||||||
|
|
||||||
def test_too_small_prev(self):
|
def test_too_small_prev(self, neighborlist):
|
||||||
"""Test fuzzyval/prev with a value smaller than any in the list."""
|
"""Test fuzzyval/prev with a value smaller than any in the list."""
|
||||||
self.nl.fuzzyval = 0
|
neighborlist.fuzzyval = 0
|
||||||
self.assertEqual(self.nl.previtem(), 1)
|
assert neighborlist.previtem() == 1
|
||||||
self.assertEqual(self.nl._idx, 2)
|
assert neighborlist._idx == 2
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
unittest.main()
|
|
||||||
|
34
tox.ini
34
tox.ini
@ -15,20 +15,25 @@ envdir = {toxinidir}/.venv
|
|||||||
usedevelop = true
|
usedevelop = true
|
||||||
|
|
||||||
[testenv:unittests]
|
[testenv:unittests]
|
||||||
setenv = QT_QPA_PLATFORM_PLUGIN_PATH={envsitepackagesdir}/PyQt5/plugins/platforms
|
# https://bitbucket.org/hpk42/tox/issue/246/ - only needed for Windows though
|
||||||
|
setenv = QT_QPA_PLATFORM_PLUGIN_PATH={envdir}/Lib/site-packages/PyQt5/plugins/platforms
|
||||||
|
passenv = DISPLAY XAUTHORITY HOME
|
||||||
deps =
|
deps =
|
||||||
|
-r{toxinidir}/requirements.txt
|
||||||
py==1.4.27
|
py==1.4.27
|
||||||
pytest==2.7.0
|
pytest==2.7.1
|
||||||
pytest-capturelog==0.7
|
pytest-capturelog==0.7
|
||||||
pytest-qt==1.3.0
|
pytest-qt==1.3.0
|
||||||
pytest-mock==0.5
|
pytest-mock==0.5
|
||||||
|
pytest-html==1.3.1
|
||||||
# We don't use {[testenv:mkvenv]commands} here because that seems to be broken
|
# We don't use {[testenv:mkvenv]commands} here because that seems to be broken
|
||||||
# on Ubuntu Trusty.
|
# on Ubuntu Trusty.
|
||||||
commands =
|
commands =
|
||||||
{envpython} scripts/link_pyqt.py --tox {envdir}
|
{envpython} scripts/link_pyqt.py --tox {envdir}
|
||||||
{envpython} -m py.test --strict {posargs}
|
{envpython} -m py.test --strict -rfEsw {posargs}
|
||||||
|
|
||||||
[testenv:coverage]
|
[testenv:coverage]
|
||||||
|
passenv = DISPLAY XAUTHORITY HOME
|
||||||
deps =
|
deps =
|
||||||
{[testenv:unittests]deps}
|
{[testenv:unittests]deps}
|
||||||
coverage==3.7.1
|
coverage==3.7.1
|
||||||
@ -36,7 +41,7 @@ deps =
|
|||||||
cov-core==1.15.0
|
cov-core==1.15.0
|
||||||
commands =
|
commands =
|
||||||
{[testenv:mkvenv]commands}
|
{[testenv:mkvenv]commands}
|
||||||
{envpython} -m py.test --strict --cov qutebrowser --cov-report term --cov-report html {posargs}
|
{envpython} -m py.test --strict -rfEswx -v --cov qutebrowser --cov-report term --cov-report html {posargs}
|
||||||
|
|
||||||
[testenv:misc]
|
[testenv:misc]
|
||||||
commands =
|
commands =
|
||||||
@ -48,7 +53,7 @@ commands =
|
|||||||
skip_install = true
|
skip_install = true
|
||||||
setenv = PYTHONPATH={toxinidir}/scripts
|
setenv = PYTHONPATH={toxinidir}/scripts
|
||||||
deps =
|
deps =
|
||||||
-rrequirements.txt
|
-r{toxinidir}/requirements.txt
|
||||||
astroid==1.3.6
|
astroid==1.3.6
|
||||||
beautifulsoup4==4.3.2
|
beautifulsoup4==4.3.2
|
||||||
pylint==1.4.3
|
pylint==1.4.3
|
||||||
@ -62,6 +67,7 @@ commands =
|
|||||||
[testenv:pep257]
|
[testenv:pep257]
|
||||||
skip_install = true
|
skip_install = true
|
||||||
deps = pep257==0.5.0
|
deps = pep257==0.5.0
|
||||||
|
passenv = LANG
|
||||||
# Disabled checks:
|
# Disabled checks:
|
||||||
# D102: Docstring missing, will be handled by others
|
# D102: Docstring missing, will be handled by others
|
||||||
# D209: Blank line before closing """ (removed from PEP257)
|
# D209: Blank line before closing """ (removed from PEP257)
|
||||||
@ -71,7 +77,7 @@ commands = {envpython} -m pep257 scripts tests qutebrowser --ignore=D102,D103,D2
|
|||||||
[testenv:flake8]
|
[testenv:flake8]
|
||||||
skip_install = true
|
skip_install = true
|
||||||
deps =
|
deps =
|
||||||
-rrequirements.txt
|
-r{toxinidir}/requirements.txt
|
||||||
pyflakes==0.8.1
|
pyflakes==0.8.1
|
||||||
pep8==1.5.7 # rq.filter: <1.6.0
|
pep8==1.5.7 # rq.filter: <1.6.0
|
||||||
flake8==2.4.0
|
flake8==2.4.0
|
||||||
@ -91,7 +97,7 @@ commands =
|
|||||||
[testenv:check-manifest]
|
[testenv:check-manifest]
|
||||||
skip_install = true
|
skip_install = true
|
||||||
deps =
|
deps =
|
||||||
check-manifest==0.24
|
check-manifest==0.25
|
||||||
commands =
|
commands =
|
||||||
{[testenv:mkvenv]commands}
|
{[testenv:mkvenv]commands}
|
||||||
{envdir}/bin/check-manifest --ignore 'qutebrowser/git-commit-id,qutebrowser/html/doc,qutebrowser/html/doc/*,*/__pycache__'
|
{envdir}/bin/check-manifest --ignore 'qutebrowser/git-commit-id,qutebrowser/html/doc,qutebrowser/html/doc/*,*/__pycache__'
|
||||||
@ -100,13 +106,25 @@ commands =
|
|||||||
skip_install = true
|
skip_install = true
|
||||||
whitelist_externals = git
|
whitelist_externals = git
|
||||||
deps =
|
deps =
|
||||||
-rrequirements.txt
|
-r{toxinidir}/requirements.txt
|
||||||
commands =
|
commands =
|
||||||
{[testenv:mkvenv]commands}
|
{[testenv:mkvenv]commands}
|
||||||
{envpython} scripts/src2asciidoc.py
|
{envpython} scripts/src2asciidoc.py
|
||||||
git --no-pager diff --exit-code --stat
|
git --no-pager diff --exit-code --stat
|
||||||
{envpython} scripts/asciidoc2html.py {posargs}
|
{envpython} scripts/asciidoc2html.py {posargs}
|
||||||
|
|
||||||
|
[testenv:smoke]
|
||||||
|
# https://bitbucket.org/hpk42/tox/issue/246/ - only needed for Windows though
|
||||||
|
setenv = QT_QPA_PLATFORM_PLUGIN_PATH={envdir}/Lib/site-packages/PyQt5/plugins/platforms
|
||||||
|
passenv = DISPLAY XAUTHORITY HOME USERNAME USER
|
||||||
|
deps =
|
||||||
|
-r{toxinidir}/requirements.txt
|
||||||
|
# We don't use {[testenv:mkvenv]commands} here because that seems to be broken
|
||||||
|
# on Ubuntu Trusty.
|
||||||
|
commands =
|
||||||
|
{envpython} scripts/link_pyqt.py --tox {envdir}
|
||||||
|
{envpython} -m qutebrowser --no-err-windows --nowindow --temp-basedir about:blank ":later 500 quit"
|
||||||
|
|
||||||
[pytest]
|
[pytest]
|
||||||
norecursedirs = .tox .venv
|
norecursedirs = .tox .venv
|
||||||
markers =
|
markers =
|
||||||
|
Loading…
Reference in New Issue
Block a user