Merge branch 'master' into spell
This commit is contained in:
commit
e20ad95666
@ -27,6 +27,7 @@ exclude scripts/asciidoc2html.py
|
||||
exclude doc/notes
|
||||
recursive-exclude doc *.asciidoc
|
||||
include doc/qutebrowser.1.asciidoc
|
||||
include doc/changelog.asciidoc
|
||||
prune tests
|
||||
prune qutebrowser/3rdparty
|
||||
prune misc/requirements
|
||||
|
@ -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/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://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"]
|
||||
|
@ -39,7 +39,10 @@ Breaking changes
|
||||
v0.9.0) is now not supported anymore.
|
||||
- Upgrading qutebrowser with a version older than v0.4.0 still running now won't
|
||||
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
|
||||
~~~~~~~~~~~~~
|
||||
@ -57,6 +60,14 @@ Added
|
||||
- New `backend` setting to select the backend to use (auto/webengine/webkit).
|
||||
Together with the previous setting, this should make wrapper scripts
|
||||
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
|
||||
~~~~~~~
|
||||
@ -66,12 +77,20 @@ Changed
|
||||
- When there are multiple messages shown, the timeout is increased.
|
||||
- `:search` now only clears the search if one was displayed before, so pressing
|
||||
`<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
|
||||
~~~~~
|
||||
|
||||
- Exiting fullscreen via `:fullscreen` or buttons on a page now
|
||||
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)
|
||||
--------------------
|
||||
|
@ -31,6 +31,11 @@ It is possible to run or bind multiple commands by separating them with `;;`.
|
||||
|<<bookmark-load,bookmark-load>>|Load a bookmark.
|
||||
|<<buffer,buffer>>|Select tab by index or url/title best match.
|
||||
|<<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-cancel,download-cancel>>|Cancel the last/[count]th download.
|
||||
|<<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.
|
||||
|<<unbind,unbind>>|Unbind a keychain.
|
||||
|<<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.
|
||||
|<<window-only,window-only>>|Close all windows except for the current one.
|
||||
|<<yank,yank>>|Yank something to the clipboard or primary selection.
|
||||
@ -115,7 +121,7 @@ How many pages to go back.
|
||||
|
||||
[[bind]]
|
||||
=== bind
|
||||
Syntax: +:bind [*--mode* 'mode'] [*--force*] 'key' ['command']+
|
||||
Syntax: +:bind [*--mode* 'mode'] 'key' ['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
|
||||
available modes.
|
||||
|
||||
* +*-f*+, +*--force*+: Rebind the key if it is already bound.
|
||||
|
||||
==== note
|
||||
* This command does not split arguments after the last argument and handles quotes literally.
|
||||
@ -184,20 +189,83 @@ Load a bookmark.
|
||||
|
||||
[[buffer]]
|
||||
=== buffer
|
||||
Syntax: +:buffer 'index'+
|
||||
Syntax: +:buffer ['index']+
|
||||
|
||||
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
|
||||
* +'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 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
|
||||
Syntax: +:download [*--mhtml*] [*--dest* 'dest'] ['url'] ['dest-old']+
|
||||
@ -773,15 +841,15 @@ Save a session.
|
||||
|
||||
[[set]]
|
||||
=== set
|
||||
Syntax: +:set [*--temp*] [*--print*] ['option'] ['values' ['values' ...]]+
|
||||
Syntax: +:set [*--temp*] [*--print*] ['option'] ['value']+
|
||||
|
||||
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
|
||||
* +'option'+: The name of the option.
|
||||
* +'values'+: The value to set, or the values to cycle through.
|
||||
* +'value'+: The value to set.
|
||||
|
||||
==== optional arguments
|
||||
* +*-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
|
||||
Syntax: +:set-cmd-text [*--space*] [*--append*] 'text'+
|
||||
Syntax: +:set-cmd-text [*--space*] [*--append*] [*--run-on-count*] 'text'+
|
||||
|
||||
Preset the statusbar to some text.
|
||||
|
||||
@ -799,6 +867,11 @@ Preset the statusbar to some text.
|
||||
==== optional arguments
|
||||
* +*-s*+, +*--space*+: If given, a space is added to the end.
|
||||
* +*-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
|
||||
* 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
|
||||
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
|
||||
The tab index to pin or unpin
|
||||
@ -948,6 +1021,10 @@ Unbind a keychain.
|
||||
=== 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.
|
||||
@ -1068,8 +1145,8 @@ How many steps to zoom out.
|
||||
|<<run-with-count,run-with-count>>|Run a command with the given count.
|
||||
|<<scroll,scroll>>|Scroll the current tab in the given direction.
|
||||
|<<scroll-page,scroll-page>>|Scroll the frame page-wise.
|
||||
|<<scroll-perc,scroll-perc>>|Scroll to a specific percentage of the page.
|
||||
|<<scroll-px,scroll-px>>|Scroll the current tab by 'count * dx/dy' pixels.
|
||||
|<<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-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.
|
||||
@ -1489,9 +1566,22 @@ Scroll the frame page-wise.
|
||||
==== count
|
||||
multiplier
|
||||
|
||||
[[scroll-perc]]
|
||||
=== scroll-perc
|
||||
Syntax: +:scroll-perc [*--horizontal*] ['perc']+
|
||||
[[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
|
||||
|
||||
[[scroll-to-perc]]
|
||||
=== scroll-to-perc
|
||||
Syntax: +:scroll-to-perc [*--horizontal*] ['perc']+
|
||||
|
||||
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
|
||||
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
|
||||
Continue the search to the ([count]th) next term.
|
||||
|
@ -11,8 +11,9 @@ Migrating older configurations
|
||||
------------------------------
|
||||
|
||||
qutebrowser does no automatic migration for the new configuration. However,
|
||||
there's a special link:qute://configdiff[] page in qutebrowser, which will show
|
||||
you the changes you did in your old configuration, compared to the old defaults.
|
||||
there's a special link:qute://configdiff/old[configdiff] page in qutebrowser,
|
||||
which will show you the changes you did in your old configuration, compared to
|
||||
the old defaults.
|
||||
|
||||
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
|
||||
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}`
|
||||
- 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
|
||||
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
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
`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:
|
||||
[source,python]
|
||||
@ -110,7 +111,7 @@ accepted values depend on the type of the option. Commonly used are:
|
||||
- Dictionaries:
|
||||
* `c.headers.custom = {'X-Hello': 'World', 'X-Awesome': 'yes'}` to override
|
||||
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:
|
||||
* `c.url.start_pages = ["https://www.qutebrowser.org/"]` to override any
|
||||
previous elements.
|
||||
@ -136,6 +137,8 @@ If you want to set settings based on their name as a string, use the
|
||||
.config.py:
|
||||
[source,python]
|
||||
----
|
||||
# Equivalent to:
|
||||
# c.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]
|
||||
----
|
||||
# Equivalent to:
|
||||
# color = c.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')
|
||||
----
|
||||
|
||||
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):
|
||||
|
||||
[source,python]
|
||||
@ -198,17 +196,52 @@ config.bind(',v', 'spawn mpv {url}')
|
||||
To suppress loading of any default keybindings, you can set
|
||||
`c.bindings.default = {}`.
|
||||
|
||||
Prevent loading `autoconfig.yml`
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Loading `autoconfig.yml`
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
If you want all customization done via `:set`, `:bind` and `:unbind` to be
|
||||
temporary, you can suppress loading `autoconfig.yml` in your `config.py` by
|
||||
doing:
|
||||
By default, all customization done via `:set`, `:bind` and `:unbind` is
|
||||
temporary as soon as a `config.py` exists. The settings done that way are always
|
||||
saved in the `autoconfig.yml` file, but you'll need to explicitly load it in
|
||||
your `config.py` by doing:
|
||||
|
||||
.config.py:
|
||||
[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
|
||||
@ -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
|
||||
confusing or you think qutebrowser could handle better, please
|
||||
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
|
||||
----
|
||||
|
@ -7,7 +7,7 @@ Documentation
|
||||
The following help pages are currently available:
|
||||
|
||||
* link:../quickstart.html[Quick start guide]
|
||||
* link:../doc.html[Frequently asked questions]
|
||||
* link:../faq.html[Frequently asked questions]
|
||||
* link:../changelog.html[Change Log]
|
||||
* link:commands.html[Documentation of commands]
|
||||
* link:configuring.html[Configuring qutebrowser]
|
||||
|
@ -55,6 +55,7 @@
|
||||
|<<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.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.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.
|
||||
@ -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.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.
|
||||
|<<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_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.
|
||||
@ -201,7 +203,7 @@
|
||||
|<<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.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).
|
||||
|<<messages.timeout,messages.timeout>>|Time (in ms) to show messages in the statusbar for.
|
||||
|<<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.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.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.
|
||||
|<<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.
|
||||
@ -284,24 +285,24 @@ Valid values:
|
||||
* +true+
|
||||
* +false+
|
||||
|
||||
Default: empty
|
||||
Default: +pass:[false]+
|
||||
|
||||
[[backend]]
|
||||
=== backend
|
||||
The backend to use to display websites.
|
||||
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.
|
||||
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.
|
||||
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. 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>>
|
||||
|
||||
Valid values:
|
||||
|
||||
* +auto+: Automatically select either QtWebEngine or QtWebKit
|
||||
* +webkit+: Force QtWebKit
|
||||
* +webengine+: Force QtWebEngine
|
||||
* +webengine+: Use QtWebEngine (based on Chromium)
|
||||
* +webkit+: Use QtWebKit (based on WebKit, similar to Safari)
|
||||
|
||||
Default: +pass:[auto]+
|
||||
Default: +pass:[webengine]+
|
||||
|
||||
[[bindings.commands]]
|
||||
=== bindings.commands
|
||||
@ -506,7 +507,7 @@ Default:
|
||||
* +pass:[B]+: +pass:[set-cmd-text -s :quickmark-load -t]+
|
||||
* +pass:[D]+: +pass:[tab-close -o]+
|
||||
* +pass:[F]+: +pass:[hint all tab]+
|
||||
* +pass:[G]+: +pass:[scroll-perc]+
|
||||
* +pass:[G]+: +pass:[scroll-to-perc]+
|
||||
* +pass:[H]+: +pass:[back]+
|
||||
* +pass:[J]+: +pass:[tab-next]+
|
||||
* +pass:[K]+: +pass:[tab-prev]+
|
||||
@ -541,7 +542,7 @@ Default:
|
||||
* +pass:[gb]+: +pass:[set-cmd-text -s :bookmark-load]+
|
||||
* +pass:[gd]+: +pass:[download]+
|
||||
* +pass:[gf]+: +pass:[view-source]+
|
||||
* +pass:[gg]+: +pass:[scroll-perc 0]+
|
||||
* +pass:[gg]+: +pass:[scroll-to-perc 0]+
|
||||
* +pass:[gl]+: +pass:[tab-move -]+
|
||||
* +pass:[gm]+: +pass:[tab-move]+
|
||||
* +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.
|
||||
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.
|
||||
Note that when a key is bound (via `bindings.default` or `bindings.commands`), the mapping is ignored.
|
||||
|
||||
Type: <<types,Dict>>
|
||||
|
||||
@ -966,7 +968,15 @@ Background color for prompts.
|
||||
|
||||
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
|
||||
@ -982,7 +992,7 @@ Background color for the selected item in filename prompts.
|
||||
|
||||
Type: <<types,QssColor>>
|
||||
|
||||
Default: +pass:[#308cc6]+
|
||||
Default: +pass:[grey]+
|
||||
|
||||
[[colors.statusbar.caret.bg]]
|
||||
=== colors.statusbar.caret.bg
|
||||
@ -1342,7 +1352,7 @@ Valid values:
|
||||
* +true+
|
||||
* +false+
|
||||
|
||||
Default: empty
|
||||
Default: +pass:[false]+
|
||||
|
||||
[[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>>
|
||||
|
||||
Default: empty
|
||||
Default: +pass:[0]+
|
||||
|
||||
This setting is only available with the QtWebKit backend.
|
||||
|
||||
@ -1466,7 +1476,7 @@ Valid values:
|
||||
* +true+
|
||||
* +false+
|
||||
|
||||
Default: empty
|
||||
Default: +pass:[false]+
|
||||
|
||||
This setting is only available with the QtWebKit backend.
|
||||
|
||||
@ -1497,7 +1507,7 @@ Valid values:
|
||||
* +true+
|
||||
* +false+
|
||||
|
||||
Default: empty
|
||||
Default: +pass:[false]+
|
||||
|
||||
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).
|
||||
|
||||
|
||||
Type: <<types,List>>
|
||||
Type: <<types,List of Url>>
|
||||
|
||||
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.
|
||||
Local domains are always exempt from hostblocking.
|
||||
|
||||
Type: <<types,List>>
|
||||
Type: <<types,List of String>>
|
||||
|
||||
Default:
|
||||
|
||||
@ -1628,7 +1638,7 @@ Valid values:
|
||||
* +true+
|
||||
* +false+
|
||||
|
||||
Default: empty
|
||||
Default: +pass:[false]+
|
||||
|
||||
[[content.images]]
|
||||
=== content.images
|
||||
@ -1668,7 +1678,7 @@ Valid values:
|
||||
* +true+
|
||||
* +false+
|
||||
|
||||
Default: empty
|
||||
Default: +pass:[false]+
|
||||
|
||||
[[content.javascript.can_close_tabs]]
|
||||
=== content.javascript.can_close_tabs
|
||||
@ -1681,7 +1691,7 @@ Valid values:
|
||||
* +true+
|
||||
* +false+
|
||||
|
||||
Default: empty
|
||||
Default: +pass:[false]+
|
||||
|
||||
This setting is only available with the QtWebKit backend.
|
||||
|
||||
@ -1696,7 +1706,7 @@ Valid values:
|
||||
* +true+
|
||||
* +false+
|
||||
|
||||
Default: empty
|
||||
Default: +pass:[false]+
|
||||
|
||||
[[content.javascript.enabled]]
|
||||
=== content.javascript.enabled
|
||||
@ -1737,7 +1747,7 @@ Valid values:
|
||||
* +true+
|
||||
* +false+
|
||||
|
||||
Default: empty
|
||||
Default: +pass:[false]+
|
||||
|
||||
[[content.javascript.prompt]]
|
||||
=== content.javascript.prompt
|
||||
@ -1776,7 +1786,7 @@ Valid values:
|
||||
* +true+
|
||||
* +false+
|
||||
|
||||
Default: empty
|
||||
Default: +pass:[false]+
|
||||
|
||||
[[content.local_storage]]
|
||||
=== content.local_storage
|
||||
@ -1842,7 +1852,7 @@ Valid values:
|
||||
* +true+
|
||||
* +false+
|
||||
|
||||
Default: empty
|
||||
Default: +pass:[false]+
|
||||
|
||||
This setting is only available with the QtWebKit backend.
|
||||
|
||||
@ -1857,7 +1867,7 @@ Valid values:
|
||||
* +true+
|
||||
* +false+
|
||||
|
||||
Default: empty
|
||||
Default: +pass:[false]+
|
||||
|
||||
[[content.print_element_backgrounds]]
|
||||
=== content.print_element_backgrounds
|
||||
@ -1885,7 +1895,7 @@ Valid values:
|
||||
* +true+
|
||||
* +false+
|
||||
|
||||
Default: empty
|
||||
Default: +pass:[false]+
|
||||
|
||||
[[content.proxy]]
|
||||
=== content.proxy
|
||||
@ -1934,7 +1944,7 @@ Default: +pass:[ask]+
|
||||
=== content.user_stylesheets
|
||||
A list of user stylesheet filenames to use.
|
||||
|
||||
Type: <<types,List>>
|
||||
Type: <<types,List of File, or File>>
|
||||
|
||||
Default: empty
|
||||
|
||||
@ -1954,7 +1964,7 @@ Default: +pass:[true]+
|
||||
[[content.xss_auditing]]
|
||||
=== content.xss_auditing
|
||||
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>>
|
||||
|
||||
@ -1963,7 +1973,7 @@ Valid values:
|
||||
* +true+
|
||||
* +false+
|
||||
|
||||
Default: empty
|
||||
Default: +pass:[false]+
|
||||
|
||||
[[downloads.location.directory]]
|
||||
=== downloads.location.directory
|
||||
@ -2143,7 +2153,7 @@ Default: +pass:[8pt monospace]+
|
||||
[[fonts.monospace]]
|
||||
=== fonts.monospace
|
||||
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>>
|
||||
|
||||
@ -2243,7 +2253,7 @@ The hard minimum font size.
|
||||
|
||||
Type: <<types,Int>>
|
||||
|
||||
Default: empty
|
||||
Default: +pass:[0]+
|
||||
|
||||
[[fonts.web.size.minimum_logical]]
|
||||
=== fonts.web.size.minimum_logical
|
||||
@ -2253,6 +2263,22 @@ Type: <<types,Int>>
|
||||
|
||||
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
|
||||
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>>
|
||||
|
||||
Default: empty
|
||||
Default: +pass:[0]+
|
||||
|
||||
[[hints.border]]
|
||||
=== hints.border
|
||||
@ -2354,7 +2380,7 @@ Default: +pass:[letter]+
|
||||
=== hints.next_regexes
|
||||
A comma-separated list of regexes to use for 'next' links.
|
||||
|
||||
Type: <<types,List>>
|
||||
Type: <<types,List of Regex>>
|
||||
|
||||
Default:
|
||||
|
||||
@ -2369,7 +2395,7 @@ Default:
|
||||
=== hints.prev_regexes
|
||||
A comma-separated list of regexes to use for 'prev' links.
|
||||
|
||||
Type: <<types,List>>
|
||||
Type: <<types,List of Regex>>
|
||||
|
||||
Default:
|
||||
|
||||
@ -2404,7 +2430,7 @@ Valid values:
|
||||
* +true+
|
||||
* +false+
|
||||
|
||||
Default: empty
|
||||
Default: +pass:[false]+
|
||||
|
||||
[[history_gap_interval]]
|
||||
=== history_gap_interval
|
||||
@ -2467,7 +2493,7 @@ Valid values:
|
||||
* +true+
|
||||
* +false+
|
||||
|
||||
Default: empty
|
||||
Default: +pass:[false]+
|
||||
|
||||
[[input.insert_mode.plugins]]
|
||||
=== input.insert_mode.plugins
|
||||
@ -2480,7 +2506,7 @@ Valid values:
|
||||
* +true+
|
||||
* +false+
|
||||
|
||||
Default: empty
|
||||
Default: +pass:[false]+
|
||||
|
||||
[[input.links_included_in_focus_chain]]
|
||||
=== input.links_included_in_focus_chain
|
||||
@ -2516,7 +2542,7 @@ Valid values:
|
||||
* +true+
|
||||
* +false+
|
||||
|
||||
Default: empty
|
||||
Default: +pass:[false]+
|
||||
|
||||
[[input.spatial_navigation]]
|
||||
=== input.spatial_navigation
|
||||
@ -2530,14 +2556,14 @@ Valid values:
|
||||
* +true+
|
||||
* +false+
|
||||
|
||||
Default: empty
|
||||
Default: +pass:[false]+
|
||||
|
||||
[[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.
|
||||
|
||||
Type: <<types,List>>
|
||||
Type: <<types,List of String>>
|
||||
|
||||
Default: empty
|
||||
|
||||
@ -2569,7 +2595,7 @@ Valid values:
|
||||
* +true+
|
||||
* +false+
|
||||
|
||||
Default: empty
|
||||
Default: +pass:[false]+
|
||||
|
||||
[[new_instance_open_target]]
|
||||
=== new_instance_open_target
|
||||
@ -2630,8 +2656,9 @@ Default: +pass:[8]+
|
||||
=== qt_args
|
||||
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.
|
||||
This setting requires a restart.
|
||||
|
||||
Type: <<types,List>>
|
||||
Type: <<types,List of String>>
|
||||
|
||||
Default: empty
|
||||
|
||||
@ -2646,7 +2673,7 @@ Valid values:
|
||||
* +true+
|
||||
* +false+
|
||||
|
||||
Default: empty
|
||||
Default: +pass:[false]+
|
||||
|
||||
[[scrolling.smooth]]
|
||||
=== scrolling.smooth
|
||||
@ -2660,7 +2687,7 @@ Valid values:
|
||||
* +true+
|
||||
* +false+
|
||||
|
||||
Default: empty
|
||||
Default: +pass:[false]+
|
||||
|
||||
[[session_default_name]]
|
||||
=== session_default_name
|
||||
@ -2682,7 +2709,7 @@ Valid values:
|
||||
* +true+
|
||||
* +false+
|
||||
|
||||
Default: empty
|
||||
Default: +pass:[false]+
|
||||
|
||||
[[statusbar.padding]]
|
||||
=== statusbar.padding
|
||||
@ -2693,8 +2720,8 @@ Type: <<types,Padding>>
|
||||
Default:
|
||||
|
||||
- +pass:[bottom]+: +pass:[1]+
|
||||
- +pass:[left]+: empty
|
||||
- +pass:[right]+: empty
|
||||
- +pass:[left]+: +pass:[0]+
|
||||
- +pass:[right]+: +pass:[0]+
|
||||
- +pass:[top]+: +pass:[1]+
|
||||
|
||||
[[statusbar.position]]
|
||||
@ -2721,7 +2748,7 @@ Valid values:
|
||||
* +true+
|
||||
* +false+
|
||||
|
||||
Default: empty
|
||||
Default: +pass:[false]+
|
||||
|
||||
[[tabs.close_mouse_button]]
|
||||
=== tabs.close_mouse_button
|
||||
@ -2768,7 +2795,7 @@ Type: <<types,Padding>>
|
||||
Default:
|
||||
|
||||
- +pass:[bottom]+: +pass:[2]+
|
||||
- +pass:[left]+: empty
|
||||
- +pass:[left]+: +pass:[0]+
|
||||
- +pass:[right]+: +pass:[4]+
|
||||
- +pass:[top]+: +pass:[2]+
|
||||
|
||||
@ -2839,10 +2866,10 @@ Type: <<types,Padding>>
|
||||
|
||||
Default:
|
||||
|
||||
- +pass:[bottom]+: empty
|
||||
- +pass:[bottom]+: +pass:[0]+
|
||||
- +pass:[left]+: +pass:[5]+
|
||||
- +pass:[right]+: +pass:[5]+
|
||||
- +pass:[top]+: empty
|
||||
- +pass:[top]+: +pass:[0]+
|
||||
|
||||
[[tabs.position]]
|
||||
=== tabs.position
|
||||
@ -2907,7 +2934,7 @@ Valid values:
|
||||
* +true+
|
||||
* +false+
|
||||
|
||||
Default: empty
|
||||
Default: +pass:[false]+
|
||||
|
||||
[[tabs.title.alignment]]
|
||||
=== tabs.title.alignment
|
||||
@ -2968,14 +2995,6 @@ Type: <<types,Int>>
|
||||
|
||||
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
|
||||
Whether to wrap when changing tabs.
|
||||
@ -3046,17 +3065,15 @@ Default:
|
||||
=== url.start_pages
|
||||
The page(s) to open at the start.
|
||||
|
||||
Type: <<types,List>>
|
||||
Type: <<types,List of FuzzyUrl, or FuzzyUrl>>
|
||||
|
||||
Default:
|
||||
|
||||
- +pass:[https://start.duckduckgo.com]+
|
||||
Default: +pass:[https://start.duckduckgo.com]+
|
||||
|
||||
[[url.yank_ignored_parameters]]
|
||||
=== url.yank_ignored_parameters
|
||||
The URL parameters to strip with `:yank url`.
|
||||
|
||||
Type: <<types,List>>
|
||||
Type: <<types,List of String>>
|
||||
|
||||
Default:
|
||||
|
||||
@ -3078,7 +3095,7 @@ Valid values:
|
||||
* +true+
|
||||
* +false+
|
||||
|
||||
Default: empty
|
||||
Default: +pass:[false]+
|
||||
|
||||
[[window.title_format]]
|
||||
=== window.title_format
|
||||
@ -3112,7 +3129,7 @@ Default: +pass:[100%]+
|
||||
=== zoom.levels
|
||||
The available zoom levels.
|
||||
|
||||
Type: <<types,List>>
|
||||
Type: <<types,List of Perc>>
|
||||
|
||||
Default:
|
||||
|
||||
@ -3152,7 +3169,7 @@ Valid values:
|
||||
* +true+
|
||||
* +false+
|
||||
|
||||
Default: empty
|
||||
Default: +pass:[false]+
|
||||
|
||||
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).
|
||||
|BoolAsk|Like `Bool`, but `ask` is allowed as additional value.
|
||||
|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.
|
||||
|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.
|
||||
|
||||
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.
|
||||
|Padding|Setting for paddings around elements.
|
||||
|Perc|A percentage.
|
||||
|
@ -264,9 +264,6 @@ Manual install
|
||||
|
||||
* Use the installer from http://www.python.org/downloads[python.org] to get
|
||||
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
|
||||
https://pip.pypa.io/en/latest/[pip]:
|
||||
|
||||
|
@ -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.
|
||||
* If you just cloned the repository, you'll need to run
|
||||
`scripts/asciidoc2html.py` to generate the documentation.
|
||||
* Go to the link:qute://settings[settings page] to set up qutebrowser the way you want it. (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
|
||||
https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser[the mailinglist] or
|
||||
https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser-announce[the announce-only mailinglist].
|
||||
|
@ -84,9 +84,6 @@ show it.
|
||||
*--force-color*::
|
||||
Force colored logging
|
||||
|
||||
*--relaxed-config*::
|
||||
Silently remove unknown config options.
|
||||
|
||||
*--nowindow*::
|
||||
Don't show the main window.
|
||||
|
||||
|
@ -5,7 +5,6 @@ import os
|
||||
|
||||
sys.path.insert(0, os.getcwd())
|
||||
from scripts import setupcommon
|
||||
from qutebrowser import utils
|
||||
|
||||
block_cipher = None
|
||||
|
||||
@ -31,9 +30,9 @@ def get_data_files():
|
||||
setupcommon.write_git_file()
|
||||
|
||||
|
||||
if utils.is_windows:
|
||||
if os.name == 'nt':
|
||||
icon = 'icons/qutebrowser.ico'
|
||||
elif utils.is_mac:
|
||||
elif sys.platform == 'darwin':
|
||||
icon = 'icons/qutebrowser.icns'
|
||||
else:
|
||||
icon = None
|
||||
|
@ -3,6 +3,6 @@
|
||||
appdirs==1.4.3
|
||||
packaging==16.8
|
||||
pyparsing==2.2.0
|
||||
setuptools==36.2.7
|
||||
six==1.10.0
|
||||
wheel==0.29.0
|
||||
setuptools==36.5.0
|
||||
six==1.11.0
|
||||
wheel==0.30.0
|
||||
|
@ -11,13 +11,13 @@ fields==5.0.0
|
||||
Flask==0.12.2
|
||||
glob2==0.6
|
||||
hunter==2.0.1
|
||||
hypothesis==3.28.3
|
||||
hypothesis==3.30.3
|
||||
itsdangerous==0.24
|
||||
# Jinja2==2.9.6
|
||||
Mako==1.0.7
|
||||
# MarkupSafe==1.0
|
||||
parse==1.8.2
|
||||
parse-type==0.3.4
|
||||
parse-type==0.4.1
|
||||
py==1.4.34
|
||||
py-cpuinfo==3.3.0
|
||||
pytest==3.2.2
|
||||
|
@ -1,6 +1,6 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
pluggy==0.4.0
|
||||
pluggy==0.5.2
|
||||
py==1.4.34
|
||||
tox==2.8.2
|
||||
virtualenv==15.1.0
|
||||
|
@ -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.
|
||||
#
|
||||
|
@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python2
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Adds DuckDuckGo bang as searchengine.
|
||||
#
|
||||
@ -8,14 +8,21 @@
|
||||
# Example:
|
||||
# :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:]:
|
||||
bang = '!' + argument
|
||||
r = requests.get('https://duckduckgo.com/',
|
||||
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('/l/?kh=-1&uddg=', '')
|
||||
searchengine = searchengine.replace('SEARCHTEXT', '{}')
|
||||
@ -24,4 +31,4 @@ for argument in sys.argv[1:]:
|
||||
with open(os.environ['QUTE_FIFO'], 'w') as fifo:
|
||||
fifo.write('set searchengines %s %s' % (bang, searchengine))
|
||||
else:
|
||||
print '%s %s' % (bang, searchengine)
|
||||
print('%s %s' % (bang, searchengine))
|
||||
|
@ -1,5 +1,5 @@
|
||||
[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
|
||||
markers =
|
||||
gui: Tests using the GUI (e.g. spawning widgets)
|
||||
|
@ -17,7 +17,25 @@
|
||||
# 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 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 sys
|
||||
@ -41,9 +59,10 @@ except ImportError:
|
||||
|
||||
import qutebrowser
|
||||
import qutebrowser.resources
|
||||
from qutebrowser.completion import completiondelegate
|
||||
from qutebrowser.completion.models import miscmodels
|
||||
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,
|
||||
downloads)
|
||||
from qutebrowser.browser.network import proxy
|
||||
@ -52,7 +71,8 @@ from qutebrowser.browser.webkit.network import networkmanager
|
||||
from qutebrowser.keyinput import macros
|
||||
from qutebrowser.mainwindow import mainwindow, prompt
|
||||
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,
|
||||
usertypes, standarddir, error)
|
||||
# pylint: disable=unused-import
|
||||
@ -77,7 +97,7 @@ def run(args):
|
||||
standarddir.init(args)
|
||||
|
||||
log.init.debug("Initializing config...")
|
||||
config.early_init(args)
|
||||
configinit.early_init(args)
|
||||
|
||||
global qApp
|
||||
qApp = Application(args)
|
||||
@ -186,12 +206,6 @@ def _init_icon():
|
||||
|
||||
def _process_args(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:
|
||||
_load_session(args.session)
|
||||
session_manager = objreg.get('session-manager')
|
||||
@ -387,13 +401,16 @@ def _init_modules(args, crash_handler):
|
||||
crash_handler: The CrashHandler instance.
|
||||
"""
|
||||
# pylint: disable=too-many-statements
|
||||
log.init.debug("Initializing prompts...")
|
||||
prompt.init()
|
||||
|
||||
log.init.debug("Initializing save manager...")
|
||||
save_manager = savemanager.SaveManager(qApp)
|
||||
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...")
|
||||
networkmanager.init()
|
||||
@ -408,11 +425,14 @@ def _init_modules(args, crash_handler):
|
||||
log.init.debug("Initializing sql...")
|
||||
try:
|
||||
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',
|
||||
pre_text='Error initializing SQL')
|
||||
sys.exit(usertypes.Exit.err_init)
|
||||
|
||||
log.init.debug("Initializing completion...")
|
||||
completiondelegate.init()
|
||||
|
||||
log.init.debug("Initializing command history...")
|
||||
cmdhistory.init()
|
||||
|
||||
@ -504,12 +524,13 @@ class Quitter:
|
||||
with tokenize.open(os.path.join(dirpath, fn)) as f:
|
||||
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.
|
||||
|
||||
Args:
|
||||
pages: The pages to re-open.
|
||||
session: The session to load, or None.
|
||||
override_args: Argument overrides as a dict.
|
||||
|
||||
Return:
|
||||
An (args, cwd) tuple.
|
||||
@ -560,6 +581,9 @@ class Quitter:
|
||||
argdict['temp_basedir'] = False
|
||||
argdict['temp_basedir_restarted'] = True
|
||||
|
||||
if override_args is not None:
|
||||
argdict.update(override_args)
|
||||
|
||||
# Dump the data
|
||||
data = json.dumps(argdict)
|
||||
args += ['--json-args', data]
|
||||
@ -584,7 +608,7 @@ class Quitter:
|
||||
if ok:
|
||||
self.shutdown(restart=True)
|
||||
|
||||
def restart(self, pages=(), session=None):
|
||||
def restart(self, pages=(), session=None, override_args=None):
|
||||
"""Inner logic to restart qutebrowser.
|
||||
|
||||
The "better" way to restart is to pass a session (_restart usually) as
|
||||
@ -597,6 +621,7 @@ class Quitter:
|
||||
Args:
|
||||
pages: A list of URLs to open.
|
||||
session: The session to load, or None.
|
||||
override_args: Argument overrides as a dict.
|
||||
|
||||
Return:
|
||||
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.argv: {}".format(sys.argv))
|
||||
log.destroy.debug("frozen: {}".format(hasattr(sys, 'frozen')))
|
||||
|
||||
# Save the session if one is given.
|
||||
if session is not None:
|
||||
session_manager = objreg.get('session-manager')
|
||||
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
|
||||
try:
|
||||
args, cwd = self._get_restart_args(pages, session)
|
||||
args, cwd = self._get_restart_args(pages, session, override_args)
|
||||
if cwd is None:
|
||||
subprocess.Popen(args)
|
||||
else:
|
||||
@ -700,7 +731,7 @@ class Quitter:
|
||||
QApplication.closeAllWindows()
|
||||
# Shut down IPC
|
||||
try:
|
||||
objreg.get('ipc-server').shutdown()
|
||||
ipc.server.shutdown()
|
||||
except KeyError:
|
||||
pass
|
||||
# Save everything
|
||||
@ -762,7 +793,7 @@ class Application(QApplication):
|
||||
"""
|
||||
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))
|
||||
super().__init__(qt_args)
|
||||
|
||||
|
@ -716,7 +716,7 @@ class AbstractTab(QWidget):
|
||||
self._set_load_status(usertypes.LoadStatus.loading)
|
||||
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."""
|
||||
if not config.val.input.insert_mode.auto_load or not ok:
|
||||
return
|
||||
@ -753,7 +753,6 @@ class AbstractTab(QWidget):
|
||||
self.load_finished.emit(ok)
|
||||
if not self.title():
|
||||
self.title_changed.emit(self.url().toDisplayString())
|
||||
self._handle_auto_insert_mode(ok)
|
||||
|
||||
@pyqtSlot()
|
||||
def _on_history_trigger(self):
|
||||
|
@ -256,7 +256,7 @@ class CommandDispatcher:
|
||||
def tab_pin(self, count=None):
|
||||
"""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,
|
||||
unless --force is passed.
|
||||
|
||||
@ -688,7 +688,7 @@ class CommandDispatcher:
|
||||
scope='window')
|
||||
@cmdutils.argument('count', count=True)
|
||||
@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.
|
||||
|
||||
The percentage can be given either as argument or as count.
|
||||
@ -1011,29 +1011,38 @@ class CommandDispatcher:
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window')
|
||||
@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.
|
||||
|
||||
Focuses window if necessary.
|
||||
Focuses window if necessary when index is given. If both index and
|
||||
count are given, use count.
|
||||
|
||||
Args:
|
||||
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.
|
||||
"""
|
||||
index_parts = index.split('/', 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)
|
||||
|
||||
try:
|
||||
for part in index_parts:
|
||||
int(part)
|
||||
except ValueError:
|
||||
model = miscmodels.buffer()
|
||||
model.set_pattern(index)
|
||||
if model.count() > 0:
|
||||
index = model.data(model.first_item())
|
||||
index_parts = index.split('/', 1)
|
||||
else:
|
||||
raise cmdexc.CommandError(
|
||||
"No matching tab for: {}".format(index))
|
||||
try:
|
||||
for part in index_parts:
|
||||
int(part)
|
||||
except ValueError:
|
||||
model = miscmodels.buffer()
|
||||
model.set_pattern(index)
|
||||
if model.count() > 0:
|
||||
index = model.data(model.first_item())
|
||||
index_parts = index.split('/', 1)
|
||||
else:
|
||||
raise cmdexc.CommandError(
|
||||
"No matching tab for: {}".format(index))
|
||||
|
||||
if len(index_parts) == 2:
|
||||
win_id = int(index_parts[0])
|
||||
|
@ -40,7 +40,10 @@ class CompletionHistory(sql.SqlTable):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
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')
|
||||
|
||||
|
||||
@ -50,6 +53,10 @@ class WebHistory(sql.SqlTable):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__("History", ['url', 'title', 'atime', 'redirect'],
|
||||
constraints={'url': 'NOT NULL',
|
||||
'title': 'NOT NULL',
|
||||
'atime': 'NOT NULL',
|
||||
'redirect': 'NOT NULL'},
|
||||
parent=parent)
|
||||
self.completion = CompletionHistory(parent=self)
|
||||
if sql.Query('pragma user_version').run().value() < _USER_VERSION:
|
||||
@ -252,10 +259,7 @@ class WebHistory(sql.SqlTable):
|
||||
except ValueError as ex:
|
||||
message.error('Failed to import history: {}'.format(ex))
|
||||
else:
|
||||
bakpath = path + '.bak'
|
||||
message.info('History import complete. Moving {} to {}'
|
||||
.format(path, bakpath))
|
||||
os.rename(path, bakpath)
|
||||
self._write_backup(path)
|
||||
|
||||
# delay to give message time to appear before locking down for import
|
||||
message.info('Converting {} to sqlite...'.format(path))
|
||||
@ -287,6 +291,16 @@ class WebHistory(sql.SqlTable):
|
||||
self.insert_batch(data)
|
||||
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):
|
||||
return url.toString(QUrl.RemovePassword | QUrl.FullyEncoded)
|
||||
|
||||
|
@ -408,11 +408,15 @@ def qute_settings(url):
|
||||
|
||||
|
||||
@add_handler('configdiff')
|
||||
def qute_configdiff(_url):
|
||||
def qute_configdiff(url):
|
||||
"""Handler for qute://configdiff."""
|
||||
try:
|
||||
return 'text/html', configdiff.get_diff()
|
||||
except OSError as e:
|
||||
error = (b'Failed to read old config: ' +
|
||||
str(e.strerror).encode('utf-8'))
|
||||
return 'text/plain', error
|
||||
if url.path() == '/old':
|
||||
try:
|
||||
return 'text/html', configdiff.get_diff()
|
||||
except OSError as e:
|
||||
error = (b'Failed to read old config: ' +
|
||||
str(e.strerror).encode('utf-8'))
|
||||
return 'text/plain', error
|
||||
else:
|
||||
data = config.instance.dump_userconfig().encode('utf-8')
|
||||
return 'text/plain', data
|
||||
|
@ -194,7 +194,7 @@ def _set_http_headers(profile):
|
||||
def _update_settings(option):
|
||||
"""Update global settings when qwebsettings changed."""
|
||||
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(private_profile)
|
||||
elif option in ['content.headers.user_agent',
|
||||
|
@ -19,9 +19,9 @@
|
||||
|
||||
"""Wrapper over a QWebEngineView."""
|
||||
|
||||
import os
|
||||
import math
|
||||
import functools
|
||||
import html as html_utils
|
||||
|
||||
import sip
|
||||
from PyQt5.QtCore import pyqtSlot, Qt, QEvent, QPoint, QUrl, QTimer
|
||||
@ -37,7 +37,7 @@ from qutebrowser.browser.webengine import (webview, webengineelem, tabhistory,
|
||||
webenginesettings)
|
||||
from qutebrowser.misc import miscwidgets
|
||||
from qutebrowser.utils import (usertypes, qtutils, log, javascript, utils,
|
||||
objreg, jinja, debug, version)
|
||||
message, objreg, jinja, debug)
|
||||
|
||||
|
||||
_qute_scheme_handler = None
|
||||
@ -49,16 +49,8 @@ def init():
|
||||
# won't work...
|
||||
# https://www.riverbankcomputing.com/pipermail/pyqt/2016-September/038075.html
|
||||
global _qute_scheme_handler
|
||||
|
||||
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...")
|
||||
_qute_scheme_handler = webenginequtescheme.QuteSchemeHandler(parent=app)
|
||||
_qute_scheme_handler.install(webenginesettings.default_profile)
|
||||
@ -678,6 +670,32 @@ class WebEngineTab(browsertab.AbstractTab):
|
||||
|
||||
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*')
|
||||
def _on_authentication_required(self, url, authenticator):
|
||||
# FIXME:qtwebengine support .netrc
|
||||
@ -755,6 +773,8 @@ class WebEngineTab(browsertab.AbstractTab):
|
||||
page.loadFinished.connect(self._on_load_finished)
|
||||
page.certificate_error.connect(self._on_ssl_errors)
|
||||
page.authenticationRequired.connect(self._on_authentication_required)
|
||||
page.proxyAuthenticationRequired.connect(
|
||||
self._on_proxy_authentication_required)
|
||||
page.fullScreenRequested.connect(self._on_fullscreen_requested)
|
||||
page.contentsSizeChanged.connect(self.contents_size_changed)
|
||||
|
||||
|
@ -515,3 +515,7 @@ class Command:
|
||||
raise cmdexc.PrerequisitesError(
|
||||
"{}: This command is only allowed in {} mode, not {}.".format(
|
||||
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)
|
||||
|
@ -154,6 +154,9 @@ class Completer(QObject):
|
||||
"partitioned: {} '{}' {}".format(prefix, center, postfix))
|
||||
return prefix, center, postfix
|
||||
|
||||
# We should always return above
|
||||
assert False, parts
|
||||
|
||||
@pyqtSlot(str)
|
||||
def on_selection_changed(self, text):
|
||||
"""Change the completed part if a new item was selected.
|
||||
|
@ -34,6 +34,9 @@ from qutebrowser.config import config
|
||||
from qutebrowser.utils import qtutils, jinja
|
||||
|
||||
|
||||
_cached_stylesheet = None
|
||||
|
||||
|
||||
class CompletionItemDelegate(QStyledItemDelegate):
|
||||
|
||||
"""Delegate used by CompletionView to draw individual items.
|
||||
@ -189,14 +192,8 @@ class CompletionItemDelegate(QStyledItemDelegate):
|
||||
self._doc.setDefaultTextOption(text_option)
|
||||
self._doc.setDocumentMargin(2)
|
||||
|
||||
stylesheet = """
|
||||
.highlight {
|
||||
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))
|
||||
assert _cached_stylesheet is not None
|
||||
self._doc.setDefaultStyleSheet(_cached_stylesheet)
|
||||
|
||||
if index.parent().isValid():
|
||||
view = self.parent()
|
||||
@ -283,3 +280,24 @@ class CompletionItemDelegate(QStyledItemDelegate):
|
||||
self._draw_focus_rect()
|
||||
|
||||
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)
|
||||
|
@ -21,7 +21,7 @@
|
||||
|
||||
from qutebrowser.config import configdata, configexc
|
||||
from qutebrowser.completion.models import completionmodel, listcategory, util
|
||||
from qutebrowser.commands import runners
|
||||
from qutebrowser.commands import runners, cmdexc
|
||||
|
||||
|
||||
def option(*, info):
|
||||
@ -44,7 +44,7 @@ def value(optname, *_values, info):
|
||||
model = completionmodel.CompletionModel(column_widths=(30, 70, 0))
|
||||
|
||||
try:
|
||||
current = info.config.get_str(optname) or '""'
|
||||
current = info.config.get_str(optname)
|
||||
except configexc.NoOptionError:
|
||||
return None
|
||||
|
||||
@ -72,8 +72,12 @@ def bind(key, *, info):
|
||||
|
||||
if cmd_text:
|
||||
parser = runners.CommandParser()
|
||||
cmd = parser.parse(cmd_text).cmd
|
||||
data = [(cmd_text, cmd.desc, key)]
|
||||
try:
|
||||
cmd = parser.parse(cmd_text).cmd
|
||||
except cmdexc.NoSuchCommandError:
|
||||
data = [(cmd_text, 'Invalid command!', key)]
|
||||
else:
|
||||
data = [(cmd_text, cmd.desc, key)]
|
||||
model.add_category(listcategory.ListCategory("Current", data))
|
||||
|
||||
cmdlist = util.get_cmd_completions(info, include_hidden=True,
|
||||
|
@ -19,20 +19,15 @@
|
||||
|
||||
"""Configuration storage and config-related utilities."""
|
||||
|
||||
import sys
|
||||
import copy
|
||||
import contextlib
|
||||
import functools
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QUrl
|
||||
from PyQt5.QtWidgets import QMessageBox
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject
|
||||
|
||||
from qutebrowser.config import configdata, configexc, configtypes, configfiles
|
||||
from qutebrowser.utils import (utils, objreg, message, log, usertypes, jinja,
|
||||
qtutils)
|
||||
from qutebrowser.misc import objects, msgbox, earlyinit
|
||||
from qutebrowser.commands import cmdexc, cmdutils, runners
|
||||
from qutebrowser.completion.models import configmodel
|
||||
from qutebrowser.config import configdata, configexc
|
||||
from qutebrowser.utils import utils, log, jinja
|
||||
from qutebrowser.misc import objects
|
||||
|
||||
# An easy way to access the config from other code via config.val.foo
|
||||
val = None
|
||||
@ -40,9 +35,7 @@ instance = None
|
||||
key_instance = None
|
||||
|
||||
# Keeping track of all change filters to validate them later.
|
||||
_change_filters = []
|
||||
# Errors which happened during init, so we can show a message box.
|
||||
_init_errors = []
|
||||
change_filters = []
|
||||
|
||||
|
||||
class change_filter: # pylint: disable=invalid-name
|
||||
@ -68,7 +61,7 @@ class change_filter: # pylint: disable=invalid-name
|
||||
"""
|
||||
self._option = option
|
||||
self._function = function
|
||||
_change_filters.append(self)
|
||||
change_filters.append(self)
|
||||
|
||||
def validate(self):
|
||||
"""Make sure the configured option or prefix exists.
|
||||
@ -175,26 +168,11 @@ class KeyConfig:
|
||||
bindings = self.get_bindings_for(mode)
|
||||
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."""
|
||||
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(
|
||||
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')
|
||||
if mode not in bindings:
|
||||
@ -223,145 +201,6 @@ class KeyConfig:
|
||||
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):
|
||||
|
||||
"""Main config object.
|
||||
@ -399,7 +238,7 @@ class Config(QObject):
|
||||
raise configexc.BackendError(objects.backend)
|
||||
|
||||
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)
|
||||
log.config.debug("Config option changed: {} = {}".format(
|
||||
@ -478,6 +317,32 @@ class Config(QObject):
|
||||
if save_yaml:
|
||||
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):
|
||||
"""Update mutable settings if they changed.
|
||||
|
||||
@ -647,114 +512,3 @@ class StyleSheetObserver(QObject):
|
||||
self._obj.setStyleSheet(qss)
|
||||
if update:
|
||||
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
|
||||
|
248
qutebrowser/config/configcommands.py
Normal file
248
qutebrowser/config/configcommands.py
Normal 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)
|
@ -93,7 +93,7 @@ def _parse_yaml_type(name, node):
|
||||
if typ is configtypes.Dict:
|
||||
kwargs['keytype'] = _parse_yaml_type(name, kwargs['keytype'])
|
||||
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'])
|
||||
except KeyError as e:
|
||||
_raise_invalid_node(name, str(e), node)
|
||||
|
@ -101,27 +101,40 @@ qt_args:
|
||||
https://peter.sh/experiments/chromium-command-line-switches/ for a list)
|
||||
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:
|
||||
type:
|
||||
name: String
|
||||
valid_values:
|
||||
- auto: Automatically select either QtWebEngine or QtWebKit
|
||||
- webkit: Force QtWebKit
|
||||
- webengine: Force QtWebEngine
|
||||
default: auto
|
||||
- webengine: Use QtWebEngine (based on Chromium)
|
||||
- webkit: Use QtWebKit (based on WebKit, similar to Safari)
|
||||
default: webengine
|
||||
desc: >-
|
||||
The backend to use to display websites.
|
||||
|
||||
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.
|
||||
|
||||
## auto_save
|
||||
|
||||
@ -545,7 +558,7 @@ content.ssl_strict:
|
||||
|
||||
content.user_stylesheets:
|
||||
type:
|
||||
name: List
|
||||
name: ListOrValue
|
||||
valtype: File
|
||||
none_ok: True
|
||||
default: null
|
||||
@ -562,10 +575,12 @@ content.xss_auditing:
|
||||
desc: >-
|
||||
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
|
||||
performance.
|
||||
|
||||
# emacs: '
|
||||
|
||||
## completion
|
||||
|
||||
completion.cmd_history_max_items:
|
||||
@ -917,11 +932,13 @@ keyhint.blacklist:
|
||||
name: String
|
||||
default: []
|
||||
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 `;`.
|
||||
Use `*` to disable keyhints.
|
||||
|
||||
# emacs: '
|
||||
|
||||
keyhint.delay:
|
||||
type:
|
||||
name: Int
|
||||
@ -1243,13 +1260,6 @@ tabs.width.indicator:
|
||||
minval: 0
|
||||
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:
|
||||
default: true
|
||||
type: Bool
|
||||
@ -1305,9 +1315,9 @@ url.searchengines:
|
||||
|
||||
url.start_pages:
|
||||
type:
|
||||
name: List
|
||||
name: ListOrValue
|
||||
valtype: FuzzyUrl
|
||||
default: ["https://start.duckduckgo.com"]
|
||||
default: "https://start.duckduckgo.com"
|
||||
desc: The page(s) to open at the start.
|
||||
|
||||
url.yank_ignored_parameters:
|
||||
@ -1616,13 +1626,18 @@ colors.prompts.fg:
|
||||
type: QssColor
|
||||
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:
|
||||
default: darkblue
|
||||
default: '#444444'
|
||||
type: QssColor
|
||||
desc: Background color for prompts.
|
||||
|
||||
colors.prompts.selected.bg:
|
||||
default: '#308cc6'
|
||||
default: grey
|
||||
type: QssColor
|
||||
desc: Background color for the selected item in filename prompts.
|
||||
|
||||
@ -1803,9 +1818,11 @@ fonts.monospace:
|
||||
desc: >-
|
||||
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.
|
||||
|
||||
# emacs: '
|
||||
|
||||
fonts.completion.entry:
|
||||
default: 8pt monospace
|
||||
type: Font
|
||||
@ -1972,6 +1989,9 @@ bindings.key_mappings:
|
||||
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.
|
||||
|
||||
bindings.default:
|
||||
default:
|
||||
normal:
|
||||
@ -2040,8 +2060,8 @@ bindings.default:
|
||||
l: scroll right
|
||||
u: undo
|
||||
<Ctrl-Shift-T>: undo
|
||||
gg: scroll-perc 0
|
||||
G: scroll-perc
|
||||
gg: scroll-to-perc 0
|
||||
G: scroll-to-perc
|
||||
n: search-next
|
||||
N: search-prev
|
||||
i: enter-mode insert
|
||||
|
@ -59,14 +59,6 @@ class KeybindingError(Error):
|
||||
"""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):
|
||||
|
||||
"""Raised when an option was not found."""
|
||||
@ -94,6 +86,12 @@ class ConfigErrorDesc:
|
||||
def __str__(self):
|
||||
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):
|
||||
|
||||
|
@ -19,18 +19,20 @@
|
||||
|
||||
"""Configuration files residing on disk."""
|
||||
|
||||
import pathlib
|
||||
import types
|
||||
import os.path
|
||||
import sys
|
||||
import textwrap
|
||||
import traceback
|
||||
import configparser
|
||||
import contextlib
|
||||
|
||||
import yaml
|
||||
from PyQt5.QtCore import QSettings
|
||||
from PyQt5.QtCore import pyqtSignal, QObject, QSettings
|
||||
|
||||
import qutebrowser
|
||||
from qutebrowser.config import configexc, config
|
||||
from qutebrowser.config import configexc, config, configdata
|
||||
from qutebrowser.utils import standarddir, utils, qtutils
|
||||
|
||||
|
||||
@ -70,7 +72,7 @@ class StateConfig(configparser.ConfigParser):
|
||||
self.write(f)
|
||||
|
||||
|
||||
class YamlConfig:
|
||||
class YamlConfig(QObject):
|
||||
|
||||
"""A config stored on disk as YAML file.
|
||||
|
||||
@ -79,8 +81,10 @@ class YamlConfig:
|
||||
"""
|
||||
|
||||
VERSION = 1
|
||||
changed = pyqtSignal()
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._filename = os.path.join(standarddir.config(auto=True),
|
||||
'autoconfig.yml')
|
||||
self._values = {}
|
||||
@ -92,20 +96,25 @@ class YamlConfig:
|
||||
We do this outside of __init__ because the config gets created before
|
||||
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):
|
||||
return self._values[name]
|
||||
|
||||
def __setitem__(self, name, value):
|
||||
self._dirty = True
|
||||
self._values[name] = value
|
||||
self._mark_changed()
|
||||
|
||||
def __contains__(self, name):
|
||||
return name in self._values
|
||||
|
||||
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):
|
||||
"""Save the settings to the YAML file if they've changed."""
|
||||
@ -153,9 +162,28 @@ class YamlConfig:
|
||||
"'global' object is not a dict")
|
||||
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._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:
|
||||
|
||||
@ -168,20 +196,26 @@ class ConfigAPI:
|
||||
Attributes:
|
||||
_config: The main Config object to use.
|
||||
_keyconfig: The KeyConfig object.
|
||||
load_autoconfig: Whether autoconfig.yml should be loaded.
|
||||
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):
|
||||
self._config = conf
|
||||
self._keyconfig = keyconfig
|
||||
self.load_autoconfig = True
|
||||
self.errors = []
|
||||
self.configdir = pathlib.Path(standarddir.config())
|
||||
self.datadir = pathlib.Path(standarddir.data())
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _handle_error(self, action, name):
|
||||
try:
|
||||
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:
|
||||
text = "While {} '{}'".format(action, name)
|
||||
self.errors.append(configexc.ConfigErrorDesc(text, e))
|
||||
@ -190,6 +224,10 @@ class ConfigAPI:
|
||||
"""Do work which needs to be done after reading config.py."""
|
||||
self._config.update_mutables()
|
||||
|
||||
def load_autoconfig(self):
|
||||
with self._handle_error('reading', 'autoconfig.yml'):
|
||||
read_autoconfig()
|
||||
|
||||
def get(self, name):
|
||||
with self._handle_error('getting', name):
|
||||
return self._config.get_obj(name)
|
||||
@ -198,24 +236,24 @@ class ConfigAPI:
|
||||
with self._handle_error('setting', name):
|
||||
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):
|
||||
self._keyconfig.bind(key, command, mode=mode, force=force)
|
||||
self._keyconfig.bind(key, command, mode=mode)
|
||||
|
||||
def unbind(self, key, mode='normal'):
|
||||
with self._handle_error('unbinding', key):
|
||||
self._keyconfig.unbind(key, mode=mode)
|
||||
|
||||
|
||||
def read_config_py(filename=None):
|
||||
"""Read a config.py file."""
|
||||
def read_config_py(filename, raising=False):
|
||||
"""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)
|
||||
|
||||
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)
|
||||
basename = os.path.basename(filename)
|
||||
|
||||
@ -234,7 +272,7 @@ def read_config_py(filename=None):
|
||||
|
||||
try:
|
||||
code = compile(source, filename, 'exec')
|
||||
except (ValueError, TypeError) as e:
|
||||
except ValueError as e:
|
||||
# source contains NUL bytes
|
||||
desc = configexc.ConfigErrorDesc("Error while compiling", e)
|
||||
raise configexc.ConfigFileErrors(basename, [desc])
|
||||
@ -244,14 +282,51 @@ def read_config_py(filename=None):
|
||||
raise configexc.ConfigFileErrors(basename, [desc])
|
||||
|
||||
try:
|
||||
exec(code, module.__dict__)
|
||||
# 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__)
|
||||
except Exception as e:
|
||||
if raising:
|
||||
raise
|
||||
api.errors.append(configexc.ConfigErrorDesc(
|
||||
"Unhandled exception",
|
||||
exception=e, traceback=traceback.format_exc()))
|
||||
|
||||
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():
|
||||
|
132
qutebrowser/config/configinit.py
Normal file
132
qutebrowser/config/configinit.py
Normal 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
|
@ -59,7 +59,7 @@ from PyQt5.QtCore import QUrl, Qt
|
||||
from PyQt5.QtGui import QColor, QFont
|
||||
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.utils import standarddir, utils, qtutils, urlutils
|
||||
|
||||
@ -227,6 +227,10 @@ class BaseType:
|
||||
return None
|
||||
return value
|
||||
|
||||
def from_obj(self, value):
|
||||
"""Get the setting value from a config.py/YAML object."""
|
||||
return value
|
||||
|
||||
def to_py(self, value):
|
||||
"""Get the setting value from a Python value.
|
||||
|
||||
@ -257,9 +261,10 @@ class BaseType:
|
||||
This currently uses asciidoc syntax.
|
||||
"""
|
||||
utils.unused(indent) # only needed for Dict/List
|
||||
if not value:
|
||||
str_value = self.to_str(value)
|
||||
if not str_value:
|
||||
return 'empty'
|
||||
return '+pass:[{}]+'.format(html.escape(self.to_str(value)))
|
||||
return '+pass:[{}]+'.format(html.escape(str_value))
|
||||
|
||||
def complete(self):
|
||||
"""Return a list of possible values for completion.
|
||||
@ -440,6 +445,11 @@ class List(BaseType):
|
||||
self.to_py(yaml_val)
|
||||
return yaml_val
|
||||
|
||||
def from_obj(self, value):
|
||||
if value is None:
|
||||
return []
|
||||
return value
|
||||
|
||||
def to_py(self, value):
|
||||
self._basic_py_validation(value, list)
|
||||
if not value:
|
||||
@ -475,6 +485,72 @@ class List(BaseType):
|
||||
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):
|
||||
|
||||
"""A list of flags.
|
||||
@ -773,33 +849,13 @@ class PercOrInt(_Numeric):
|
||||
|
||||
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):
|
||||
self._basic_py_validation(value, str)
|
||||
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
|
||||
Since validation is quite tricky here, we don't do so, and instead let
|
||||
invalid commands (in bindings/aliases) fail when used.
|
||||
"""
|
||||
|
||||
def complete(self):
|
||||
out = []
|
||||
@ -807,6 +863,10 @@ class Command(BaseType):
|
||||
out.append((cmdname, obj.desc))
|
||||
return out
|
||||
|
||||
def to_py(self, value):
|
||||
self._basic_py_validation(value, str)
|
||||
return value
|
||||
|
||||
|
||||
class ColorSystem(MappingType):
|
||||
|
||||
@ -1130,6 +1190,11 @@ class Dict(BaseType):
|
||||
self.to_py(yaml_val)
|
||||
return yaml_val
|
||||
|
||||
def from_obj(self, value):
|
||||
if value is None:
|
||||
return {}
|
||||
return value
|
||||
|
||||
def _fill_fixed_keys(self, value):
|
||||
"""Fill missing fixed keys with a None-value."""
|
||||
if self.fixed_keys is None:
|
||||
@ -1260,6 +1325,8 @@ class ShellCommand(List):
|
||||
placeholder: If there should be a placeholder.
|
||||
"""
|
||||
|
||||
_show_valtype = False
|
||||
|
||||
def __init__(self, placeholder=False, none_ok=False):
|
||||
super().__init__(valtype=String(), none_ok=none_ok)
|
||||
self.placeholder = placeholder
|
||||
|
@ -17,6 +17,9 @@ pre { margin: 2px; }
|
||||
th, td { border: 1px solid grey; padding: 0px 5px; }
|
||||
th { background: lightgrey; }
|
||||
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-text { margin-bottom: 5cm; }
|
||||
.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>
|
||||
<header><h1>{{ title }}</h1></header>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Setting</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
{% for option in configdata.DATA.values() %}
|
||||
<tr>
|
||||
<!-- 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 %}
|
||||
<p class="option_description">{{ option.description|e }}</p>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<td class="value">
|
||||
<input type="text"
|
||||
id="input-{{ option.name }}"
|
||||
onblur="cset('{{ option.name }}', this.value)"
|
||||
|
@ -18,8 +18,6 @@
|
||||
* along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* eslint-disable max-len */
|
||||
|
||||
/**
|
||||
* Snippet to position caret at top of the page when caret mode is enabled.
|
||||
* Some code was borrowed from:
|
||||
@ -28,8 +26,6 @@
|
||||
* https://github.com/1995eaton/chromium-vim/blob/master/content_scripts/visual.js
|
||||
*/
|
||||
|
||||
/* eslint-enable max-len */
|
||||
|
||||
"use strict";
|
||||
|
||||
(function() {
|
||||
|
@ -122,37 +122,40 @@ class BaseKeyParser(QObject):
|
||||
self._debug_log("Ignoring only-modifier keyeevent.")
|
||||
return False
|
||||
|
||||
key_mappings = config.val.bindings.key_mappings
|
||||
try:
|
||||
binding = key_mappings['<{}>'.format(binding)][1:-1]
|
||||
except KeyError:
|
||||
pass
|
||||
if binding not in self.special_bindings:
|
||||
key_mappings = config.val.bindings.key_mappings
|
||||
try:
|
||||
binding = key_mappings['<{}>'.format(binding)][1:-1]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
try:
|
||||
cmdstr = self.special_bindings[binding]
|
||||
except KeyError:
|
||||
self._debug_log("No special binding found for {}.".format(binding))
|
||||
return False
|
||||
count, _command = self._split_count()
|
||||
count, _command = self._split_count(self._keystring)
|
||||
self.execute(cmdstr, self.Type.special, count)
|
||||
self.clear_keystring()
|
||||
return True
|
||||
|
||||
def _split_count(self):
|
||||
def _split_count(self, keystring):
|
||||
"""Get count and command from the current keystring.
|
||||
|
||||
Args:
|
||||
keystring: The key string to split.
|
||||
|
||||
Return:
|
||||
A (count, command) tuple.
|
||||
"""
|
||||
if self._supports_count:
|
||||
(countstr, cmd_input) = re.match(r'^(\d*)(.*)',
|
||||
self._keystring).groups()
|
||||
(countstr, cmd_input) = re.match(r'^(\d*)(.*)', keystring).groups()
|
||||
count = int(countstr) if countstr else None
|
||||
if count == 0 and not cmd_input:
|
||||
cmd_input = self._keystring
|
||||
cmd_input = keystring
|
||||
count = None
|
||||
else:
|
||||
cmd_input = self._keystring
|
||||
cmd_input = keystring
|
||||
count = None
|
||||
return count, cmd_input
|
||||
|
||||
@ -183,18 +186,17 @@ class BaseKeyParser(QObject):
|
||||
self._debug_log("Ignoring, no text char")
|
||||
return self.Match.none
|
||||
|
||||
key_mappings = config.val.bindings.key_mappings
|
||||
txt = key_mappings.get(txt, txt)
|
||||
self._keystring += txt
|
||||
|
||||
count, cmd_input = self._split_count()
|
||||
|
||||
if not cmd_input:
|
||||
# Only a count, no command yet, but we handled it
|
||||
return self.Match.other
|
||||
|
||||
count, cmd_input = self._split_count(self._keystring + txt)
|
||||
match, binding = self._match_key(cmd_input)
|
||||
if match == self.Match.none:
|
||||
mappings = config.val.bindings.key_mappings
|
||||
mapped = mappings.get(txt, None)
|
||||
if mapped is not None:
|
||||
txt = mapped
|
||||
count, cmd_input = self._split_count(self._keystring + txt)
|
||||
match, binding = self._match_key(cmd_input)
|
||||
|
||||
self._keystring += txt
|
||||
if match == self.Match.definitive:
|
||||
self._debug_log("Definitive match for '{}'.".format(
|
||||
self._keystring))
|
||||
@ -207,6 +209,8 @@ class BaseKeyParser(QObject):
|
||||
self._debug_log("Giving up with '{}', no matches".format(
|
||||
self._keystring))
|
||||
self.clear_keystring()
|
||||
elif match == self.Match.other:
|
||||
pass
|
||||
else:
|
||||
raise AssertionError("Invalid match value {!r}".format(match))
|
||||
return match
|
||||
@ -223,6 +227,9 @@ class BaseKeyParser(QObject):
|
||||
binding: - None with Match.partial/Match.none.
|
||||
- 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.
|
||||
definitive_match = None
|
||||
partial_match = False
|
||||
|
@ -437,7 +437,8 @@ class MainWindow(QWidget):
|
||||
# commands
|
||||
keyparsers[usertypes.KeyMode.normal].keystring_updated.connect(
|
||||
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)
|
||||
|
||||
# key hint popup
|
||||
|
@ -28,7 +28,8 @@ import sip
|
||||
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, Qt, QTimer, QDir, QModelIndex,
|
||||
QItemSelectionModel, QObject, QEventLoop)
|
||||
from PyQt5.QtWidgets import (QWidget, QGridLayout, QVBoxLayout, QLineEdit,
|
||||
QLabel, QFileSystemModel, QTreeView, QSizePolicy)
|
||||
QLabel, QFileSystemModel, QTreeView, QSizePolicy,
|
||||
QSpacerItem)
|
||||
|
||||
from qutebrowser.browser import downloads
|
||||
from qutebrowser.config import config
|
||||
@ -256,11 +257,21 @@ class PromptContainer(QWidget):
|
||||
background-color: {{ conf.colors.prompts.bg }};
|
||||
}
|
||||
|
||||
QTreeView {
|
||||
selection-background-color: {{ conf.colors.prompts.selected.bg }};
|
||||
QLineEdit {
|
||||
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 }};
|
||||
}
|
||||
"""
|
||||
@ -433,7 +444,6 @@ class LineEdit(QLineEdit):
|
||||
super().__init__(parent)
|
||||
self.setStyleSheet("""
|
||||
QLineEdit {
|
||||
border: 1px solid grey;
|
||||
background-color: transparent;
|
||||
}
|
||||
""")
|
||||
@ -511,6 +521,9 @@ class _BasePrompt(QWidget):
|
||||
self._key_grid.addWidget(key_label, i, 0)
|
||||
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)
|
||||
|
||||
def accept(self, value=None):
|
||||
@ -559,8 +572,7 @@ class FilenamePrompt(_BasePrompt):
|
||||
def __init__(self, question, parent=None):
|
||||
super().__init__(question, parent)
|
||||
self._init_texts(question)
|
||||
self._init_fileview()
|
||||
self._set_fileview_root(question.default)
|
||||
self._init_key_label()
|
||||
|
||||
self._lineedit = LineEdit(self)
|
||||
if question.default:
|
||||
@ -569,7 +581,9 @@ class FilenamePrompt(_BasePrompt):
|
||||
self._vbox.addWidget(self._lineedit)
|
||||
|
||||
self.setFocusProxy(self._lineedit)
|
||||
self._init_key_label()
|
||||
|
||||
self._init_fileview()
|
||||
self._set_fileview_root(question.default)
|
||||
|
||||
if config.val.prompt.filebrowser:
|
||||
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
|
||||
|
@ -38,7 +38,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
|
||||
|
||||
Signals:
|
||||
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
|
||||
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.
|
||||
"""
|
||||
|
||||
got_cmd = pyqtSignal(str)
|
||||
got_cmd = pyqtSignal([str], [str, int])
|
||||
clear_completion_selection = pyqtSignal()
|
||||
hide_completion = pyqtSignal()
|
||||
update_completion = pyqtSignal()
|
||||
@ -91,7 +91,9 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
|
||||
|
||||
@cmdutils.register(instance='status-command', name='set-cmd-text',
|
||||
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.
|
||||
|
||||
//
|
||||
@ -101,8 +103,11 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
|
||||
|
||||
Args:
|
||||
text: The commandline to set.
|
||||
count: The count if given.
|
||||
space: If given, a space is added to the end.
|
||||
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:
|
||||
text += ' '
|
||||
@ -114,7 +119,10 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
|
||||
if not text or text[0] not in modeparsers.STARTCHARS:
|
||||
raise cmdexc.CommandError(
|
||||
"Invalid command text '{}'.".format(text))
|
||||
self.set_cmd_text(text)
|
||||
if run_on_count and count is not None:
|
||||
self.got_cmd[str, int].emit(text, count)
|
||||
else:
|
||||
self.set_cmd_text(text)
|
||||
|
||||
@cmdutils.register(instance='status-command', hide=True,
|
||||
modes=[usertypes.KeyMode.command], scope='window')
|
||||
@ -156,7 +164,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
|
||||
text = self.text()
|
||||
self.history.append(text)
|
||||
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)
|
||||
def on_mode_left(self, mode):
|
||||
|
@ -272,10 +272,6 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
if last_close == 'ignore' and count == 1:
|
||||
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)
|
||||
|
||||
if count == 1: # We just closed the last tab above.
|
||||
@ -689,6 +685,7 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
self._update_tab_title(idx)
|
||||
if idx == self.currentIndex():
|
||||
self._update_window_title()
|
||||
tab.handle_auto_insert_mode(ok)
|
||||
|
||||
@pyqtSlot()
|
||||
def on_scroll_pos_changed(self):
|
||||
|
@ -92,26 +92,16 @@ class TabWidget(QTabWidget):
|
||||
bar.update(bar.tabRect(idx))
|
||||
|
||||
def set_tab_pinned(self, tab: QWidget,
|
||||
pinned: bool, *, loading: bool = False) -> None:
|
||||
pinned: bool) -> None:
|
||||
"""Set the tab status as pinned.
|
||||
|
||||
Args:
|
||||
tab: The tab to pin
|
||||
pinned: Pinned tab state to set.
|
||||
loading: Whether to ignore current data state when
|
||||
counting pinned_count.
|
||||
"""
|
||||
bar = self.tabBar()
|
||||
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)
|
||||
tab.data.pinned = pinned
|
||||
self._update_tab_title(idx)
|
||||
@ -310,7 +300,6 @@ class TabBar(QTabBar):
|
||||
self._on_show_switching_delay_changed()
|
||||
self.setAutoFillBackground(True)
|
||||
self._set_colors()
|
||||
self.pinned_count = 0
|
||||
QTimer.singleShot(0, self.maybe_hide)
|
||||
|
||||
def __repr__(self):
|
||||
@ -435,18 +424,25 @@ class TabBar(QTabBar):
|
||||
return
|
||||
super().mousePressEvent(e)
|
||||
|
||||
def minimumTabSizeHint(self, index):
|
||||
def minimumTabSizeHint(self, index, ellipsis: bool = True):
|
||||
"""Set the minimum tab size to indicator/icon/... text.
|
||||
|
||||
Args:
|
||||
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:
|
||||
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)
|
||||
padding = config.val.tabs.padding
|
||||
indicator_padding = config.val.tabs.indicator_padding
|
||||
padding_h = padding.left + padding.right
|
||||
padding_h += indicator_padding.left + indicator_padding.right
|
||||
padding_v = padding.top + padding.bottom
|
||||
if icon.isNull():
|
||||
icon_size = QSize(0, 0)
|
||||
@ -454,15 +450,32 @@ class TabBar(QTabBar):
|
||||
extent = self.style().pixelMetric(QStyle.PM_TabBarIconSize, None,
|
||||
self)
|
||||
icon_size = icon.actualSize(QSize(extent, extent))
|
||||
padding_h += self.style().pixelMetric(
|
||||
PixelMetrics.icon_padding, None, self)
|
||||
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)
|
||||
return QSize(width, height)
|
||||
|
||||
def tabSizeHint(self, index):
|
||||
"""Override tabSizeHint so all tabs are the same size.
|
||||
def _tab_total_width_pinned(self):
|
||||
"""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
|
||||
|
||||
@ -490,43 +503,17 @@ class TabBar(QTabBar):
|
||||
# want to ensure it's valid in this special case.
|
||||
return QSize()
|
||||
else:
|
||||
try:
|
||||
pinned = self.tab_data(index, 'pinned')
|
||||
except KeyError:
|
||||
pinned = False
|
||||
|
||||
no_pinned_count = self.count() - self.pinned_count
|
||||
pinned_width = config.val.tabs.width.pinned * self.pinned_count
|
||||
pinned = self._tab_pinned(index)
|
||||
no_pinned_count = self.count() - self._pinnedCount()
|
||||
pinned_width = self._tab_total_width_pinned()
|
||||
no_pinned_width = self.width() - pinned_width
|
||||
|
||||
if pinned:
|
||||
size = QSize(config.val.tabs.width.pinned, height)
|
||||
qtutils.ensure_valid(size)
|
||||
return size
|
||||
|
||||
# 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)
|
||||
# Give pinned tabs the minimum size they need to display their
|
||||
# titles, let Qt handle scaling it down if we get too small.
|
||||
width = self.minimumTabSizeHint(index, ellipsis=False).width()
|
||||
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
|
||||
else:
|
||||
width = self.width() / self.count()
|
||||
width = no_pinned_width / no_pinned_count
|
||||
|
||||
# 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.
|
||||
|
335
qutebrowser/misc/backendproblem.py
Normal file
335
qutebrowser/misc/backendproblem.py
Normal 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)
|
@ -32,15 +32,19 @@ import pkg_resources
|
||||
from PyQt5.QtCore import pyqtSlot, Qt, QSize
|
||||
from PyQt5.QtWidgets import (QDialog, QLabel, QTextEdit, QPushButton,
|
||||
QVBoxLayout, QHBoxLayout, QCheckBox,
|
||||
QDialogButtonBox, QMessageBox, QApplication)
|
||||
QDialogButtonBox, QApplication)
|
||||
|
||||
import qutebrowser
|
||||
from qutebrowser.utils import version, log, utils, objreg, usertypes
|
||||
from qutebrowser.misc import (miscwidgets, autoupdate, msgbox, httpclient,
|
||||
pastebin, objects)
|
||||
pastebin)
|
||||
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):
|
||||
"""Get useful information from a fatal faulthandler stacktrace.
|
||||
|
||||
@ -65,41 +69,6 @@ def parse_fatal_stacktrace(text):
|
||||
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():
|
||||
"""Gather environment variables for the crash info."""
|
||||
masks = ('DESKTOP_SESSION', 'DE', 'QT_*', 'PYTHON*', 'LC_*', 'LANG',
|
||||
@ -478,9 +447,9 @@ class ExceptionCrashDialog(_CrashDialog):
|
||||
def finish(self):
|
||||
self._save_contact_info()
|
||||
if self._chk_restore.isChecked():
|
||||
self.accept()
|
||||
self.done(Result.restore)
|
||||
else:
|
||||
self.reject()
|
||||
self.done(Result.no_restore)
|
||||
|
||||
|
||||
class FatalCrashDialog(_CrashDialog):
|
||||
|
@ -36,10 +36,10 @@ except ImportError:
|
||||
import attr
|
||||
from PyQt5.QtCore import (pyqtSlot, qInstallMessageHandler, QObject,
|
||||
QSocketNotifier, QTimer, QUrl)
|
||||
from PyQt5.QtWidgets import QApplication, QDialog
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
|
||||
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
|
||||
|
||||
|
||||
@ -94,7 +94,7 @@ class CrashHandler(QObject):
|
||||
if data:
|
||||
# Crashlog exists and has data in it, so something crashed
|
||||
# previously.
|
||||
self._crash_dialog = crashdialog.get_fatal_crash_dialog(
|
||||
self._crash_dialog = crashdialog.FatalCrashDialog(
|
||||
self._args.debug, data)
|
||||
self._crash_dialog.show()
|
||||
else:
|
||||
@ -236,7 +236,7 @@ class CrashHandler(QObject):
|
||||
info = self._get_exception_info()
|
||||
|
||||
try:
|
||||
objreg.get('ipc-server').ignored = True
|
||||
ipc.server.ignored = True
|
||||
except Exception:
|
||||
log.destroy.exception("Error while ignoring ipc")
|
||||
|
||||
@ -258,7 +258,7 @@ class CrashHandler(QObject):
|
||||
self._args.debug, info.pages, info.cmd_history, exc,
|
||||
info.objects)
|
||||
ret = self._crash_dialog.exec_()
|
||||
if ret == QDialog.Accepted: # restore
|
||||
if ret == crashdialog.Result.restore:
|
||||
self._quitter.restart(info.pages)
|
||||
|
||||
# We might risk a segfault here, but that's better than continuing to
|
||||
|
@ -47,33 +47,25 @@ except ImportError:
|
||||
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.
|
||||
|
||||
Args:
|
||||
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
|
||||
"""
|
||||
blocks = ["Fatal error: <b>{}</b> is required to run qutebrowser but "
|
||||
"could not be imported! Maybe it's not installed?".format(name),
|
||||
"<b>The error encountered was:</b><br />%ERROR%"]
|
||||
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))
|
||||
if not webengine:
|
||||
lines = ['<b>If you installed a qutebrowser package for your '
|
||||
'distribution, please report this as a bug.</b>']
|
||||
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)
|
||||
|
||||
|
||||
@ -142,11 +134,7 @@ def check_pyqt_core():
|
||||
try:
|
||||
import PyQt5.QtCore # pylint: disable=unused-variable
|
||||
except ImportError as e:
|
||||
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 = _missing_str('PyQt5')
|
||||
text = text.replace('<b>', '')
|
||||
text = text.replace('</b>', '')
|
||||
text = text.replace('<br />', '\n')
|
||||
@ -199,23 +187,6 @@ def check_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):
|
||||
"""Make sure the given modules are available."""
|
||||
from qutebrowser.utils import log
|
||||
@ -230,7 +201,14 @@ def _check_modules(modules):
|
||||
'Flags not at the start of the expression']
|
||||
with log.ignore_py_warnings(
|
||||
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)
|
||||
except ImportError as e:
|
||||
_die(text, e)
|
||||
@ -239,31 +217,12 @@ def _check_modules(modules):
|
||||
def check_libraries():
|
||||
"""Check if all needed Python libraries are installed."""
|
||||
modules = {
|
||||
'pkg_resources':
|
||||
_missing_str("pkg_resources/setuptools",
|
||||
windows="Run python -m ensurepip."),
|
||||
'pypeg2':
|
||||
_missing_str("pypeg2",
|
||||
pip="pypeg2"),
|
||||
'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"),
|
||||
'pkg_resources': _missing_str("pkg_resources/setuptools"),
|
||||
'pypeg2': _missing_str("pypeg2"),
|
||||
'jinja2': _missing_str("jinja2"),
|
||||
'pygments': _missing_str("pygments"),
|
||||
'yaml': _missing_str("PyYAML"),
|
||||
'attr': _missing_str("attrs"),
|
||||
'PyQt5.QtQml': _missing_str("PyQt5.QtQml"),
|
||||
'PyQt5.QtSql': _missing_str("PyQt5.QtSql"),
|
||||
'PyQt5.QtOpenGL': _missing_str("PyQt5.QtOpenGL"),
|
||||
@ -271,35 +230,6 @@ def check_libraries():
|
||||
_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():
|
||||
"""Remove the PyQt input hook.
|
||||
|
||||
@ -352,16 +282,3 @@ def early_init(args):
|
||||
remove_inputhook()
|
||||
check_ssl_support()
|
||||
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)
|
||||
|
@ -35,8 +35,9 @@ class ExternalEditor(QObject):
|
||||
|
||||
Attributes:
|
||||
_text: The current text before the editor is opened.
|
||||
_file: The file handle as tempfile.NamedTemporaryFile. Note that this
|
||||
handle will be closed after the initial file has been created.
|
||||
_filename: The name of the file to be edited.
|
||||
_remove_file: Whether the file should be removed when the editor is
|
||||
closed.
|
||||
_proc: The GUIProcess of the editor.
|
||||
"""
|
||||
|
||||
@ -44,18 +45,20 @@ class ExternalEditor(QObject):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._text = None
|
||||
self._file = None
|
||||
self._filename = None
|
||||
self._proc = None
|
||||
self._remove_file = None
|
||||
|
||||
def _cleanup(self):
|
||||
"""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.
|
||||
return
|
||||
|
||||
try:
|
||||
if self._proc.exit_status() != QProcess.CrashExit:
|
||||
os.remove(self._file.name)
|
||||
os.remove(self._filename)
|
||||
except OSError as e:
|
||||
# NOTE: Do not replace this with "raise CommandError" as it's
|
||||
# executed async.
|
||||
@ -77,7 +80,7 @@ class ExternalEditor(QObject):
|
||||
return
|
||||
encoding = config.val.editor.encoding
|
||||
try:
|
||||
with open(self._file.name, 'r', encoding=encoding) as f:
|
||||
with open(self._filename, 'r', encoding=encoding) as f:
|
||||
text = f.read()
|
||||
except OSError as e:
|
||||
# NOTE: Do not replace this with "raise CommandError" as it's
|
||||
@ -99,9 +102,8 @@ class ExternalEditor(QObject):
|
||||
Args:
|
||||
text: The initial text to edit.
|
||||
"""
|
||||
if self._text is not None:
|
||||
if self._filename is not None:
|
||||
raise ValueError("Already editing a file!")
|
||||
self._text = text
|
||||
try:
|
||||
# Close while the external process is running, as otherwise systems
|
||||
# with exclusive write access (e.g. Windows) may fail to update
|
||||
@ -113,15 +115,27 @@ class ExternalEditor(QObject):
|
||||
delete=False) as fobj:
|
||||
if text:
|
||||
fobj.write(text)
|
||||
self._file = fobj
|
||||
self._filename = fobj.name
|
||||
except OSError as e:
|
||||
message.error("Failed to create initial file: {}".format(e))
|
||||
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.finished.connect(self.on_proc_closed)
|
||||
self._proc.error.connect(self.on_proc_error)
|
||||
editor = config.val.editor.command
|
||||
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))
|
||||
self._proc.start(executable, args)
|
||||
|
@ -30,7 +30,7 @@ from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, Qt
|
||||
from PyQt5.QtNetwork import QLocalSocket, QLocalServer, QAbstractSocket
|
||||
|
||||
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
|
||||
@ -40,6 +40,10 @@ ATIME_INTERVAL = 60 * 60 * 3 * 1000 # 3 hours
|
||||
PROTOCOL_VERSION = 1
|
||||
|
||||
|
||||
# The ipc server instance
|
||||
server = None
|
||||
|
||||
|
||||
def _get_socketname_windows(basedir):
|
||||
"""Get a socketname to use for Windows."""
|
||||
parts = ['qutebrowser', getpass.getuser()]
|
||||
@ -109,15 +113,15 @@ class ListenError(Error):
|
||||
message: The error message.
|
||||
"""
|
||||
|
||||
def __init__(self, server):
|
||||
def __init__(self, local_server):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
server: The QLocalServer which has the error set.
|
||||
local_server: The QLocalServer which has the error set.
|
||||
"""
|
||||
super().__init__()
|
||||
self.code = server.serverError()
|
||||
self.message = server.errorString()
|
||||
self.code = local_server.serverError()
|
||||
self.message = local_server.errorString()
|
||||
|
||||
def __str__(self):
|
||||
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.
|
||||
None if an instance was running and received our request.
|
||||
"""
|
||||
global server
|
||||
socketname = _get_socketname(args.basedir)
|
||||
try:
|
||||
try:
|
||||
@ -492,7 +497,6 @@ def send_or_listen(args):
|
||||
log.init.debug("Starting IPC server...")
|
||||
server = IPCServer(socketname)
|
||||
server.listen()
|
||||
objreg.register('ipc-server', server)
|
||||
return server
|
||||
except AddressInUseError as e:
|
||||
# This could be a race condition...
|
||||
|
@ -26,12 +26,14 @@ It is intended to help discoverability of keybindings.
|
||||
|
||||
import html
|
||||
import fnmatch
|
||||
import re
|
||||
|
||||
from PyQt5.QtWidgets import QLabel, QSizePolicy
|
||||
from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import utils, usertypes
|
||||
from qutebrowser.commands import cmdutils
|
||||
|
||||
|
||||
class KeyHintView(QLabel):
|
||||
@ -85,6 +87,7 @@ class KeyHintView(QLabel):
|
||||
Args:
|
||||
prefix: The current partial keystring.
|
||||
"""
|
||||
countstr, prefix = re.match(r'^(\d*)(.*)', prefix).groups()
|
||||
if not prefix:
|
||||
self._show_timer.stop()
|
||||
self.hide()
|
||||
@ -94,11 +97,18 @@ class KeyHintView(QLabel):
|
||||
return any(fnmatch.fnmatchcase(keychain, glob)
|
||||
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 = [(k, v) for (k, v) in sorted(bindings_dict.items())
|
||||
if k.startswith(prefix) and
|
||||
not utils.is_special_key(k) and
|
||||
not blacklisted(k)]
|
||||
not blacklisted(k) and
|
||||
(takes_count(v) or not countstr)]
|
||||
|
||||
if not bindings:
|
||||
self._show_timer.stop()
|
||||
|
@ -19,10 +19,21 @@
|
||||
|
||||
"""Convenience functions to show message boxes."""
|
||||
|
||||
import sys
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
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,
|
||||
on_finished=None, plain_text=None):
|
||||
@ -40,6 +51,11 @@ def msgbox(parent, title, text, *, icon, buttons=QMessageBox.Ok,
|
||||
Return:
|
||||
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.setAttribute(Qt.WA_DeleteOnClose)
|
||||
box.setIcon(icon)
|
||||
|
@ -164,6 +164,11 @@ class SaveManager(QObject):
|
||||
self.saveables[name].save(is_exit=is_exit, explicit=explicit,
|
||||
silent=silent, force=force)
|
||||
|
||||
def save_all(self, *args, **kwargs):
|
||||
"""Save all saveables."""
|
||||
for saveable in self.saveables:
|
||||
self.save(saveable, *args, **kwargs)
|
||||
|
||||
@pyqtSlot()
|
||||
def autosave(self):
|
||||
"""Slot used when the configs are auto-saved."""
|
||||
|
@ -393,8 +393,7 @@ class SessionManager(QObject):
|
||||
if tab.get('active', False):
|
||||
tab_to_focus = i
|
||||
if new_tab.data.pinned:
|
||||
tabbed_browser.set_tab_pinned(
|
||||
new_tab, new_tab.data.pinned, loading=True)
|
||||
tabbed_browser.set_tab_pinned(new_tab, new_tab.data.pinned)
|
||||
if tab_to_focus is not None:
|
||||
tabbed_browser.setCurrentIndex(tab_to_focus)
|
||||
if win.get('active', False):
|
||||
|
@ -22,12 +22,12 @@
|
||||
import collections
|
||||
|
||||
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."""
|
||||
|
||||
@ -38,12 +38,14 @@ def init(db_path):
|
||||
"""Initialize the SQL database connection."""
|
||||
database = QSqlDatabase.addDatabase('QSQLITE')
|
||||
if not database.isValid():
|
||||
raise SqlException('Failed to add database. '
|
||||
raise SqlError('Failed to add database. '
|
||||
'Are sqlite and Qt sqlite support installed?')
|
||||
database.setDatabaseName(db_path)
|
||||
if not database.open():
|
||||
raise SqlException("Failed to open sqlite database at {}: {}"
|
||||
.format(db_path, database.lastError().text()))
|
||||
error = database.lastError()
|
||||
_log_error(error)
|
||||
raise SqlError("Failed to open sqlite database at {}: {}"
|
||||
.format(db_path, error.text()))
|
||||
|
||||
|
||||
def close():
|
||||
@ -60,10 +62,32 @@ def version():
|
||||
close()
|
||||
return ver
|
||||
return Query("select sqlite_version()").run().value()
|
||||
except SqlException as e:
|
||||
except SqlError as 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):
|
||||
|
||||
"""A prepared SQL Query."""
|
||||
@ -79,13 +103,12 @@ class Query(QSqlQuery):
|
||||
super().__init__(QSqlDatabase.database())
|
||||
log.sql.debug('Preparing SQL query: "{}"'.format(querystr))
|
||||
if not self.prepare(querystr):
|
||||
raise SqlException('Failed to prepare query "{}": "{}"'.format(
|
||||
querystr, self.lastError().text()))
|
||||
_handle_query_error('prepare', querystr, self.lastError())
|
||||
self.setForwardOnly(forward_only)
|
||||
|
||||
def __iter__(self):
|
||||
if not self.isActive():
|
||||
raise SqlException("Cannot iterate inactive query")
|
||||
raise SqlError("Cannot iterate inactive query")
|
||||
rec = self.record()
|
||||
fields = [rec.fieldName(i) for i in range(rec.count())]
|
||||
rowtype = collections.namedtuple('ResultRow', fields)
|
||||
@ -101,14 +124,13 @@ class Query(QSqlQuery):
|
||||
self.bindValue(':{}'.format(key), val)
|
||||
log.sql.debug('query bindings: {}'.format(self.boundValues()))
|
||||
if not self.exec_():
|
||||
raise SqlException('Failed to exec query "{}": "{}"'.format(
|
||||
self.lastQuery(), self.lastError().text()))
|
||||
_handle_query_error('exec', self.lastQuery(), self.lastError())
|
||||
return self
|
||||
|
||||
def value(self):
|
||||
"""Return the result of a single-value query (e.g. an EXISTS)."""
|
||||
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)
|
||||
|
||||
|
||||
@ -128,7 +150,7 @@ class SqlTable(QObject):
|
||||
def __init__(self, name, fields, constraints=None, parent=None):
|
||||
"""Create a new table in the sql database.
|
||||
|
||||
Raises SqlException if the table already exists.
|
||||
Does nothing if the table already exists.
|
||||
|
||||
Args:
|
||||
name: Name of the table.
|
||||
@ -228,8 +250,7 @@ class SqlTable(QObject):
|
||||
db = QSqlDatabase.database()
|
||||
db.transaction()
|
||||
if not q.execBatch():
|
||||
raise SqlException('Failed to exec query "{}": "{}"'.format(
|
||||
q.lastQuery(), q.lastError().text()))
|
||||
_handle_query_error('exec', q.lastQuery(), q.lastError())
|
||||
db.commit()
|
||||
self.changed.emit()
|
||||
|
||||
|
@ -171,12 +171,15 @@ def debug_cache_stats():
|
||||
prefix_info = configdata.is_valid_prefix.cache_info()
|
||||
# pylint: disable=protected-access
|
||||
render_stylesheet_info = config._render_stylesheet.cache_info()
|
||||
|
||||
history_info = None
|
||||
try:
|
||||
from PyQt5.QtWebKit import QWebHistoryInterface
|
||||
interface = QWebHistoryInterface.defaultInterface()
|
||||
history_info = interface.historyContains.cache_info()
|
||||
if interface is not None:
|
||||
history_info = interface.historyContains.cache_info()
|
||||
except ImportError:
|
||||
history_info = None
|
||||
pass
|
||||
|
||||
log.misc.debug('is_valid_prefix: {}'.format(prefix_info))
|
||||
log.misc.debug('_render_stylesheet: {}'.format(render_stylesheet_info))
|
||||
@ -339,3 +342,12 @@ def window_only(current_win_id):
|
||||
def nop():
|
||||
"""Do nothing."""
|
||||
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)
|
||||
|
@ -17,7 +17,21 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# 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 json
|
||||
@ -95,8 +109,6 @@ def get_argparser():
|
||||
action='store_false', dest='color')
|
||||
debug.add_argument('--force-color', help="Force colored logging",
|
||||
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 "
|
||||
"the main window.")
|
||||
debug.add_argument('--temp-basedir', action='store_true', help="Use a "
|
||||
|
@ -39,6 +39,7 @@ except ImportError:
|
||||
colorama = None
|
||||
|
||||
_log_inited = False
|
||||
_args = None
|
||||
|
||||
COLORS = ['black', 'red', 'green', 'yellow', 'blue', 'purple', 'cyan', 'white']
|
||||
COLOR_ESCAPES = {color: '\033[{}m'.format(i)
|
||||
@ -189,8 +190,9 @@ def init_log(args):
|
||||
logging.captureWarnings(True)
|
||||
_init_py_warnings()
|
||||
QtCore.qInstallMessageHandler(qt_message_handler)
|
||||
global _log_inited
|
||||
global _log_inited, _args
|
||||
_log_inited = True
|
||||
_args = args
|
||||
|
||||
|
||||
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"
|
||||
" pacman -S libxkbcommon-x11")
|
||||
faulthandler.disable()
|
||||
stack = ''.join(traceback.format_stack())
|
||||
|
||||
if _args.debug:
|
||||
stack = ''.join(traceback.format_stack())
|
||||
else:
|
||||
stack = None
|
||||
record = qt.makeRecord(name, level, context.file, context.line, msg, None,
|
||||
None, func, sinfo=stack)
|
||||
qt.handle(record)
|
||||
|
@ -36,8 +36,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir,
|
||||
os.pardir))
|
||||
|
||||
import qutebrowser
|
||||
from qutebrowser.utils import utils
|
||||
from scripts import utils as scriptutils
|
||||
from scripts import utils
|
||||
# from scripts.dev import update_3rdparty
|
||||
|
||||
|
||||
@ -71,7 +70,7 @@ def call_tox(toxenv, *args, python=sys.executable):
|
||||
|
||||
def run_asciidoc2html(args):
|
||||
"""Common buildsteps used for all OS'."""
|
||||
scriptutils.print_title("Running asciidoc2html.py")
|
||||
utils.print_title("Running asciidoc2html.py")
|
||||
if args.asciidoc is not None:
|
||||
a2h_args = ['--asciidoc'] + args.asciidoc
|
||||
else:
|
||||
@ -128,7 +127,7 @@ def patch_mac_app():
|
||||
|
||||
def build_mac():
|
||||
"""Build macOS .dmg/.app."""
|
||||
scriptutils.print_title("Cleaning up...")
|
||||
utils.print_title("Cleaning up...")
|
||||
for f in ['wc.dmg', 'template.dmg']:
|
||||
try:
|
||||
os.remove(f)
|
||||
@ -136,20 +135,20 @@ def build_mac():
|
||||
pass
|
||||
for d in ['dist', 'build']:
|
||||
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
|
||||
# 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')
|
||||
scriptutils.print_title("Patching .app")
|
||||
utils.print_title("Patching .app")
|
||||
patch_mac_app()
|
||||
scriptutils.print_title("Building .dmg")
|
||||
utils.print_title("Building .dmg")
|
||||
subprocess.check_call(['make', '-f', 'scripts/dev/Makefile-dmg'])
|
||||
|
||||
dmg_name = 'qutebrowser-{}.dmg'.format(qutebrowser.__version__)
|
||||
os.rename('qutebrowser.dmg', dmg_name)
|
||||
|
||||
scriptutils.print_title("Running smoke test")
|
||||
utils.print_title("Running smoke test")
|
||||
|
||||
try:
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
@ -178,11 +177,11 @@ def patch_windows(out_dir):
|
||||
|
||||
def build_windows():
|
||||
"""Build windows executables/setups."""
|
||||
scriptutils.print_title("Updating 3rdparty content")
|
||||
utils.print_title("Updating 3rdparty content")
|
||||
# Currently disabled because QtWebEngine has no pdfjs support
|
||||
# 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)
|
||||
ver = ''.join(parts)
|
||||
python_x86 = r'C:\Python{}-32\python.exe'.format(ver)
|
||||
@ -195,19 +194,19 @@ def build_windows():
|
||||
|
||||
artifacts = []
|
||||
|
||||
scriptutils.print_title("Running pyinstaller 32bit")
|
||||
utils.print_title("Running pyinstaller 32bit")
|
||||
_maybe_remove(out_32)
|
||||
call_tox('pyinstaller', '-r', python=python_x86)
|
||||
shutil.move(out_pyinstaller, out_32)
|
||||
patch_windows(out_32)
|
||||
|
||||
scriptutils.print_title("Running pyinstaller 64bit")
|
||||
utils.print_title("Running pyinstaller 64bit")
|
||||
_maybe_remove(out_64)
|
||||
call_tox('pyinstaller', '-r', python=python_x64)
|
||||
shutil.move(out_pyinstaller, out_64)
|
||||
patch_windows(out_64)
|
||||
|
||||
scriptutils.print_title("Building installers")
|
||||
utils.print_title("Building installers")
|
||||
subprocess.check_call(['makensis.exe',
|
||||
'/DVERSION={}'.format(qutebrowser.__version__),
|
||||
'misc/qutebrowser.nsi'])
|
||||
@ -228,12 +227,12 @@ def build_windows():
|
||||
'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'))
|
||||
scriptutils.print_title("Running 64bit smoke test")
|
||||
utils.print_title("Running 64bit smoke test")
|
||||
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(
|
||||
qutebrowser.__version__)
|
||||
shutil.make_archive(name, 'zip', 'dist', os.path.basename(out_32))
|
||||
@ -241,7 +240,7 @@ def build_windows():
|
||||
'application/zip',
|
||||
'Windows 32bit standalone'))
|
||||
|
||||
scriptutils.print_title("Zipping 64bit standalone...")
|
||||
utils.print_title("Zipping 64bit standalone...")
|
||||
name = 'qutebrowser-{}-windows-standalone-amd64'.format(
|
||||
qutebrowser.__version__)
|
||||
shutil.make_archive(name, 'zip', 'dist', os.path.basename(out_64))
|
||||
@ -254,7 +253,7 @@ def build_windows():
|
||||
|
||||
def build_sdist():
|
||||
"""Build an sdist and list the contents."""
|
||||
scriptutils.print_title("Building sdist")
|
||||
utils.print_title("Building sdist")
|
||||
|
||||
_maybe_remove('dist')
|
||||
|
||||
@ -277,10 +276,10 @@ def build_sdist():
|
||||
|
||||
assert '.pyc' not in by_ext
|
||||
|
||||
scriptutils.print_title("sdist contents")
|
||||
utils.print_title("sdist contents")
|
||||
|
||||
for ext, files in sorted(by_ext.items()):
|
||||
scriptutils.print_subtitle(ext)
|
||||
utils.print_subtitle(ext)
|
||||
print('\n'.join(files))
|
||||
|
||||
filename = 'qutebrowser-{}.tar.gz'.format(qutebrowser.__version__)
|
||||
@ -309,7 +308,7 @@ def github_upload(artifacts, tag):
|
||||
tag: The name of the release tag
|
||||
"""
|
||||
import github3
|
||||
scriptutils.print_title("Uploading to github...")
|
||||
utils.print_title("Uploading to github...")
|
||||
|
||||
token = read_github_token()
|
||||
gh = github3.login(token=token)
|
||||
@ -344,7 +343,7 @@ def main():
|
||||
parser.add_argument('--upload', help="Tag to upload the release for",
|
||||
nargs=1, required=False, metavar='TAG')
|
||||
args = parser.parse_args()
|
||||
scriptutils.change_cwd()
|
||||
utils.change_cwd()
|
||||
|
||||
upload_to_pypi = False
|
||||
|
||||
@ -354,7 +353,8 @@ def main():
|
||||
import github3 # pylint: disable=unused-variable
|
||||
read_github_token()
|
||||
|
||||
if utils.is_windows:
|
||||
run_asciidoc2html(args)
|
||||
if os.name == 'nt':
|
||||
if sys.maxsize > 2**32:
|
||||
# WORKAROUND
|
||||
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("https://github.com/pypa/virtualenv/issues/774")
|
||||
sys.exit(1)
|
||||
run_asciidoc2html(args)
|
||||
artifacts = build_windows()
|
||||
elif utils.is_mac:
|
||||
run_asciidoc2html(args)
|
||||
elif sys.platform == 'darwin':
|
||||
artifacts = build_mac()
|
||||
else:
|
||||
artifacts = build_sdist()
|
||||
upload_to_pypi = True
|
||||
|
||||
if args.upload is not None:
|
||||
scriptutils.print_title("Press enter to release...")
|
||||
utils.print_title("Press enter to release...")
|
||||
input()
|
||||
github_upload(artifacts, args.upload[0])
|
||||
if upload_to_pypi:
|
||||
pypi_upload(artifacts)
|
||||
else:
|
||||
print()
|
||||
utils.print_title("Artifacts")
|
||||
for artifact in artifacts:
|
||||
print(artifact)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
@ -141,6 +141,10 @@ PERFECT_FILES = [
|
||||
'config/configfiles.py'),
|
||||
('tests/unit/config/test_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',
|
||||
'utils/qtutils.py'),
|
||||
|
@ -5,7 +5,7 @@ if [[ $DOCKER ]]; then
|
||||
elif [[ $TESTENV == eslint ]]; then
|
||||
# Can't run this via tox as we can't easily install tox in the javascript travis env
|
||||
cd qutebrowser/javascript || exit 1
|
||||
eslint --color .
|
||||
eslint --color --report-unused-disable-directives .
|
||||
else
|
||||
args=()
|
||||
[[ $TRAVIS_OS_NAME == osx ]] && args=('--qute-bdd-webengine' '--no-xvfb')
|
||||
|
@ -41,7 +41,7 @@ from qutebrowser.browser import qutescheme
|
||||
from qutebrowser.config import configtypes
|
||||
|
||||
|
||||
def whitelist_generator():
|
||||
def whitelist_generator(): # noqa
|
||||
"""Generator which yields lines to add to a vulture whitelist."""
|
||||
# qutebrowser commands
|
||||
for cmd in cmdutils.cmd_dict.values():
|
||||
@ -108,6 +108,8 @@ def whitelist_generator():
|
||||
yield 'qutebrowser.config.configexc.ConfigErrorDesc.traceback'
|
||||
yield 'qutebrowser.config.configfiles.ConfigAPI.load_autoconfig'
|
||||
yield 'types.ModuleType.c' # configfiles:read_config_py
|
||||
for name in ['configdir', 'datadir']:
|
||||
yield 'qutebrowser.config.configfiles.ConfigAPI.' + name
|
||||
|
||||
yield 'include_aliases'
|
||||
|
||||
|
@ -416,8 +416,8 @@ def _generate_setting_option(f, opt):
|
||||
f.write("=== {}".format(opt.name) + "\n")
|
||||
f.write(opt.description + "\n")
|
||||
f.write("\n")
|
||||
f.write('Type: <<types,{typ}>>\n'.format(
|
||||
typ=opt.typ.__class__.__name__))
|
||||
typ = opt.typ.get_name().replace(',', ',')
|
||||
f.write('Type: <<types,{typ}>>\n'.format(typ=typ))
|
||||
f.write("\n")
|
||||
|
||||
valid_values = opt.typ.get_valid_values()
|
||||
|
@ -18,11 +18,9 @@
|
||||
# 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 re
|
||||
import ast
|
||||
import os
|
||||
import os.path
|
||||
import subprocess
|
||||
@ -30,42 +28,16 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
|
||||
|
||||
|
||||
if sys.hexversion >= 0x03000000:
|
||||
_open = open
|
||||
open_file = open
|
||||
else:
|
||||
import codecs
|
||||
_open = codecs.open
|
||||
open_file = codecs.open
|
||||
|
||||
|
||||
BASEDIR = os.path.join(os.path.dirname(os.path.realpath(__file__)),
|
||||
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():
|
||||
"""Try to find out git version.
|
||||
|
||||
@ -95,37 +67,5 @@ def write_git_file():
|
||||
if gitstr is None:
|
||||
gitstr = ''
|
||||
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)
|
||||
|
||||
|
||||
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',
|
||||
}
|
||||
|
57
setup.py
57
setup.py
@ -21,6 +21,8 @@
|
||||
|
||||
"""setuptools installer script for qutebrowser."""
|
||||
|
||||
import re
|
||||
import ast
|
||||
import os
|
||||
import os.path
|
||||
|
||||
@ -35,6 +37,32 @@ except NameError:
|
||||
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:
|
||||
common.write_git_file()
|
||||
setuptools.setup(
|
||||
@ -42,10 +70,35 @@ try:
|
||||
include_package_data=True,
|
||||
entry_points={'gui_scripts':
|
||||
['qutebrowser = qutebrowser.qutebrowser:main']},
|
||||
test_suite='qutebrowser.test',
|
||||
zip_safe=True,
|
||||
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:
|
||||
if BASEDIR is not None:
|
||||
|
@ -43,7 +43,7 @@ import qutebrowser.app # To register commands
|
||||
|
||||
# Set hypothesis settings
|
||||
hypothesis.settings.register_profile('default',
|
||||
hypothesis.settings(deadline=400))
|
||||
hypothesis.settings(deadline=600))
|
||||
hypothesis.settings.load_profile('default')
|
||||
|
||||
|
||||
|
@ -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
|
@ -76,7 +76,9 @@ Feature: Using completion
|
||||
And I open data/hello2.txt in a new tab
|
||||
And I run :set-cmd-text -s :buffer
|
||||
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 wait for "setting text = ':buffer 0/2', *" in the log
|
||||
And I run :completion-item-del
|
||||
Then the following tabs should be open:
|
||||
- data/hello.txt (active)
|
||||
|
@ -246,7 +246,7 @@ Feature: Using hints
|
||||
Scenario: Ignoring key presses after auto-following hints
|
||||
When I set hints.auto_follow_timeout to 1000
|
||||
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 hint with args "all"
|
||||
And I press the key "f"
|
||||
@ -259,7 +259,7 @@ Feature: Using hints
|
||||
Scenario: Turning off auto_follow_timeout
|
||||
When I set hints.auto_follow_timeout to 0
|
||||
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 hint with args "all"
|
||||
And I press the key "f"
|
||||
@ -362,6 +362,7 @@ Feature: Using hints
|
||||
And I set hints.mode to letter
|
||||
And I hint with args "--mode number all"
|
||||
And I press the key "s"
|
||||
And I wait for "Filtering hints on 's'" in the log
|
||||
And I run :follow-hint 1
|
||||
Then data/numbers/7.txt should be loaded
|
||||
|
||||
|
@ -21,7 +21,7 @@ Feature: Setting positional marks
|
||||
Scenario: Jumping back after jumping to a particular percentage
|
||||
When I run :scroll-px 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 run :jump-mark "'"
|
||||
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
|
||||
When I run :scroll-px 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 hint with args "links hover" and follow s
|
||||
And I run :jump-mark "'"
|
||||
|
@ -47,6 +47,14 @@ Feature: Various utility commands.
|
||||
When I run :set-cmd-text foo
|
||||
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
|
||||
|
||||
Scenario: :jseval
|
||||
@ -399,6 +407,7 @@ Feature: Various utility commands.
|
||||
When I open data/hello2.txt in a new tab
|
||||
And I open data/hello3.txt in a new window
|
||||
And I run :window-only
|
||||
And I wait for "Closing window *" in the log
|
||||
Then the session should look like:
|
||||
windows:
|
||||
- tabs:
|
||||
@ -527,6 +536,11 @@ Feature: Various utility commands.
|
||||
And I open data/numbers/3.txt
|
||||
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
|
||||
|
||||
@qtwebkit_skip @qt>=5.8 @cannot_have_dict=af-ZA
|
||||
@ -537,4 +551,4 @@ Feature: Various utility commands.
|
||||
@qtwebkit_skip @qt>=5.8 @must_have_dict=en-US
|
||||
Scenario: Set valid and installed language
|
||||
When I run :set spellcheck.languages ["en-US"]
|
||||
Then the option spellcheck.languages should be set to ["en-US"]
|
||||
Then the option spellcheck.languages should be set to ["en-US"]
|
@ -77,7 +77,7 @@ Feature: Special qute:// pages
|
||||
When I set ignore_case to never
|
||||
And I open qute://settings
|
||||
# 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 :click-element id input-ignore_case
|
||||
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
|
||||
When I open qute://settings
|
||||
# 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 :click-element id input-ignore_case
|
||||
And I wait for "Entering mode KeyMode.insert *" in the log
|
||||
|
@ -156,86 +156,86 @@ Feature: Scrolling
|
||||
And I run :scroll down
|
||||
Then the page should not be scrolled
|
||||
|
||||
## :scroll-perc
|
||||
## :scroll-to-perc
|
||||
|
||||
Scenario: Scrolling to bottom with :scroll-perc
|
||||
When I run :scroll-perc 100
|
||||
Scenario: Scrolling to bottom with :scroll-to-perc
|
||||
When I run :scroll-to-perc 100
|
||||
Then the page should be scrolled vertically
|
||||
|
||||
Scenario: Scrolling to bottom and to top with :scroll-perc
|
||||
When I run :scroll-perc 100
|
||||
Scenario: Scrolling to bottom and to top with :scroll-to-perc
|
||||
When I run :scroll-to-perc 100
|
||||
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
|
||||
Then the page should not be scrolled
|
||||
|
||||
Scenario: Scrolling to middle with :scroll-perc
|
||||
When I run :scroll-perc 50
|
||||
Scenario: Scrolling to middle with :scroll-to-perc
|
||||
When I run :scroll-to-perc 50
|
||||
Then the page should be scrolled vertically
|
||||
|
||||
Scenario: Scrolling to middle with :scroll-perc (float)
|
||||
When I run :scroll-perc 50.5
|
||||
Scenario: Scrolling to middle with :scroll-to-perc (float)
|
||||
When I run :scroll-to-perc 50.5
|
||||
Then the page should be scrolled vertically
|
||||
|
||||
Scenario: Scrolling to middle and to top with :scroll-perc
|
||||
When I run :scroll-perc 50
|
||||
Scenario: Scrolling to middle and to top with :scroll-to-perc
|
||||
When I run :scroll-to-perc 50
|
||||
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
|
||||
Then the page should not be scrolled
|
||||
|
||||
Scenario: Scrolling to right with :scroll-perc
|
||||
When I run :scroll-perc --horizontal 100
|
||||
Scenario: Scrolling to right with :scroll-to-perc
|
||||
When I run :scroll-to-perc --horizontal 100
|
||||
Then the page should be scrolled horizontally
|
||||
|
||||
Scenario: Scrolling to right and to left with :scroll-perc
|
||||
When I run :scroll-perc --horizontal 100
|
||||
Scenario: Scrolling to right and to left with :scroll-to-perc
|
||||
When I run :scroll-to-perc --horizontal 100
|
||||
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
|
||||
Then the page should not be scrolled
|
||||
|
||||
Scenario: Scrolling to middle (horizontally) with :scroll-perc
|
||||
When I run :scroll-perc --horizontal 50
|
||||
Scenario: Scrolling to middle (horizontally) with :scroll-to-perc
|
||||
When I run :scroll-to-perc --horizontal 50
|
||||
Then the page should be scrolled horizontally
|
||||
|
||||
Scenario: Scrolling to middle and to left with :scroll-perc
|
||||
When I run :scroll-perc --horizontal 50
|
||||
Scenario: Scrolling to middle and to left with :scroll-to-perc
|
||||
When I run :scroll-to-perc --horizontal 50
|
||||
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
|
||||
Then the page should not be scrolled
|
||||
|
||||
Scenario: :scroll-perc without argument
|
||||
When I run :scroll-perc
|
||||
Scenario: :scroll-to-perc without argument
|
||||
When I run :scroll-to-perc
|
||||
Then the page should be scrolled vertically
|
||||
|
||||
Scenario: :scroll-perc without argument and --horizontal
|
||||
When I run :scroll-perc --horizontal
|
||||
Scenario: :scroll-to-perc without argument and --horizontal
|
||||
When I run :scroll-to-perc --horizontal
|
||||
Then the page should be scrolled horizontally
|
||||
|
||||
Scenario: :scroll-perc with count
|
||||
When I run :scroll-perc with count 50
|
||||
Scenario: :scroll-to-perc with count
|
||||
When I run :scroll-to-perc with count 50
|
||||
Then the page should be scrolled vertically
|
||||
|
||||
@qtwebengine_skip: Causes memory leak...
|
||||
Scenario: :scroll-perc with a very big value
|
||||
When I run :scroll-perc 99999999999
|
||||
Scenario: :scroll-to-perc with a very big value
|
||||
When I run :scroll-to-perc 99999999999
|
||||
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
|
||||
And I run :scroll-perc 20
|
||||
And I run :scroll-to-perc 20
|
||||
Then the page should not be scrolled
|
||||
|
||||
Scenario: :scroll-perc with count and argument
|
||||
When I run :scroll-perc 0 with count 50
|
||||
Scenario: :scroll-to-perc with count and argument
|
||||
When I run :scroll-to-perc 0 with count 50
|
||||
Then the page should be scrolled vertically
|
||||
|
||||
# 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
|
||||
And I run :scroll-perc 100
|
||||
And I run :scroll-to-perc 100
|
||||
Then the page should be scrolled vertically
|
||||
|
||||
## :scroll-page
|
||||
@ -280,14 +280,14 @@ Feature: Scrolling
|
||||
Then the page should not be scrolled
|
||||
|
||||
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 run :scroll-page --bottom-navigate next 0 1
|
||||
Then data/hello2.txt should be loaded
|
||||
|
||||
Scenario: :scroll-page with --bottom-navigate and zoom
|
||||
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 run :scroll-page --bottom-navigate next 0 1
|
||||
Then data/hello2.txt should be loaded
|
||||
@ -317,7 +317,7 @@ Feature: Scrolling
|
||||
|
||||
Scenario: Relative scroll position with a position:absolute page
|
||||
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 run :scroll-page --bottom-navigate next 0 1
|
||||
Then data/hello2.txt should be loaded
|
||||
|
@ -897,9 +897,9 @@ Feature: Tab management
|
||||
|
||||
# :buffer
|
||||
|
||||
Scenario: :buffer without args
|
||||
Scenario: :buffer without args or count
|
||||
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
|
||||
When I open data/title.html
|
||||
@ -953,7 +953,7 @@ Feature: Tab management
|
||||
And I run :buffer "99/1"
|
||||
Then the error "There's no window with id 99!" should be shown
|
||||
|
||||
@qtwebengine_flaky
|
||||
@skip # Too flaky
|
||||
Scenario: :buffer with matching window index
|
||||
Given I have a fresh instance
|
||||
When I open data/title.html
|
||||
|
@ -17,6 +17,8 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import json
|
||||
|
||||
import pytest_bdd as bdd
|
||||
bdd.scenarios('misc.feature')
|
||||
|
||||
@ -26,3 +28,10 @@ def pdf_exists(quteproc, tmpdir, filename):
|
||||
path = tmpdir / filename
|
||||
data = path.read_binary()
|
||||
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))
|
||||
|
@ -458,7 +458,7 @@ class QuteProc(testprocess.Process):
|
||||
__tracebackhide__ = (lambda e:
|
||||
e.errisinstance(testprocess.WaitForTimeout))
|
||||
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
|
||||
else:
|
||||
kwargs['divisor'] = 1
|
||||
@ -494,7 +494,13 @@ class QuteProc(testprocess.Process):
|
||||
if 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."""
|
||||
settings = [
|
||||
('messages.timeout', '0'),
|
||||
|
@ -47,6 +47,9 @@ class FakeConfig:
|
||||
'--verbose': False,
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.webengine = False
|
||||
|
||||
def getoption(self, name):
|
||||
return self.ARGS[name]
|
||||
|
||||
|
@ -242,7 +242,8 @@ class Process(QObject):
|
||||
self._after_start()
|
||||
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):
|
||||
"""Actually start the process."""
|
||||
|
@ -22,23 +22,18 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.parametrize(['file_name', 'elem_id', 'source', 'input_text',
|
||||
'auto_insert'], [
|
||||
('textarea.html', 'qute-textarea', 'clipboard', 'qutebrowser', 'false'),
|
||||
('textarea.html', 'qute-textarea', 'keypress', 'superqutebrowser',
|
||||
'false'),
|
||||
('input.html', 'qute-input', 'clipboard', 'amazingqutebrowser', 'false'),
|
||||
('input.html', 'qute-input', 'keypress', 'awesomequtebrowser', 'false'),
|
||||
('autofocus.html', 'qute-input-autofocus', 'keypress', 'cutebrowser',
|
||||
'true'),
|
||||
@pytest.mark.parametrize(['file_name', 'elem_id', 'source', 'input_text'], [
|
||||
('textarea.html', 'qute-textarea', 'clipboard', 'qutebrowser'),
|
||||
('textarea.html', 'qute-textarea', 'keypress', 'superqutebrowser'),
|
||||
('input.html', 'qute-input', 'clipboard', 'amazingqutebrowser'),
|
||||
('input.html', 'qute-input', 'keypress', 'awesomequtebrowser'),
|
||||
('autofocus.html', 'qute-input-autofocus', 'keypress', 'cutebrowser'),
|
||||
])
|
||||
@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):
|
||||
url_path = 'data/insert_mode_settings/html/{}'.format(file_name)
|
||||
quteproc.open_path(url_path)
|
||||
|
||||
quteproc.set_setting('input.insert_mode.auto_load', auto_insert)
|
||||
quteproc.send_cmd(':zoom {}'.format(zoom))
|
||||
|
||||
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')
|
||||
|
||||
|
||||
@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):
|
||||
url_path = 'data/insert_mode_settings/html/autofocus.html'
|
||||
quteproc.open_path(url_path)
|
||||
|
@ -319,3 +319,18 @@ def test_qute_settings_persistence(short_tmpdir, request, quteproc_new):
|
||||
|
||||
quteproc_new.start(args)
|
||||
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()
|
||||
|
@ -336,6 +336,7 @@ class FakeCommand:
|
||||
deprecated = attr.ib(False)
|
||||
completion = attr.ib(None)
|
||||
maxsplit = attr.ib(None)
|
||||
takes_count = attr.ib(lambda: False)
|
||||
|
||||
|
||||
class FakeTimer(QObject):
|
||||
@ -417,9 +418,6 @@ class FakeYamlConfig:
|
||||
self.loaded = False
|
||||
self._values = {}
|
||||
|
||||
def load(self):
|
||||
self.loaded = True
|
||||
|
||||
def __contains__(self, item):
|
||||
return item in self._values
|
||||
|
||||
@ -432,6 +430,12 @@ class FakeYamlConfig:
|
||||
def __getitem__(self, key):
|
||||
return self._values[key]
|
||||
|
||||
def unset(self, name):
|
||||
self._values.pop(name, None)
|
||||
|
||||
def clear(self):
|
||||
self._values = []
|
||||
|
||||
|
||||
class StatusBarCommandStub(QLineEdit):
|
||||
|
||||
|
@ -36,8 +36,8 @@ def test_timeout(timer):
|
||||
func2 = mock.Mock()
|
||||
timer.timeout.connect(func)
|
||||
timer.timeout.connect(func2)
|
||||
assert not func.called
|
||||
assert not func2.called
|
||||
func.assert_not_called()
|
||||
func2.assert_not_called()
|
||||
timer.timeout.emit()
|
||||
func.assert_called_once_with()
|
||||
func2.assert_called_once_with()
|
||||
@ -49,7 +49,7 @@ def test_disconnect_all(timer):
|
||||
timer.timeout.connect(func)
|
||||
timer.timeout.disconnect()
|
||||
timer.timeout.emit()
|
||||
assert not func.called
|
||||
func.assert_not_called()
|
||||
|
||||
|
||||
def test_disconnect_one(timer):
|
||||
@ -58,7 +58,7 @@ def test_disconnect_one(timer):
|
||||
timer.timeout.connect(func)
|
||||
timer.timeout.disconnect(func)
|
||||
timer.timeout.emit()
|
||||
assert not func.called
|
||||
func.assert_not_called()
|
||||
|
||||
|
||||
def test_disconnect_all_invalid(timer):
|
||||
@ -74,8 +74,8 @@ def test_disconnect_one_invalid(timer):
|
||||
timer.timeout.connect(func1)
|
||||
with pytest.raises(TypeError):
|
||||
timer.timeout.disconnect(func2)
|
||||
assert not func1.called
|
||||
assert not func2.called
|
||||
func1.assert_not_called()
|
||||
func2.assert_not_called()
|
||||
timer.timeout.emit()
|
||||
func1.assert_called_once_with()
|
||||
|
||||
|
@ -284,6 +284,22 @@ def test_import_txt(hist, data_tmpdir, monkeypatch, stubs):
|
||||
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', [
|
||||
'',
|
||||
'#12345 http://example.com/commented',
|
||||
|
@ -26,7 +26,6 @@ import pytest
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject
|
||||
|
||||
from qutebrowser.browser import signalfilter
|
||||
from qutebrowser.utils import objreg
|
||||
|
||||
|
||||
class Signaller(QObject):
|
||||
@ -66,18 +65,11 @@ def objects():
|
||||
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)])
|
||||
def test_filtering(objects, tabbed_browser, index_of, emitted):
|
||||
tabbed_browser.current_index = 0
|
||||
tabbed_browser.index_of = index_of
|
||||
def test_filtering(objects, tabbed_browser_stubs, index_of, emitted):
|
||||
browser = tabbed_browser_stubs[0]
|
||||
browser.current_index = 0
|
||||
browser.index_of = index_of
|
||||
objects.signaller.signal.emit('foo')
|
||||
if emitted:
|
||||
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')])
|
||||
def test_logging(caplog, objects, tabbed_browser, index_of, verb):
|
||||
tabbed_browser.current_index = 0
|
||||
tabbed_browser.index_of = index_of
|
||||
def test_logging(caplog, objects, tabbed_browser_stubs, index_of, verb):
|
||||
browser = tabbed_browser_stubs[0]
|
||||
browser.current_index = 0
|
||||
browser.index_of = index_of
|
||||
|
||||
with caplog.at_level(logging.DEBUG, logger='signals'):
|
||||
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])
|
||||
def test_no_logging(caplog, objects, tabbed_browser, index_of):
|
||||
tabbed_browser.current_index = 0
|
||||
tabbed_browser.index_of = index_of
|
||||
def test_no_logging(caplog, objects, tabbed_browser_stubs, index_of):
|
||||
browser = tabbed_browser_stubs[0]
|
||||
browser.current_index = 0
|
||||
browser.index_of = index_of
|
||||
|
||||
with caplog.at_level(logging.DEBUG, logger='signals'):
|
||||
objects.signaller.link_hovered.emit('foo')
|
||||
@ -109,9 +103,10 @@ def test_no_logging(caplog, objects, tabbed_browser, index_of):
|
||||
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."""
|
||||
tabbed_browser.current_index = 0
|
||||
tabbed_browser.index_of = RuntimeError
|
||||
browser = tabbed_browser_stubs[0]
|
||||
browser.current_index = 0
|
||||
browser.index_of = RuntimeError
|
||||
objects.signaller.signal.emit('foo')
|
||||
assert objects.signaller.filtered_signal_arg is None
|
||||
|
@ -25,7 +25,7 @@ import pytest
|
||||
from PyQt5.QtCore import QUrl
|
||||
|
||||
from qutebrowser.commands import argparser, cmdexc
|
||||
from qutebrowser.utils import usertypes, objreg
|
||||
from qutebrowser.utils import usertypes
|
||||
|
||||
|
||||
Enum = usertypes.enum('Enum', ['foo', 'foo_bar'])
|
||||
@ -37,13 +37,6 @@ class TestArgumentParser:
|
||||
def parser(self):
|
||||
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):
|
||||
assert parser.name == 'foo'
|
||||
|
||||
@ -60,14 +53,14 @@ class TestArgumentParser:
|
||||
match="Unrecognized arguments: --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)
|
||||
|
||||
with pytest.raises(argparser.ArgumentParserExit):
|
||||
parser.parse_args(['--help'])
|
||||
|
||||
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', [
|
||||
|
@ -22,7 +22,6 @@
|
||||
import pytest
|
||||
|
||||
from qutebrowser.commands import runners, cmdexc
|
||||
from qutebrowser.config import configtypes
|
||||
|
||||
|
||||
class TestCommandParser:
|
||||
@ -47,7 +46,6 @@ class TestCommandParser:
|
||||
if not cmdline_test.cmd:
|
||||
pytest.skip("Empty command")
|
||||
|
||||
monkeypatch.setattr(configtypes.Command, 'unvalidated', True)
|
||||
config_stub.val.aliases = {'alias_name': cmdline_test.cmd}
|
||||
|
||||
parser = runners.CommandParser()
|
||||
|
@ -120,7 +120,7 @@ def cmdutils_patch(monkeypatch, stubs, miscmodels_patch):
|
||||
|
||||
@cmdutils.argument('win_id', win_id=True)
|
||||
@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."""
|
||||
pass
|
||||
|
||||
|
@ -103,7 +103,7 @@ def test_delete_cur_item_no_func():
|
||||
parent = model.index(0, 0)
|
||||
with pytest.raises(cmdexc.CommandError):
|
||||
model.delete_cur_item(model.index(0, 0, parent))
|
||||
assert not callback.called
|
||||
callback.assert_not_called()
|
||||
|
||||
|
||||
def test_delete_cur_item_no_cat():
|
||||
@ -114,4 +114,4 @@ def test_delete_cur_item_no_cat():
|
||||
model.rowsRemoved.connect(callback)
|
||||
with pytest.raises(qtutils.QtValueError):
|
||||
model.delete_cur_item(QModelIndex())
|
||||
assert not callback.called
|
||||
callback.assert_not_called()
|
||||
|
@ -242,7 +242,7 @@ def test_completion_item_del_no_selection(completionview):
|
||||
completionview.set_model(model)
|
||||
with pytest.raises(cmdexc.CommandError, match='No item selected!'):
|
||||
completionview.completion_item_del()
|
||||
assert not func.called
|
||||
func.assert_not_called()
|
||||
|
||||
|
||||
def test_resize_no_model(completionview, qtbot):
|
||||
|
@ -140,20 +140,24 @@ def test_sorting(max_items, before, after, model_validator, hist, config_stub):
|
||||
|
||||
|
||||
def test_remove_rows(hist, model_validator):
|
||||
hist.insert({'url': 'foo', 'title': 'Foo'})
|
||||
hist.insert({'url': 'bar', 'title': 'Bar'})
|
||||
hist.insert({'url': 'foo', 'title': 'Foo', 'last_atime': 0})
|
||||
hist.insert({'url': 'bar', 'title': 'Bar', 'last_atime': 0})
|
||||
cat = histcategory.HistoryCategory()
|
||||
model_validator.set_model(cat)
|
||||
cat.set_pattern('')
|
||||
hist.delete('url', 'foo')
|
||||
cat.removeRows(0, 1)
|
||||
model_validator.validate([('bar', 'Bar', '')])
|
||||
model_validator.validate([('bar', 'Bar', '1970-01-01')])
|
||||
|
||||
|
||||
def test_remove_rows_fetch(hist):
|
||||
"""removeRows should fetch enough data to make the current index valid."""
|
||||
# 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.set_pattern('')
|
||||
|
||||
|
@ -119,6 +119,7 @@ def configdata_stub(monkeypatch, configdata_init):
|
||||
'normal': collections.OrderedDict([
|
||||
('<ctrl+q>', 'quit'),
|
||||
('ZQ', 'quit'),
|
||||
('I', 'invalid'),
|
||||
])
|
||||
},
|
||||
backends=[],
|
||||
@ -538,7 +539,8 @@ def test_setting_option_completion(qtmodeltester, config_stub,
|
||||
"Options": [
|
||||
('aliases', 'Aliases for commands.', '{"q": "quit"}'),
|
||||
('bindings.commands', 'Default keybindings',
|
||||
'{"normal": {"<ctrl+q>": "quit", "ZQ": "quit"}}'),
|
||||
'{"normal": {"<ctrl+q>": "quit", "ZQ": "quit", '
|
||||
'"I": "invalid"}}'),
|
||||
('bindings.default', 'Default keybindings',
|
||||
'{"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,
|
||||
key_config_stub, configdata_stub, info):
|
||||
"""Test keybinding completion with no current binding."""
|
||||
|
@ -1,7 +1,6 @@
|
||||
# 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.
|
||||
#
|
||||
# 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
|
||||
# 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'))
|
||||
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))
|
||||
@pytest.fixture
|
||||
def keyconf(config_stub):
|
||||
config_stub.val.aliases = {}
|
||||
return config.KeyConfig(config_stub)
|
@ -18,20 +18,16 @@
|
||||
|
||||
"""Tests for qutebrowser.config.config."""
|
||||
|
||||
import sys
|
||||
import copy
|
||||
import types
|
||||
import logging
|
||||
import unittest.mock
|
||||
|
||||
import pytest
|
||||
from PyQt5.QtCore import QObject, QUrl
|
||||
from PyQt5.QtCore import QObject
|
||||
from PyQt5.QtGui import QColor
|
||||
|
||||
from qutebrowser import qutebrowser
|
||||
from qutebrowser.commands import cmdexc
|
||||
from qutebrowser.config import config, configdata, configexc, configfiles
|
||||
from qutebrowser.utils import objreg, usertypes
|
||||
from qutebrowser.utils import usertypes
|
||||
from qutebrowser.misc import objects
|
||||
|
||||
|
||||
@ -42,18 +38,12 @@ def configdata_init():
|
||||
configdata.init()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def keyconf(config_stub):
|
||||
config_stub.val.aliases = {}
|
||||
return config.KeyConfig(config_stub)
|
||||
|
||||
|
||||
class TestChangeFilter:
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def cleanup_globals(self, monkeypatch):
|
||||
"""Make sure config._change_filters is cleaned up."""
|
||||
monkeypatch.setattr(config, '_change_filters', [])
|
||||
"""Make sure config.change_filters is cleaned up."""
|
||||
monkeypatch.setattr(config, 'change_filters', [])
|
||||
|
||||
@pytest.mark.parametrize('option', ['foobar', 'tab', 'tabss', 'tabs.'])
|
||||
def test_unknown_option(self, option):
|
||||
@ -65,7 +55,7 @@ class TestChangeFilter:
|
||||
def test_validate(self, option):
|
||||
cf = config.change_filter(option)
|
||||
cf.validate()
|
||||
assert cf in config._change_filters
|
||||
assert cf in config.change_filters
|
||||
|
||||
@pytest.mark.parametrize('method', [True, False])
|
||||
@pytest.mark.parametrize('option, changed, matches', [
|
||||
@ -182,38 +172,24 @@ class TestKeyConfig:
|
||||
config_stub.val.bindings.commands = {'normal': bindings}
|
||||
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'])
|
||||
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',
|
||||
'<Ctrl+x>': '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')
|
||||
assert keyconf.get_command(key, 'normal') == 'nop'
|
||||
keyconf.bind(key, 'message-info foo', mode='normal')
|
||||
assert keyconf.get_command(key, 'normal') == 'message-info foo'
|
||||
|
||||
@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.commands = no_bindings
|
||||
|
||||
command = 'message-info foo'
|
||||
|
||||
with qtbot.wait_signal(config_stub.changed):
|
||||
keyconf.bind('a', command, mode=mode)
|
||||
|
||||
@ -221,6 +197,16 @@ class TestKeyConfig:
|
||||
assert keyconf.get_bindings_for(mode)['a'] == 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', [
|
||||
('a', 'a'), # default bindings
|
||||
('b', 'b'), # custom bindings
|
||||
@ -264,335 +250,18 @@ class TestKeyConfig:
|
||||
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:
|
||||
|
||||
@pytest.fixture
|
||||
def conf(self, stubs):
|
||||
yaml_config = stubs.FakeYamlConfig()
|
||||
def conf(self, config_tmpdir):
|
||||
yaml_config = configfiles.YamlConfig()
|
||||
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):
|
||||
opt = conf.get_opt('tabs.show')
|
||||
with qtbot.wait_signal(conf.changed) as blocker:
|
||||
@ -610,19 +279,60 @@ class TestConfig:
|
||||
conf._set_value(opt, 'never')
|
||||
assert conf._values['tabs.show'] == 'never'
|
||||
|
||||
def test_read_yaml(self, conf):
|
||||
assert not conf._yaml.loaded
|
||||
conf._yaml['content.plugins'] = True
|
||||
@pytest.mark.parametrize('save_yaml', [True, False])
|
||||
def test_unset(self, conf, qtbot, save_yaml):
|
||||
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._values['content.plugins'] is True
|
||||
assert conf.get(name) == 'always'
|
||||
if save_yaml:
|
||||
assert name not in conf._yaml
|
||||
else:
|
||||
assert conf._yaml[name] == 'never'
|
||||
|
||||
def test_read_yaml_invalid(self, conf):
|
||||
conf._yaml['foo.bar'] = True
|
||||
def test_unset_never_set(self, conf, qtbot):
|
||||
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):
|
||||
conf.read_yaml()
|
||||
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()
|
||||
assert conf._values['content.plugins'] is True
|
||||
|
||||
def test_get_opt_valid(self, conf):
|
||||
assert conf.get_opt('tabs.show') == configdata.DATA['tabs.show']
|
||||
@ -722,6 +432,19 @@ class TestConfig:
|
||||
assert not conf._mutables
|
||||
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):
|
||||
"""Make sure we don't have unknown mutable types."""
|
||||
conf._values['aliases'] = set() # This would never happen
|
||||
@ -873,205 +596,3 @@ def test_set_register_stylesheet(delete, stylesheet_param, update, qtbot,
|
||||
expected = 'yellow'
|
||||
|
||||
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
|
||||
|
479
tests/unit/config/test_configcommands.py
Normal file
479
tests/unit/config/test_configcommands.py
Normal 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)
|
@ -43,10 +43,11 @@ def test_backend_error():
|
||||
assert str(e) == "This setting is not available with the QtWebKit backend!"
|
||||
|
||||
|
||||
def test_duplicate_key_error():
|
||||
e = configexc.DuplicateKeyError('asdf')
|
||||
assert isinstance(e, configexc.KeybindingError)
|
||||
assert str(e) == "Duplicate key asdf"
|
||||
def test_desc_with_text():
|
||||
"""Test ConfigErrorDesc.with_text."""
|
||||
old = configexc.ConfigErrorDesc("Error text", Exception("Exception text"))
|
||||
new = old.with_text("additional text")
|
||||
assert str(new) == 'Error text (additional text): Exception text'
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
@ -19,15 +19,24 @@
|
||||
"""Tests for qutebrowser.config.configfiles."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import unittest.mock
|
||||
|
||||
import pytest
|
||||
|
||||
from qutebrowser.config import config, configfiles, configexc
|
||||
from qutebrowser.config import config, configfiles, configexc, configdata
|
||||
from qutebrowser.utils import utils
|
||||
|
||||
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', [
|
||||
(None, 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')
|
||||
|
||||
state = configfiles.StateConfig()
|
||||
state.init_save_manager(fake_save_manager)
|
||||
|
||||
if insert:
|
||||
state['general']['newval'] = '23'
|
||||
@ -52,141 +62,276 @@ def test_state_config(fake_save_manager, data_tmpdir,
|
||||
state._save()
|
||||
|
||||
assert statefile.read_text('utf-8') == new_data
|
||||
fake_save_manager.add_saveable('state-config', unittest.mock.ANY)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('old_config', [
|
||||
None,
|
||||
'global:\n colors.hints.fg: magenta',
|
||||
])
|
||||
@pytest.mark.parametrize('insert', [True, False])
|
||||
def test_yaml_config(fake_save_manager, config_tmpdir, old_config, insert):
|
||||
autoconfig = config_tmpdir / 'autoconfig.yml'
|
||||
if old_config is not None:
|
||||
autoconfig.write_text(old_config, 'utf-8')
|
||||
class TestYaml:
|
||||
|
||||
yaml = configfiles.YamlConfig()
|
||||
yaml.load()
|
||||
pytestmark = pytest.mark.usefixtures('config_tmpdir')
|
||||
|
||||
if insert:
|
||||
yaml['tabs.show'] = 'never'
|
||||
@pytest.fixture
|
||||
def yaml(self):
|
||||
return configfiles.YamlConfig()
|
||||
|
||||
yaml._save()
|
||||
@pytest.mark.parametrize('old_config', [
|
||||
None,
|
||||
'global:\n colors.hints.fg: magenta',
|
||||
])
|
||||
@pytest.mark.parametrize('insert', [True, False])
|
||||
def test_yaml_config(self, yaml, config_tmpdir, old_config, insert):
|
||||
autoconfig = config_tmpdir / 'autoconfig.yml'
|
||||
if old_config is not None:
|
||||
autoconfig.write_text(old_config, 'utf-8')
|
||||
|
||||
if not insert and old_config is None:
|
||||
lines = []
|
||||
else:
|
||||
text = autoconfig.read_text('utf-8')
|
||||
lines = text.splitlines()
|
||||
yaml.load()
|
||||
|
||||
if insert:
|
||||
assert lines[0].startswith('# DO NOT edit this file by hand,')
|
||||
assert 'config_version: {}'.format(yaml.VERSION) in lines
|
||||
yaml['tabs.show'] = 'never'
|
||||
|
||||
assert 'global:' in lines
|
||||
yaml._save()
|
||||
|
||||
print(lines)
|
||||
if not insert and old_config is None:
|
||||
lines = []
|
||||
else:
|
||||
text = autoconfig.read_text('utf-8')
|
||||
lines = text.splitlines()
|
||||
|
||||
# WORKAROUND for https://github.com/PyCQA/pylint/issues/574
|
||||
if 'magenta' in (old_config or ''): # pylint: disable=superfluous-parens
|
||||
assert ' colors.hints.fg: magenta' in lines
|
||||
if insert:
|
||||
assert ' tabs.show: never' in lines
|
||||
if insert:
|
||||
assert lines[0].startswith('# DO NOT edit this file by hand,')
|
||||
assert 'config_version: {}'.format(yaml.VERSION) in lines
|
||||
|
||||
assert 'global:' in lines
|
||||
|
||||
@pytest.mark.parametrize('old_config', [
|
||||
None,
|
||||
'global:\n colors.hints.fg: magenta',
|
||||
])
|
||||
@pytest.mark.parametrize('key, value', [
|
||||
('colors.hints.fg', 'green'),
|
||||
('colors.hints.bg', None),
|
||||
('confirm_quit', True),
|
||||
('confirm_quit', False),
|
||||
])
|
||||
def test_yaml_config_changed(fake_save_manager, config_tmpdir, old_config,
|
||||
key, value):
|
||||
autoconfig = config_tmpdir / 'autoconfig.yml'
|
||||
if old_config is not None:
|
||||
autoconfig.write_text(old_config, 'utf-8')
|
||||
print(lines)
|
||||
|
||||
yaml = configfiles.YamlConfig()
|
||||
yaml.load()
|
||||
# WORKAROUND for https://github.com/PyCQA/pylint/issues/574
|
||||
# pylint: disable=superfluous-parens
|
||||
if 'magenta' in (old_config or ''):
|
||||
assert ' colors.hints.fg: magenta' in lines
|
||||
if insert:
|
||||
assert ' tabs.show: never' in lines
|
||||
|
||||
yaml[key] = value
|
||||
assert key in yaml
|
||||
assert yaml[key] == value
|
||||
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)
|
||||
|
||||
yaml._save()
|
||||
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 = configfiles.YamlConfig()
|
||||
yaml.load()
|
||||
yaml.load()
|
||||
yaml._save()
|
||||
|
||||
assert key in yaml
|
||||
assert yaml[key] == value
|
||||
lines = autoconfig.read_text('utf-8').splitlines()
|
||||
assert ' hello:' not in lines
|
||||
|
||||
@pytest.mark.parametrize('old_config', [
|
||||
None,
|
||||
'global:\n colors.hints.fg: magenta',
|
||||
])
|
||||
@pytest.mark.parametrize('key, value', [
|
||||
('colors.hints.fg', 'green'),
|
||||
('colors.hints.bg', None),
|
||||
('confirm_quit', True),
|
||||
('confirm_quit', False),
|
||||
])
|
||||
def test_changed(self, yaml, qtbot, config_tmpdir, old_config, key, value):
|
||||
autoconfig = config_tmpdir / 'autoconfig.yml'
|
||||
if old_config is not None:
|
||||
autoconfig.write_text(old_config, 'utf-8')
|
||||
|
||||
@pytest.mark.parametrize('old_config', [
|
||||
None,
|
||||
'global:\n colors.hints.fg: magenta',
|
||||
])
|
||||
def test_yaml_config_unchanged(fake_save_manager, config_tmpdir, old_config):
|
||||
autoconfig = config_tmpdir / 'autoconfig.yml'
|
||||
mtime = None
|
||||
if old_config is not None:
|
||||
autoconfig.write_text(old_config, 'utf-8')
|
||||
mtime = autoconfig.stat().mtime
|
||||
|
||||
yaml = configfiles.YamlConfig()
|
||||
yaml.load()
|
||||
yaml._save()
|
||||
|
||||
if old_config is None:
|
||||
assert not autoconfig.exists()
|
||||
else:
|
||||
assert autoconfig.stat().mtime == mtime
|
||||
|
||||
|
||||
@pytest.mark.parametrize('line, text, exception', [
|
||||
('%', 'While parsing', 'while scanning a directive'),
|
||||
('global: 42', 'While loading data', "'global' object is not a dict"),
|
||||
('foo: 42', 'While loading data',
|
||||
"Toplevel object does not contain 'global' key"),
|
||||
('42', 'While loading data', "Toplevel object is not a dict"),
|
||||
])
|
||||
def test_yaml_config_invalid(fake_save_manager, config_tmpdir,
|
||||
line, text, exception):
|
||||
autoconfig = config_tmpdir / 'autoconfig.yml'
|
||||
autoconfig.write_text(line, 'utf-8', ensure=True)
|
||||
|
||||
yaml = configfiles.YamlConfig()
|
||||
|
||||
with pytest.raises(configexc.ConfigFileErrors) as excinfo:
|
||||
yaml.load()
|
||||
|
||||
assert len(excinfo.value.errors) == 1
|
||||
error = excinfo.value.errors[0]
|
||||
assert error.text == text
|
||||
assert str(error.exception).splitlines()[0] == exception
|
||||
assert error.traceback is None
|
||||
with qtbot.wait_signal(yaml.changed):
|
||||
yaml[key] = value
|
||||
|
||||
assert key in yaml
|
||||
assert yaml[key] == value
|
||||
|
||||
def test_yaml_oserror(fake_save_manager, config_tmpdir):
|
||||
autoconfig = config_tmpdir / 'autoconfig.yml'
|
||||
autoconfig.ensure()
|
||||
autoconfig.chmod(0)
|
||||
if os.access(str(autoconfig), os.R_OK):
|
||||
# Docker container or similar
|
||||
pytest.skip("File was still readable")
|
||||
yaml._save()
|
||||
|
||||
yaml = configfiles.YamlConfig()
|
||||
with pytest.raises(configexc.ConfigFileErrors) as excinfo:
|
||||
yaml = configfiles.YamlConfig()
|
||||
yaml.load()
|
||||
|
||||
assert len(excinfo.value.errors) == 1
|
||||
error = excinfo.value.errors[0]
|
||||
assert error.text == "While reading"
|
||||
assert isinstance(error.exception, OSError)
|
||||
assert error.traceback is None
|
||||
assert key in yaml
|
||||
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', [
|
||||
None,
|
||||
'global:\n colors.hints.fg: magenta',
|
||||
])
|
||||
def test_unchanged(self, yaml, config_tmpdir, old_config):
|
||||
autoconfig = config_tmpdir / 'autoconfig.yml'
|
||||
mtime = None
|
||||
if old_config is not None:
|
||||
autoconfig.write_text(old_config, 'utf-8')
|
||||
mtime = autoconfig.stat().mtime
|
||||
|
||||
yaml.load()
|
||||
yaml._save()
|
||||
|
||||
if old_config is None:
|
||||
assert not autoconfig.exists()
|
||||
else:
|
||||
assert autoconfig.stat().mtime == mtime
|
||||
|
||||
@pytest.mark.parametrize('line, text, exception', [
|
||||
('%', 'While parsing', 'while scanning a directive'),
|
||||
('global: 42', 'While loading data', "'global' object is not a dict"),
|
||||
('foo: 42', 'While loading data',
|
||||
"Toplevel object does not contain 'global' key"),
|
||||
('42', 'While loading data', "Toplevel object is not a dict"),
|
||||
])
|
||||
def test_invalid(self, yaml, config_tmpdir, line, text, exception):
|
||||
autoconfig = config_tmpdir / 'autoconfig.yml'
|
||||
autoconfig.write_text(line, 'utf-8', ensure=True)
|
||||
|
||||
with pytest.raises(configexc.ConfigFileErrors) as excinfo:
|
||||
yaml.load()
|
||||
|
||||
assert len(excinfo.value.errors) == 1
|
||||
error = excinfo.value.errors[0]
|
||||
assert error.text == text
|
||||
assert str(error.exception).splitlines()[0] == exception
|
||||
assert error.traceback is None
|
||||
|
||||
def test_oserror(self, yaml, config_tmpdir):
|
||||
autoconfig = config_tmpdir / 'autoconfig.yml'
|
||||
autoconfig.ensure()
|
||||
autoconfig.chmod(0)
|
||||
if os.access(str(autoconfig), os.R_OK):
|
||||
# Docker container or similar
|
||||
pytest.skip("File was still readable")
|
||||
|
||||
with pytest.raises(configexc.ConfigFileErrors) as excinfo:
|
||||
yaml.load()
|
||||
|
||||
assert len(excinfo.value.errors) == 1
|
||||
error = excinfo.value.errors[0]
|
||||
assert error.text == "While reading"
|
||||
assert isinstance(error.exception, OSError)
|
||||
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:
|
||||
@ -195,26 +340,23 @@ class TestConfigPy:
|
||||
|
||||
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
|
||||
def confpy(self, tmpdir):
|
||||
return self.ConfPy(tmpdir)
|
||||
def confpy(self, tmpdir, config_tmpdir, data_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', [
|
||||
'c.colors.hints.bg = "red"',
|
||||
@ -231,25 +373,15 @@ class TestConfigPy:
|
||||
'config.get("colors.hints.fg")',
|
||||
])
|
||||
def test_get(self, confpy, set_first, get_line):
|
||||
"""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.
|
||||
"""
|
||||
"""Test whether getting options works correctly."""
|
||||
# pylint: disable=bad-config-option
|
||||
config.val.colors.hints.fg = 'green'
|
||||
if set_first:
|
||||
confpy.write('c.colors.hints.fg = "red"',
|
||||
'c.colors.hints.bg = {}'.format(get_line))
|
||||
expected = 'red'
|
||||
'assert {} == "red"'.format(get_line))
|
||||
else:
|
||||
confpy.write('c.colors.hints.bg = {}'.format(get_line))
|
||||
expected = 'green'
|
||||
confpy.write('assert {} == "green"'.format(get_line))
|
||||
confpy.read()
|
||||
assert config.instance._values['colors.hints.bg'] == expected
|
||||
|
||||
@pytest.mark.parametrize('line, mode', [
|
||||
('config.bind(",a", "message-info foo")', 'normal'),
|
||||
@ -261,6 +393,29 @@ class TestConfigPy:
|
||||
expected = {mode: {',a': 'message-info foo'}}
|
||||
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', [
|
||||
('config.unbind("o")', 'o', 'normal'),
|
||||
('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']['bar'] == 'message-info bar'
|
||||
|
||||
def test_reading_default_location(self, 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):
|
||||
def test_oserror(self, tmpdir, data_tmpdir, config_tmpdir):
|
||||
with pytest.raises(configexc.ConfigFileErrors) as excinfo:
|
||||
configfiles.read_config_py(str(tmpdir / 'foo'))
|
||||
|
||||
@ -305,7 +450,7 @@ class TestConfigPy:
|
||||
|
||||
assert len(excinfo.value.errors) == 1
|
||||
error = excinfo.value.errors[0]
|
||||
assert isinstance(error.exception, (TypeError, ValueError))
|
||||
assert isinstance(error.exception, ValueError)
|
||||
assert error.text == "Error while compiling"
|
||||
exception_text = 'source code string cannot contain null bytes'
|
||||
assert str(error.exception) == exception_text
|
||||
@ -330,13 +475,9 @@ class TestConfigPy:
|
||||
assert " ^" in tblines
|
||||
|
||||
def test_unhandled_exception(self, confpy):
|
||||
confpy.write("config.load_autoconfig = False", "1/0")
|
||||
api = configfiles.read_config_py(confpy.filename)
|
||||
confpy.write("1/0")
|
||||
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 isinstance(error.exception, ZeroDivisionError)
|
||||
|
||||
@ -348,9 +489,8 @@ class TestConfigPy:
|
||||
def test_config_val(self, confpy):
|
||||
"""Using config.val should not work in config.py files."""
|
||||
confpy.write("config.val.colors.hints.bg = 'red'")
|
||||
api = configfiles.read_config_py(confpy.filename)
|
||||
assert len(api.errors) == 1
|
||||
error = api.errors[0]
|
||||
error = confpy.read(error=True)
|
||||
|
||||
assert error.text == "Unhandled exception"
|
||||
assert isinstance(error.exception, AttributeError)
|
||||
message = "'ConfigAPI' object has no attribute 'val'"
|
||||
@ -358,13 +498,9 @@ class TestConfigPy:
|
||||
|
||||
@pytest.mark.parametrize('line', ["c.foo = 42", "config.set('foo', 42)"])
|
||||
def test_config_error(self, confpy, line):
|
||||
confpy.write(line, "config.load_autoconfig = False")
|
||||
api = configfiles.read_config_py(confpy.filename)
|
||||
confpy.write(line)
|
||||
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 isinstance(error.exception, configexc.NoOptionError)
|
||||
assert str(error.exception) == "No option 'foo'"
|
||||
@ -372,16 +508,20 @@ class TestConfigPy:
|
||||
|
||||
def test_multiple_errors(self, confpy):
|
||||
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 isinstance(error.exception, configexc.NoOptionError)
|
||||
assert str(error.exception) == "No option 'foo'"
|
||||
assert error.traceback is None
|
||||
|
||||
error = api.errors[2]
|
||||
error = errors[2]
|
||||
assert error.text == "Unhandled exception"
|
||||
assert isinstance(error.exception, ZeroDivisionError)
|
||||
assert error.traceback is not None
|
||||
|
307
tests/unit/config/test_configinit.py
Normal file
307
tests/unit/config/test_configinit.py
Normal 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
|
@ -193,7 +193,8 @@ class TestAll:
|
||||
if member in [configtypes.BaseType, configtypes.MappingType,
|
||||
configtypes._Numeric]:
|
||||
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.Url())
|
||||
elif member is configtypes.Dict:
|
||||
@ -240,6 +241,9 @@ class TestAll:
|
||||
configtypes.PercOrInt, # ditto
|
||||
]:
|
||||
return
|
||||
if (isinstance(typ, configtypes.ListOrValue) and
|
||||
isinstance(typ.valtype, configtypes.Int)):
|
||||
return
|
||||
|
||||
assert converted == s
|
||||
|
||||
@ -250,7 +254,7 @@ class TestAll:
|
||||
to_py_expected = configtypes.PaddingValues(None, None, None, None)
|
||||
elif isinstance(typ, configtypes.Dict):
|
||||
to_py_expected = {}
|
||||
elif isinstance(typ, configtypes.List):
|
||||
elif isinstance(typ, (configtypes.List, configtypes.ListOrValue)):
|
||||
to_py_expected = []
|
||||
else:
|
||||
to_py_expected = None
|
||||
@ -366,6 +370,10 @@ class TestBaseType:
|
||||
def test_to_doc(self, klass, 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):
|
||||
|
||||
@ -546,6 +554,14 @@ class TestList:
|
||||
with pytest.raises(configexc.ValidationError):
|
||||
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']])
|
||||
def test_to_py_valid(self, klass, val):
|
||||
assert klass().to_py(val) == val
|
||||
@ -670,6 +686,108 @@ class TestFlagList:
|
||||
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:
|
||||
|
||||
TESTS = {
|
||||
@ -718,8 +836,10 @@ class TestBool:
|
||||
def test_to_str(self, klass, val, expected):
|
||||
assert klass().to_str(val) == expected
|
||||
|
||||
def test_to_doc(self, klass):
|
||||
assert klass().to_doc(True) == '+pass:[true]+'
|
||||
@pytest.mark.parametrize('value, expected', [(True, '+pass:[true]+'),
|
||||
(False, '+pass:[false]+')])
|
||||
def test_to_doc(self, klass, value, expected):
|
||||
assert klass().to_doc(value) == expected
|
||||
|
||||
|
||||
class TestBoolAsk:
|
||||
@ -1072,37 +1192,10 @@ class TestCommand:
|
||||
monkeypatch.setattr(configtypes, '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
|
||||
def klass(self):
|
||||
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):
|
||||
"""Test completion."""
|
||||
items = klass().complete()
|
||||
@ -1461,6 +1554,16 @@ class TestDict:
|
||||
valtype=configtypes.Int())
|
||||
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', [
|
||||
(configtypes.String(), configtypes.String(), {'hello': 'world'}),
|
||||
(configtypes.String(), configtypes.Int(), {'hello': 42}),
|
||||
|
@ -31,6 +31,12 @@ BINDINGS = {'prompt': {'<Ctrl-a>': 'message-info ctrla',
|
||||
'command': {'foo': 'message-info bar',
|
||||
'<Ctrl+X>': 'message-info ctrlx'},
|
||||
'normal': {'a': 'message-info a', 'ba': 'message-info ba'}}
|
||||
MAPPINGS = {
|
||||
'<Ctrl+a>': 'a',
|
||||
'<Ctrl+b>': '<Ctrl+a>',
|
||||
'x': 'a',
|
||||
'b': 'a',
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@ -38,3 +44,4 @@ def keyinput_bindings(config_stub, key_config_stub):
|
||||
"""Register some test bindings."""
|
||||
config_stub.val.bindings.default = {}
|
||||
config_stub.val.bindings.commands = dict(BINDINGS)
|
||||
config_stub.val.bindings.key_mappings = dict(MAPPINGS)
|
||||
|
@ -91,8 +91,7 @@ class TestDebugLog:
|
||||
])
|
||||
def test_split_count(config_stub, input_key, supports_count, expected):
|
||||
kp = basekeyparser.BaseKeyParser(0, supports_count=supports_count)
|
||||
kp._keystring = input_key
|
||||
assert kp._split_count() == expected
|
||||
assert kp._split_count(input_key) == expected
|
||||
|
||||
|
||||
@pytest.mark.usefixtures('keyinput_bindings')
|
||||
@ -165,20 +164,14 @@ class TestSpecialKeys:
|
||||
keyparser._read_config('prompt')
|
||||
|
||||
def test_valid_key(self, fake_keyevent_factory, keyparser):
|
||||
if utils.is_mac:
|
||||
modifier = Qt.MetaModifier
|
||||
else:
|
||||
modifier = Qt.ControlModifier
|
||||
modifier = Qt.MetaModifier if utils.is_mac else Qt.ControlModifier
|
||||
keyparser.handle(fake_keyevent_factory(Qt.Key_A, modifier))
|
||||
keyparser.handle(fake_keyevent_factory(Qt.Key_X, modifier))
|
||||
keyparser.execute.assert_called_once_with(
|
||||
'message-info ctrla', keyparser.Type.special, None)
|
||||
|
||||
def test_valid_key_count(self, fake_keyevent_factory, keyparser):
|
||||
if utils.is_mac:
|
||||
modifier = Qt.MetaModifier
|
||||
else:
|
||||
modifier = Qt.ControlModifier
|
||||
modifier = Qt.MetaModifier if utils.is_mac else Qt.ControlModifier
|
||||
keyparser.handle(fake_keyevent_factory(5, text='5'))
|
||||
keyparser.handle(fake_keyevent_factory(Qt.Key_A, modifier, text='A'))
|
||||
keyparser.execute.assert_called_once_with(
|
||||
@ -199,6 +192,22 @@ class TestSpecialKeys:
|
||||
keyparser.handle(fake_keyevent_factory(Qt.Key_A, Qt.NoModifier))
|
||||
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:
|
||||
|
||||
@ -230,7 +239,7 @@ class TestKeyChain:
|
||||
handle_text((Qt.Key_X, 'x'),
|
||||
# Then start the real chain
|
||||
(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)
|
||||
assert keyparser._keystring == ''
|
||||
|
||||
@ -249,6 +258,16 @@ class TestKeyChain:
|
||||
handle_text((Qt.Key_C, 'c'))
|
||||
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:
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user