Merge branch 'master' into spell

This commit is contained in:
Michał Siedlaczek 2017-10-04 09:47:42 -04:00 committed by GitHub
commit e20ad95666
111 changed files with 3641 additions and 1956 deletions

View File

@ -27,6 +27,7 @@ exclude scripts/asciidoc2html.py
exclude doc/notes exclude doc/notes
recursive-exclude doc *.asciidoc recursive-exclude doc *.asciidoc
include doc/qutebrowser.1.asciidoc include doc/qutebrowser.1.asciidoc
include doc/changelog.asciidoc
prune tests prune tests
prune qutebrowser/3rdparty prune qutebrowser/3rdparty
prune misc/requirements prune misc/requirements

View File

@ -11,7 +11,6 @@ image:icons/qutebrowser-64x64.png[qutebrowser logo] *A keyboard-driven, vim-like
image:https://img.shields.io/pypi/l/qutebrowser.svg?style=flat["license badge",link="https://github.com/qutebrowser/qutebrowser/blob/master/LICENSE"] image:https://img.shields.io/pypi/l/qutebrowser.svg?style=flat["license badge",link="https://github.com/qutebrowser/qutebrowser/blob/master/LICENSE"]
image:https://img.shields.io/pypi/v/qutebrowser.svg?style=flat["version badge",link="https://pypi.python.org/pypi/qutebrowser/"] image:https://img.shields.io/pypi/v/qutebrowser.svg?style=flat["version badge",link="https://pypi.python.org/pypi/qutebrowser/"]
image:https://requires.io/github/qutebrowser/qutebrowser/requirements.svg?branch=master["requirements badge",link="https://requires.io/github/qutebrowser/qutebrowser/requirements/?branch=master"]
image:https://travis-ci.org/qutebrowser/qutebrowser.svg?branch=master["Build Status", link="https://travis-ci.org/qutebrowser/qutebrowser"] image:https://travis-ci.org/qutebrowser/qutebrowser.svg?branch=master["Build Status", link="https://travis-ci.org/qutebrowser/qutebrowser"]
image:https://ci.appveyor.com/api/projects/status/5pyauww2k68bbow2/branch/master?svg=true["AppVeyor build status", link="https://ci.appveyor.com/project/qutebrowser/qutebrowser"] image:https://ci.appveyor.com/api/projects/status/5pyauww2k68bbow2/branch/master?svg=true["AppVeyor build status", link="https://ci.appveyor.com/project/qutebrowser/qutebrowser"]
image:https://codecov.io/github/qutebrowser/qutebrowser/coverage.svg?branch=master["coverage badge",link="https://codecov.io/github/qutebrowser/qutebrowser?branch=master"] image:https://codecov.io/github/qutebrowser/qutebrowser/coverage.svg?branch=master["coverage badge",link="https://codecov.io/github/qutebrowser/qutebrowser?branch=master"]

View File

@ -39,7 +39,10 @@ Breaking changes
v0.9.0) is now not supported anymore. v0.9.0) is now not supported anymore.
- Upgrading qutebrowser with a version older than v0.4.0 still running now won't - Upgrading qutebrowser with a version older than v0.4.0 still running now won't
work properly anymore. work properly anymore.
- The `--harfbuzz` commandline argument got dropped. - The `--harfbuzz` and `--relaxed-config` commandline arguments got dropped.
- `:set` now doesn't support toggling/cycling values anymore, that functionality
got moved to `:config-cycle`.
- `:scroll-perc` got renamed to `:scroll-to-perc`.
Major changes Major changes
~~~~~~~~~~~~~ ~~~~~~~~~~~~~
@ -57,6 +60,14 @@ Added
- New `backend` setting to select the backend to use (auto/webengine/webkit). - New `backend` setting to select the backend to use (auto/webengine/webkit).
Together with the previous setting, this should make wrapper scripts Together with the previous setting, this should make wrapper scripts
unnecessary. unnecessary.
- Proxy authentication is now supported with the QtWebEngine backend.
- New config commands:
- `:config-cycle` to cycle an option between multiple values.
- `:config-unset` to remove a configured option
- `:config-clear` to remove all configured options
- `:config-source` to (re-)read a `config.py` file
- `:config-edit` to open the `config.py` file in an editor
- New `:version` command which opens `qute://version`.
Changed Changed
~~~~~~~ ~~~~~~~
@ -66,12 +77,20 @@ Changed
- When there are multiple messages shown, the timeout is increased. - When there are multiple messages shown, the timeout is increased.
- `:search` now only clears the search if one was displayed before, so pressing - `:search` now only clears the search if one was displayed before, so pressing
`<Escape>` doesn't un-focus inputs anymore. `<Escape>` doesn't un-focus inputs anymore.
- Pinned tabs now adjust to their text's width, so the `tabs.width.pinned`
setting got removed.
- `:set-cmd-text` now has a `--run-on-count` argument to run the underlying
command directly if a count was given.
Fixes Fixes
~~~~~ ~~~~~
- Exiting fullscreen via `:fullscreen` or buttons on a page now - Exiting fullscreen via `:fullscreen` or buttons on a page now
restores the correct previous window state (maximized/fullscreen). restores the correct previous window state (maximized/fullscreen).
- When `input.insert_mode.auto_load` is set, background tabs now don't enter
insert mode anymore.
- The keybinding help widget now works correctly when using keybindings with a
count.
v0.11.1 (unreleased) v0.11.1 (unreleased)
-------------------- --------------------

View File

@ -31,6 +31,11 @@ It is possible to run or bind multiple commands by separating them with `;;`.
|<<bookmark-load,bookmark-load>>|Load a bookmark. |<<bookmark-load,bookmark-load>>|Load a bookmark.
|<<buffer,buffer>>|Select tab by index or url/title best match. |<<buffer,buffer>>|Select tab by index or url/title best match.
|<<close,close>>|Close the current window. |<<close,close>>|Close the current window.
|<<config-clear,config-clear>>|Set all settings back to their default.
|<<config-cycle,config-cycle>>|Cycle an option between multiple values.
|<<config-edit,config-edit>>|Open the config.py file in the editor.
|<<config-source,config-source>>|Read a config.py file.
|<<config-unset,config-unset>>|Unset an option.
|<<download,download>>|Download a given URL, or current page if no URL given. |<<download,download>>|Download a given URL, or current page if no URL given.
|<<download-cancel,download-cancel>>|Cancel the last/[count]th download. |<<download-cancel,download-cancel>>|Cancel the last/[count]th download.
|<<download-clear,download-clear>>|Remove all finished downloads from the list. |<<download-clear,download-clear>>|Remove all finished downloads from the list.
@ -86,6 +91,7 @@ It is possible to run or bind multiple commands by separating them with `;;`.
|<<tab-prev,tab-prev>>|Switch to the previous tab, or switch [count] tabs back. |<<tab-prev,tab-prev>>|Switch to the previous tab, or switch [count] tabs back.
|<<unbind,unbind>>|Unbind a keychain. |<<unbind,unbind>>|Unbind a keychain.
|<<undo,undo>>|Re-open a closed tab. |<<undo,undo>>|Re-open a closed tab.
|<<version,version>>|Show version information.
|<<view-source,view-source>>|Show the source of the current page in a new tab. |<<view-source,view-source>>|Show the source of the current page in a new tab.
|<<window-only,window-only>>|Close all windows except for the current one. |<<window-only,window-only>>|Close all windows except for the current one.
|<<yank,yank>>|Yank something to the clipboard or primary selection. |<<yank,yank>>|Yank something to the clipboard or primary selection.
@ -115,7 +121,7 @@ How many pages to go back.
[[bind]] [[bind]]
=== bind === bind
Syntax: +:bind [*--mode* 'mode'] [*--force*] 'key' ['command']+ Syntax: +:bind [*--mode* 'mode'] 'key' ['command']+
Bind a key to a command. Bind a key to a command.
@ -128,7 +134,6 @@ Bind a key to a command.
* +*-m*+, +*--mode*+: A comma-separated list of modes to bind the key in (default: `normal`). See `:help bindings.commands` for the * +*-m*+, +*--mode*+: A comma-separated list of modes to bind the key in (default: `normal`). See `:help bindings.commands` for the
available modes. available modes.
* +*-f*+, +*--force*+: Rebind the key if it is already bound.
==== note ==== note
* This command does not split arguments after the last argument and handles quotes literally. * This command does not split arguments after the last argument and handles quotes literally.
@ -184,20 +189,83 @@ Load a bookmark.
[[buffer]] [[buffer]]
=== buffer === buffer
Syntax: +:buffer 'index'+ Syntax: +:buffer ['index']+
Select tab by index or url/title best match. Select tab by index or url/title best match.
Focuses window if necessary. Focuses window if necessary when index is given. If both index and count are given, use count.
==== positional arguments ==== positional arguments
* +'index'+: The [win_id/]index of the tab to focus. Or a substring in which case the closest match will be focused. * +'index'+: The [win_id/]index of the tab to focus. Or a substring in which case the closest match will be focused.
==== count
The tab index to focus, starting with 1.
[[close]] [[close]]
=== close === close
Close the current window. Close the current window.
[[config-clear]]
=== config-clear
Syntax: +:config-clear [*--save*]+
Set all settings back to their default.
==== optional arguments
* +*-s*+, +*--save*+: If given, all configuration in autoconfig.yml is also removed.
[[config-cycle]]
=== config-cycle
Syntax: +:config-cycle [*--temp*] [*--print*] 'option' ['values' ['values' ...]]+
Cycle an option between multiple values.
==== positional arguments
* +'option'+: The name of the option.
* +'values'+: The values to cycle through.
==== optional arguments
* +*-t*+, +*--temp*+: Set value temporarily until qutebrowser is closed.
* +*-p*+, +*--print*+: Print the value after setting.
[[config-edit]]
=== config-edit
Syntax: +:config-edit [*--no-source*]+
Open the config.py file in the editor.
==== optional arguments
* +*-n*+, +*--no-source*+: Don't re-source the config file after editing.
[[config-source]]
=== config-source
Syntax: +:config-source [*--clear*] ['filename']+
Read a config.py file.
==== positional arguments
* +'filename'+: The file to load. If not given, loads the default config.py.
==== optional arguments
* +*-c*+, +*--clear*+: Clear current settings first.
[[config-unset]]
=== config-unset
Syntax: +:config-unset [*--temp*] 'option'+
Unset an option.
This sets an option back to its default and removes it from autoconfig.yml.
==== positional arguments
* +'option'+: The name of the option.
==== optional arguments
* +*-t*+, +*--temp*+: Don't touch autoconfig.yml.
[[download]] [[download]]
=== download === download
Syntax: +:download [*--mhtml*] [*--dest* 'dest'] ['url'] ['dest-old']+ Syntax: +:download [*--mhtml*] [*--dest* 'dest'] ['url'] ['dest-old']+
@ -773,15 +841,15 @@ Save a session.
[[set]] [[set]]
=== set === set
Syntax: +:set [*--temp*] [*--print*] ['option'] ['values' ['values' ...]]+ Syntax: +:set [*--temp*] [*--print*] ['option'] ['value']+
Set an option. Set an option.
If the option name ends with '?', the value of the option is shown instead. If the option name ends with '!' and it is a boolean value, toggle it. If the option name ends with '?', the value of the option is shown instead.
==== positional arguments ==== positional arguments
* +'option'+: The name of the option. * +'option'+: The name of the option.
* +'values'+: The value to set, or the values to cycle through. * +'value'+: The value to set.
==== optional arguments ==== optional arguments
* +*-t*+, +*--temp*+: Set value temporarily until qutebrowser is closed. * +*-t*+, +*--temp*+: Set value temporarily until qutebrowser is closed.
@ -789,7 +857,7 @@ If the option name ends with '?', the value of the option is shown instead. If t
[[set-cmd-text]] [[set-cmd-text]]
=== set-cmd-text === set-cmd-text
Syntax: +:set-cmd-text [*--space*] [*--append*] 'text'+ Syntax: +:set-cmd-text [*--space*] [*--append*] [*--run-on-count*] 'text'+
Preset the statusbar to some text. Preset the statusbar to some text.
@ -799,6 +867,11 @@ Preset the statusbar to some text.
==== optional arguments ==== optional arguments
* +*-s*+, +*--space*+: If given, a space is added to the end. * +*-s*+, +*--space*+: If given, a space is added to the end.
* +*-a*+, +*--append*+: If given, the text is appended to the current text. * +*-a*+, +*--append*+: If given, the text is appended to the current text.
* +*-r*+, +*--run-on-count*+: If given with a count, the command is run with the given count rather than setting the command text.
==== count
The count if given.
==== note ==== note
* This command does not split arguments after the last argument and handles quotes literally. * This command does not split arguments after the last argument and handles quotes literally.
@ -919,7 +992,7 @@ Close all tabs except for the current one.
=== tab-pin === tab-pin
Pin/Unpin the current/[count]th tab. Pin/Unpin the current/[count]th tab.
Pinning a tab shrinks it to `tabs.width.pinned` size. Attempting to close a pinned tab will cause a confirmation, unless --force is passed. Pinning a tab shrinks it to the size of its title text. Attempting to close a pinned tab will cause a confirmation, unless --force is passed.
==== count ==== count
The tab index to pin or unpin The tab index to pin or unpin
@ -948,6 +1021,10 @@ Unbind a keychain.
=== undo === undo
Re-open a closed tab. Re-open a closed tab.
[[version]]
=== version
Show version information.
[[view-source]] [[view-source]]
=== view-source === view-source
Show the source of the current page in a new tab. Show the source of the current page in a new tab.
@ -1068,8 +1145,8 @@ How many steps to zoom out.
|<<run-with-count,run-with-count>>|Run a command with the given count. |<<run-with-count,run-with-count>>|Run a command with the given count.
|<<scroll,scroll>>|Scroll the current tab in the given direction. |<<scroll,scroll>>|Scroll the current tab in the given direction.
|<<scroll-page,scroll-page>>|Scroll the frame page-wise. |<<scroll-page,scroll-page>>|Scroll the frame page-wise.
|<<scroll-perc,scroll-perc>>|Scroll to a specific percentage of the page.
|<<scroll-px,scroll-px>>|Scroll the current tab by 'count * dx/dy' pixels. |<<scroll-px,scroll-px>>|Scroll the current tab by 'count * dx/dy' pixels.
|<<scroll-to-perc,scroll-to-perc>>|Scroll to a specific percentage of the page.
|<<search-next,search-next>>|Continue the search to the ([count]th) next term. |<<search-next,search-next>>|Continue the search to the ([count]th) next term.
|<<search-prev,search-prev>>|Continue the search to the ([count]th) previous term. |<<search-prev,search-prev>>|Continue the search to the ([count]th) previous term.
|<<set-mark,set-mark>>|Set a mark at the current scroll position in the current tab. |<<set-mark,set-mark>>|Set a mark at the current scroll position in the current tab.
@ -1489,9 +1566,22 @@ Scroll the frame page-wise.
==== count ==== count
multiplier multiplier
[[scroll-perc]] [[scroll-px]]
=== scroll-perc === scroll-px
Syntax: +:scroll-perc [*--horizontal*] ['perc']+ 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 y-direction.
==== count
multiplier
[[scroll-to-perc]]
=== scroll-to-perc
Syntax: +:scroll-to-perc [*--horizontal*] ['perc']+
Scroll to a specific percentage of the page. Scroll to a specific percentage of the page.
@ -1506,19 +1596,6 @@ The percentage can be given either as argument or as count. If no percentage is
==== count ==== count
Percentage to scroll. Percentage to scroll.
[[scroll-px]]
=== scroll-px
Syntax: +:scroll-px 'dx' 'dy'+
Scroll the current tab by 'count * dx/dy' pixels.
==== positional arguments
* +'dx'+: How much to scroll in x-direction.
* +'dy'+: How much to scroll in y-direction.
==== count
multiplier
[[search-next]] [[search-next]]
=== search-next === search-next
Continue the search to the ([count]th) next term. Continue the search to the ([count]th) next term.

View File

@ -11,8 +11,9 @@ Migrating older configurations
------------------------------ ------------------------------
qutebrowser does no automatic migration for the new configuration. However, qutebrowser does no automatic migration for the new configuration. However,
there's a special link:qute://configdiff[] page in qutebrowser, which will show there's a special link:qute://configdiff/old[configdiff] page in qutebrowser,
you the changes you did in your old configuration, compared to the old defaults. which will show you the changes you did in your old configuration, compared to
the old defaults.
Other changes in default settings: Other changes in default settings:
@ -57,11 +58,9 @@ To get more help about a setting, use e.g. `:help tabs.position`.
To bind and unbind keys, you can use the link:commands.html#bind[`:bind`] and To bind and unbind keys, you can use the link:commands.html#bind[`:bind`] and
link:commands.html#unbind[`:unbind`] commands: link:commands.html#unbind[`:unbind`] commands:
- Binding the key chain "`,`, `v`" to the `:spawn mpv {url}` command: - Binding the key chain `,v` to the `:spawn mpv {url}` command:
`:bind ,v spawn mpv {url}` `:bind ,v spawn mpv {url}`
- Unbinding the same key chain: `:unbind ,v` - Unbinding the same key chain: `:unbind ,v`
- Changing an existing binding: `bind --force ,v message-info foo`. Without
`--force`, qutebrowser will show an error because `,v` is already bound.
Key chains starting with a comma are ideal for custom bindings, as the comma key Key chains starting with a comma are ideal for custom bindings, as the comma key
will never be used in a default keybinding. will never be used in a default keybinding.
@ -88,7 +87,9 @@ Two global objects are pre-defined when running `config.py`: `c` and `config`.
Changing settings Changing settings
~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~
`c` is a shorthand object to easily set settings like this: While you can set settings using the `config.set()` method (which is explained
in the next section), it's easier to use the `c` shorthand object to easily set
settings like this:
.config.py: .config.py:
[source,python] [source,python]
@ -110,7 +111,7 @@ accepted values depend on the type of the option. Commonly used are:
- Dictionaries: - Dictionaries:
* `c.headers.custom = {'X-Hello': 'World', 'X-Awesome': 'yes'}` to override * `c.headers.custom = {'X-Hello': 'World', 'X-Awesome': 'yes'}` to override
any other values in the dictionary. any other values in the dictionary.
* `c.aliases['foo'] = ':message-info foo'` to add a single value. * `c.aliases['foo'] = 'message-info foo'` to add a single value.
- Lists: - Lists:
* `c.url.start_pages = ["https://www.qutebrowser.org/"]` to override any * `c.url.start_pages = ["https://www.qutebrowser.org/"]` to override any
previous elements. previous elements.
@ -136,6 +137,8 @@ If you want to set settings based on their name as a string, use the
.config.py: .config.py:
[source,python] [source,python]
---- ----
# Equivalent to:
# c.content.javascript.enabled = False
config.set('content.javascript.enabled', False) config.set('content.javascript.enabled', False)
---- ----
@ -143,6 +146,8 @@ To read a setting, use the `config.get` method:
[source,python] [source,python]
---- ----
# Equivalent to:
# color = c.colors.completion.fg
color = config.get('colors.completion.fg') color = config.get('colors.completion.fg')
---- ----
@ -172,13 +177,6 @@ To bind a key in a mode other than `'normal'`, add a `mode` argument:
config.bind('<Ctrl-y>', 'prompt-yes', mode='prompt') config.bind('<Ctrl-y>', 'prompt-yes', mode='prompt')
---- ----
If the key is already bound, `force=True` needs to be given to rebind it:
[source,python]
----
config.bind('<Ctrl-v>', 'message-info foo', force=True)
----
To unbind a key (either a key which has been bound before, or a default binding): To unbind a key (either a key which has been bound before, or a default binding):
[source,python] [source,python]
@ -198,17 +196,52 @@ config.bind(',v', 'spawn mpv {url}')
To suppress loading of any default keybindings, you can set To suppress loading of any default keybindings, you can set
`c.bindings.default = {}`. `c.bindings.default = {}`.
Prevent loading `autoconfig.yml` Loading `autoconfig.yml`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~
If you want all customization done via `:set`, `:bind` and `:unbind` to be By default, all customization done via `:set`, `:bind` and `:unbind` is
temporary, you can suppress loading `autoconfig.yml` in your `config.py` by temporary as soon as a `config.py` exists. The settings done that way are always
doing: saved in the `autoconfig.yml` file, but you'll need to explicitly load it in
your `config.py` by doing:
.config.py: .config.py:
[source,python] [source,python]
---- ----
config.load_autoconfig = False config.load_autoconfig()
----
If you do so at the top of your file, your `config.py` settings will take
precedence as they overwrite the settings done in `autoconfig.yml`.
Importing other modules
~~~~~~~~~~~~~~~~~~~~~~~
You can import any module from the
https://docs.python.org/3/library/index.html[Python standard library] (e.g.
`import os.path`), as well as any module installed in the environment
qutebrowser is run with.
If you have an `utils.py` file in your qutebrowser config folder, you can import
that via `import utils` as well.
While it's in some cases possible to import code from the qutebrowser
installation, doing so is unsupported and discouraged.
Getting the config directory
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you need to get the qutebrowser config directory, you can do so by reading
`config.configdir`. Similarily, you can get the qutebrowser data directory via
`config.datadir`.
This gives you a https://docs.python.org/3/library/pathlib.html[`pathlib.Path`
object], on which you can use `/` to add more directory parts, or `str(...)` to
get a string:
.config.py:
[source,python]
----
print(str(config.configdir / 'config.py')
---- ----
Handling errors Handling errors
@ -221,3 +254,106 @@ qutebrowser tries to display errors which are easy to understand even for people
who are not used to writing Python. If you see a config error which you find who are not used to writing Python. If you see a config error which you find
confusing or you think qutebrowser could handle better, please confusing or you think qutebrowser could handle better, please
https://github.com/qutebrowser/qutebrowser/issues[open an issue]! https://github.com/qutebrowser/qutebrowser/issues[open an issue]!
Recipes
~~~~~~~
Reading a YAML file
^^^^^^^^^^^^^^^^^^^
To read a YAML config like this:
.config.yml:
----
tabs.position: left
tabs.show: switching
----
You can use:
.config.py:
[source,python]
----
import yaml
with (config.configdir / 'config.yml').open() as f:
yaml_data = yaml.load(f)
for k, v in yaml_data.items():
config.set(k, v)
----
Reading a nested YAML file
^^^^^^^^^^^^^^^^^^^^^^^^^^
To read a YAML file with nested values like this:
.colors.yml:
----
colors:
statusbar:
normal:
bg: lime
fg: black
url:
fg: red
----
You can use:
.config.py:
[source,python]
----
import yaml
with (config.configdir / 'colors.yml').open() as f:
yaml_data = yaml.load(f)
def dict_attrs(obj, path=''):
if isinstance(obj, dict):
for k, v in obj.items():
yield from dict_attrs(v, '{}.{}'.format(path, k) if path else k)
else:
yield path, obj
for k, v in dict_attrs(yaml_data):
config.set(k, v)
----
Note that this won't work for values which are dictionaries.
Binding chained commands
^^^^^^^^^^^^^^^^^^^^^^^^
If you have a lot of chained commands you want to bind, you can write a helper
to do so:
[source,python]
----
def bind_chained(key, *commands):
config.bind(key, ' ;; '.join(commands))
bind_chained('<Escape>', 'clear-keychain', 'search')
----
Avoiding flake8 errors
^^^^^^^^^^^^^^^^^^^^^^
If you use an editor with flake8 integration which complains about `c` and `config` being undefined, you can use:
[source,python]
----
c = c # noqa: F821
config = config # noqa: F821
----
For type annotation support (note that those imports aren't guaranteed to be
stable across qutebrowser versions):
[source,python]
----
from qutebrowser.config.configfiles import ConfigAPI # noqa: F401
from qutebrowser.config.config import ConfigContainer # noqa: F401
config = config # type: ConfigAPI # noqa: F821
c = c # type: ConfigContainer # noqa: F821
----

View File

@ -7,7 +7,7 @@ Documentation
The following help pages are currently available: The following help pages are currently available:
* link:../quickstart.html[Quick start guide] * link:../quickstart.html[Quick start guide]
* link:../doc.html[Frequently asked questions] * link:../faq.html[Frequently asked questions]
* link:../changelog.html[Change Log] * link:../changelog.html[Change Log]
* link:commands.html[Documentation of commands] * link:commands.html[Documentation of commands]
* link:configuring.html[Configuring qutebrowser] * link:configuring.html[Configuring qutebrowser]

View File

@ -55,6 +55,7 @@
|<<colors.messages.warning.border,colors.messages.warning.border>>|Border color of an error message. |<<colors.messages.warning.border,colors.messages.warning.border>>|Border color of an error message.
|<<colors.messages.warning.fg,colors.messages.warning.fg>>|Foreground color a warning message. |<<colors.messages.warning.fg,colors.messages.warning.fg>>|Foreground color a warning message.
|<<colors.prompts.bg,colors.prompts.bg>>|Background color for prompts. |<<colors.prompts.bg,colors.prompts.bg>>|Background color for prompts.
|<<colors.prompts.border,colors.prompts.border>>|Border used around UI elements in prompts.
|<<colors.prompts.fg,colors.prompts.fg>>|Foreground color for prompts. |<<colors.prompts.fg,colors.prompts.fg>>|Foreground color for prompts.
|<<colors.prompts.selected.bg,colors.prompts.selected.bg>>|Background color for the selected item in filename prompts. |<<colors.prompts.selected.bg,colors.prompts.selected.bg>>|Background color for the selected item in filename prompts.
|<<colors.statusbar.caret.bg,colors.statusbar.caret.bg>>|Background color of the statusbar in caret mode. |<<colors.statusbar.caret.bg,colors.statusbar.caret.bg>>|Background color of the statusbar in caret mode.
@ -178,6 +179,7 @@
|<<fonts.web.size.default_fixed,fonts.web.size.default_fixed>>|The default font size for fixed-pitch text. |<<fonts.web.size.default_fixed,fonts.web.size.default_fixed>>|The default font size for fixed-pitch text.
|<<fonts.web.size.minimum,fonts.web.size.minimum>>|The hard minimum font size. |<<fonts.web.size.minimum,fonts.web.size.minimum>>|The hard minimum font size.
|<<fonts.web.size.minimum_logical,fonts.web.size.minimum_logical>>|The minimum logical font size that is applied when zooming out. |<<fonts.web.size.minimum_logical,fonts.web.size.minimum_logical>>|The minimum logical font size that is applied when zooming out.
|<<force_software_rendering,force_software_rendering>>|Force software rendering for QtWebEngine.
|<<hints.auto_follow,hints.auto_follow>>|Controls when a hint can be automatically followed without pressing Enter. |<<hints.auto_follow,hints.auto_follow>>|Controls when a hint can be automatically followed without pressing Enter.
|<<hints.auto_follow_timeout,hints.auto_follow_timeout>>|A timeout (in milliseconds) to ignore normal-mode key bindings after a successful auto-follow. |<<hints.auto_follow_timeout,hints.auto_follow_timeout>>|A timeout (in milliseconds) to ignore normal-mode key bindings after a successful auto-follow.
|<<hints.border,hints.border>>|CSS border value for hints. |<<hints.border,hints.border>>|CSS border value for hints.
@ -201,7 +203,7 @@
|<<input.partial_timeout,input.partial_timeout>>|Timeout (in milliseconds) for partially typed key bindings. |<<input.partial_timeout,input.partial_timeout>>|Timeout (in milliseconds) for partially typed key bindings.
|<<input.rocker_gestures,input.rocker_gestures>>|Enable Opera-like mouse rocker gestures. |<<input.rocker_gestures,input.rocker_gestures>>|Enable Opera-like mouse rocker gestures.
|<<input.spatial_navigation,input.spatial_navigation>>|Enable Spatial Navigation. |<<input.spatial_navigation,input.spatial_navigation>>|Enable Spatial Navigation.
|<<keyhint.blacklist,keyhint.blacklist>>|Keychains that shouldn\'t be shown in the keyhint dialog. |<<keyhint.blacklist,keyhint.blacklist>>|Keychains that shouldn't be shown in the keyhint dialog.
|<<keyhint.delay,keyhint.delay>>|Time from pressing a key to seeing the keyhint dialog (ms). |<<keyhint.delay,keyhint.delay>>|Time from pressing a key to seeing the keyhint dialog (ms).
|<<messages.timeout,messages.timeout>>|Time (in ms) to show messages in the statusbar for. |<<messages.timeout,messages.timeout>>|Time (in ms) to show messages in the statusbar for.
|<<messages.unfocused,messages.unfocused>>|Show messages in unfocused windows. |<<messages.unfocused,messages.unfocused>>|Show messages in unfocused windows.
@ -236,7 +238,6 @@
|<<tabs.title.format_pinned,tabs.title.format_pinned>>|The format to use for the tab title for pinned tabs. The same placeholders like for `tabs.title.format` are defined. |<<tabs.title.format_pinned,tabs.title.format_pinned>>|The format to use for the tab title for pinned tabs. The same placeholders like for `tabs.title.format` are defined.
|<<tabs.width.bar,tabs.width.bar>>|The width of the tab bar if it's vertical, in px or as percentage of the window. |<<tabs.width.bar,tabs.width.bar>>|The width of the tab bar if it's vertical, in px or as percentage of the window.
|<<tabs.width.indicator,tabs.width.indicator>>|Width of the progress indicator (0 to disable). |<<tabs.width.indicator,tabs.width.indicator>>|Width of the progress indicator (0 to disable).
|<<tabs.width.pinned,tabs.width.pinned>>|The width for pinned tabs with a horizontal tabbar, in px.
|<<tabs.wrap,tabs.wrap>>|Whether to wrap when changing tabs. |<<tabs.wrap,tabs.wrap>>|Whether to wrap when changing tabs.
|<<url.auto_search,url.auto_search>>|Whether to start a search when something else than a URL is entered. |<<url.auto_search,url.auto_search>>|Whether to start a search when something else than a URL is entered.
|<<url.default_page,url.default_page>>|The page to open if :open -t/-b/-w is used without URL. |<<url.default_page,url.default_page>>|The page to open if :open -t/-b/-w is used without URL.
@ -284,24 +285,24 @@ Valid values:
* +true+ * +true+
* +false+ * +false+
Default: empty Default: +pass:[false]+
[[backend]] [[backend]]
=== backend === backend
The backend to use to display websites. The backend to use to display websites.
qutebrowser supports two different web rendering engines / backends, QtWebKit and QtWebEngine. qutebrowser supports two different web rendering engines / backends, QtWebKit and QtWebEngine.
QtWebKit is based on WebKit (similar to Safari). It was discontinued by the Qt project with Qt 5.6, but picked up as a well maintained fork: https://github.com/annulen/webkit/wiki - qutebrowser only supports the fork. QtWebKit was discontinued by the Qt project with Qt 5.6, but picked up as a well maintained fork: https://github.com/annulen/webkit/wiki - qutebrowser only supports the fork.
QtWebEngine is Qt's official successor to QtWebKit and based on the Chromium project. It's slightly more resource hungry that QtWebKit and has a couple of missing features in qutebrowser, but is generally the preferred choice. QtWebEngine is Qt's official successor to QtWebKit. It's slightly more resource hungry that QtWebKit and has a couple of missing features in qutebrowser, but is generally the preferred choice.
This setting requires a restart.
Type: <<types,String>> Type: <<types,String>>
Valid values: Valid values:
* +auto+: Automatically select either QtWebEngine or QtWebKit * +webengine+: Use QtWebEngine (based on Chromium)
* +webkit+: Force QtWebKit * +webkit+: Use QtWebKit (based on WebKit, similar to Safari)
* +webengine+: Force QtWebEngine
Default: +pass:[auto]+ Default: +pass:[webengine]+
[[bindings.commands]] [[bindings.commands]]
=== bindings.commands === bindings.commands
@ -506,7 +507,7 @@ Default:
* +pass:[B]+: +pass:[set-cmd-text -s :quickmark-load -t]+ * +pass:[B]+: +pass:[set-cmd-text -s :quickmark-load -t]+
* +pass:[D]+: +pass:[tab-close -o]+ * +pass:[D]+: +pass:[tab-close -o]+
* +pass:[F]+: +pass:[hint all tab]+ * +pass:[F]+: +pass:[hint all tab]+
* +pass:[G]+: +pass:[scroll-perc]+ * +pass:[G]+: +pass:[scroll-to-perc]+
* +pass:[H]+: +pass:[back]+ * +pass:[H]+: +pass:[back]+
* +pass:[J]+: +pass:[tab-next]+ * +pass:[J]+: +pass:[tab-next]+
* +pass:[K]+: +pass:[tab-prev]+ * +pass:[K]+: +pass:[tab-prev]+
@ -541,7 +542,7 @@ Default:
* +pass:[gb]+: +pass:[set-cmd-text -s :bookmark-load]+ * +pass:[gb]+: +pass:[set-cmd-text -s :bookmark-load]+
* +pass:[gd]+: +pass:[download]+ * +pass:[gd]+: +pass:[download]+
* +pass:[gf]+: +pass:[view-source]+ * +pass:[gf]+: +pass:[view-source]+
* +pass:[gg]+: +pass:[scroll-perc 0]+ * +pass:[gg]+: +pass:[scroll-to-perc 0]+
* +pass:[gl]+: +pass:[tab-move -]+ * +pass:[gl]+: +pass:[tab-move -]+
* +pass:[gm]+: +pass:[tab-move]+ * +pass:[gm]+: +pass:[tab-move]+
* +pass:[go]+: +pass:[set-cmd-text :open {url:pretty}]+ * +pass:[go]+: +pass:[set-cmd-text :open {url:pretty}]+
@ -627,6 +628,7 @@ Default:
This setting can be used to map keys to other keys. This setting can be used to map keys to other keys.
When the key used as dictionary-key is pressed, the binding for the key used as dictionary-value is invoked instead. When the key used as dictionary-key is pressed, the binding for the key used as dictionary-value is invoked instead.
This is useful for global remappings of keys, for example to map Ctrl-[ to Escape. This is useful for global remappings of keys, for example to map Ctrl-[ to Escape.
Note that when a key is bound (via `bindings.default` or `bindings.commands`), the mapping is ignored.
Type: <<types,Dict>> Type: <<types,Dict>>
@ -966,7 +968,15 @@ Background color for prompts.
Type: <<types,QssColor>> Type: <<types,QssColor>>
Default: +pass:[darkblue]+ Default: +pass:[#444444]+
[[colors.prompts.border]]
=== colors.prompts.border
Border used around UI elements in prompts.
Type: <<types,String>>
Default: +pass:[1px solid gray]+
[[colors.prompts.fg]] [[colors.prompts.fg]]
=== colors.prompts.fg === colors.prompts.fg
@ -982,7 +992,7 @@ Background color for the selected item in filename prompts.
Type: <<types,QssColor>> Type: <<types,QssColor>>
Default: +pass:[#308cc6]+ Default: +pass:[grey]+
[[colors.statusbar.caret.bg]] [[colors.statusbar.caret.bg]]
=== colors.statusbar.caret.bg === colors.statusbar.caret.bg
@ -1342,7 +1352,7 @@ Valid values:
* +true+ * +true+
* +false+ * +false+
Default: empty Default: +pass:[false]+
[[completion.timestamp_format]] [[completion.timestamp_format]]
=== completion.timestamp_format === completion.timestamp_format
@ -1402,7 +1412,7 @@ For more information about the feature, please refer to: http://webkit.org/blog/
Type: <<types,Int>> Type: <<types,Int>>
Default: empty Default: +pass:[0]+
This setting is only available with the QtWebKit backend. This setting is only available with the QtWebKit backend.
@ -1466,7 +1476,7 @@ Valid values:
* +true+ * +true+
* +false+ * +false+
Default: empty Default: +pass:[false]+
This setting is only available with the QtWebKit backend. This setting is only available with the QtWebKit backend.
@ -1497,7 +1507,7 @@ Valid values:
* +true+ * +true+
* +false+ * +false+
Default: empty Default: +pass:[false]+
This setting is only available with the QtWebKit backend. This setting is only available with the QtWebKit backend.
@ -1595,7 +1605,7 @@ The file can be in one of the following formats:
`hosts` (with any extension). `hosts` (with any extension).
Type: <<types,List>> Type: <<types,List of Url>>
Default: Default:
@ -1611,7 +1621,7 @@ List of domains that should always be loaded, despite being ad-blocked.
Domains may contain * and ? wildcards and are otherwise required to exactly match the requested domain. Domains may contain * and ? wildcards and are otherwise required to exactly match the requested domain.
Local domains are always exempt from hostblocking. Local domains are always exempt from hostblocking.
Type: <<types,List>> Type: <<types,List of String>>
Default: Default:
@ -1628,7 +1638,7 @@ Valid values:
* +true+ * +true+
* +false+ * +false+
Default: empty Default: +pass:[false]+
[[content.images]] [[content.images]]
=== content.images === content.images
@ -1668,7 +1678,7 @@ Valid values:
* +true+ * +true+
* +false+ * +false+
Default: empty Default: +pass:[false]+
[[content.javascript.can_close_tabs]] [[content.javascript.can_close_tabs]]
=== content.javascript.can_close_tabs === content.javascript.can_close_tabs
@ -1681,7 +1691,7 @@ Valid values:
* +true+ * +true+
* +false+ * +false+
Default: empty Default: +pass:[false]+
This setting is only available with the QtWebKit backend. This setting is only available with the QtWebKit backend.
@ -1696,7 +1706,7 @@ Valid values:
* +true+ * +true+
* +false+ * +false+
Default: empty Default: +pass:[false]+
[[content.javascript.enabled]] [[content.javascript.enabled]]
=== content.javascript.enabled === content.javascript.enabled
@ -1737,7 +1747,7 @@ Valid values:
* +true+ * +true+
* +false+ * +false+
Default: empty Default: +pass:[false]+
[[content.javascript.prompt]] [[content.javascript.prompt]]
=== content.javascript.prompt === content.javascript.prompt
@ -1776,7 +1786,7 @@ Valid values:
* +true+ * +true+
* +false+ * +false+
Default: empty Default: +pass:[false]+
[[content.local_storage]] [[content.local_storage]]
=== content.local_storage === content.local_storage
@ -1842,7 +1852,7 @@ Valid values:
* +true+ * +true+
* +false+ * +false+
Default: empty Default: +pass:[false]+
This setting is only available with the QtWebKit backend. This setting is only available with the QtWebKit backend.
@ -1857,7 +1867,7 @@ Valid values:
* +true+ * +true+
* +false+ * +false+
Default: empty Default: +pass:[false]+
[[content.print_element_backgrounds]] [[content.print_element_backgrounds]]
=== content.print_element_backgrounds === content.print_element_backgrounds
@ -1885,7 +1895,7 @@ Valid values:
* +true+ * +true+
* +false+ * +false+
Default: empty Default: +pass:[false]+
[[content.proxy]] [[content.proxy]]
=== content.proxy === content.proxy
@ -1934,7 +1944,7 @@ Default: +pass:[ask]+
=== content.user_stylesheets === content.user_stylesheets
A list of user stylesheet filenames to use. A list of user stylesheet filenames to use.
Type: <<types,List>> Type: <<types,List of File&#44; or File>>
Default: empty Default: empty
@ -1954,7 +1964,7 @@ Default: +pass:[true]+
[[content.xss_auditing]] [[content.xss_auditing]]
=== content.xss_auditing === content.xss_auditing
Whether load requests should be monitored for cross-site scripting attempts. Whether load requests should be monitored for cross-site scripting attempts.
Suspicious scripts will be blocked and reported in the inspector\'s JavaScript console. Enabling this feature might have an impact on performance. Suspicious scripts will be blocked and reported in the inspector's JavaScript console. Enabling this feature might have an impact on performance.
Type: <<types,Bool>> Type: <<types,Bool>>
@ -1963,7 +1973,7 @@ Valid values:
* +true+ * +true+
* +false+ * +false+
Default: empty Default: +pass:[false]+
[[downloads.location.directory]] [[downloads.location.directory]]
=== downloads.location.directory === downloads.location.directory
@ -2143,7 +2153,7 @@ Default: +pass:[8pt monospace]+
[[fonts.monospace]] [[fonts.monospace]]
=== fonts.monospace === fonts.monospace
Default monospace fonts. Default monospace fonts.
Whenever "monospace" is used in a font setting, it\'s replaced with the fonts listed here. Whenever "monospace" is used in a font setting, it's replaced with the fonts listed here.
Type: <<types,Font>> Type: <<types,Font>>
@ -2243,7 +2253,7 @@ The hard minimum font size.
Type: <<types,Int>> Type: <<types,Int>>
Default: empty Default: +pass:[0]+
[[fonts.web.size.minimum_logical]] [[fonts.web.size.minimum_logical]]
=== fonts.web.size.minimum_logical === fonts.web.size.minimum_logical
@ -2253,6 +2263,22 @@ Type: <<types,Int>>
Default: +pass:[6]+ Default: +pass:[6]+
[[force_software_rendering]]
=== force_software_rendering
Force software rendering for QtWebEngine.
This is needed for QtWebEngine to work with Nouveau drivers. This setting requires a restart.
Type: <<types,Bool>>
Valid values:
* +true+
* +false+
Default: +pass:[false]+
This setting is only available with the QtWebEngine backend.
[[hints.auto_follow]] [[hints.auto_follow]]
=== hints.auto_follow === hints.auto_follow
Controls when a hint can be automatically followed without pressing Enter. Controls when a hint can be automatically followed without pressing Enter.
@ -2274,7 +2300,7 @@ A timeout (in milliseconds) to ignore normal-mode key bindings after a successfu
Type: <<types,Int>> Type: <<types,Int>>
Default: empty Default: +pass:[0]+
[[hints.border]] [[hints.border]]
=== hints.border === hints.border
@ -2354,7 +2380,7 @@ Default: +pass:[letter]+
=== hints.next_regexes === hints.next_regexes
A comma-separated list of regexes to use for 'next' links. A comma-separated list of regexes to use for 'next' links.
Type: <<types,List>> Type: <<types,List of Regex>>
Default: Default:
@ -2369,7 +2395,7 @@ Default:
=== hints.prev_regexes === hints.prev_regexes
A comma-separated list of regexes to use for 'prev' links. A comma-separated list of regexes to use for 'prev' links.
Type: <<types,List>> Type: <<types,List of Regex>>
Default: Default:
@ -2404,7 +2430,7 @@ Valid values:
* +true+ * +true+
* +false+ * +false+
Default: empty Default: +pass:[false]+
[[history_gap_interval]] [[history_gap_interval]]
=== history_gap_interval === history_gap_interval
@ -2467,7 +2493,7 @@ Valid values:
* +true+ * +true+
* +false+ * +false+
Default: empty Default: +pass:[false]+
[[input.insert_mode.plugins]] [[input.insert_mode.plugins]]
=== input.insert_mode.plugins === input.insert_mode.plugins
@ -2480,7 +2506,7 @@ Valid values:
* +true+ * +true+
* +false+ * +false+
Default: empty Default: +pass:[false]+
[[input.links_included_in_focus_chain]] [[input.links_included_in_focus_chain]]
=== input.links_included_in_focus_chain === input.links_included_in_focus_chain
@ -2516,7 +2542,7 @@ Valid values:
* +true+ * +true+
* +false+ * +false+
Default: empty Default: +pass:[false]+
[[input.spatial_navigation]] [[input.spatial_navigation]]
=== input.spatial_navigation === input.spatial_navigation
@ -2530,14 +2556,14 @@ Valid values:
* +true+ * +true+
* +false+ * +false+
Default: empty Default: +pass:[false]+
[[keyhint.blacklist]] [[keyhint.blacklist]]
=== keyhint.blacklist === keyhint.blacklist
Keychains that shouldn\'t be shown in the keyhint dialog. Keychains that shouldn't be shown in the keyhint dialog.
Globs are supported, so `;*` will blacklist all keychains starting with `;`. Use `*` to disable keyhints. Globs are supported, so `;*` will blacklist all keychains starting with `;`. Use `*` to disable keyhints.
Type: <<types,List>> Type: <<types,List of String>>
Default: empty Default: empty
@ -2569,7 +2595,7 @@ Valid values:
* +true+ * +true+
* +false+ * +false+
Default: empty Default: +pass:[false]+
[[new_instance_open_target]] [[new_instance_open_target]]
=== new_instance_open_target === new_instance_open_target
@ -2630,8 +2656,9 @@ Default: +pass:[8]+
=== qt_args === qt_args
Additional arguments to pass to Qt, without leading `--`. Additional arguments to pass to Qt, without leading `--`.
With QtWebEngine, some Chromium arguments (see https://peter.sh/experiments/chromium-command-line-switches/ for a list) will work. With QtWebEngine, some Chromium arguments (see https://peter.sh/experiments/chromium-command-line-switches/ for a list) will work.
This setting requires a restart.
Type: <<types,List>> Type: <<types,List of String>>
Default: empty Default: empty
@ -2646,7 +2673,7 @@ Valid values:
* +true+ * +true+
* +false+ * +false+
Default: empty Default: +pass:[false]+
[[scrolling.smooth]] [[scrolling.smooth]]
=== scrolling.smooth === scrolling.smooth
@ -2660,7 +2687,7 @@ Valid values:
* +true+ * +true+
* +false+ * +false+
Default: empty Default: +pass:[false]+
[[session_default_name]] [[session_default_name]]
=== session_default_name === session_default_name
@ -2682,7 +2709,7 @@ Valid values:
* +true+ * +true+
* +false+ * +false+
Default: empty Default: +pass:[false]+
[[statusbar.padding]] [[statusbar.padding]]
=== statusbar.padding === statusbar.padding
@ -2693,8 +2720,8 @@ Type: <<types,Padding>>
Default: Default:
- +pass:[bottom]+: +pass:[1]+ - +pass:[bottom]+: +pass:[1]+
- +pass:[left]+: empty - +pass:[left]+: +pass:[0]+
- +pass:[right]+: empty - +pass:[right]+: +pass:[0]+
- +pass:[top]+: +pass:[1]+ - +pass:[top]+: +pass:[1]+
[[statusbar.position]] [[statusbar.position]]
@ -2721,7 +2748,7 @@ Valid values:
* +true+ * +true+
* +false+ * +false+
Default: empty Default: +pass:[false]+
[[tabs.close_mouse_button]] [[tabs.close_mouse_button]]
=== tabs.close_mouse_button === tabs.close_mouse_button
@ -2768,7 +2795,7 @@ Type: <<types,Padding>>
Default: Default:
- +pass:[bottom]+: +pass:[2]+ - +pass:[bottom]+: +pass:[2]+
- +pass:[left]+: empty - +pass:[left]+: +pass:[0]+
- +pass:[right]+: +pass:[4]+ - +pass:[right]+: +pass:[4]+
- +pass:[top]+: +pass:[2]+ - +pass:[top]+: +pass:[2]+
@ -2839,10 +2866,10 @@ Type: <<types,Padding>>
Default: Default:
- +pass:[bottom]+: empty - +pass:[bottom]+: +pass:[0]+
- +pass:[left]+: +pass:[5]+ - +pass:[left]+: +pass:[5]+
- +pass:[right]+: +pass:[5]+ - +pass:[right]+: +pass:[5]+
- +pass:[top]+: empty - +pass:[top]+: +pass:[0]+
[[tabs.position]] [[tabs.position]]
=== tabs.position === tabs.position
@ -2907,7 +2934,7 @@ Valid values:
* +true+ * +true+
* +false+ * +false+
Default: empty Default: +pass:[false]+
[[tabs.title.alignment]] [[tabs.title.alignment]]
=== tabs.title.alignment === tabs.title.alignment
@ -2968,14 +2995,6 @@ Type: <<types,Int>>
Default: +pass:[3]+ Default: +pass:[3]+
[[tabs.width.pinned]]
=== tabs.width.pinned
The width for pinned tabs with a horizontal tabbar, in px.
Type: <<types,Int>>
Default: +pass:[43]+
[[tabs.wrap]] [[tabs.wrap]]
=== tabs.wrap === tabs.wrap
Whether to wrap when changing tabs. Whether to wrap when changing tabs.
@ -3046,17 +3065,15 @@ Default:
=== url.start_pages === url.start_pages
The page(s) to open at the start. The page(s) to open at the start.
Type: <<types,List>> Type: <<types,List of FuzzyUrl&#44; or FuzzyUrl>>
Default: Default: +pass:[https://start.duckduckgo.com]+
- +pass:[https://start.duckduckgo.com]+
[[url.yank_ignored_parameters]] [[url.yank_ignored_parameters]]
=== url.yank_ignored_parameters === url.yank_ignored_parameters
The URL parameters to strip with `:yank url`. The URL parameters to strip with `:yank url`.
Type: <<types,List>> Type: <<types,List of String>>
Default: Default:
@ -3078,7 +3095,7 @@ Valid values:
* +true+ * +true+
* +false+ * +false+
Default: empty Default: +pass:[false]+
[[window.title_format]] [[window.title_format]]
=== window.title_format === window.title_format
@ -3112,7 +3129,7 @@ Default: +pass:[100%]+
=== zoom.levels === zoom.levels
The available zoom levels. The available zoom levels.
Type: <<types,List>> Type: <<types,List of Perc>>
Default: Default:
@ -3152,7 +3169,7 @@ Valid values:
* +true+ * +true+
* +false+ * +false+
Default: empty Default: +pass:[false]+
This setting is only available with the QtWebKit backend. This setting is only available with the QtWebKit backend.
@ -3166,7 +3183,7 @@ This setting is only available with the QtWebKit backend.
When setting from a string, `1`, `yes`, `on` and `true` count as true, while `0`, `no`, `off` and `false` count as false (case-insensitive). When setting from a string, `1`, `yes`, `on` and `true` count as true, while `0`, `no`, `off` and `false` count as false (case-insensitive).
|BoolAsk|Like `Bool`, but `ask` is allowed as additional value. |BoolAsk|Like `Bool`, but `ask` is allowed as additional value.
|ColorSystem|The color system to use for color interpolation. |ColorSystem|The color system to use for color interpolation.
|Command|Base class for a command value with arguments. |Command|A qutebrowser command with arguments.
|ConfirmQuit|Whether to display a confirmation when the window is closed. |ConfirmQuit|Whether to display a confirmation when the window is closed.
|Dict|A dictionary of values. |Dict|A dictionary of values.
@ -3189,6 +3206,7 @@ Lists with duplicate flags are invalid. Each item is checked against the valid v
|List|A list of values. |List|A list of values.
When setting from a string, pass a json-like list, e.g. `["one", "two"]`. When setting from a string, pass a json-like list, e.g. `["one", "two"]`.
|ListOrValue|A list of values, or a single value.
|NewTabPosition|How new tabs are positioned. |NewTabPosition|How new tabs are positioned.
|Padding|Setting for paddings around elements. |Padding|Setting for paddings around elements.
|Perc|A percentage. |Perc|A percentage.

View File

@ -264,9 +264,6 @@ Manual install
* Use the installer from http://www.python.org/downloads[python.org] to get * Use the installer from http://www.python.org/downloads[python.org] to get
Python 3 (be sure to install pip). Python 3 (be sure to install pip).
* Use the installer from
http://www.riverbankcomputing.com/software/pyqt/download5[Riverbank computing]
to get Qt and PyQt5.
* Install https://testrun.org/tox/latest/index.html[tox] via * Install https://testrun.org/tox/latest/index.html[tox] via
https://pip.pypa.io/en/latest/[pip]: https://pip.pypa.io/en/latest/[pip]:

View File

@ -31,7 +31,7 @@ image:http://qutebrowser.org/img/cheatsheet-small.png["qutebrowser key binding c
* Run `:adblock-update` to download adblock lists and activate adblocking. * Run `:adblock-update` to download adblock lists and activate adblocking.
* If you just cloned the repository, you'll need to run * If you just cloned the repository, you'll need to run
`scripts/asciidoc2html.py` to generate the documentation. `scripts/asciidoc2html.py` to generate the documentation.
* Go to the link:qute://settings[settings page] to set up qutebrowser the way you want it. (Currently not available with the QtWebEngine backend and on the macOS build - use the `:set` command instead) * Go to the link:qute://settings[settings page] to set up qutebrowser the way you want it.
* Subscribe to * Subscribe to
https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser[the mailinglist] or https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser[the mailinglist] or
https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser-announce[the announce-only mailinglist]. https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser-announce[the announce-only mailinglist].

View File

@ -84,9 +84,6 @@ show it.
*--force-color*:: *--force-color*::
Force colored logging Force colored logging
*--relaxed-config*::
Silently remove unknown config options.
*--nowindow*:: *--nowindow*::
Don't show the main window. Don't show the main window.

View File

@ -5,7 +5,6 @@ import os
sys.path.insert(0, os.getcwd()) sys.path.insert(0, os.getcwd())
from scripts import setupcommon from scripts import setupcommon
from qutebrowser import utils
block_cipher = None block_cipher = None
@ -31,9 +30,9 @@ def get_data_files():
setupcommon.write_git_file() setupcommon.write_git_file()
if utils.is_windows: if os.name == 'nt':
icon = 'icons/qutebrowser.ico' icon = 'icons/qutebrowser.ico'
elif utils.is_mac: elif sys.platform == 'darwin':
icon = 'icons/qutebrowser.icns' icon = 'icons/qutebrowser.icns'
else: else:
icon = None icon = None

View File

@ -3,6 +3,6 @@
appdirs==1.4.3 appdirs==1.4.3
packaging==16.8 packaging==16.8
pyparsing==2.2.0 pyparsing==2.2.0
setuptools==36.2.7 setuptools==36.5.0
six==1.10.0 six==1.11.0
wheel==0.29.0 wheel==0.30.0

View File

@ -11,13 +11,13 @@ fields==5.0.0
Flask==0.12.2 Flask==0.12.2
glob2==0.6 glob2==0.6
hunter==2.0.1 hunter==2.0.1
hypothesis==3.28.3 hypothesis==3.30.3
itsdangerous==0.24 itsdangerous==0.24
# Jinja2==2.9.6 # Jinja2==2.9.6
Mako==1.0.7 Mako==1.0.7
# MarkupSafe==1.0 # MarkupSafe==1.0
parse==1.8.2 parse==1.8.2
parse-type==0.3.4 parse-type==0.4.1
py==1.4.34 py==1.4.34
py-cpuinfo==3.3.0 py-cpuinfo==3.3.0
pytest==3.2.2 pytest==3.2.2

View File

@ -1,6 +1,6 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py # This file is automatically generated by scripts/dev/recompile_requirements.py
pluggy==0.4.0 pluggy==0.5.2
py==1.4.34 py==1.4.34
tox==2.8.2 tox==2.8.2
virtualenv==15.1.0 virtualenv==15.1.0

View File

@ -1,4 +1,4 @@
#!/usr/bin/env python2 #!/usr/bin/env python
# #
# Executes python-readability on current page and opens the summary as new tab. # Executes python-readability on current page and opens the summary as new tab.
# #

View File

@ -1,4 +1,4 @@
#!/usr/bin/env python2 #!/usr/bin/env python
# #
# Adds DuckDuckGo bang as searchengine. # Adds DuckDuckGo bang as searchengine.
# #
@ -8,14 +8,21 @@
# Example: # Example:
# :spawn --userscript ripbang amazon maps # :spawn --userscript ripbang amazon maps
# #
import os, re, requests, sys, urllib
from __future__ import print_function
import os, re, requests, sys
try:
from urllib.parse import unquote
except ImportError:
from urllib import unquote
for argument in sys.argv[1:]: for argument in sys.argv[1:]:
bang = '!' + argument bang = '!' + argument
r = requests.get('https://duckduckgo.com/', r = requests.get('https://duckduckgo.com/',
params={'q': bang + ' SEARCHTEXT'}) params={'q': bang + ' SEARCHTEXT'})
searchengine = urllib.unquote(re.search("url=[^']+", r.text).group(0)) searchengine = unquote(re.search("url=[^']+", r.text).group(0))
searchengine = searchengine.replace('url=', '') searchengine = searchengine.replace('url=', '')
searchengine = searchengine.replace('/l/?kh=-1&uddg=', '') searchengine = searchengine.replace('/l/?kh=-1&uddg=', '')
searchengine = searchengine.replace('SEARCHTEXT', '{}') searchengine = searchengine.replace('SEARCHTEXT', '{}')
@ -24,4 +31,4 @@ for argument in sys.argv[1:]:
with open(os.environ['QUTE_FIFO'], 'w') as fifo: with open(os.environ['QUTE_FIFO'], 'w') as fifo:
fifo.write('set searchengines %s %s' % (bang, searchengine)) fifo.write('set searchengines %s %s' % (bang, searchengine))
else: else:
print '%s %s' % (bang, searchengine) print('%s %s' % (bang, searchengine))

View File

@ -1,5 +1,5 @@
[pytest] [pytest]
addopts = --strict -rfEw --faulthandler-timeout=70 --instafail --pythonwarnings error --benchmark-columns=Min,Max,Median addopts = --strict -rfEw --faulthandler-timeout=90 --instafail --pythonwarnings error --benchmark-columns=Min,Max,Median
testpaths = tests testpaths = tests
markers = markers =
gui: Tests using the GUI (e.g. spawning widgets) gui: Tests using the GUI (e.g. spawning widgets)

View File

@ -17,7 +17,25 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. # along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Initialization of qutebrowser and application-wide things.""" """Initialization of qutebrowser and application-wide things.
The run() function will get called once early initialization (in
qutebrowser.py/earlyinit.py) is done. See the qutebrowser.py docstring for
details about early initialization.
As we need to access the config before the QApplication is created, we
initialize everything the config needs before the QApplication is created, and
then leave it in a partially initialized state (no saving, no config errors
shown yet).
We then set up the QApplication object and initialize a few more low-level
things.
After that, init() and _init_modules() take over and initialize the rest.
After all initialization is done, the qt_mainloop() function is called, which
blocks and spins the Qt mainloop.
"""
import os import os
import sys import sys
@ -41,9 +59,10 @@ except ImportError:
import qutebrowser import qutebrowser
import qutebrowser.resources import qutebrowser.resources
from qutebrowser.completion import completiondelegate
from qutebrowser.completion.models import miscmodels from qutebrowser.completion.models import miscmodels
from qutebrowser.commands import cmdutils, runners, cmdexc from qutebrowser.commands import cmdutils, runners, cmdexc
from qutebrowser.config import config, websettings, configexc, configfiles from qutebrowser.config import config, websettings, configfiles, configinit
from qutebrowser.browser import (urlmarks, adblock, history, browsertab, from qutebrowser.browser import (urlmarks, adblock, history, browsertab,
downloads) downloads)
from qutebrowser.browser.network import proxy from qutebrowser.browser.network import proxy
@ -52,7 +71,8 @@ from qutebrowser.browser.webkit.network import networkmanager
from qutebrowser.keyinput import macros from qutebrowser.keyinput import macros
from qutebrowser.mainwindow import mainwindow, prompt from qutebrowser.mainwindow import mainwindow, prompt
from qutebrowser.misc import (readline, ipc, savemanager, sessions, from qutebrowser.misc import (readline, ipc, savemanager, sessions,
crashsignal, earlyinit, sql, cmdhistory) crashsignal, earlyinit, sql, cmdhistory,
backendproblem)
from qutebrowser.utils import (log, version, message, utils, urlutils, objreg, from qutebrowser.utils import (log, version, message, utils, urlutils, objreg,
usertypes, standarddir, error) usertypes, standarddir, error)
# pylint: disable=unused-import # pylint: disable=unused-import
@ -77,7 +97,7 @@ def run(args):
standarddir.init(args) standarddir.init(args)
log.init.debug("Initializing config...") log.init.debug("Initializing config...")
config.early_init(args) configinit.early_init(args)
global qApp global qApp
qApp = Application(args) qApp = Application(args)
@ -186,12 +206,6 @@ def _init_icon():
def _process_args(args): def _process_args(args):
"""Open startpage etc. and process commandline args.""" """Open startpage etc. and process commandline args."""
for opt, val in args.temp_settings:
try:
config.instance.set_str(opt, val)
except configexc.Error as e:
message.error("set: {} - {}".format(e.__class__.__name__, e))
if not args.override_restore: if not args.override_restore:
_load_session(args.session) _load_session(args.session)
session_manager = objreg.get('session-manager') session_manager = objreg.get('session-manager')
@ -387,13 +401,16 @@ def _init_modules(args, crash_handler):
crash_handler: The CrashHandler instance. crash_handler: The CrashHandler instance.
""" """
# pylint: disable=too-many-statements # pylint: disable=too-many-statements
log.init.debug("Initializing prompts...")
prompt.init()
log.init.debug("Initializing save manager...") log.init.debug("Initializing save manager...")
save_manager = savemanager.SaveManager(qApp) save_manager = savemanager.SaveManager(qApp)
objreg.register('save-manager', save_manager) objreg.register('save-manager', save_manager)
config.late_init(save_manager) configinit.late_init(save_manager)
log.init.debug("Checking backend requirements...")
backendproblem.init()
log.init.debug("Initializing prompts...")
prompt.init()
log.init.debug("Initializing network...") log.init.debug("Initializing network...")
networkmanager.init() networkmanager.init()
@ -408,11 +425,14 @@ def _init_modules(args, crash_handler):
log.init.debug("Initializing sql...") log.init.debug("Initializing sql...")
try: try:
sql.init(os.path.join(standarddir.data(), 'history.sqlite')) sql.init(os.path.join(standarddir.data(), 'history.sqlite'))
except sql.SqlException as e: except sql.SqlError as e:
error.handle_fatal_exc(e, args, 'Error initializing SQL', error.handle_fatal_exc(e, args, 'Error initializing SQL',
pre_text='Error initializing SQL') pre_text='Error initializing SQL')
sys.exit(usertypes.Exit.err_init) sys.exit(usertypes.Exit.err_init)
log.init.debug("Initializing completion...")
completiondelegate.init()
log.init.debug("Initializing command history...") log.init.debug("Initializing command history...")
cmdhistory.init() cmdhistory.init()
@ -504,12 +524,13 @@ class Quitter:
with tokenize.open(os.path.join(dirpath, fn)) as f: with tokenize.open(os.path.join(dirpath, fn)) as f:
compile(f.read(), fn, 'exec') compile(f.read(), fn, 'exec')
def _get_restart_args(self, pages=(), session=None): def _get_restart_args(self, pages=(), session=None, override_args=None):
"""Get the current working directory and args to relaunch qutebrowser. """Get the current working directory and args to relaunch qutebrowser.
Args: Args:
pages: The pages to re-open. pages: The pages to re-open.
session: The session to load, or None. session: The session to load, or None.
override_args: Argument overrides as a dict.
Return: Return:
An (args, cwd) tuple. An (args, cwd) tuple.
@ -560,6 +581,9 @@ class Quitter:
argdict['temp_basedir'] = False argdict['temp_basedir'] = False
argdict['temp_basedir_restarted'] = True argdict['temp_basedir_restarted'] = True
if override_args is not None:
argdict.update(override_args)
# Dump the data # Dump the data
data = json.dumps(argdict) data = json.dumps(argdict)
args += ['--json-args', data] args += ['--json-args', data]
@ -584,7 +608,7 @@ class Quitter:
if ok: if ok:
self.shutdown(restart=True) self.shutdown(restart=True)
def restart(self, pages=(), session=None): def restart(self, pages=(), session=None, override_args=None):
"""Inner logic to restart qutebrowser. """Inner logic to restart qutebrowser.
The "better" way to restart is to pass a session (_restart usually) as The "better" way to restart is to pass a session (_restart usually) as
@ -597,6 +621,7 @@ class Quitter:
Args: Args:
pages: A list of URLs to open. pages: A list of URLs to open.
session: The session to load, or None. session: The session to load, or None.
override_args: Argument overrides as a dict.
Return: Return:
True if the restart succeeded, False otherwise. True if the restart succeeded, False otherwise.
@ -606,13 +631,19 @@ class Quitter:
log.destroy.debug("sys.path: {}".format(sys.path)) log.destroy.debug("sys.path: {}".format(sys.path))
log.destroy.debug("sys.argv: {}".format(sys.argv)) log.destroy.debug("sys.argv: {}".format(sys.argv))
log.destroy.debug("frozen: {}".format(hasattr(sys, 'frozen'))) log.destroy.debug("frozen: {}".format(hasattr(sys, 'frozen')))
# Save the session if one is given. # Save the session if one is given.
if session is not None: if session is not None:
session_manager = objreg.get('session-manager') session_manager = objreg.get('session-manager')
session_manager.save(session, with_private=True) session_manager.save(session, with_private=True)
# Make sure we're not accepting a connection from the new process
# before we fully exited.
ipc.server.shutdown()
# Open a new process and immediately shutdown the existing one # Open a new process and immediately shutdown the existing one
try: try:
args, cwd = self._get_restart_args(pages, session) args, cwd = self._get_restart_args(pages, session, override_args)
if cwd is None: if cwd is None:
subprocess.Popen(args) subprocess.Popen(args)
else: else:
@ -700,7 +731,7 @@ class Quitter:
QApplication.closeAllWindows() QApplication.closeAllWindows()
# Shut down IPC # Shut down IPC
try: try:
objreg.get('ipc-server').shutdown() ipc.server.shutdown()
except KeyError: except KeyError:
pass pass
# Save everything # Save everything
@ -762,7 +793,7 @@ class Application(QApplication):
""" """
self._last_focus_object = None self._last_focus_object = None
qt_args = config.qt_args(args) qt_args = configinit.qt_args(args)
log.init.debug("Qt arguments: {}, based on {}".format(qt_args, args)) log.init.debug("Qt arguments: {}, based on {}".format(qt_args, args))
super().__init__(qt_args) super().__init__(qt_args)

View File

@ -716,7 +716,7 @@ class AbstractTab(QWidget):
self._set_load_status(usertypes.LoadStatus.loading) self._set_load_status(usertypes.LoadStatus.loading)
self.load_started.emit() self.load_started.emit()
def _handle_auto_insert_mode(self, ok): def handle_auto_insert_mode(self, ok):
"""Handle `input.insert_mode.auto_load` after loading finished.""" """Handle `input.insert_mode.auto_load` after loading finished."""
if not config.val.input.insert_mode.auto_load or not ok: if not config.val.input.insert_mode.auto_load or not ok:
return return
@ -753,7 +753,6 @@ class AbstractTab(QWidget):
self.load_finished.emit(ok) self.load_finished.emit(ok)
if not self.title(): if not self.title():
self.title_changed.emit(self.url().toDisplayString()) self.title_changed.emit(self.url().toDisplayString())
self._handle_auto_insert_mode(ok)
@pyqtSlot() @pyqtSlot()
def _on_history_trigger(self): def _on_history_trigger(self):

View File

@ -256,7 +256,7 @@ class CommandDispatcher:
def tab_pin(self, count=None): def tab_pin(self, count=None):
"""Pin/Unpin the current/[count]th tab. """Pin/Unpin the current/[count]th tab.
Pinning a tab shrinks it to `tabs.width.pinned` size. Pinning a tab shrinks it to the size of its title text.
Attempting to close a pinned tab will cause a confirmation, Attempting to close a pinned tab will cause a confirmation,
unless --force is passed. unless --force is passed.
@ -688,7 +688,7 @@ class CommandDispatcher:
scope='window') scope='window')
@cmdutils.argument('count', count=True) @cmdutils.argument('count', count=True)
@cmdutils.argument('horizontal', flag='x') @cmdutils.argument('horizontal', flag='x')
def scroll_perc(self, perc: float = None, horizontal=False, count=None): def scroll_to_perc(self, perc: float = None, horizontal=False, count=None):
"""Scroll to a specific percentage of the page. """Scroll to a specific percentage of the page.
The percentage can be given either as argument or as count. The percentage can be given either as argument or as count.
@ -1011,15 +1011,24 @@ class CommandDispatcher:
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('index', completion=miscmodels.buffer) @cmdutils.argument('index', completion=miscmodels.buffer)
def buffer(self, index): @cmdutils.argument('count', count=True)
def buffer(self, index=None, count=None):
"""Select tab by index or url/title best match. """Select tab by index or url/title best match.
Focuses window if necessary. Focuses window if necessary when index is given. If both index and
count are given, use count.
Args: Args:
index: The [win_id/]index of the tab to focus. Or a substring index: The [win_id/]index of the tab to focus. Or a substring
in which case the closest match will be focused. in which case the closest match will be focused.
count: The tab index to focus, starting with 1.
""" """
if count is not None:
index_parts = [count]
elif index is None:
raise cmdexc.CommandError("buffer: Either a count or the argument "
"index must be specified.")
else:
index_parts = index.split('/', 1) index_parts = index.split('/', 1)
try: try:

View File

@ -40,7 +40,10 @@ class CompletionHistory(sql.SqlTable):
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__("CompletionHistory", ['url', 'title', 'last_atime'], super().__init__("CompletionHistory", ['url', 'title', 'last_atime'],
constraints={'url': 'PRIMARY KEY'}, parent=parent) constraints={'url': 'PRIMARY KEY',
'title': 'NOT NULL',
'last_atime': 'NOT NULL'},
parent=parent)
self.create_index('CompletionHistoryAtimeIndex', 'last_atime') self.create_index('CompletionHistoryAtimeIndex', 'last_atime')
@ -50,6 +53,10 @@ class WebHistory(sql.SqlTable):
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__("History", ['url', 'title', 'atime', 'redirect'], super().__init__("History", ['url', 'title', 'atime', 'redirect'],
constraints={'url': 'NOT NULL',
'title': 'NOT NULL',
'atime': 'NOT NULL',
'redirect': 'NOT NULL'},
parent=parent) parent=parent)
self.completion = CompletionHistory(parent=self) self.completion = CompletionHistory(parent=self)
if sql.Query('pragma user_version').run().value() < _USER_VERSION: if sql.Query('pragma user_version').run().value() < _USER_VERSION:
@ -252,10 +259,7 @@ class WebHistory(sql.SqlTable):
except ValueError as ex: except ValueError as ex:
message.error('Failed to import history: {}'.format(ex)) message.error('Failed to import history: {}'.format(ex))
else: else:
bakpath = path + '.bak' self._write_backup(path)
message.info('History import complete. Moving {} to {}'
.format(path, bakpath))
os.rename(path, bakpath)
# delay to give message time to appear before locking down for import # delay to give message time to appear before locking down for import
message.info('Converting {} to sqlite...'.format(path)) message.info('Converting {} to sqlite...'.format(path))
@ -287,6 +291,16 @@ class WebHistory(sql.SqlTable):
self.insert_batch(data) self.insert_batch(data)
self.completion.insert_batch(completion_data, replace=True) self.completion.insert_batch(completion_data, replace=True)
def _write_backup(self, path):
bak = path + '.bak'
message.info('History import complete. Appending {} to {}'
.format(path, bak))
with open(path, 'r', encoding='utf-8') as infile:
with open(bak, 'a', encoding='utf-8') as outfile:
for line in infile:
outfile.write('\n' + line)
os.remove(path)
def _format_url(self, url): def _format_url(self, url):
return url.toString(QUrl.RemovePassword | QUrl.FullyEncoded) return url.toString(QUrl.RemovePassword | QUrl.FullyEncoded)

View File

@ -408,11 +408,15 @@ def qute_settings(url):
@add_handler('configdiff') @add_handler('configdiff')
def qute_configdiff(_url): def qute_configdiff(url):
"""Handler for qute://configdiff.""" """Handler for qute://configdiff."""
if url.path() == '/old':
try: try:
return 'text/html', configdiff.get_diff() return 'text/html', configdiff.get_diff()
except OSError as e: except OSError as e:
error = (b'Failed to read old config: ' + error = (b'Failed to read old config: ' +
str(e.strerror).encode('utf-8')) str(e.strerror).encode('utf-8'))
return 'text/plain', error return 'text/plain', error
else:
data = config.instance.dump_userconfig().encode('utf-8')
return 'text/plain', data

View File

@ -194,7 +194,7 @@ def _set_http_headers(profile):
def _update_settings(option): def _update_settings(option):
"""Update global settings when qwebsettings changed.""" """Update global settings when qwebsettings changed."""
websettings.update_mappings(MAPPINGS, option) websettings.update_mappings(MAPPINGS, option)
if option in ['scrollbar.hide', 'content.user_stylesheets']: if option in ['scrolling.bar', 'content.user_stylesheets']:
_init_stylesheet(default_profile) _init_stylesheet(default_profile)
_init_stylesheet(private_profile) _init_stylesheet(private_profile)
elif option in ['content.headers.user_agent', elif option in ['content.headers.user_agent',

View File

@ -19,9 +19,9 @@
"""Wrapper over a QWebEngineView.""" """Wrapper over a QWebEngineView."""
import os
import math import math
import functools import functools
import html as html_utils
import sip import sip
from PyQt5.QtCore import pyqtSlot, Qt, QEvent, QPoint, QUrl, QTimer from PyQt5.QtCore import pyqtSlot, Qt, QEvent, QPoint, QUrl, QTimer
@ -37,7 +37,7 @@ from qutebrowser.browser.webengine import (webview, webengineelem, tabhistory,
webenginesettings) webenginesettings)
from qutebrowser.misc import miscwidgets from qutebrowser.misc import miscwidgets
from qutebrowser.utils import (usertypes, qtutils, log, javascript, utils, from qutebrowser.utils import (usertypes, qtutils, log, javascript, utils,
objreg, jinja, debug, version) message, objreg, jinja, debug)
_qute_scheme_handler = None _qute_scheme_handler = None
@ -49,16 +49,8 @@ def init():
# won't work... # won't work...
# https://www.riverbankcomputing.com/pipermail/pyqt/2016-September/038075.html # https://www.riverbankcomputing.com/pipermail/pyqt/2016-September/038075.html
global _qute_scheme_handler global _qute_scheme_handler
app = QApplication.instance() app = QApplication.instance()
software_rendering = (os.environ.get('LIBGL_ALWAYS_SOFTWARE') == '1' or
'QT_XCB_FORCE_SOFTWARE_OPENGL' in os.environ)
if version.opengl_vendor() == 'nouveau' and not software_rendering:
# FIXME:qtwebengine display something more sophisticated here
raise browsertab.WebTabError(
"QtWebEngine is not supported with Nouveau graphics (unless "
"QT_XCB_FORCE_SOFTWARE_OPENGL is set as environment variable).")
log.init.debug("Initializing qute://* handler...") log.init.debug("Initializing qute://* handler...")
_qute_scheme_handler = webenginequtescheme.QuteSchemeHandler(parent=app) _qute_scheme_handler = webenginequtescheme.QuteSchemeHandler(parent=app)
_qute_scheme_handler.install(webenginesettings.default_profile) _qute_scheme_handler.install(webenginesettings.default_profile)
@ -678,6 +670,32 @@ class WebEngineTab(browsertab.AbstractTab):
self.add_history_item.emit(url, requested_url, title) self.add_history_item.emit(url, requested_url, title)
@pyqtSlot(QUrl, 'QAuthenticator*', 'QString')
def _on_proxy_authentication_required(self, url, authenticator,
proxy_host):
"""Called when a proxy needs authentication."""
msg = "<b>{}</b> requires a username and password.".format(
html_utils.escape(proxy_host))
answer = message.ask(
title="Proxy authentication required", text=msg,
mode=usertypes.PromptMode.user_pwd,
abort_on=[self.shutting_down, self.load_started])
if answer is not None:
authenticator.setUser(answer.user)
authenticator.setPassword(answer.password)
else:
try:
# pylint: disable=no-member, useless-suppression
sip.assign(authenticator, QAuthenticator())
except AttributeError:
url_string = url.toDisplayString()
error_page = jinja.render(
'error.html',
title="Error loading page: {}".format(url_string),
url=url_string, error="Proxy authentication required",
icon='')
self.set_html(error_page)
@pyqtSlot(QUrl, 'QAuthenticator*') @pyqtSlot(QUrl, 'QAuthenticator*')
def _on_authentication_required(self, url, authenticator): def _on_authentication_required(self, url, authenticator):
# FIXME:qtwebengine support .netrc # FIXME:qtwebengine support .netrc
@ -755,6 +773,8 @@ class WebEngineTab(browsertab.AbstractTab):
page.loadFinished.connect(self._on_load_finished) page.loadFinished.connect(self._on_load_finished)
page.certificate_error.connect(self._on_ssl_errors) page.certificate_error.connect(self._on_ssl_errors)
page.authenticationRequired.connect(self._on_authentication_required) page.authenticationRequired.connect(self._on_authentication_required)
page.proxyAuthenticationRequired.connect(
self._on_proxy_authentication_required)
page.fullScreenRequested.connect(self._on_fullscreen_requested) page.fullScreenRequested.connect(self._on_fullscreen_requested)
page.contentsSizeChanged.connect(self.contents_size_changed) page.contentsSizeChanged.connect(self.contents_size_changed)

View File

@ -515,3 +515,7 @@ class Command:
raise cmdexc.PrerequisitesError( raise cmdexc.PrerequisitesError(
"{}: This command is only allowed in {} mode, not {}.".format( "{}: This command is only allowed in {} mode, not {}.".format(
self.name, mode_names, mode.name)) self.name, mode_names, mode.name))
def takes_count(self):
"""Return true iff this command can take a count argument."""
return any(arg.count for arg in self._qute_args)

View File

@ -154,6 +154,9 @@ class Completer(QObject):
"partitioned: {} '{}' {}".format(prefix, center, postfix)) "partitioned: {} '{}' {}".format(prefix, center, postfix))
return prefix, center, postfix return prefix, center, postfix
# We should always return above
assert False, parts
@pyqtSlot(str) @pyqtSlot(str)
def on_selection_changed(self, text): def on_selection_changed(self, text):
"""Change the completed part if a new item was selected. """Change the completed part if a new item was selected.

View File

@ -34,6 +34,9 @@ from qutebrowser.config import config
from qutebrowser.utils import qtutils, jinja from qutebrowser.utils import qtutils, jinja
_cached_stylesheet = None
class CompletionItemDelegate(QStyledItemDelegate): class CompletionItemDelegate(QStyledItemDelegate):
"""Delegate used by CompletionView to draw individual items. """Delegate used by CompletionView to draw individual items.
@ -189,14 +192,8 @@ class CompletionItemDelegate(QStyledItemDelegate):
self._doc.setDefaultTextOption(text_option) self._doc.setDefaultTextOption(text_option)
self._doc.setDocumentMargin(2) self._doc.setDocumentMargin(2)
stylesheet = """ assert _cached_stylesheet is not None
.highlight { self._doc.setDefaultStyleSheet(_cached_stylesheet)
color: {{ conf.colors.completion.match.fg }};
}
"""
with jinja.environment.no_autoescape():
template = jinja.environment.from_string(stylesheet)
self._doc.setDefaultStyleSheet(template.render(conf=config.val))
if index.parent().isValid(): if index.parent().isValid():
view = self.parent() view = self.parent()
@ -283,3 +280,24 @@ class CompletionItemDelegate(QStyledItemDelegate):
self._draw_focus_rect() self._draw_focus_rect()
self._painter.restore() self._painter.restore()
@config.change_filter('colors.completion.match.fg', function=True)
def _update_stylesheet():
"""Update the cached stylesheet."""
stylesheet = """
.highlight {
color: {{ conf.colors.completion.match.fg }};
}
"""
with jinja.environment.no_autoescape():
template = jinja.environment.from_string(stylesheet)
global _cached_stylesheet
_cached_stylesheet = template.render(conf=config.val)
def init():
"""Initialize the cached stylesheet."""
_update_stylesheet()
config.instance.changed.connect(_update_stylesheet)

View File

@ -21,7 +21,7 @@
from qutebrowser.config import configdata, configexc from qutebrowser.config import configdata, configexc
from qutebrowser.completion.models import completionmodel, listcategory, util from qutebrowser.completion.models import completionmodel, listcategory, util
from qutebrowser.commands import runners from qutebrowser.commands import runners, cmdexc
def option(*, info): def option(*, info):
@ -44,7 +44,7 @@ def value(optname, *_values, info):
model = completionmodel.CompletionModel(column_widths=(30, 70, 0)) model = completionmodel.CompletionModel(column_widths=(30, 70, 0))
try: try:
current = info.config.get_str(optname) or '""' current = info.config.get_str(optname)
except configexc.NoOptionError: except configexc.NoOptionError:
return None return None
@ -72,7 +72,11 @@ def bind(key, *, info):
if cmd_text: if cmd_text:
parser = runners.CommandParser() parser = runners.CommandParser()
try:
cmd = parser.parse(cmd_text).cmd cmd = parser.parse(cmd_text).cmd
except cmdexc.NoSuchCommandError:
data = [(cmd_text, 'Invalid command!', key)]
else:
data = [(cmd_text, cmd.desc, key)] data = [(cmd_text, cmd.desc, key)]
model.add_category(listcategory.ListCategory("Current", data)) model.add_category(listcategory.ListCategory("Current", data))

View File

@ -19,20 +19,15 @@
"""Configuration storage and config-related utilities.""" """Configuration storage and config-related utilities."""
import sys
import copy import copy
import contextlib import contextlib
import functools import functools
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QUrl from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject
from PyQt5.QtWidgets import QMessageBox
from qutebrowser.config import configdata, configexc, configtypes, configfiles from qutebrowser.config import configdata, configexc
from qutebrowser.utils import (utils, objreg, message, log, usertypes, jinja, from qutebrowser.utils import utils, log, jinja
qtutils) from qutebrowser.misc import objects
from qutebrowser.misc import objects, msgbox, earlyinit
from qutebrowser.commands import cmdexc, cmdutils, runners
from qutebrowser.completion.models import configmodel
# An easy way to access the config from other code via config.val.foo # An easy way to access the config from other code via config.val.foo
val = None val = None
@ -40,9 +35,7 @@ instance = None
key_instance = None key_instance = None
# Keeping track of all change filters to validate them later. # Keeping track of all change filters to validate them later.
_change_filters = [] change_filters = []
# Errors which happened during init, so we can show a message box.
_init_errors = []
class change_filter: # pylint: disable=invalid-name class change_filter: # pylint: disable=invalid-name
@ -68,7 +61,7 @@ class change_filter: # pylint: disable=invalid-name
""" """
self._option = option self._option = option
self._function = function self._function = function
_change_filters.append(self) change_filters.append(self)
def validate(self): def validate(self):
"""Make sure the configured option or prefix exists. """Make sure the configured option or prefix exists.
@ -175,26 +168,11 @@ class KeyConfig:
bindings = self.get_bindings_for(mode) bindings = self.get_bindings_for(mode)
return bindings.get(key, None) return bindings.get(key, None)
def bind(self, key, command, *, mode, force=False, save_yaml=False): def bind(self, key, command, *, mode, save_yaml=False):
"""Add a new binding from key to command.""" """Add a new binding from key to command."""
key = self._prepare(key, mode) key = self._prepare(key, mode)
parser = runners.CommandParser()
try:
results = parser.parse_all(command)
except cmdexc.Error as e:
raise configexc.KeybindingError("Invalid command: {}".format(e))
for result in results: # pragma: no branch
try:
result.cmd.validate_mode(usertypes.KeyMode[mode])
except cmdexc.PrerequisitesError as e:
raise configexc.KeybindingError(str(e))
log.keyboard.vdebug("Adding binding {} -> {} in mode {}.".format( log.keyboard.vdebug("Adding binding {} -> {} in mode {}.".format(
key, command, mode)) key, command, mode))
if key in self.get_bindings_for(mode) and not force:
raise configexc.DuplicateKeyError(key)
bindings = self._config.get_obj('bindings.commands') bindings = self._config.get_obj('bindings.commands')
if mode not in bindings: if mode not in bindings:
@ -223,145 +201,6 @@ class KeyConfig:
self._config.update_mutables(save_yaml=save_yaml) self._config.update_mutables(save_yaml=save_yaml)
class ConfigCommands:
"""qutebrowser commands related to the configuration."""
def __init__(self, config, keyconfig):
self._config = config
self._keyconfig = keyconfig
@cmdutils.register(instance='config-commands', star_args_optional=True)
@cmdutils.argument('option', completion=configmodel.option)
@cmdutils.argument('values', completion=configmodel.value)
@cmdutils.argument('win_id', win_id=True)
def set(self, win_id, option=None, *values, temp=False, print_=False):
"""Set an option.
If the option name ends with '?', the value of the option is shown
instead.
If the option name ends with '!' and it is a boolean value, toggle it.
Args:
option: The name of the option.
values: The value to set, or the values to cycle through.
temp: Set value temporarily until qutebrowser is closed.
print_: Print the value after setting.
"""
if option is None:
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
tabbed_browser.openurl(QUrl('qute://settings'), newtab=False)
return
if option.endswith('?') and option != '?':
self._print_value(option[:-1])
return
with self._handle_config_error():
if option.endswith('!') and option != '!' and not values:
# Handle inversion as special cases of the cycle code path
option = option[:-1]
opt = self._config.get_opt(option)
if isinstance(opt.typ, configtypes.Bool):
values = ['false', 'true']
else:
raise cmdexc.CommandError(
"set: Can't toggle non-bool setting {}".format(option))
elif not values:
raise cmdexc.CommandError("set: The following arguments "
"are required: value")
self._set_next(option, values, temp=temp)
if print_:
self._print_value(option)
def _print_value(self, option):
"""Print the value of the given option."""
with self._handle_config_error():
value = self._config.get_str(option)
message.info("{} = {}".format(option, value))
def _set_next(self, option, values, *, temp):
"""Set the next value out of a list of values."""
if len(values) == 1:
# If we have only one value, just set it directly (avoid
# breaking stuff like aliases or other pseudo-settings)
self._config.set_str(option, values[0], save_yaml=not temp)
return
# Use the next valid value from values, or the first if the current
# value does not appear in the list
old_value = self._config.get_str(option)
try:
idx = values.index(str(old_value))
idx = (idx + 1) % len(values)
value = values[idx]
except ValueError:
value = values[0]
self._config.set_str(option, value, save_yaml=not temp)
@contextlib.contextmanager
def _handle_config_error(self):
"""Catch errors in set_command and raise CommandError."""
try:
yield
except configexc.Error as e:
raise cmdexc.CommandError("set: {}".format(e))
@cmdutils.register(instance='config-commands', maxsplit=1,
no_cmd_split=True, no_replace_variables=True)
@cmdutils.argument('command', completion=configmodel.bind)
def bind(self, key, command=None, *, mode='normal', force=False):
"""Bind a key to a command.
Args:
key: The keychain or special key (inside `<...>`) to bind.
command: The command to execute, with optional args, or None to
print the current binding.
mode: A comma-separated list of modes to bind the key in
(default: `normal`). See `:help bindings.commands` for the
available modes.
force: Rebind the key if it is already bound.
"""
if command is None:
if utils.is_special_key(key):
# self._keyconfig.get_command does this, but we also need it
# normalized for the output below
key = utils.normalize_keystr(key)
cmd = self._keyconfig.get_command(key, mode)
if cmd is None:
message.info("{} is unbound in {} mode".format(key, mode))
else:
message.info("{} is bound to '{}' in {} mode".format(
key, cmd, mode))
return
try:
self._keyconfig.bind(key, command, mode=mode, force=force,
save_yaml=True)
except configexc.DuplicateKeyError as e:
raise cmdexc.CommandError("bind: {} - use --force to override!"
.format(e))
except configexc.KeybindingError as e:
raise cmdexc.CommandError("bind: {}".format(e))
@cmdutils.register(instance='config-commands')
def unbind(self, key, *, mode='normal'):
"""Unbind a keychain.
Args:
key: The keychain or special key (inside <...>) to unbind.
mode: A mode to unbind the key in (default: `normal`).
See `:help bindings.commands` for the available modes.
"""
try:
self._keyconfig.unbind(key, mode=mode, save_yaml=True)
except configexc.KeybindingError as e:
raise cmdexc.CommandError('unbind: {}'.format(e))
class Config(QObject): class Config(QObject):
"""Main config object. """Main config object.
@ -399,7 +238,7 @@ class Config(QObject):
raise configexc.BackendError(objects.backend) raise configexc.BackendError(objects.backend)
opt.typ.to_py(value) # for validation opt.typ.to_py(value) # for validation
self._values[opt.name] = value self._values[opt.name] = opt.typ.from_obj(value)
self.changed.emit(opt.name) self.changed.emit(opt.name)
log.config.debug("Config option changed: {} = {}".format( log.config.debug("Config option changed: {} = {}".format(
@ -478,6 +317,32 @@ class Config(QObject):
if save_yaml: if save_yaml:
self._yaml[name] = converted self._yaml[name] = converted
def unset(self, name, *, save_yaml=False):
"""Set the given setting back to its default."""
self.get_opt(name)
try:
del self._values[name]
except KeyError:
return
self.changed.emit(name)
if save_yaml:
self._yaml.unset(name)
def clear(self, *, save_yaml=False):
"""Clear all settings in the config.
If save_yaml=True is given, also remove all customization from the YAML
file.
"""
old_values = self._values
self._values = {}
for name in old_values:
self.changed.emit(name)
if save_yaml:
self._yaml.clear()
def update_mutables(self, *, save_yaml=False): def update_mutables(self, *, save_yaml=False):
"""Update mutable settings if they changed. """Update mutable settings if they changed.
@ -647,114 +512,3 @@ class StyleSheetObserver(QObject):
self._obj.setStyleSheet(qss) self._obj.setStyleSheet(qss)
if update: if update:
instance.changed.connect(self._update_stylesheet) instance.changed.connect(self._update_stylesheet)
def early_init(args):
"""Initialize the part of the config which works without a QApplication."""
configdata.init()
yaml_config = configfiles.YamlConfig()
global val, instance, key_instance
instance = Config(yaml_config=yaml_config)
val = ConfigContainer(instance)
key_instance = KeyConfig(instance)
for cf in _change_filters:
cf.validate()
configtypes.Font.monospace_fonts = val.fonts.monospace
config_commands = ConfigCommands(instance, key_instance)
objreg.register('config-commands', config_commands)
config_api = None
try:
config_api = configfiles.read_config_py()
# Raised here so we get the config_api back.
if config_api.errors:
raise configexc.ConfigFileErrors('config.py', config_api.errors)
except configexc.ConfigFileErrors as e:
log.config.exception("Error while loading config.py")
_init_errors.append(e)
try:
if getattr(config_api, 'load_autoconfig', True):
try:
instance.read_yaml()
except configexc.ConfigFileErrors as e:
raise # caught in outer block
except configexc.Error as e:
desc = configexc.ConfigErrorDesc("Error", e)
raise configexc.ConfigFileErrors('autoconfig.yml', [desc])
except configexc.ConfigFileErrors as e:
log.config.exception("Error while loading config.py")
_init_errors.append(e)
configfiles.init()
objects.backend = get_backend(args)
earlyinit.init_with_backend(objects.backend)
def get_backend(args):
"""Find out what backend to use based on available libraries."""
try:
import PyQt5.QtWebKit # pylint: disable=unused-variable
except ImportError:
webkit_available = False
else:
webkit_available = qtutils.is_new_qtwebkit()
str_to_backend = {
'webkit': usertypes.Backend.QtWebKit,
'webengine': usertypes.Backend.QtWebEngine,
}
if args.backend is not None:
return str_to_backend[args.backend]
elif val.backend != 'auto':
return str_to_backend[val.backend]
elif webkit_available:
return usertypes.Backend.QtWebKit
else:
return usertypes.Backend.QtWebEngine
def late_init(save_manager):
"""Initialize the rest of the config after the QApplication is created."""
global _init_errors
for err in _init_errors:
errbox = msgbox.msgbox(parent=None,
title="Error while reading config",
text=err.to_html(),
icon=QMessageBox.Warning,
plain_text=False)
errbox.exec_()
_init_errors = []
instance.init_save_manager(save_manager)
configfiles.state.init_save_manager(save_manager)
def qt_args(namespace):
"""Get the Qt QApplication arguments based on an argparse namespace.
Args:
namespace: The argparse namespace.
Return:
The argv list to be passed to Qt.
"""
argv = [sys.argv[0]]
if namespace.qt_flag is not None:
argv += ['--' + flag[0] for flag in namespace.qt_flag]
if namespace.qt_arg is not None:
for name, value in namespace.qt_arg:
argv += ['--' + name, value]
argv += ['--' + arg for arg in val.qt_args]
return argv

View File

@ -0,0 +1,248 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-2017 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/>.
"""Commands related to the configuration."""
import os.path
import contextlib
from PyQt5.QtCore import QUrl
from qutebrowser.commands import cmdexc, cmdutils
from qutebrowser.completion.models import configmodel
from qutebrowser.utils import objreg, utils, message, standarddir
from qutebrowser.config import configtypes, configexc, configfiles
from qutebrowser.misc import editor
class ConfigCommands:
"""qutebrowser commands related to the configuration."""
def __init__(self, config, keyconfig):
self._config = config
self._keyconfig = keyconfig
@contextlib.contextmanager
def _handle_config_error(self):
"""Catch errors in set_command and raise CommandError."""
try:
yield
except configexc.Error as e:
raise cmdexc.CommandError("set: {}".format(e))
def _print_value(self, option):
"""Print the value of the given option."""
with self._handle_config_error():
value = self._config.get_str(option)
message.info("{} = {}".format(option, value))
@cmdutils.register(instance='config-commands')
@cmdutils.argument('option', completion=configmodel.option)
@cmdutils.argument('value', completion=configmodel.value)
@cmdutils.argument('win_id', win_id=True)
def set(self, win_id, option=None, value=None, temp=False, print_=False):
"""Set an option.
If the option name ends with '?', the value of the option is shown
instead.
Args:
option: The name of the option.
value: The value to set.
temp: Set value temporarily until qutebrowser is closed.
print_: Print the value after setting.
"""
if option is None:
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
tabbed_browser.openurl(QUrl('qute://settings'), newtab=False)
return
if option.endswith('?') and option != '?':
self._print_value(option[:-1])
return
with self._handle_config_error():
if value is None:
raise cmdexc.CommandError("set: The following arguments "
"are required: value")
else:
self._config.set_str(option, value, save_yaml=not temp)
if print_:
self._print_value(option)
@cmdutils.register(instance='config-commands', maxsplit=1,
no_cmd_split=True, no_replace_variables=True)
@cmdutils.argument('command', completion=configmodel.bind)
def bind(self, key, command=None, *, mode='normal'):
"""Bind a key to a command.
Args:
key: The keychain or special key (inside `<...>`) to bind.
command: The command to execute, with optional args, or None to
print the current binding.
mode: A comma-separated list of modes to bind the key in
(default: `normal`). See `:help bindings.commands` for the
available modes.
"""
if command is None:
if utils.is_special_key(key):
# self._keyconfig.get_command does this, but we also need it
# normalized for the output below
key = utils.normalize_keystr(key)
cmd = self._keyconfig.get_command(key, mode)
if cmd is None:
message.info("{} is unbound in {} mode".format(key, mode))
else:
message.info("{} is bound to '{}' in {} mode".format(
key, cmd, mode))
return
try:
self._keyconfig.bind(key, command, mode=mode, save_yaml=True)
except configexc.KeybindingError as e:
raise cmdexc.CommandError("bind: {}".format(e))
@cmdutils.register(instance='config-commands')
def unbind(self, key, *, mode='normal'):
"""Unbind a keychain.
Args:
key: The keychain or special key (inside <...>) to unbind.
mode: A mode to unbind the key in (default: `normal`).
See `:help bindings.commands` for the available modes.
"""
try:
self._keyconfig.unbind(key, mode=mode, save_yaml=True)
except configexc.KeybindingError as e:
raise cmdexc.CommandError('unbind: {}'.format(e))
@cmdutils.register(instance='config-commands', star_args_optional=True)
@cmdutils.argument('option', completion=configmodel.option)
@cmdutils.argument('values', completion=configmodel.value)
def config_cycle(self, option, *values, temp=False, print_=False):
"""Cycle an option between multiple values.
Args:
option: The name of the option.
values: The values to cycle through.
temp: Set value temporarily until qutebrowser is closed.
print_: Print the value after setting.
"""
with self._handle_config_error():
opt = self._config.get_opt(option)
old_value = self._config.get_obj(option, mutable=False)
if not values and isinstance(opt.typ, configtypes.Bool):
values = ['true', 'false']
if len(values) < 2:
raise cmdexc.CommandError("Need at least two values for "
"non-boolean settings.")
# Use the next valid value from values, or the first if the current
# value does not appear in the list
with self._handle_config_error():
values = [opt.typ.from_str(val) for val in values]
try:
idx = values.index(old_value)
idx = (idx + 1) % len(values)
value = values[idx]
except ValueError:
value = values[0]
with self._handle_config_error():
self._config.set_obj(option, value, save_yaml=not temp)
if print_:
self._print_value(option)
@cmdutils.register(instance='config-commands')
@cmdutils.argument('option', completion=configmodel.option)
def config_unset(self, option, temp=False):
"""Unset an option.
This sets an option back to its default and removes it from
autoconfig.yml.
Args:
option: The name of the option.
temp: Don't touch autoconfig.yml.
"""
with self._handle_config_error():
self._config.unset(option, save_yaml=not temp)
@cmdutils.register(instance='config-commands')
def config_clear(self, save=False):
"""Set all settings back to their default.
Args:
save: If given, all configuration in autoconfig.yml is also
removed.
"""
self._config.clear(save_yaml=save)
@cmdutils.register(instance='config-commands')
def config_source(self, filename=None, clear=False):
"""Read a config.py file.
Args:
filename: The file to load. If not given, loads the default
config.py.
clear: Clear current settings first.
"""
if filename is None:
filename = os.path.join(standarddir.config(), 'config.py')
else:
filename = os.path.expanduser(filename)
if clear:
self.config_clear()
try:
configfiles.read_config_py(filename)
except configexc.ConfigFileErrors as e:
raise cmdexc.CommandError(e)
@cmdutils.register(instance='config-commands')
def config_edit(self, no_source=False):
"""Open the config.py file in the editor.
Args:
no_source: Don't re-source the config file after editing.
"""
def on_editing_finished():
"""Source the new config when editing finished.
This can't use cmdexc.CommandError as it's run async.
"""
try:
configfiles.read_config_py(filename)
except configexc.ConfigFileErrors as e:
message.error(str(e))
ed = editor.ExternalEditor(self._config)
if not no_source:
ed.editing_finished.connect(on_editing_finished)
filename = os.path.join(standarddir.config(), 'config.py')
ed.edit_file(filename)

View File

@ -93,7 +93,7 @@ def _parse_yaml_type(name, node):
if typ is configtypes.Dict: if typ is configtypes.Dict:
kwargs['keytype'] = _parse_yaml_type(name, kwargs['keytype']) kwargs['keytype'] = _parse_yaml_type(name, kwargs['keytype'])
kwargs['valtype'] = _parse_yaml_type(name, kwargs['valtype']) kwargs['valtype'] = _parse_yaml_type(name, kwargs['valtype'])
elif typ is configtypes.List: elif typ is configtypes.List or typ is configtypes.ListOrValue:
kwargs['valtype'] = _parse_yaml_type(name, kwargs['valtype']) kwargs['valtype'] = _parse_yaml_type(name, kwargs['valtype'])
except KeyError as e: except KeyError as e:
_raise_invalid_node(name, str(e), node) _raise_invalid_node(name, str(e), node)

View File

@ -101,27 +101,40 @@ qt_args:
https://peter.sh/experiments/chromium-command-line-switches/ for a list) https://peter.sh/experiments/chromium-command-line-switches/ for a list)
will work. will work.
This setting requires a restart.
force_software_rendering:
type: Bool
default: false
backend: QtWebEngine
desc: >-
Force software rendering for QtWebEngine.
This is needed for QtWebEngine to work with Nouveau drivers.
This setting requires a restart.
backend: backend:
type: type:
name: String name: String
valid_values: valid_values:
- auto: Automatically select either QtWebEngine or QtWebKit - webengine: Use QtWebEngine (based on Chromium)
- webkit: Force QtWebKit - webkit: Use QtWebKit (based on WebKit, similar to Safari)
- webengine: Force QtWebEngine default: webengine
default: auto
desc: >- desc: >-
The backend to use to display websites. The backend to use to display websites.
qutebrowser supports two different web rendering engines / backends, qutebrowser supports two different web rendering engines / backends,
QtWebKit and QtWebEngine. QtWebKit and QtWebEngine.
QtWebKit is based on WebKit (similar to Safari). It was discontinued by the QtWebKit was discontinued by the Qt project with Qt 5.6, but picked up as a
Qt project with Qt 5.6, but picked up as a well maintained fork: well maintained fork: https://github.com/annulen/webkit/wiki - qutebrowser
https://github.com/annulen/webkit/wiki - qutebrowser only supports the fork. only supports the fork.
QtWebEngine is Qt's official successor to QtWebKit and based on the Chromium QtWebEngine is Qt's official successor to QtWebKit. It's slightly more
project. It's slightly more resource hungry that QtWebKit and has a couple resource hungry that QtWebKit and has a couple of missing features in
of missing features in qutebrowser, but is generally the preferred choice. qutebrowser, but is generally the preferred choice.
This setting requires a restart.
## auto_save ## auto_save
@ -545,7 +558,7 @@ content.ssl_strict:
content.user_stylesheets: content.user_stylesheets:
type: type:
name: List name: ListOrValue
valtype: File valtype: File
none_ok: True none_ok: True
default: null default: null
@ -562,10 +575,12 @@ content.xss_auditing:
desc: >- desc: >-
Whether load requests should be monitored for cross-site scripting attempts. Whether load requests should be monitored for cross-site scripting attempts.
Suspicious scripts will be blocked and reported in the inspector\'s Suspicious scripts will be blocked and reported in the inspector's
JavaScript console. Enabling this feature might have an impact on JavaScript console. Enabling this feature might have an impact on
performance. performance.
# emacs: '
## completion ## completion
completion.cmd_history_max_items: completion.cmd_history_max_items:
@ -917,11 +932,13 @@ keyhint.blacklist:
name: String name: String
default: [] default: []
desc: >- desc: >-
Keychains that shouldn\'t be shown in the keyhint dialog. Keychains that shouldn't be shown in the keyhint dialog.
Globs are supported, so `;*` will blacklist all keychains starting with `;`. Globs are supported, so `;*` will blacklist all keychains starting with `;`.
Use `*` to disable keyhints. Use `*` to disable keyhints.
# emacs: '
keyhint.delay: keyhint.delay:
type: type:
name: Int name: Int
@ -1243,13 +1260,6 @@ tabs.width.indicator:
minval: 0 minval: 0
desc: Width of the progress indicator (0 to disable). desc: Width of the progress indicator (0 to disable).
tabs.width.pinned:
default: 43
type:
name: Int
minval: 10
desc: The width for pinned tabs with a horizontal tabbar, in px.
tabs.wrap: tabs.wrap:
default: true default: true
type: Bool type: Bool
@ -1305,9 +1315,9 @@ url.searchengines:
url.start_pages: url.start_pages:
type: type:
name: List name: ListOrValue
valtype: FuzzyUrl valtype: FuzzyUrl
default: ["https://start.duckduckgo.com"] default: "https://start.duckduckgo.com"
desc: The page(s) to open at the start. desc: The page(s) to open at the start.
url.yank_ignored_parameters: url.yank_ignored_parameters:
@ -1616,13 +1626,18 @@ colors.prompts.fg:
type: QssColor type: QssColor
desc: Foreground color for prompts. desc: Foreground color for prompts.
colors.prompts.border:
default: 1px solid gray
type: String
desc: Border used around UI elements in prompts.
colors.prompts.bg: colors.prompts.bg:
default: darkblue default: '#444444'
type: QssColor type: QssColor
desc: Background color for prompts. desc: Background color for prompts.
colors.prompts.selected.bg: colors.prompts.selected.bg:
default: '#308cc6' default: grey
type: QssColor type: QssColor
desc: Background color for the selected item in filename prompts. desc: Background color for the selected item in filename prompts.
@ -1803,9 +1818,11 @@ fonts.monospace:
desc: >- desc: >-
Default monospace fonts. Default monospace fonts.
Whenever "monospace" is used in a font setting, it\'s replaced with the Whenever "monospace" is used in a font setting, it's replaced with the
fonts listed here. fonts listed here.
# emacs: '
fonts.completion.entry: fonts.completion.entry:
default: 8pt monospace default: 8pt monospace
type: Font type: Font
@ -1972,6 +1989,9 @@ bindings.key_mappings:
This is useful for global remappings of keys, for example to map Ctrl-[ to This is useful for global remappings of keys, for example to map Ctrl-[ to
Escape. Escape.
Note that when a key is bound (via `bindings.default` or
`bindings.commands`), the mapping is ignored.
bindings.default: bindings.default:
default: default:
normal: normal:
@ -2040,8 +2060,8 @@ bindings.default:
l: scroll right l: scroll right
u: undo u: undo
<Ctrl-Shift-T>: undo <Ctrl-Shift-T>: undo
gg: scroll-perc 0 gg: scroll-to-perc 0
G: scroll-perc G: scroll-to-perc
n: search-next n: search-next
N: search-prev N: search-prev
i: enter-mode insert i: enter-mode insert

View File

@ -59,14 +59,6 @@ class KeybindingError(Error):
"""Raised for issues with keybindings.""" """Raised for issues with keybindings."""
class DuplicateKeyError(KeybindingError):
"""Raised when there was a duplicate key."""
def __init__(self, key):
super().__init__("Duplicate key {}".format(key))
class NoOptionError(Error): class NoOptionError(Error):
"""Raised when an option was not found.""" """Raised when an option was not found."""
@ -94,6 +86,12 @@ class ConfigErrorDesc:
def __str__(self): def __str__(self):
return '{}: {}'.format(self.text, self.exception) return '{}: {}'.format(self.text, self.exception)
def with_text(self, text):
"""Get a new ConfigErrorDesc with the given text appended."""
return self.__class__(text='{} ({})'.format(self.text, text),
exception=self.exception,
traceback=self.traceback)
class ConfigFileErrors(Error): class ConfigFileErrors(Error):

View File

@ -19,18 +19,20 @@
"""Configuration files residing on disk.""" """Configuration files residing on disk."""
import pathlib
import types import types
import os.path import os.path
import sys
import textwrap import textwrap
import traceback import traceback
import configparser import configparser
import contextlib import contextlib
import yaml import yaml
from PyQt5.QtCore import QSettings from PyQt5.QtCore import pyqtSignal, QObject, QSettings
import qutebrowser import qutebrowser
from qutebrowser.config import configexc, config from qutebrowser.config import configexc, config, configdata
from qutebrowser.utils import standarddir, utils, qtutils from qutebrowser.utils import standarddir, utils, qtutils
@ -70,7 +72,7 @@ class StateConfig(configparser.ConfigParser):
self.write(f) self.write(f)
class YamlConfig: class YamlConfig(QObject):
"""A config stored on disk as YAML file. """A config stored on disk as YAML file.
@ -79,8 +81,10 @@ class YamlConfig:
""" """
VERSION = 1 VERSION = 1
changed = pyqtSignal()
def __init__(self): def __init__(self, parent=None):
super().__init__(parent)
self._filename = os.path.join(standarddir.config(auto=True), self._filename = os.path.join(standarddir.config(auto=True),
'autoconfig.yml') 'autoconfig.yml')
self._values = {} self._values = {}
@ -92,20 +96,25 @@ class YamlConfig:
We do this outside of __init__ because the config gets created before We do this outside of __init__ because the config gets created before
the save_manager exists. the save_manager exists.
""" """
save_manager.add_saveable('yaml-config', self._save) save_manager.add_saveable('yaml-config', self._save, self.changed)
def __getitem__(self, name): def __getitem__(self, name):
return self._values[name] return self._values[name]
def __setitem__(self, name, value): def __setitem__(self, name, value):
self._dirty = True
self._values[name] = value self._values[name] = value
self._mark_changed()
def __contains__(self, name): def __contains__(self, name):
return name in self._values return name in self._values
def __iter__(self): def __iter__(self):
return iter(self._values.items()) return iter(sorted(self._values.items()))
def _mark_changed(self):
"""Mark the YAML config as changed."""
self._dirty = True
self.changed.emit()
def _save(self): def _save(self):
"""Save the settings to the YAML file if they've changed.""" """Save the settings to the YAML file if they've changed."""
@ -153,9 +162,28 @@ class YamlConfig:
"'global' object is not a dict") "'global' object is not a dict")
raise configexc.ConfigFileErrors('autoconfig.yml', [desc]) raise configexc.ConfigFileErrors('autoconfig.yml', [desc])
# Delete unknown values
# (e.g. options which were removed from configdata.yml)
for name in list(global_obj):
if name not in configdata.DATA:
del global_obj[name]
self._values = global_obj self._values = global_obj
self._dirty = False self._dirty = False
def unset(self, name):
"""Remove the given option name if it's configured."""
try:
del self._values[name]
except KeyError:
return
self._mark_changed()
def clear(self):
"""Clear all values from the YAML file."""
self._values = []
self._mark_changed()
class ConfigAPI: class ConfigAPI:
@ -168,20 +196,26 @@ class ConfigAPI:
Attributes: Attributes:
_config: The main Config object to use. _config: The main Config object to use.
_keyconfig: The KeyConfig object. _keyconfig: The KeyConfig object.
load_autoconfig: Whether autoconfig.yml should be loaded.
errors: Errors which occurred while setting options. errors: Errors which occurred while setting options.
configdir: The qutebrowser config directory, as pathlib.Path.
datadir: The qutebrowser data directory, as pathlib.Path.
""" """
def __init__(self, conf, keyconfig): def __init__(self, conf, keyconfig):
self._config = conf self._config = conf
self._keyconfig = keyconfig self._keyconfig = keyconfig
self.load_autoconfig = True
self.errors = [] self.errors = []
self.configdir = pathlib.Path(standarddir.config())
self.datadir = pathlib.Path(standarddir.data())
@contextlib.contextmanager @contextlib.contextmanager
def _handle_error(self, action, name): def _handle_error(self, action, name):
try: try:
yield yield
except configexc.ConfigFileErrors as e:
for err in e.errors:
new_err = err.with_text(e.basename)
self.errors.append(new_err)
except configexc.Error as e: except configexc.Error as e:
text = "While {} '{}'".format(action, name) text = "While {} '{}'".format(action, name)
self.errors.append(configexc.ConfigErrorDesc(text, e)) self.errors.append(configexc.ConfigErrorDesc(text, e))
@ -190,6 +224,10 @@ class ConfigAPI:
"""Do work which needs to be done after reading config.py.""" """Do work which needs to be done after reading config.py."""
self._config.update_mutables() self._config.update_mutables()
def load_autoconfig(self):
with self._handle_error('reading', 'autoconfig.yml'):
read_autoconfig()
def get(self, name): def get(self, name):
with self._handle_error('getting', name): with self._handle_error('getting', name):
return self._config.get_obj(name) return self._config.get_obj(name)
@ -198,24 +236,24 @@ class ConfigAPI:
with self._handle_error('setting', name): with self._handle_error('setting', name):
self._config.set_obj(name, value) self._config.set_obj(name, value)
def bind(self, key, command, mode='normal', *, force=False): def bind(self, key, command, mode='normal'):
with self._handle_error('binding', key): with self._handle_error('binding', key):
self._keyconfig.bind(key, command, mode=mode, force=force) self._keyconfig.bind(key, command, mode=mode)
def unbind(self, key, mode='normal'): def unbind(self, key, mode='normal'):
with self._handle_error('unbinding', key): with self._handle_error('unbinding', key):
self._keyconfig.unbind(key, mode=mode) self._keyconfig.unbind(key, mode=mode)
def read_config_py(filename=None): def read_config_py(filename, raising=False):
"""Read a config.py file.""" """Read a config.py file.
Arguments;
filename: The name of the file to read.
raising: Raise exceptions happening in config.py.
This is needed during tests to use pytest's inspection.
"""
api = ConfigAPI(config.instance, config.key_instance) api = ConfigAPI(config.instance, config.key_instance)
if filename is None:
filename = os.path.join(standarddir.config(), 'config.py')
if not os.path.exists(filename):
return api
container = config.ConfigContainer(config.instance, configapi=api) container = config.ConfigContainer(config.instance, configapi=api)
basename = os.path.basename(filename) basename = os.path.basename(filename)
@ -234,7 +272,7 @@ def read_config_py(filename=None):
try: try:
code = compile(source, filename, 'exec') code = compile(source, filename, 'exec')
except (ValueError, TypeError) as e: except ValueError as e:
# source contains NUL bytes # source contains NUL bytes
desc = configexc.ConfigErrorDesc("Error while compiling", e) desc = configexc.ConfigErrorDesc("Error while compiling", e)
raise configexc.ConfigFileErrors(basename, [desc]) raise configexc.ConfigFileErrors(basename, [desc])
@ -244,14 +282,51 @@ def read_config_py(filename=None):
raise configexc.ConfigFileErrors(basename, [desc]) raise configexc.ConfigFileErrors(basename, [desc])
try: try:
# Save and restore sys variables
with saved_sys_properties():
# Add config directory to python path, so config.py can import
# other files in logical places
config_dir = os.path.dirname(filename)
if config_dir not in sys.path:
sys.path.insert(0, config_dir)
exec(code, module.__dict__) exec(code, module.__dict__)
except Exception as e: except Exception as e:
if raising:
raise
api.errors.append(configexc.ConfigErrorDesc( api.errors.append(configexc.ConfigErrorDesc(
"Unhandled exception", "Unhandled exception",
exception=e, traceback=traceback.format_exc())) exception=e, traceback=traceback.format_exc()))
api.finalize() api.finalize()
return api
if api.errors:
raise configexc.ConfigFileErrors('config.py', api.errors)
def read_autoconfig():
"""Read the autoconfig.yml file."""
try:
config.instance.read_yaml()
except configexc.ConfigFileErrors as e:
raise # caught in outer block
except configexc.Error as e:
desc = configexc.ConfigErrorDesc("Error", e)
raise configexc.ConfigFileErrors('autoconfig.yml', [desc])
@contextlib.contextmanager
def saved_sys_properties():
"""Save various sys properties such as sys.path and sys.modules."""
old_path = sys.path.copy()
old_modules = sys.modules.copy()
try:
yield
finally:
sys.path = old_path
for module in set(sys.modules).difference(old_modules):
del sys.modules[module]
def init(): def init():

View File

@ -0,0 +1,132 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2017 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/>.
"""Initialization of the configuration."""
import os.path
import sys
from PyQt5.QtWidgets import QMessageBox
from qutebrowser.config import (config, configdata, configfiles, configtypes,
configexc, configcommands)
from qutebrowser.utils import objreg, usertypes, log, standarddir, message
from qutebrowser.misc import msgbox, objects
# Error which happened during init, so we can show a message box.
_init_errors = None
def early_init(args):
"""Initialize the part of the config which works without a QApplication."""
configdata.init()
yaml_config = configfiles.YamlConfig()
config.instance = config.Config(yaml_config=yaml_config)
config.val = config.ConfigContainer(config.instance)
config.key_instance = config.KeyConfig(config.instance)
yaml_config.setParent(config.instance)
for cf in config.change_filters:
cf.validate()
configtypes.Font.monospace_fonts = config.val.fonts.monospace
config_commands = configcommands.ConfigCommands(
config.instance, config.key_instance)
objreg.register('config-commands', config_commands)
config_file = os.path.join(standarddir.config(), 'config.py')
try:
if os.path.exists(config_file):
configfiles.read_config_py(config_file)
else:
configfiles.read_autoconfig()
except configexc.ConfigFileErrors as e:
log.config.exception("Error while loading {}".format(e.basename))
global _init_errors
_init_errors = e
configfiles.init()
objects.backend = get_backend(args)
for opt, val in args.temp_settings:
try:
config.instance.set_str(opt, val)
except configexc.Error as e:
message.error("set: {} - {}".format(e.__class__.__name__, e))
if (objects.backend == usertypes.Backend.QtWebEngine and
config.val.force_software_rendering):
os.environ['QT_XCB_FORCE_SOFTWARE_OPENGL'] = '1'
def get_backend(args):
"""Find out what backend to use based on available libraries."""
str_to_backend = {
'webkit': usertypes.Backend.QtWebKit,
'webengine': usertypes.Backend.QtWebEngine,
}
if args.backend is not None:
return str_to_backend[args.backend]
else:
return str_to_backend[config.val.backend]
def late_init(save_manager):
"""Initialize the rest of the config after the QApplication is created."""
global _init_errors
if _init_errors is not None:
errbox = msgbox.msgbox(parent=None,
title="Error while reading config",
text=_init_errors.to_html(),
icon=QMessageBox.Warning,
plain_text=False)
errbox.exec_()
_init_errors = None
config.instance.init_save_manager(save_manager)
configfiles.state.init_save_manager(save_manager)
def qt_args(namespace):
"""Get the Qt QApplication arguments based on an argparse namespace.
Args:
namespace: The argparse namespace.
Return:
The argv list to be passed to Qt.
"""
argv = [sys.argv[0]]
if namespace.qt_flag is not None:
argv += ['--' + flag[0] for flag in namespace.qt_flag]
if namespace.qt_arg is not None:
for name, value in namespace.qt_arg:
argv += ['--' + name, value]
argv += ['--' + arg for arg in config.val.qt_args]
return argv

View File

@ -59,7 +59,7 @@ from PyQt5.QtCore import QUrl, Qt
from PyQt5.QtGui import QColor, QFont from PyQt5.QtGui import QColor, QFont
from PyQt5.QtWidgets import QTabWidget, QTabBar from PyQt5.QtWidgets import QTabWidget, QTabBar
from qutebrowser.commands import cmdutils, runners, cmdexc from qutebrowser.commands import cmdutils
from qutebrowser.config import configexc from qutebrowser.config import configexc
from qutebrowser.utils import standarddir, utils, qtutils, urlutils from qutebrowser.utils import standarddir, utils, qtutils, urlutils
@ -227,6 +227,10 @@ class BaseType:
return None return None
return value return value
def from_obj(self, value):
"""Get the setting value from a config.py/YAML object."""
return value
def to_py(self, value): def to_py(self, value):
"""Get the setting value from a Python value. """Get the setting value from a Python value.
@ -257,9 +261,10 @@ class BaseType:
This currently uses asciidoc syntax. This currently uses asciidoc syntax.
""" """
utils.unused(indent) # only needed for Dict/List utils.unused(indent) # only needed for Dict/List
if not value: str_value = self.to_str(value)
if not str_value:
return 'empty' return 'empty'
return '+pass:[{}]+'.format(html.escape(self.to_str(value))) return '+pass:[{}]+'.format(html.escape(str_value))
def complete(self): def complete(self):
"""Return a list of possible values for completion. """Return a list of possible values for completion.
@ -440,6 +445,11 @@ class List(BaseType):
self.to_py(yaml_val) self.to_py(yaml_val)
return yaml_val return yaml_val
def from_obj(self, value):
if value is None:
return []
return value
def to_py(self, value): def to_py(self, value):
self._basic_py_validation(value, list) self._basic_py_validation(value, list)
if not value: if not value:
@ -475,6 +485,72 @@ class List(BaseType):
return '\n'.join(lines) return '\n'.join(lines)
class ListOrValue(BaseType):
"""A list of values, or a single value.
//
Internally, the value is stored as either a value (of valtype), or a list.
to_py() then ensures that it's always a list.
"""
_show_valtype = True
def __init__(self, valtype, none_ok=False, *args, **kwargs):
super().__init__(none_ok)
assert not isinstance(valtype, (List, ListOrValue)), valtype
self.listtype = List(valtype, none_ok=none_ok, *args, **kwargs)
self.valtype = valtype
def get_name(self):
return self.listtype.get_name() + ', or ' + self.valtype.get_name()
def get_valid_values(self):
return self.valtype.get_valid_values()
def from_str(self, value):
try:
return self.listtype.from_str(value)
except configexc.ValidationError:
return self.valtype.from_str(value)
def from_obj(self, value):
if value is None:
return []
return value
def to_py(self, value):
try:
return [self.valtype.to_py(value)]
except configexc.ValidationError:
return self.listtype.to_py(value)
def to_str(self, value):
if value is None:
return ''
if isinstance(value, list):
if len(value) == 1:
return self.valtype.to_str(value[0])
else:
return self.listtype.to_str(value)
else:
return self.valtype.to_str(value)
def to_doc(self, value, indent=0):
if value is None:
return 'empty'
if isinstance(value, list):
if len(value) == 1:
return self.valtype.to_doc(value[0], indent)
else:
return self.listtype.to_doc(value, indent)
else:
return self.valtype.to_doc(value, indent)
class FlagList(List): class FlagList(List):
"""A list of flags. """A list of flags.
@ -773,33 +849,13 @@ class PercOrInt(_Numeric):
class Command(BaseType): class Command(BaseType):
"""Base class for a command value with arguments.""" """A qutebrowser command with arguments.
# See to_py for details //
unvalidated = False
def to_py(self, value): Since validation is quite tricky here, we don't do so, and instead let
self._basic_py_validation(value, str) invalid commands (in bindings/aliases) fail when used.
if not value: """
return None
# This requires some trickery, as runners.CommandParser uses
# conf.val.aliases, which in turn map to a command again,
# leading to an endless recursion.
# To fix that, we turn off validating other commands (alias values)
# while validating a command.
if not Command.unvalidated:
Command.unvalidated = True
try:
parser = runners.CommandParser()
try:
parser.parse_all(value)
except cmdexc.Error as e:
raise configexc.ValidationError(value, str(e))
finally:
Command.unvalidated = False
return value
def complete(self): def complete(self):
out = [] out = []
@ -807,6 +863,10 @@ class Command(BaseType):
out.append((cmdname, obj.desc)) out.append((cmdname, obj.desc))
return out return out
def to_py(self, value):
self._basic_py_validation(value, str)
return value
class ColorSystem(MappingType): class ColorSystem(MappingType):
@ -1130,6 +1190,11 @@ class Dict(BaseType):
self.to_py(yaml_val) self.to_py(yaml_val)
return yaml_val return yaml_val
def from_obj(self, value):
if value is None:
return {}
return value
def _fill_fixed_keys(self, value): def _fill_fixed_keys(self, value):
"""Fill missing fixed keys with a None-value.""" """Fill missing fixed keys with a None-value."""
if self.fixed_keys is None: if self.fixed_keys is None:
@ -1260,6 +1325,8 @@ class ShellCommand(List):
placeholder: If there should be a placeholder. placeholder: If there should be a placeholder.
""" """
_show_valtype = False
def __init__(self, placeholder=False, none_ok=False): def __init__(self, placeholder=False, none_ok=False):
super().__init__(valtype=String(), none_ok=none_ok) super().__init__(valtype=String(), none_ok=none_ok)
self.placeholder = placeholder self.placeholder = placeholder

View File

@ -17,6 +17,9 @@ pre { margin: 2px; }
th, td { border: 1px solid grey; padding: 0px 5px; } th, td { border: 1px solid grey; padding: 0px 5px; }
th { background: lightgrey; } th { background: lightgrey; }
th pre { color: grey; text-align: left; } th pre { color: grey; text-align: left; }
input { width: 98%; }
.setting { width: 75%; }
.value { width: 25%; text-align: center; }
.noscript, .noscript-text { color:red; } .noscript, .noscript-text { color:red; }
.noscript-text { margin-bottom: 5cm; } .noscript-text { margin-bottom: 5cm; }
.option_description { margin: .5ex 0; color: grey; font-size: 80%; font-style: italic; white-space: pre-line; } .option_description { margin: .5ex 0; color: grey; font-size: 80%; font-style: italic; white-space: pre-line; }
@ -26,15 +29,19 @@ th pre { color: grey; text-align: left; }
<noscript><h1 class="noscript">View Only</h1><p class="noscript-text">Changing settings requires javascript to be enabled!</p></noscript> <noscript><h1 class="noscript">View Only</h1><p class="noscript-text">Changing settings requires javascript to be enabled!</p></noscript>
<header><h1>{{ title }}</h1></header> <header><h1>{{ title }}</h1></header>
<table> <table>
<tr>
<th>Setting</th>
<th>Value</th>
</tr>
{% for option in configdata.DATA.values() %} {% for option in configdata.DATA.values() %}
<tr> <tr>
<!-- FIXME: convert to string properly --> <!-- FIXME: convert to string properly -->
<td>{{ option.name }} (Current: {{ confget(option.name) | string |truncate(100) }}) <td class="setting">{{ option.name }} (Current: {{ confget(option.name) | string |truncate(100) }})
{% if option.description %} {% if option.description %}
<p class="option_description">{{ option.description|e }}</p> <p class="option_description">{{ option.description|e }}</p>
{% endif %} {% endif %}
</td> </td>
<td> <td class="value">
<input type="text" <input type="text"
id="input-{{ option.name }}" id="input-{{ option.name }}"
onblur="cset('{{ option.name }}', this.value)" onblur="cset('{{ option.name }}', this.value)"

View File

@ -18,8 +18,6 @@
* along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. * 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. * Snippet to position caret at top of the page when caret mode is enabled.
* Some code was borrowed from: * Some code was borrowed from:
@ -28,8 +26,6 @@
* https://github.com/1995eaton/chromium-vim/blob/master/content_scripts/visual.js * https://github.com/1995eaton/chromium-vim/blob/master/content_scripts/visual.js
*/ */
/* eslint-enable max-len */
"use strict"; "use strict";
(function() { (function() {

View File

@ -122,6 +122,7 @@ class BaseKeyParser(QObject):
self._debug_log("Ignoring only-modifier keyeevent.") self._debug_log("Ignoring only-modifier keyeevent.")
return False return False
if binding not in self.special_bindings:
key_mappings = config.val.bindings.key_mappings key_mappings = config.val.bindings.key_mappings
try: try:
binding = key_mappings['<{}>'.format(binding)][1:-1] binding = key_mappings['<{}>'.format(binding)][1:-1]
@ -133,26 +134,28 @@ class BaseKeyParser(QObject):
except KeyError: except KeyError:
self._debug_log("No special binding found for {}.".format(binding)) self._debug_log("No special binding found for {}.".format(binding))
return False return False
count, _command = self._split_count() count, _command = self._split_count(self._keystring)
self.execute(cmdstr, self.Type.special, count) self.execute(cmdstr, self.Type.special, count)
self.clear_keystring() self.clear_keystring()
return True return True
def _split_count(self): def _split_count(self, keystring):
"""Get count and command from the current keystring. """Get count and command from the current keystring.
Args:
keystring: The key string to split.
Return: Return:
A (count, command) tuple. A (count, command) tuple.
""" """
if self._supports_count: if self._supports_count:
(countstr, cmd_input) = re.match(r'^(\d*)(.*)', (countstr, cmd_input) = re.match(r'^(\d*)(.*)', keystring).groups()
self._keystring).groups()
count = int(countstr) if countstr else None count = int(countstr) if countstr else None
if count == 0 and not cmd_input: if count == 0 and not cmd_input:
cmd_input = self._keystring cmd_input = keystring
count = None count = None
else: else:
cmd_input = self._keystring cmd_input = keystring
count = None count = None
return count, cmd_input return count, cmd_input
@ -183,18 +186,17 @@ class BaseKeyParser(QObject):
self._debug_log("Ignoring, no text char") self._debug_log("Ignoring, no text char")
return self.Match.none return self.Match.none
key_mappings = config.val.bindings.key_mappings count, cmd_input = self._split_count(self._keystring + txt)
txt = key_mappings.get(txt, txt) match, binding = self._match_key(cmd_input)
self._keystring += txt if match == self.Match.none:
mappings = config.val.bindings.key_mappings
count, cmd_input = self._split_count() mapped = mappings.get(txt, None)
if mapped is not None:
if not cmd_input: txt = mapped
# Only a count, no command yet, but we handled it count, cmd_input = self._split_count(self._keystring + txt)
return self.Match.other
match, binding = self._match_key(cmd_input) match, binding = self._match_key(cmd_input)
self._keystring += txt
if match == self.Match.definitive: if match == self.Match.definitive:
self._debug_log("Definitive match for '{}'.".format( self._debug_log("Definitive match for '{}'.".format(
self._keystring)) self._keystring))
@ -207,6 +209,8 @@ class BaseKeyParser(QObject):
self._debug_log("Giving up with '{}', no matches".format( self._debug_log("Giving up with '{}', no matches".format(
self._keystring)) self._keystring))
self.clear_keystring() self.clear_keystring()
elif match == self.Match.other:
pass
else: else:
raise AssertionError("Invalid match value {!r}".format(match)) raise AssertionError("Invalid match value {!r}".format(match))
return match return match
@ -223,6 +227,9 @@ class BaseKeyParser(QObject):
binding: - None with Match.partial/Match.none. binding: - None with Match.partial/Match.none.
- The found binding with Match.definitive. - The found binding with Match.definitive.
""" """
if not cmd_input:
# Only a count, no command yet, but we handled it
return (self.Match.other, None)
# A (cmd_input, binding) tuple (k, v of bindings) or None. # A (cmd_input, binding) tuple (k, v of bindings) or None.
definitive_match = None definitive_match = None
partial_match = False partial_match = False

View File

@ -437,7 +437,8 @@ class MainWindow(QWidget):
# commands # commands
keyparsers[usertypes.KeyMode.normal].keystring_updated.connect( keyparsers[usertypes.KeyMode.normal].keystring_updated.connect(
status.keystring.setText) status.keystring.setText)
cmd.got_cmd.connect(self._commandrunner.run_safely) cmd.got_cmd[str].connect(self._commandrunner.run_safely)
cmd.got_cmd[str, int].connect(self._commandrunner.run_safely)
cmd.returnPressed.connect(tabs.on_cmd_return_pressed) cmd.returnPressed.connect(tabs.on_cmd_return_pressed)
# key hint popup # key hint popup

View File

@ -28,7 +28,8 @@ import sip
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, Qt, QTimer, QDir, QModelIndex, from PyQt5.QtCore import (pyqtSlot, pyqtSignal, Qt, QTimer, QDir, QModelIndex,
QItemSelectionModel, QObject, QEventLoop) QItemSelectionModel, QObject, QEventLoop)
from PyQt5.QtWidgets import (QWidget, QGridLayout, QVBoxLayout, QLineEdit, from PyQt5.QtWidgets import (QWidget, QGridLayout, QVBoxLayout, QLineEdit,
QLabel, QFileSystemModel, QTreeView, QSizePolicy) QLabel, QFileSystemModel, QTreeView, QSizePolicy,
QSpacerItem)
from qutebrowser.browser import downloads from qutebrowser.browser import downloads
from qutebrowser.config import config from qutebrowser.config import config
@ -256,11 +257,21 @@ class PromptContainer(QWidget):
background-color: {{ conf.colors.prompts.bg }}; background-color: {{ conf.colors.prompts.bg }};
} }
QTreeView { QLineEdit {
selection-background-color: {{ conf.colors.prompts.selected.bg }}; border: {{ conf.colors.prompts.border }};
} }
QTreeView::item:selected, QTreeView::item:selected:hover { QTreeView {
selection-background-color: {{ conf.colors.prompts.selected.bg }};
border: {{ conf.colors.prompts.border }};
}
QTreeView::branch {
background-color: {{ conf.colors.prompts.bg }};
}
QTreeView::item:selected, QTreeView::item:selected:hover,
QTreeView::branch:selected {
background-color: {{ conf.colors.prompts.selected.bg }}; background-color: {{ conf.colors.prompts.selected.bg }};
} }
""" """
@ -433,7 +444,6 @@ class LineEdit(QLineEdit):
super().__init__(parent) super().__init__(parent)
self.setStyleSheet(""" self.setStyleSheet("""
QLineEdit { QLineEdit {
border: 1px solid grey;
background-color: transparent; background-color: transparent;
} }
""") """)
@ -511,6 +521,9 @@ class _BasePrompt(QWidget):
self._key_grid.addWidget(key_label, i, 0) self._key_grid.addWidget(key_label, i, 0)
self._key_grid.addWidget(text_label, i, 1) self._key_grid.addWidget(text_label, i, 1)
spacer = QSpacerItem(0, 0, QSizePolicy.Expanding)
self._key_grid.addItem(spacer, 0, 2)
self._vbox.addLayout(self._key_grid) self._vbox.addLayout(self._key_grid)
def accept(self, value=None): def accept(self, value=None):
@ -559,8 +572,7 @@ class FilenamePrompt(_BasePrompt):
def __init__(self, question, parent=None): def __init__(self, question, parent=None):
super().__init__(question, parent) super().__init__(question, parent)
self._init_texts(question) self._init_texts(question)
self._init_fileview() self._init_key_label()
self._set_fileview_root(question.default)
self._lineedit = LineEdit(self) self._lineedit = LineEdit(self)
if question.default: if question.default:
@ -569,7 +581,9 @@ class FilenamePrompt(_BasePrompt):
self._vbox.addWidget(self._lineedit) self._vbox.addWidget(self._lineedit)
self.setFocusProxy(self._lineedit) self.setFocusProxy(self._lineedit)
self._init_key_label()
self._init_fileview()
self._set_fileview_root(question.default)
if config.val.prompt.filebrowser: if config.val.prompt.filebrowser:
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)

View File

@ -38,7 +38,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
Signals: Signals:
got_cmd: Emitted when a command is triggered by the user. got_cmd: Emitted when a command is triggered by the user.
arg: The command string. arg: The command string and also potentially the count.
clear_completion_selection: Emitted before the completion widget is clear_completion_selection: Emitted before the completion widget is
hidden. hidden.
hide_completion: Emitted when the completion widget should be hidden. hide_completion: Emitted when the completion widget should be hidden.
@ -47,7 +47,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
hide_cmd: Emitted when command input can be hidden. hide_cmd: Emitted when command input can be hidden.
""" """
got_cmd = pyqtSignal(str) got_cmd = pyqtSignal([str], [str, int])
clear_completion_selection = pyqtSignal() clear_completion_selection = pyqtSignal()
hide_completion = pyqtSignal() hide_completion = pyqtSignal()
update_completion = pyqtSignal() update_completion = pyqtSignal()
@ -91,7 +91,9 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
@cmdutils.register(instance='status-command', name='set-cmd-text', @cmdutils.register(instance='status-command', name='set-cmd-text',
scope='window', maxsplit=0) scope='window', maxsplit=0)
def set_cmd_text_command(self, text, space=False, append=False): @cmdutils.argument('count', count=True)
def set_cmd_text_command(self, text, count=None, space=False, append=False,
run_on_count=False):
"""Preset the statusbar to some text. """Preset the statusbar to some text.
// //
@ -101,8 +103,11 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
Args: Args:
text: The commandline to set. text: The commandline to set.
count: The count if given.
space: If given, a space is added to the end. space: If given, a space is added to the end.
append: If given, the text is appended to the current text. append: If given, the text is appended to the current text.
run_on_count: If given with a count, the command is run with the
given count rather than setting the command text.
""" """
if space: if space:
text += ' ' text += ' '
@ -114,6 +119,9 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
if not text or text[0] not in modeparsers.STARTCHARS: if not text or text[0] not in modeparsers.STARTCHARS:
raise cmdexc.CommandError( raise cmdexc.CommandError(
"Invalid command text '{}'.".format(text)) "Invalid command text '{}'.".format(text))
if run_on_count and count is not None:
self.got_cmd[str, int].emit(text, count)
else:
self.set_cmd_text(text) self.set_cmd_text(text)
@cmdutils.register(instance='status-command', hide=True, @cmdutils.register(instance='status-command', hide=True,
@ -156,7 +164,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
text = self.text() text = self.text()
self.history.append(text) self.history.append(text)
modeman.leave(self._win_id, usertypes.KeyMode.command, 'cmd accept') modeman.leave(self._win_id, usertypes.KeyMode.command, 'cmd accept')
self.got_cmd.emit(prefixes[text[0]] + text[1:]) self.got_cmd[str].emit(prefixes[text[0]] + text[1:])
@pyqtSlot(usertypes.KeyMode) @pyqtSlot(usertypes.KeyMode)
def on_mode_left(self, mode): def on_mode_left(self, mode):

View File

@ -272,10 +272,6 @@ class TabbedBrowser(tabwidget.TabWidget):
if last_close == 'ignore' and count == 1: if last_close == 'ignore' and count == 1:
return return
# If we are removing a pinned tab, decrease count
if tab.data.pinned:
self.tabBar().pinned_count -= 1
self._remove_tab(tab, add_undo=add_undo) self._remove_tab(tab, add_undo=add_undo)
if count == 1: # We just closed the last tab above. if count == 1: # We just closed the last tab above.
@ -689,6 +685,7 @@ class TabbedBrowser(tabwidget.TabWidget):
self._update_tab_title(idx) self._update_tab_title(idx)
if idx == self.currentIndex(): if idx == self.currentIndex():
self._update_window_title() self._update_window_title()
tab.handle_auto_insert_mode(ok)
@pyqtSlot() @pyqtSlot()
def on_scroll_pos_changed(self): def on_scroll_pos_changed(self):

View File

@ -92,26 +92,16 @@ class TabWidget(QTabWidget):
bar.update(bar.tabRect(idx)) bar.update(bar.tabRect(idx))
def set_tab_pinned(self, tab: QWidget, def set_tab_pinned(self, tab: QWidget,
pinned: bool, *, loading: bool = False) -> None: pinned: bool) -> None:
"""Set the tab status as pinned. """Set the tab status as pinned.
Args: Args:
tab: The tab to pin tab: The tab to pin
pinned: Pinned tab state to set. pinned: Pinned tab state to set.
loading: Whether to ignore current data state when
counting pinned_count.
""" """
bar = self.tabBar() bar = self.tabBar()
idx = self.indexOf(tab) idx = self.indexOf(tab)
# Only modify pinned_count if we had a change
# always modify pinned_count if we are loading
if tab.data.pinned != pinned or loading:
if pinned:
bar.pinned_count += 1
elif not pinned:
bar.pinned_count -= 1
bar.set_tab_data(idx, 'pinned', pinned) bar.set_tab_data(idx, 'pinned', pinned)
tab.data.pinned = pinned tab.data.pinned = pinned
self._update_tab_title(idx) self._update_tab_title(idx)
@ -310,7 +300,6 @@ class TabBar(QTabBar):
self._on_show_switching_delay_changed() self._on_show_switching_delay_changed()
self.setAutoFillBackground(True) self.setAutoFillBackground(True)
self._set_colors() self._set_colors()
self.pinned_count = 0
QTimer.singleShot(0, self.maybe_hide) QTimer.singleShot(0, self.maybe_hide)
def __repr__(self): def __repr__(self):
@ -435,18 +424,25 @@ class TabBar(QTabBar):
return return
super().mousePressEvent(e) super().mousePressEvent(e)
def minimumTabSizeHint(self, index): def minimumTabSizeHint(self, index, ellipsis: bool = True):
"""Set the minimum tab size to indicator/icon/... text. """Set the minimum tab size to indicator/icon/... text.
Args: Args:
index: The index of the tab to get a size hint for. index: The index of the tab to get a size hint for.
ellipsis: Whether to use ellipsis to calculate width
instead of the tab's text.
Return: Return:
A QSize. A QSize of the smallest tab size we can make.
""" """
text = '\u2026' if ellipsis else self.tabText(index)
# Don't ever shorten if text is shorter than the ellipsis
text_width = min(self.fontMetrics().width(text),
self.fontMetrics().width(self.tabText(index)))
icon = self.tabIcon(index) icon = self.tabIcon(index)
padding = config.val.tabs.padding padding = config.val.tabs.padding
indicator_padding = config.val.tabs.indicator_padding
padding_h = padding.left + padding.right padding_h = padding.left + padding.right
padding_h += indicator_padding.left + indicator_padding.right
padding_v = padding.top + padding.bottom padding_v = padding.top + padding.bottom
if icon.isNull(): if icon.isNull():
icon_size = QSize(0, 0) icon_size = QSize(0, 0)
@ -454,15 +450,32 @@ class TabBar(QTabBar):
extent = self.style().pixelMetric(QStyle.PM_TabBarIconSize, None, extent = self.style().pixelMetric(QStyle.PM_TabBarIconSize, None,
self) self)
icon_size = icon.actualSize(QSize(extent, extent)) icon_size = icon.actualSize(QSize(extent, extent))
padding_h += self.style().pixelMetric(
PixelMetrics.icon_padding, None, self)
height = self.fontMetrics().height() + padding_v height = self.fontMetrics().height() + padding_v
width = (self.fontMetrics().width('\u2026') + icon_size.width() + width = (text_width + icon_size.width() +
padding_h + config.val.tabs.width.indicator) padding_h + config.val.tabs.width.indicator)
return QSize(width, height) return QSize(width, height)
def tabSizeHint(self, index): def _tab_total_width_pinned(self):
"""Override tabSizeHint so all tabs are the same size. """Get the current total width of pinned tabs.
This width is calculated assuming no shortening due to ellipsis."""
return sum(self.minimumTabSizeHint(idx, ellipsis=False).width()
for idx in range(self.count())
if self._tab_pinned(idx))
def _pinnedCount(self) -> int:
"""Get the number of pinned tabs."""
return sum(self._tab_pinned(idx) for idx in range(self.count()))
def _tab_pinned(self, index: int) -> bool:
"""Return True if tab is pinned."""
try:
return self.tab_data(index, 'pinned')
except KeyError:
return False
def tabSizeHint(self, index: int):
"""Override tabSizeHint to customize qb's tab size.
https://wiki.python.org/moin/PyQt/Customising%20tab%20bars https://wiki.python.org/moin/PyQt/Customising%20tab%20bars
@ -490,43 +503,17 @@ class TabBar(QTabBar):
# want to ensure it's valid in this special case. # want to ensure it's valid in this special case.
return QSize() return QSize()
else: else:
try: pinned = self._tab_pinned(index)
pinned = self.tab_data(index, 'pinned') no_pinned_count = self.count() - self._pinnedCount()
except KeyError: pinned_width = self._tab_total_width_pinned()
pinned = False
no_pinned_count = self.count() - self.pinned_count
pinned_width = config.val.tabs.width.pinned * self.pinned_count
no_pinned_width = self.width() - pinned_width no_pinned_width = self.width() - pinned_width
if pinned: if pinned:
size = QSize(config.val.tabs.width.pinned, height) # Give pinned tabs the minimum size they need to display their
qtutils.ensure_valid(size) # titles, let Qt handle scaling it down if we get too small.
return size width = self.minimumTabSizeHint(index, ellipsis=False).width()
# If we *do* have enough space, tabs should occupy the whole window
# width. If there are pinned tabs their size will be subtracted
# from the total window width.
# During shutdown the self.count goes down,
# but the self.pinned_count not - this generates some odd behavior.
# To avoid this we compare self.count against self.pinned_count.
if self.pinned_count > 0 and self.count() > self.pinned_count:
pinned_width = config.val.tabs.width.pinned * self.pinned_count
no_pinned_width = self.width() - pinned_width
width = no_pinned_width / (self.count() - self.pinned_count)
else: else:
# Tabs should attempt to occupy the whole window width. If
# there are pinned tabs their size will be subtracted from the
# total window width. During shutdown the self.count goes
# down, but the self.pinned_count not - this generates some odd
# behavior. To avoid this we compare self.count against
# self.pinned_count. If we end up having too little space, we
# set the minimum size below.
if self.pinned_count > 0 and no_pinned_count > 0:
width = no_pinned_width / no_pinned_count width = no_pinned_width / no_pinned_count
else:
width = self.width() / self.count()
# If no_pinned_width is not divisible by no_pinned_count, add a # If no_pinned_width is not divisible by no_pinned_count, add a
# pixel to some tabs so that there is no ugly leftover space. # pixel to some tabs so that there is no ugly leftover space.

View File

@ -0,0 +1,335 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2017 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/>.
"""Dialogs shown when there was a problem with a backend choice."""
import os
import sys
import functools
import html
import attr
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (QApplication, QDialog, QPushButton, QHBoxLayout,
QVBoxLayout, QLabel, QMessageBox)
from PyQt5.QtNetwork import QSslSocket
from qutebrowser.config import config
from qutebrowser.utils import usertypes, objreg, version, qtutils, log
from qutebrowser.misc import objects, msgbox
_Result = usertypes.enum(
'_Result',
['quit', 'restart', 'restart_webkit', 'restart_webengine'],
is_int=True, start=QDialog.Accepted + 1)
@attr.s
class _Button:
"""A button passed to BackendProblemDialog."""
text = attr.ib()
setting = attr.ib()
value = attr.ib()
default = attr.ib(default=False)
def _other_backend(backend):
"""Get the other backend enum/setting for a given backend."""
other_backend = {
usertypes.Backend.QtWebKit: usertypes.Backend.QtWebEngine,
usertypes.Backend.QtWebEngine: usertypes.Backend.QtWebKit,
}[backend]
other_setting = other_backend.name.lower()[2:]
return (other_backend, other_setting)
def _error_text(because, text, backend):
"""Get an error text for the given information."""
other_backend, other_setting = _other_backend(backend)
return ("<b>Failed to start with the {backend} backend!</b>"
"<p>qutebrowser tried to start with the {backend} backend but "
"failed because {because}.</p>{text}"
"<p><b>Forcing the {other_backend.name} backend</b></p>"
"<p>This forces usage of the {other_backend.name} backend by "
"setting the <i>backend = '{other_setting}'</i> option "
"(if you have a <i>config.py</i> file, you'll need to set "
"this manually).</p>".format(
backend=backend.name, because=because, text=text,
other_backend=other_backend, other_setting=other_setting))
class _Dialog(QDialog):
"""A dialog which gets shown if there are issues with the backend."""
def __init__(self, because, text, backend, buttons=None, parent=None):
super().__init__(parent)
vbox = QVBoxLayout(self)
other_backend, other_setting = _other_backend(backend)
text = _error_text(because, text, backend)
label = QLabel(text, wordWrap=True)
label.setTextFormat(Qt.RichText)
vbox.addWidget(label)
hbox = QHBoxLayout()
buttons = [] if buttons is None else buttons
quit_button = QPushButton("Quit")
quit_button.clicked.connect(lambda: self.done(_Result.quit))
hbox.addWidget(quit_button)
backend_button = QPushButton("Force {} backend".format(
other_backend.name))
backend_button.clicked.connect(functools.partial(
self._change_setting, 'backend', other_setting))
hbox.addWidget(backend_button)
for button in buttons:
btn = QPushButton(button.text, default=button.default)
btn.clicked.connect(functools.partial(
self._change_setting, button.setting, button.value))
hbox.addWidget(btn)
vbox.addLayout(hbox)
def _change_setting(self, setting, value):
"""Change the given setting and restart."""
config.instance.set_obj(setting, value, save_yaml=True)
save_manager = objreg.get('save-manager')
save_manager.save_all(is_exit=True)
if setting == 'backend' and value == 'webkit':
self.done(_Result.restart_webkit)
elif setting == 'backend' and value == 'webengine':
self.done(_Result.restart_webengine)
else:
self.done(_Result.restart)
def _show_dialog(*args, **kwargs):
"""Show a dialog for a backend problem."""
cmd_args = objreg.get('args')
if cmd_args.no_err_windows:
text = _error_text(*args, **kwargs)
print(text, file=sys.stderr)
sys.exit(usertypes.Exit.err_init)
dialog = _Dialog(*args, **kwargs)
status = dialog.exec_()
quitter = objreg.get('quitter')
if status in [_Result.quit, QDialog.Rejected]:
pass
elif status == _Result.restart_webkit:
quitter.restart(override_args={'backend': 'webkit'})
elif status == _Result.restart_webengine:
quitter.restart(override_args={'backend': 'webengine'})
elif status == _Result.restart:
quitter.restart()
else:
assert False, status
sys.exit(usertypes.Exit.err_init)
def _handle_nouveau_graphics():
assert objects.backend == usertypes.Backend.QtWebEngine, objects.backend
if version.opengl_vendor() != 'nouveau':
return
if (os.environ.get('LIBGL_ALWAYS_SOFTWARE') == '1' or
'QT_XCB_FORCE_SOFTWARE_OPENGL' in os.environ):
return
button = _Button("Force software rendering", 'force_software_rendering',
True)
_show_dialog(
backend=usertypes.Backend.QtWebEngine,
because="you're using Nouveau graphics",
text="<p>There are two ways to fix this:</p>"
"<p><b>Forcing software rendering</b></p>"
"<p>This allows you to use the newer QtWebEngine backend (based "
"on Chromium) but could have noticable performance impact "
"(depending on your hardware). "
"This sets the <i>force_software_rendering = True</i> option "
"(if you have a <i>config.py</i> file, you'll need to set this "
"manually).</p>",
buttons=[button],
)
# Should never be reached
assert False
def _handle_wayland():
assert objects.backend == usertypes.Backend.QtWebEngine, objects.backend
platform = QApplication.instance().platformName()
if platform not in ['wayland', 'wayland-egl']:
return
_show_dialog(
backend=usertypes.Backend.QtWebEngine,
because="you're using Wayland",
text="<p>There are two ways to fix this:</p>"
"<p><b>Set up XWayland</b></p>"
"<p>This allows you to use the newer QtWebEngine backend (based "
"on Chromium). "
)
# Should never be reached
assert False
@attr.s
class BackendImports:
"""Whether backend modules could be imported."""
webkit_available = attr.ib(default=None)
webengine_available = attr.ib(default=None)
webkit_error = attr.ib(default=None)
webengine_error = attr.ib(default=None)
def _try_import_backends():
"""Check whether backends can be imported and return BackendImports."""
# pylint: disable=unused-variable
results = BackendImports()
try:
from PyQt5 import QtWebKit
from PyQt5 import QtWebKitWidgets
except ImportError as e:
results.webkit_available = False
results.webkit_error = str(e)
else:
if qtutils.is_new_qtwebkit():
results.webkit_available = True
else:
results.webkit_available = False
results.webkit_error = "Unsupported legacy QtWebKit found"
try:
from PyQt5 import QtWebEngineWidgets
except ImportError as e:
results.webengine_available = False
results.webengine_error = str(e)
else:
results.webengine_available = True
assert results.webkit_available is not None
assert results.webengine_available is not None
if not results.webkit_available:
assert results.webkit_error is not None
if not results.webengine_available:
assert results.webengine_error is not None
return results
def _handle_ssl_support(fatal=False):
"""Check for full SSL availability.
If "fatal" is given, show an error and exit.
"""
text = ("Could not initialize QtNetwork SSL support. If you use "
"OpenSSL 1.1 with a PyQt package from PyPI (e.g. on Archlinux "
"or Debian Stretch), you need to set LD_LIBRARY_PATH to the path "
"of OpenSSL 1.0. This only affects downloads.")
if QSslSocket.supportsSsl():
return
if fatal:
errbox = msgbox.msgbox(parent=None,
title="SSL error",
text="Could not initialize SSL support.",
icon=QMessageBox.Critical,
plain_text=False)
errbox.exec_()
sys.exit(usertypes.Exit.err_init)
assert not fatal
log.init.warning(text)
def _check_backend_modules():
"""Check for the modules needed for QtWebKit/QtWebEngine."""
imports = _try_import_backends()
if imports.webkit_available and imports.webengine_available:
return
elif not imports.webkit_available and not imports.webengine_available:
text = ("<p>qutebrowser needs QtWebKit or QtWebEngine, but neither "
"could be imported!</p>"
"<p>The errors encountered were:<ul>"
"<li><b>QtWebKit:</b> {webkit_error}"
"<li><b>QtWebEngine:</b> {webengine_error}"
"</ul></p>".format(
webkit_error=html.escape(imports.webkit_error),
webengine_error=html.escape(imports.webengine_error)))
errbox = msgbox.msgbox(parent=None,
title="No backend library found!",
text=text,
icon=QMessageBox.Critical,
plain_text=False)
errbox.exec_()
sys.exit(usertypes.Exit.err_init)
elif objects.backend == usertypes.Backend.QtWebKit:
if imports.webkit_available:
return
assert imports.webengine_available
_show_dialog(
backend=usertypes.Backend.QtWebKit,
because="QtWebKit could not be imported",
text="<p><b>The error encountered was:</b><br/>{}</p>".format(
html.escape(imports.webkit_error))
)
elif objects.backend == usertypes.Backend.QtWebEngine:
if imports.webengine_available:
return
assert imports.webkit_available
_show_dialog(
backend=usertypes.Backend.QtWebEngine,
because="QtWebEngine could not be imported",
text="<p><b>The error encountered was:</b><br/>{}</p>".format(
html.escape(imports.webengine_error))
)
# Should never be reached
assert False
def init():
_check_backend_modules()
if objects.backend == usertypes.Backend.QtWebEngine:
_handle_ssl_support()
_handle_wayland()
_handle_nouveau_graphics()
else:
assert objects.backend == usertypes.Backend.QtWebKit, objects.backend
_handle_ssl_support(fatal=True)

View File

@ -32,15 +32,19 @@ import pkg_resources
from PyQt5.QtCore import pyqtSlot, Qt, QSize from PyQt5.QtCore import pyqtSlot, Qt, QSize
from PyQt5.QtWidgets import (QDialog, QLabel, QTextEdit, QPushButton, from PyQt5.QtWidgets import (QDialog, QLabel, QTextEdit, QPushButton,
QVBoxLayout, QHBoxLayout, QCheckBox, QVBoxLayout, QHBoxLayout, QCheckBox,
QDialogButtonBox, QMessageBox, QApplication) QDialogButtonBox, QApplication)
import qutebrowser import qutebrowser
from qutebrowser.utils import version, log, utils, objreg, usertypes from qutebrowser.utils import version, log, utils, objreg, usertypes
from qutebrowser.misc import (miscwidgets, autoupdate, msgbox, httpclient, from qutebrowser.misc import (miscwidgets, autoupdate, msgbox, httpclient,
pastebin, objects) pastebin)
from qutebrowser.config import config, configfiles from qutebrowser.config import config, configfiles
Result = usertypes.enum('Result', ['restore', 'no_restore'], is_int=True,
start=QDialog.Accepted + 1)
def parse_fatal_stacktrace(text): def parse_fatal_stacktrace(text):
"""Get useful information from a fatal faulthandler stacktrace. """Get useful information from a fatal faulthandler stacktrace.
@ -65,41 +69,6 @@ def parse_fatal_stacktrace(text):
return (m.group(1), m.group(3)) return (m.group(1), m.group(3))
def get_fatal_crash_dialog(debug, data):
"""Get a fatal crash dialog based on a crash log.
If the crash is a segfault in qt_mainloop and we're on an old Qt version
this is a simple error dialog which lets the user know they should upgrade
if possible.
If it's anything else, it's a normal FatalCrashDialog with the possibility
to report the crash.
Args:
debug: Whether the debug flag (--debug) was given.
data: The crash log data.
"""
ignored_frames = ['qt_mainloop', 'paintEvent']
errtype, frame = parse_fatal_stacktrace(data)
if (errtype == 'Segmentation fault' and
frame in ignored_frames and
objects.backend == usertypes.Backend.QtWebKit):
title = "qutebrowser was restarted after a fatal crash!"
text = ("<b>qutebrowser was restarted after a fatal crash!</b><br/>"
"Unfortunately, this crash occurred in Qt (the library "
"qutebrowser uses), and QtWebKit (the current backend) is not "
"maintained anymore.<br/><br/>Since I can't do much about "
"those crashes I disabled the crash reporter for this case, "
"but this will likely be resolved in the future with the new "
"QtWebEngine backend.")
box = QMessageBox(QMessageBox.Critical, title, text, QMessageBox.Ok)
box.setAttribute(Qt.WA_DeleteOnClose)
return box
else:
return FatalCrashDialog(debug, data)
def _get_environment_vars(): def _get_environment_vars():
"""Gather environment variables for the crash info.""" """Gather environment variables for the crash info."""
masks = ('DESKTOP_SESSION', 'DE', 'QT_*', 'PYTHON*', 'LC_*', 'LANG', masks = ('DESKTOP_SESSION', 'DE', 'QT_*', 'PYTHON*', 'LC_*', 'LANG',
@ -478,9 +447,9 @@ class ExceptionCrashDialog(_CrashDialog):
def finish(self): def finish(self):
self._save_contact_info() self._save_contact_info()
if self._chk_restore.isChecked(): if self._chk_restore.isChecked():
self.accept() self.done(Result.restore)
else: else:
self.reject() self.done(Result.no_restore)
class FatalCrashDialog(_CrashDialog): class FatalCrashDialog(_CrashDialog):

View File

@ -36,10 +36,10 @@ except ImportError:
import attr import attr
from PyQt5.QtCore import (pyqtSlot, qInstallMessageHandler, QObject, from PyQt5.QtCore import (pyqtSlot, qInstallMessageHandler, QObject,
QSocketNotifier, QTimer, QUrl) QSocketNotifier, QTimer, QUrl)
from PyQt5.QtWidgets import QApplication, QDialog from PyQt5.QtWidgets import QApplication
from qutebrowser.commands import cmdutils from qutebrowser.commands import cmdutils
from qutebrowser.misc import earlyinit, crashdialog from qutebrowser.misc import earlyinit, crashdialog, ipc
from qutebrowser.utils import usertypes, standarddir, log, objreg, debug, utils from qutebrowser.utils import usertypes, standarddir, log, objreg, debug, utils
@ -94,7 +94,7 @@ class CrashHandler(QObject):
if data: if data:
# Crashlog exists and has data in it, so something crashed # Crashlog exists and has data in it, so something crashed
# previously. # previously.
self._crash_dialog = crashdialog.get_fatal_crash_dialog( self._crash_dialog = crashdialog.FatalCrashDialog(
self._args.debug, data) self._args.debug, data)
self._crash_dialog.show() self._crash_dialog.show()
else: else:
@ -236,7 +236,7 @@ class CrashHandler(QObject):
info = self._get_exception_info() info = self._get_exception_info()
try: try:
objreg.get('ipc-server').ignored = True ipc.server.ignored = True
except Exception: except Exception:
log.destroy.exception("Error while ignoring ipc") log.destroy.exception("Error while ignoring ipc")
@ -258,7 +258,7 @@ class CrashHandler(QObject):
self._args.debug, info.pages, info.cmd_history, exc, self._args.debug, info.pages, info.cmd_history, exc,
info.objects) info.objects)
ret = self._crash_dialog.exec_() ret = self._crash_dialog.exec_()
if ret == QDialog.Accepted: # restore if ret == crashdialog.Result.restore:
self._quitter.restart(info.pages) self._quitter.restart(info.pages)
# We might risk a segfault here, but that's better than continuing to # We might risk a segfault here, but that's better than continuing to

View File

@ -47,33 +47,25 @@ except ImportError:
START_TIME = datetime.datetime.now() START_TIME = datetime.datetime.now()
def _missing_str(name, *, windows=None, pip=None, webengine=False): def _missing_str(name, *, webengine=False):
"""Get an error string for missing packages. """Get an error string for missing packages.
Args: Args:
name: The name of the package. name: The name of the package.
windows: String to be displayed for Windows.
pip: pypi package name.
webengine: Whether this is checking the QtWebEngine package webengine: Whether this is checking the QtWebEngine package
""" """
blocks = ["Fatal error: <b>{}</b> is required to run qutebrowser but " blocks = ["Fatal error: <b>{}</b> is required to run qutebrowser but "
"could not be imported! Maybe it's not installed?".format(name), "could not be imported! Maybe it's not installed?".format(name),
"<b>The error encountered was:</b><br />%ERROR%"] "<b>The error encountered was:</b><br />%ERROR%"]
lines = ['Please search for the python3 version of {} in your ' lines = ['Please search for the python3 version of {} in your '
'distributions packages, or install it via pip.'.format(name)] 'distributions packages, or see '
'https://github.com/qutebrowser/qutebrowser/blob/master/doc/install.asciidoc'
.format(name)]
blocks.append('<br />'.join(lines)) blocks.append('<br />'.join(lines))
if not webengine: if not webengine:
lines = ['<b>If you installed a qutebrowser package for your ' lines = ['<b>If you installed a qutebrowser package for your '
'distribution, please report this as a bug.</b>'] 'distribution, please report this as a bug.</b>']
blocks.append('<br />'.join(lines)) blocks.append('<br />'.join(lines))
if windows is not None:
lines = ["<b>On Windows:</b>"]
lines += windows.splitlines()
blocks.append('<br />'.join(lines))
if pip is not None:
lines = ["<b>Using pip:</b>"]
lines.append("pip3 install {}".format(pip))
blocks.append('<br />'.join(lines))
return '<br /><br />'.join(blocks) return '<br /><br />'.join(blocks)
@ -142,11 +134,7 @@ def check_pyqt_core():
try: try:
import PyQt5.QtCore # pylint: disable=unused-variable import PyQt5.QtCore # pylint: disable=unused-variable
except ImportError as e: except ImportError as e:
text = _missing_str('PyQt5', text = _missing_str('PyQt5')
windows="Use the installer by Riverbank computing "
"or the standalone qutebrowser exe.<br />"
"http://www.riverbankcomputing.co.uk/"
"software/pyqt/download5")
text = text.replace('<b>', '') text = text.replace('<b>', '')
text = text.replace('</b>', '') text = text.replace('</b>', '')
text = text.replace('<br />', '\n') text = text.replace('<br />', '\n')
@ -199,23 +187,6 @@ def check_ssl_support():
_die("Fatal error: Your Qt is built without SSL support.") _die("Fatal error: Your Qt is built without SSL support.")
def check_backend_ssl_support(backend):
"""Check for full SSL availability when we know the backend."""
from PyQt5.QtNetwork import QSslSocket
from qutebrowser.utils import log, usertypes
text = ("Could not initialize QtNetwork SSL support. If you use "
"OpenSSL 1.1 with a PyQt package from PyPI (e.g. on Archlinux "
"or Debian Stretch), you need to set LD_LIBRARY_PATH to the path "
"of OpenSSL 1.0. This only affects downloads.")
if not QSslSocket.supportsSsl():
if backend == usertypes.Backend.QtWebKit:
_die("Could not initialize SSL support.")
else:
assert backend == usertypes.Backend.QtWebEngine
log.init.warning(text)
def _check_modules(modules): def _check_modules(modules):
"""Make sure the given modules are available.""" """Make sure the given modules are available."""
from qutebrowser.utils import log from qutebrowser.utils import log
@ -230,7 +201,14 @@ def _check_modules(modules):
'Flags not at the start of the expression'] 'Flags not at the start of the expression']
with log.ignore_py_warnings( with log.ignore_py_warnings(
category=DeprecationWarning, category=DeprecationWarning,
message=r'({})'.format('|'.join(messages))): message=r'({})'.format('|'.join(messages))
), log.ignore_py_warnings(
category=PendingDeprecationWarning,
module='imp'
), log.ignore_py_warnings(
category=ImportWarning,
message=r'Not importing directory .*: missing __init__'
):
importlib.import_module(name) importlib.import_module(name)
except ImportError as e: except ImportError as e:
_die(text, e) _die(text, e)
@ -239,31 +217,12 @@ def _check_modules(modules):
def check_libraries(): def check_libraries():
"""Check if all needed Python libraries are installed.""" """Check if all needed Python libraries are installed."""
modules = { modules = {
'pkg_resources': 'pkg_resources': _missing_str("pkg_resources/setuptools"),
_missing_str("pkg_resources/setuptools", 'pypeg2': _missing_str("pypeg2"),
windows="Run python -m ensurepip."), 'jinja2': _missing_str("jinja2"),
'pypeg2': 'pygments': _missing_str("pygments"),
_missing_str("pypeg2", 'yaml': _missing_str("PyYAML"),
pip="pypeg2"), 'attr': _missing_str("attrs"),
'jinja2':
_missing_str("jinja2",
windows="Install from http://www.lfd.uci.edu/"
"~gohlke/pythonlibs/#jinja2 or via pip.",
pip="jinja2"),
'pygments':
_missing_str("pygments",
windows="Install from http://www.lfd.uci.edu/"
"~gohlke/pythonlibs/#pygments or via pip.",
pip="pygments"),
'yaml':
_missing_str("PyYAML",
windows="Use the installers at "
"http://pyyaml.org/download/pyyaml/ (py3.4) "
"or Install via pip.",
pip="PyYAML"),
'attr':
_missing_str("attrs",
pip="attrs"),
'PyQt5.QtQml': _missing_str("PyQt5.QtQml"), 'PyQt5.QtQml': _missing_str("PyQt5.QtQml"),
'PyQt5.QtSql': _missing_str("PyQt5.QtSql"), 'PyQt5.QtSql': _missing_str("PyQt5.QtSql"),
'PyQt5.QtOpenGL': _missing_str("PyQt5.QtOpenGL"), 'PyQt5.QtOpenGL': _missing_str("PyQt5.QtOpenGL"),
@ -271,35 +230,6 @@ def check_libraries():
_check_modules(modules) _check_modules(modules)
def check_backend_libraries(backend):
"""Make sure the libraries needed by the given backend are available.
Args:
backend: The backend as usertypes.Backend member.
"""
from qutebrowser.utils import usertypes
if backend == usertypes.Backend.QtWebEngine:
modules = {
'PyQt5.QtWebEngineWidgets':
_missing_str("QtWebEngine", webengine=True),
}
else:
assert backend == usertypes.Backend.QtWebKit, backend
modules = {
'PyQt5.QtWebKit': _missing_str("PyQt5.QtWebKit"),
'PyQt5.QtWebKitWidgets': _missing_str("PyQt5.QtWebKitWidgets"),
}
_check_modules(modules)
def check_new_webkit(backend):
"""Make sure we use QtWebEngine or a new QtWebKit."""
from qutebrowser.utils import usertypes, qtutils
if backend == usertypes.Backend.QtWebKit and not qtutils.is_new_qtwebkit():
_die("qutebrowser does not support legacy QtWebKit versions anymore, "
"see the installation docs for details.")
def remove_inputhook(): def remove_inputhook():
"""Remove the PyQt input hook. """Remove the PyQt input hook.
@ -352,16 +282,3 @@ def early_init(args):
remove_inputhook() remove_inputhook()
check_ssl_support() check_ssl_support()
check_optimize_flag() check_optimize_flag()
def init_with_backend(backend):
"""Do later stages of init when we know the backend.
Args:
backend: The backend as usertypes.Backend member.
"""
assert not isinstance(backend, str), backend
assert backend is not None
check_backend_libraries(backend)
check_backend_ssl_support(backend)
check_new_webkit(backend)

View File

@ -35,8 +35,9 @@ class ExternalEditor(QObject):
Attributes: Attributes:
_text: The current text before the editor is opened. _text: The current text before the editor is opened.
_file: The file handle as tempfile.NamedTemporaryFile. Note that this _filename: The name of the file to be edited.
handle will be closed after the initial file has been created. _remove_file: Whether the file should be removed when the editor is
closed.
_proc: The GUIProcess of the editor. _proc: The GUIProcess of the editor.
""" """
@ -44,18 +45,20 @@ class ExternalEditor(QObject):
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
self._text = None self._filename = None
self._file = None
self._proc = None self._proc = None
self._remove_file = None
def _cleanup(self): def _cleanup(self):
"""Clean up temporary files after the editor closed.""" """Clean up temporary files after the editor closed."""
if self._file is None: assert self._remove_file is not None
if self._filename is None or not self._remove_file:
# Could not create initial file. # Could not create initial file.
return return
try: try:
if self._proc.exit_status() != QProcess.CrashExit: if self._proc.exit_status() != QProcess.CrashExit:
os.remove(self._file.name) os.remove(self._filename)
except OSError as e: except OSError as e:
# NOTE: Do not replace this with "raise CommandError" as it's # NOTE: Do not replace this with "raise CommandError" as it's
# executed async. # executed async.
@ -77,7 +80,7 @@ class ExternalEditor(QObject):
return return
encoding = config.val.editor.encoding encoding = config.val.editor.encoding
try: try:
with open(self._file.name, 'r', encoding=encoding) as f: with open(self._filename, 'r', encoding=encoding) as f:
text = f.read() text = f.read()
except OSError as e: except OSError as e:
# NOTE: Do not replace this with "raise CommandError" as it's # NOTE: Do not replace this with "raise CommandError" as it's
@ -99,9 +102,8 @@ class ExternalEditor(QObject):
Args: Args:
text: The initial text to edit. text: The initial text to edit.
""" """
if self._text is not None: if self._filename is not None:
raise ValueError("Already editing a file!") raise ValueError("Already editing a file!")
self._text = text
try: try:
# Close while the external process is running, as otherwise systems # Close while the external process is running, as otherwise systems
# with exclusive write access (e.g. Windows) may fail to update # with exclusive write access (e.g. Windows) may fail to update
@ -113,15 +115,27 @@ class ExternalEditor(QObject):
delete=False) as fobj: delete=False) as fobj:
if text: if text:
fobj.write(text) fobj.write(text)
self._file = fobj self._filename = fobj.name
except OSError as e: except OSError as e:
message.error("Failed to create initial file: {}".format(e)) message.error("Failed to create initial file: {}".format(e))
return return
self._remove_file = True
self._start_editor()
def edit_file(self, filename):
"""Edit the file with the given filename."""
self._filename = filename
self._remove_file = False
self._start_editor()
def _start_editor(self):
"""Start the editor with the file opened as self._filename."""
self._proc = guiprocess.GUIProcess(what='editor', parent=self) self._proc = guiprocess.GUIProcess(what='editor', parent=self)
self._proc.finished.connect(self.on_proc_closed) self._proc.finished.connect(self.on_proc_closed)
self._proc.error.connect(self.on_proc_error) self._proc.error.connect(self.on_proc_error)
editor = config.val.editor.command editor = config.val.editor.command
executable = editor[0] executable = editor[0]
args = [arg.replace('{}', self._file.name) for arg in editor[1:]] args = [arg.replace('{}', self._filename) for arg in editor[1:]]
log.procs.debug("Calling \"{}\" with args {}".format(executable, args)) log.procs.debug("Calling \"{}\" with args {}".format(executable, args))
self._proc.start(executable, args) self._proc.start(executable, args)

View File

@ -30,7 +30,7 @@ from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, Qt
from PyQt5.QtNetwork import QLocalSocket, QLocalServer, QAbstractSocket from PyQt5.QtNetwork import QLocalSocket, QLocalServer, QAbstractSocket
import qutebrowser import qutebrowser
from qutebrowser.utils import log, usertypes, error, objreg, standarddir, utils from qutebrowser.utils import log, usertypes, error, standarddir, utils
CONNECT_TIMEOUT = 100 # timeout for connecting/disconnecting CONNECT_TIMEOUT = 100 # timeout for connecting/disconnecting
@ -40,6 +40,10 @@ ATIME_INTERVAL = 60 * 60 * 3 * 1000 # 3 hours
PROTOCOL_VERSION = 1 PROTOCOL_VERSION = 1
# The ipc server instance
server = None
def _get_socketname_windows(basedir): def _get_socketname_windows(basedir):
"""Get a socketname to use for Windows.""" """Get a socketname to use for Windows."""
parts = ['qutebrowser', getpass.getuser()] parts = ['qutebrowser', getpass.getuser()]
@ -109,15 +113,15 @@ class ListenError(Error):
message: The error message. message: The error message.
""" """
def __init__(self, server): def __init__(self, local_server):
"""Constructor. """Constructor.
Args: Args:
server: The QLocalServer which has the error set. local_server: The QLocalServer which has the error set.
""" """
super().__init__() super().__init__()
self.code = server.serverError() self.code = local_server.serverError()
self.message = server.errorString() self.message = local_server.errorString()
def __str__(self): def __str__(self):
return "Error while listening to IPC server: {} (error {})".format( return "Error while listening to IPC server: {} (error {})".format(
@ -482,6 +486,7 @@ def send_or_listen(args):
The IPCServer instance if no running instance was detected. The IPCServer instance if no running instance was detected.
None if an instance was running and received our request. None if an instance was running and received our request.
""" """
global server
socketname = _get_socketname(args.basedir) socketname = _get_socketname(args.basedir)
try: try:
try: try:
@ -492,7 +497,6 @@ def send_or_listen(args):
log.init.debug("Starting IPC server...") log.init.debug("Starting IPC server...")
server = IPCServer(socketname) server = IPCServer(socketname)
server.listen() server.listen()
objreg.register('ipc-server', server)
return server return server
except AddressInUseError as e: except AddressInUseError as e:
# This could be a race condition... # This could be a race condition...

View File

@ -26,12 +26,14 @@ It is intended to help discoverability of keybindings.
import html import html
import fnmatch import fnmatch
import re
from PyQt5.QtWidgets import QLabel, QSizePolicy from PyQt5.QtWidgets import QLabel, QSizePolicy
from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.utils import utils, usertypes from qutebrowser.utils import utils, usertypes
from qutebrowser.commands import cmdutils
class KeyHintView(QLabel): class KeyHintView(QLabel):
@ -85,6 +87,7 @@ class KeyHintView(QLabel):
Args: Args:
prefix: The current partial keystring. prefix: The current partial keystring.
""" """
countstr, prefix = re.match(r'^(\d*)(.*)', prefix).groups()
if not prefix: if not prefix:
self._show_timer.stop() self._show_timer.stop()
self.hide() self.hide()
@ -94,11 +97,18 @@ class KeyHintView(QLabel):
return any(fnmatch.fnmatchcase(keychain, glob) return any(fnmatch.fnmatchcase(keychain, glob)
for glob in config.val.keyhint.blacklist) for glob in config.val.keyhint.blacklist)
def takes_count(cmdstr):
"""Return true iff this command can take a count argument."""
cmdname = cmdstr.split(' ')[0]
cmd = cmdutils.cmd_dict.get(cmdname)
return cmd and cmd.takes_count()
bindings_dict = config.key_instance.get_bindings_for(modename) bindings_dict = config.key_instance.get_bindings_for(modename)
bindings = [(k, v) for (k, v) in sorted(bindings_dict.items()) bindings = [(k, v) for (k, v) in sorted(bindings_dict.items())
if k.startswith(prefix) and if k.startswith(prefix) and
not utils.is_special_key(k) and not utils.is_special_key(k) and
not blacklisted(k)] not blacklisted(k) and
(takes_count(v) or not countstr)]
if not bindings: if not bindings:
self._show_timer.stop() self._show_timer.stop()

View File

@ -19,10 +19,21 @@
"""Convenience functions to show message boxes.""" """Convenience functions to show message boxes."""
import sys
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QMessageBox from PyQt5.QtWidgets import QMessageBox
from qutebrowser.utils import objreg
class DummyBox:
"""A dummy QMessageBox returned when --no-err-windows is used."""
def exec_(self):
pass
def msgbox(parent, title, text, *, icon, buttons=QMessageBox.Ok, def msgbox(parent, title, text, *, icon, buttons=QMessageBox.Ok,
on_finished=None, plain_text=None): on_finished=None, plain_text=None):
@ -40,6 +51,11 @@ def msgbox(parent, title, text, *, icon, buttons=QMessageBox.Ok,
Return: Return:
A new QMessageBox. A new QMessageBox.
""" """
args = objreg.get('args')
if args.no_err_windows:
print('Message box: {}; {}'.format(title, text), file=sys.stderr)
return DummyBox()
box = QMessageBox(parent) box = QMessageBox(parent)
box.setAttribute(Qt.WA_DeleteOnClose) box.setAttribute(Qt.WA_DeleteOnClose)
box.setIcon(icon) box.setIcon(icon)

View File

@ -164,6 +164,11 @@ class SaveManager(QObject):
self.saveables[name].save(is_exit=is_exit, explicit=explicit, self.saveables[name].save(is_exit=is_exit, explicit=explicit,
silent=silent, force=force) silent=silent, force=force)
def save_all(self, *args, **kwargs):
"""Save all saveables."""
for saveable in self.saveables:
self.save(saveable, *args, **kwargs)
@pyqtSlot() @pyqtSlot()
def autosave(self): def autosave(self):
"""Slot used when the configs are auto-saved.""" """Slot used when the configs are auto-saved."""

View File

@ -393,8 +393,7 @@ class SessionManager(QObject):
if tab.get('active', False): if tab.get('active', False):
tab_to_focus = i tab_to_focus = i
if new_tab.data.pinned: if new_tab.data.pinned:
tabbed_browser.set_tab_pinned( tabbed_browser.set_tab_pinned(new_tab, new_tab.data.pinned)
new_tab, new_tab.data.pinned, loading=True)
if tab_to_focus is not None: if tab_to_focus is not None:
tabbed_browser.setCurrentIndex(tab_to_focus) tabbed_browser.setCurrentIndex(tab_to_focus)
if win.get('active', False): if win.get('active', False):

View File

@ -22,12 +22,12 @@
import collections import collections
from PyQt5.QtCore import QObject, pyqtSignal from PyQt5.QtCore import QObject, pyqtSignal
from PyQt5.QtSql import QSqlDatabase, QSqlQuery from PyQt5.QtSql import QSqlDatabase, QSqlQuery, QSqlError
from qutebrowser.utils import log from qutebrowser.utils import log, debug
class SqlException(Exception): class SqlError(Exception):
"""Raised on an error interacting with the SQL database.""" """Raised on an error interacting with the SQL database."""
@ -38,12 +38,14 @@ def init(db_path):
"""Initialize the SQL database connection.""" """Initialize the SQL database connection."""
database = QSqlDatabase.addDatabase('QSQLITE') database = QSqlDatabase.addDatabase('QSQLITE')
if not database.isValid(): if not database.isValid():
raise SqlException('Failed to add database. ' raise SqlError('Failed to add database. '
'Are sqlite and Qt sqlite support installed?') 'Are sqlite and Qt sqlite support installed?')
database.setDatabaseName(db_path) database.setDatabaseName(db_path)
if not database.open(): if not database.open():
raise SqlException("Failed to open sqlite database at {}: {}" error = database.lastError()
.format(db_path, database.lastError().text())) _log_error(error)
raise SqlError("Failed to open sqlite database at {}: {}"
.format(db_path, error.text()))
def close(): def close():
@ -60,10 +62,32 @@ def version():
close() close()
return ver return ver
return Query("select sqlite_version()").run().value() return Query("select sqlite_version()").run().value()
except SqlException as e: except SqlError as e:
return 'UNAVAILABLE ({})'.format(e) return 'UNAVAILABLE ({})'.format(e)
def _log_error(error):
"""Log informations about a SQL error to the debug log."""
log.sql.debug("SQL error:")
log.sql.debug("type: {}".format(debug.qenum_key(QSqlError, error.type())))
log.sql.debug("database text: {}".format(error.databaseText()))
log.sql.debug("driver text: {}".format(error.driverText()))
log.sql.debug("error code: {}".format(error.nativeErrorCode()))
def _handle_query_error(what, query, error):
"""Handle a sqlite error.
Arguments:
what: What we were doing when the error happened.
query: The query which was executed.
error: The QSqlError object.
"""
_log_error(error)
msg = 'Failed to {} query "{}": "{}"'.format(what, query, error.text())
raise SqlError(msg)
class Query(QSqlQuery): class Query(QSqlQuery):
"""A prepared SQL Query.""" """A prepared SQL Query."""
@ -79,13 +103,12 @@ class Query(QSqlQuery):
super().__init__(QSqlDatabase.database()) super().__init__(QSqlDatabase.database())
log.sql.debug('Preparing SQL query: "{}"'.format(querystr)) log.sql.debug('Preparing SQL query: "{}"'.format(querystr))
if not self.prepare(querystr): if not self.prepare(querystr):
raise SqlException('Failed to prepare query "{}": "{}"'.format( _handle_query_error('prepare', querystr, self.lastError())
querystr, self.lastError().text()))
self.setForwardOnly(forward_only) self.setForwardOnly(forward_only)
def __iter__(self): def __iter__(self):
if not self.isActive(): if not self.isActive():
raise SqlException("Cannot iterate inactive query") raise SqlError("Cannot iterate inactive query")
rec = self.record() rec = self.record()
fields = [rec.fieldName(i) for i in range(rec.count())] fields = [rec.fieldName(i) for i in range(rec.count())]
rowtype = collections.namedtuple('ResultRow', fields) rowtype = collections.namedtuple('ResultRow', fields)
@ -101,14 +124,13 @@ class Query(QSqlQuery):
self.bindValue(':{}'.format(key), val) self.bindValue(':{}'.format(key), val)
log.sql.debug('query bindings: {}'.format(self.boundValues())) log.sql.debug('query bindings: {}'.format(self.boundValues()))
if not self.exec_(): if not self.exec_():
raise SqlException('Failed to exec query "{}": "{}"'.format( _handle_query_error('exec', self.lastQuery(), self.lastError())
self.lastQuery(), self.lastError().text()))
return self return self
def value(self): def value(self):
"""Return the result of a single-value query (e.g. an EXISTS).""" """Return the result of a single-value query (e.g. an EXISTS)."""
if not self.next(): if not self.next():
raise SqlException("No result for single-result query") raise SqlError("No result for single-result query")
return self.record().value(0) return self.record().value(0)
@ -128,7 +150,7 @@ class SqlTable(QObject):
def __init__(self, name, fields, constraints=None, parent=None): def __init__(self, name, fields, constraints=None, parent=None):
"""Create a new table in the sql database. """Create a new table in the sql database.
Raises SqlException if the table already exists. Does nothing if the table already exists.
Args: Args:
name: Name of the table. name: Name of the table.
@ -228,8 +250,7 @@ class SqlTable(QObject):
db = QSqlDatabase.database() db = QSqlDatabase.database()
db.transaction() db.transaction()
if not q.execBatch(): if not q.execBatch():
raise SqlException('Failed to exec query "{}": "{}"'.format( _handle_query_error('exec', q.lastQuery(), q.lastError())
q.lastQuery(), q.lastError().text()))
db.commit() db.commit()
self.changed.emit() self.changed.emit()

View File

@ -171,12 +171,15 @@ def debug_cache_stats():
prefix_info = configdata.is_valid_prefix.cache_info() prefix_info = configdata.is_valid_prefix.cache_info()
# pylint: disable=protected-access # pylint: disable=protected-access
render_stylesheet_info = config._render_stylesheet.cache_info() render_stylesheet_info = config._render_stylesheet.cache_info()
history_info = None
try: try:
from PyQt5.QtWebKit import QWebHistoryInterface from PyQt5.QtWebKit import QWebHistoryInterface
interface = QWebHistoryInterface.defaultInterface() interface = QWebHistoryInterface.defaultInterface()
if interface is not None:
history_info = interface.historyContains.cache_info() history_info = interface.historyContains.cache_info()
except ImportError: except ImportError:
history_info = None pass
log.misc.debug('is_valid_prefix: {}'.format(prefix_info)) log.misc.debug('is_valid_prefix: {}'.format(prefix_info))
log.misc.debug('_render_stylesheet: {}'.format(render_stylesheet_info)) log.misc.debug('_render_stylesheet: {}'.format(render_stylesheet_info))
@ -339,3 +342,12 @@ def window_only(current_win_id):
def nop(): def nop():
"""Do nothing.""" """Do nothing."""
return return
@cmdutils.register()
@cmdutils.argument('win_id', win_id=True)
def version(win_id):
"""Show version information."""
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
tabbed_browser.openurl(QUrl('qute://version'), newtab=True)

View File

@ -17,7 +17,21 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. # along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Early initialization and main entry point.""" """Early initialization and main entry point.
qutebrowser's initialization process roughly looks like this:
- This file gets imported, either via the setuptools entry point or
__main__.py.
- At import time, we check for the correct Python version and show an error if
it's too old.
- The main() function in this file gets invoked
- Argument parsing takes place
- earlyinit.early_init() gets invoked to do various low-level initialization
and checks whether all dependencies are met.
- app.run() gets called, which takes over.
See the docstring of app.py for details.
"""
import sys import sys
import json import json
@ -95,8 +109,6 @@ def get_argparser():
action='store_false', dest='color') action='store_false', dest='color')
debug.add_argument('--force-color', help="Force colored logging", debug.add_argument('--force-color', help="Force colored logging",
action='store_true') action='store_true')
debug.add_argument('--relaxed-config', action='store_true',
help="Silently remove unknown config options.")
debug.add_argument('--nowindow', action='store_true', help="Don't show " debug.add_argument('--nowindow', action='store_true', help="Don't show "
"the main window.") "the main window.")
debug.add_argument('--temp-basedir', action='store_true', help="Use a " debug.add_argument('--temp-basedir', action='store_true', help="Use a "

View File

@ -39,6 +39,7 @@ except ImportError:
colorama = None colorama = None
_log_inited = False _log_inited = False
_args = None
COLORS = ['black', 'red', 'green', 'yellow', 'blue', 'purple', 'cyan', 'white'] COLORS = ['black', 'red', 'green', 'yellow', 'blue', 'purple', 'cyan', 'white']
COLOR_ESCAPES = {color: '\033[{}m'.format(i) COLOR_ESCAPES = {color: '\033[{}m'.format(i)
@ -189,8 +190,9 @@ def init_log(args):
logging.captureWarnings(True) logging.captureWarnings(True)
_init_py_warnings() _init_py_warnings()
QtCore.qInstallMessageHandler(qt_message_handler) QtCore.qInstallMessageHandler(qt_message_handler)
global _log_inited global _log_inited, _args
_log_inited = True _log_inited = True
_args = args
def _init_py_warnings(): def _init_py_warnings():
@ -442,7 +444,11 @@ def qt_message_handler(msg_type, context, msg):
msg += ("\n\nOn Archlinux, this should fix the problem:\n" msg += ("\n\nOn Archlinux, this should fix the problem:\n"
" pacman -S libxkbcommon-x11") " pacman -S libxkbcommon-x11")
faulthandler.disable() faulthandler.disable()
if _args.debug:
stack = ''.join(traceback.format_stack()) stack = ''.join(traceback.format_stack())
else:
stack = None
record = qt.makeRecord(name, level, context.file, context.line, msg, None, record = qt.makeRecord(name, level, context.file, context.line, msg, None,
None, func, sinfo=stack) None, func, sinfo=stack)
qt.handle(record) qt.handle(record)

View File

@ -36,8 +36,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir,
os.pardir)) os.pardir))
import qutebrowser import qutebrowser
from qutebrowser.utils import utils from scripts import utils
from scripts import utils as scriptutils
# from scripts.dev import update_3rdparty # from scripts.dev import update_3rdparty
@ -71,7 +70,7 @@ def call_tox(toxenv, *args, python=sys.executable):
def run_asciidoc2html(args): def run_asciidoc2html(args):
"""Common buildsteps used for all OS'.""" """Common buildsteps used for all OS'."""
scriptutils.print_title("Running asciidoc2html.py") utils.print_title("Running asciidoc2html.py")
if args.asciidoc is not None: if args.asciidoc is not None:
a2h_args = ['--asciidoc'] + args.asciidoc a2h_args = ['--asciidoc'] + args.asciidoc
else: else:
@ -128,7 +127,7 @@ def patch_mac_app():
def build_mac(): def build_mac():
"""Build macOS .dmg/.app.""" """Build macOS .dmg/.app."""
scriptutils.print_title("Cleaning up...") utils.print_title("Cleaning up...")
for f in ['wc.dmg', 'template.dmg']: for f in ['wc.dmg', 'template.dmg']:
try: try:
os.remove(f) os.remove(f)
@ -136,20 +135,20 @@ def build_mac():
pass pass
for d in ['dist', 'build']: for d in ['dist', 'build']:
shutil.rmtree(d, ignore_errors=True) shutil.rmtree(d, ignore_errors=True)
scriptutils.print_title("Updating 3rdparty content") utils.print_title("Updating 3rdparty content")
# Currently disabled because QtWebEngine has no pdfjs support # Currently disabled because QtWebEngine has no pdfjs support
# update_3rdparty.run(ace=False, pdfjs=True, fancy_dmg=False) # update_3rdparty.run(ace=False, pdfjs=True, fancy_dmg=False)
scriptutils.print_title("Building .app via pyinstaller") utils.print_title("Building .app via pyinstaller")
call_tox('pyinstaller', '-r') call_tox('pyinstaller', '-r')
scriptutils.print_title("Patching .app") utils.print_title("Patching .app")
patch_mac_app() patch_mac_app()
scriptutils.print_title("Building .dmg") utils.print_title("Building .dmg")
subprocess.check_call(['make', '-f', 'scripts/dev/Makefile-dmg']) subprocess.check_call(['make', '-f', 'scripts/dev/Makefile-dmg'])
dmg_name = 'qutebrowser-{}.dmg'.format(qutebrowser.__version__) dmg_name = 'qutebrowser-{}.dmg'.format(qutebrowser.__version__)
os.rename('qutebrowser.dmg', dmg_name) os.rename('qutebrowser.dmg', dmg_name)
scriptutils.print_title("Running smoke test") utils.print_title("Running smoke test")
try: try:
with tempfile.TemporaryDirectory() as tmpdir: with tempfile.TemporaryDirectory() as tmpdir:
@ -178,11 +177,11 @@ def patch_windows(out_dir):
def build_windows(): def build_windows():
"""Build windows executables/setups.""" """Build windows executables/setups."""
scriptutils.print_title("Updating 3rdparty content") utils.print_title("Updating 3rdparty content")
# Currently disabled because QtWebEngine has no pdfjs support # Currently disabled because QtWebEngine has no pdfjs support
# update_3rdparty.run(ace=False, pdfjs=True, fancy_dmg=False) # update_3rdparty.run(ace=False, pdfjs=True, fancy_dmg=False)
scriptutils.print_title("Building Windows binaries") utils.print_title("Building Windows binaries")
parts = str(sys.version_info.major), str(sys.version_info.minor) parts = str(sys.version_info.major), str(sys.version_info.minor)
ver = ''.join(parts) ver = ''.join(parts)
python_x86 = r'C:\Python{}-32\python.exe'.format(ver) python_x86 = r'C:\Python{}-32\python.exe'.format(ver)
@ -195,19 +194,19 @@ def build_windows():
artifacts = [] artifacts = []
scriptutils.print_title("Running pyinstaller 32bit") utils.print_title("Running pyinstaller 32bit")
_maybe_remove(out_32) _maybe_remove(out_32)
call_tox('pyinstaller', '-r', python=python_x86) call_tox('pyinstaller', '-r', python=python_x86)
shutil.move(out_pyinstaller, out_32) shutil.move(out_pyinstaller, out_32)
patch_windows(out_32) patch_windows(out_32)
scriptutils.print_title("Running pyinstaller 64bit") utils.print_title("Running pyinstaller 64bit")
_maybe_remove(out_64) _maybe_remove(out_64)
call_tox('pyinstaller', '-r', python=python_x64) call_tox('pyinstaller', '-r', python=python_x64)
shutil.move(out_pyinstaller, out_64) shutil.move(out_pyinstaller, out_64)
patch_windows(out_64) patch_windows(out_64)
scriptutils.print_title("Building installers") utils.print_title("Building installers")
subprocess.check_call(['makensis.exe', subprocess.check_call(['makensis.exe',
'/DVERSION={}'.format(qutebrowser.__version__), '/DVERSION={}'.format(qutebrowser.__version__),
'misc/qutebrowser.nsi']) 'misc/qutebrowser.nsi'])
@ -228,12 +227,12 @@ def build_windows():
'Windows 64bit installer'), 'Windows 64bit installer'),
] ]
scriptutils.print_title("Running 32bit smoke test") utils.print_title("Running 32bit smoke test")
smoke_test(os.path.join(out_32, 'qutebrowser.exe')) smoke_test(os.path.join(out_32, 'qutebrowser.exe'))
scriptutils.print_title("Running 64bit smoke test") utils.print_title("Running 64bit smoke test")
smoke_test(os.path.join(out_64, 'qutebrowser.exe')) smoke_test(os.path.join(out_64, 'qutebrowser.exe'))
scriptutils.print_title("Zipping 32bit standalone...") utils.print_title("Zipping 32bit standalone...")
name = 'qutebrowser-{}-windows-standalone-win32'.format( name = 'qutebrowser-{}-windows-standalone-win32'.format(
qutebrowser.__version__) qutebrowser.__version__)
shutil.make_archive(name, 'zip', 'dist', os.path.basename(out_32)) shutil.make_archive(name, 'zip', 'dist', os.path.basename(out_32))
@ -241,7 +240,7 @@ def build_windows():
'application/zip', 'application/zip',
'Windows 32bit standalone')) 'Windows 32bit standalone'))
scriptutils.print_title("Zipping 64bit standalone...") utils.print_title("Zipping 64bit standalone...")
name = 'qutebrowser-{}-windows-standalone-amd64'.format( name = 'qutebrowser-{}-windows-standalone-amd64'.format(
qutebrowser.__version__) qutebrowser.__version__)
shutil.make_archive(name, 'zip', 'dist', os.path.basename(out_64)) shutil.make_archive(name, 'zip', 'dist', os.path.basename(out_64))
@ -254,7 +253,7 @@ def build_windows():
def build_sdist(): def build_sdist():
"""Build an sdist and list the contents.""" """Build an sdist and list the contents."""
scriptutils.print_title("Building sdist") utils.print_title("Building sdist")
_maybe_remove('dist') _maybe_remove('dist')
@ -277,10 +276,10 @@ def build_sdist():
assert '.pyc' not in by_ext assert '.pyc' not in by_ext
scriptutils.print_title("sdist contents") utils.print_title("sdist contents")
for ext, files in sorted(by_ext.items()): for ext, files in sorted(by_ext.items()):
scriptutils.print_subtitle(ext) utils.print_subtitle(ext)
print('\n'.join(files)) print('\n'.join(files))
filename = 'qutebrowser-{}.tar.gz'.format(qutebrowser.__version__) filename = 'qutebrowser-{}.tar.gz'.format(qutebrowser.__version__)
@ -309,7 +308,7 @@ def github_upload(artifacts, tag):
tag: The name of the release tag tag: The name of the release tag
""" """
import github3 import github3
scriptutils.print_title("Uploading to github...") utils.print_title("Uploading to github...")
token = read_github_token() token = read_github_token()
gh = github3.login(token=token) gh = github3.login(token=token)
@ -344,7 +343,7 @@ def main():
parser.add_argument('--upload', help="Tag to upload the release for", parser.add_argument('--upload', help="Tag to upload the release for",
nargs=1, required=False, metavar='TAG') nargs=1, required=False, metavar='TAG')
args = parser.parse_args() args = parser.parse_args()
scriptutils.change_cwd() utils.change_cwd()
upload_to_pypi = False upload_to_pypi = False
@ -354,7 +353,8 @@ def main():
import github3 # pylint: disable=unused-variable import github3 # pylint: disable=unused-variable
read_github_token() read_github_token()
if utils.is_windows: run_asciidoc2html(args)
if os.name == 'nt':
if sys.maxsize > 2**32: if sys.maxsize > 2**32:
# WORKAROUND # WORKAROUND
print("Due to a python/Windows bug, this script needs to be run ") print("Due to a python/Windows bug, this script needs to be run ")
@ -363,21 +363,24 @@ def main():
print("See http://bugs.python.org/issue24493 and ") print("See http://bugs.python.org/issue24493 and ")
print("https://github.com/pypa/virtualenv/issues/774") print("https://github.com/pypa/virtualenv/issues/774")
sys.exit(1) sys.exit(1)
run_asciidoc2html(args)
artifacts = build_windows() artifacts = build_windows()
elif utils.is_mac: elif sys.platform == 'darwin':
run_asciidoc2html(args)
artifacts = build_mac() artifacts = build_mac()
else: else:
artifacts = build_sdist() artifacts = build_sdist()
upload_to_pypi = True upload_to_pypi = True
if args.upload is not None: if args.upload is not None:
scriptutils.print_title("Press enter to release...") utils.print_title("Press enter to release...")
input() input()
github_upload(artifacts, args.upload[0]) github_upload(artifacts, args.upload[0])
if upload_to_pypi: if upload_to_pypi:
pypi_upload(artifacts) pypi_upload(artifacts)
else:
print()
utils.print_title("Artifacts")
for artifact in artifacts:
print(artifact)
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -141,6 +141,10 @@ PERFECT_FILES = [
'config/configfiles.py'), 'config/configfiles.py'),
('tests/unit/config/test_configtypes.py', ('tests/unit/config/test_configtypes.py',
'config/configtypes.py'), 'config/configtypes.py'),
('tests/unit/config/test_configinit.py',
'config/configinit.py'),
('tests/unit/config/test_configcommands.py',
'config/configcommands.py'),
('tests/unit/utils/test_qtutils.py', ('tests/unit/utils/test_qtutils.py',
'utils/qtutils.py'), 'utils/qtutils.py'),

View File

@ -5,7 +5,7 @@ if [[ $DOCKER ]]; then
elif [[ $TESTENV == eslint ]]; then elif [[ $TESTENV == eslint ]]; then
# Can't run this via tox as we can't easily install tox in the javascript travis env # Can't run this via tox as we can't easily install tox in the javascript travis env
cd qutebrowser/javascript || exit 1 cd qutebrowser/javascript || exit 1
eslint --color . eslint --color --report-unused-disable-directives .
else else
args=() args=()
[[ $TRAVIS_OS_NAME == osx ]] && args=('--qute-bdd-webengine' '--no-xvfb') [[ $TRAVIS_OS_NAME == osx ]] && args=('--qute-bdd-webengine' '--no-xvfb')

View File

@ -41,7 +41,7 @@ from qutebrowser.browser import qutescheme
from qutebrowser.config import configtypes from qutebrowser.config import configtypes
def whitelist_generator(): def whitelist_generator(): # noqa
"""Generator which yields lines to add to a vulture whitelist.""" """Generator which yields lines to add to a vulture whitelist."""
# qutebrowser commands # qutebrowser commands
for cmd in cmdutils.cmd_dict.values(): for cmd in cmdutils.cmd_dict.values():
@ -108,6 +108,8 @@ def whitelist_generator():
yield 'qutebrowser.config.configexc.ConfigErrorDesc.traceback' yield 'qutebrowser.config.configexc.ConfigErrorDesc.traceback'
yield 'qutebrowser.config.configfiles.ConfigAPI.load_autoconfig' yield 'qutebrowser.config.configfiles.ConfigAPI.load_autoconfig'
yield 'types.ModuleType.c' # configfiles:read_config_py yield 'types.ModuleType.c' # configfiles:read_config_py
for name in ['configdir', 'datadir']:
yield 'qutebrowser.config.configfiles.ConfigAPI.' + name
yield 'include_aliases' yield 'include_aliases'

View File

@ -416,8 +416,8 @@ def _generate_setting_option(f, opt):
f.write("=== {}".format(opt.name) + "\n") f.write("=== {}".format(opt.name) + "\n")
f.write(opt.description + "\n") f.write(opt.description + "\n")
f.write("\n") f.write("\n")
f.write('Type: <<types,{typ}>>\n'.format( typ = opt.typ.get_name().replace(',', '&#44;')
typ=opt.typ.__class__.__name__)) f.write('Type: <<types,{typ}>>\n'.format(typ=typ))
f.write("\n") f.write("\n")
valid_values = opt.typ.get_valid_values() valid_values = opt.typ.get_valid_values()

View File

@ -18,11 +18,9 @@
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. # along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Data used by setup.py and scripts/freeze.py.""" """Data used by setup.py and the PyInstaller qutebrowser.spec."""
import sys import sys
import re
import ast
import os import os
import os.path import os.path
import subprocess import subprocess
@ -30,42 +28,16 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
if sys.hexversion >= 0x03000000: if sys.hexversion >= 0x03000000:
_open = open open_file = open
else: else:
import codecs import codecs
_open = codecs.open open_file = codecs.open
BASEDIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), BASEDIR = os.path.join(os.path.dirname(os.path.realpath(__file__)),
os.path.pardir) os.path.pardir)
def read_file(name):
"""Get the string contained in the file named name."""
with _open(name, 'r', encoding='utf-8') as f:
return f.read()
def _get_constant(name):
"""Read a __magic__ constant from qutebrowser/__init__.py.
We don't import qutebrowser here because it can go wrong for multiple
reasons. Instead we use re/ast to get the value directly from the source
file.
Args:
name: The name of the argument to get.
Return:
The value of the argument.
"""
field_re = re.compile(r'__{}__\s+=\s+(.*)'.format(re.escape(name)))
path = os.path.join(BASEDIR, 'qutebrowser', '__init__.py')
line = field_re.search(read_file(path)).group(1)
value = ast.literal_eval(line)
return value
def _git_str(): def _git_str():
"""Try to find out git version. """Try to find out git version.
@ -95,37 +67,5 @@ def write_git_file():
if gitstr is None: if gitstr is None:
gitstr = '' gitstr = ''
path = os.path.join(BASEDIR, 'qutebrowser', 'git-commit-id') path = os.path.join(BASEDIR, 'qutebrowser', 'git-commit-id')
with _open(path, 'w', encoding='ascii') as f: with open_file(path, 'w', encoding='ascii') as f:
f.write(gitstr) f.write(gitstr)
setupdata = {
'name': 'qutebrowser',
'version': '.'.join(str(e) for e in _get_constant('version_info')),
'description': _get_constant('description'),
'long_description': read_file('README.asciidoc'),
'url': 'https://www.qutebrowser.org/',
'requires': ['pypeg2', 'jinja2', 'pygments', 'PyYAML', 'attrs'],
'author': _get_constant('author'),
'author_email': _get_constant('email'),
'license': _get_constant('license'),
'classifiers': [
'Development Status :: 3 - Alpha',
'Environment :: X11 Applications :: Qt',
'Intended Audience :: End Users/Desktop',
'License :: OSI Approved :: GNU General Public License v3 or later '
'(GPLv3+)',
'Natural Language :: English',
'Operating System :: Microsoft :: Windows',
'Operating System :: Microsoft :: Windows :: Windows XP',
'Operating System :: Microsoft :: Windows :: Windows 7',
'Operating System :: POSIX :: Linux',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Topic :: Internet',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Internet :: WWW/HTTP :: Browsers',
],
'keywords': 'pyqt browser web qt webkit',
}

View File

@ -21,6 +21,8 @@
"""setuptools installer script for qutebrowser.""" """setuptools installer script for qutebrowser."""
import re
import ast
import os import os
import os.path import os.path
@ -35,6 +37,32 @@ except NameError:
BASEDIR = None BASEDIR = None
def read_file(name):
"""Get the string contained in the file named name."""
with common.open_file(name, 'r', encoding='utf-8') as f:
return f.read()
def _get_constant(name):
"""Read a __magic__ constant from qutebrowser/__init__.py.
We don't import qutebrowser here because it can go wrong for multiple
reasons. Instead we use re/ast to get the value directly from the source
file.
Args:
name: The name of the argument to get.
Return:
The value of the argument.
"""
field_re = re.compile(r'__{}__\s+=\s+(.*)'.format(re.escape(name)))
path = os.path.join(BASEDIR, 'qutebrowser', '__init__.py')
line = field_re.search(read_file(path)).group(1)
value = ast.literal_eval(line)
return value
try: try:
common.write_git_file() common.write_git_file()
setuptools.setup( setuptools.setup(
@ -42,10 +70,35 @@ try:
include_package_data=True, include_package_data=True,
entry_points={'gui_scripts': entry_points={'gui_scripts':
['qutebrowser = qutebrowser.qutebrowser:main']}, ['qutebrowser = qutebrowser.qutebrowser:main']},
test_suite='qutebrowser.test',
zip_safe=True, zip_safe=True,
install_requires=['pypeg2', 'jinja2', 'pygments', 'PyYAML', 'attrs'], install_requires=['pypeg2', 'jinja2', 'pygments', 'PyYAML', 'attrs'],
**common.setupdata name='qutebrowser',
version='.'.join(str(e) for e in _get_constant('version_info')),
description=_get_constant('description'),
long_description=read_file('README.asciidoc'),
url='https://www.qutebrowser.org/',
author=_get_constant('author'),
author_email=_get_constant('email'),
license=_get_constant('license'),
classifiers=[
'Development Status :: 4 - Beta',
'Environment :: X11 Applications :: Qt',
'Intended Audience :: End Users/Desktop',
'License :: OSI Approved :: GNU General Public License v3 or later '
'(GPLv3+)',
'Natural Language :: English',
'Operating System :: Microsoft :: Windows',
'Operating System :: POSIX :: Linux',
'Operating System :: MacOS',
'Operating System :: POSIX :: BSD',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Topic :: Internet',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Internet :: WWW/HTTP :: Browsers',
],
keywords='pyqt browser web qt webkit qtwebkit qtwebengine',
) )
finally: finally:
if BASEDIR is not None: if BASEDIR is not None:

View File

@ -43,7 +43,7 @@ import qutebrowser.app # To register commands
# Set hypothesis settings # Set hypothesis settings
hypothesis.settings.register_profile('default', hypothesis.settings.register_profile('default',
hypothesis.settings(deadline=400)) hypothesis.settings(deadline=600))
hypothesis.settings.load_profile('default') hypothesis.settings.load_profile('default')

View File

@ -1,8 +0,0 @@
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
Feature: Ad blocking
Scenario: Simple adblock update
When I set up "simple" as block lists
And I run :adblock-update
Then the message "adblock: Read 1 hosts from 1 sources." should be shown

View File

@ -76,7 +76,9 @@ Feature: Using completion
And I open data/hello2.txt in a new tab And I open data/hello2.txt in a new tab
And I run :set-cmd-text -s :buffer And I run :set-cmd-text -s :buffer
And I run :completion-item-focus next And I run :completion-item-focus next
And I wait for "setting text = ':buffer 0/1', *" in the log
And I run :completion-item-focus next And I run :completion-item-focus next
And I wait for "setting text = ':buffer 0/2', *" in the log
And I run :completion-item-del And I run :completion-item-del
Then the following tabs should be open: Then the following tabs should be open:
- data/hello.txt (active) - data/hello.txt (active)

View File

@ -246,7 +246,7 @@ Feature: Using hints
Scenario: Ignoring key presses after auto-following hints Scenario: Ignoring key presses after auto-following hints
When I set hints.auto_follow_timeout to 1000 When I set hints.auto_follow_timeout to 1000
And I set hints.mode to number And I set hints.mode to number
And I run :bind --force , message-error "This error message was triggered via a keybinding which should have been inhibited" And I run :bind , message-error "This error message was triggered via a keybinding which should have been inhibited"
And I open data/hints/html/simple.html And I open data/hints/html/simple.html
And I hint with args "all" And I hint with args "all"
And I press the key "f" And I press the key "f"
@ -259,7 +259,7 @@ Feature: Using hints
Scenario: Turning off auto_follow_timeout Scenario: Turning off auto_follow_timeout
When I set hints.auto_follow_timeout to 0 When I set hints.auto_follow_timeout to 0
And I set hints.mode to number And I set hints.mode to number
And I run :bind --force , message-info "Keypress worked!" And I run :bind , message-info "Keypress worked!"
And I open data/hints/html/simple.html And I open data/hints/html/simple.html
And I hint with args "all" And I hint with args "all"
And I press the key "f" And I press the key "f"
@ -362,6 +362,7 @@ Feature: Using hints
And I set hints.mode to letter And I set hints.mode to letter
And I hint with args "--mode number all" And I hint with args "--mode number all"
And I press the key "s" And I press the key "s"
And I wait for "Filtering hints on 's'" in the log
And I run :follow-hint 1 And I run :follow-hint 1
Then data/numbers/7.txt should be loaded Then data/numbers/7.txt should be loaded

View File

@ -21,7 +21,7 @@ Feature: Setting positional marks
Scenario: Jumping back after jumping to a particular percentage Scenario: Jumping back after jumping to a particular percentage
When I run :scroll-px 10 20 When I run :scroll-px 10 20
And I wait until the scroll position changed to 10/20 And I wait until the scroll position changed to 10/20
And I run :scroll-perc 100 And I run :scroll-to-perc 100
And I wait until the scroll position changed And I wait until the scroll position changed
And I run :jump-mark "'" And I run :jump-mark "'"
And I wait until the scroll position changed to 10/20 And I wait until the scroll position changed to 10/20
@ -116,7 +116,7 @@ Feature: Setting positional marks
Scenario: Hovering a hint does not set the ' mark Scenario: Hovering a hint does not set the ' mark
When I run :scroll-px 30 20 When I run :scroll-px 30 20
And I wait until the scroll position changed to 30/20 And I wait until the scroll position changed to 30/20
And I run :scroll-perc 0 And I run :scroll-to-perc 0
And I wait until the scroll position changed And I wait until the scroll position changed
And I hint with args "links hover" and follow s And I hint with args "links hover" and follow s
And I run :jump-mark "'" And I run :jump-mark "'"

View File

@ -47,6 +47,14 @@ Feature: Various utility commands.
When I run :set-cmd-text foo When I run :set-cmd-text foo
Then the error "Invalid command text 'foo'." should be shown Then the error "Invalid command text 'foo'." should be shown
Scenario: :set-cmd-text with run on count flag and no count
When I run :set-cmd-text --run-on-count :message-info "Hello World"
Then "message:info:86 Hello World" should not be logged
Scenario: :set-cmd-text with run on count flag and a count
When I run :set-cmd-text --run-on-count :message-info "Hello World" with count 1
Then the message "Hello World" should be shown
## :jseval ## :jseval
Scenario: :jseval Scenario: :jseval
@ -399,6 +407,7 @@ Feature: Various utility commands.
When I open data/hello2.txt in a new tab When I open data/hello2.txt in a new tab
And I open data/hello3.txt in a new window And I open data/hello3.txt in a new window
And I run :window-only And I run :window-only
And I wait for "Closing window *" in the log
Then the session should look like: Then the session should look like:
windows: windows:
- tabs: - tabs:
@ -527,6 +536,11 @@ Feature: Various utility commands.
And I open data/numbers/3.txt And I open data/numbers/3.txt
Then no crash should happen Then no crash should happen
Scenario: Simple adblock update
When I set up "simple" as block lists
And I run :adblock-update
Then the message "adblock: Read 1 hosts from 1 sources." should be shown
## Spellcheck ## Spellcheck
@qtwebkit_skip @qt>=5.8 @cannot_have_dict=af-ZA @qtwebkit_skip @qt>=5.8 @cannot_have_dict=af-ZA

View File

@ -77,7 +77,7 @@ Feature: Special qute:// pages
When I set ignore_case to never When I set ignore_case to never
And I open qute://settings And I open qute://settings
# scroll to the right - the table does not fit in the default screen # scroll to the right - the table does not fit in the default screen
And I run :scroll-perc -x 100 And I run :scroll-to-perc -x 100
And I run :jseval document.getElementById('input-ignore_case').value = '' And I run :jseval document.getElementById('input-ignore_case').value = ''
And I run :click-element id input-ignore_case And I run :click-element id input-ignore_case
And I wait for "Entering mode KeyMode.insert *" in the log And I wait for "Entering mode KeyMode.insert *" in the log
@ -91,7 +91,7 @@ Feature: Special qute:// pages
Scenario: Focusing input fields in qute://settings and entering invalid value Scenario: Focusing input fields in qute://settings and entering invalid value
When I open qute://settings When I open qute://settings
# scroll to the right - the table does not fit in the default screen # scroll to the right - the table does not fit in the default screen
And I run :scroll-perc -x 100 And I run :scroll-to-perc -x 100
And I run :jseval document.getElementById('input-ignore_case').value = '' And I run :jseval document.getElementById('input-ignore_case').value = ''
And I run :click-element id input-ignore_case And I run :click-element id input-ignore_case
And I wait for "Entering mode KeyMode.insert *" in the log And I wait for "Entering mode KeyMode.insert *" in the log

View File

@ -156,86 +156,86 @@ Feature: Scrolling
And I run :scroll down And I run :scroll down
Then the page should not be scrolled Then the page should not be scrolled
## :scroll-perc ## :scroll-to-perc
Scenario: Scrolling to bottom with :scroll-perc Scenario: Scrolling to bottom with :scroll-to-perc
When I run :scroll-perc 100 When I run :scroll-to-perc 100
Then the page should be scrolled vertically Then the page should be scrolled vertically
Scenario: Scrolling to bottom and to top with :scroll-perc Scenario: Scrolling to bottom and to top with :scroll-to-perc
When I run :scroll-perc 100 When I run :scroll-to-perc 100
And I wait until the scroll position changed And I wait until the scroll position changed
And I run :scroll-perc 0 And I run :scroll-to-perc 0
And I wait until the scroll position changed to 0/0 And I wait until the scroll position changed to 0/0
Then the page should not be scrolled Then the page should not be scrolled
Scenario: Scrolling to middle with :scroll-perc Scenario: Scrolling to middle with :scroll-to-perc
When I run :scroll-perc 50 When I run :scroll-to-perc 50
Then the page should be scrolled vertically Then the page should be scrolled vertically
Scenario: Scrolling to middle with :scroll-perc (float) Scenario: Scrolling to middle with :scroll-to-perc (float)
When I run :scroll-perc 50.5 When I run :scroll-to-perc 50.5
Then the page should be scrolled vertically Then the page should be scrolled vertically
Scenario: Scrolling to middle and to top with :scroll-perc Scenario: Scrolling to middle and to top with :scroll-to-perc
When I run :scroll-perc 50 When I run :scroll-to-perc 50
And I wait until the scroll position changed And I wait until the scroll position changed
And I run :scroll-perc 0 And I run :scroll-to-perc 0
And I wait until the scroll position changed to 0/0 And I wait until the scroll position changed to 0/0
Then the page should not be scrolled Then the page should not be scrolled
Scenario: Scrolling to right with :scroll-perc Scenario: Scrolling to right with :scroll-to-perc
When I run :scroll-perc --horizontal 100 When I run :scroll-to-perc --horizontal 100
Then the page should be scrolled horizontally Then the page should be scrolled horizontally
Scenario: Scrolling to right and to left with :scroll-perc Scenario: Scrolling to right and to left with :scroll-to-perc
When I run :scroll-perc --horizontal 100 When I run :scroll-to-perc --horizontal 100
And I wait until the scroll position changed And I wait until the scroll position changed
And I run :scroll-perc --horizontal 0 And I run :scroll-to-perc --horizontal 0
And I wait until the scroll position changed to 0/0 And I wait until the scroll position changed to 0/0
Then the page should not be scrolled Then the page should not be scrolled
Scenario: Scrolling to middle (horizontally) with :scroll-perc Scenario: Scrolling to middle (horizontally) with :scroll-to-perc
When I run :scroll-perc --horizontal 50 When I run :scroll-to-perc --horizontal 50
Then the page should be scrolled horizontally Then the page should be scrolled horizontally
Scenario: Scrolling to middle and to left with :scroll-perc Scenario: Scrolling to middle and to left with :scroll-to-perc
When I run :scroll-perc --horizontal 50 When I run :scroll-to-perc --horizontal 50
And I wait until the scroll position changed And I wait until the scroll position changed
And I run :scroll-perc --horizontal 0 And I run :scroll-to-perc --horizontal 0
And I wait until the scroll position changed to 0/0 And I wait until the scroll position changed to 0/0
Then the page should not be scrolled Then the page should not be scrolled
Scenario: :scroll-perc without argument Scenario: :scroll-to-perc without argument
When I run :scroll-perc When I run :scroll-to-perc
Then the page should be scrolled vertically Then the page should be scrolled vertically
Scenario: :scroll-perc without argument and --horizontal Scenario: :scroll-to-perc without argument and --horizontal
When I run :scroll-perc --horizontal When I run :scroll-to-perc --horizontal
Then the page should be scrolled horizontally Then the page should be scrolled horizontally
Scenario: :scroll-perc with count Scenario: :scroll-to-perc with count
When I run :scroll-perc with count 50 When I run :scroll-to-perc with count 50
Then the page should be scrolled vertically Then the page should be scrolled vertically
@qtwebengine_skip: Causes memory leak... @qtwebengine_skip: Causes memory leak...
Scenario: :scroll-perc with a very big value Scenario: :scroll-to-perc with a very big value
When I run :scroll-perc 99999999999 When I run :scroll-to-perc 99999999999
Then no crash should happen Then no crash should happen
Scenario: :scroll-perc on a page without scrolling Scenario: :scroll-to-perc on a page without scrolling
When I open data/hello.txt When I open data/hello.txt
And I run :scroll-perc 20 And I run :scroll-to-perc 20
Then the page should not be scrolled Then the page should not be scrolled
Scenario: :scroll-perc with count and argument Scenario: :scroll-to-perc with count and argument
When I run :scroll-perc 0 with count 50 When I run :scroll-to-perc 0 with count 50
Then the page should be scrolled vertically Then the page should be scrolled vertically
# https://github.com/qutebrowser/qutebrowser/issues/1821 # https://github.com/qutebrowser/qutebrowser/issues/1821
Scenario: :scroll-perc without doctype Scenario: :scroll-to-perc without doctype
When I open data/scroll/no_doctype.html When I open data/scroll/no_doctype.html
And I run :scroll-perc 100 And I run :scroll-to-perc 100
Then the page should be scrolled vertically Then the page should be scrolled vertically
## :scroll-page ## :scroll-page
@ -280,14 +280,14 @@ Feature: Scrolling
Then the page should not be scrolled Then the page should not be scrolled
Scenario: :scroll-page with --bottom-navigate Scenario: :scroll-page with --bottom-navigate
When I run :scroll-perc 100 When I run :scroll-to-perc 100
And I wait until the scroll position changed And I wait until the scroll position changed
And I run :scroll-page --bottom-navigate next 0 1 And I run :scroll-page --bottom-navigate next 0 1
Then data/hello2.txt should be loaded Then data/hello2.txt should be loaded
Scenario: :scroll-page with --bottom-navigate and zoom Scenario: :scroll-page with --bottom-navigate and zoom
When I run :zoom 200 When I run :zoom 200
And I run :scroll-perc 100 And I run :scroll-to-perc 100
And I wait until the scroll position changed And I wait until the scroll position changed
And I run :scroll-page --bottom-navigate next 0 1 And I run :scroll-page --bottom-navigate next 0 1
Then data/hello2.txt should be loaded Then data/hello2.txt should be loaded
@ -317,7 +317,7 @@ Feature: Scrolling
Scenario: Relative scroll position with a position:absolute page Scenario: Relative scroll position with a position:absolute page
When I open data/scroll/position_absolute.html When I open data/scroll/position_absolute.html
And I run :scroll-perc 100 And I run :scroll-to-perc 100
And I wait until the scroll position changed And I wait until the scroll position changed
And I run :scroll-page --bottom-navigate next 0 1 And I run :scroll-page --bottom-navigate next 0 1
Then data/hello2.txt should be loaded Then data/hello2.txt should be loaded

View File

@ -897,9 +897,9 @@ Feature: Tab management
# :buffer # :buffer
Scenario: :buffer without args Scenario: :buffer without args or count
When I run :buffer When I run :buffer
Then the error "buffer: The following arguments are required: index" should be shown Then the error "buffer: Either a count or the argument index must be specified." should be shown
Scenario: :buffer with a matching title Scenario: :buffer with a matching title
When I open data/title.html When I open data/title.html
@ -953,7 +953,7 @@ Feature: Tab management
And I run :buffer "99/1" And I run :buffer "99/1"
Then the error "There's no window with id 99!" should be shown Then the error "There's no window with id 99!" should be shown
@qtwebengine_flaky @skip # Too flaky
Scenario: :buffer with matching window index Scenario: :buffer with matching window index
Given I have a fresh instance Given I have a fresh instance
When I open data/title.html When I open data/title.html

View File

@ -17,6 +17,8 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. # along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
import json
import pytest_bdd as bdd import pytest_bdd as bdd
bdd.scenarios('misc.feature') bdd.scenarios('misc.feature')
@ -26,3 +28,10 @@ def pdf_exists(quteproc, tmpdir, filename):
path = tmpdir / filename path = tmpdir / filename
data = path.read_binary() data = path.read_binary()
assert data.startswith(b'%PDF') assert data.startswith(b'%PDF')
@bdd.when(bdd.parsers.parse('I set up "{lists}" as block lists'))
def set_up_blocking(quteproc, lists, server):
url = 'http://localhost:{}/data/adblock/'.format(server.port)
urls = [url + item.strip() for item in lists.split(',')]
quteproc.set_setting('content.host_blocking.lists', json.dumps(urls))

View File

@ -458,7 +458,7 @@ class QuteProc(testprocess.Process):
__tracebackhide__ = (lambda e: __tracebackhide__ = (lambda e:
e.errisinstance(testprocess.WaitForTimeout)) e.errisinstance(testprocess.WaitForTimeout))
xfail = self.request.node.get_marker('xfail') xfail = self.request.node.get_marker('xfail')
if xfail and xfail.args[0]: if xfail and (not xfail.args or xfail.args[0]):
kwargs['divisor'] = 10 kwargs['divisor'] = 10
else: else:
kwargs['divisor'] = 1 kwargs['divisor'] = 1
@ -494,7 +494,13 @@ class QuteProc(testprocess.Process):
if skip_texts: if skip_texts:
pytest.skip(', '.join(skip_texts)) pytest.skip(', '.join(skip_texts))
def _after_start(self): def before_test(self):
"""Clear settings before every test."""
super().before_test()
self.send_cmd(':config-clear')
self._init_settings()
def _init_settings(self):
"""Adjust some qutebrowser settings after starting.""" """Adjust some qutebrowser settings after starting."""
settings = [ settings = [
('messages.timeout', '0'), ('messages.timeout', '0'),

View File

@ -47,6 +47,9 @@ class FakeConfig:
'--verbose': False, '--verbose': False,
} }
def __init__(self):
self.webengine = False
def getoption(self, name): def getoption(self, name):
return self.ARGS[name] return self.ARGS[name]

View File

@ -242,7 +242,8 @@ class Process(QObject):
self._after_start() self._after_start()
return return
raise WaitForTimeout("Timed out while waiting for process start.") raise WaitForTimeout("Timed out while waiting for process start.\n" +
_render_log(self.captured_log))
def _start(self, args, env): def _start(self, args, env):
"""Actually start the process.""" """Actually start the process."""

View File

@ -22,23 +22,18 @@
import pytest import pytest
@pytest.mark.parametrize(['file_name', 'elem_id', 'source', 'input_text', @pytest.mark.parametrize(['file_name', 'elem_id', 'source', 'input_text'], [
'auto_insert'], [ ('textarea.html', 'qute-textarea', 'clipboard', 'qutebrowser'),
('textarea.html', 'qute-textarea', 'clipboard', 'qutebrowser', 'false'), ('textarea.html', 'qute-textarea', 'keypress', 'superqutebrowser'),
('textarea.html', 'qute-textarea', 'keypress', 'superqutebrowser', ('input.html', 'qute-input', 'clipboard', 'amazingqutebrowser'),
'false'), ('input.html', 'qute-input', 'keypress', 'awesomequtebrowser'),
('input.html', 'qute-input', 'clipboard', 'amazingqutebrowser', 'false'), ('autofocus.html', 'qute-input-autofocus', 'keypress', 'cutebrowser'),
('input.html', 'qute-input', 'keypress', 'awesomequtebrowser', 'false'),
('autofocus.html', 'qute-input-autofocus', 'keypress', 'cutebrowser',
'true'),
]) ])
@pytest.mark.parametrize('zoom', [100, 125, 250]) @pytest.mark.parametrize('zoom', [100, 125, 250])
def test_insert_mode(file_name, elem_id, source, input_text, auto_insert, zoom, def test_insert_mode(file_name, elem_id, source, input_text, zoom,
quteproc, request): quteproc, request):
url_path = 'data/insert_mode_settings/html/{}'.format(file_name) url_path = 'data/insert_mode_settings/html/{}'.format(file_name)
quteproc.open_path(url_path) quteproc.open_path(url_path)
quteproc.set_setting('input.insert_mode.auto_load', auto_insert)
quteproc.send_cmd(':zoom {}'.format(zoom)) quteproc.send_cmd(':zoom {}'.format(zoom))
quteproc.send_cmd(':click-element --force-event id {}'.format(elem_id)) quteproc.send_cmd(':click-element --force-event id {}'.format(elem_id))
@ -57,6 +52,24 @@ def test_insert_mode(file_name, elem_id, source, input_text, auto_insert, zoom,
quteproc.send_cmd(':leave-mode') quteproc.send_cmd(':leave-mode')
@pytest.mark.parametrize('auto_load, background, insert_mode', [
(False, False, False), # auto_load disabled
(True, False, True), # enabled and foreground tab
(True, True, False), # background tab
])
def test_auto_load(quteproc, auto_load, background, insert_mode):
quteproc.set_setting('input.insert_mode.auto_load', str(auto_load))
url_path = 'data/insert_mode_settings/html/autofocus.html'
quteproc.open_path(url_path, new_bg_tab=background)
log_message = 'Entering mode KeyMode.insert (reason: *)'
if insert_mode:
quteproc.wait_for(message=log_message)
quteproc.send_cmd(':leave-mode')
else:
quteproc.ensure_not_logged(message=log_message)
def test_auto_leave_insert_mode(quteproc): def test_auto_leave_insert_mode(quteproc):
url_path = 'data/insert_mode_settings/html/autofocus.html' url_path = 'data/insert_mode_settings/html/autofocus.html'
quteproc.open_path(url_path) quteproc.open_path(url_path)

View File

@ -319,3 +319,18 @@ def test_qute_settings_persistence(short_tmpdir, request, quteproc_new):
quteproc_new.start(args) quteproc_new.start(args)
assert quteproc_new.get_setting('ignore_case') == 'always' assert quteproc_new.get_setting('ignore_case') == 'always'
@pytest.mark.no_xvfb
@pytest.mark.no_ci
def test_force_software_rendering(request, quteproc_new):
"""Make sure we can force software rendering with -s."""
if not request.config.webengine:
pytest.skip("Only runs with QtWebEngine")
args = (_base_args(request.config) +
['--temp-basedir', '-s', 'force_software_rendering', 'true'])
quteproc_new.start(args)
quteproc_new.open_path('chrome://gpu')
message = 'Canvas: Software only, hardware acceleration unavailable'
assert message in quteproc_new.get_content()

View File

@ -336,6 +336,7 @@ class FakeCommand:
deprecated = attr.ib(False) deprecated = attr.ib(False)
completion = attr.ib(None) completion = attr.ib(None)
maxsplit = attr.ib(None) maxsplit = attr.ib(None)
takes_count = attr.ib(lambda: False)
class FakeTimer(QObject): class FakeTimer(QObject):
@ -417,9 +418,6 @@ class FakeYamlConfig:
self.loaded = False self.loaded = False
self._values = {} self._values = {}
def load(self):
self.loaded = True
def __contains__(self, item): def __contains__(self, item):
return item in self._values return item in self._values
@ -432,6 +430,12 @@ class FakeYamlConfig:
def __getitem__(self, key): def __getitem__(self, key):
return self._values[key] return self._values[key]
def unset(self, name):
self._values.pop(name, None)
def clear(self):
self._values = []
class StatusBarCommandStub(QLineEdit): class StatusBarCommandStub(QLineEdit):

View File

@ -36,8 +36,8 @@ def test_timeout(timer):
func2 = mock.Mock() func2 = mock.Mock()
timer.timeout.connect(func) timer.timeout.connect(func)
timer.timeout.connect(func2) timer.timeout.connect(func2)
assert not func.called func.assert_not_called()
assert not func2.called func2.assert_not_called()
timer.timeout.emit() timer.timeout.emit()
func.assert_called_once_with() func.assert_called_once_with()
func2.assert_called_once_with() func2.assert_called_once_with()
@ -49,7 +49,7 @@ def test_disconnect_all(timer):
timer.timeout.connect(func) timer.timeout.connect(func)
timer.timeout.disconnect() timer.timeout.disconnect()
timer.timeout.emit() timer.timeout.emit()
assert not func.called func.assert_not_called()
def test_disconnect_one(timer): def test_disconnect_one(timer):
@ -58,7 +58,7 @@ def test_disconnect_one(timer):
timer.timeout.connect(func) timer.timeout.connect(func)
timer.timeout.disconnect(func) timer.timeout.disconnect(func)
timer.timeout.emit() timer.timeout.emit()
assert not func.called func.assert_not_called()
def test_disconnect_all_invalid(timer): def test_disconnect_all_invalid(timer):
@ -74,8 +74,8 @@ def test_disconnect_one_invalid(timer):
timer.timeout.connect(func1) timer.timeout.connect(func1)
with pytest.raises(TypeError): with pytest.raises(TypeError):
timer.timeout.disconnect(func2) timer.timeout.disconnect(func2)
assert not func1.called func1.assert_not_called()
assert not func2.called func2.assert_not_called()
timer.timeout.emit() timer.timeout.emit()
func1.assert_called_once_with() func1.assert_called_once_with()

View File

@ -284,6 +284,22 @@ def test_import_txt(hist, data_tmpdir, monkeypatch, stubs):
assert (data_tmpdir / 'history.bak').exists() assert (data_tmpdir / 'history.bak').exists()
def test_import_txt_existing_backup(hist, data_tmpdir, monkeypatch, stubs):
monkeypatch.setattr(history, 'QTimer', stubs.InstaTimer)
histfile = data_tmpdir / 'history'
bakfile = data_tmpdir / 'history.bak'
histfile.write('12345 http://example.com/ title')
bakfile.write('12346 http://qutebrowser.org/')
hist.import_txt()
assert list(hist) == [('http://example.com/', 'title', 12345, False)]
assert not histfile.exists()
assert bakfile.read().split('\n') == ['12346 http://qutebrowser.org/',
'12345 http://example.com/ title']
@pytest.mark.parametrize('line', [ @pytest.mark.parametrize('line', [
'', '',
'#12345 http://example.com/commented', '#12345 http://example.com/commented',

View File

@ -26,7 +26,6 @@ import pytest
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject
from qutebrowser.browser import signalfilter from qutebrowser.browser import signalfilter
from qutebrowser.utils import objreg
class Signaller(QObject): class Signaller(QObject):
@ -66,18 +65,11 @@ def objects():
return Objects(signal_filter=signal_filter, signaller=signaller) return Objects(signal_filter=signal_filter, signaller=signaller)
@pytest.fixture
def tabbed_browser(stubs, win_registry):
tb = stubs.TabbedBrowserStub()
objreg.register('tabbed-browser', tb, scope='window', window=0)
yield tb
objreg.delete('tabbed-browser', scope='window', window=0)
@pytest.mark.parametrize('index_of, emitted', [(0, True), (1, False)]) @pytest.mark.parametrize('index_of, emitted', [(0, True), (1, False)])
def test_filtering(objects, tabbed_browser, index_of, emitted): def test_filtering(objects, tabbed_browser_stubs, index_of, emitted):
tabbed_browser.current_index = 0 browser = tabbed_browser_stubs[0]
tabbed_browser.index_of = index_of browser.current_index = 0
browser.index_of = index_of
objects.signaller.signal.emit('foo') objects.signaller.signal.emit('foo')
if emitted: if emitted:
assert objects.signaller.filtered_signal_arg == 'foo' assert objects.signaller.filtered_signal_arg == 'foo'
@ -86,9 +78,10 @@ def test_filtering(objects, tabbed_browser, index_of, emitted):
@pytest.mark.parametrize('index_of, verb', [(0, 'emitting'), (1, 'ignoring')]) @pytest.mark.parametrize('index_of, verb', [(0, 'emitting'), (1, 'ignoring')])
def test_logging(caplog, objects, tabbed_browser, index_of, verb): def test_logging(caplog, objects, tabbed_browser_stubs, index_of, verb):
tabbed_browser.current_index = 0 browser = tabbed_browser_stubs[0]
tabbed_browser.index_of = index_of browser.current_index = 0
browser.index_of = index_of
with caplog.at_level(logging.DEBUG, logger='signals'): with caplog.at_level(logging.DEBUG, logger='signals'):
objects.signaller.signal.emit('foo') objects.signaller.signal.emit('foo')
@ -99,9 +92,10 @@ def test_logging(caplog, objects, tabbed_browser, index_of, verb):
@pytest.mark.parametrize('index_of', [0, 1]) @pytest.mark.parametrize('index_of', [0, 1])
def test_no_logging(caplog, objects, tabbed_browser, index_of): def test_no_logging(caplog, objects, tabbed_browser_stubs, index_of):
tabbed_browser.current_index = 0 browser = tabbed_browser_stubs[0]
tabbed_browser.index_of = index_of browser.current_index = 0
browser.index_of = index_of
with caplog.at_level(logging.DEBUG, logger='signals'): with caplog.at_level(logging.DEBUG, logger='signals'):
objects.signaller.link_hovered.emit('foo') objects.signaller.link_hovered.emit('foo')
@ -109,9 +103,10 @@ def test_no_logging(caplog, objects, tabbed_browser, index_of):
assert not caplog.records assert not caplog.records
def test_runtime_error(objects, tabbed_browser): def test_runtime_error(objects, tabbed_browser_stubs):
"""Test that there's no crash if indexOf() raises RuntimeError.""" """Test that there's no crash if indexOf() raises RuntimeError."""
tabbed_browser.current_index = 0 browser = tabbed_browser_stubs[0]
tabbed_browser.index_of = RuntimeError browser.current_index = 0
browser.index_of = RuntimeError
objects.signaller.signal.emit('foo') objects.signaller.signal.emit('foo')
assert objects.signaller.filtered_signal_arg is None assert objects.signaller.filtered_signal_arg is None

View File

@ -25,7 +25,7 @@ import pytest
from PyQt5.QtCore import QUrl from PyQt5.QtCore import QUrl
from qutebrowser.commands import argparser, cmdexc from qutebrowser.commands import argparser, cmdexc
from qutebrowser.utils import usertypes, objreg from qutebrowser.utils import usertypes
Enum = usertypes.enum('Enum', ['foo', 'foo_bar']) Enum = usertypes.enum('Enum', ['foo', 'foo_bar'])
@ -37,13 +37,6 @@ class TestArgumentParser:
def parser(self): def parser(self):
return argparser.ArgumentParser('foo') return argparser.ArgumentParser('foo')
@pytest.fixture
def tabbed_browser(self, stubs, win_registry):
tb = stubs.TabbedBrowserStub()
objreg.register('tabbed-browser', tb, scope='window', window=0)
yield tb
objreg.delete('tabbed-browser', scope='window', window=0)
def test_name(self, parser): def test_name(self, parser):
assert parser.name == 'foo' assert parser.name == 'foo'
@ -60,14 +53,14 @@ class TestArgumentParser:
match="Unrecognized arguments: --foo"): match="Unrecognized arguments: --foo"):
parser.parse_args(['--foo']) parser.parse_args(['--foo'])
def test_help(self, parser, tabbed_browser): def test_help(self, parser, tabbed_browser_stubs):
parser.add_argument('--help', action=argparser.HelpAction, nargs=0) parser.add_argument('--help', action=argparser.HelpAction, nargs=0)
with pytest.raises(argparser.ArgumentParserExit): with pytest.raises(argparser.ArgumentParserExit):
parser.parse_args(['--help']) parser.parse_args(['--help'])
expected_url = QUrl('qute://help/commands.html#foo') expected_url = QUrl('qute://help/commands.html#foo')
assert tabbed_browser.opened_url == expected_url assert tabbed_browser_stubs[1].opened_url == expected_url
@pytest.mark.parametrize('types, value, expected', [ @pytest.mark.parametrize('types, value, expected', [

View File

@ -22,7 +22,6 @@
import pytest import pytest
from qutebrowser.commands import runners, cmdexc from qutebrowser.commands import runners, cmdexc
from qutebrowser.config import configtypes
class TestCommandParser: class TestCommandParser:
@ -47,7 +46,6 @@ class TestCommandParser:
if not cmdline_test.cmd: if not cmdline_test.cmd:
pytest.skip("Empty command") pytest.skip("Empty command")
monkeypatch.setattr(configtypes.Command, 'unvalidated', True)
config_stub.val.aliases = {'alias_name': cmdline_test.cmd} config_stub.val.aliases = {'alias_name': cmdline_test.cmd}
parser = runners.CommandParser() parser = runners.CommandParser()

View File

@ -120,7 +120,7 @@ def cmdutils_patch(monkeypatch, stubs, miscmodels_patch):
@cmdutils.argument('win_id', win_id=True) @cmdutils.argument('win_id', win_id=True)
@cmdutils.argument('command', completion=miscmodels_patch.command) @cmdutils.argument('command', completion=miscmodels_patch.command)
def bind(key, win_id, command=None, *, mode='normal', force=False): def bind(key, win_id, command=None, *, mode='normal'):
"""docstring.""" """docstring."""
pass pass

View File

@ -103,7 +103,7 @@ def test_delete_cur_item_no_func():
parent = model.index(0, 0) parent = model.index(0, 0)
with pytest.raises(cmdexc.CommandError): with pytest.raises(cmdexc.CommandError):
model.delete_cur_item(model.index(0, 0, parent)) model.delete_cur_item(model.index(0, 0, parent))
assert not callback.called callback.assert_not_called()
def test_delete_cur_item_no_cat(): def test_delete_cur_item_no_cat():
@ -114,4 +114,4 @@ def test_delete_cur_item_no_cat():
model.rowsRemoved.connect(callback) model.rowsRemoved.connect(callback)
with pytest.raises(qtutils.QtValueError): with pytest.raises(qtutils.QtValueError):
model.delete_cur_item(QModelIndex()) model.delete_cur_item(QModelIndex())
assert not callback.called callback.assert_not_called()

View File

@ -242,7 +242,7 @@ def test_completion_item_del_no_selection(completionview):
completionview.set_model(model) completionview.set_model(model)
with pytest.raises(cmdexc.CommandError, match='No item selected!'): with pytest.raises(cmdexc.CommandError, match='No item selected!'):
completionview.completion_item_del() completionview.completion_item_del()
assert not func.called func.assert_not_called()
def test_resize_no_model(completionview, qtbot): def test_resize_no_model(completionview, qtbot):

View File

@ -140,20 +140,24 @@ def test_sorting(max_items, before, after, model_validator, hist, config_stub):
def test_remove_rows(hist, model_validator): def test_remove_rows(hist, model_validator):
hist.insert({'url': 'foo', 'title': 'Foo'}) hist.insert({'url': 'foo', 'title': 'Foo', 'last_atime': 0})
hist.insert({'url': 'bar', 'title': 'Bar'}) hist.insert({'url': 'bar', 'title': 'Bar', 'last_atime': 0})
cat = histcategory.HistoryCategory() cat = histcategory.HistoryCategory()
model_validator.set_model(cat) model_validator.set_model(cat)
cat.set_pattern('') cat.set_pattern('')
hist.delete('url', 'foo') hist.delete('url', 'foo')
cat.removeRows(0, 1) cat.removeRows(0, 1)
model_validator.validate([('bar', 'Bar', '')]) model_validator.validate([('bar', 'Bar', '1970-01-01')])
def test_remove_rows_fetch(hist): def test_remove_rows_fetch(hist):
"""removeRows should fetch enough data to make the current index valid.""" """removeRows should fetch enough data to make the current index valid."""
# we cannot use model_validator as it will fetch everything up front # we cannot use model_validator as it will fetch everything up front
hist.insert_batch({'url': [str(i) for i in range(300)]}) hist.insert_batch({
'url': [str(i) for i in range(300)],
'title': [str(i) for i in range(300)],
'last_atime': [0] * 300,
})
cat = histcategory.HistoryCategory() cat = histcategory.HistoryCategory()
cat.set_pattern('') cat.set_pattern('')

View File

@ -119,6 +119,7 @@ def configdata_stub(monkeypatch, configdata_init):
'normal': collections.OrderedDict([ 'normal': collections.OrderedDict([
('<ctrl+q>', 'quit'), ('<ctrl+q>', 'quit'),
('ZQ', 'quit'), ('ZQ', 'quit'),
('I', 'invalid'),
]) ])
}, },
backends=[], backends=[],
@ -538,7 +539,8 @@ def test_setting_option_completion(qtmodeltester, config_stub,
"Options": [ "Options": [
('aliases', 'Aliases for commands.', '{"q": "quit"}'), ('aliases', 'Aliases for commands.', '{"q": "quit"}'),
('bindings.commands', 'Default keybindings', ('bindings.commands', 'Default keybindings',
'{"normal": {"<ctrl+q>": "quit", "ZQ": "quit"}}'), '{"normal": {"<ctrl+q>": "quit", "ZQ": "quit", '
'"I": "invalid"}}'),
('bindings.default', 'Default keybindings', ('bindings.default', 'Default keybindings',
'{"normal": {"<ctrl+q>": "quit"}}'), '{"normal": {"<ctrl+q>": "quit"}}'),
] ]
@ -573,6 +575,25 @@ def test_bind_completion(qtmodeltester, cmdutils_stub, config_stub,
}) })
def test_bind_completion_invalid(cmdutils_stub, config_stub, key_config_stub,
configdata_stub, info):
"""Test command completion with an invalid command bound."""
model = configmodel.bind('I', info=info)
model.set_pattern('')
_check_completions(model, {
"Current": [
('invalid', 'Invalid command!', 'I'),
],
"Commands": [
('open', 'open a url', ''),
('q', "Alias for 'quit'", ''),
('quit', 'quit qutebrowser', 'ZQ, <ctrl+q>'),
('scroll', 'Scroll the current tab in the given direction.', '')
],
})
def test_bind_completion_no_current(qtmodeltester, cmdutils_stub, config_stub, def test_bind_completion_no_current(qtmodeltester, cmdutils_stub, config_stub,
key_config_stub, configdata_stub, info): key_config_stub, configdata_stub, info):
"""Test keybinding completion with no current binding.""" """Test keybinding completion with no current binding."""

View File

@ -1,7 +1,6 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2017 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# Copyright 2016-2017 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser. # This file is part of qutebrowser.
# #
# qutebrowser is free software: you can redistribute it and/or modify # qutebrowser is free software: you can redistribute it and/or modify
@ -17,15 +16,14 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. # along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
import json """Fixtures needed in various config test files."""
import pytest_bdd as bdd import pytest
bdd.scenarios('adblock.feature') from qutebrowser.config import config
@bdd.when(bdd.parsers.parse('I set up "{lists}" as block lists')) @pytest.fixture
def set_up_blocking(quteproc, lists, server): def keyconf(config_stub):
url = 'http://localhost:{}/data/adblock/'.format(server.port) config_stub.val.aliases = {}
urls = [url + item.strip() for item in lists.split(',')] return config.KeyConfig(config_stub)
quteproc.set_setting('content.host_blocking.lists', json.dumps(urls))

View File

@ -18,20 +18,16 @@
"""Tests for qutebrowser.config.config.""" """Tests for qutebrowser.config.config."""
import sys
import copy import copy
import types import types
import logging
import unittest.mock import unittest.mock
import pytest import pytest
from PyQt5.QtCore import QObject, QUrl from PyQt5.QtCore import QObject
from PyQt5.QtGui import QColor from PyQt5.QtGui import QColor
from qutebrowser import qutebrowser
from qutebrowser.commands import cmdexc
from qutebrowser.config import config, configdata, configexc, configfiles from qutebrowser.config import config, configdata, configexc, configfiles
from qutebrowser.utils import objreg, usertypes from qutebrowser.utils import usertypes
from qutebrowser.misc import objects from qutebrowser.misc import objects
@ -42,18 +38,12 @@ def configdata_init():
configdata.init() configdata.init()
@pytest.fixture
def keyconf(config_stub):
config_stub.val.aliases = {}
return config.KeyConfig(config_stub)
class TestChangeFilter: class TestChangeFilter:
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def cleanup_globals(self, monkeypatch): def cleanup_globals(self, monkeypatch):
"""Make sure config._change_filters is cleaned up.""" """Make sure config.change_filters is cleaned up."""
monkeypatch.setattr(config, '_change_filters', []) monkeypatch.setattr(config, 'change_filters', [])
@pytest.mark.parametrize('option', ['foobar', 'tab', 'tabss', 'tabs.']) @pytest.mark.parametrize('option', ['foobar', 'tab', 'tabss', 'tabs.'])
def test_unknown_option(self, option): def test_unknown_option(self, option):
@ -65,7 +55,7 @@ class TestChangeFilter:
def test_validate(self, option): def test_validate(self, option):
cf = config.change_filter(option) cf = config.change_filter(option)
cf.validate() cf.validate()
assert cf in config._change_filters assert cf in config.change_filters
@pytest.mark.parametrize('method', [True, False]) @pytest.mark.parametrize('method', [True, False])
@pytest.mark.parametrize('option, changed, matches', [ @pytest.mark.parametrize('option, changed, matches', [
@ -182,38 +172,24 @@ class TestKeyConfig:
config_stub.val.bindings.commands = {'normal': bindings} config_stub.val.bindings.commands = {'normal': bindings}
assert keyconf.get_reverse_bindings_for('normal') == expected assert keyconf.get_reverse_bindings_for('normal') == expected
def test_bind_invalid_command(self, keyconf):
with pytest.raises(configexc.KeybindingError,
match='Invalid command: foobar'):
keyconf.bind('a', 'foobar', mode='normal')
def test_bind_invalid_mode(self, keyconf):
with pytest.raises(configexc.KeybindingError,
match='completion-item-del: This command is only '
'allowed in command mode, not normal.'):
keyconf.bind('a', 'completion-item-del', mode='normal')
@pytest.mark.parametrize('force', [True, False])
@pytest.mark.parametrize('key', ['a', '<Ctrl-X>', 'b']) @pytest.mark.parametrize('key', ['a', '<Ctrl-X>', 'b'])
def test_bind_duplicate(self, keyconf, config_stub, force, key): def test_bind_duplicate(self, keyconf, config_stub, key):
config_stub.val.bindings.default = {'normal': {'a': 'nop', config_stub.val.bindings.default = {'normal': {'a': 'nop',
'<Ctrl+x>': 'nop'}} '<Ctrl+x>': 'nop'}}
config_stub.val.bindings.commands = {'normal': {'b': 'nop'}} config_stub.val.bindings.commands = {'normal': {'b': 'nop'}}
if force:
keyconf.bind(key, 'message-info foo', mode='normal', force=True)
assert keyconf.get_command(key, 'normal') == 'message-info foo'
else:
with pytest.raises(configexc.DuplicateKeyError):
keyconf.bind(key, 'message-info foo', mode='normal') keyconf.bind(key, 'message-info foo', mode='normal')
assert keyconf.get_command(key, 'normal') == 'nop' assert keyconf.get_command(key, 'normal') == 'message-info foo'
@pytest.mark.parametrize('mode', ['normal', 'caret']) @pytest.mark.parametrize('mode', ['normal', 'caret'])
def test_bind(self, keyconf, config_stub, qtbot, no_bindings, mode): @pytest.mark.parametrize('command', [
'message-info foo',
'nop ;; wq', # https://github.com/qutebrowser/qutebrowser/issues/3002
])
def test_bind(self, keyconf, config_stub, qtbot, no_bindings,
mode, command):
config_stub.val.bindings.default = no_bindings config_stub.val.bindings.default = no_bindings
config_stub.val.bindings.commands = no_bindings config_stub.val.bindings.commands = no_bindings
command = 'message-info foo'
with qtbot.wait_signal(config_stub.changed): with qtbot.wait_signal(config_stub.changed):
keyconf.bind('a', command, mode=mode) keyconf.bind('a', command, mode=mode)
@ -221,6 +197,16 @@ class TestKeyConfig:
assert keyconf.get_bindings_for(mode)['a'] == command assert keyconf.get_bindings_for(mode)['a'] == command
assert keyconf.get_command('a', mode) == command assert keyconf.get_command('a', mode) == command
def test_bind_mode_changing(self, keyconf, config_stub, no_bindings):
"""Make sure we can bind to a command which changes the mode.
https://github.com/qutebrowser/qutebrowser/issues/2989
"""
config_stub.val.bindings.default = no_bindings
config_stub.val.bindings.commands = no_bindings
keyconf.bind('a', 'set-cmd-text :nop ;; rl-beginning-of-line',
mode='normal')
@pytest.mark.parametrize('key, normalized', [ @pytest.mark.parametrize('key, normalized', [
('a', 'a'), # default bindings ('a', 'a'), # default bindings
('b', 'b'), # custom bindings ('b', 'b'), # custom bindings
@ -264,335 +250,18 @@ class TestKeyConfig:
keyconf.unbind('foobar', mode='normal') keyconf.unbind('foobar', mode='normal')
class TestSetConfigCommand:
"""Tests for :set."""
@pytest.fixture
def commands(self, config_stub, keyconf):
return config.ConfigCommands(config_stub, keyconf)
@pytest.fixture
def tabbed_browser(self, stubs, win_registry):
tb = stubs.TabbedBrowserStub()
objreg.register('tabbed-browser', tb, scope='window', window=0)
yield tb
objreg.delete('tabbed-browser', scope='window', window=0)
def test_set_no_args(self, commands, tabbed_browser):
"""Run ':set'.
Should open qute://settings."""
commands.set(win_id=0)
assert tabbed_browser.opened_url == QUrl('qute://settings')
def test_get(self, config_stub, commands, message_mock):
"""Run ':set url.auto_search?'.
Should show the value.
"""
config_stub.val.url.auto_search = 'never'
commands.set(win_id=0, option='url.auto_search?')
msg = message_mock.getmsg(usertypes.MessageLevel.info)
assert msg.text == 'url.auto_search = never'
@pytest.mark.parametrize('temp', [True, False])
@pytest.mark.parametrize('option, old_value, inp, new_value', [
('url.auto_search', 'naive', 'dns', 'dns'),
# https://github.com/qutebrowser/qutebrowser/issues/2962
('editor.command', ['gvim', '-f', '{}'], '[emacs, "{}"]',
['emacs', '{}']),
])
def test_set_simple(self, monkeypatch, commands, config_stub,
temp, option, old_value, inp, new_value):
"""Run ':set [-t] option value'.
Should set the setting accordingly.
"""
monkeypatch.setattr(objects, 'backend', usertypes.Backend.QtWebKit)
assert config_stub.get(option) == old_value
commands.set(0, option, inp, temp=temp)
assert config_stub.get(option) == new_value
if temp:
assert option not in config_stub._yaml
else:
assert config_stub._yaml[option] == new_value
@pytest.mark.parametrize('temp', [True, False])
def test_set_temp_override(self, commands, config_stub, temp):
"""Invoking :set twice.
:set url.auto_search dns
:set -t url.auto_search never
Should set the setting accordingly.
"""
assert config_stub.val.url.auto_search == 'naive'
commands.set(0, 'url.auto_search', 'dns')
commands.set(0, 'url.auto_search', 'never', temp=True)
assert config_stub.val.url.auto_search == 'never'
assert config_stub._yaml['url.auto_search'] == 'dns'
def test_set_print(self, config_stub, commands, message_mock):
"""Run ':set -p url.auto_search never'.
Should set show the value.
"""
assert config_stub.val.url.auto_search == 'naive'
commands.set(0, 'url.auto_search', 'dns', print_=True)
assert config_stub.val.url.auto_search == 'dns'
msg = message_mock.getmsg(usertypes.MessageLevel.info)
assert msg.text == 'url.auto_search = dns'
def test_set_toggle(self, commands, config_stub):
"""Run ':set auto_save.session!'.
Should toggle the value.
"""
assert not config_stub.val.auto_save.session
commands.set(0, 'auto_save.session!')
assert config_stub.val.auto_save.session
assert config_stub._yaml['auto_save.session']
def test_set_toggle_nonbool(self, commands, config_stub):
"""Run ':set url.auto_search!'.
Should show an error
"""
assert config_stub.val.url.auto_search == 'naive'
with pytest.raises(cmdexc.CommandError, match="set: Can't toggle "
"non-bool setting url.auto_search"):
commands.set(0, 'url.auto_search!')
assert config_stub.val.url.auto_search == 'naive'
def test_set_toggle_print(self, commands, config_stub, message_mock):
"""Run ':set -p auto_save.session!'.
Should toggle the value and show the new value.
"""
commands.set(0, 'auto_save.session!', print_=True)
msg = message_mock.getmsg(usertypes.MessageLevel.info)
assert msg.text == 'auto_save.session = true'
def test_set_invalid_option(self, commands):
"""Run ':set foo bar'.
Should show an error.
"""
with pytest.raises(cmdexc.CommandError, match="set: No option 'foo'"):
commands.set(0, 'foo', 'bar')
def test_set_invalid_value(self, commands):
"""Run ':set auto_save.session blah'.
Should show an error.
"""
with pytest.raises(cmdexc.CommandError,
match="set: Invalid value 'blah' - must be a "
"boolean!"):
commands.set(0, 'auto_save.session', 'blah')
def test_set_wrong_backend(self, commands, monkeypatch):
monkeypatch.setattr(objects, 'backend', usertypes.Backend.QtWebEngine)
with pytest.raises(cmdexc.CommandError,
match="set: This setting is not available with the "
"QtWebEngine backend!"):
commands.set(0, 'content.cookies.accept', 'all')
@pytest.mark.parametrize('option', ['?', '!', 'url.auto_search'])
def test_empty(self, commands, option):
"""Run ':set ?' / ':set !' / ':set url.auto_search'.
Should show an error.
See https://github.com/qutebrowser/qutebrowser/issues/1109
"""
with pytest.raises(cmdexc.CommandError,
match="set: The following arguments are required: "
"value"):
commands.set(win_id=0, option=option)
@pytest.mark.parametrize('suffix', '?!')
def test_invalid(self, commands, suffix):
"""Run ':set foo?' / ':set foo!'.
Should show an error.
"""
with pytest.raises(cmdexc.CommandError, match="set: No option 'foo'"):
commands.set(win_id=0, option='foo' + suffix)
@pytest.mark.parametrize('initial, expected', [
# Normal cycling
('magenta', 'blue'),
# Through the end of the list
('yellow', 'green'),
# Value which is not in the list
('red', 'green'),
])
def test_cycling(self, commands, config_stub, initial, expected):
"""Run ':set' with multiple values."""
opt = 'colors.statusbar.normal.bg'
config_stub.set_obj(opt, initial)
commands.set(0, opt, 'green', 'magenta', 'blue', 'yellow')
assert config_stub.get(opt) == expected
assert config_stub._yaml[opt] == expected
class TestBindConfigCommand:
"""Tests for :bind and :unbind."""
@pytest.fixture
def commands(self, config_stub, keyconf):
return config.ConfigCommands(config_stub, keyconf)
@pytest.fixture
def no_bindings(self):
"""Get a dict with no bindings."""
return {'normal': {}}
@pytest.mark.parametrize('command', ['nop', 'nope'])
def test_bind(self, commands, config_stub, no_bindings, keyconf, command):
"""Simple :bind test (and aliases)."""
config_stub.val.aliases = {'nope': 'nop'}
config_stub.val.bindings.default = no_bindings
config_stub.val.bindings.commands = no_bindings
commands.bind('a', command)
assert keyconf.get_command('a', 'normal') == command
yaml_bindings = config_stub._yaml['bindings.commands']['normal']
assert yaml_bindings['a'] == command
@pytest.mark.parametrize('key, mode, expected', [
# Simple
('a', 'normal', "a is bound to 'message-info a' in normal mode"),
# Alias
('b', 'normal', "b is bound to 'mib' in normal mode"),
# Custom binding
('c', 'normal', "c is bound to 'message-info c' in normal mode"),
# Special key
('<Ctrl-X>', 'normal',
"<ctrl+x> is bound to 'message-info C-x' in normal mode"),
# unbound
('x', 'normal', "x is unbound in normal mode"),
# non-default mode
('x', 'caret', "x is bound to 'nop' in caret mode"),
])
def test_bind_print(self, commands, config_stub, message_mock,
key, mode, expected):
"""Run ':bind key'.
Should print the binding.
"""
config_stub.val.aliases = {'mib': 'message-info b'}
config_stub.val.bindings.default = {
'normal': {'a': 'message-info a',
'b': 'mib',
'<Ctrl+x>': 'message-info C-x'},
'caret': {'x': 'nop'}
}
config_stub.val.bindings.commands = {
'normal': {'c': 'message-info c'}
}
commands.bind(key, mode=mode)
msg = message_mock.getmsg(usertypes.MessageLevel.info)
assert msg.text == expected
@pytest.mark.parametrize('command, mode, expected', [
('foobar', 'normal', "bind: Invalid command: foobar"),
('completion-item-del', 'normal',
"bind: completion-item-del: This command is only allowed in "
"command mode, not normal."),
('nop', 'wrongmode', "bind: Invalid mode wrongmode!"),
])
def test_bind_invalid(self, commands, command, mode, expected):
"""Run ':bind a foobar' / ':bind a completion-item-del'.
Should show an error.
"""
with pytest.raises(cmdexc.CommandError, match=expected):
commands.bind('a', command, mode=mode)
@pytest.mark.parametrize('force', [True, False])
@pytest.mark.parametrize('key', ['a', 'b', '<Ctrl-X>'])
def test_bind_duplicate(self, commands, config_stub, keyconf, force, key):
"""Run ':bind' with a key which already has been bound.'.
Also tests for https://github.com/qutebrowser/qutebrowser/issues/1544
"""
config_stub.val.bindings.default = {
'normal': {'a': 'nop', '<Ctrl+x>': 'nop'}
}
config_stub.val.bindings.commands = {
'normal': {'b': 'nop'},
}
if force:
commands.bind(key, 'message-info foo', mode='normal', force=True)
assert keyconf.get_command(key, 'normal') == 'message-info foo'
else:
with pytest.raises(cmdexc.CommandError,
match="bind: Duplicate key .* - use --force to "
"override"):
commands.bind(key, 'message-info foo', mode='normal')
assert keyconf.get_command(key, 'normal') == 'nop'
@pytest.mark.parametrize('key, normalized', [
('a', 'a'), # default bindings
('b', 'b'), # custom bindings
('c', 'c'), # :bind then :unbind
('<Ctrl-X>', '<ctrl+x>') # normalized special binding
])
def test_unbind(self, commands, keyconf, config_stub, key, normalized):
config_stub.val.bindings.default = {
'normal': {'a': 'nop', '<ctrl+x>': 'nop'},
'caret': {'a': 'nop', '<ctrl+x>': 'nop'},
}
config_stub.val.bindings.commands = {
'normal': {'b': 'nop'},
'caret': {'b': 'nop'},
}
if key == 'c':
# Test :bind and :unbind
commands.bind(key, 'nop')
commands.unbind(key)
assert keyconf.get_command(key, 'normal') is None
yaml_bindings = config_stub._yaml['bindings.commands']['normal']
if key in 'bc':
# Custom binding
assert normalized not in yaml_bindings
else:
assert yaml_bindings[normalized] is None
@pytest.mark.parametrize('key, mode, expected', [
('foobar', 'normal',
"unbind: Can't find binding 'foobar' in normal mode"),
('x', 'wrongmode', "unbind: Invalid mode wrongmode!"),
])
def test_unbind_invalid(self, commands, key, mode, expected):
"""Run ':unbind foobar' / ':unbind x wrongmode'.
Should show an error.
"""
with pytest.raises(cmdexc.CommandError, match=expected):
commands.unbind(key, mode=mode)
class TestConfig: class TestConfig:
@pytest.fixture @pytest.fixture
def conf(self, stubs): def conf(self, config_tmpdir):
yaml_config = stubs.FakeYamlConfig() yaml_config = configfiles.YamlConfig()
return config.Config(yaml_config) return config.Config(yaml_config)
def test_init_save_manager(self, conf, fake_save_manager):
conf.init_save_manager(fake_save_manager)
fake_save_manager.add_saveable.assert_called_once_with(
'yaml-config', unittest.mock.ANY, unittest.mock.ANY)
def test_set_value(self, qtbot, conf, caplog): def test_set_value(self, qtbot, conf, caplog):
opt = conf.get_opt('tabs.show') opt = conf.get_opt('tabs.show')
with qtbot.wait_signal(conf.changed) as blocker: with qtbot.wait_signal(conf.changed) as blocker:
@ -610,19 +279,60 @@ class TestConfig:
conf._set_value(opt, 'never') conf._set_value(opt, 'never')
assert conf._values['tabs.show'] == 'never' assert conf._values['tabs.show'] == 'never'
def test_read_yaml(self, conf): @pytest.mark.parametrize('save_yaml', [True, False])
assert not conf._yaml.loaded def test_unset(self, conf, qtbot, save_yaml):
conf._yaml['content.plugins'] = True name = 'tabs.show'
conf.set_obj(name, 'never', save_yaml=True)
assert conf.get(name) == 'never'
conf.read_yaml() with qtbot.wait_signal(conf.changed):
conf.unset(name, save_yaml=save_yaml)
assert conf._yaml.loaded assert conf.get(name) == 'always'
assert conf._values['content.plugins'] is True if save_yaml:
assert name not in conf._yaml
else:
assert conf._yaml[name] == 'never'
def test_read_yaml_invalid(self, conf): def test_unset_never_set(self, conf, qtbot):
conf._yaml['foo.bar'] = True name = 'tabs.show'
assert conf.get(name) == 'always'
with qtbot.assert_not_emitted(conf.changed):
conf.unset(name)
assert conf.get(name) == 'always'
def test_unset_unknown(self, conf):
with pytest.raises(configexc.NoOptionError): with pytest.raises(configexc.NoOptionError):
conf.unset('tabs')
@pytest.mark.parametrize('save_yaml', [True, False])
def test_clear(self, conf, qtbot, save_yaml):
name1 = 'tabs.show'
name2 = 'content.plugins'
conf.set_obj(name1, 'never', save_yaml=True)
conf.set_obj(name2, True, save_yaml=True)
assert conf._values[name1] == 'never'
assert conf._values[name2] is True
with qtbot.waitSignals([conf.changed, conf.changed]) as blocker:
conf.clear(save_yaml=save_yaml)
options = {e.args[0] for e in blocker.all_signals_and_args}
assert options == {name1, name2}
if save_yaml:
assert name1 not in conf._yaml
assert name2 not in conf._yaml
else:
assert conf._yaml[name1] == 'never'
assert conf._yaml[name2] is True
def test_read_yaml(self, conf):
conf._yaml['content.plugins'] = True
conf.read_yaml() conf.read_yaml()
assert conf._values['content.plugins'] is True
def test_get_opt_valid(self, conf): def test_get_opt_valid(self, conf):
assert conf.get_opt('tabs.show') == configdata.DATA['tabs.show'] assert conf.get_opt('tabs.show') == configdata.DATA['tabs.show']
@ -722,6 +432,19 @@ class TestConfig:
assert not conf._mutables assert not conf._mutables
assert conf.get_obj(option) == new assert conf.get_obj(option) == new
def test_get_mutable_twice(self, conf):
"""Get a mutable value twice."""
option = 'content.headers.custom'
obj = conf.get_obj(option, mutable=True)
obj['X-Foo'] = 'fooval'
obj2 = conf.get_obj(option, mutable=True)
obj2['X-Bar'] = 'barval'
conf.update_mutables()
expected = {'X-Foo': 'fooval', 'X-Bar': 'barval'}
assert conf.get_obj(option) == expected
def test_get_obj_unknown_mutable(self, conf): def test_get_obj_unknown_mutable(self, conf):
"""Make sure we don't have unknown mutable types.""" """Make sure we don't have unknown mutable types."""
conf._values['aliases'] = set() # This would never happen conf._values['aliases'] = set() # This would never happen
@ -873,205 +596,3 @@ def test_set_register_stylesheet(delete, stylesheet_param, update, qtbot,
expected = 'yellow' expected = 'yellow'
assert obj.rendered_stylesheet == expected assert obj.rendered_stylesheet == expected
@pytest.fixture
def init_patch(qapp, fake_save_manager, monkeypatch, config_tmpdir,
data_tmpdir):
monkeypatch.setattr(configdata, 'DATA', None)
monkeypatch.setattr(configfiles, 'state', None)
monkeypatch.setattr(config, 'instance', None)
monkeypatch.setattr(config, 'key_instance', None)
monkeypatch.setattr(config, '_change_filters', [])
monkeypatch.setattr(config, '_init_errors', [])
# Make sure we get no SSL warning
monkeypatch.setattr(config.earlyinit, 'check_backend_ssl_support',
lambda _backend: None)
yield
try:
objreg.delete('config-commands')
except KeyError:
pass
@pytest.mark.parametrize('load_autoconfig', [True, False]) # noqa
@pytest.mark.parametrize('config_py', [True, 'error', False])
@pytest.mark.parametrize('invalid_yaml', ['42', 'unknown', False])
# pylint: disable=too-many-branches
def test_early_init(init_patch, config_tmpdir, caplog, fake_args,
load_autoconfig, config_py, invalid_yaml):
# Prepare files
autoconfig_file = config_tmpdir / 'autoconfig.yml'
config_py_file = config_tmpdir / 'config.py'
if invalid_yaml == '42':
autoconfig_file.write_text('42', 'utf-8', ensure=True)
elif invalid_yaml == 'unknown':
autoconfig_file.write_text('global:\n colors.foobar: magenta\n',
'utf-8', ensure=True)
else:
assert not invalid_yaml
autoconfig_file.write_text('global:\n colors.hints.fg: magenta\n',
'utf-8', ensure=True)
if config_py:
config_py_lines = ['c.colors.hints.bg = "red"']
if not load_autoconfig:
config_py_lines.append('config.load_autoconfig = False')
if config_py == 'error':
config_py_lines.append('c.foo = 42')
config_py_file.write_text('\n'.join(config_py_lines),
'utf-8', ensure=True)
with caplog.at_level(logging.ERROR):
config.early_init(fake_args)
# Check error messages
expected_errors = []
if config_py == 'error':
expected_errors.append(
"Errors occurred while reading config.py:\n"
" While setting 'foo': No option 'foo'")
if invalid_yaml and (load_autoconfig or not config_py):
error = "Errors occurred while reading autoconfig.yml:\n"
if invalid_yaml == '42':
error += " While loading data: Toplevel object is not a dict"
elif invalid_yaml == 'unknown':
error += " Error: No option 'colors.foobar'"
else:
assert False, invalid_yaml
expected_errors.append(error)
actual_errors = [str(err) for err in config._init_errors]
assert actual_errors == expected_errors
# Make sure things have been init'ed
objreg.get('config-commands')
assert isinstance(config.instance, config.Config)
assert isinstance(config.key_instance, config.KeyConfig)
# Check config values
if config_py and load_autoconfig and not invalid_yaml:
assert config.instance._values == {
'colors.hints.bg': 'red',
'colors.hints.fg': 'magenta',
}
elif config_py:
assert config.instance._values == {'colors.hints.bg': 'red'}
elif invalid_yaml:
assert config.instance._values == {}
else:
assert config.instance._values == {'colors.hints.fg': 'magenta'}
def test_early_init_invalid_change_filter(init_patch, fake_args):
config.change_filter('foobar')
with pytest.raises(configexc.NoOptionError):
config.early_init(fake_args)
@pytest.mark.parametrize('errors', [True, False])
def test_late_init(init_patch, monkeypatch, fake_save_manager, fake_args,
mocker, errors):
config.early_init(fake_args)
if errors:
err = configexc.ConfigErrorDesc("Error text", Exception("Exception"))
errs = configexc.ConfigFileErrors("config.py", [err])
monkeypatch.setattr(config, '_init_errors', [errs])
msgbox_mock = mocker.patch('qutebrowser.config.config.msgbox.msgbox',
autospec=True)
config.late_init(fake_save_manager)
fake_save_manager.add_saveable.assert_any_call(
'state-config', unittest.mock.ANY)
fake_save_manager.add_saveable.assert_any_call(
'yaml-config', unittest.mock.ANY)
if errors:
assert len(msgbox_mock.call_args_list) == 1
_call_posargs, call_kwargs = msgbox_mock.call_args_list[0]
text = call_kwargs['text'].strip()
assert text.startswith('Errors occurred while reading config.py:')
assert '<b>Error text</b>: Exception' in text
else:
assert not msgbox_mock.called
class TestQtArgs:
@pytest.fixture
def parser(self, mocker):
"""Fixture to provide an argparser.
Monkey-patches .exit() of the argparser so it doesn't exit on errors.
"""
parser = qutebrowser.get_argparser()
mocker.patch.object(parser, 'exit', side_effect=Exception)
return parser
@pytest.mark.parametrize('args, expected', [
# No Qt arguments
(['--debug'], [sys.argv[0]]),
# Qt flag
(['--debug', '--qt-flag', 'reverse'], [sys.argv[0], '--reverse']),
# Qt argument with value
(['--qt-arg', 'stylesheet', 'foo'],
[sys.argv[0], '--stylesheet', 'foo']),
# --qt-arg given twice
(['--qt-arg', 'stylesheet', 'foo', '--qt-arg', 'geometry', 'bar'],
[sys.argv[0], '--stylesheet', 'foo', '--geometry', 'bar']),
# --qt-flag given twice
(['--qt-flag', 'foo', '--qt-flag', 'bar'],
[sys.argv[0], '--foo', '--bar']),
])
def test_qt_args(self, config_stub, args, expected, parser):
"""Test commandline with no Qt arguments given."""
parsed = parser.parse_args(args)
assert config.qt_args(parsed) == expected
def test_qt_both(self, config_stub, parser):
"""Test commandline with a Qt argument and flag."""
args = parser.parse_args(['--qt-arg', 'stylesheet', 'foobar',
'--qt-flag', 'reverse'])
qt_args = config.qt_args(args)
assert qt_args[0] == sys.argv[0]
assert '--reverse' in qt_args
assert '--stylesheet' in qt_args
assert 'foobar' in qt_args
def test_with_settings(self, config_stub, parser):
parsed = parser.parse_args(['--qt-flag', 'foo'])
config_stub.val.qt_args = ['bar']
assert config.qt_args(parsed) == [sys.argv[0], '--foo', '--bar']
@pytest.mark.parametrize('arg, confval, can_import, is_new_webkit, used', [
# overridden by commandline arg
('webkit', 'auto', False, False, usertypes.Backend.QtWebKit),
# overridden by config
(None, 'webkit', False, False, usertypes.Backend.QtWebKit),
# WebKit available but too old
(None, 'auto', True, False, usertypes.Backend.QtWebEngine),
# WebKit available and new
(None, 'auto', True, True, usertypes.Backend.QtWebKit),
# WebKit unavailable
(None, 'auto', False, False, usertypes.Backend.QtWebEngine),
])
def test_get_backend(monkeypatch, fake_args, config_stub,
arg, confval, can_import, is_new_webkit, used):
real_import = __import__
def fake_import(name, *args, **kwargs):
if name != 'PyQt5.QtWebKit':
return real_import(name, *args, **kwargs)
if can_import:
return None
raise ImportError
fake_args.backend = arg
config_stub.val.backend = confval
monkeypatch.setattr(config.qtutils, 'is_new_qtwebkit',
lambda: is_new_webkit)
monkeypatch.setattr('builtins.__import__', fake_import)
assert config.get_backend(fake_args) == used

View File

@ -0,0 +1,479 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-2017 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 qutebrowser.config.configcommands."""
import logging
import unittest.mock
import pytest
from PyQt5.QtCore import QUrl, QProcess
from qutebrowser.config import configcommands
from qutebrowser.commands import cmdexc
from qutebrowser.utils import usertypes
from qutebrowser.misc import objects
@pytest.fixture
def commands(config_stub, keyconf):
return configcommands.ConfigCommands(config_stub, keyconf)
class TestSet:
"""Tests for :set."""
def test_set_no_args(self, commands, tabbed_browser_stubs):
"""Run ':set'.
Should open qute://settings."""
commands.set(win_id=0)
assert tabbed_browser_stubs[0].opened_url == QUrl('qute://settings')
def test_get(self, config_stub, commands, message_mock):
"""Run ':set url.auto_search?'.
Should show the value.
"""
config_stub.val.url.auto_search = 'never'
commands.set(win_id=0, option='url.auto_search?')
msg = message_mock.getmsg(usertypes.MessageLevel.info)
assert msg.text == 'url.auto_search = never'
@pytest.mark.parametrize('temp', [True, False])
@pytest.mark.parametrize('option, old_value, inp, new_value', [
('url.auto_search', 'naive', 'dns', 'dns'),
# https://github.com/qutebrowser/qutebrowser/issues/2962
('editor.command', ['gvim', '-f', '{}'], '[emacs, "{}"]',
['emacs', '{}']),
])
def test_set_simple(self, monkeypatch, commands, config_stub,
temp, option, old_value, inp, new_value):
"""Run ':set [-t] option value'.
Should set the setting accordingly.
"""
monkeypatch.setattr(objects, 'backend', usertypes.Backend.QtWebKit)
assert config_stub.get(option) == old_value
commands.set(0, option, inp, temp=temp)
assert config_stub.get(option) == new_value
if temp:
assert option not in config_stub._yaml
else:
assert config_stub._yaml[option] == new_value
@pytest.mark.parametrize('temp', [True, False])
def test_set_temp_override(self, commands, config_stub, temp):
"""Invoking :set twice.
:set url.auto_search dns
:set -t url.auto_search never
Should set the setting accordingly.
"""
assert config_stub.val.url.auto_search == 'naive'
commands.set(0, 'url.auto_search', 'dns')
commands.set(0, 'url.auto_search', 'never', temp=True)
assert config_stub.val.url.auto_search == 'never'
assert config_stub._yaml['url.auto_search'] == 'dns'
def test_set_print(self, config_stub, commands, message_mock):
"""Run ':set -p url.auto_search never'.
Should set show the value.
"""
assert config_stub.val.url.auto_search == 'naive'
commands.set(0, 'url.auto_search', 'dns', print_=True)
assert config_stub.val.url.auto_search == 'dns'
msg = message_mock.getmsg(usertypes.MessageLevel.info)
assert msg.text == 'url.auto_search = dns'
def test_set_invalid_option(self, commands):
"""Run ':set foo bar'.
Should show an error.
"""
with pytest.raises(cmdexc.CommandError, match="set: No option 'foo'"):
commands.set(0, 'foo', 'bar')
def test_set_invalid_value(self, commands):
"""Run ':set auto_save.session blah'.
Should show an error.
"""
with pytest.raises(cmdexc.CommandError,
match="set: Invalid value 'blah' - must be a "
"boolean!"):
commands.set(0, 'auto_save.session', 'blah')
def test_set_wrong_backend(self, commands, monkeypatch):
monkeypatch.setattr(objects, 'backend', usertypes.Backend.QtWebEngine)
with pytest.raises(cmdexc.CommandError,
match="set: This setting is not available with the "
"QtWebEngine backend!"):
commands.set(0, 'content.cookies.accept', 'all')
@pytest.mark.parametrize('option', ['?', '!', 'url.auto_search'])
def test_empty(self, commands, option):
"""Run ':set ?' / ':set !' / ':set url.auto_search'.
Should show an error.
See https://github.com/qutebrowser/qutebrowser/issues/1109
"""
with pytest.raises(cmdexc.CommandError,
match="set: The following arguments are required: "
"value"):
commands.set(win_id=0, option=option)
def test_invalid(self, commands):
"""Run ':set foo?'.
Should show an error.
"""
with pytest.raises(cmdexc.CommandError, match="set: No option 'foo'"):
commands.set(win_id=0, option='foo?')
class TestCycle:
"""Test :config-cycle."""
@pytest.mark.parametrize('initial, expected', [
# Normal cycling
('magenta', 'blue'),
# Through the end of the list
('yellow', 'green'),
# Value which is not in the list
('red', 'green'),
])
def test_cycling(self, commands, config_stub, initial, expected):
"""Run ':set' with multiple values."""
opt = 'colors.statusbar.normal.bg'
config_stub.set_obj(opt, initial)
commands.config_cycle(opt, 'green', 'magenta', 'blue', 'yellow')
assert config_stub.get(opt) == expected
assert config_stub._yaml[opt] == expected
def test_different_representation(self, commands, config_stub):
"""When using a different representation, cycling should work.
For example, we use [foo] which is represented as ["foo"].
"""
opt = 'qt_args'
config_stub.set_obj(opt, ['foo'])
commands.config_cycle(opt, '[foo]', '[bar]')
assert config_stub.get(opt) == ['bar']
commands.config_cycle(opt, '[foo]', '[bar]')
assert config_stub.get(opt) == ['foo']
def test_toggle(self, commands, config_stub):
"""Run ':config-cycle auto_save.session'.
Should toggle the value.
"""
assert not config_stub.val.auto_save.session
commands.config_cycle('auto_save.session')
assert config_stub.val.auto_save.session
assert config_stub._yaml['auto_save.session']
@pytest.mark.parametrize('args', [
['url.auto_search'], ['url.auto_search', 'foo']
])
def test_toggle_nonbool(self, commands, config_stub, args):
"""Run :config-cycle without a bool and 0/1 value.
:config-cycle url.auto_search
:config-cycle url.auto_search foo
Should show an error.
"""
assert config_stub.val.url.auto_search == 'naive'
with pytest.raises(cmdexc.CommandError, match="Need at least "
"two values for non-boolean settings."):
commands.config_cycle(*args)
assert config_stub.val.url.auto_search == 'naive'
def test_set_toggle_print(self, commands, config_stub, message_mock):
"""Run ':config-cycle -p auto_save.session'.
Should toggle the value and show the new value.
"""
commands.config_cycle('auto_save.session', print_=True)
msg = message_mock.getmsg(usertypes.MessageLevel.info)
assert msg.text == 'auto_save.session = true'
class TestUnsetAndClear:
"""Test :config-unset and :config-clear."""
@pytest.mark.parametrize('temp', [True, False])
def test_unset(self, commands, config_stub, temp):
name = 'tabs.show'
config_stub.set_obj(name, 'never', save_yaml=True)
commands.config_unset(name, temp=temp)
assert config_stub.get(name) == 'always'
if temp:
assert config_stub._yaml[name] == 'never'
else:
assert name not in config_stub._yaml
def test_unset_unknown_option(self, commands):
with pytest.raises(cmdexc.CommandError, match="No option 'tabs'"):
commands.config_unset('tabs')
@pytest.mark.parametrize('save', [True, False])
def test_clear(self, commands, config_stub, save):
name = 'tabs.show'
config_stub.set_obj(name, 'never', save_yaml=True)
commands.config_clear(save=save)
assert config_stub.get(name) == 'always'
if save:
assert name not in config_stub._yaml
else:
assert config_stub._yaml[name] == 'never'
class TestSource:
"""Test :config-source."""
pytestmark = pytest.mark.usefixtures('config_tmpdir', 'data_tmpdir')
@pytest.mark.parametrize('use_default_dir', [True, False])
@pytest.mark.parametrize('clear', [True, False])
def test_config_source(self, tmpdir, commands, config_stub, config_tmpdir,
use_default_dir, clear):
assert config_stub.val.content.javascript.enabled
config_stub.val.ignore_case = 'always'
if use_default_dir:
pyfile = config_tmpdir / 'config.py'
arg = None
else:
pyfile = tmpdir / 'sourced.py'
arg = str(pyfile)
pyfile.write_text('c.content.javascript.enabled = False\n',
encoding='utf-8')
commands.config_source(arg, clear=clear)
assert not config_stub.val.content.javascript.enabled
assert config_stub.val.ignore_case == ('smart' if clear else 'always')
def test_errors(self, commands, config_tmpdir):
pyfile = config_tmpdir / 'config.py'
pyfile.write_text('c.foo = 42', encoding='utf-8')
with pytest.raises(cmdexc.CommandError) as excinfo:
commands.config_source()
expected = ("Errors occurred while reading config.py:\n"
" While setting 'foo': No option 'foo'")
assert str(excinfo.value) == expected
class TestEdit:
"""Tests for :config-edit."""
def test_no_source(self, commands, mocker, config_tmpdir):
mock = mocker.patch('qutebrowser.config.configcommands.editor.'
'ExternalEditor._start_editor', autospec=True)
commands.config_edit(no_source=True)
mock.assert_called_once_with(unittest.mock.ANY)
@pytest.fixture
def patch_editor(self, mocker, config_tmpdir, data_tmpdir):
"""Write a config.py file."""
def do_patch(text):
def _write_file(editor_self):
with open(editor_self._filename, 'w', encoding='utf-8') as f:
f.write(text)
editor_self.on_proc_closed(0, QProcess.NormalExit)
return mocker.patch('qutebrowser.config.configcommands.editor.'
'ExternalEditor._start_editor', autospec=True,
side_effect=_write_file)
return do_patch
def test_with_sourcing(self, commands, config_stub, patch_editor):
assert config_stub.val.content.javascript.enabled
mock = patch_editor('c.content.javascript.enabled = False')
commands.config_edit()
mock.assert_called_once_with(unittest.mock.ANY)
assert not config_stub.val.content.javascript.enabled
def test_error(self, commands, config_stub, patch_editor, message_mock,
caplog):
patch_editor('c.foo = 42')
with caplog.at_level(logging.ERROR):
commands.config_edit()
msg = message_mock.getmsg()
expected = ("Errors occurred while reading config.py:\n"
" While setting 'foo': No option 'foo'")
assert msg.text == expected
class TestBind:
"""Tests for :bind and :unbind."""
@pytest.fixture
def no_bindings(self):
"""Get a dict with no bindings."""
return {'normal': {}}
@pytest.mark.parametrize('command', ['nop', 'nope'])
def test_bind(self, commands, config_stub, no_bindings, keyconf, command):
"""Simple :bind test (and aliases)."""
config_stub.val.aliases = {'nope': 'nop'}
config_stub.val.bindings.default = no_bindings
config_stub.val.bindings.commands = no_bindings
commands.bind('a', command)
assert keyconf.get_command('a', 'normal') == command
yaml_bindings = config_stub._yaml['bindings.commands']['normal']
assert yaml_bindings['a'] == command
@pytest.mark.parametrize('key, mode, expected', [
# Simple
('a', 'normal', "a is bound to 'message-info a' in normal mode"),
# Alias
('b', 'normal', "b is bound to 'mib' in normal mode"),
# Custom binding
('c', 'normal', "c is bound to 'message-info c' in normal mode"),
# Special key
('<Ctrl-X>', 'normal',
"<ctrl+x> is bound to 'message-info C-x' in normal mode"),
# unbound
('x', 'normal', "x is unbound in normal mode"),
# non-default mode
('x', 'caret', "x is bound to 'nop' in caret mode"),
])
def test_bind_print(self, commands, config_stub, message_mock,
key, mode, expected):
"""Run ':bind key'.
Should print the binding.
"""
config_stub.val.aliases = {'mib': 'message-info b'}
config_stub.val.bindings.default = {
'normal': {'a': 'message-info a',
'b': 'mib',
'<Ctrl+x>': 'message-info C-x'},
'caret': {'x': 'nop'}
}
config_stub.val.bindings.commands = {
'normal': {'c': 'message-info c'}
}
commands.bind(key, mode=mode)
msg = message_mock.getmsg(usertypes.MessageLevel.info)
assert msg.text == expected
def test_bind_invalid_mode(self, commands):
"""Run ':bind --mode=wrongmode nop'.
Should show an error.
"""
with pytest.raises(cmdexc.CommandError,
match='bind: Invalid mode wrongmode!'):
commands.bind('a', 'nop', mode='wrongmode')
@pytest.mark.parametrize('key', ['a', 'b', '<Ctrl-X>'])
def test_bind_duplicate(self, commands, config_stub, keyconf, key):
"""Run ':bind' with a key which already has been bound.'.
Also tests for https://github.com/qutebrowser/qutebrowser/issues/1544
"""
config_stub.val.bindings.default = {
'normal': {'a': 'nop', '<Ctrl+x>': 'nop'}
}
config_stub.val.bindings.commands = {
'normal': {'b': 'nop'},
}
commands.bind(key, 'message-info foo', mode='normal')
assert keyconf.get_command(key, 'normal') == 'message-info foo'
def test_bind_none(self, commands, config_stub):
config_stub.val.bindings.commands = None
commands.bind(',x', 'nop')
def test_unbind_none(self, commands, config_stub):
config_stub.val.bindings.commands = None
commands.unbind('H')
@pytest.mark.parametrize('key, normalized', [
('a', 'a'), # default bindings
('b', 'b'), # custom bindings
('c', 'c'), # :bind then :unbind
('<Ctrl-X>', '<ctrl+x>') # normalized special binding
])
def test_unbind(self, commands, keyconf, config_stub, key, normalized):
config_stub.val.bindings.default = {
'normal': {'a': 'nop', '<ctrl+x>': 'nop'},
'caret': {'a': 'nop', '<ctrl+x>': 'nop'},
}
config_stub.val.bindings.commands = {
'normal': {'b': 'nop'},
'caret': {'b': 'nop'},
}
if key == 'c':
# Test :bind and :unbind
commands.bind(key, 'nop')
commands.unbind(key)
assert keyconf.get_command(key, 'normal') is None
yaml_bindings = config_stub._yaml['bindings.commands']['normal']
if key in 'bc':
# Custom binding
assert normalized not in yaml_bindings
else:
assert yaml_bindings[normalized] is None
@pytest.mark.parametrize('key, mode, expected', [
('foobar', 'normal',
"unbind: Can't find binding 'foobar' in normal mode"),
('x', 'wrongmode', "unbind: Invalid mode wrongmode!"),
])
def test_unbind_invalid(self, commands, key, mode, expected):
"""Run ':unbind foobar' / ':unbind x wrongmode'.
Should show an error.
"""
with pytest.raises(cmdexc.CommandError, match=expected):
commands.unbind(key, mode=mode)

View File

@ -43,10 +43,11 @@ def test_backend_error():
assert str(e) == "This setting is not available with the QtWebKit backend!" assert str(e) == "This setting is not available with the QtWebKit backend!"
def test_duplicate_key_error(): def test_desc_with_text():
e = configexc.DuplicateKeyError('asdf') """Test ConfigErrorDesc.with_text."""
assert isinstance(e, configexc.KeybindingError) old = configexc.ConfigErrorDesc("Error text", Exception("Exception text"))
assert str(e) == "Duplicate key asdf" new = old.with_text("additional text")
assert str(new) == 'Error text (additional text): Exception text'
@pytest.fixture @pytest.fixture

View File

@ -19,15 +19,24 @@
"""Tests for qutebrowser.config.configfiles.""" """Tests for qutebrowser.config.configfiles."""
import os import os
import sys
import unittest.mock
import pytest import pytest
from qutebrowser.config import config, configfiles, configexc from qutebrowser.config import config, configfiles, configexc, configdata
from qutebrowser.utils import utils from qutebrowser.utils import utils
from PyQt5.QtCore import QSettings from PyQt5.QtCore import QSettings
@pytest.fixture(autouse=True)
def configdata_init():
"""Initialize configdata if needed."""
if configdata.DATA is None:
configdata.init()
@pytest.mark.parametrize('old_data, insert, new_data', [ @pytest.mark.parametrize('old_data, insert, new_data', [
(None, False, '[general]\n\n[geometry]\n\n'), (None, False, '[general]\n\n[geometry]\n\n'),
('[general]\nfooled = true', False, '[general]\n\n[geometry]\n\n'), ('[general]\nfooled = true', False, '[general]\n\n[geometry]\n\n'),
@ -42,6 +51,7 @@ def test_state_config(fake_save_manager, data_tmpdir,
statefile.write_text(old_data, 'utf-8') statefile.write_text(old_data, 'utf-8')
state = configfiles.StateConfig() state = configfiles.StateConfig()
state.init_save_manager(fake_save_manager)
if insert: if insert:
state['general']['newval'] = '23' state['general']['newval'] = '23'
@ -52,19 +62,27 @@ def test_state_config(fake_save_manager, data_tmpdir,
state._save() state._save()
assert statefile.read_text('utf-8') == new_data assert statefile.read_text('utf-8') == new_data
fake_save_manager.add_saveable('state-config', unittest.mock.ANY)
class TestYaml:
pytestmark = pytest.mark.usefixtures('config_tmpdir')
@pytest.fixture
def yaml(self):
return configfiles.YamlConfig()
@pytest.mark.parametrize('old_config', [ @pytest.mark.parametrize('old_config', [
None, None,
'global:\n colors.hints.fg: magenta', 'global:\n colors.hints.fg: magenta',
]) ])
@pytest.mark.parametrize('insert', [True, False]) @pytest.mark.parametrize('insert', [True, False])
def test_yaml_config(fake_save_manager, config_tmpdir, old_config, insert): def test_yaml_config(self, yaml, config_tmpdir, old_config, insert):
autoconfig = config_tmpdir / 'autoconfig.yml' autoconfig = config_tmpdir / 'autoconfig.yml'
if old_config is not None: if old_config is not None:
autoconfig.write_text(old_config, 'utf-8') autoconfig.write_text(old_config, 'utf-8')
yaml = configfiles.YamlConfig()
yaml.load() yaml.load()
if insert: if insert:
@ -87,11 +105,27 @@ def test_yaml_config(fake_save_manager, config_tmpdir, old_config, insert):
print(lines) print(lines)
# WORKAROUND for https://github.com/PyCQA/pylint/issues/574 # WORKAROUND for https://github.com/PyCQA/pylint/issues/574
if 'magenta' in (old_config or ''): # pylint: disable=superfluous-parens # pylint: disable=superfluous-parens
if 'magenta' in (old_config or ''):
assert ' colors.hints.fg: magenta' in lines assert ' colors.hints.fg: magenta' in lines
if insert: if insert:
assert ' tabs.show: never' in lines assert ' tabs.show: never' in lines
def test_init_save_manager(self, yaml, fake_save_manager):
yaml.init_save_manager(fake_save_manager)
fake_save_manager.add_saveable.assert_called_with(
'yaml-config', unittest.mock.ANY, unittest.mock.ANY)
def test_unknown_key(self, yaml, config_tmpdir):
"""An unknown setting should be deleted."""
autoconfig = config_tmpdir / 'autoconfig.yml'
autoconfig.write_text('global:\n hello: world', encoding='utf-8')
yaml.load()
yaml._save()
lines = autoconfig.read_text('utf-8').splitlines()
assert ' hello:' not in lines
@pytest.mark.parametrize('old_config', [ @pytest.mark.parametrize('old_config', [
None, None,
@ -103,16 +137,16 @@ def test_yaml_config(fake_save_manager, config_tmpdir, old_config, insert):
('confirm_quit', True), ('confirm_quit', True),
('confirm_quit', False), ('confirm_quit', False),
]) ])
def test_yaml_config_changed(fake_save_manager, config_tmpdir, old_config, def test_changed(self, yaml, qtbot, config_tmpdir, old_config, key, value):
key, value):
autoconfig = config_tmpdir / 'autoconfig.yml' autoconfig = config_tmpdir / 'autoconfig.yml'
if old_config is not None: if old_config is not None:
autoconfig.write_text(old_config, 'utf-8') autoconfig.write_text(old_config, 'utf-8')
yaml = configfiles.YamlConfig()
yaml.load() yaml.load()
with qtbot.wait_signal(yaml.changed):
yaml[key] = value yaml[key] = value
assert key in yaml assert key in yaml
assert yaml[key] == value assert yaml[key] == value
@ -124,19 +158,22 @@ def test_yaml_config_changed(fake_save_manager, config_tmpdir, old_config,
assert key in yaml assert key in yaml
assert yaml[key] == value assert yaml[key] == value
def test_iter(self, yaml):
yaml['foo'] = 23
yaml['bar'] = 42
assert list(iter(yaml)) == [('bar', 42), ('foo', 23)]
@pytest.mark.parametrize('old_config', [ @pytest.mark.parametrize('old_config', [
None, None,
'global:\n colors.hints.fg: magenta', 'global:\n colors.hints.fg: magenta',
]) ])
def test_yaml_config_unchanged(fake_save_manager, config_tmpdir, old_config): def test_unchanged(self, yaml, config_tmpdir, old_config):
autoconfig = config_tmpdir / 'autoconfig.yml' autoconfig = config_tmpdir / 'autoconfig.yml'
mtime = None mtime = None
if old_config is not None: if old_config is not None:
autoconfig.write_text(old_config, 'utf-8') autoconfig.write_text(old_config, 'utf-8')
mtime = autoconfig.stat().mtime mtime = autoconfig.stat().mtime
yaml = configfiles.YamlConfig()
yaml.load() yaml.load()
yaml._save() yaml._save()
@ -145,7 +182,6 @@ def test_yaml_config_unchanged(fake_save_manager, config_tmpdir, old_config):
else: else:
assert autoconfig.stat().mtime == mtime assert autoconfig.stat().mtime == mtime
@pytest.mark.parametrize('line, text, exception', [ @pytest.mark.parametrize('line, text, exception', [
('%', 'While parsing', 'while scanning a directive'), ('%', 'While parsing', 'while scanning a directive'),
('global: 42', 'While loading data', "'global' object is not a dict"), ('global: 42', 'While loading data', "'global' object is not a dict"),
@ -153,13 +189,10 @@ def test_yaml_config_unchanged(fake_save_manager, config_tmpdir, old_config):
"Toplevel object does not contain 'global' key"), "Toplevel object does not contain 'global' key"),
('42', 'While loading data', "Toplevel object is not a dict"), ('42', 'While loading data', "Toplevel object is not a dict"),
]) ])
def test_yaml_config_invalid(fake_save_manager, config_tmpdir, def test_invalid(self, yaml, config_tmpdir, line, text, exception):
line, text, exception):
autoconfig = config_tmpdir / 'autoconfig.yml' autoconfig = config_tmpdir / 'autoconfig.yml'
autoconfig.write_text(line, 'utf-8', ensure=True) autoconfig.write_text(line, 'utf-8', ensure=True)
yaml = configfiles.YamlConfig()
with pytest.raises(configexc.ConfigFileErrors) as excinfo: with pytest.raises(configexc.ConfigFileErrors) as excinfo:
yaml.load() yaml.load()
@ -169,8 +202,7 @@ def test_yaml_config_invalid(fake_save_manager, config_tmpdir,
assert str(error.exception).splitlines()[0] == exception assert str(error.exception).splitlines()[0] == exception
assert error.traceback is None assert error.traceback is None
def test_oserror(self, yaml, config_tmpdir):
def test_yaml_oserror(fake_save_manager, config_tmpdir):
autoconfig = config_tmpdir / 'autoconfig.yml' autoconfig = config_tmpdir / 'autoconfig.yml'
autoconfig.ensure() autoconfig.ensure()
autoconfig.chmod(0) autoconfig.chmod(0)
@ -178,7 +210,6 @@ def test_yaml_oserror(fake_save_manager, config_tmpdir):
# Docker container or similar # Docker container or similar
pytest.skip("File was still readable") pytest.skip("File was still readable")
yaml = configfiles.YamlConfig()
with pytest.raises(configexc.ConfigFileErrors) as excinfo: with pytest.raises(configexc.ConfigFileErrors) as excinfo:
yaml.load() yaml.load()
@ -188,6 +219,120 @@ def test_yaml_oserror(fake_save_manager, config_tmpdir):
assert isinstance(error.exception, OSError) assert isinstance(error.exception, OSError)
assert error.traceback is None assert error.traceback is None
def test_unset(self, yaml, qtbot, config_tmpdir):
name = 'tabs.show'
yaml[name] = 'never'
with qtbot.wait_signal(yaml.changed):
yaml.unset(name)
assert name not in yaml
def test_unset_never_set(self, yaml, qtbot, config_tmpdir):
with qtbot.assert_not_emitted(yaml.changed):
yaml.unset('tabs.show')
def test_clear(self, yaml, qtbot, config_tmpdir):
name = 'tabs.show'
yaml[name] = 'never'
with qtbot.wait_signal(yaml.changed):
yaml.clear()
assert name not in yaml
class ConfPy:
"""Helper class to get a confpy fixture."""
def __init__(self, tmpdir, filename: str = "config.py"):
self._file = tmpdir / filename
self.filename = str(self._file)
def write(self, *lines):
text = '\n'.join(lines)
self._file.write_text(text, 'utf-8', ensure=True)
def read(self, error=False):
"""Read the config.py via configfiles and check for errors."""
if error:
with pytest.raises(configexc.ConfigFileErrors) as excinfo:
configfiles.read_config_py(self.filename)
errors = excinfo.value.errors
assert len(errors) == 1
return errors[0]
else:
configfiles.read_config_py(self.filename, raising=True)
return None
def write_qbmodule(self):
self.write('import qbmodule',
'qbmodule.run(config)')
class TestConfigPyModules:
pytestmark = pytest.mark.usefixtures('config_stub', 'key_config_stub')
@pytest.fixture
def confpy(self, tmpdir, config_tmpdir, data_tmpdir):
return ConfPy(tmpdir)
@pytest.fixture
def qbmodulepy(self, tmpdir):
return ConfPy(tmpdir, filename="qbmodule.py")
@pytest.fixture(autouse=True)
def restore_sys_path(self):
old_path = sys.path.copy()
yield
sys.path = old_path
def test_bind_in_module(self, confpy, qbmodulepy, tmpdir):
qbmodulepy.write('def run(config):',
' config.bind(",a", "message-info foo", mode="normal")')
confpy.write_qbmodule()
confpy.read()
expected = {'normal': {',a': 'message-info foo'}}
assert config.instance._values['bindings.commands'] == expected
assert "qbmodule" not in sys.modules.keys()
assert tmpdir not in sys.path
def test_restore_sys_on_err(self, confpy, qbmodulepy, tmpdir):
confpy.write_qbmodule()
qbmodulepy.write('def run(config):',
' 1/0')
error = confpy.read(error=True)
assert error.text == "Unhandled exception"
assert isinstance(error.exception, ZeroDivisionError)
assert "qbmodule" not in sys.modules.keys()
assert tmpdir not in sys.path
def test_fail_on_nonexistent_module(self, confpy, qbmodulepy, tmpdir):
qbmodulepy.write('def run(config):',
' pass')
confpy.write('import foobar',
'foobar.run(config)')
error = confpy.read(error=True)
assert error.text == "Unhandled exception"
assert isinstance(error.exception, ImportError)
tblines = error.traceback.strip().splitlines()
assert tblines[0] == "Traceback (most recent call last):"
assert tblines[-1].endswith("Error: No module named 'foobar'")
def test_no_double_if_path_exists(self, confpy, qbmodulepy, tmpdir):
sys.path.insert(0, tmpdir)
confpy.write('import sys',
'if sys.path[0] in sys.path[1:]:',
' raise Exception("Path not expected")')
confpy.read()
assert sys.path.count(tmpdir) == 1
class TestConfigPy: class TestConfigPy:
@ -195,26 +340,23 @@ class TestConfigPy:
pytestmark = pytest.mark.usefixtures('config_stub', 'key_config_stub') pytestmark = pytest.mark.usefixtures('config_stub', 'key_config_stub')
class ConfPy:
"""Helper class to get a confpy fixture."""
def __init__(self, tmpdir):
self._confpy = tmpdir / 'config.py'
self.filename = str(self._confpy)
def write(self, *lines):
text = '\n'.join(lines)
self._confpy.write_text(text, 'utf-8', ensure=True)
def read(self):
"""Read the config.py via configfiles and check for errors."""
api = configfiles.read_config_py(self.filename)
assert not api.errors
@pytest.fixture @pytest.fixture
def confpy(self, tmpdir): def confpy(self, tmpdir, config_tmpdir, data_tmpdir):
return self.ConfPy(tmpdir) return ConfPy(tmpdir)
def test_assertions(self, confpy):
"""Make sure assertions in config.py work for these tests."""
confpy.write('assert False')
with pytest.raises(AssertionError):
confpy.read() # no errors=True so it gets raised
@pytest.mark.parametrize('what', ['configdir', 'datadir'])
def test_getting_dirs(self, confpy, what):
confpy.write('import pathlib',
'directory = config.{}'.format(what),
'assert isinstance(directory, pathlib.Path)',
'assert directory.exists()')
confpy.read()
@pytest.mark.parametrize('line', [ @pytest.mark.parametrize('line', [
'c.colors.hints.bg = "red"', 'c.colors.hints.bg = "red"',
@ -231,25 +373,15 @@ class TestConfigPy:
'config.get("colors.hints.fg")', 'config.get("colors.hints.fg")',
]) ])
def test_get(self, confpy, set_first, get_line): def test_get(self, confpy, set_first, get_line):
"""Test whether getting options works correctly. """Test whether getting options works correctly."""
We test this by doing the following:
- Set colors.hints.fg to some value (inside the config.py with
set_first, outside of it otherwise).
- In the config.py, read .fg and set .bg to the same value.
- Verify that .bg has been set correctly.
"""
# pylint: disable=bad-config-option # pylint: disable=bad-config-option
config.val.colors.hints.fg = 'green' config.val.colors.hints.fg = 'green'
if set_first: if set_first:
confpy.write('c.colors.hints.fg = "red"', confpy.write('c.colors.hints.fg = "red"',
'c.colors.hints.bg = {}'.format(get_line)) 'assert {} == "red"'.format(get_line))
expected = 'red'
else: else:
confpy.write('c.colors.hints.bg = {}'.format(get_line)) confpy.write('assert {} == "green"'.format(get_line))
expected = 'green'
confpy.read() confpy.read()
assert config.instance._values['colors.hints.bg'] == expected
@pytest.mark.parametrize('line, mode', [ @pytest.mark.parametrize('line, mode', [
('config.bind(",a", "message-info foo")', 'normal'), ('config.bind(",a", "message-info foo")', 'normal'),
@ -261,6 +393,29 @@ class TestConfigPy:
expected = {mode: {',a': 'message-info foo'}} expected = {mode: {',a': 'message-info foo'}}
assert config.instance._values['bindings.commands'] == expected assert config.instance._values['bindings.commands'] == expected
def test_bind_freshly_defined_alias(self, confpy):
"""Make sure we can bind to a new alias.
https://github.com/qutebrowser/qutebrowser/issues/3001
"""
confpy.write("c.aliases['foo'] = 'message-info foo'",
"config.bind(',f', 'foo')")
confpy.read()
def test_bind_duplicate_key(self, confpy):
"""Make sure overriding a keybinding works."""
confpy.write("config.bind('H', 'message-info back')")
confpy.read()
expected = {'normal': {'H': 'message-info back'}}
assert config.instance._values['bindings.commands'] == expected
def test_bind_none(self, confpy):
confpy.write("c.bindings.commands = None",
"config.bind(',x', 'nop')")
confpy.read()
expected = {'normal': {',x': 'nop'}}
assert config.instance._values['bindings.commands'] == expected
@pytest.mark.parametrize('line, key, mode', [ @pytest.mark.parametrize('line, key, mode', [
('config.unbind("o")', 'o', 'normal'), ('config.unbind("o")', 'o', 'normal'),
('config.unbind("y", mode="prompt")', 'y', 'prompt'), ('config.unbind("y", mode="prompt")', 'y', 'prompt'),
@ -278,17 +433,7 @@ class TestConfigPy:
assert config.instance._values['aliases']['foo'] == 'message-info foo' assert config.instance._values['aliases']['foo'] == 'message-info foo'
assert config.instance._values['aliases']['bar'] == 'message-info bar' assert config.instance._values['aliases']['bar'] == 'message-info bar'
def test_reading_default_location(self, config_tmpdir): def test_oserror(self, tmpdir, data_tmpdir, config_tmpdir):
(config_tmpdir / 'config.py').write_text(
'c.colors.hints.bg = "red"', 'utf-8')
configfiles.read_config_py()
assert config.instance._values['colors.hints.bg'] == 'red'
def test_reading_missing_default_location(self, config_tmpdir):
assert not (config_tmpdir / 'config.py').exists()
configfiles.read_config_py() # Should not crash
def test_oserror(self, tmpdir):
with pytest.raises(configexc.ConfigFileErrors) as excinfo: with pytest.raises(configexc.ConfigFileErrors) as excinfo:
configfiles.read_config_py(str(tmpdir / 'foo')) configfiles.read_config_py(str(tmpdir / 'foo'))
@ -305,7 +450,7 @@ class TestConfigPy:
assert len(excinfo.value.errors) == 1 assert len(excinfo.value.errors) == 1
error = excinfo.value.errors[0] error = excinfo.value.errors[0]
assert isinstance(error.exception, (TypeError, ValueError)) assert isinstance(error.exception, ValueError)
assert error.text == "Error while compiling" assert error.text == "Error while compiling"
exception_text = 'source code string cannot contain null bytes' exception_text = 'source code string cannot contain null bytes'
assert str(error.exception) == exception_text assert str(error.exception) == exception_text
@ -330,13 +475,9 @@ class TestConfigPy:
assert " ^" in tblines assert " ^" in tblines
def test_unhandled_exception(self, confpy): def test_unhandled_exception(self, confpy):
confpy.write("config.load_autoconfig = False", "1/0") confpy.write("1/0")
api = configfiles.read_config_py(confpy.filename) error = confpy.read(error=True)
assert not api.load_autoconfig
assert len(api.errors) == 1
error = api.errors[0]
assert error.text == "Unhandled exception" assert error.text == "Unhandled exception"
assert isinstance(error.exception, ZeroDivisionError) assert isinstance(error.exception, ZeroDivisionError)
@ -348,9 +489,8 @@ class TestConfigPy:
def test_config_val(self, confpy): def test_config_val(self, confpy):
"""Using config.val should not work in config.py files.""" """Using config.val should not work in config.py files."""
confpy.write("config.val.colors.hints.bg = 'red'") confpy.write("config.val.colors.hints.bg = 'red'")
api = configfiles.read_config_py(confpy.filename) error = confpy.read(error=True)
assert len(api.errors) == 1
error = api.errors[0]
assert error.text == "Unhandled exception" assert error.text == "Unhandled exception"
assert isinstance(error.exception, AttributeError) assert isinstance(error.exception, AttributeError)
message = "'ConfigAPI' object has no attribute 'val'" message = "'ConfigAPI' object has no attribute 'val'"
@ -358,13 +498,9 @@ class TestConfigPy:
@pytest.mark.parametrize('line', ["c.foo = 42", "config.set('foo', 42)"]) @pytest.mark.parametrize('line', ["c.foo = 42", "config.set('foo', 42)"])
def test_config_error(self, confpy, line): def test_config_error(self, confpy, line):
confpy.write(line, "config.load_autoconfig = False") confpy.write(line)
api = configfiles.read_config_py(confpy.filename) error = confpy.read(error=True)
assert not api.load_autoconfig
assert len(api.errors) == 1
error = api.errors[0]
assert error.text == "While setting 'foo'" assert error.text == "While setting 'foo'"
assert isinstance(error.exception, configexc.NoOptionError) assert isinstance(error.exception, configexc.NoOptionError)
assert str(error.exception) == "No option 'foo'" assert str(error.exception) == "No option 'foo'"
@ -372,16 +508,20 @@ class TestConfigPy:
def test_multiple_errors(self, confpy): def test_multiple_errors(self, confpy):
confpy.write("c.foo = 42", "config.set('foo', 42)", "1/0") confpy.write("c.foo = 42", "config.set('foo', 42)", "1/0")
api = configfiles.read_config_py(confpy.filename)
assert len(api.errors) == 3
for error in api.errors[:2]: with pytest.raises(configexc.ConfigFileErrors) as excinfo:
configfiles.read_config_py(confpy.filename)
errors = excinfo.value.errors
assert len(errors) == 3
for error in errors[:2]:
assert error.text == "While setting 'foo'" assert error.text == "While setting 'foo'"
assert isinstance(error.exception, configexc.NoOptionError) assert isinstance(error.exception, configexc.NoOptionError)
assert str(error.exception) == "No option 'foo'" assert str(error.exception) == "No option 'foo'"
assert error.traceback is None assert error.traceback is None
error = api.errors[2] error = errors[2]
assert error.text == "Unhandled exception" assert error.text == "Unhandled exception"
assert isinstance(error.exception, ZeroDivisionError) assert isinstance(error.exception, ZeroDivisionError)
assert error.traceback is not None assert error.traceback is not None

View File

@ -0,0 +1,307 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2017 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 qutebrowser.config.configinit."""
import os
import sys
import logging
import unittest.mock
import pytest
from qutebrowser import qutebrowser
from qutebrowser.config import (config, configexc, configfiles, configinit,
configdata)
from qutebrowser.utils import objreg, usertypes
@pytest.fixture
def init_patch(qapp, fake_save_manager, monkeypatch, config_tmpdir,
data_tmpdir):
monkeypatch.setattr(configfiles, 'state', None)
monkeypatch.setattr(config, 'instance', None)
monkeypatch.setattr(config, 'key_instance', None)
monkeypatch.setattr(config, 'change_filters', [])
monkeypatch.setattr(configinit, '_init_errors', None)
yield
try:
objreg.delete('config-commands')
except KeyError:
pass
@pytest.fixture
def args(fake_args):
"""Arguments needed for the config to init."""
fake_args.temp_settings = []
return fake_args
@pytest.fixture(autouse=True)
def configdata_init(monkeypatch):
"""Make sure configdata is init'ed and no test re-init's it."""
if not configdata.DATA:
configdata.init()
monkeypatch.setattr(configdata, 'init', lambda: None)
class TestEarlyInit:
@pytest.mark.parametrize('config_py', [True, 'error', False])
def test_config_py(self, init_patch, config_tmpdir, caplog, args,
config_py):
"""Test loading with only a config.py."""
config_py_file = config_tmpdir / 'config.py'
if config_py:
config_py_lines = ['c.colors.hints.bg = "red"']
if config_py == 'error':
config_py_lines.append('c.foo = 42')
config_py_file.write_text('\n'.join(config_py_lines),
'utf-8', ensure=True)
with caplog.at_level(logging.ERROR):
configinit.early_init(args)
# Check error messages
expected_errors = []
if config_py == 'error':
expected_errors.append("While setting 'foo': No option 'foo'")
if configinit._init_errors is None:
actual_errors = []
else:
actual_errors = [str(err)
for err in configinit._init_errors.errors]
assert actual_errors == expected_errors
# Make sure things have been init'ed
objreg.get('config-commands')
assert isinstance(config.instance, config.Config)
assert isinstance(config.key_instance, config.KeyConfig)
# Check config values
if config_py:
assert config.instance._values == {'colors.hints.bg': 'red'}
else:
assert config.instance._values == {}
@pytest.mark.parametrize('load_autoconfig', [True, False])
@pytest.mark.parametrize('config_py', [True, 'error', False])
@pytest.mark.parametrize('invalid_yaml', ['42', 'unknown', 'wrong-type',
False])
def test_autoconfig_yml(self, init_patch, config_tmpdir, caplog, args,
load_autoconfig, config_py, invalid_yaml):
"""Test interaction between config.py and autoconfig.yml."""
# pylint: disable=too-many-locals,too-many-branches
# Prepare files
autoconfig_file = config_tmpdir / 'autoconfig.yml'
config_py_file = config_tmpdir / 'config.py'
yaml_text = {
'42': '42',
'unknown': 'global:\n colors.foobar: magenta\n',
'wrong-type': 'global:\n tabs.position: true\n',
False: 'global:\n colors.hints.fg: magenta\n',
}
autoconfig_file.write_text(yaml_text[invalid_yaml], 'utf-8',
ensure=True)
if config_py:
config_py_lines = ['c.colors.hints.bg = "red"']
if load_autoconfig:
config_py_lines.append('config.load_autoconfig()')
if config_py == 'error':
config_py_lines.append('c.foo = 42')
config_py_file.write_text('\n'.join(config_py_lines),
'utf-8', ensure=True)
with caplog.at_level(logging.ERROR):
configinit.early_init(args)
# Check error messages
expected_errors = []
if load_autoconfig or not config_py:
suffix = ' (autoconfig.yml)' if config_py else ''
if invalid_yaml == '42':
error = ("While loading data{}: Toplevel object is not a dict"
.format(suffix))
expected_errors.append(error)
elif invalid_yaml == 'wrong-type':
error = ("Error{}: Invalid value 'True' - expected a value of "
"type str but got bool.".format(suffix))
expected_errors.append(error)
if config_py == 'error':
expected_errors.append("While setting 'foo': No option 'foo'")
if configinit._init_errors is None:
actual_errors = []
else:
actual_errors = [str(err)
for err in configinit._init_errors.errors]
assert actual_errors == expected_errors
# Check config values
if config_py and load_autoconfig and not invalid_yaml:
assert config.instance._values == {
'colors.hints.bg': 'red',
'colors.hints.fg': 'magenta',
}
elif config_py:
assert config.instance._values == {'colors.hints.bg': 'red'}
elif invalid_yaml:
assert config.instance._values == {}
else:
assert config.instance._values == {'colors.hints.fg': 'magenta'}
def test_invalid_change_filter(self, init_patch, args):
config.change_filter('foobar')
with pytest.raises(configexc.NoOptionError):
configinit.early_init(args)
def test_temp_settings_valid(self, init_patch, args):
args.temp_settings = [('colors.completion.fg', 'magenta')]
configinit.early_init(args)
assert config.instance._values['colors.completion.fg'] == 'magenta'
def test_temp_settings_invalid(self, caplog, init_patch, message_mock,
args):
"""Invalid temp settings should show an error."""
args.temp_settings = [('foo', 'bar')]
with caplog.at_level(logging.ERROR):
configinit.early_init(args)
msg = message_mock.getmsg()
assert msg.level == usertypes.MessageLevel.error
assert msg.text == "set: NoOptionError - No option 'foo'"
assert 'colors.completion.fg' not in config.instance._values
def test_force_software_rendering(self, monkeypatch, init_patch, args):
"""Setting force_software_rendering should set the environment var."""
envvar = 'QT_XCB_FORCE_SOFTWARE_OPENGL'
monkeypatch.setattr(configinit.objects, 'backend',
usertypes.Backend.QtWebEngine)
monkeypatch.delenv(envvar, raising=False)
args.temp_settings = [('force_software_rendering', 'true')]
args.backend = 'webengine'
configinit.early_init(args)
assert os.environ[envvar] == '1'
@pytest.mark.parametrize('errors', [True, False])
def test_late_init(init_patch, monkeypatch, fake_save_manager, args,
mocker, errors):
configinit.early_init(args)
if errors:
err = configexc.ConfigErrorDesc("Error text", Exception("Exception"))
errs = configexc.ConfigFileErrors("config.py", [err])
monkeypatch.setattr(configinit, '_init_errors', errs)
msgbox_mock = mocker.patch('qutebrowser.config.configinit.msgbox.msgbox',
autospec=True)
configinit.late_init(fake_save_manager)
fake_save_manager.add_saveable.assert_any_call(
'state-config', unittest.mock.ANY)
fake_save_manager.add_saveable.assert_any_call(
'yaml-config', unittest.mock.ANY, unittest.mock.ANY)
if errors:
assert len(msgbox_mock.call_args_list) == 1
_call_posargs, call_kwargs = msgbox_mock.call_args_list[0]
text = call_kwargs['text'].strip()
assert text.startswith('Errors occurred while reading config.py:')
assert '<b>Error text</b>: Exception' in text
else:
assert not msgbox_mock.called
class TestQtArgs:
@pytest.fixture
def parser(self, mocker):
"""Fixture to provide an argparser.
Monkey-patches .exit() of the argparser so it doesn't exit on errors.
"""
parser = qutebrowser.get_argparser()
mocker.patch.object(parser, 'exit', side_effect=Exception)
return parser
@pytest.mark.parametrize('args, expected', [
# No Qt arguments
(['--debug'], [sys.argv[0]]),
# Qt flag
(['--debug', '--qt-flag', 'reverse'], [sys.argv[0], '--reverse']),
# Qt argument with value
(['--qt-arg', 'stylesheet', 'foo'],
[sys.argv[0], '--stylesheet', 'foo']),
# --qt-arg given twice
(['--qt-arg', 'stylesheet', 'foo', '--qt-arg', 'geometry', 'bar'],
[sys.argv[0], '--stylesheet', 'foo', '--geometry', 'bar']),
# --qt-flag given twice
(['--qt-flag', 'foo', '--qt-flag', 'bar'],
[sys.argv[0], '--foo', '--bar']),
])
def test_qt_args(self, config_stub, args, expected, parser):
"""Test commandline with no Qt arguments given."""
parsed = parser.parse_args(args)
assert configinit.qt_args(parsed) == expected
def test_qt_both(self, config_stub, parser):
"""Test commandline with a Qt argument and flag."""
args = parser.parse_args(['--qt-arg', 'stylesheet', 'foobar',
'--qt-flag', 'reverse'])
qt_args = configinit.qt_args(args)
assert qt_args[0] == sys.argv[0]
assert '--reverse' in qt_args
assert '--stylesheet' in qt_args
assert 'foobar' in qt_args
def test_with_settings(self, config_stub, parser):
parsed = parser.parse_args(['--qt-flag', 'foo'])
config_stub.val.qt_args = ['bar']
assert configinit.qt_args(parsed) == [sys.argv[0], '--foo', '--bar']
@pytest.mark.parametrize('arg, confval, used', [
# overridden by commandline arg
('webkit', 'webengine', usertypes.Backend.QtWebKit),
# set in config
(None, 'webkit', usertypes.Backend.QtWebKit),
])
def test_get_backend(monkeypatch, args, config_stub,
arg, confval, used):
real_import = __import__
def fake_import(name, *args, **kwargs):
if name != 'PyQt5.QtWebKit':
return real_import(name, *args, **kwargs)
raise ImportError
args.backend = arg
config_stub.val.backend = confval
monkeypatch.setattr('builtins.__import__', fake_import)
assert configinit.get_backend(args) == used

View File

@ -193,7 +193,8 @@ class TestAll:
if member in [configtypes.BaseType, configtypes.MappingType, if member in [configtypes.BaseType, configtypes.MappingType,
configtypes._Numeric]: configtypes._Numeric]:
pass pass
elif member is configtypes.List: elif (member is configtypes.List or
member is configtypes.ListOrValue):
yield functools.partial(member, valtype=configtypes.Int()) yield functools.partial(member, valtype=configtypes.Int())
yield functools.partial(member, valtype=configtypes.Url()) yield functools.partial(member, valtype=configtypes.Url())
elif member is configtypes.Dict: elif member is configtypes.Dict:
@ -240,6 +241,9 @@ class TestAll:
configtypes.PercOrInt, # ditto configtypes.PercOrInt, # ditto
]: ]:
return return
if (isinstance(typ, configtypes.ListOrValue) and
isinstance(typ.valtype, configtypes.Int)):
return
assert converted == s assert converted == s
@ -250,7 +254,7 @@ class TestAll:
to_py_expected = configtypes.PaddingValues(None, None, None, None) to_py_expected = configtypes.PaddingValues(None, None, None, None)
elif isinstance(typ, configtypes.Dict): elif isinstance(typ, configtypes.Dict):
to_py_expected = {} to_py_expected = {}
elif isinstance(typ, configtypes.List): elif isinstance(typ, (configtypes.List, configtypes.ListOrValue)):
to_py_expected = [] to_py_expected = []
else: else:
to_py_expected = None to_py_expected = None
@ -366,6 +370,10 @@ class TestBaseType:
def test_to_doc(self, klass, value, expected): def test_to_doc(self, klass, value, expected):
assert klass().to_doc(value) == expected assert klass().to_doc(value) == expected
@pytest.mark.parametrize('obj', [42, '', None, 'foo'])
def test_from_obj(self, klass, obj):
assert klass(none_ok=True).from_obj(obj) == obj
class MappingSubclass(configtypes.MappingType): class MappingSubclass(configtypes.MappingType):
@ -546,6 +554,14 @@ class TestList:
with pytest.raises(configexc.ValidationError): with pytest.raises(configexc.ValidationError):
klass().from_str(val) klass().from_str(val)
@pytest.mark.parametrize('obj, expected', [
([1], [1]),
([], []),
(None, []),
])
def test_from_obj(self, klass, obj, expected):
assert klass(none_ok_outer=True).from_obj(obj) == expected
@pytest.mark.parametrize('val', [['foo'], ['foo', 'bar']]) @pytest.mark.parametrize('val', [['foo'], ['foo', 'bar']])
def test_to_py_valid(self, klass, val): def test_to_py_valid(self, klass, val):
assert klass().to_py(val) == val assert klass().to_py(val) == val
@ -670,6 +686,108 @@ class TestFlagList:
assert klass().complete() is None assert klass().complete() is None
class TestListOrValue:
@pytest.fixture
def klass(self):
return configtypes.ListOrValue
@pytest.fixture
def strtype(self):
return configtypes.String()
@pytest.mark.parametrize('val, expected', [
('["foo"]', ['foo']),
('["foo", "bar"]', ['foo', 'bar']),
('foo', 'foo'),
])
def test_from_str(self, klass, strtype, val, expected):
assert klass(strtype).from_str(val) == expected
def test_from_str_invalid(self, klass):
valtype = configtypes.String(minlen=10)
with pytest.raises(configexc.ValidationError):
klass(valtype).from_str('123')
@pytest.mark.parametrize('val, expected', [
(['foo'], ['foo']),
('foo', ['foo']),
])
def test_to_py_valid(self, klass, strtype, val, expected):
assert klass(strtype).to_py(val) == expected
@pytest.mark.parametrize('val', [[42], ['\U00010000']])
def test_to_py_invalid(self, klass, strtype, val):
with pytest.raises(configexc.ValidationError):
klass(strtype).to_py(val)
@pytest.mark.parametrize('val', [None, ['foo', 'bar'], 'abcd'])
def test_to_py_length(self, strtype, klass, val):
klass(strtype, none_ok=True, length=2).to_py(val)
@pytest.mark.parametrize('obj, expected', [
(['a'], ['a']),
([], []),
(None, []),
])
def test_from_obj(self, klass, obj, expected):
typ = klass(none_ok=True, valtype=configtypes.String())
assert typ.from_obj(obj) == expected
@pytest.mark.parametrize('val', [['a'], ['a', 'b'], ['a', 'b', 'c', 'd']])
def test_wrong_length(self, strtype, klass, val):
with pytest.raises(configexc.ValidationError,
match='Exactly 3 values need to be set!'):
klass(strtype, length=3).to_py(val)
def test_get_name(self, strtype, klass):
assert klass(strtype).get_name() == 'List of String, or String'
def test_get_valid_values(self, klass):
valid_values = configtypes.ValidValues('foo', 'bar', 'baz')
valtype = configtypes.String(valid_values=valid_values)
assert klass(valtype).get_valid_values() == valid_values
def test_to_str(self, strtype, klass):
assert klass(strtype).to_str(["a", True]) == '["a", true]'
@hypothesis.given(val=strategies.lists(strategies.just('foo')))
def test_hypothesis(self, strtype, klass, val):
typ = klass(strtype, none_ok=True)
try:
converted = typ.to_py(val)
except configexc.ValidationError:
pass
else:
expected = converted if converted else []
assert typ.to_py(typ.from_str(typ.to_str(converted))) == expected
@hypothesis.given(val=strategies.lists(strategies.just('foo')))
def test_hypothesis_text(self, strtype, klass, val):
typ = klass(strtype)
text = json.dumps(val)
try:
typ.to_str(typ.from_str(text))
except configexc.ValidationError:
pass
@pytest.mark.parametrize('val, expected', [
# simple list
(['foo', 'bar'], '\n\n- +pass:[foo]+\n- +pass:[bar]+'),
# only one value
(['foo'], '+pass:[foo]+'),
# value without list
('foo', '+pass:[foo]+'),
# empty
([], 'empty'),
(None, 'empty'),
])
def test_to_doc(self, klass, strtype, val, expected):
doc = klass(strtype).to_doc(val)
print(doc)
assert doc == expected
class TestBool: class TestBool:
TESTS = { TESTS = {
@ -718,8 +836,10 @@ class TestBool:
def test_to_str(self, klass, val, expected): def test_to_str(self, klass, val, expected):
assert klass().to_str(val) == expected assert klass().to_str(val) == expected
def test_to_doc(self, klass): @pytest.mark.parametrize('value, expected', [(True, '+pass:[true]+'),
assert klass().to_doc(True) == '+pass:[true]+' (False, '+pass:[false]+')])
def test_to_doc(self, klass, value, expected):
assert klass().to_doc(value) == expected
class TestBoolAsk: class TestBoolAsk:
@ -1072,37 +1192,10 @@ class TestCommand:
monkeypatch.setattr(configtypes, 'cmdutils', cmd_utils) monkeypatch.setattr(configtypes, 'cmdutils', cmd_utils)
monkeypatch.setattr('qutebrowser.commands.runners.cmdutils', cmd_utils) monkeypatch.setattr('qutebrowser.commands.runners.cmdutils', cmd_utils)
@pytest.fixture(autouse=True)
def patch_aliases(self, config_stub):
"""Patch the aliases setting."""
configtypes.Command.unvalidated = True
config_stub.val.aliases = {'alias': 'cmd1'}
configtypes.Command.unvalidated = False
@pytest.fixture @pytest.fixture
def klass(self): def klass(self):
return configtypes.Command return configtypes.Command
@pytest.mark.parametrize('val', ['cmd1', 'cmd2', 'cmd1 foo bar',
'cmd2 baz fish', 'alias foo'])
def test_to_py_valid(self, patch_cmdutils, klass, val):
expected = None if not val else val
assert klass().to_py(val) == expected
@pytest.mark.parametrize('val', ['cmd3', 'cmd3 foo bar', ' '])
def test_to_py_invalid(self, patch_cmdutils, klass, val):
with pytest.raises(configexc.ValidationError):
klass().to_py(val)
def test_cmdline(self, klass, cmdline_test):
"""Test some commandlines from the cmdline_test fixture."""
typ = klass()
if cmdline_test.valid:
typ.to_py(cmdline_test.cmd)
else:
with pytest.raises(configexc.ValidationError):
typ.to_py(cmdline_test.cmd)
def test_complete(self, patch_cmdutils, klass): def test_complete(self, patch_cmdutils, klass):
"""Test completion.""" """Test completion."""
items = klass().complete() items = klass().complete()
@ -1461,6 +1554,16 @@ class TestDict:
valtype=configtypes.Int()) valtype=configtypes.Int())
assert typ.from_str('{"answer": 42}') == {"answer": 42} assert typ.from_str('{"answer": 42}') == {"answer": 42}
@pytest.mark.parametrize('obj, expected', [
({'a': 'b'}, {'a': 'b'}),
({}, {}),
(None, {}),
])
def test_from_obj(self, klass, obj, expected):
d = klass(keytype=configtypes.String(), valtype=configtypes.String(),
none_ok=True)
assert d.from_obj(obj) == expected
@pytest.mark.parametrize('keytype, valtype, val', [ @pytest.mark.parametrize('keytype, valtype, val', [
(configtypes.String(), configtypes.String(), {'hello': 'world'}), (configtypes.String(), configtypes.String(), {'hello': 'world'}),
(configtypes.String(), configtypes.Int(), {'hello': 42}), (configtypes.String(), configtypes.Int(), {'hello': 42}),

View File

@ -31,6 +31,12 @@ BINDINGS = {'prompt': {'<Ctrl-a>': 'message-info ctrla',
'command': {'foo': 'message-info bar', 'command': {'foo': 'message-info bar',
'<Ctrl+X>': 'message-info ctrlx'}, '<Ctrl+X>': 'message-info ctrlx'},
'normal': {'a': 'message-info a', 'ba': 'message-info ba'}} 'normal': {'a': 'message-info a', 'ba': 'message-info ba'}}
MAPPINGS = {
'<Ctrl+a>': 'a',
'<Ctrl+b>': '<Ctrl+a>',
'x': 'a',
'b': 'a',
}
@pytest.fixture @pytest.fixture
@ -38,3 +44,4 @@ def keyinput_bindings(config_stub, key_config_stub):
"""Register some test bindings.""" """Register some test bindings."""
config_stub.val.bindings.default = {} config_stub.val.bindings.default = {}
config_stub.val.bindings.commands = dict(BINDINGS) config_stub.val.bindings.commands = dict(BINDINGS)
config_stub.val.bindings.key_mappings = dict(MAPPINGS)

View File

@ -91,8 +91,7 @@ class TestDebugLog:
]) ])
def test_split_count(config_stub, input_key, supports_count, expected): def test_split_count(config_stub, input_key, supports_count, expected):
kp = basekeyparser.BaseKeyParser(0, supports_count=supports_count) kp = basekeyparser.BaseKeyParser(0, supports_count=supports_count)
kp._keystring = input_key assert kp._split_count(input_key) == expected
assert kp._split_count() == expected
@pytest.mark.usefixtures('keyinput_bindings') @pytest.mark.usefixtures('keyinput_bindings')
@ -165,20 +164,14 @@ class TestSpecialKeys:
keyparser._read_config('prompt') keyparser._read_config('prompt')
def test_valid_key(self, fake_keyevent_factory, keyparser): def test_valid_key(self, fake_keyevent_factory, keyparser):
if utils.is_mac: modifier = Qt.MetaModifier if utils.is_mac else Qt.ControlModifier
modifier = Qt.MetaModifier
else:
modifier = Qt.ControlModifier
keyparser.handle(fake_keyevent_factory(Qt.Key_A, modifier)) keyparser.handle(fake_keyevent_factory(Qt.Key_A, modifier))
keyparser.handle(fake_keyevent_factory(Qt.Key_X, modifier)) keyparser.handle(fake_keyevent_factory(Qt.Key_X, modifier))
keyparser.execute.assert_called_once_with( keyparser.execute.assert_called_once_with(
'message-info ctrla', keyparser.Type.special, None) 'message-info ctrla', keyparser.Type.special, None)
def test_valid_key_count(self, fake_keyevent_factory, keyparser): def test_valid_key_count(self, fake_keyevent_factory, keyparser):
if utils.is_mac: modifier = Qt.MetaModifier if utils.is_mac else Qt.ControlModifier
modifier = Qt.MetaModifier
else:
modifier = Qt.ControlModifier
keyparser.handle(fake_keyevent_factory(5, text='5')) keyparser.handle(fake_keyevent_factory(5, text='5'))
keyparser.handle(fake_keyevent_factory(Qt.Key_A, modifier, text='A')) keyparser.handle(fake_keyevent_factory(Qt.Key_A, modifier, text='A'))
keyparser.execute.assert_called_once_with( keyparser.execute.assert_called_once_with(
@ -199,6 +192,22 @@ class TestSpecialKeys:
keyparser.handle(fake_keyevent_factory(Qt.Key_A, Qt.NoModifier)) keyparser.handle(fake_keyevent_factory(Qt.Key_A, Qt.NoModifier))
assert not keyparser.execute.called assert not keyparser.execute.called
def test_mapping(self, config_stub, fake_keyevent_factory, keyparser):
modifier = Qt.MetaModifier if utils.is_mac else Qt.ControlModifier
keyparser.handle(fake_keyevent_factory(Qt.Key_B, modifier))
keyparser.execute.assert_called_once_with(
'message-info ctrla', keyparser.Type.special, None)
def test_binding_and_mapping(self, config_stub, fake_keyevent_factory,
keyparser):
"""with a conflicting binding/mapping, the binding should win."""
modifier = Qt.MetaModifier if utils.is_mac else Qt.ControlModifier
keyparser.handle(fake_keyevent_factory(Qt.Key_A, modifier))
keyparser.execute.assert_called_once_with(
'message-info ctrla', keyparser.Type.special, None)
class TestKeyChain: class TestKeyChain:
@ -230,7 +239,7 @@ class TestKeyChain:
handle_text((Qt.Key_X, 'x'), handle_text((Qt.Key_X, 'x'),
# Then start the real chain # Then start the real chain
(Qt.Key_B, 'b'), (Qt.Key_A, 'a')) (Qt.Key_B, 'b'), (Qt.Key_A, 'a'))
keyparser.execute.assert_called_once_with( keyparser.execute.assert_called_with(
'message-info ba', keyparser.Type.chain, None) 'message-info ba', keyparser.Type.chain, None)
assert keyparser._keystring == '' assert keyparser._keystring == ''
@ -249,6 +258,16 @@ class TestKeyChain:
handle_text((Qt.Key_C, 'c')) handle_text((Qt.Key_C, 'c'))
assert keyparser._keystring == '' assert keyparser._keystring == ''
def test_mapping(self, config_stub, handle_text, keyparser):
handle_text((Qt.Key_X, 'x'))
keyparser.execute.assert_called_once_with(
'message-info a', keyparser.Type.chain, None)
def test_binding_and_mapping(self, config_stub, handle_text, keyparser):
"""with a conflicting binding/mapping, the binding should win."""
handle_text((Qt.Key_B, 'b'))
assert not keyparser.execute.called
class TestCount: class TestCount:

Some files were not shown because too many files have changed in this diff Show More