Merge branch 'master' into more-color-settings

This commit is contained in:
Austin Anderson 2015-05-30 15:56:11 -04:00
commit b59dc8e89b
84 changed files with 3043 additions and 1066 deletions

View File

@ -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
View 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
View File

@ -21,3 +21,4 @@ __pycache__
/.coverage /.coverage
/htmlcov /htmlcov
/.tox /.tox
/testresults.html

View File

@ -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]
----------------------------------------------------------------------- -----------------------------------------------------------------------

View File

@ -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`:

View File

@ -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`.

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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

View 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"

View 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"

View File

@ -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,6 +397,7 @@ 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...")
if not args.no_err_windows:
crash_handler.handle_segfault() crash_handler.handle_segfault()
log.init.debug("Initializing sessions...") log.init.debug("Initializing sessions...")
sessions.init(qApp) sessions.init(qApp)
@ -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)

View File

@ -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')

View File

@ -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)
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.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."""
if (section, option) == ('storage', 'cache-size'):
self.setMaximumCacheSize(config.get('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

View File

@ -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()
win_id = new_window.win_id return new_window.tabbed_browser
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,6 +160,11 @@ class CommandDispatcher:
perc = 100 perc = 100
elif perc is None: elif perc is None:
perc = count perc = count
if perc == 0:
self.scroll('top')
elif perc == 100:
self.scroll('bottom')
else:
perc = qtutils.check_overflow(perc, 'int', fatal=False) perc = qtutils.check_overflow(perc, 'int', fatal=False)
frame = self._current_widget().page().currentFrame() frame = self._current_widget().page().currentFrame()
m = frame.scrollBarMaximum(orientation) m = frame.scrollBarMaximum(orientation)
@ -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):

View File

@ -691,6 +691,9 @@ 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)
if suggested_fn is None:
suggested_fn = 'qutebrowser-download'
else:
encoding = sys.getfilesystemencoding() encoding = sys.getfilesystemencoding()
suggested_fn = utils.force_encoding(suggested_fn, encoding) suggested_fn = utils.force_encoding(suggested_fn, encoding)
q = self._prepare_question() q = self._prepare_question()
@ -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."""

View File

@ -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()

View File

@ -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':
try:
host_tpl = urlutils.host_tuple(reply.url()) host_tpl = urlutils.host_tuple(reply.url())
if set(errors).issubset(self._accepted_ssl_errors[host_tpl]): 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:

View File

@ -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):

View File

@ -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,6 +304,7 @@ 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)
if not annotation_info.hide:
self.pos_args.append((param.name, name)) self.pos_args.append((param.name, name))
return args return args
@ -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:

View File

@ -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

View File

@ -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.
""" """
try:
args = objreg.get('args') args = objreg.get('args')
try:
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.
""" """
try:
args = objreg.get('args') args = objreg.get('args')
try:
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:

View File

@ -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'),
] ]

View File

@ -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."))

View File

@ -47,10 +47,14 @@ 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))
if self._configfile is not None:
self.read(self._configfile, encoding='utf-8') self.read(self._configfile, encoding='utf-8')
def __repr__(self): def __repr__(self):
@ -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))

View File

@ -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)
if not sect: bindings_to_add[sectname] = collections.OrderedDict()
if not only_new:
self.keybindings[sectname] = collections.OrderedDict()
self._mark_config_dirty()
else:
for command, keychains in sect.items(): for command, keychains in sect.items():
for e in keychains: for e in keychains:
if not only_new or self._is_new(sectname, command, e): if not only_new or self._is_new(sectname, command, e):
self._add_binding(sectname, e, command) assert e not in bindings_to_add[sectname]
self._mark_config_dirty() bindings_to_add[sectname][e] = command
for sectname, sect in bindings_to_add.items():
if not sect:
if not only_new:
self.keybindings[sectname] = collections.OrderedDict()
else:
for keychain, command in sect.items():
self._add_binding(sectname, keychain, command)
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.

View File

@ -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)
if cache_path is not None:
QWebSettings.setOfflineWebApplicationCachePath( QWebSettings.setOfflineWebApplicationCachePath(
os.path.join(standarddir.cache(), 'application-cache')) os.path.join(cache_path, 'application-cache'))
if data_path is not None:
QWebSettings.globalSettings().setLocalStoragePath( QWebSettings.globalSettings().setLocalStoragePath(
os.path.join(standarddir.data(), 'local-storage')) os.path.join(data_path, 'local-storage'))
QWebSettings.setOfflineStoragePath( QWebSettings.setOfflineStoragePath(
os.path.join(standarddir.data(), 'offline-storage')) 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]

View 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);
}
})();

View File

@ -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

View File

@ -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

View File

@ -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')

View File

@ -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()

View File

@ -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):
@ -81,6 +82,12 @@ class StatusBar(QWidget):
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.
""" """
if mode == usertypes.KeyMode.insert:
log.statusbar.debug("Setting insert_active to {}".format(val)) log.statusbar.debug("Setting insert_active to {}".format(val))
self._insert_active = 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):

View File

@ -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()

View File

@ -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):

View File

@ -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)

View File

