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 =
qutebrowser/__main__.py
*/__init__.py
qutebrowser/resources.py
[report]
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
/htmlcov
/.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.
- 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
~~~~~~~
@ -31,6 +43,23 @@ Changed
- 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>`
- `: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)
-------------------
@ -40,6 +69,12 @@ Fixed
- Fixed searching for terms starting with a hyphen (e.g. `/-foo`)
- 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]
-----------------------------------------------------------------------

View File

@ -86,7 +86,7 @@ Useful utilities
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.
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-git/[qutebrowser-git].
You can install them like this:
You can install them (and the needed pypeg2 dependency) like this:
----
$ mkdir qutebrowser
$ cd qutebrowser
$ wget https://aur.archlinux.org/packages/qu/qutebrowser-git/PKGBUILD
$ wget https://aur.archlinux.org/packages/py/python-pypeg2/python-pypeg2.tar.gz
$ tar xzf python-pypeg2.tar.gz
$ cd python-pypeg2
$ 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`.

View File

@ -2,6 +2,7 @@ global-exclude __pycache__ *.pyc *.pyo
recursive-include qutebrowser/html *.html
recursive-include qutebrowser/test *.py
recursive-include qutebrowser/javascript *.js
graft icons
graft scripts/pylint_checkers
graft doc/img
@ -29,4 +30,5 @@ exclude qutebrowser.rcc
exclude .coveragerc
exclude .flake8
exclude .pylintrc
exclude .eslintrc
exclude doc/help

View File

@ -138,16 +138,20 @@ Contributors, sorted by the number of commits in descending order:
* Raphael Pierzina
* Joel Torstensson
* Claude
* Artur Shaik
* ZDarian
* Peter Vilim
* John ShaggyTwoDope Jenkins
* Jimmy
* Zach-Button
* rikn00
* Patric Schmitz
* Martin Zimmermann
* Martin Tournoij
* Error 800
* Brian Jackson
* sbinix
* Tobias Patzl
* Johannes Altmanninger
* Samir Benmendil
* 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.
|<<completion-item-next,completion-item-next>>|Select the next 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.
|<<follow-hint,follow-hint>>|Follow the currently selected hint.
|<<leave-mode,leave-mode>>|Leave the mode we're currently in.
|<<message-error,message-error>>|Show an error 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.
|<<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.
|<<prompt-accept,prompt-accept>>|Accept the current 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-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.
|<<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-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-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
@ -738,6 +757,10 @@ Select the next completion item.
=== completion-item-prev
Select the previous completion item.
[[drop-selection]]
=== drop-selection
Drop selection and keep selection mode enabled.
[[enter-mode]]
=== enter-mode
Syntax: +:enter-mode 'mode'+
@ -782,6 +805,99 @@ Show a warning message in the statusbar.
==== positional arguments
* +'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 an external editor with the currently selected form field.
@ -880,13 +996,13 @@ This acts like readline's yank.
[[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
* +'dx'+: How much to scroll in x-direction.
* +'dy'+: How much to scroll in x-direction.
* +'direction'+: In which direction to scroll (up/down/left/right/top/bottom).
==== count
multiplier
@ -921,6 +1037,19 @@ The percentage can be given either as argument or as count. If no percentage is
==== count
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
Continue the search to the ([count]th) next term.
@ -935,6 +1064,20 @@ Continue the search to the ([count]th) previous term.
==== count
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
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-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-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-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:
@ -110,6 +111,7 @@
|<<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-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''
@ -134,6 +136,9 @@
|<<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-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-notifications,notifications>>|Allow websites to show notifications.
|<<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.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.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.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.
@ -392,10 +399,10 @@ How to open links in an existing instance if a new one is launched.
Valid values:
* +tab+: Open a new tab in the existing window and activate it.
* +tab-bg+: Open a new background tab in the existing window and activate it.
* +tab-silent+: Open a new tab in the existing window without activating it.
* +tab-bg-silent+: Open a new background tab in the existing window without activating 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 the window.
* +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 the window.
* +window+: Open in a new window.
Default: +pass:[window]+
@ -531,6 +538,17 @@ Set the CSS media type.
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]]
=== remove-finished-downloads
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}]+
[[tabs-mousewheel-tab-switching]]
=== mousewheel-tab-switching
Switch between tabs using the mouse wheel.
Valid values:
* +true+
* +false+
Default: +pass:[true]+
== storage
Settings related to cache and storage.
@ -1138,6 +1167,39 @@ Valid values:
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]]
=== geolocation
Allow websites to request geolocations.
@ -1498,6 +1560,18 @@ Background color of the statusbar in insert mode.
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]]
=== statusbar.progress.bg
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]
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"]
* Run `:adblock-update` to download adblock lists and activate adblocking.
* If you just cloned the repository, you'll need to run
`scripts/asciidoc2html.py` to generate the documentation.
* 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'::
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*::
Show version and quit.
@ -81,12 +90,15 @@ show it.
*--debug-exit*::
Turn on debugging of late exit.
*--no-crash-dialog*::
Don't show a crash dialog.
*--pdb-postmortem*::
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'::
Set the window name.

View File

@ -13,7 +13,7 @@
height="640"
id="svg2"
sodipodi:version="0.32"
inkscape:version="0.91 r13725"
inkscape:version="0.48.5 r10040"
version="1.0"
sodipodi:docname="cheatsheet.svg"
inkscape:output_extension="org.inkscape.output.svg.inkscape"
@ -33,16 +33,16 @@
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.8791156"
inkscape:cx="641.54005"
inkscape:cy="233.0095"
inkscape:cx="768.67127"
inkscape:cy="133.80749"
inkscape:document-units="px"
inkscape:current-layer="layer1"
width="1024px"
height="640px"
showgrid="false"
inkscape:window-width="1366"
inkscape:window-height="768"
inkscape:window-x="0"
inkscape:window-width="636"
inkscape:window-height="536"
inkscape:window-x="2560"
inkscape:window-y="0"
showguides="true"
inkscape:guide-bbox="true"
@ -1939,7 +1939,7 @@
x="542.06946"
sodipodi:role="line"
id="tspan4938"
style="font-size:8px">scoll</tspan><tspan
style="font-size:8px">scroll</tspan><tspan
y="276.1955"
x="542.06946"
sodipodi:role="line"
@ -3326,27 +3326,15 @@
style="font-size:8px">tab</tspan></text>
<text
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"
x="267.67316"
y="326.20523"
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="274.21381"
y="343.17578"
id="text10547-23-6-7"
sodipodi:linespacing="89.999998%"><tspan
sodipodi:role="line"
x="267.67316"
y="326.20523"
id="tspan10560-1-3-1" /><tspan
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>
x="274.21381"
y="343.17578"
id="tspan4052">(10)</tspan></text>
<text
sodipodi:linespacing="89.999998%"
id="text10564-6-7-8-0"
@ -3471,5 +3459,20 @@
y="177.63554"
style="font-size:8px"
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>
</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 json
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.QtCore import (pyqtSlot, qInstallMessageHandler, QTimer, QUrl,
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 utilcmds # pylint: disable=unused-import
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.
@ -64,7 +67,10 @@ def run(args):
print(qutebrowser.__copyright__)
print()
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)
objreg.register('quitter', quitter)
@ -84,11 +90,11 @@ def run(args):
objreg.register('signal-handler', signal_handler)
try:
sent = ipc.send_to_running_instance(args.command)
sent = ipc.send_to_running_instance(args)
if sent:
sys.exit(0)
sys.exit(usertypes.Exit.ok)
log.init.debug("Starting IPC server...")
server = ipc.IPCServer(qApp)
server = ipc.IPCServer(args, qApp)
objreg.register('ipc-server', server)
server.got_args.connect(lambda args, cwd:
process_pos_args(args, cwd=cwd, via_ipc=True))
@ -96,16 +102,16 @@ def run(args):
# This could be a race condition...
log.init.debug("Got AddressInUseError, trying again.")
time.sleep(500)
sent = ipc.send_to_running_instance(args.command)
sent = ipc.send_to_running_instance(args)
if sent:
sys.exit(0)
sys.exit(usertypes.Exit.ok)
else:
ipc.display_error(e)
sys.exit(1)
ipc.display_error(e, args)
sys.exit(usertypes.Exit.err_ipc)
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.
sys.exit(1)
sys.exit(usertypes.Exit.err_ipc)
init(args, crash_handler)
ret = qt_mainloop()
@ -139,11 +145,9 @@ def init(args, crash_handler):
try:
_init_modules(args, crash_handler)
except (OSError, UnicodeDecodeError) as e:
msgbox = QMessageBox(
QMessageBox.Critical, "Error while initializing!",
"Error while initializing: {}".format(e))
msgbox.exec_()
sys.exit(1)
error.handle_fatal_exc(e, args, "Error while initializing!",
pre_text="Error while initializing")
sys.exit(usertypes.Exit.err_init)
QTimer.singleShot(0, functools.partial(_process_args, args))
log.init.debug("Initializing eventfilter...")
@ -199,7 +203,7 @@ def _process_args(args):
process_pos_args(args.command)
_open_startpage()
_open_quickstart()
_open_quickstart(args)
def _load_session(name):
@ -303,8 +307,15 @@ def _open_startpage(win_id=None):
tabbed_browser.tabopen(url)
def _open_quickstart():
"""Open quickstart if it's the first start."""
def _open_quickstart(args):
"""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')
try:
quickstart_done = state_config['general']['quickstart-done'] == '1'
@ -386,6 +397,7 @@ def _init_modules(args, crash_handler):
log.init.debug("Initializing web history...")
history.init(qApp)
log.init.debug("Initializing crashlog...")
if not args.no_err_windows:
crash_handler.handle_segfault()
log.init.debug("Initializing sessions...")
sessions.init(qApp)
@ -624,13 +636,15 @@ class Quitter:
try:
save_manager.save(key, is_exit=True)
except OSError as e:
msgbox = QMessageBox(
QMessageBox.Critical, "Error while saving!",
"Error while saving {}: {}".format(key, e))
msgbox.exec_()
error.handle_fatal_exc(
e, self._args, "Error while saving!",
pre_text="Error while saving {}".format(key))
# Re-enable faulthandler to stdout, then remove crash log
log.destroy.debug("Deactivating crash log...")
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
log.destroy.debug("Deactiving message handler...")
qInstallMessageHandler(None)

View File

@ -27,7 +27,7 @@ import zipfile
from qutebrowser.config import config
from qutebrowser.utils import objreg, standarddir, log, message
from qutebrowser.commands import cmdutils
from qutebrowser.commands import cmdutils, cmdexc
def guess_zip_filename(zf):
@ -90,12 +90,18 @@ class HostBlocker:
self.blocked_hosts = set()
self._in_progress = []
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)
def read_hosts(self):
"""Read hosts from the existing blocked-hosts file."""
self.blocked_hosts = set()
if self._hosts_file is None:
return
if os.path.exists(self._hosts_file):
try:
with open(self._hosts_file, 'r', encoding='utf-8') as f:
@ -104,13 +110,17 @@ class HostBlocker:
except OSError:
log.misc.exception("Failed to read host blocklist!")
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',
"Run :adblock-update to get adblock lists.")
@cmdutils.register(instance='host-blocker', win_id='win_id')
def adblock_update(self, win_id):
"""Update the adblock block lists."""
if self._hosts_file is None:
raise cmdexc.CommandError("No data storage is configured!")
self.blocked_hosts = set()
self._done_count = 0
urls = config.get('content', 'host-block-lists')

View File

@ -21,6 +21,7 @@
import os.path
from PyQt5.QtCore import pyqtSlot
from PyQt5.QtNetwork import QNetworkDiskCache, QNetworkCacheMetaData
from qutebrowser.config import config
@ -29,23 +30,41 @@ from qutebrowser.utils import utils, standarddir, objreg
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):
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.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):
return utils.get_repr(self, size=self.cacheSize(),
maxsize=self.maximumCacheSize(),
path=self.cacheDirectory())
@config.change_filter('storage', 'cache-size')
def cache_size_changed(self):
"""Update cache size if the config was changed."""
@pyqtSlot(str, str)
def on_config_changed(self, section, option):
"""Update cache size/activated if the config was changed."""
if (section, option) == ('storage', 'cache-size'):
self.setMaximumCacheSize(config.get('storage', 'cache-size'))
elif (section, option) == ('general', 'private-browsing'):
if (config.get('general', 'private-browsing') or
standarddir.cache() is None):
self._activated = False
else:
self._activated = True
self.setCacheDirectory(
os.path.join(standarddir.cache(), 'http'))
def cacheSize(self):
"""Return the current size taken up by the cache.
@ -53,10 +72,10 @@ class DiskCache(QNetworkDiskCache):
Return:
An int.
"""
if config.get('general', 'private-browsing'):
return 0
else:
if self._activated:
return super().cacheSize()
else:
return 0
def fileMetaData(self, filename):
"""Return the QNetworkCacheMetaData for the cache file filename.
@ -67,10 +86,10 @@ class DiskCache(QNetworkDiskCache):
Return:
A QNetworkCacheMetaData object.
"""
if config.get('general', 'private-browsing'):
return QNetworkCacheMetaData()
else:
if self._activated:
return super().fileMetaData(filename)
else:
return QNetworkCacheMetaData()
def data(self, url):
"""Return the data associated with url.
@ -81,10 +100,10 @@ class DiskCache(QNetworkDiskCache):
return:
A QIODevice or None.
"""
if config.get('general', 'private-browsing'):
return None
else:
if self._activated:
return super().data(url)
else:
return None
def insert(self, device):
"""Insert the data in device and the prepared meta data into the cache.
@ -92,10 +111,10 @@ class DiskCache(QNetworkDiskCache):
Args:
device: A QIODevice.
"""
if config.get('general', 'private-browsing'):
return
else:
if self._activated:
super().insert(device)
else:
return None
def metaData(self, url):
"""Return the meta data for the url url.
@ -106,10 +125,10 @@ class DiskCache(QNetworkDiskCache):
Return:
A QNetworkCacheMetaData object.
"""
if config.get('general', 'private-browsing'):
return QNetworkCacheMetaData()
else:
if self._activated:
return super().metaData(url)
else:
return QNetworkCacheMetaData()
def prepare(self, meta_data):
"""Return the device that should be populated with the data.
@ -120,10 +139,10 @@ class DiskCache(QNetworkDiskCache):
Return:
A QIODevice or None.
"""
if config.get('general', 'private-browsing'):
return None
else:
if self._activated:
return super().prepare(meta_data)
else:
return None
def remove(self, url):
"""Remove the cache entry for url.
@ -131,10 +150,10 @@ class DiskCache(QNetworkDiskCache):
Return:
True on success, False otherwise.
"""
if config.get('general', 'private-browsing'):
return False
else:
if self._activated:
return super().remove(url)
else:
return False
def updateMetaData(self, meta_data):
"""Update the cache meta date for the meta_data's url to meta_data.
@ -142,14 +161,14 @@ class DiskCache(QNetworkDiskCache):
Args:
meta_data: A QNetworkCacheMetaData object.
"""
if config.get('general', 'private-browsing'):
return
else:
if self._activated:
super().updateMetaData(meta_data)
else:
return
def clear(self):
"""Remove all items from the cache."""
if config.get('general', 'private-browsing'):
return
else:
if self._activated:
super().clear()
else:
return

View File

@ -27,8 +27,8 @@ import posixpath
import functools
from PyQt5.QtWidgets import QApplication, QTabBar
from PyQt5.QtCore import Qt, QUrl
from PyQt5.QtGui import QClipboard
from PyQt5.QtCore import Qt, QUrl, QEvent
from PyQt5.QtGui import QClipboard, QKeyEvent
from PyQt5.QtPrintSupport import QPrintDialog, QPrintPreviewDialog
from PyQt5.QtWebKitWidgets import QWebPage
import pygments
@ -38,8 +38,10 @@ import pygments.formatters
from qutebrowser.commands import userscripts, cmdexc, cmdutils
from qutebrowser.config import config, configexc
from qutebrowser.browser import webelem, inspector
from qutebrowser.keyinput import modeman
from qutebrowser.utils import (message, usertypes, log, qtutils, urlutils,
objreg, utils)
from qutebrowser.utils.usertypes import KeyMode
from qutebrowser.misc import editor
@ -56,46 +58,40 @@ class CommandDispatcher:
Attributes:
_editor: The ExternalEditor object.
_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._win_id = win_id
self._tabbed_browser = tabbed_browser
def __repr__(self):
return utils.get_repr(self)
def _tabbed_browser(self, window=False):
"""Convienence method to get the right tabbed-browser.
Args:
window: If True, open a new window.
"""
def _new_tabbed_browser(self):
"""Get a tabbed-browser from a new window."""
from qutebrowser.mainwindow import mainwindow
if window:
new_window = mainwindow.MainWindow()
new_window.show()
win_id = new_window.win_id
else:
win_id = self._win_id
return objreg.get('tabbed-browser', scope='window', window=win_id)
return new_window.tabbed_browser
def _count(self):
"""Convenience method to get the widget count."""
return self._tabbed_browser().count()
return self._tabbed_browser.count()
def _set_current_index(self, idx):
"""Convenience method to set the current widget index."""
return self._tabbed_browser().setCurrentIndex(idx)
return self._tabbed_browser.setCurrentIndex(idx)
def _current_index(self):
"""Convenience method to get the current widget index."""
return self._tabbed_browser().currentIndex()
return self._tabbed_browser.currentIndex()
def _current_url(self):
"""Convenience method to get the current url."""
try:
return self._tabbed_browser().current_url()
return self._tabbed_browser.current_url()
except qtutils.QtValueError as e:
msg = "Current URL is invalid"
if e.reason:
@ -105,7 +101,7 @@ class CommandDispatcher:
def _current_widget(self):
"""Get the currently active widget from a command."""
widget = self._tabbed_browser().currentWidget()
widget = self._tabbed_browser.currentWidget()
if widget is None:
raise cmdexc.CommandError("No WebView available yet!")
return widget
@ -120,10 +116,10 @@ class CommandDispatcher:
window: Whether to open in a new window
"""
urlutils.raise_cmdexc_if_invalid(url)
tabbed_browser = self._tabbed_browser()
tabbed_browser = self._tabbed_browser
cmdutils.check_exclusive((tab, background, window), 'tbw')
if window:
tabbed_browser = self._tabbed_browser(window=True)
tabbed_browser = self._new_tabbed_browser()
tabbed_browser.tabopen(url)
elif tab:
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.
None if no widget was found.
"""
tabbed_browser = self._tabbed_browser()
if count is None:
return tabbed_browser.currentWidget()
return self._tabbed_browser.currentWidget()
elif 1 <= count <= self._count():
cmdutils.check_overflow(count + 1, 'int')
return tabbed_browser.widget(count - 1)
return self._tabbed_browser.widget(count - 1)
else:
return None
@ -165,6 +160,11 @@ class CommandDispatcher:
perc = 100
elif perc is None:
perc = count
if perc == 0:
self.scroll('top')
elif perc == 100:
self.scroll('bottom')
else:
perc = qtutils.check_overflow(perc, 'int', fatal=False)
frame = self._current_widget().page().currentFrame()
m = frame.scrollBarMaximum(orientation)
@ -208,7 +208,7 @@ class CommandDispatcher:
window=self._win_id)
except KeyError:
raise cmdexc.CommandError("No last focused tab!")
idx = self._tabbed_browser().indexOf(tab)
idx = self._tabbed_browser.indexOf(tab)
if idx == -1:
raise cmdexc.CommandError("Last focused tab vanished!")
self._set_current_index(idx)
@ -266,16 +266,15 @@ class CommandDispatcher:
tab = self._cntwidget(count)
if tab is None:
return
tabbed_browser = self._tabbed_browser()
tabbar = tabbed_browser.tabBar()
tabbar = self._tabbed_browser.tabBar()
selection_override = self._get_selection_override(left, right,
opposite)
if selection_override is None:
tabbed_browser.close_tab(tab)
self._tabbed_browser.close_tab(tab)
else:
old_selection_behavior = tabbar.selectionBehaviorOnRemove()
tabbar.setSelectionBehaviorOnRemove(selection_override)
tabbed_browser.close_tab(tab)
self._tabbed_browser.close_tab(tab)
tabbar.setSelectionBehaviorOnRemove(old_selection_behavior)
@cmdutils.register(instance='command-dispatcher', name='open',
@ -310,7 +309,7 @@ class CommandDispatcher:
if count is None:
# We want to open a URL in the current tab, but none exists
# yet.
self._tabbed_browser().tabopen(url)
self._tabbed_browser.tabopen(url)
else:
# Explicit count with a tab that doesn't exist.
return
@ -386,12 +385,14 @@ class CommandDispatcher:
"""
if bg and window:
raise cmdexc.CommandError("Only one of -b/-w can be given!")
cur_tabbed_browser = self._tabbed_browser()
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
# 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)
new_tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=newtab.win_id)
@ -409,9 +410,8 @@ class CommandDispatcher:
"""Detach the current tab to its own window."""
url = self._current_url()
self._open(url, window=True)
tabbed_browser = self._tabbed_browser()
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):
"""Helper function for :back/:forward."""
@ -555,8 +555,8 @@ class CommandDispatcher:
@cmdutils.register(instance='command-dispatcher', hide=True,
scope='window', count='count')
def scroll(self, dx: {'type': float}, dy: {'type': float}, count=1):
"""Scroll the current tab by 'count * dx/dy'.
def scroll_px(self, dx: {'type': float}, dy: {'type': float}, count=1):
"""Scroll the current tab by 'count * dx/dy' pixels.
Args:
dx: How much to scroll in x-direction.
@ -569,6 +569,61 @@ class CommandDispatcher:
cmdutils.check_overflow(dy, 'int')
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,
scope='window', count='count')
def scroll_perc(self, perc: {'type': float}=None,
@ -596,10 +651,22 @@ class CommandDispatcher:
y: How many pages to scroll down.
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()
size = frame.geometry()
dx = count * x * size.width()
dy = count * y * size.height()
dx = mult_x * size.width()
dy = mult_y * size.height()
cmdutils.check_overflow(dx, 'int')
cmdutils.check_overflow(dy, 'int')
frame.scroll(dx, dy)
@ -614,7 +681,7 @@ class CommandDispatcher:
"""
clipboard = QApplication.clipboard()
if title:
s = self._tabbed_browser().page_title(self._current_index())
s = self._tabbed_browser.page_title(self._current_index())
else:
s = self._current_url().toString(
QUrl.FullyEncoded | QUrl.RemovePassword)
@ -680,22 +747,21 @@ class CommandDispatcher:
right: Keep tabs to the right of the current.
"""
cmdutils.check_exclusive((left, right), 'lr')
tabbed_browser = self._tabbed_browser()
cur_idx = tabbed_browser.currentIndex()
cur_idx = self._tabbed_browser.currentIndex()
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
(right and i > cur_idx)):
continue
else:
tabbed_browser.close_tab(tab)
self._tabbed_browser.close_tab(tab)
@cmdutils.register(instance='command-dispatcher', scope='window')
def undo(self):
"""Re-open a closed tab (optionally skipping [count] closed tabs)."""
try:
self._tabbed_browser().undo()
self._tabbed_browser.undo()
except IndexError:
raise cmdexc.CommandError("Nothing to undo!")
@ -808,20 +874,19 @@ class CommandDispatcher:
if not 0 <= new_idx < self._count():
raise cmdexc.CommandError("Can't move tab to position {}!".format(
new_idx))
tabbed_browser = self._tabbed_browser()
tab = self._current_widget()
cur_idx = self._current_index()
icon = tabbed_browser.tabIcon(cur_idx)
label = tabbed_browser.page_title(cur_idx)
icon = self._tabbed_browser.tabIcon(cur_idx)
label = self._tabbed_browser.page_title(cur_idx)
cmdutils.check_overflow(cur_idx, 'int')
cmdutils.check_overflow(new_idx, 'int')
tabbed_browser.setUpdatesEnabled(False)
self._tabbed_browser.setUpdatesEnabled(False)
try:
tabbed_browser.removeTab(cur_idx)
tabbed_browser.insertTab(new_idx, tab, icon, label)
self._tabbed_browser.removeTab(cur_idx)
self._tabbed_browser.insertTab(new_idx, tab, icon, label)
self._set_current_index(new_idx)
finally:
tabbed_browser.setUpdatesEnabled(True)
self._tabbed_browser.setUpdatesEnabled(True)
@cmdutils.register(instance='command-dispatcher', scope='window',
win_id='win_id')
@ -877,11 +942,10 @@ class CommandDispatcher:
}
idx = self._current_index()
tabbed_browser = self._tabbed_browser()
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:
mainframe = None
else:
@ -891,7 +955,7 @@ class CommandDispatcher:
mainframe = webview.page().mainFrame()
try:
url = tabbed_browser.current_url()
url = self._tabbed_browser.current_url()
except qtutils.QtValueError:
pass
else:
@ -983,7 +1047,7 @@ class CommandDispatcher:
full=True, linenos='table')
highlighted = pygments.highlight(html, lexer, formatter)
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.viewing_source = True
@ -1030,8 +1094,7 @@ class CommandDispatcher:
self._open(url, tab, bg, window)
@cmdutils.register(instance='command-dispatcher',
modes=[usertypes.KeyMode.insert],
hide=True, scope='window')
modes=[KeyMode.insert], hide=True, scope='window')
def open_editor(self):
"""Open an external editor with the currently selected form field.
@ -1056,7 +1119,7 @@ class CommandDispatcher:
else:
text = elem.evaluateJavaScript('this.value')
self._editor = editor.ExternalEditor(
self._win_id, self._tabbed_browser())
self._win_id, self._tabbed_browser)
self._editor.editing_finished.connect(
functools.partial(self.on_editing_finished, elem))
self._editor.edit(text)
@ -1151,6 +1214,283 @@ class CommandDispatcher:
for _ in range(count):
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',
count='count', debug=True)
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:
return self.fetch_request(request, page, fileobj, filename,
auto_remove, suggested_fn)
if suggested_fn is None:
suggested_fn = 'qutebrowser-download'
else:
encoding = sys.getfilesystemencoding()
suggested_fn = utils.force_encoding(suggested_fn, encoding)
q = self._prepare_question()
@ -1043,7 +1046,7 @@ class DownloadManager(QAbstractListModel):
"""Override flags so items aren't selectable.
The default would be Qt.ItemIsEnabled | Qt.ItemIsSelectable."""
return Qt.ItemIsEnabled
return Qt.ItemIsEnabled | Qt.ItemNeverHasChildren
def rowCount(self, parent=QModelIndex()):
"""Get count of active downloads."""

View File

@ -83,25 +83,7 @@ class WebHistory(QWebHistoryInterface):
self._lineparser = lineparser.AppendLineParser(
standarddir.data(), 'history', parent=self)
self._history_dict = collections.OrderedDict()
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
# 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._read_history()
self._new_history = []
self._saved_count = 0
objreg.get('save-manager').add_saveable(
@ -119,6 +101,36 @@ class WebHistory(QWebHistoryInterface):
def __len__(self):
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):
"""Get the most recent history entries."""
old = self._lineparser.get_recent()

View File

@ -195,10 +195,20 @@ class NetworkManager(QNetworkAccessManager):
errors = [SslError(e) for e in errors]
ssl_strict = config.get('network', 'ssl-strict')
if ssl_strict == 'ask':
try:
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()
elif set(errors).issubset(self._rejected_ssl_errors[host_tpl]):
elif is_rejected:
pass
else:
err_string = '\n'.join('- ' + err.errorString() for err in
@ -208,9 +218,11 @@ class NetworkManager(QNetworkAccessManager):
owner=reply)
if answer:
reply.ignoreSslErrors()
self._accepted_ssl_errors[host_tpl] += errors
d = self._accepted_ssl_errors
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:
pass
else:

View File

@ -106,6 +106,7 @@ class WebView(QWebView):
self.keep_icon = False
self.search_text = None
self.search_flags = 0
self.selection_enabled = False
self.init_neighborlist()
cfg = objreg.get('config')
cfg.changed.connect(self.init_neighborlist)
@ -378,6 +379,8 @@ class WebView(QWebView):
if url.isValid():
self.cur_url = url
self.url_text_changed.emit(url.toDisplayString())
if not self.title():
self.titleChanged.emit(self.url().toDisplayString())
@pyqtSlot('QMouseEvent')
def on_mouse_event(self, evt):
@ -396,7 +399,7 @@ class WebView(QWebView):
@pyqtSlot()
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
true when the QWebPage has an ErrorPageExtension implemented.
@ -409,6 +412,12 @@ class WebView(QWebView):
self._set_load_status(LoadStatus.warn)
else:
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'):
return
mode_manager = objreg.get('mode-manager', scope='window',
@ -435,6 +444,18 @@ class WebView(QWebView):
log.webview.debug("Ignoring focus because mode {} was "
"entered.".format(mode))
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)
def on_mode_left(self, mode):
@ -443,6 +464,15 @@ class WebView(QWebView):
usertypes.KeyMode.yesno):
log.webview.debug("Restoring focus policy because mode {} was "
"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)
def search(self, text, flags):

View File

@ -61,7 +61,7 @@ class Command:
"""
AnnotationInfo = collections.namedtuple('AnnotationInfo',
['kwargs', 'type', 'flag'])
['kwargs', 'type', 'flag', 'hide'])
def __init__(self, *, handler, name, instance=None, maxsplit=None,
hide=False, completion=None, modes=None, not_modes=None,
@ -304,6 +304,7 @@ class Command:
self.flags_with_args += [short_flag, long_flag]
else:
args.append(name)
if not annotation_info.hide:
self.pos_args.append((param.name, name))
return args
@ -321,11 +322,11 @@ class Command:
flag: The short name/flag 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:
log.commands.vdebug("Parsing annotation {}".format(
param.annotation))
for field in ('type', 'flag', 'name'):
for field in ('type', 'flag', 'name', 'hide'):
if field in param.annotation:
info[field] = param.annotation[field]
if 'nargs' in param.annotation:

View File

@ -109,7 +109,8 @@ class BaseCompletionModel(QStandardItemModel):
qtutils.ensure_valid(index)
if index.parent().isValid():
# item
return Qt.ItemIsEnabled | Qt.ItemIsSelectable
return (Qt.ItemIsEnabled | Qt.ItemIsSelectable |
Qt.ItemNeverHasChildren)
else:
# category
return Qt.NoItemFlags

View File

@ -33,12 +33,12 @@ import collections
import collections.abc
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QUrl, QSettings
from PyQt5.QtWidgets import QMessageBox
from qutebrowser.config import configdata, configexc, textwrapper
from qutebrowser.config.parsers import ini, keyconf
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
@ -137,8 +137,8 @@ def _init_main_config(parent=None):
Args:
parent: The parent to pass to ConfigManager.
"""
try:
args = objreg.get('args')
try:
config_obj = ConfigManager(standarddir.config(), 'qutebrowser.conf',
args.relaxed_config, parent=parent)
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
except AttributeError:
pass
errstr += "\n{}".format(e)
msgbox = QMessageBox(QMessageBox.Critical,
"Error while reading config!", errstr)
msgbox.exec_()
errstr += "\n"
error.handle_fatal_exc(e, args, "Error while reading config!",
pre_text=errstr)
# We didn't really initialize much so far, so we just quit hard.
sys.exit(1)
sys.exit(usertypes.Exit.err_config)
else:
objreg.register('config', config_obj)
if standarddir.config() is not None:
@ -178,8 +177,8 @@ def _init_key_config(parent):
Args:
parent: The parent to use for the KeyConfigParser.
"""
try:
args = objreg.get('args')
try:
key_config = keyconf.KeyConfigParser(standarddir.config(), 'keys.conf',
args.relaxed_config,
parent=parent)
@ -188,12 +187,10 @@ def _init_key_config(parent):
errstr = "Error while reading key config:\n"
if e.lineno is not None:
errstr += "In line {}: ".format(e.lineno)
errstr += str(e)
msgbox = QMessageBox(QMessageBox.Critical,
"Error while reading key config!", errstr)
msgbox.exec_()
error.handle_fatal_exc(e, args, "Error while reading key config!",
pre_text=errstr)
# We didn't really initialize much so far, so we just quit hard.
sys.exit(1)
sys.exit(usertypes.Exit.err_key_config)
else:
objreg.register('key-config', key_config)
if standarddir.config() is not None:

View File

@ -280,6 +280,10 @@ def data(readonly=False):
SettingValue(typ.String(none_ok=True), ''),
"Set the CSS media type."),
('smooth-scrolling',
SettingValue(typ.Bool(), 'false'),
"Whether to enable smooth scrolling for webpages."),
('remove-finished-downloads',
SettingValue(typ.Bool(), 'false'),
"Whether to remove finished downloads automatically."),
@ -522,6 +526,10 @@ def data(readonly=False):
"* `{index}`: The index of this tab.\n"
"* `{id}`: The internal tab ID of this tab."),
('mousewheel-tab-switching',
SettingValue(typ.Bool(), 'true'),
"Switch between tabs using the mouse wheel."),
readonly=readonly
)),
@ -609,6 +617,18 @@ def data(readonly=False):
'Qt plugins with a mimetype such as "application/x-qt-plugin" '
"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',
SettingValue(typ.BoolAsk(), 'ask'),
"Allow websites to request geolocations."),
@ -844,6 +864,24 @@ def data(readonly=False):
SettingValue(typ.QssColor(), '${statusbar.bg}'),
"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',
SettingValue(typ.QssColor(), 'white'),
"Background color of the progress bar."),
@ -1129,6 +1167,8 @@ KEY_SECTION_DESC = {
" * `prompt-accept`: Confirm the entered value.\n"
" * `prompt-yes`: Answer yes to a yes/no question.\n"
" * `prompt-no`: Answer no to a yes/no question."),
'caret': (
""),
}
@ -1138,7 +1178,7 @@ KEY_DATA = collections.OrderedDict([
])),
('normal', collections.OrderedDict([
('search ""', ['<Escape>']),
('search', ['<Escape>']),
('set-cmd-text -s :open', ['o']),
('set-cmd-text :open {url}', ['go']),
('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 yank', [';y']),
('hint links yank-primary', [';Y']),
('hint links rapid', [';r']),
('hint links rapid-win', [';R']),
('hint --rapid links tab-bg', [';r']),
('hint --rapid links window', [';R']),
('hint links download', [';d']),
('scroll -50 0', ['h']),
('scroll 0 50', ['j']),
('scroll 0 -50', ['k']),
('scroll 50 0', ['l']),
('scroll left', ['h']),
('scroll down', ['j']),
('scroll up', ['k']),
('scroll right', ['l']),
('undo', ['u', '<Ctrl-Shift-T>']),
('scroll-perc 0', ['gg']),
('scroll-perc', ['G']),
('search-next', ['n']),
('search-prev', ['N']),
('enter-mode insert', ['i']),
('enter-mode caret', ['v']),
('yank', ['yy']),
('yank -s', ['yY']),
('yank -t', ['yt']),
@ -1294,6 +1335,33 @@ KEY_DATA = collections.OrderedDict([
('rl-delete-char', ['<Ctrl-?>']),
('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 = [
(re.compile(r'^open -([twb]) about:blank$'), r'open -\1'),
(re.compile(r'^download-page$'), r'download'),
(re.compile(r'^cancel-download$'), r'download-cancel'),
(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 \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."""
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 "
"existing window and activate it."),
"existing window and activate the "
"window."),
('tab-silent', "Open a new tab in the existing "
"window without activating "
"it."),
"the window."),
('tab-bg-silent', "Open a new background tab "
"in the existing window "
"without activating it."),
"without activating the "
"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._configdir = configdir
self._fname = fname
if self._configdir is None:
self._configfile = None
return
self._configfile = os.path.join(self._configdir, fname)
if not os.path.isfile(self._configfile):
return
log.init.debug("Reading config from {}".format(self._configfile))
if self._configfile is not None:
self.read(self._configfile, encoding='utf-8')
def __repr__(self):
@ -64,6 +68,8 @@ class ReadWriteConfigParser(ReadConfigParser):
def save(self):
"""Save the config file."""
if self._configdir is None:
return
if not os.path.exists(self._configdir):
os.makedirs(self._configdir, 0o755)
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
(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():
sectname = self._normalize_sectname(sectname)
if not sect:
if not only_new:
self.keybindings[sectname] = collections.OrderedDict()
self._mark_config_dirty()
else:
bindings_to_add[sectname] = collections.OrderedDict()
for command, keychains in sect.items():
for e in keychains:
if not only_new or self._is_new(sectname, command, e):
self._add_binding(sectname, e, command)
self._mark_config_dirty()
assert e not in bindings_to_add[sectname]
bindings_to_add[sectname][e] = command
for sectname, sect in bindings_to_add.items():
if not sect:
if not only_new:
self.keybindings[sectname] = collections.OrderedDict()
else:
for keychain, command in sect.items():
self._add_binding(sectname, keychain, command)
self.changed.emit(sectname)
if bindings_to_add:
self._mark_config_dirty()
def _is_new(self, sectname, command, keychain):
"""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
instance.
"""
log.config.vdebug("Restoring default {!r}.".format(self._default))
if self._default is not UNSET:
log.config.vdebug("Restoring default {!r}.".format(self._default))
self._set(self._default, qws=qws)
def get(self, qws=None):
@ -254,6 +254,12 @@ MAPPINGS = {
# Attribute(QWebSettings.JavaEnabled),
'allow-plugins':
Attribute(QWebSettings.PluginsEnabled),
'webgl':
Attribute(QWebSettings.WebGLEnabled),
'css-regions':
Attribute(QWebSettings.CSSRegionsEnabled),
'hyperlink-auditing':
Attribute(QWebSettings.HyperlinkAuditingEnabled),
'local-content-can-access-remote-urls':
Attribute(QWebSettings.LocalContentCanAccessRemoteUrls),
'local-content-can-access-file-urls':
@ -322,6 +328,8 @@ MAPPINGS = {
'css-media-type':
NullStringSetter(getter=QWebSettings.cssMediaType,
setter=QWebSettings.setCSSMediaType),
'smooth-scrolling':
Attribute(QWebSettings.ScrollAnimatorEnabled),
#'accelerated-compositing':
# Attribute(QWebSettings.AcceleratedCompositingEnabled),
#'tiled-backing-store':
@ -369,16 +377,20 @@ MAPPINGS = {
def init():
"""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('')
else:
QWebSettings.setIconDatabasePath(standarddir.cache())
QWebSettings.setIconDatabasePath(cache_path)
if cache_path is not None:
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(
os.path.join(standarddir.data(), 'local-storage'))
os.path.join(data_path, 'local-storage'))
QWebSettings.setOfflineStoragePath(
os.path.join(standarddir.data(), 'offline-storage'))
os.path.join(data_path, 'offline-storage'))
for sectname, section in MAPPINGS.items():
for optname, mapping in section.items():
@ -394,11 +406,12 @@ def init():
def update_settings(section, option):
"""Update global settings when qwebsettings changed."""
cache_path = standarddir.cache()
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('')
else:
QWebSettings.setIconDatabasePath(standarddir.cache())
QWebSettings.setIconDatabasePath(cache_path)
else:
try:
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*)(.*)',
self._keystring).groups()
count = int(countstr) if countstr else None
if count == 0 and not cmd_input:
cmd_input = self._keystring
count = None
else:
cmd_input = self._keystring
count = None

View File

@ -78,6 +78,7 @@ def init(win_id, parent):
KM.prompt: keyparser.PassthroughKeyParser(win_id, 'prompt', modeman,
warn=False),
KM.yesno: modeparsers.PromptKeyParser(win_id, modeman),
KM.caret: modeparsers.CaretKeyParser(win_id, modeman),
}
objreg.register('keyparsers', keyparsers, scope='window', window=win_id)
modeman.destroyed.connect(
@ -92,6 +93,7 @@ def init(win_id, parent):
passthrough=True)
modeman.register(KM.prompt, keyparsers[KM.prompt].handle, passthrough=True)
modeman.register(KM.yesno, keyparsers[KM.yesno].handle)
modeman.register(KM.caret, keyparsers[KM.caret].handle, passthrough=True)
return modeman

View File

@ -218,3 +218,13 @@ class HintKeyParser(keyparser.CommandKeyParser):
hintmanager = objreg.get('hintmanager', scope='tab',
window=self._win_id, tab='current')
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 base64
import itertools
import functools
from PyQt5.QtCore import pyqtSlot, QRect, QPoint, QTimer, Qt
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication
@ -33,7 +34,7 @@ from qutebrowser.mainwindow import tabbedbrowser
from qutebrowser.mainwindow.statusbar import bar
from qutebrowser.completion import completionwidget
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)
@ -89,8 +90,8 @@ class MainWindow(QWidget):
Attributes:
status: The StatusBar widget.
tabbed_browser: The TabbedBrowser widget.
_downloadview: The DownloadView widget.
_tabbed_browser: The TabbedBrowser widget.
_vbox: The main QVBoxLayout.
_commandrunner: The main CommandRunner instance.
"""
@ -138,9 +139,16 @@ class MainWindow(QWidget):
self._downloadview = downloadview.DownloadView(self.win_id)
self._tabbed_browser = tabbedbrowser.TabbedBrowser(self.win_id)
objreg.register('tabbed-browser', self._tabbed_browser, scope='window',
self.tabbed_browser = tabbedbrowser.TabbedBrowser(self.win_id)
objreg.register('tabbed-browser', self.tabbed_browser, scope='window',
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
# show/hide magic immediately which would mean it'd show up as a
@ -185,15 +193,15 @@ class MainWindow(QWidget):
def _add_widgets(self):
"""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.status)
position = config.get('ui', 'downloads-position')
if position == 'north':
self._vbox.addWidget(self._downloadview)
self._vbox.addWidget(self._tabbed_browser)
self._vbox.addWidget(self.tabbed_browser)
elif position == 'south':
self._vbox.addWidget(self._tabbed_browser)
self._vbox.addWidget(self.tabbed_browser)
self._vbox.addWidget(self._downloadview)
else:
raise ValueError("Invalid position {}!".format(position))
@ -260,7 +268,7 @@ class MainWindow(QWidget):
prompter = self._get_object('prompter')
# 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)
# status bar
@ -381,12 +389,12 @@ class MainWindow(QWidget):
super().resizeEvent(e)
self.resize_completion()
self._downloadview.updateGeometry()
self._tabbed_browser.tabBar().refresh()
self.tabbed_browser.tabBar().refresh()
def closeEvent(self, e):
"""Override closeEvent to display a confirmation if needed."""
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',
window=self.win_id)
download_count = download_manager.rowCount()
@ -416,8 +424,7 @@ class MainWindow(QWidget):
e.ignore()
return
e.accept()
if len(objreg.window_registry) == 1:
objreg.get('session-manager').save_last_window_session()
self._save_geometry()
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',
'command'])
Severity = usertypes.enum('Severity', ['normal', 'warning', 'error'])
CaretMode = usertypes.enum('CaretMode', ['off', 'on', 'selection'])
class StatusBar(QWidget):
@ -81,6 +82,12 @@ class StatusBar(QWidget):
For some reason we need to have this as class attribute
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:
resized: Emitted when the statusbar has resized, so the completion
widget can adjust its size to it.
@ -96,6 +103,7 @@ class StatusBar(QWidget):
_prompt_active = False
_insert_active = False
_command_active = False
_caret_mode = CaretMode.off
STYLESHEET = """
QWidget#StatusBar {
@ -110,6 +118,26 @@ class StatusBar(QWidget):
{{ 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"] {
{{ color['statusbar.bg.error'] }}
}
@ -313,14 +341,37 @@ class StatusBar(QWidget):
"""Getter for self.insert_active, so it can be used as Qt property."""
return self._insert_active
def _set_insert_active(self, val):
"""Setter for self.insert_active.
@pyqtProperty(str)
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
updated by Qt properly.
"""
if mode == usertypes.KeyMode.insert:
log.statusbar.debug("Setting insert_active to {}".format(val))
self._insert_active = val
if mode == usertypes.KeyMode.command:
log.statusbar.debug("Setting command_active to {}".format(val))
self._command_active = val
elif mode == usertypes.KeyMode.caret:
webview = objreg.get('tabbed-browser', scope='window',
window=self._win_id).currentWidget()
log.statusbar.debug("Setting caret_mode - val {}, selection "
"{}".format(val, webview.selection_enabled))
if val:
if webview.selection_enabled:
self._set_mode_text("{} selection".format(mode.name))
self._caret_mode = CaretMode.selection
else:
self._set_mode_text(mode.name)
self._caret_mode = CaretMode.on
else:
self._caret_mode = CaretMode.off
self.setStyleSheet(style.get_stylesheet(self.STYLESHEET))
def _set_mode_text(self, mode):
@ -498,10 +549,10 @@ class StatusBar(QWidget):
window=self._win_id)
if mode in mode_manager.passthrough:
self._set_mode_text(mode.name)
if mode == usertypes.KeyMode.insert:
self._set_insert_active(True)
if mode == usertypes.KeyMode.command:
self._set_command_active(True)
if mode in (usertypes.KeyMode.insert,
usertypes.KeyMode.command,
usertypes.KeyMode.caret):
self.set_mode_active(mode, True)
@pyqtSlot(usertypes.KeyMode, usertypes.KeyMode)
def on_mode_left(self, old_mode, new_mode):
@ -513,10 +564,10 @@ class StatusBar(QWidget):
self._set_mode_text(new_mode.name)
else:
self.txt.set_text(self.txt.Text.normal, '')
if old_mode == usertypes.KeyMode.insert:
self._set_insert_active(False)
if old_mode == usertypes.KeyMode.command:
self._set_command_active(False)
if old_mode in (usertypes.KeyMode.insert,
usertypes.KeyMode.command,
usertypes.KeyMode.caret):
self.set_mode_active(old_mode, False)
@config.change_filter('ui', 'message-timeout')
def set_pop_timer_interval(self):

View File

@ -29,7 +29,7 @@ from PyQt5.QtGui import QIcon
from qutebrowser.config import config
from qutebrowser.keyinput import modeman
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
@ -107,12 +107,6 @@ class TabbedBrowser(tabwidget.TabWidget):
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self._undo_stack = []
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
# FIXME adjust this to font size
# https://github.com/The-Compiler/qutebrowser/issues/119
@ -518,7 +512,8 @@ class TabbedBrowser(tabwidget.TabWidget):
tab = self.widget(idx)
log.modes.debug("Current tab changed, focusing {!r}".format(tab))
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')
if self._now_focused is not None:
objreg.register('last-focused-tab', self._now_focused, update=True,
@ -582,3 +577,14 @@ class TabbedBrowser(tabwidget.TabWidget):
"""
super().resizeEvent(e)
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, '')
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):

View File

@ -47,7 +47,7 @@ def check_python_version():
version_str = '.'.join(map(str, sys.version_info[:3]))
text = ("At least Python 3.4 is required to run qutebrowser, but " +
version_str + " is installed!\n")
if Tk:
if Tk and '--no-err-windows' not in sys.argv:
root = Tk()
root.withdraw()
messagebox.showerror("qutebrowser: Fatal error!", text)

View File

@ -584,3 +584,38 @@ class ReportErrorDialog(QDialog):
btn.clicked.connect(self.close)
hbox.addWidget(btn)
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 faulthandler
import os.path
import collections
from PyQt5.QtCore import (pyqtSlot, qInstallMessageHandler, QObject,
QSocketNotifier, QTimer, QUrl)
@ -37,6 +38,10 @@ from qutebrowser.misc import earlyinit, crashdialog
from qutebrowser.utils import usertypes, standarddir, log, objreg, debug
ExceptionInfo = collections.namedtuple('ExceptionInfo',
'pages, cmd_history, objects')
class CrashHandler(QObject):
"""Handler for crashes, reports and exceptions.
@ -63,7 +68,10 @@ class CrashHandler(QObject):
def handle_segfault(self):
"""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:
# First check if an old logfile exists.
if os.path.exists(logname):
@ -118,7 +126,11 @@ class CrashHandler(QObject):
def _init_crashlogfile(self):
"""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:
self._crash_log_file = open(logname, 'w', encoding='ascii')
except OSError:
@ -153,6 +165,31 @@ class CrashHandler(QObject):
except OSError:
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
"""Handle uncaught python exceptions.
@ -175,12 +212,11 @@ class CrashHandler(QObject):
if self._args.pdb_postmortem:
pdb.post_mortem(tb)
if (is_ignored_exception or self._args.no_crash_dialog or
self._args.pdb_postmortem):
if is_ignored_exception or self._args.pdb_postmortem:
# pdb exit, KeyboardInterrupt, ...
status = 0 if is_ignored_exception else 2
try:
qapp.shutdown(status)
self._quitter.shutdown(status)
return
except Exception:
log.init.exception("Error while shutting down")
@ -188,24 +224,7 @@ class CrashHandler(QObject):
return
self._quitter.quit_status['crash'] = False
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 = ""
info = self._get_exception_info()
try:
objreg.get('ipc-server').ignored = True
@ -218,18 +237,23 @@ class CrashHandler(QObject):
except TypeError:
log.destroy.exception("Error while preventing shutdown")
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._args.debug, pages, cmd_history, exc, objects)
self._args.debug, info.pages, info.cmd_history, exc,
info.objects)
ret = self._crash_dialog.exec_()
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
# run in some undefined state, so we only do the most needed shutdown
# here.
qInstallMessageHandler(None)
self.destroy_crashlogfile()
sys.exit(1)
sys.exit(usertypes.Exit.exception)
def raise_crashdlg(self):
"""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.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)
traceback.print_exc()
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)
msgbox = QMessageBox(QMessageBox.Critical, "qutebrowser: Fatal error!",
message)
@ -186,13 +191,13 @@ def check_pyqt_core():
text = text.replace('</b>', '')
text = text.replace('<br />', '\n')
text += '\n\nError: {}'.format(e)
if tkinter:
if tkinter and '--no-err-windows' not in sys.argv:
root = tkinter.Tk()
root.withdraw()
tkinter.messagebox.showerror("qutebrowser: Fatal error!", text)
else:
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)
traceback.print_exc()
sys.exit(1)

View File

@ -122,7 +122,8 @@ class ExternalEditor(QObject):
raise ValueError("Already editing a file!")
self._text = text
try:
self._oshandle, self._filename = tempfile.mkstemp(text=True)
self._oshandle, self._filename = tempfile.mkstemp(
text=True, prefix='qutebrowser-editor-')
if text:
encoding = config.get('general', 'editor-encoding')
with open(self._filename, 'w', encoding=encoding) as f:

View File

@ -23,20 +23,28 @@ import os
import json
import getpass
import binascii
import hashlib
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject
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
WRITE_TIMEOUT = 1000
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):
"""Exception raised when there was a problem with IPC."""
@ -80,6 +88,7 @@ class IPCServer(QObject):
_timer: A timer to handle timeouts.
_server: A QLocalServer to accept new connections.
_socket: The QLocalSocket we're currently connected to.
_socketname: The socketname to use.
Signals:
got_args: Emitted when there was an IPC connection and arguments were
@ -88,16 +97,22 @@ class IPCServer(QObject):
got_args = pyqtSignal(list, str)
def __init__(self, parent=None):
"""Start the IPC server and listen to commands."""
def __init__(self, args, parent=None):
"""Start the IPC server and listen to commands.
Args:
args: The argparse namespace.
parent: The parent to be used.
"""
super().__init__(parent)
self.ignored = False
self._socketname = _get_socketname(args)
self._remove_server()
self._timer = usertypes.Timer(self, 'ipc-timeout')
self._timer.setInterval(READ_TIMEOUT)
self._timer.timeout.connect(self.on_timeout)
self._server = QLocalServer(self)
ok = self._server.listen(SOCKETNAME)
ok = self._server.listen(self._socketname)
if not ok:
if self._server.serverError() == QAbstractSocket.AddressInUseError:
raise AddressInUseError(self._server)
@ -108,17 +123,18 @@ class IPCServer(QObject):
def _remove_server(self):
"""Remove an existing server."""
ok = QLocalServer.removeServer(SOCKETNAME)
ok = QLocalServer.removeServer(self._socketname)
if not ok:
raise Error("Error while removing server {}!".format(SOCKETNAME))
raise Error("Error while removing server {}!".format(
self._socketname))
@pyqtSlot(int)
def on_error(self, error):
def on_error(self, err):
"""Convenience method which calls _socket_error on an error."""
self._timer.stop()
log.ipc.debug("Socket error {}: {}".format(
self._socket.error(), self._socket.errorString()))
if error != QLocalSocket.PeerClosedError:
if err != QLocalSocket.PeerClosedError:
_socket_error("handling IPC connection", self._socket)
@pyqtSlot()
@ -223,23 +239,23 @@ def _socket_error(action, socket):
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.
Blocks for CONNECT_TIMEOUT ms.
Args:
cmdlist: A list to send (URLs/commands)
args: The argparse namespace.
Return:
True if connecting was successful, False if no connection was made.
"""
socket = QLocalSocket()
socket.connectToServer(SOCKETNAME)
socket.connectToServer(_get_socketname(args))
connected = socket.waitForConnected(100)
if connected:
log.ipc.info("Opening in existing instance")
json_data = {'args': cmdlist}
json_data = {'args': args.command}
try:
cwd = os.getcwd()
except OSError:
@ -265,9 +281,8 @@ def send_to_running_instance(cmdlist):
return False
def display_error(exc):
def display_error(exc, args):
"""Display a message box with an IPC error."""
text = '{}\n\nMaybe another instance is running but frozen?'.format(exc)
msgbox = QMessageBox(QMessageBox.Critical, "Error while connecting to "
"running instance!", text)
msgbox.exec_()
error.handle_fatal_exc(
exc, args, "Error while connecting to running instance!",
post_text="Maybe another instance is running but frozen?")

View File

@ -35,7 +35,7 @@ class BaseLineParser(QObject):
"""A LineParser without any real data.
Attributes:
_configdir: The directory to read the config from.
_configdir: Directory to read the config from, or None.
_configfile: The config file path.
_fname: Filename of the config.
_binary: Whether to open the file in binary mode.
@ -53,12 +53,17 @@ class BaseLineParser(QObject):
configdir: Directory to read the config from.
fname: Filename of the config file.
binary: Whether to open the file in binary mode.
_opened: Whether the underlying file is open
"""
super().__init__(parent)
self._configdir = configdir
if self._configdir is None:
self._configfile = None
else:
self._configfile = os.path.join(self._configdir, fname)
self._fname = fname
self._binary = binary
self._opened = False
def __repr__(self):
return utils.get_repr(self, constructor=True,
@ -66,21 +71,38 @@ class BaseLineParser(QObject):
binary=self._binary)
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))
if not os.path.exists(self._configdir):
os.makedirs(self._configdir, 0o755)
return True
@contextlib.contextmanager
def _open(self, mode):
"""Open self._configfile for reading.
Args:
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:
return open(self._configfile, mode + 'b')
with open(self._configfile, mode + 'b') as f:
yield f
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):
"""Write the data to a file.
@ -150,7 +172,9 @@ class AppendLineParser(BaseLineParser):
return data
def save(self):
self._prepare_save()
do_save = self._prepare_save()
if not do_save:
return
with self._open('a') as f:
self._write(f, self.new_data)
self.new_data = []
@ -173,7 +197,7 @@ class LineParser(BaseLineParser):
binary: Whether to open the file in binary mode.
"""
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 = []
else:
log.init.debug("Reading {}".format(self._configfile))
@ -195,9 +219,18 @@ class LineParser(BaseLineParser):
def save(self):
"""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:
self._write(f, self.data)
finally:
self._opened = False
class LimitLineParser(LineParser):
@ -213,14 +246,14 @@ class LimitLineParser(LineParser):
"""Constructor.
Args:
configdir: Directory to read the config from.
configdir: Directory to read the config from, or None.
fname: Filename of the config file.
limit: Config tuple (section, option) which contains a limit.
binary: Whether to open the file in binary mode.
"""
super().__init__(configdir, fname, binary=binary, parent=parent)
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)
def __repr__(self):
@ -231,6 +264,7 @@ class LimitLineParser(LineParser):
@pyqtSlot(str, str)
def cleanup_file(self, section, option):
"""Delete the file if the limit was changed to 0."""
assert self._configfile is not None
if (section, option) != self._limit:
return
value = config.get(section, option)
@ -243,6 +277,9 @@ class LimitLineParser(LineParser):
limit = config.get(*self._limit)
if limit == 0:
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:
self._write(f, self.data[-limit:])

View File

@ -82,10 +82,14 @@ class SessionManager(QObject):
def __init__(self, parent=None):
super().__init__(parent)
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._last_window_session = None
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)
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
os.path.exists(path)):
return path
elif self._base_path is None:
if check_exists:
raise SessionNotFoundError(name)
else:
return None
else:
path = os.path.join(self._base_path, name + '.yml')
if check_exists and not os.path.exists(path):
@ -194,6 +203,8 @@ class SessionManager(QObject):
else:
name = 'default'
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))
if last_window:
@ -289,6 +300,8 @@ class SessionManager(QObject):
def list_sessions(self):
"""Get a list of all session names."""
sessions = []
if self._base_path is None:
return sessions
for filename in os.listdir(self._base_path):
base, ext = os.path.splitext(filename)
if ext == '.yml':

View File

@ -48,6 +48,12 @@ def get_argparser():
description=qutebrowser.__description__)
parser.add_argument('-c', '--confdir', help="Set config directory (empty "
"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.",
action='store_true')
parser.add_argument('-s', '--set', help="Set a temporary setting for "
@ -84,10 +90,12 @@ def get_argparser():
"the main window.")
debug.add_argument('--debug-exit', help="Turn on debugging of late exit.",
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',
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
# store_true because we want the default to be None, to make
# 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 warnings
from PyQt5.QtCore import (QtDebugMsg, QtWarningMsg, QtCriticalMsg, QtFatalMsg,
qInstallMessageHandler)
from PyQt5 import QtCore
# Optional imports
try:
import colorama
@ -153,15 +152,15 @@ def init_log(args):
root.setLevel(logging.NOTSET)
logging.captureWarnings(True)
warnings.simplefilter('default')
qInstallMessageHandler(qt_message_handler)
QtCore.qInstallMessageHandler(qt_message_handler)
@contextlib.contextmanager
def disable_qt_msghandler():
"""Contextmanager which temporarily disables the Qt message handler."""
old_handler = qInstallMessageHandler(None)
old_handler = QtCore.qInstallMessageHandler(None)
yield
qInstallMessageHandler(old_handler)
QtCore.qInstallMessageHandler(old_handler)
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
# to critical.
qt_to_logging = {
QtDebugMsg: logging.DEBUG,
QtWarningMsg: logging.WARNING,
QtCriticalMsg: logging.ERROR,
QtFatalMsg: logging.CRITICAL,
QtCore.QtDebugMsg: logging.DEBUG,
QtCore.QtWarningMsg: logging.WARNING,
QtCore.QtCriticalMsg: logging.ERROR,
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
# shown to the user.
# 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):
"""Ensure a Qt object with an .isNull() method is not null."""
if obj.isNull():
raise QtValueError(obj)
raise QtValueError(obj, null=True)
def check_qdatastream(stream):
@ -180,7 +180,7 @@ def deserialize_stream(stream, obj):
def savefile_open(filename, binary=False, encoding='utf-8'):
"""Context manager to easily use a QSaveFile."""
f = QSaveFile(filename)
new_f = None
cancelled = False
try:
ok = f.open(QIODevice.WriteOnly)
if not ok:
@ -192,13 +192,14 @@ def savefile_open(filename, binary=False, encoding='utf-8'):
yield new_f
except:
f.cancelWriting()
cancelled = True
raise
finally:
if new_f is not None:
else:
new_f.flush()
finally:
commit_ok = f.commit()
if not commit_ok:
raise OSError(f.errorString())
if not commit_ok and not cancelled:
raise OSError("Commit failed!")
@contextlib.contextmanager
@ -221,27 +222,58 @@ class PyQIODevice(io.BufferedIOBase):
"""Wrapper for a QIODevice which provides a python interface.
Attributes:
_dev: The underlying QIODevice.
dev: The underlying QIODevice.
"""
# pylint: disable=missing-docstring
def __init__(self, dev):
self._dev = dev
self.dev = dev
def __len__(self):
return self._dev.size()
return self.dev.size()
def _check_open(self):
"""Check if the device is open, raise OSError if not."""
if not self._dev.isOpen():
raise OSError("IO operation on closed device!")
"""Check if the device is open, raise ValueError if not."""
if not self.dev.isOpen():
raise ValueError("IO operation on closed device!")
def _check_random(self):
"""Check if the device supports random access, raise OSError if not."""
if not self.seekable():
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):
raise io.UnsupportedOperation
@ -249,84 +281,101 @@ class PyQIODevice(io.BufferedIOBase):
self._check_open()
self._check_random()
if whence == io.SEEK_SET:
ok = self._dev.seek(offset)
ok = self.dev.seek(offset)
elif whence == io.SEEK_CUR:
ok = self._dev.seek(self.tell() + offset)
ok = self.dev.seek(self.tell() + offset)
elif whence == io.SEEK_END:
ok = self._dev.seek(len(self) + offset)
ok = self.dev.seek(len(self) + offset)
else:
raise io.UnsupportedOperation("whence = {} is not "
"supported!".format(whence))
if not ok:
raise OSError(self._dev.errorString())
raise OSError("seek failed!")
def truncate(self, size=None): # pylint: disable=unused-argument
raise io.UnsupportedOperation
def close(self):
self._dev.close()
@property
def closed(self):
return not self._dev.isOpen()
return not self.dev.isOpen()
def flush(self):
self._check_open()
self._dev.waitForBytesWritten(-1)
self.dev.waitForBytesWritten(-1)
def isatty(self):
self._check_open()
return False
def readable(self):
return self._dev.isReadable()
return self.dev.isReadable()
def readline(self, size=-1):
self._check_open()
if size == -1:
size = 0
return self._dev.readLine(size)
self._check_readable()
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):
return not self._dev.isSequential()
return not self.dev.isSequential()
def tell(self):
self._check_open()
self._check_random()
return self._dev.pos()
return self.dev.pos()
def writable(self):
return self._dev.isWritable()
def readinto(self, b):
self._check_open()
return self._dev.read(b, len(b))
return self.dev.isWritable()
def write(self, b):
self._check_open()
num = self._dev.write(b)
self._check_writable()
num = self.dev.write(b)
if num == -1 or num < len(b):
raise OSError(self._dev.errorString())
raise OSError(self.dev.errorString())
return num
def read(self, size):
def read(self, size=-1):
self._check_open()
buf = bytes()
num = self._dev.read(buf, size)
if num == -1:
raise OSError(self._dev.errorString())
return num
self._check_readable()
if size < 0:
buf = self.dev.readAll()
else:
buf = self.dev.read(size)
if buf is None:
raise OSError(self.dev.errorString())
return buf
class QtValueError(ValueError):
"""Exception which gets raised by ensure_valid."""
def __init__(self, obj):
def __init__(self, obj, null=False):
try:
self.reason = obj.errorString()
except AttributeError:
self.reason = None
if null:
err = "{} is null".format(obj)
else:
err = "{} is not valid".format(obj)
if 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.
"""
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:
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:
argname = typ_to_argparse_arg[typ]
except KeyError:
@ -135,8 +151,18 @@ def init(args):
"""Initialize all standard dirs."""
global _args
_args = args
# http://www.brynosaurus.com/cachedir/spec.html
cachedir_tag = os.path.join(cache(), 'CACHEDIR.TAG')
_init_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):
try:
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 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
@ -74,8 +74,7 @@ def _get_search_url(txt):
"""
log.url.debug("Finding search engine for '{}'".format(txt))
engine, term = _parse_search_term(txt)
if not term:
raise FuzzyUrlError("No search term given")
assert term
if engine is None:
template = config.get('searchengines', 'DEFAULT')
else:
@ -95,11 +94,9 @@ def _is_url_naive(urlstr):
True if the URL really is a URL, False otherwise.
"""
url = qurl_from_user_input(urlstr)
try:
ipaddress.ip_address(urlstr)
except ValueError:
pass
else:
assert url.isValid()
if not utils.raises(ValueError, ipaddress.ip_address, urlstr):
# Valid IPv4/IPv6 address
return True
@ -109,31 +106,36 @@ def _is_url_naive(urlstr):
if not QHostAddress(urlstr).isNull():
return False
if not url.isValid():
return False
elif '.' in url.host():
return True
elif url.host() == 'localhost':
if '.' in url.host():
return True
else:
return False
def _is_url_dns(url):
def _is_url_dns(urlstr):
"""Check if a URL is really a URL via DNS.
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:
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
host = url.host()
log.url.debug("DNS request for {}".format(host))
if not host:
log.url.debug("URL has no host -> False")
return False
log.url.debug("Doing DNS request for {}".format(host))
info = QHostInfo.fromName(host)
return not info.error()
@ -230,6 +232,7 @@ def is_url(urlstr):
urlstr = urlstr.strip()
qurl = QUrl(urlstr)
qurl_userinput = qurl_from_user_input(urlstr)
if not autosearch:
# no autosearch, so everything is a URL unless it has an explicit
@ -240,29 +243,33 @@ def is_url(urlstr):
else:
return False
if not qurl_userinput.isValid():
# This will also catch URLs containing spaces.
return False
if _has_explicit_scheme(qurl):
# URLs with explicit schemes are always URLs
log.url.debug("Contains explicit scheme")
url = True
elif ' ' in urlstr:
# A URL will never contain a space
log.url.debug("Contains space -> no URL")
url = False
elif qurl_userinput.host() in ('localhost', '127.0.0.1', '::1'):
log.url.debug("Is localhost.")
url = True
elif is_special_url(qurl):
# Special URLs are always URLs, even with autosearch=False
log.url.debug("Is an special URL.")
url = True
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
# "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':
log.url.debug("Checking via naive check")
url = _is_url_naive(urlstr)
else:
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):
@ -311,20 +318,15 @@ def invalid_url_error(win_id, url, action):
if url.isValid():
raise ValueError("Calling invalid_url_error with valid URL {}".format(
url.toDisplayString()))
errstring = "Trying to {} with invalid URL".format(action)
if url.errorString():
errstring += " - {}".format(url.errorString())
errstring = get_errstring(
url, "Trying to {} with invalid URL".format(action))
message.error(win_id, errstring)
def raise_cmdexc_if_invalid(url):
"""Check if the given QUrl is invalid, and if so, raise a CommandError."""
if not url.isValid():
errstr = "Invalid URL {}".format(url.toDisplayString())
url_error = url.errorString()
if url_error:
errstr += " - {}".format(url_error)
raise cmdexc.CommandError(errstr)
raise cmdexc.CommandError(get_errstring(url))
def filename_from_url(url):
@ -348,11 +350,46 @@ def filename_from_url(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.
"""
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):
@ -360,17 +397,19 @@ class FuzzyUrlError(Exception):
"""Exception raised by fuzzy_url on problems.
Attributes:
msg: The error message to use.
url: The QUrl which caused the error.
"""
def __init__(self, msg, url=None):
super().__init__(msg)
if url is not None:
assert not url.isValid()
if url is not None and url.isValid():
raise ValueError("Got valid URL {}!".format(url.toDisplayString()))
self.url = url
self.msg = msg
def __str__(self):
if self.url is None or not self.url.errorString():
return str(super())
return self.msg
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
KeyMode = enum('KeyMode', ['normal', 'hint', 'command', 'yesno', 'prompt',
'insert', 'passthrough'])
'insert', 'passthrough', 'caret'])
# Available command completions
@ -240,6 +240,11 @@ Completion = enum('Completion', ['command', 'section', 'option', 'value',
'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):
"""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
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([
(Qt.ControlModifier, 'Ctrl'),
(Qt.AltModifier, 'Alt'),
(Qt.MetaModifier, 'Meta'),
(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,
Qt.Key_AltGr, Qt.Key_Super_L, Qt.Key_Super_R,
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__)
parts = []
for name, val in attrs.items():
items = sorted(attrs.items())
for name, val in items:
parts.append('{}={!r}'.format(name, val))
if constructor:
return '{}({})'.format(cls, ', '.join(parts))

View File

@ -130,7 +130,7 @@ def _module_versions():
try:
import sipconfig # pylint: disable=import-error,unused-variable
except ImportError:
pass
lines.append('SIP: ?')
else:
try:
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:
raise Error("Did not find sip in {}!".format(sys_path))
files = ['PyQt5']
files += [os.path.basename(e) for e in globbed_sip]
for fn in files:
files = [('PyQt5', True), ('sipconfig.py', False)]
files += [(os.path.basename(e), True) for e in globbed_sip]
for fn, required in files:
source = os.path.join(sys_path, fn)
dest = os.path.join(venv_path, fn)
if not os.path.exists(source):
if required:
raise FileNotFoundError(source)
else:
continue
if os.path.exists(dest):
if os.path.isdir(dest) and not os.path.islink(dest):
shutil.rmtree(dest)

View File

@ -39,18 +39,29 @@ if '--profile-keep' in sys.argv:
profilefile = os.path.join(os.getcwd(), 'profile')
else:
profilefile = os.path.join(tempdir, 'profile')
if '--profile-noconv' in sys.argv:
sys.argv.remove('--profile-noconv')
noconv = True
else:
noconv = False
if '--profile-dot' in sys.argv:
sys.argv.remove('--profile-dot')
dot = True
else:
dot = False
callgraphfile = os.path.join(tempdir, 'callgraph')
profiler = cProfile.Profile()
profiler.run('qutebrowser.qutebrowser.main()')
profiler.dump_stats(profilefile)
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,
'-o', callgraphfile])
shutil.rmtree(tempdir)

View File

@ -211,7 +211,11 @@ def _get_command_doc_count(cmd, parser):
if cmd.count_arg is not None:
yield ""
yield "==== count"
try:
yield parser.arg_descs[cmd.count_arg]
except KeyError:
raise KeyError("No description for count arg {!r} of command "
"{!r}!".format(cmd.count_arg, cmd.name))
def _get_command_doc_notes(cmd):

View File

@ -378,10 +378,10 @@ class TestIsEditable:
webelem.config = old_config
@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."""
config_stub.data = {'input': {}}
mocker.patch('qutebrowser.browser.webelem.config', new=config_stub)
monkeypatch.setattr('qutebrowser.browser.webelem.config', config_stub)
return config_stub
def test_input_plain(self):

View File

@ -177,6 +177,60 @@ class TestKeyConfigParser:
with pytest.raises(keyconf.KeyConfigError):
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:
@ -221,7 +275,7 @@ class TestConfigInit:
def test_config_none(self, monkeypatch):
"""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():
monkeypatch.setenv(k, v)
standarddir.init(args)

View File

@ -883,11 +883,12 @@ class TestCommand:
"""Test Command."""
@pytest.fixture(autouse=True)
def setup(self, mocker, stubs):
def setup(self, monkeypatch, stubs):
self.t = configtypes.Command()
cmd_utils = stubs.FakeCmdUtils({'cmd1': stubs.FakeCommand("desc 1"),
'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):
"""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."""
import sys
import logging
from unittest import mock
@ -28,35 +29,17 @@ from PyQt5.QtCore import Qt
import pytest
from qutebrowser.keyinput import basekeyparser
from qutebrowser.utils import objreg, log
from qutebrowser.utils import log
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
def mock_timer(mocker, stubs):
def mock_timer(monkeypatch, stubs):
"""Mock the Timer class used by the usertypes module with a stub."""
mocker.patch('qutebrowser.keyinput.basekeyparser.usertypes.Timer',
new=stubs.FakeTimer)
monkeypatch.setattr('qutebrowser.keyinput.basekeyparser.usertypes.Timer',
stubs.FakeTimer)
class TestSplitCount:
@ -131,8 +114,12 @@ class TestSpecialKeys:
def test_valid_key(self, fake_keyevent_factory):
"""Test a valid special keyevent."""
self.kp.handle(fake_keyevent_factory(Qt.Key_A, Qt.ControlModifier))
self.kp.handle(fake_keyevent_factory(Qt.Key_X, Qt.ControlModifier))
if sys.platform == 'darwin':
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)
def test_invalid_key(self, fake_keyevent_factory):
@ -167,8 +154,12 @@ class TestKeyChain:
def test_valid_special_key(self, fake_keyevent_factory):
"""Test valid special key."""
self.kp.handle(fake_keyevent_factory(Qt.Key_A, Qt.ControlModifier))
self.kp.handle(fake_keyevent_factory(Qt.Key_X, Qt.ControlModifier))
if sys.platform == 'darwin':
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)
assert self.kp._keystring == ''
@ -189,12 +180,18 @@ class TestKeyChain:
self.kp.execute.assert_called_once_with('ba', self.kp.Type.chain, None)
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,
mocker):
monkeypatch):
"""Test ambiguous keychain."""
config_stub.data = CONFIG
mocker.patch('qutebrowser.keyinput.basekeyparser.config',
new=config_stub)
monkeypatch.setattr('qutebrowser.keyinput.basekeyparser.config',
config_stub)
timer = self.kp._ambiguous_timer
assert not timer.isActive()
# 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_B, text='b'))
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 == ''
def test_count_42(self, fake_keyevent_factory):

View File

@ -19,25 +19,18 @@
"""Tests for mode parsers."""
from unittest import mock
from PyQt5.QtCore import Qt
from unittest import mock
import pytest
from qutebrowser.keyinput import modeparsers
from qutebrowser.utils import objreg
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:
"""Tests for NormalKeyParser.
@ -49,19 +42,18 @@ class TestsNormalKeyParser:
# pylint: disable=protected-access
@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."""
mocker.patch('qutebrowser.keyinput.basekeyparser.usertypes.Timer',
new=stubs.FakeTimer)
monkeypatch.setattr(
'qutebrowser.keyinput.basekeyparser.usertypes.Timer',
stubs.FakeTimer)
config_stub.data = CONFIG
mocker.patch('qutebrowser.keyinput.modeparsers.config',
new=config_stub)
monkeypatch.setattr('qutebrowser.keyinput.modeparsers.config',
config_stub)
objreg.register('key-config', fake_keyconfig)
self.kp = modeparsers.NormalKeyParser(0)
self.kp.execute = mock.Mock()
yield
objreg.delete('key-config')
def test_keychain(self, fake_keyevent_factory):
"""Test valid keychain."""

View File

@ -19,8 +19,6 @@
"""Tests for qutebrowser.misc.crashdialog."""
import unittest
from qutebrowser.misc import crashdialog
@ -52,7 +50,7 @@ Hello world!
"""
class ParseFatalStacktraceTests(unittest.TestCase):
class TestParseFatalStacktrace:
"""Tests for parse_fatal_stacktrace."""
@ -60,30 +58,22 @@ class ParseFatalStacktraceTests(unittest.TestCase):
"""Test parse_fatal_stacktrace with a valid text."""
text = VALID_CRASH_TEXT.strip().replace('_', ' ')
typ, func = crashdialog.parse_fatal_stacktrace(text)
self.assertEqual(typ, "Segmentation fault")
self.assertEqual(func, 'testfunc')
assert (typ, func) == ("Segmentation fault", 'testfunc')
def test_valid_text_thread(self):
"""Test parse_fatal_stacktrace with a valid text #2."""
text = VALID_CRASH_TEXT_THREAD.strip().replace('_', ' ')
typ, func = crashdialog.parse_fatal_stacktrace(text)
self.assertEqual(typ, "Segmentation fault")
self.assertEqual(func, 'testfunc')
assert (typ, func) == ("Segmentation fault", 'testfunc')
def test_valid_text_empty(self):
"""Test parse_fatal_stacktrace with a valid text but empty function."""
text = VALID_CRASH_TEXT_EMPTY.strip().replace('_', ' ')
typ, func = crashdialog.parse_fatal_stacktrace(text)
self.assertEqual(typ, 'Aborted')
self.assertEqual(func, '')
assert (typ, func) == ('Aborted', '')
def test_invalid_text(self):
"""Test parse_fatal_stacktrace with an invalid text."""
text = INVALID_CRASH_TEXT.strip().replace('_', ' ')
typ, func = crashdialog.parse_fatal_stacktrace(text)
self.assertEqual(typ, '')
self.assertEqual(func, '')
if __name__ == '__main__':
unittest.main()
assert (typ, func) == ('', '')

View File

@ -41,18 +41,18 @@ class TestArg:
"""
@pytest.yield_fixture(autouse=True)
def setup(self, mocker, stubs):
mocker.patch('qutebrowser.misc.editor.QProcess',
new_callable=stubs.FakeQProcess)
def setup(self, monkeypatch, stubs):
monkeypatch.setattr('qutebrowser.misc.editor.QProcess',
stubs.FakeQProcess())
self.editor = editor.ExternalEditor(0)
yield
self.editor._cleanup() # pylint: disable=protected-access
@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."""
config_stub.data = {'input': {}}
mocker.patch('qutebrowser.misc.editor.config', new=config_stub)
monkeypatch.setattr('qutebrowser.misc.editor.config', config_stub)
return config_stub
def test_simple_start_args(self, stubbed_config):
@ -98,14 +98,14 @@ class TestFileHandling:
"""
@pytest.fixture(autouse=True)
def setup(self, mocker, stubs, config_stub):
mocker.patch('qutebrowser.misc.editor.message',
new=stubs.MessageModule())
mocker.patch('qutebrowser.misc.editor.QProcess',
new_callable=stubs.FakeQProcess)
def setup(self, monkeypatch, stubs, config_stub):
monkeypatch.setattr('qutebrowser.misc.editor.message',
stubs.MessageModule())
monkeypatch.setattr('qutebrowser.misc.editor.QProcess',
stubs.FakeQProcess())
config_stub.data = {'general': {'editor': [''],
'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)
def test_file_handling_closed_ok(self):
@ -147,12 +147,12 @@ class TestModifyTests:
"""
@pytest.fixture(autouse=True)
def setup(self, mocker, stubs, config_stub):
mocker.patch('qutebrowser.misc.editor.QProcess',
new_callable=stubs.FakeQProcess)
def setup(self, monkeypatch, stubs, config_stub):
monkeypatch.setattr('qutebrowser.misc.editor.QProcess',
stubs.FakeQProcess())
config_stub.data = {'general': {'editor': [''],
'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.editing_finished = mock.Mock()
@ -219,14 +219,14 @@ class TestErrorMessage:
"""
@pytest.yield_fixture(autouse=True)
def setup(self, mocker, stubs, config_stub):
mocker.patch('qutebrowser.misc.editor.QProcess',
new_callable=stubs.FakeQProcess)
mocker.patch('qutebrowser.misc.editor.message',
new=stubs.MessageModule())
def setup(self, monkeypatch, stubs, config_stub):
monkeypatch.setattr('qutebrowser.misc.editor.QProcess',
stubs.FakeQProcess())
monkeypatch.setattr('qutebrowser.misc.editor.message',
stubs.MessageModule())
config_stub.data = {'general': {'editor': [''],
'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)
yield
self.editor._cleanup() # pylint: disable=protected-access

View File

@ -23,10 +23,10 @@
import io
import os
import unittest
from unittest import mock
from qutebrowser.misc import lineparser
import pytest
from qutebrowser.misc import lineparser as lineparsermod
class LineParserWrapper:
@ -69,118 +69,129 @@ class LineParserWrapper:
def _prepare_save(self):
"""Keep track if _prepare_save has been called."""
self._test_save_prepared = True
return True
class TestableAppendLineParser(LineParserWrapper, lineparser.AppendLineParser):
class AppendLineParserTestable(LineParserWrapper,
lineparsermod.AppendLineParser):
"""Wrapper over AppendLineParser to make it testable."""
pass
class TestableLineParser(LineParserWrapper, lineparser.LineParser):
class LineParserTestable(LineParserWrapper, lineparsermod.LineParser):
"""Wrapper over LineParser to make it testable."""
pass
class TestableLimitLineParser(LineParserWrapper, lineparser.LimitLineParser):
class LimitLineParserTestable(LineParserWrapper,
lineparsermod.LimitLineParser):
"""Wrapper over LimitLineParser to make it testable."""
pass
@mock.patch('qutebrowser.misc.lineparser.os.path')
@mock.patch('qutebrowser.misc.lineparser.os')
class BaseLineParserTests(unittest.TestCase):
class TestBaseLineParser:
"""Tests for BaseLineParser."""
def setUp(self):
self._confdir = "this really doesn't matter"
self._fname = "and neither does this"
self._lineparser = lineparser.BaseLineParser(
self._confdir, self._fname)
CONFDIR = "this really doesn't matter"
FILENAME = "and neither does this"
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."""
os_path_mock.exists.return_value = True
self._lineparser._prepare_save()
self.assertFalse(os_mock.makedirs.called)
exists_mock = mocker.patch(
'qutebrowser.misc.lineparser.os.path.exists')
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."""
os_path_mock.exists.return_value = False
self._lineparser._prepare_save()
os_mock.makedirs.assert_called_with(self._confdir, 0o755)
exists_mock = mocker.patch(
'qutebrowser.misc.lineparser.os.path.exists')
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."""
def setUp(self):
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()
BASE_DATA = ['old data 1', 'old data 2']
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."""
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()."""
self._lineparser.new_data = ['new data 1', 'new data 2']
self._expected_data += self._lineparser.new_data
self._lineparser.save()
self.assertEqual(self._lineparser._data, self._get_expected())
new_data = ['new data 1', 'new data 2']
lineparser.new_data = new_data
lineparser.save()
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()."""
with self.assertRaises(ValueError):
iter(self._lineparser)
with pytest.raises(ValueError):
iter(lineparser)
def test_iter(self):
def test_iter(self, lineparser):
"""Test __iter__."""
self._lineparser.new_data = ['new data 1', 'new data 2']
self._expected_data += self._lineparser.new_data
with self._lineparser.open():
self.assertEqual(list(self._lineparser), self._expected_data)
new_data = ['new data 1', 'new data 2']
lineparser.new_data = new_data
with lineparser.open():
assert list(lineparser) == self.BASE_DATA + new_data
@mock.patch('qutebrowser.misc.lineparser.AppendLineParser._open')
def test_iter_not_found(self, open_mock):
def test_iter_not_found(self, mocker):
"""Test __iter__ with no file."""
open_mock = mocker.patch(
'qutebrowser.misc.lineparser.AppendLineParser._open')
open_mock.side_effect = FileNotFoundError
linep = lineparser.AppendLineParser('foo', 'bar')
linep.new_data = ['new data 1', 'new data 2']
expected_data = linep.new_data
new_data = ['new data 1', 'new data 2']
linep = lineparsermod.AppendLineParser('foo', 'bar')
linep.new_data = new_data
with linep.open():
self.assertEqual(list(linep), expected_data)
assert list(linep) == new_data
def test_get_recent_none(self):
"""Test get_recent with no data."""
linep = TestableAppendLineParser('this really', 'does not matter')
self.assertEqual(linep.get_recent(), [])
linep = AppendLineParserTestable('this really', 'does not matter')
assert linep.get_recent() == []
def test_get_recent_little(self):
def test_get_recent_little(self, lineparser):
"""Test get_recent with little data."""
data = [e + '\n' for e in self._expected_data]
self.assertEqual(self._lineparser.get_recent(), data)
data = [e + '\n' for e in self.BASE_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."""
size = 64
new_data = ['new data {}'.format(i) for i in range(size)]
self._lineparser.new_data = new_data
self._lineparser.save()
data = '\n'.join(self._expected_data + new_data)
lineparser.new_data = new_data
lineparser.save()
data = '\n'.join(self.BASE_DATA + new_data)
data = [e + '\n' for e in data[-(size - 1):].splitlines()]
self.assertEqual(self._lineparser.get_recent(size), data)
if __name__ == '__main__':
unittest.main()
assert lineparser.get_recent(size) == data

View File

@ -31,10 +31,11 @@ from qutebrowser.misc import readline
@pytest.fixture
def mocked_qapp(mocker, stubs):
def mocked_qapp(monkeypatch, stubs):
"""Fixture that mocks readline.QApplication and returns it."""
return mocker.patch('qutebrowser.misc.readline.QApplication',
new_callable=stubs.FakeQApplication)
stub = stubs.FakeQApplication()
monkeypatch.setattr('qutebrowser.misc.readline.QApplication', stub)
return stub
class TestNoneWidget:

View File

@ -22,231 +22,241 @@
"""Tests for qutebrowser.utils.log."""
import logging
import unittest
import argparse
import itertools
import sys
from unittest import mock
import pytest
from PyQt5.QtCore import qWarning
from qutebrowser.utils import log
from PyQt5.QtCore import qWarning
class BaseTest(unittest.TestCase):
"""Base class for logging tests.
@pytest.yield_fixture(autouse=True)
def restore_loggers():
"""Fixture to save/restore the logging state.
Based on CPython's Lib/test/test_logging.py.
"""
def setUp(self):
"""Save the old logging configuration."""
logger_dict = logging.getLogger().manager.loggerDict
logging._acquireLock()
try:
self.saved_handlers = logging._handlers.copy()
self.saved_handler_list = logging._handlerList[:]
self.saved_loggers = saved_loggers = logger_dict.copy()
self.saved_name_to_level = logging._nameToLevel.copy()
self.saved_level_to_name = logging._levelToName.copy()
self.logger_states = {}
saved_handlers = logging._handlers.copy()
saved_handler_list = logging._handlerList[:]
saved_loggers = saved_loggers = logger_dict.copy()
saved_name_to_level = logging._nameToLevel.copy()
saved_level_to_name = logging._levelToName.copy()
logger_states = {}
for name in saved_loggers:
self.logger_states[name] = getattr(saved_loggers[name],
'disabled', None)
logger_states[name] = getattr(saved_loggers[name], 'disabled',
None)
finally:
logging._releaseLock()
self.root_logger = logging.getLogger("")
self.root_handlers = self.root_logger.handlers[:]
self.original_logging_level = self.root_logger.getEffectiveLevel()
root_logger = logging.getLogger("")
root_handlers = root_logger.handlers[:]
original_logging_level = root_logger.getEffectiveLevel()
def tearDown(self):
"""Restore the original logging configuration."""
while self.root_logger.handlers:
h = self.root_logger.handlers[0]
self.root_logger.removeHandler(h)
yield
while root_logger.handlers:
h = root_logger.handlers[0]
root_logger.removeHandler(h)
h.close()
self.root_logger.setLevel(self.original_logging_level)
for h in self.root_handlers:
self.root_logger.addHandler(h)
root_logger.setLevel(original_logging_level)
for h in root_handlers:
root_logger.addHandler(h)
logging._acquireLock()
try:
logging._levelToName.clear()
logging._levelToName.update(self.saved_level_to_name)
logging._levelToName.update(saved_level_to_name)
logging._nameToLevel.clear()
logging._nameToLevel.update(self.saved_name_to_level)
logging._nameToLevel.update(saved_name_to_level)
logging._handlers.clear()
logging._handlers.update(self.saved_handlers)
logging._handlerList[:] = self.saved_handler_list
logging._handlers.update(saved_handlers)
logging._handlerList[:] = saved_handler_list
logger_dict = logging.getLogger().manager.loggerDict
logger_dict.clear()
logger_dict.update(self.saved_loggers)
logger_states = self.logger_states
for name in self.logger_states:
logger_dict.update(saved_loggers)
logger_states = logger_states
for name in logger_states:
if logger_states[name] is not None:
self.saved_loggers[name].disabled = logger_states[name]
saved_loggers[name].disabled = logger_states[name]
finally:
logging._releaseLock()
class LogFilterTests(unittest.TestCase):
@pytest.fixture(scope='session')
def log_counter():
"""Counter for logger fixture to get unique loggers."""
return itertools.count()
"""Tests for LogFilter.
Attributes:
logger: The logger we use to create records.
@pytest.fixture
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."""
return self.logger.makeRecord(name, level=level, fn=None, lno=0,
msg="", args=None, exc_info=None)
return logger.makeRecord(name, level=level, fn=None, lno=0, msg="",
args=None, exc_info=None)
def test_empty(self):
def test_empty(self, logger):
"""Test if an empty filter lets all messages through."""
logfilter = log.LogFilter(None)
record = self._make_record("eggs.bacon.spam")
self.assertTrue(logfilter.filter(record))
record = self._make_record("eggs")
self.assertTrue(logfilter.filter(record))
record = self._make_record(logger, "eggs.bacon.spam")
assert logfilter.filter(record)
record = self._make_record(logger, "eggs")
assert logfilter.filter(record)
def test_matching(self):
def test_matching(self, logger):
"""Test if a filter lets an exactly matching log record through."""
logfilter = log.LogFilter(["eggs", "bacon"])
record = self._make_record("eggs")
self.assertTrue(logfilter.filter(record))
record = self._make_record("bacon")
self.assertTrue(logfilter.filter(record))
record = self._make_record("spam")
self.assertFalse(logfilter.filter(record))
record = self._make_record(logger, "eggs")
assert logfilter.filter(record)
record = self._make_record(logger, "bacon")
assert logfilter.filter(record)
record = self._make_record(logger, "spam")
assert not logfilter.filter(record)
logfilter = log.LogFilter(["eggs.bacon"])
record = self._make_record("eggs.bacon")
self.assertTrue(logfilter.filter(record))
record = self._make_record(logger, "eggs.bacon")
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."""
logfilter = log.LogFilter(["eggs"])
record = self._make_record("eggsauce")
self.assertFalse(logfilter.filter(record))
record = self._make_record(logger, "eggsauce")
assert not logfilter.filter(record)
logfilter = log.LogFilter("eggs.bacon")
record = self._make_record("eggs.baconstrips")
self.assertFalse(logfilter.filter(record))
record = self._make_record(logger, "eggs.baconstrips")
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."""
logfilter = log.LogFilter(["eggs.bacon", "spam.ham"])
record = self._make_record("eggs.bacon.spam")
self.assertTrue(logfilter.filter(record))
record = self._make_record("spam.ham.salami")
self.assertTrue(logfilter.filter(record))
record = self._make_record(logger, "eggs.bacon.spam")
assert logfilter.filter(record)
record = self._make_record(logger, "spam.ham.salami")
assert logfilter.filter(record)
def test_debug(self):
def test_debug(self, logger):
"""Test if messages more important than debug are never filtered."""
logfilter = log.LogFilter(["eggs"])
# First check if the filter works as intended with debug messages
record = self._make_record("eggs")
self.assertTrue(logfilter.filter(record))
record = self._make_record("bacon")
self.assertFalse(logfilter.filter(record))
record = self._make_record(logger, "eggs")
assert logfilter.filter(record)
record = self._make_record(logger, "bacon")
assert not logfilter.filter(record)
# Then check if info is not filtered
record = self._make_record("eggs", level=logging.INFO)
self.assertTrue(logfilter.filter(record))
record = self._make_record("bacon", level=logging.INFO)
self.assertTrue(logfilter.filter(record))
record = self._make_record(logger, "eggs", level=logging.INFO)
assert logfilter.filter(record)
record = self._make_record(logger, "bacon", level=logging.INFO)
assert logfilter.filter(record)
class RAMHandlerTests(BaseTest):
class TestRAMHandler:
"""Tests for RAMHandler.
"""Tests for RAMHandler."""
Attributes:
logger: The logger we use to log to the handler.
handler: The RAMHandler we're testing.
old_level: The level the root logger had before executing the test.
old_handlers: The handlers the root logger had before executing the
test.
"""
@pytest.fixture
def handler(self, logger):
"""Fixture providing a RAMHandler."""
handler = log.RAMHandler(capacity=2)
handler.setLevel(logging.NOTSET)
logger.addHandler(handler)
return handler
def setUp(self):
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):
def test_filled(self, handler, logger):
"""Test handler with exactly as much records as it can hold."""
self.logger.debug("One")
self.logger.debug("Two")
self.assertEqual(len(self.handler._data), 2)
self.assertEqual(self.handler._data[0].msg, "One")
self.assertEqual(self.handler._data[1].msg, "Two")
logger.debug("One")
logger.debug("Two")
assert len(handler._data) == 2
assert handler._data[0].msg == "One"
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."""
self.logger.debug("One")
self.logger.debug("Two")
self.logger.debug("Three")
self.assertEqual(len(self.handler._data), 2)
self.assertEqual(self.handler._data[0].msg, "Two")
self.assertEqual(self.handler._data[1].msg, "Three")
logger.debug("One")
logger.debug("Two")
logger.debug("Three")
assert len(handler._data) == 2
assert handler._data[0].msg == "Two"
assert handler._data[1].msg == "Three"
def test_dump_log(self):
def test_dump_log(self, handler, logger):
"""Test dump_log()."""
self.logger.debug("One")
self.logger.debug("Two")
self.logger.debug("Three")
self.assertEqual(self.handler.dump_log(), "Two\nThree")
logger.debug("One")
logger.debug("Two")
logger.debug("Three")
assert handler.dump_log() == "Two\nThree"
@mock.patch('qutebrowser.utils.log.qInstallMessageHandler', autospec=True)
class InitLogTests(BaseTest):
class TestInitLog:
"""Tests for init_log."""
def setUp(self):
super().setUp()
self.args = argparse.Namespace(debug=True, loglevel=logging.DEBUG,
@pytest.fixture(autouse=True)
def setup(self, mocker):
"""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="")
def test_stderr_none(self, _mock):
def test_stderr_none(self, args):
"""Test init_log with sys.stderr = None."""
old_stderr = sys.stderr
sys.stderr = None
log.init_log(self.args)
log.init_log(args)
sys.stderr = old_stderr
class HideQtWarningTests(BaseTest):
class TestHideQtWarning:
"""Tests for hide_qt_warning/QtWarningFilter."""
def test_unfiltered(self):
def test_unfiltered(self, caplog):
"""Test a message which is not filtered."""
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")
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)."""
with log.hide_qt_warning("Hello", logger='qt-tests'):
with caplog.atLevel(logging.WARNING, logger='qt-tests'):
qWarning("Hello")
assert not caplog.records()
def test_filtered_start(self):
def test_filtered_start(self, caplog):
"""Test a message which is filtered (match at line start)."""
with log.hide_qt_warning("Hello", logger='qt-tests'):
with caplog.atLevel(logging.WARNING, logger='qt-tests'):
qWarning("Hello World")
assert not caplog.records()
def test_filtered_whitespace(self):
def test_filtered_whitespace(self, caplog):
"""Test a message which is filtered (match with whitespace)."""
with log.hide_qt_warning("Hello", logger='qt-tests'):
with caplog.atLevel(logging.WARNING, logger='qt-tests'):
qWarning(" Hello World ")
if __name__ == '__main__':
unittest.main()
assert not caplog.records()

View File

@ -22,6 +22,8 @@
import os
import os.path
import sys
import types
import collections
from PyQt5.QtWidgets import QApplication
import pytest
@ -114,3 +116,51 @@ class TestGetStandardDirWindows:
"""Test cache dir."""
expected = ['qutebrowser_test', 'cache']
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."""
@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."""
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):
"""Test default search engine."""
@ -159,24 +159,24 @@ class TestIsUrl:
)
@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."""
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
@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."""
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
@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."""
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')

View File

@ -313,16 +313,14 @@ class TestKeyEventToString:
def test_key_and_modifier(self, fake_keyevent_factory):
"""Test with key and modifier pressed."""
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):
"""Test with key and multiple modifier pressed."""
"""Test with key and multiple modifiers pressed."""
evt = fake_keyevent_factory(
key=Qt.Key_A, modifiers=(Qt.ControlModifier | Qt.AltModifier |
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'

View File

@ -19,45 +19,38 @@
"""Tests for the Enum class."""
import unittest
from qutebrowser.utils import usertypes
import pytest
# 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:
enum: The enum we're testing.
"""
def setUp(self):
self.enum = usertypes.enum('Enum', ['one', 'two'])
def test_values(self):
def test_values(enum):
"""Test if enum members resolve to the right values."""
self.assertEqual(self.enum.one.value, 1)
self.assertEqual(self.enum.two.value, 2)
assert enum.one.value == 1
assert enum.two.value == 2
def test_name(self):
def test_name(enum):
"""Test .name mapping."""
self.assertEqual(self.enum.one.name, 'one')
self.assertEqual(self.enum.two.name, 'two')
assert enum.one.name == 'one'
assert enum.two.name == 'two'
def test_unknown(self):
def test_unknown(enum):
"""Test invalid values which should raise an AttributeError."""
with self.assertRaises(AttributeError):
_ = self.enum.three
with pytest.raises(AttributeError):
_ = enum.three
def test_start(self):
def test_start():
"""Test the start= argument."""
e = usertypes.enum('Enum', ['three', 'four'], start=3)
self.assertEqual(e.three.value, 3)
self.assertEqual(e.four.value, 4)
if __name__ == '__main__':
unittest.main()
assert e.three.value == 3
assert e.four.value == 4

View File

@ -21,364 +21,331 @@
"""Tests for the NeighborList class."""
import unittest
from qutebrowser.utils import usertypes
import pytest
class InitTests(unittest.TestCase):
"""Just try to init some neighborlists.
class TestInit:
Attributes:
nl: The NeighborList we're testing.
"""
"""Just try to init some neighborlists."""
def test_empty(self):
"""Test constructing an empty NeighborList."""
nl = usertypes.NeighborList()
self.assertEqual(nl.items, [])
assert nl.items == []
def test_items(self):
"""Test constructing an NeighborList with items."""
nl = usertypes.NeighborList([1, 2, 3])
self.assertEqual(nl.items, [1, 2, 3])
assert nl.items == [1, 2, 3]
def test_len(self):
"""Test len() on NeighborList."""
nl = usertypes.NeighborList([1, 2, 3])
self.assertEqual(len(nl), 3)
assert len(nl) == 3
def test_contains(self):
"""Test 'in' on NeighborList."""
nl = usertypes.NeighborList([1, 2, 3])
self.assertIn(2, nl)
self.assertNotIn(4, nl)
assert 2 in nl
assert 4 not in nl
class DefaultTests(unittest.TestCase):
class TestDefaultArg:
"""Test the default argument.
Attributes:
nl: The NeighborList we're testing.
"""
"""Test the default argument."""
def test_simple(self):
"""Test default with a numeric argument."""
nl = usertypes.NeighborList([1, 2, 3], default=2)
self.assertEqual(nl._idx, 1)
assert nl._idx == 1
def test_none(self):
"""Test default 'None'."""
nl = usertypes.NeighborList([1, 2, None], default=None)
self.assertEqual(nl._idx, 2)
assert nl._idx == 2
def test_unset(self):
"""Test unset default value."""
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:
nl: The NeighborList we're testing.
"""
@pytest.fixture
def neighborlist(self):
return usertypes.NeighborList()
def setUp(self):
self.nl = usertypes.NeighborList()
def test_curitem(self):
def test_curitem(self, neighborlist):
"""Test curitem with no item."""
with self.assertRaises(IndexError):
self.nl.curitem()
with pytest.raises(IndexError):
neighborlist.curitem()
def test_firstitem(self):
def test_firstitem(self, neighborlist):
"""Test firstitem with no item."""
with self.assertRaises(IndexError):
self.nl.firstitem()
with pytest.raises(IndexError):
neighborlist.firstitem()
def test_lastitem(self):
def test_lastitem(self, neighborlist):
"""Test lastitem with no item."""
with self.assertRaises(IndexError):
self.nl.lastitem()
with pytest.raises(IndexError):
neighborlist.lastitem()
def test_getitem(self):
def test_getitem(self, neighborlist):
"""Test getitem with no item."""
with self.assertRaises(IndexError):
self.nl.getitem(1)
with pytest.raises(IndexError):
neighborlist.getitem(1)
class ItemTests(unittest.TestCase):
class TestItems:
"""Tests with items.
"""Tests with items."""
Attributes:
nl: The NeighborList we're testing.
"""
@pytest.fixture
def neighborlist(self):
return usertypes.NeighborList([1, 2, 3, 4, 5], default=3)
def setUp(self):
self.nl = usertypes.NeighborList([1, 2, 3, 4, 5], default=3)
def test_curitem(self):
def test_curitem(self, neighborlist):
"""Test curitem()."""
self.assertEqual(self.nl._idx, 2)
self.assertEqual(self.nl.curitem(), 3)
self.assertEqual(self.nl._idx, 2)
assert neighborlist._idx == 2
assert neighborlist.curitem() == 3
assert neighborlist._idx == 2
def test_nextitem(self):
def test_nextitem(self, neighborlist):
"""Test nextitem()."""
self.assertEqual(self.nl.nextitem(), 4)
self.assertEqual(self.nl._idx, 3)
self.assertEqual(self.nl.nextitem(), 5)
self.assertEqual(self.nl._idx, 4)
assert neighborlist.nextitem() == 4
assert neighborlist._idx == 3
assert neighborlist.nextitem() == 5
assert neighborlist._idx == 4
def test_previtem(self):
def test_previtem(self, neighborlist):
"""Test previtem()."""
self.assertEqual(self.nl.previtem(), 2)
self.assertEqual(self.nl._idx, 1)
self.assertEqual(self.nl.previtem(), 1)
self.assertEqual(self.nl._idx, 0)
assert neighborlist.previtem() == 2
assert neighborlist._idx == 1
assert neighborlist.previtem() == 1
assert neighborlist._idx == 0
def test_firstitem(self):
def test_firstitem(self, neighborlist):
"""Test firstitem()."""
self.assertEqual(self.nl.firstitem(), 1)
self.assertEqual(self.nl._idx, 0)
assert neighborlist.firstitem() == 1
assert neighborlist._idx == 0
def test_lastitem(self):
def test_lastitem(self, neighborlist):
"""Test lastitem()."""
self.assertEqual(self.nl.lastitem(), 5)
self.assertEqual(self.nl._idx, 4)
assert neighborlist.lastitem() == 5
assert neighborlist._idx == 4
def test_reset(self):
def test_reset(self, neighborlist):
"""Test reset()."""
self.nl.nextitem()
self.assertEqual(self.nl._idx, 3)
self.nl.reset()
self.assertEqual(self.nl._idx, 2)
neighborlist.nextitem()
assert neighborlist._idx == 3
neighborlist.reset()
assert neighborlist._idx == 2
def test_getitem(self):
def test_getitem(self, neighborlist):
"""Test getitem()."""
self.assertEqual(self.nl.getitem(2), 5)
self.assertEqual(self.nl._idx, 4)
self.nl.reset()
self.assertEqual(self.nl.getitem(-2), 1)
self.assertEqual(self.nl._idx, 0)
assert neighborlist.getitem(2) == 5
assert neighborlist._idx == 4
neighborlist.reset()
assert neighborlist.getitem(-2) == 1
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:
nl: The NeighborList we're testing.
"""
@pytest.fixture
def neighborlist(self):
return usertypes.NeighborList([1], default=1)
def setUp(self):
self.nl = usertypes.NeighborList([1], default=1)
def test_first_wrap(self):
def test_first_wrap(self, neighborlist):
"""Test out of bounds previtem() with mode=wrap."""
self.nl._mode = usertypes.NeighborList.Modes.wrap
self.nl.firstitem()
self.assertEqual(self.nl._idx, 0)
self.assertEqual(self.nl.previtem(), 1)
self.assertEqual(self.nl._idx, 0)
neighborlist._mode = usertypes.NeighborList.Modes.wrap
neighborlist.firstitem()
assert neighborlist._idx == 0
assert neighborlist.previtem() == 1
assert neighborlist._idx == 0
def test_first_block(self):
def test_first_block(self, neighborlist):
"""Test out of bounds previtem() with mode=block."""
self.nl._mode = usertypes.NeighborList.Modes.block
self.nl.firstitem()
self.assertEqual(self.nl._idx, 0)
self.assertEqual(self.nl.previtem(), 1)
self.assertEqual(self.nl._idx, 0)
neighborlist._mode = usertypes.NeighborList.Modes.block
neighborlist.firstitem()
assert neighborlist._idx == 0
assert neighborlist.previtem() == 1
assert neighborlist._idx == 0
def test_first_raise(self):
def test_first_raise(self, neighborlist):
"""Test out of bounds previtem() with mode=raise."""
self.nl._mode = usertypes.NeighborList.Modes.exception
self.nl.firstitem()
self.assertEqual(self.nl._idx, 0)
with self.assertRaises(IndexError):
self.nl.previtem()
self.assertEqual(self.nl._idx, 0)
neighborlist._mode = usertypes.NeighborList.Modes.exception
neighborlist.firstitem()
assert neighborlist._idx == 0
with pytest.raises(IndexError):
neighborlist.previtem()
assert neighborlist._idx == 0
def test_last_wrap(self):
def test_last_wrap(self, neighborlist):
"""Test out of bounds nextitem() with mode=wrap."""
self.nl._mode = usertypes.NeighborList.Modes.wrap
self.nl.lastitem()
self.assertEqual(self.nl._idx, 0)
self.assertEqual(self.nl.nextitem(), 1)
self.assertEqual(self.nl._idx, 0)
neighborlist._mode = usertypes.NeighborList.Modes.wrap
neighborlist.lastitem()
assert neighborlist._idx == 0
assert neighborlist.nextitem() == 1
assert neighborlist._idx == 0
def test_last_block(self):
def test_last_block(self, neighborlist):
"""Test out of bounds nextitem() with mode=block."""
self.nl._mode = usertypes.NeighborList.Modes.block
self.nl.lastitem()
self.assertEqual(self.nl._idx, 0)
self.assertEqual(self.nl.nextitem(), 1)
self.assertEqual(self.nl._idx, 0)
neighborlist._mode = usertypes.NeighborList.Modes.block
neighborlist.lastitem()
assert neighborlist._idx == 0
assert neighborlist.nextitem() == 1
assert neighborlist._idx == 0
def test_last_raise(self):
def test_last_raise(self, neighborlist):
"""Test out of bounds nextitem() with mode=raise."""
self.nl._mode = usertypes.NeighborList.Modes.exception
self.nl.lastitem()
self.assertEqual(self.nl._idx, 0)
with self.assertRaises(IndexError):
self.nl.nextitem()
self.assertEqual(self.nl._idx, 0)
neighborlist._mode = usertypes.NeighborList.Modes.exception
neighborlist.lastitem()
assert neighborlist._idx == 0
with pytest.raises(IndexError):
neighborlist.nextitem()
assert neighborlist._idx == 0
class BlockTests(unittest.TestCase):
class TestBlockMode:
"""Tests with mode=block.
"""Tests with mode=block."""
Attributes:
nl: The NeighborList we're testing.
"""
def setUp(self):
self.nl = usertypes.NeighborList(
@pytest.fixture
def neighborlist(self):
return usertypes.NeighborList(
[1, 2, 3, 4, 5], default=3,
mode=usertypes.NeighborList.Modes.block)
def test_first(self):
def test_first(self, neighborlist):
"""Test out of bounds previtem()."""
self.nl.firstitem()
self.assertEqual(self.nl._idx, 0)
self.assertEqual(self.nl.previtem(), 1)
self.assertEqual(self.nl._idx, 0)
neighborlist.firstitem()
assert neighborlist._idx == 0
assert neighborlist.previtem() == 1
assert neighborlist._idx == 0
def test_last(self):
def test_last(self, neighborlist):
"""Test out of bounds nextitem()."""
self.nl.lastitem()
self.assertEqual(self.nl._idx, 4)
self.assertEqual(self.nl.nextitem(), 5)
self.assertEqual(self.nl._idx, 4)
neighborlist.lastitem()
assert neighborlist._idx == 4
assert neighborlist.nextitem() == 5
assert neighborlist._idx == 4
class WrapTests(unittest.TestCase):
class TestWrapMode:
"""Tests with mode=wrap.
"""Tests with mode=wrap."""
Attributes:
nl: The NeighborList we're testing.
"""
def setUp(self):
self.nl = usertypes.NeighborList(
@pytest.fixture
def neighborlist(self):
return usertypes.NeighborList(
[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()."""
self.nl.firstitem()
self.assertEqual(self.nl._idx, 0)
self.assertEqual(self.nl.previtem(), 5)
self.assertEqual(self.nl._idx, 4)
neighborlist.firstitem()
assert neighborlist._idx == 0
assert neighborlist.previtem() == 5
assert neighborlist._idx == 4
def test_last(self):
def test_last(self, neighborlist):
"""Test out of bounds nextitem()."""
self.nl.lastitem()
self.assertEqual(self.nl._idx, 4)
self.assertEqual(self.nl.nextitem(), 1)
self.assertEqual(self.nl._idx, 0)
neighborlist.lastitem()
assert neighborlist._idx == 4
assert neighborlist.nextitem() == 1
assert neighborlist._idx == 0
class RaiseTests(unittest.TestCase):
class TestExceptionMode:
"""Tests with mode=exception.
"""Tests with mode=exception."""
Attributes:
nl: The NeighborList we're testing.
"""
def setUp(self):
self.nl = usertypes.NeighborList(
@pytest.fixture
def neighborlist(self):
return usertypes.NeighborList(
[1, 2, 3, 4, 5], default=3,
mode=usertypes.NeighborList.Modes.exception)
def test_first(self):
def test_first(self, neighborlist):
"""Test out of bounds previtem()."""
self.nl.firstitem()
self.assertEqual(self.nl._idx, 0)
with self.assertRaises(IndexError):
self.nl.previtem()
self.assertEqual(self.nl._idx, 0)
neighborlist.firstitem()
assert neighborlist._idx == 0
with pytest.raises(IndexError):
neighborlist.previtem()
assert neighborlist._idx == 0
def test_last(self):
def test_last(self, neighborlist):
"""Test out of bounds nextitem()."""
self.nl.lastitem()
self.assertEqual(self.nl._idx, 4)
with self.assertRaises(IndexError):
self.nl.nextitem()
self.assertEqual(self.nl._idx, 4)
neighborlist.lastitem()
assert neighborlist._idx == 4
with pytest.raises(IndexError):
neighborlist.nextitem()
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:
nl: The NeighborList we're testing.
"""
@pytest.fixture
def neighborlist(self):
return usertypes.NeighborList([20, 9, 1, 5])
def setUp(self):
self.nl = usertypes.NeighborList([20, 9, 1, 5])
def test_bigger(self):
def test_bigger(self, neighborlist):
"""Test fuzzyval with snapping to a bigger value."""
self.nl.fuzzyval = 7
self.assertEqual(self.nl.nextitem(), 9)
self.assertEqual(self.nl._idx, 1)
self.assertEqual(self.nl.nextitem(), 1)
self.assertEqual(self.nl._idx, 2)
neighborlist.fuzzyval = 7
assert neighborlist.nextitem() == 9
assert neighborlist._idx == 1
assert neighborlist.nextitem() == 1
assert neighborlist._idx == 2
def test_smaller(self):
def test_smaller(self, neighborlist):
"""Test fuzzyval with snapping to a smaller value."""
self.nl.fuzzyval = 7
self.assertEqual(self.nl.previtem(), 5)
self.assertEqual(self.nl._idx, 3)
self.assertEqual(self.nl.previtem(), 1)
self.assertEqual(self.nl._idx, 2)
neighborlist.fuzzyval = 7
assert neighborlist.previtem() == 5
assert neighborlist._idx == 3
assert neighborlist.previtem() == 1
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."""
self.nl.fuzzyval = 20
self.assertEqual(self.nl.nextitem(), 9)
self.assertEqual(self.nl._idx, 1)
neighborlist.fuzzyval = 20
assert neighborlist.nextitem() == 9
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."""
self.nl.fuzzyval = 5
self.assertEqual(self.nl.previtem(), 1)
self.assertEqual(self.nl._idx, 2)
neighborlist.fuzzyval = 5
assert neighborlist.previtem() == 1
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."""
self.nl.fuzzyval = 30
self.assertEqual(self.nl.nextitem(), 20)
self.assertEqual(self.nl._idx, 0)
neighborlist.fuzzyval = 30
assert neighborlist.nextitem() == 20
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."""
self.nl.fuzzyval = 30
self.assertEqual(self.nl.previtem(), 20)
self.assertEqual(self.nl._idx, 0)
neighborlist.fuzzyval = 30
assert neighborlist.previtem() == 20
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."""
self.nl.fuzzyval = 0
self.assertEqual(self.nl.nextitem(), 1)
self.assertEqual(self.nl._idx, 2)
neighborlist.fuzzyval = 0
assert neighborlist.nextitem() == 1
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."""
self.nl.fuzzyval = 0
self.assertEqual(self.nl.previtem(), 1)
self.assertEqual(self.nl._idx, 2)
if __name__ == '__main__':
unittest.main()
neighborlist.fuzzyval = 0
assert neighborlist.previtem() == 1
assert neighborlist._idx == 2

34
tox.ini
View File

@ -15,20 +15,25 @@ envdir = {toxinidir}/.venv
usedevelop = true
[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 =
-r{toxinidir}/requirements.txt
py==1.4.27
pytest==2.7.0
pytest==2.7.1
pytest-capturelog==0.7
pytest-qt==1.3.0
pytest-mock==0.5
pytest-html==1.3.1
# 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 py.test --strict {posargs}
{envpython} -m py.test --strict -rfEsw {posargs}
[testenv:coverage]
passenv = DISPLAY XAUTHORITY HOME
deps =
{[testenv:unittests]deps}
coverage==3.7.1
@ -36,7 +41,7 @@ deps =
cov-core==1.15.0
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]
commands =
@ -48,7 +53,7 @@ commands =
skip_install = true
setenv = PYTHONPATH={toxinidir}/scripts
deps =
-rrequirements.txt
-r{toxinidir}/requirements.txt
astroid==1.3.6
beautifulsoup4==4.3.2
pylint==1.4.3
@ -62,6 +67,7 @@ commands =
[testenv:pep257]
skip_install = true
deps = pep257==0.5.0
passenv = LANG
# Disabled checks:
# D102: Docstring missing, will be handled by others
# 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]
skip_install = true
deps =
-rrequirements.txt
-r{toxinidir}/requirements.txt
pyflakes==0.8.1
pep8==1.5.7 # rq.filter: <1.6.0
flake8==2.4.0
@ -91,7 +97,7 @@ commands =
[testenv:check-manifest]
skip_install = true
deps =
check-manifest==0.24
check-manifest==0.25
commands =
{[testenv:mkvenv]commands}
{envdir}/bin/check-manifest --ignore 'qutebrowser/git-commit-id,qutebrowser/html/doc,qutebrowser/html/doc/*,*/__pycache__'
@ -100,13 +106,25 @@ commands =
skip_install = true
whitelist_externals = git
deps =
-rrequirements.txt
-r{toxinidir}/requirements.txt
commands =
{[testenv:mkvenv]commands}
{envpython} scripts/src2asciidoc.py
git --no-pager diff --exit-code --stat
{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]
norecursedirs = .tox .venv
markers =