@ -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)

View File

@ -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()
if self._args.no_err_windows:
crashdialog.dump_exception_info(exc, info.pages, info.cmd_history,
info.objects)
else:
self._crash_dialog = crashdialog.ExceptionCrashDialog( self._crash_dialog = crashdialog.ExceptionCrashDialog(
self._args.debug, pages, cmd_history, exc, objects) self._args.debug, info.pages, info.cmd_history, exc,
info.objects)
ret = self._crash_dialog.exec_() ret = self._crash_dialog.exec_()
if ret == QDialog.Accepted: # restore if ret == QDialog.Accepted: # restore
self._quitter.restart(pages) 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."""

View File

@ -80,10 +80,15 @@ 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)
if '--no-err-windows' in sys.argv:
print(message, file=sys.stderr)
print("Exiting because of --no-err-windows.", file=sys.stderr)
else:
message += '<br/><br/><br/><b>Error:</b><br/>{}'.format(exception) message += '<br/><br/><br/><b>Error:</b><br/>{}'.format(exception)
msgbox = QMessageBox(QMessageBox.Critical, "qutebrowser: Fatal error!", msgbox = QMessageBox(QMessageBox.Critical, "qutebrowser: Fatal error!",
message) message)
@ -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)

View File

@ -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:

View File

@ -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_()

View File

@ -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
if self._configdir is None:
self._configfile = None
else:
self._configfile = os.path.join(self._configdir, fname) 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')
""" """
assert self._configfile is not None
if self._opened:
raise IOError("Refusing to double-open AppendLineParser.")
self._opened = True
try:
if self._binary: if self._binary:
return open(self._configfile, mode + 'b') with open(self._configfile, mode + 'b') as f:
yield f
else: else:
return open(self._configfile, mode, encoding='utf-8') 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:
raise IOError("Refusing to double-open AppendLineParser.")
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: with qtutils.savefile_open(self._configfile, self._binary) as f:
self._write(f, self.data) 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:])

View File

@ -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
data_dir = standarddir.data()
if data_dir is None:
self._base_path = None
else:
self._base_path = os.path.join(standarddir.data(), 'sessions') 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':

View File

@ -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.

View 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_()

View File

@ -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.

View File

@ -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
finally: else:
if new_f is not None:
new_f.flush() new_f.flush()
finally:
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,84 +281,101 @@ 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
if null:
err = "{} is null".format(obj)
else:
err = "{} is not valid".format(obj) err = "{} is not valid".format(obj)
if self.reason: if self.reason:
err += ": {}".format(self.reason) err += ": {}".format(self.reason)

View File

@ -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:

View File

@ -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())

View File

@ -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.

View File

@ -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..
""" """
if sys.platform == 'darwin':
# Qt swaps Ctrl/Meta on OS X, so we switch it back here so the user can
# use it in the config as expected. See:
# https://github.com/The-Compiler/qutebrowser/issues/110
# 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([ modmask2str = collections.OrderedDict([
(Qt.ControlModifier, 'Ctrl'), (Qt.ControlModifier, 'Ctrl'),
(Qt.AltModifier, 'Alt'), (Qt.AltModifier, 'Alt'),
(Qt.MetaModifier, 'Meta'), (Qt.MetaModifier, 'Meta'),
(Qt.ShiftModifier, 'Shift'), (Qt.ShiftModifier, 'Shift'),
]) ])
if sys.platform == 'darwin':
# FIXME verify this feels right on a real Mac as well.
# In my Virtualbox VM, the Ctrl key shows up as meta.
# https://github.com/The-Compiler/qutebrowser/issues/110
modmask2str[Qt.MetaModifier] = 'Ctrl'
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))

View File

@ -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
View 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_()

View File

@ -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):
if required:
raise FileNotFoundError(source) 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)

View File

@ -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:
if dot:
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, subprocess.call(['pyprof2calltree', '-k', '-i', profilefile,
'-o', callgraphfile]) '-o', callgraphfile])
shutil.rmtree(tempdir) shutil.rmtree(tempdir)

View File

@ -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"
try:
yield parser.arg_descs[cmd.count_arg] 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):

View File

@ -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):

View File

@ -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)

View File

@ -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."""

View 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>

View 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)

View 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 %}

View 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 %}

View 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 %}

View File

@ -0,0 +1,4 @@
{% extends "base.html" %}
{% block content %}
<p>MARKER this should be the paragraph the caret is on.</p>
{% endblock %}

View 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()

View 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')

View File

@ -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):

View File

@ -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."""

View File

@ -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()

View File

@ -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

View File

@ -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()

View File

@ -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:

View File

@ -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.
""" """
def setUp(self):
"""Save the old logging configuration."""
logger_dict = logging.getLogger().manager.loggerDict logger_dict = logging.getLogger().manager.loggerDict
logging._acquireLock() logging._acquireLock()
try: try:
self.saved_handlers = logging._handlers.copy() saved_handlers = logging._handlers.copy()
self.saved_handler_list = logging._handlerList[:] saved_handler_list = logging._handlerList[:]
self.saved_loggers = saved_loggers = logger_dict.copy() saved_loggers = saved_loggers = logger_dict.copy()
self.saved_name_to_level = logging._nameToLevel.copy() saved_name_to_level = logging._nameToLevel.copy()
self.saved_level_to_name = logging._levelToName.copy() saved_level_to_name = logging._levelToName.copy()
self.logger_states = {} logger_states = {}
for name in saved_loggers: for name in saved_loggers:
self.logger_states[name] = getattr(saved_loggers[name], logger_states[name] = getattr(saved_loggers[name], 'disabled',
'disabled', None) None)
finally: finally:
logging._releaseLock() logging._releaseLock()
self.root_logger = logging.getLogger("") root_logger = logging.getLogger("")
self.root_handlers = self.root_logger.handlers[:] root_handlers = root_logger.handlers[:]
self.original_logging_level = self.root_logger.getEffectiveLevel() original_logging_level = root_logger.getEffectiveLevel()
def tearDown(self): yield
"""Restore the original logging configuration."""
while self.root_logger.handlers: while root_logger.handlers:
h = self.root_logger.handlers[0] h = root_logger.handlers[0]
self.root_logger.removeHandler(h) root_logger.removeHandler(h)
h.close() h.close()
self.root_logger.setLevel(self.original_logging_level) root_logger.setLevel(original_logging_level)
for h in self.root_handlers: for h in root_handlers:
self.root_logger.addHandler(h) root_logger.addHandler(h)
logging._acquireLock() logging._acquireLock()
try: try:
logging._levelToName.clear() logging._levelToName.clear()
logging._levelToName.update(self.saved_level_to_name) logging._levelToName.update(saved_level_to_name)
logging._nameToLevel.clear() logging._nameToLevel.clear()
logging._nameToLevel.update(self.saved_name_to_level) logging._nameToLevel.update(saved_name_to_level)
logging._handlers.clear() logging._handlers.clear()
logging._handlers.update(self.saved_handlers) logging._handlers.update(saved_handlers)
logging._handlerList[:] = self.saved_handler_list logging._handlerList[:] = saved_handler_list
logger_dict = logging.getLogger().manager.loggerDict logger_dict = logging.getLogger().manager.loggerDict
logger_dict.clear() logger_dict.clear()
logger_dict.update(self.saved_loggers) logger_dict.update(saved_loggers)
logger_states = self.logger_states logger_states = logger_states
for name in self.logger_states: for name in logger_states:
if logger_states[name] is not None: if logger_states[name] is not None:
self.saved_loggers[name].disabled = logger_states[name] saved_loggers[name].disabled = logger_states[name]
finally: finally:
logging._releaseLock() 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."""
mocker.patch('qutebrowser.utils.log.QtCore.qInstallMessageHandler',
autospec=True)
@pytest.fixture
def args(self):
"""Fixture providing an argparse namespace."""
return argparse.Namespace(debug=True, loglevel=logging.DEBUG,
color=True, loglines=10, logfilter="") color=True, loglines=10, logfilter="")
def test_stderr_none(self, _mock): 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'):
with caplog.atLevel(logging.WARNING, logger='qt-tests'):
qWarning("Hello") 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'):
with caplog.atLevel(logging.WARNING, logger='qt-tests'):
qWarning("Hello World") 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'):
with caplog.atLevel(logging.WARNING, logger='qt-tests'):
qWarning(" Hello World ") qWarning(" Hello World ")
assert not caplog.records()
if __name__ == '__main__':
unittest.main()

View File

@ -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

View File

@ -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')

View File

@ -313,16 +313,14 @@ 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+Shift+A'
else:
assert utils.keyevent_to_string(evt) == 'Ctrl+Alt+Meta+Shift+A' assert utils.keyevent_to_string(evt) == 'Ctrl+Alt+Meta+Shift+A'

View File

@ -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():
return usertypes.enum('Enum', ['one', 'two'])
"""Test simple enums.
Attributes: def test_values(enum):
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.""" """Test if enum members resolve to the right values."""
self.assertEqual(self.enum.one.value, 1) assert enum.one.value == 1
self.assertEqual(self.enum.two.value, 2) assert enum.two.value == 2
def test_name(self):
def test_name(enum):
"""Test .name mapping.""" """Test .name mapping."""
self.assertEqual(self.enum.one.name, 'one') assert enum.one.name == 'one'
self.assertEqual(self.enum.two.name, 'two') assert enum.two.name == 'two'
def test_unknown(self):
def test_unknown(enum):
"""Test invalid values which should raise an AttributeError.""" """Test invalid values which should raise an AttributeError."""
with self.assertRaises(AttributeError): with pytest.raises(AttributeError):
_ = self.enum.three _ = enum.three
def test_start(self):
def test_start():
"""Test the start= argument.""" """Test the start= argument."""
e = usertypes.enum('Enum', ['three', 'four'], start=3) e = usertypes.enum('Enum', ['three', 'four'], start=3)
self.assertEqual(e.three.value, 3) assert e.three.value == 3
self.assertEqual(e.four.value, 4) assert e.four.value == 4
if __name__ == '__main__':
unittest.main()

View File

@ -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
View File

@ -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 =