Merge branch 'new-config'

This commit is contained in:
Florian Bruhin 2017-09-16 16:12:21 +02:00
commit 624c3a4c27
225 changed files with 13744 additions and 14630 deletions

View File

@ -43,6 +43,8 @@ putty-ignore =
tests/helpers/fixtures.py : +N806
tests/unit/browser/webkit/http/test_content_disposition.py : +D400
scripts/dev/ci/appveyor_install.py : +FI53
# FIXME:conf
tests/unit/completion/test_models.py : +F821
copyright-check = True
copyright-regexp = # Copyright [\d-]+ .*
copyright-min-file-size = 110

View File

@ -30,6 +30,7 @@ disable=no-self-use,
broad-except,
bare-except,
eval-used,
exec-used,
ungrouped-imports,
suppressed-message,
too-many-return-statements,

View File

@ -193,7 +193,7 @@ Configuration not saved after modifying config.::
Unable to view flash content.::
If you have flash installed for on your system, it's necessary to enable plugins
to use the flash plugin. Using the command `:set content allow-plugins true`
to use the flash plugin. Using the command `:set content.plugins true`
in qutebrowser will enable plugins. Packages for flash should
be provided for your platform or it can be obtained from
http://get.adobe.com/flashplayer/[Adobe].

View File

@ -17,6 +17,7 @@ include requirements.txt
include tox.ini
include qutebrowser.py
include misc/cheatsheet.svg
include qutebrowser/config/configdata.yml
prune www
prune scripts/dev

View File

@ -50,6 +50,7 @@ image:https://qutebrowser.org/img/cheatsheet-small.png["qutebrowser key binding
* link:doc/quickstart.asciidoc[Quick start guide]
* A https://www.shortcutfoo.com/app/dojos/qutebrowser[free training course] to remember those key bindings.
* link:FAQ.asciidoc[Frequently asked questions]
* link:doc/help/configuring.html[Configuring qutebrowser]
* link:CONTRIBUTING.asciidoc[Contributing to qutebrowser]
* link:INSTALL.asciidoc[INSTALL]
* link:CHANGELOG.asciidoc[Change Log]

View File

@ -88,7 +88,6 @@ It is possible to run or bind multiple commands by separating them with `;;`.
|<<undo,undo>>|Re-open a closed tab.
|<<view-source,view-source>>|Show the source of the current page in a new tab.
|<<window-only,window-only>>|Close all windows except for the current one.
|<<wq,wq>>|Save open pages and quit.
|<<yank,yank>>|Yank something to the clipboard or primary selection.
|<<zoom,zoom>>|Set the zoom level for the current tab.
|<<zoom-in,zoom-in>>|Increase the zoom level for the current tab.
@ -126,7 +125,8 @@ Bind a key to a command.
==== optional arguments
* +*-m*+, +*--mode*+: A comma-separated list of modes to bind the key in (default: `normal`).
* +*-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.
@ -281,7 +281,7 @@ Syntax: +:edit-url [*--bg*] [*--tab*] [*--window*] ['url']+
Navigate to a url formed in an external editor.
The editor which should be launched can be configured via the `general -> editor` config option.
The editor which should be launched can be configured via the `editor.command` config option.
==== positional arguments
* +'url'+: URL to edit; defaults to the current page url.
@ -338,7 +338,7 @@ Show help about a command or setting.
* +'topic'+: The topic to show help for.
- :__command__ for commands.
- __section__\->__option__ for settings.
- __section__.__option__ for settings.
==== optional arguments
@ -368,7 +368,7 @@ Start hinting.
- `normal`: Open the link.
- `current`: Open the link in the current tab.
- `tab`: Open the link in a new tab (honoring the
background-tabs setting).
`tabs.background_tabs` setting).
- `tab-fg`: Open the link in a new foreground tab.
- `tab-bg`: Open the link in a new background tab.
- `window`: Open the link in a new window.
@ -402,13 +402,13 @@ Start hinting.
==== optional arguments
* +*-r*+, +*--rapid*+: Whether to do rapid hinting. This is only possible with targets `tab` (with background-tabs=true), `tab-bg`,
* +*-r*+, +*--rapid*+: Whether to do rapid hinting. This is only possible with targets `tab` (with `tabs.background_tabs=true`), `tab-bg`,
`window`, `run`, `hover`, `userscript` and `spawn`.
* +*-m*+, +*--mode*+: The hinting mode to use.
- `number`: Use numeric hints.
- `letter`: Use the chars in the hints->chars settings.
- `letter`: Use the chars in the hints.chars setting.
- `word`: Use hint words based on the html elements and the
extra words.
@ -545,7 +545,7 @@ For `increment` and `decrement`, the number to change the URL by. For `up`, the
[[open]]
=== open
Syntax: +:open [*--implicit*] [*--bg*] [*--tab*] [*--window*] [*--secure*] [*--private*]
Syntax: +:open [*--related*] [*--bg*] [*--tab*] [*--window*] [*--secure*] [*--private*]
['url']+
Open a URL in the current/[count]th tab.
@ -556,7 +556,7 @@ If the URL contains newlines, each line gets opened in its own tab.
* +'url'+: The URL to open.
==== optional arguments
* +*-i*+, +*--implicit*+: If opening a new tab, treat the tab as implicit (like clicking on a link).
* +*-r*+, +*--related*+: If opening a new tab, position the tab as related to the current one (like clicking on a link).
* +*-b*+, +*--bg*+: Open in a new background tab.
* +*-t*+, +*--tab*+: Open in a new tab.
@ -632,8 +632,17 @@ Save the current page as a quickmark.
[[quit]]
=== quit
Syntax: +:quit [*--save*] ['session']+
Quit qutebrowser.
==== positional arguments
* +'session'+: The name of the session to save.
==== optional arguments
* +*-s*+, +*--save*+: When given, save the open windows even if auto_save.session is turned off.
[[record-macro]]
=== record-macro
Syntax: +:record-macro ['register']+
@ -752,7 +761,7 @@ Syntax: +:session-save [*--current*] [*--quiet*] [*--force*] [*--only-active-win
Save a session.
==== positional arguments
* +'name'+: The name of the session. If not given, the session configured in general -> session-default-name is saved.
* +'name'+: The name of the session. If not given, the session configured in session_default_name is saved.
==== optional arguments
@ -764,19 +773,18 @@ Save a session.
[[set]]
=== set
Syntax: +:set [*--temp*] [*--print*] ['section'] ['option'] ['values' ['values' ...]]+
Syntax: +:set [*--temp*] [*--print*] ['option'] ['values' ['values' ...]]+
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.
==== positional arguments
* +'section'+: The section where the option is in.
* +'option'+: The name of the option.
* +'values'+: The value to set, or the values to cycle through.
==== optional arguments
* +*-t*+, +*--temp*+: Set value temporarily.
* +*-t*+, +*--temp*+: Set value temporarily until qutebrowser is closed.
* +*-p*+, +*--print*+: Print the value after setting.
[[set-cmd-text]]
@ -843,7 +851,7 @@ Close the current/[count]th tab.
==== optional arguments
* +*-p*+, +*--prev*+: Force selecting the tab before the current tab.
* +*-n*+, +*--next*+: Force selecting the tab after the current tab.
* +*-o*+, +*--opposite*+: Force selecting the tab in the opposite direction of what's configured in 'tabs->select-on-remove'.
* +*-o*+, +*--opposite*+: Force selecting the tab in the opposite direction of what's configured in 'tabs.select_on_remove'.
* +*-f*+, +*--force*+: Avoid confirmation for pinned tabs.
@ -911,7 +919,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->pinned-width size. Attempting to close a pinned tab will cause a confirmation, unless --force is passed.
Pinning a tab shrinks it to `tabs.width.pinned` size. Attempting to close a pinned tab will cause a confirmation, unless --force is passed.
==== count
The tab index to pin or unpin
@ -925,13 +933,15 @@ How many tabs to switch back.
[[unbind]]
=== unbind
Syntax: +:unbind 'key' ['mode']+
Syntax: +:unbind [*--mode* 'mode'] 'key'+
Unbind a keychain.
==== positional arguments
* +'key'+: The keychain or special key (inside <...>) to unbind.
* +'mode'+: A comma-separated list of modes to unbind the key in (default: `normal`).
==== optional arguments
* +*-m*+, +*--mode*+: A mode to unbind the key in (default: `normal`). See `:help bindings.commands` for the available modes.
[[undo]]
@ -946,15 +956,6 @@ Show the source of the current page in a new tab.
=== window-only
Close all windows except for the current one.
[[wq]]
=== wq
Syntax: +:wq ['name']+
Save open pages and quit.
==== positional arguments
* +'name'+: The name of the session.
[[yank]]
=== yank
Syntax: +:yank [*--sel*] [*--keep*] ['what']+
@ -1043,6 +1044,7 @@ How many steps to zoom out.
|<<move-to-start-of-line,move-to-start-of-line>>|Move the cursor or selection to the start of the line.
|<<move-to-start-of-next-block,move-to-start-of-next-block>>|Move the cursor or selection to the start of next block.
|<<move-to-start-of-prev-block,move-to-start-of-prev-block>>|Move the cursor or selection to the start of previous block.
|<<nop,nop>>|Do nothing.
|<<open-editor,open-editor>>|Open an external editor with the currently selected form field.
|<<prompt-accept,prompt-accept>>|Accept the current prompt.
|<<prompt-item-focus,prompt-item-focus>>|Shift the focus of the prompt file completion menu to another item.
@ -1290,11 +1292,15 @@ Move the cursor or selection to the start of previous block.
==== count
How many blocks to move.
[[nop]]
=== nop
Do nothing.
[[open-editor]]
=== open-editor
Open an external editor with the currently selected form field.
The editor which should be launched can be configured via the `general -> editor` config option.
The editor which should be launched can be configured via the `editor.command` config option.
[[prompt-accept]]
=== prompt-accept

View File

@ -0,0 +1,180 @@
Configuring qutebrowser
=======================
IMPORTANT: qutebrowser's configuration system was completely rewritten in
September 2017. This information is not applicable to older releases, and older
information elsewhere might be outdated. **If you had an old configuration
around and upgraded, this page will automatically open once**. To view it at a
later time, use the `:help` command.
Migrating older configurations
------------------------------
qutebrowser does no automatic migration for the new configuration. However,
there's a special link:qute://configdiff[config diff page] which will show you
the changes you did in your old configuration, compared to the old defaults.
Configuring qutebrowser via the user interface
----------------------------------------------
The easy (but less flexible) way to configure qutebrowser is using its user
interface or command line. Changes you make this way are immediately active
(with the exception of a few settings, where this is pointed out in the
documentation) and are persisted in an `autoconfig.yml` file.
Using the link:commands.html#set[`:set`] command and command completion, you
can quickly set settings interactively, for example `:set tabs.position left`.
To get more help about a setting, use e.g. `:help tabs.position`.
If you want to customize many settings, you can open the link:qute://settings[]
page by running `:set` without any arguments, where all settings are listed and
customizable.
To bind and unbind keys, you can use the link:commands.html#bind[`:bind`] and
link:commands.html#unbind[`:unbind`] commands:
- Binding a key: `:bind ,v spawn mpv {url}`
- Unbinding: `:unbind ,v`
- Changing an existing binding: `bind --force ,v message-info foo`
Key chains starting with a comma are ideal for custom bindings, as the comma key
will never be used in a default keybinding.
The `autoconfig.yml` file is located in the "config" folder listed on the
link:qute://version[] page. On macOS, the "auto config" folder is used, which is
different from where hand-written config files are kept.
**Do not** edit `autoconfig.yml` by hand. Instead, see the next section.
Configuring qutebrowser via config.py
-------------------------------------
For more powerful configuration possibilities, you can create a `config.py`
file. Since it's a Python file, you have much more flexibility for
configuration. Note that qutebrowser will never touch this file - this means
you'll be responsible for updating it when upgrading to a newer qutebrowser
version.
The file should be located in the "config" location listed on
link:qute://version[], which is typically `~/.config/qutebrowser/config.py` on
Linux, `~/.qutebrowser/config.py` on macOS, and
`%APPDATA%/qutebrowser/config.py` on Windows.
Two global objects are pre-defined when executing the file: `c` and `config`.
Changing settings
~~~~~~~~~~~~~~~~~
`c` is a shorthand object to easily set settings like this:
[source,python]
----
c.tabs.position = "left"
c.completion.shrink = True
----
See the link:settings.html[settings help page] for all available settings. The
accepted values depend on the type of the option. Commonly used are:
- Strings: `c.tabs.position = "left"`
- Booleans: `c.completion.shrink = True`
- Integers: `c.messages.timeout = 5000`
- Dictionaries:
* `c.headers.custom = {'X-Hello': 'World'}` to override any other values in the
dictionary.
* `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.
* `c.url.start_pages.append("https://www.python.org/")` to add a new value.
Any other config types (e.g. a color) are specified as a string, with the
exception of the `Regex` type which can take either a string (with an `r` prefix
to preserve backslashes) or a Python regex object:
- `c.hints.next_regexes.append(r'\bvor\b')`
- `c.hints.prev_regexes.append(re.compile(r'\bzurück\b'))`
If you want to read a setting, you can use the `c` object to do so as well:
`c.colors.tabs.even.bg = c.colors.tabs.odd.bg`.
Using strings for setting names
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you want to set settings based on their name as a string, use the
`config.set` method:
[source,python]
----
config.set('content.javascript.enabled', False)
----
To read a setting, use the `config.get` method:
[source,python]
----
color = config.get('colors.completion.fg')
----
Binding keys
~~~~~~~~~~~~
While it's possible to change the `bindings.commands` setting to bind keys, it's
preferred to use the `config.bind` command. Doing so ensures the commands are
valid and normalizes different expressions which map to the same key.
For details on how to specify keys and the available modes, see the
link:settings.html#bindings.commands[documentation] for the `bindings.commands`
setting.
To bind a key:
[source,python]
----
config.bind(',v', 'spawn mpv {url}', mode='normal')
----
If the key is already bound, `force=True` needs to be given to rebind it:
[source,python]
----
config.bind(',v', 'message-info foo', mode='normal', force=True)
----
To unbind a key (either a key which has been bound before, or a default binding):
[source,python]
----
config.unbind(',v', mode='normal')
----
Key chains starting with a comma are ideal for custom bindings, as the comma key
will never be used in a default keybinding.
To suppress loading of any default keybindings, you can set `c.bindings.defaults
= {}`.
Prevent 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:
[source,python]
----
config.load_autoconfig = False
----
Handling errors
~~~~~~~~~~~~~~~
If there are errors in your `config.py`, qutebrowser will try to apply as much
of it as possible, and show an error dialog before starting.
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]!

View File

@ -10,6 +10,7 @@ The following help pages are currently available:
* link:../../FAQ.html[Frequently asked questions]
* link:../../CHANGELOG.html[Change Log]
* link:commands.html[Documentation of commands]
* link:configuring.html[Configuring qutebrowser]
* link:settings.html[Documentation of settings]
* link:../userscripts.html[How to write userscripts]
* link:../../CONTRIBUTING.html[Contributing to qutebrowser]

File diff suppressed because it is too large Load Diff

View File

@ -44,7 +44,7 @@ show it.
*-V*, *--version*::
Show version and quit.
*-s* 'SECTION' 'OPTION' 'VALUE', *--set* 'SECTION' 'OPTION' 'VALUE'::
*-s* 'OPTION' 'VALUE', *--set* 'OPTION' 'VALUE'::
Set a temporary setting for this session.
*-r* 'SESSION', *--restore* 'SESSION'::

View File

@ -15,7 +15,8 @@ def get_data_files():
('../qutebrowser/img', 'img'),
('../qutebrowser/javascript', 'javascript'),
('../qutebrowser/html/doc', 'html/doc'),
('../qutebrowser/git-commit-id', '')
('../qutebrowser/git-commit-id', ''),
('../qutebrowser/config/configdata.yml', 'config'),
]
# if os.path.exists(os.path.join('qutebrowser', '3rdparty', 'pdfjs')):

View File

@ -23,7 +23,7 @@
# If run from qutebrowser as a userscript, it runs :open on the URL
# If not, it opens a new qutebrowser window at the URL
#
# Ideal for use with tabs-are-windows. Set a hotkey to launch this script, then:
# Ideal for use with tabs_are_windows. Set a hotkey to launch this script, then:
# :bind o spawn --userscript dmenu_qutebrowser
#
# Use the hotkey to open in new tab/window, press 'o' to open URL in current tab/window

View File

@ -12,7 +12,7 @@
# - rofi (in a recent version)
# - xdg-open and xdg-mime
# - You should configure qutebrowser to download files to a single directory
# - It comes in handy if you enable remove-finished-downloads. If you want to
# - It comes in handy if you enable downloads.remove_finished. If you want to
# see the recent downloads, just press "sd".
#
# Thorsten Wißmann, 2015 (thorsten` on freenode)

View File

@ -21,7 +21,7 @@
# Opens all links to feeds defined in the head of a site
#
# Ideal for use with tabs-are-windows. Set a hotkey to launch this script, then:
# Ideal for use with tabs_are_windows. Set a hotkey to launch this script, then:
# :bind gF spawn --userscript openfeeds
#
# Use the hotkey to open the feeds in new tab/window, press 'gF' to open

View File

@ -15,6 +15,7 @@ markers =
end2end: End to end tests which run qutebrowser as subprocess
xfail_norun: xfail the test with out running it
ci: Tests which should only run on CI.
no_ci: Tests which should not run on CI.
qtwebengine_todo: Features still missing with QtWebEngine
qtwebengine_skip: Tests not applicable with QtWebEngine
qtwebkit_skip: Tests not applicable with QtWebKit
@ -26,6 +27,7 @@ markers =
this: Used to mark tests during development
no_invalid_lines: Don't fail on unparseable lines in end2end tests
issue2478: Tests which are broken on Windows with QtWebEngine, https://github.com/qutebrowser/qutebrowser/issues/2478
qt55: Tests only running on Qt 5.5 or later
qt_log_level_fail = WARNING
qt_log_ignore =
^SpellCheck: .*
@ -50,7 +52,8 @@ qt_log_ignore =
^Error when parsing the netrc file
^Image of format '' blocked because it is not considered safe. If you are sure it is safe to do so, you can white-list the format by setting the environment variable QTWEBKIT_IMAGEFORMAT_WHITELIST=
^QPainter::end: Painter ended with \d+ saved states
^QSslSocket: cannot resolve SSLv[23]_(client|server)_method
^QSslSocket: cannot resolve *
^Incompatible version of OpenSSL
^QQuickWidget::invalidateRenderControl could not make context current
^libpng warning: iCCP: known incorrect sRGB profile
xfail_strict = true

View File

@ -22,7 +22,6 @@
import os
import sys
import subprocess
import configparser
import functools
import json
import shutil
@ -44,8 +43,7 @@ import qutebrowser
import qutebrowser.resources
from qutebrowser.completion.models import miscmodels
from qutebrowser.commands import cmdutils, runners, cmdexc
from qutebrowser.config import style, config, websettings, configexc
from qutebrowser.config.parsers import keyconf
from qutebrowser.config import config, websettings, configexc
from qutebrowser.browser import (urlmarks, adblock, history, browsertab,
downloads)
from qutebrowser.browser.network import proxy
@ -54,7 +52,7 @@ 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, objects, sql)
crashsignal, earlyinit, objects, sql, cmdhistory)
from qutebrowser.misc import utilcmds # pylint: disable=unused-import
from qutebrowser.utils import (log, version, message, utils, qtutils, urlutils,
objreg, usertypes, standarddir, error)
@ -148,8 +146,6 @@ def init(args, crash_handler):
objreg.register('event-filter', event_filter)
log.init.debug("Connecting signals...")
config_obj = objreg.get('config')
config_obj.style_changed.connect(style.get_stylesheet.cache_clear)
qApp.focusChanged.connect(on_focus_changed)
_process_args(args)
@ -184,11 +180,10 @@ def _init_icon():
def _process_args(args):
"""Open startpage etc. and process commandline args."""
config_obj = objreg.get('config')
for sect, opt, val in args.temp_settings:
for opt, val in args.temp_settings:
try:
config_obj.set('temp', sect, opt, val)
except (configexc.Error, configparser.Error) as e:
config.instance.set_str(opt, val)
except configexc.Error as e:
message.error("set: {} - {}".format(e.__class__.__name__, e))
if not args.override_restore:
@ -274,7 +269,7 @@ def process_pos_args(args, via_ipc=False, cwd=None, target_arg=None):
if via_ipc and target_arg and target_arg != 'auto':
open_target = target_arg
else:
open_target = config.get('general', 'new-instance-open-target')
open_target = config.val.new_instance_open_target
win_id = mainwindow.get_window(via_ipc, force_target=open_target)
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
@ -289,7 +284,7 @@ def process_pos_args(args, via_ipc=False, cwd=None, target_arg=None):
else:
background = open_target in ['tab-bg', 'tab-bg-silent']
tabbed_browser.tabopen(url, background=background,
explicit=True)
related=False)
def _open_startpage(win_id=None):
@ -309,15 +304,9 @@ def _open_startpage(win_id=None):
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=cur_win_id)
if tabbed_browser.count() == 0:
log.init.debug("Opening startpage")
for urlstr in config.get('general', 'startpage'):
try:
url = urlutils.fuzzy_url(urlstr, do_search=False)
except urlutils.InvalidUrlError as e:
message.error("Error when opening startpage: {}".format(e))
tabbed_browser.tabopen(QUrl('about:blank'))
else:
tabbed_browser.tabopen(url)
log.init.debug("Opening start pages")
for url in config.val.url.start_pages:
tabbed_browser.tabopen(url)
def _open_special_pages(args):
@ -335,6 +324,7 @@ def _open_special_pages(args):
return
state_config = objreg.get('state-config')
general_sect = state_config['general']
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window='last-focused')
@ -342,21 +332,32 @@ def _open_special_pages(args):
needs_warning = (objects.backend == usertypes.Backend.QtWebKit and
not qtutils.is_qtwebkit_ng())
warning_shown = state_config['general'].get('backend-warning-shown') == '1'
warning_shown = general_sect.get('backend-warning-shown') == '1'
if not warning_shown and needs_warning:
tabbed_browser.tabopen(QUrl('qute://backend-warning'),
background=False)
state_config['general']['backend-warning-shown'] = '1'
general_sect['backend-warning-shown'] = '1'
# Quickstart page
quickstart_done = state_config['general'].get('quickstart-done') == '1'
quickstart_done = general_sect.get('quickstart-done') == '1'
if not quickstart_done:
tabbed_browser.tabopen(
QUrl('https://www.qutebrowser.org/quickstart.html'))
state_config['general']['quickstart-done'] = '1'
general_sect['quickstart-done'] = '1'
# Setting migration page
needs_migration = os.path.exists(
os.path.join(standarddir.config(), 'qutebrowser.conf'))
migration_shown = general_sect.get('config-migration-shown') == '1'
if needs_migration and not migration_shown:
tabbed_browser.tabopen(QUrl('qute://help/configuring.html'),
background=False)
general_sect['config-migration-shown'] = '1'
def _save_version():
@ -422,9 +423,6 @@ def _init_modules(args, crash_handler):
config.init(qApp)
save_manager.init_autosave()
log.init.debug("Initializing keys...")
keyconf.init(qApp)
log.init.debug("Initializing sql...")
try:
sql.init(os.path.join(standarddir.data(), 'history.sqlite'))
@ -433,6 +431,9 @@ def _init_modules(args, crash_handler):
pre_text='Error initializing SQL')
sys.exit(usertypes.Exit.err_init)
log.init.debug("Initializing command history...")
cmdhistory.init()
log.init.debug("Initializing web history...")
history.init(qApp)
@ -470,7 +471,7 @@ def _init_modules(args, crash_handler):
objreg.register('cache', diskcache)
log.init.debug("Misc initialization...")
if config.get('ui', 'hide-wayland-decoration'):
if config.val.window.hide_wayland_decoration:
os.environ['QT_WAYLAND_DISABLE_WINDOWDECORATION'] = '1'
else:
os.environ.pop('QT_WAYLAND_DISABLE_WINDOWDECORATION', None)
@ -640,8 +641,25 @@ class Quitter:
else:
return True
@cmdutils.register(instance='quitter', name=['quit', 'q'],
ignore_args=True)
@cmdutils.register(instance='quitter', name='quit')
@cmdutils.argument('session', completion=miscmodels.session)
def quit(self, save=False, session=None):
"""Quit qutebrowser.
Args:
save: When given, save the open windows even if auto_save.session
is turned off.
session: The name of the session to save.
"""
if session is not None and not save:
raise cmdexc.CommandError("Session name given without --save!")
if save:
if session is None:
session = sessions.default
self.shutdown(session=session)
else:
self.shutdown()
def shutdown(self, status=0, session=None, last_window=False,
restart=False):
"""Quit qutebrowser.
@ -663,7 +681,7 @@ class Quitter:
if session is not None:
session_manager.save(session, last_window=last_window,
load_next_time=True)
elif config.get('general', 'save-session'):
elif config.val.auto_save.session:
session_manager.save(sessions.default, last_window=last_window,
load_next_time=True)
@ -742,16 +760,6 @@ class Quitter:
# segfaults.
QTimer.singleShot(0, functools.partial(qApp.exit, status))
@cmdutils.register(instance='quitter', name='wq')
@cmdutils.argument('name', completion=miscmodels.session)
def save_and_quit(self, name=sessions.default):
"""Save open pages and quit.
Args:
name: The name of the session.
"""
self.shutdown(session=name)
class Application(QApplication):

View File

@ -67,11 +67,7 @@ def is_whitelisted_host(host):
Args:
host: The host of the request as string.
"""
whitelist = config.get('content', 'host-blocking-whitelist')
if whitelist is None:
return False
for pattern in whitelist:
for pattern in config.val.content.host_blocking.whitelist:
if fnmatch.fnmatch(host, pattern.lower()):
return True
return False
@ -114,16 +110,16 @@ class HostBlocker:
data_dir = standarddir.data()
self._local_hosts_file = os.path.join(data_dir, 'blocked-hosts')
self.on_config_changed()
self._update_files()
config_dir = standarddir.config()
self._config_hosts_file = os.path.join(config_dir, 'blocked-hosts')
objreg.get('config').changed.connect(self.on_config_changed)
config.instance.changed.connect(self._update_files)
def is_blocked(self, url):
"""Check if the given URL (as QUrl) is blocked."""
if not config.get('content', 'host-blocking-enabled'):
if not config.val.content.host_blocking.enabled:
return False
host = url.host()
return ((host in self._blocked_hosts or
@ -164,9 +160,9 @@ class HostBlocker:
if not found:
args = objreg.get('args')
if (config.get('content', 'host-block-lists') is not None and
if (config.val.content.host_blocking.lists and
args.basedir is None and
config.get('content', 'host-blocking-enabled')):
config.val.content.host_blocking.enabled):
message.info("Run :adblock-update to get adblock lists.")
@cmdutils.register(instance='host-blocker')
@ -180,18 +176,16 @@ class HostBlocker:
self._config_blocked_hosts)
self._blocked_hosts = set()
self._done_count = 0
urls = config.get('content', 'host-block-lists')
download_manager = objreg.get('qtnetwork-download-manager',
scope='window', window='last-focused')
if urls is None:
return
for url in urls:
for url in config.val.content.host_blocking.lists:
if url.scheme() == 'file':
filename = url.toLocalFile()
try:
fileobj = open(url.path(), 'rb')
fileobj = open(filename, 'rb')
except OSError as e:
message.error("adblock: Error while reading {}: {}".format(
url.path(), e.strerror))
filename, e.strerror))
continue
download = FakeDownload(fileobj)
self._in_progress.append(download)
@ -292,11 +286,10 @@ class HostBlocker:
message.info("adblock: Read {} hosts from {} sources.".format(
len(self._blocked_hosts), self._done_count))
@config.change_filter('content', 'host-block-lists')
def on_config_changed(self):
@config.change_filter('content.host_blocking.lists')
def _update_files(self):
"""Update files when the config changed."""
urls = config.get('content', 'host-block-lists')
if urls is None:
if not config.val.content.host_blocking.lists:
try:
os.remove(self._local_hosts_file)
except FileNotFoundError:

View File

@ -61,9 +61,6 @@ def init():
if objects.backend == usertypes.Backend.QtWebEngine:
from qutebrowser.browser.webengine import webenginetab
webenginetab.init()
else:
from qutebrowser.browser.webkit import webkittab
webkittab.init()
class WebTabError(Exception):
@ -188,13 +185,28 @@ class AbstractSearch(QObject):
self.text = None
self.search_displayed = False
def search(self, text, *, ignore_case=False, reverse=False,
def _is_case_sensitive(self, ignore_case):
"""Check if case-sensitivity should be used.
This assumes self.text is already set properly.
Arguments:
ignore_case: The ignore_case value from the config.
"""
mapping = {
'smart': not self.text.islower(),
'never': True,
'always': False,
}
return mapping[ignore_case]
def search(self, text, *, ignore_case='never', reverse=False,
result_cb=None):
"""Find the given text on the page.
Args:
text: The text to search for.
ignore_case: Search case-insensitively. (True/False/'smart')
ignore_case: Search case-insensitively. ('always'/'never/'smart')
reverse: Reverse search direction.
result_cb: Called with a bool indicating whether a match was found.
"""
@ -236,7 +248,7 @@ class AbstractZoom(QObject):
self._win_id = win_id
self._default_zoom_changed = False
self._init_neighborlist()
objreg.get('config').changed.connect(self._on_config_changed)
config.instance.changed.connect(self._on_config_changed)
# # FIXME:qtwebengine is this needed?
# # For some reason, this signal doesn't get disconnected automatically
@ -245,21 +257,21 @@ class AbstractZoom(QObject):
# self.destroyed.connect(functools.partial(
# cfg.changed.disconnect, self.init_neighborlist))
@pyqtSlot(str, str)
def _on_config_changed(self, section, option):
if section == 'ui' and option in ['zoom-levels', 'default-zoom']:
@pyqtSlot(str)
def _on_config_changed(self, option):
if option in ['zoom.levels', 'zoom.default']:
if not self._default_zoom_changed:
factor = float(config.get('ui', 'default-zoom')) / 100
factor = float(config.val.zoom.default) / 100
self._set_factor_internal(factor)
self._default_zoom_changed = False
self._init_neighborlist()
def _init_neighborlist(self):
"""Initialize self._neighborlist."""
levels = config.get('ui', 'zoom-levels')
levels = config.val.zoom.levels
self._neighborlist = usertypes.NeighborList(
levels, mode=usertypes.NeighborList.Modes.edge)
self._neighborlist.fuzzyval = config.get('ui', 'default-zoom')
self._neighborlist.fuzzyval = config.val.zoom.default
def offset(self, offset):
"""Increase/Decrease the zoom level by the given offset.
@ -295,8 +307,7 @@ class AbstractZoom(QObject):
raise NotImplementedError
def set_default(self):
default_zoom = config.get('ui', 'default-zoom')
self._set_factor_internal(float(default_zoom) / 100)
self._set_factor_internal(float(config.val.zoom.default) / 100)
class AbstractCaret(QObject):
@ -705,8 +716,8 @@ class AbstractTab(QWidget):
self.load_started.emit()
def _handle_auto_insert_mode(self, ok):
"""Handle auto-insert-mode after loading finished."""
if not config.get('input', 'auto-insert-mode') or not ok:
"""Handle `input.insert_mode.auto_load` after loading finished."""
if not config.val.input.insert_mode.auto_load or not ok:
return
cur_mode = self._mode_manager.mode

View File

@ -34,7 +34,7 @@ import pygments.lexers
import pygments.formatters
from qutebrowser.commands import userscripts, cmdexc, cmdutils, runners
from qutebrowser.config import config, configexc
from qutebrowser.config import config, configdata
from qutebrowser.browser import (urlmarks, browsertab, inspector, navigate,
webelem, downloads)
from qutebrowser.keyinput import modeman
@ -111,7 +111,7 @@ class CommandDispatcher:
return widget
def _open(self, url, tab=False, background=False, window=False,
explicit=True, private=None):
related=False, private=None):
"""Helper function to open a page.
Args:
@ -132,9 +132,9 @@ class CommandDispatcher:
tabbed_browser = self._new_tabbed_browser(private)
tabbed_browser.tabopen(url)
elif tab:
tabbed_browser.tabopen(url, background=False, explicit=explicit)
tabbed_browser.tabopen(url, background=False, related=related)
elif background:
tabbed_browser.tabopen(url, background=True, explicit=explicit)
tabbed_browser.tabopen(url, background=True, related=related)
else:
widget = self._current_widget()
widget.openurl(url)
@ -179,7 +179,7 @@ class CommandDispatcher:
prev: Force selecting the tab before the current tab.
next_: Force selecting the tab after the current tab.
opposite: Force selecting the tab in the opposite direction of
what's configured in 'tabs->select-on-remove'.
what's configured in 'tabs.select_on_remove'.
Return:
QTabBar.SelectLeftTab, QTabBar.SelectRightTab, or None if no change
@ -191,17 +191,17 @@ class CommandDispatcher:
elif next_:
return QTabBar.SelectRightTab
elif opposite:
conf_selection = config.get('tabs', 'select-on-remove')
conf_selection = config.val.tabs.select_on_remove
if conf_selection == QTabBar.SelectLeftTab:
return QTabBar.SelectRightTab
elif conf_selection == QTabBar.SelectRightTab:
return QTabBar.SelectLeftTab
elif conf_selection == QTabBar.SelectPreviousTab:
raise cmdexc.CommandError(
"-o is not supported with 'tabs->select-on-remove' set to "
"-o is not supported with 'tabs.select_on_remove' set to "
"'last-used'!")
else: # pragma: no cover
raise ValueError("Invalid select-on-remove value "
raise ValueError("Invalid select_on_remove value "
"{!r}!".format(conf_selection))
return None
@ -213,7 +213,7 @@ class CommandDispatcher:
prev: Force selecting the tab before the current tab.
next_: Force selecting the tab after the current tab.
opposite: Force selecting the tab in the opposite direction of
what's configured in 'tabs->select-on-remove'.
what's configured in 'tabs.select_on_remove'.
count: The tab index to close, or None
"""
tabbar = self._tabbed_browser.tabBar()
@ -238,7 +238,7 @@ class CommandDispatcher:
prev: Force selecting the tab before the current tab.
next_: Force selecting the tab after the current tab.
opposite: Force selecting the tab in the opposite direction of
what's configured in 'tabs->select-on-remove'.
what's configured in 'tabs.select_on_remove'.
force: Avoid confirmation for pinned tabs.
count: The tab index to close, or None
"""
@ -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->pinned-width size.
Pinning a tab shrinks it to `tabs.width.pinned` size.
Attempting to close a pinned tab will cause a confirmation,
unless --force is passed.
@ -274,7 +274,7 @@ class CommandDispatcher:
maxsplit=0, scope='window')
@cmdutils.argument('url', completion=urlmodel.url)
@cmdutils.argument('count', count=True)
def openurl(self, url=None, implicit=False,
def openurl(self, url=None, related=False,
bg=False, tab=False, window=False, count=None, secure=False,
private=False):
"""Open a URL in the current/[count]th tab.
@ -286,14 +286,14 @@ class CommandDispatcher:
bg: Open in a new background tab.
tab: Open in a new tab.
window: Open in a new window.
implicit: If opening a new tab, treat the tab as implicit (like
clicking on a link).
related: If opening a new tab, position the tab as related to the
current one (like clicking on a link).
count: The tab index to open the URL in, or None.
secure: Force HTTPS.
private: Open a new window in private browsing mode.
"""
if url is None:
urls = [config.get('general', 'default-page')]
urls = [config.val.url.default_page]
else:
urls = self._parse_url_input(url)
@ -305,7 +305,7 @@ class CommandDispatcher:
bg = True
if tab or bg or window or private:
self._open(cur_url, tab, bg, window, explicit=not implicit,
self._open(cur_url, tab, bg, window, related=related,
private=private)
else:
curtab = self._cntwidget(count)
@ -490,7 +490,7 @@ class CommandDispatcher:
raise cmdexc.CommandError(e)
# The new tab could be in a new tabbed_browser (e.g. because of
# tabs-are-windows being set)
# tabs.tabs_are_windows being set)
if window:
new_tabbed_browser = self._new_tabbed_browser(
private=self._tabbed_browser.private)
@ -502,9 +502,9 @@ class CommandDispatcher:
idx = new_tabbed_browser.indexOf(newtab)
new_tabbed_browser.set_page_title(idx, cur_title)
if config.get('tabs', 'show-favicons'):
if config.val.tabs.favicons.show:
new_tabbed_browser.setTabIcon(idx, curtab.icon())
if config.get('tabs', 'tabs-are-windows'):
if config.val.tabs.tabs_are_windows:
new_tabbed_browser.window().setWindowIcon(curtab.icon())
newtab.data.keep_icon = True
@ -622,7 +622,7 @@ class CommandDispatcher:
tab=tab, background=bg, window=window)
elif where in ['up', 'increment', 'decrement']:
new_url = handlers[where](url, count)
self._open(new_url, tab, bg, window, explicit=False)
self._open(new_url, tab, bg, window, related=True)
else: # pragma: no cover
raise ValueError("Got called with invalid value {} for "
"`where'.".format(where))
@ -771,7 +771,7 @@ class CommandDispatcher:
url_query.setQueryDelimiters('=', ';')
url_query.setQuery(url_query_str)
for key in dict(url_query.queryItems()):
if key in config.get('general', 'yank-ignored-url-parameters'):
if key in config.val.url.yank_ignored_parameters:
url_query.removeQueryItem(key)
url.setQuery(url_query)
return url.toString(flags)
@ -842,7 +842,7 @@ class CommandDispatcher:
perc = tab.zoom.offset(count)
except ValueError as e:
raise cmdexc.CommandError(e)
message.info("Zoom level: {}%".format(perc), replace=True)
message.info("Zoom level: {}%".format(int(perc)), replace=True)
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('count', count=True)
@ -857,7 +857,7 @@ class CommandDispatcher:
perc = tab.zoom.offset(-count)
except ValueError as e:
raise cmdexc.CommandError(e)
message.info("Zoom level: {}%".format(perc), replace=True)
message.info("Zoom level: {}%".format(int(perc)), replace=True)
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('count', count=True)
@ -881,14 +881,14 @@ class CommandDispatcher:
level = count if count is not None else zoom
if level is None:
level = config.get('ui', 'default-zoom')
level = config.val.zoom.default
tab = self._current_widget()
try:
tab.zoom.set_factor(float(level) / 100)
except ValueError:
raise cmdexc.CommandError("Can't zoom {}%!".format(level))
message.info("Zoom level: {}%".format(level), replace=True)
message.info("Zoom level: {}%".format(int(level)), replace=True)
@cmdutils.register(instance='command-dispatcher', scope='window')
def tab_only(self, prev=False, next_=False, force=False):
@ -947,7 +947,7 @@ class CommandDispatcher:
newidx = self._current_index() - count
if newidx >= 0:
self._set_current_index(newidx)
elif config.get('tabs', 'wrap'):
elif config.val.tabs.wrap:
self._set_current_index(newidx % self._count())
else:
raise cmdexc.CommandError("First tab")
@ -967,7 +967,7 @@ class CommandDispatcher:
newidx = self._current_index() + count
if newidx < self._count():
self._set_current_index(newidx)
elif config.get('tabs', 'wrap'):
elif config.val.tabs.wrap:
self._set_current_index(newidx % self._count())
else:
raise cmdexc.CommandError("Last tab")
@ -1124,7 +1124,7 @@ class CommandDispatcher:
elif index == '+': # pragma: no branch
new_idx += delta
if config.get('tabs', 'wrap'):
if config.val.tabs.wrap:
new_idx %= self._count()
else:
# absolute moving
@ -1186,7 +1186,7 @@ class CommandDispatcher:
@cmdutils.register(instance='command-dispatcher', scope='window')
def home(self):
"""Open main startpage in current tab."""
self.openurl(config.get('general', 'startpage')[0])
self._current_widget().openurl(config.val.url.start_pages[0])
def _run_userscript(self, cmd, *args, verbose=False):
"""Run a userscript given as argument.
@ -1532,7 +1532,7 @@ class CommandDispatcher:
topic: The topic to show help for.
- :__command__ for commands.
- __section__\->__option__ for settings.
- __section__.__option__ for settings.
"""
if topic is None:
path = 'index.html'
@ -1542,20 +1542,8 @@ class CommandDispatcher:
raise cmdexc.CommandError("Invalid command {}!".format(
command))
path = 'commands.html#{}'.format(command)
elif '->' in topic:
parts = topic.split('->')
if len(parts) != 2:
raise cmdexc.CommandError("Invalid help topic {}!".format(
topic))
try:
config.get(*parts)
except configexc.NoSectionError:
raise cmdexc.CommandError("Invalid section {}!".format(
parts[0]))
except configexc.NoOptionError:
raise cmdexc.CommandError("Invalid option {}!".format(
parts[1]))
path = 'settings.html#{}'.format(topic.replace('->', '-'))
elif topic in configdata.DATA:
path = 'settings.html#{}'.format(topic)
else:
raise cmdexc.CommandError("Invalid help topic {}!".format(topic))
url = QUrl('qute://help/{}'.format(path))
@ -1608,7 +1596,7 @@ class CommandDispatcher:
"""Open an external editor with the currently selected form field.
The editor which should be launched can be configured via the
`general -> editor` config option.
`editor.command` config option.
"""
tab = self._current_widget()
tab.elements.find_focused(self._open_editor_cb)
@ -1749,7 +1737,7 @@ class CommandDispatcher:
return
options = {
'ignore_case': config.get('general', 'ignore-case'),
'ignore_case': config.val.ignore_case,
'reverse': reverse,
}
@ -2103,7 +2091,7 @@ class CommandDispatcher:
"""Navigate to a url formed in an external editor.
The editor which should be launched can be configured via the
`general -> editor` config option.
`editor.command` config option.
Args:
url: URL to edit; defaults to the current page url.

View File

@ -69,15 +69,22 @@ class UnsupportedOperationError(Exception):
def download_dir():
"""Get the download directory to use."""
directory = config.get('storage', 'download-directory')
remember_dir = config.get('storage', 'remember-download-directory')
directory = config.val.downloads.location.directory
remember_dir = config.val.downloads.location.remember
if remember_dir and last_used_directory is not None:
return last_used_directory
ddir = last_used_directory
elif directory is None:
return standarddir.download()
ddir = standarddir.download()
else:
return directory
ddir = directory
try:
os.makedirs(ddir)
except FileExistsError:
pass
return ddir
def immediate_download_path(prompt_download_directory=None):
@ -88,11 +95,10 @@ def immediate_download_path(prompt_download_directory=None):
Args:
prompt_download_directory: If this is something else than None, it
will overwrite the
storage->prompt-download-directory setting.
downloads.location.prompt setting.
"""
if prompt_download_directory is None:
prompt_download_directory = config.get('storage',
'prompt-download-directory')
prompt_download_directory = config.val.downloads.location.prompt
if not prompt_download_directory:
return download_dir()
@ -104,7 +110,7 @@ def _path_suggestion(filename):
Args:
filename: The filename to use if included in the suggestion.
"""
suggestion = config.get('completion', 'download-path-suggestion')
suggestion = config.val.downloads.location.suggestion
if suggestion == 'path':
# add trailing '/' if not present
return os.path.join(download_dir(), '')
@ -494,13 +500,13 @@ class AbstractDownloadItem(QObject):
Args:
position: The color type requested, can be 'fg' or 'bg'.
"""
# pylint: disable=bad-config-call
# WORKAROUND for https://bitbucket.org/logilab/astroid/issue/104/
assert position in ["fg", "bg"]
start = config.get('colors', 'downloads.{}.start'.format(position))
stop = config.get('colors', 'downloads.{}.stop'.format(position))
system = config.get('colors', 'downloads.{}.system'.format(position))
error = config.get('colors', 'downloads.{}.error'.format(position))
# pylint: disable=bad-config-option
start = getattr(config.val.colors.downloads.start, position)
stop = getattr(config.val.colors.downloads.stop, position)
system = getattr(config.val.colors.downloads.system, position)
error = getattr(config.val.colors.downloads.error, position)
# pylint: enable=bad-config-option
if self.error_msg is not None:
assert not self.successful
return error
@ -572,7 +578,7 @@ class AbstractDownloadItem(QObject):
Args:
cmdline: The command to use as string. A `{}` is expanded to the
filename. None means to use the system's default
application or `default-open-dispatcher` if set. If no
application or `downloads.open_dispatcher` if set. If no
`{}` is found, the filename is appended to the cmdline.
"""
assert self.successful
@ -757,7 +763,7 @@ class AbstractDownloadManager(QObject):
download.remove_requested.connect(functools.partial(
self._remove_item, download))
delay = config.get('ui', 'remove-finished-downloads')
delay = config.val.downloads.remove_finished
if delay > -1:
download.finished.connect(
lambda: QTimer.singleShot(delay, download.remove))

View File

@ -26,7 +26,7 @@ from PyQt5.QtCore import pyqtSlot, QSize, Qt, QTimer
from PyQt5.QtWidgets import QListView, QSizePolicy, QMenu, QStyleFactory
from qutebrowser.browser import downloads
from qutebrowser.config import style
from qutebrowser.config import config
from qutebrowser.utils import qtutils, utils, objreg
@ -64,8 +64,8 @@ class DownloadView(QListView):
STYLESHEET = """
QListView {
background-color: {{ color['downloads.bg.bar'] }};
font: {{ font['downloads'] }};
background-color: {{ conf.colors.downloads.bar.bg }};
font: {{ conf.fonts.downloads }};
}
QListView::item {
@ -76,7 +76,7 @@ class DownloadView(QListView):
def __init__(self, win_id, parent=None):
super().__init__(parent)
self.setStyle(QStyleFactory.create('Fusion'))
style.set_register_stylesheet(self)
config.set_register_stylesheet(self)
self.setResizeMode(QListView.Adjust)
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed)

View File

@ -29,7 +29,7 @@ from string import ascii_lowercase
from PyQt5.QtCore import pyqtSlot, QObject, Qt, QUrl
from PyQt5.QtWidgets import QLabel
from qutebrowser.config import config, style
from qutebrowser.config import config
from qutebrowser.keyinput import modeman, modeparsers
from qutebrowser.browser import webelem
from qutebrowser.commands import userscripts, cmdexc, cmdutils, runners
@ -65,10 +65,10 @@ class HintLabel(QLabel):
STYLESHEET = """
QLabel {
background-color: {{ color['hints.bg'] }};
color: {{ color['hints.fg'] }};
font: {{ font['hints'] }};
border: {{ config.get('hints', 'border') }};
background-color: {{ conf.colors.hints.bg }};
color: {{ conf.colors.hints.fg }};
font: {{ conf.fonts.hints }};
border: {{ conf.hints.border }};
padding-left: -3px;
padding-right: -3px;
}
@ -80,7 +80,7 @@ class HintLabel(QLabel):
self.elem = elem
self.setAttribute(Qt.WA_StyledBackground, True)
style.set_register_stylesheet(self)
config.set_register_stylesheet(self)
self._context.tab.contents_size_changed.connect(self._move_to_elem)
self._move_to_elem()
@ -100,7 +100,7 @@ class HintLabel(QLabel):
matched: The part of the text which was typed.
unmatched: The part of the text which was not typed yet.
"""
if (config.get('hints', 'uppercase') and
if (config.val.hints.uppercase and
self._context.hint_mode in ['letter', 'word']):
matched = html.escape(matched.upper())
unmatched = html.escape(unmatched.upper())
@ -108,7 +108,7 @@ class HintLabel(QLabel):
matched = html.escape(matched)
unmatched = html.escape(unmatched)
match_color = html.escape(config.get('colors', 'hints.fg.match'))
match_color = html.escape(config.val.colors.hints.match.fg)
self.setText('<font color="{}">{}</font>{}'.format(
match_color, matched, unmatched))
self.adjustSize()
@ -121,7 +121,7 @@ class HintLabel(QLabel):
log.hints.debug("Frame for {!r} vanished!".format(self))
self.hide()
return
no_js = config.get('hints', 'find-implementation') != 'javascript'
no_js = config.val.hints.find_implementation != 'javascript'
rect = self.elem.rect_on_view(no_js=no_js)
self.move(rect.x(), rect.y())
@ -203,7 +203,7 @@ class HintActions:
Target.window: usertypes.ClickTarget.window,
Target.hover: usertypes.ClickTarget.normal,
}
if config.get('tabs', 'background-tabs'):
if config.val.tabs.background:
target_mapping[Target.tab] = usertypes.ClickTarget.tab_bg
else:
target_mapping[Target.tab] = usertypes.ClickTarget.tab
@ -421,9 +421,9 @@ class HintManager(QObject):
if hint_mode == 'number':
chars = '0123456789'
else:
chars = config.get('hints', 'chars')
min_chars = config.get('hints', 'min-chars')
if config.get('hints', 'scatter') and hint_mode != 'number':
chars = config.val.hints.chars
min_chars = config.val.hints.min_chars
if config.val.hints.scatter and hint_mode != 'number':
return self._hint_scattered(min_chars, chars, elems)
else:
return self._hint_linear(min_chars, chars, elems)
@ -603,7 +603,7 @@ class HintManager(QObject):
modeman.enter(self._win_id, usertypes.KeyMode.hint,
'HintManager.start')
# to make auto-follow == 'always' work
# to make auto_follow == 'always' work
self._handle_auto_follow()
@cmdutils.register(instance='hintmanager', scope='tab', name='hint',
@ -615,7 +615,7 @@ class HintManager(QObject):
Args:
rapid: Whether to do rapid hinting. This is only possible with
targets `tab` (with background-tabs=true), `tab-bg`,
targets `tab` (with `tabs.background_tabs=true`), `tab-bg`,
`window`, `run`, `hover`, `userscript` and `spawn`.
add_history: Whether to add the spawned or yanked link to the
browsing history.
@ -631,7 +631,7 @@ class HintManager(QObject):
- `normal`: Open the link.
- `current`: Open the link in the current tab.
- `tab`: Open the link in a new tab (honoring the
background-tabs setting).
`tabs.background_tabs` setting).
- `tab-fg`: Open the link in a new foreground tab.
- `tab-bg`: Open the link in a new background tab.
- `window`: Open the link in a new window.
@ -649,7 +649,7 @@ class HintManager(QObject):
mode: The hinting mode to use.
- `number`: Use numeric hints.
- `letter`: Use the chars in the hints->chars settings.
- `letter`: Use the chars in the hints.chars setting.
- `word`: Use hint words based on the html elements and the
extra words.
@ -684,8 +684,7 @@ class HintManager(QObject):
Target.hover, Target.userscript, Target.spawn,
Target.download, Target.normal, Target.current]:
pass
elif (target == Target.tab and
config.get('tabs', 'background-tabs')):
elif target == Target.tab and config.val.tabs.background:
pass
else:
name = target.name.replace('_', '-')
@ -693,7 +692,7 @@ class HintManager(QObject):
"target {}!".format(name))
if mode is None:
mode = config.get('hints', 'mode')
mode = config.val.hints.mode
self._check_args(target, *args)
self._context = HintContext()
@ -720,7 +719,7 @@ class HintManager(QObject):
return self._context.hint_mode
def _handle_auto_follow(self, keystr="", filterstr="", visible=None):
"""Handle the auto-follow option."""
"""Handle the auto_follow option."""
if visible is None:
visible = {string: label
for string, label in self._context.labels.items()
@ -729,7 +728,7 @@ class HintManager(QObject):
if len(visible) != 1:
return
auto_follow = config.get('hints', 'auto-follow')
auto_follow = config.val.hints.auto_follow
if auto_follow == "always":
follow = True
@ -746,8 +745,8 @@ class HintManager(QObject):
self._context.to_follow = list(visible.keys())[0]
if follow:
# apply auto-follow-timeout
timeout = config.get('hints', 'auto-follow-timeout')
# apply auto_follow_timeout
timeout = config.val.hints.auto_follow_timeout
keyparsers = objreg.get('keyparsers', scope='window',
window=self._win_id)
normal_parser = keyparsers[usertypes.KeyMode.normal]
@ -771,9 +770,9 @@ class HintManager(QObject):
label.show()
else:
# element doesn't match anymore -> hide it, unless in rapid
# mode and hide-unmatched-rapid-hints is false (see #1799)
# mode and hide_unmatched_rapid_hints is false (see #1799)
if (not self._context.rapid or
config.get('hints', 'hide-unmatched-rapid-hints')):
config.val.hints.hide_unmatched_rapid_hints):
label.hide()
except webelem.Error:
pass
@ -793,6 +792,8 @@ class HintManager(QObject):
else:
self._context.filterstr = filterstr
log.hints.debug("Filtering hints on {!r}".format(filterstr))
visible = []
for label in self._context.all_labels:
try:
@ -938,7 +939,7 @@ class WordHinter:
def ensure_initialized(self):
"""Generate the used words if yet uninitialized."""
dictionary = config.get("hints", "dictionary")
dictionary = config.val.hints.dictionary
if not self.words or self.dictionary != dictionary:
self.words.clear()
self.dictionary = dictionary

View File

@ -85,7 +85,7 @@ class MouseEventFilter(QObject):
def _handle_mouse_press(self, e):
"""Handle pressing of a mouse button."""
is_rocker_gesture = (config.get('input', 'rocker-gestures') and
is_rocker_gesture = (config.val.input.rocker_gestures and
e.buttons() == Qt.LeftButton | Qt.RightButton)
if e.button() in [Qt.XButton1, Qt.XButton2] or is_rocker_gesture:
@ -119,7 +119,7 @@ class MouseEventFilter(QObject):
return True
if e.modifiers() & Qt.ControlModifier:
divider = config.get('input', 'mouse-zoom-divider')
divider = config.val.zoom.mouse_divider
if divider == 0:
return False
factor = self._tab.zoom.factor() + (e.angleDelta().y() / divider)
@ -139,7 +139,7 @@ class MouseEventFilter(QObject):
def _handle_context_menu(self, _e):
"""Suppress context menus if rocker gestures are turned on."""
return config.get('input', 'rocker-gestures')
return config.val.input.rocker_gestures
def _mousepress_insertmode_cb(self, elem):
"""Check if the clicked element is editable."""
@ -157,7 +157,7 @@ class MouseEventFilter(QObject):
'click', only_if_normal=True)
else:
log.mouse.debug("Clicked non-editable element!")
if config.get('input', 'auto-leave-insert-mode'):
if config.val.input.insert_mode.auto_leave:
modeman.leave(self._tab.win_id, usertypes.KeyMode.insert,
'click', maybe=True)
@ -179,7 +179,7 @@ class MouseEventFilter(QObject):
'click-delayed', only_if_normal=True)
else:
log.mouse.debug("Clicked non-editable element (delayed)!")
if config.get('input', 'auto-leave-insert-mode'):
if config.val.input.insert_mode.auto_leave:
modeman.leave(self._tab.win_id, usertypes.KeyMode.insert,
'click-delayed', maybe=True)

View File

@ -42,7 +42,7 @@ def incdec(url, count, inc_or_dec):
background: Open the link in a new background tab.
window: Open the link in a new window.
"""
segments = set(config.get('general', 'url-incdec-segments'))
segments = set(config.val.url.incdec_segments)
try:
new_url = urlutils.incdec_number(url, inc_or_dec, count,
segments=segments)
@ -80,10 +80,13 @@ def _find_prevnext(prev, elems):
# Then check for regular links/buttons.
elems = [e for e in elems if e.tag_name() != 'link']
option = 'prev-regexes' if prev else 'next-regexes'
option = 'prev_regexes' if prev else 'next_regexes'
if not elems:
return None
for regex in config.get('hints', option):
# pylint: disable=bad-config-option
for regex in getattr(config.val.hints, option):
# pylint: enable=bad-config-option
log.hints.vdebug("== Checking regex '{}'.".format(regex.pattern))
for e in elems:
text = str(e)

View File

@ -247,10 +247,21 @@ class PACFetcher(QObject):
self._pac_url = url
self._manager = QNetworkAccessManager()
self._manager.setProxy(QNetworkProxy(QNetworkProxy.NoProxy))
self._reply = self._manager.get(QNetworkRequest(url))
self._reply.finished.connect(self._finish)
self._pac = None
self._error_message = None
self._reply = None
def __eq__(self, other):
# pylint: disable=protected-access
return self._pac_url == other._pac_url
def __repr__(self):
return utils.get_repr(self, url=self._pac_url, constructor=True)
def fetch(self):
"""Fetch the proxy from the remote URL."""
self._reply = self._manager.get(QNetworkRequest(self._pac_url))
self._reply.finished.connect(self._finish)
@pyqtSlot()
def _finish(self):

View File

@ -44,7 +44,7 @@ class ProxyFactory(QNetworkProxyFactory):
Return:
None if proxy is correct, otherwise an error message.
"""
proxy = config.get('network', 'proxy')
proxy = config.val.content.proxy
if isinstance(proxy, pac.PACFetcher):
return proxy.fetch_error()
else:
@ -59,7 +59,7 @@ class ProxyFactory(QNetworkProxyFactory):
Return:
A list of QNetworkProxy objects in order of preference.
"""
proxy = config.get('network', 'proxy')
proxy = config.val.content.proxy
if proxy is configtypes.SYSTEM_PROXY:
proxies = QNetworkProxyFactory.systemProxyForQuery(query)
elif isinstance(proxy, pac.PACFetcher):
@ -69,7 +69,7 @@ class ProxyFactory(QNetworkProxyFactory):
for p in proxies:
if p.type() != QNetworkProxy.NoProxy:
capabilities = p.capabilities()
if config.get('network', 'proxy-dns-requests'):
if config.val.content.proxy_dns_requests:
capabilities |= QNetworkProxy.HostNameLookupCapability
else:
capabilities &= ~QNetworkProxy.HostNameLookupCapability

View File

@ -368,7 +368,7 @@ class DownloadManager(downloads.AbstractDownloadManager):
super().__init__(parent)
self._networkmanager = networkmanager.NetworkManager(
win_id=win_id, tab_id=None,
private=config.get('general', 'private-browsing'), parent=self)
private=config.val.content.private_browsing, parent=self)
@pyqtSlot('QUrl')
def get(self, url, *, user_agent=None, **kwargs):
@ -483,7 +483,7 @@ class DownloadManager(downloads.AbstractDownloadManager):
reply: The QNetworkReply to download.
target: Where to save the download as downloads.DownloadTarget.
auto_remove: Whether to remove the download even if
ui -> remove-finished-downloads is set to -1.
downloads.remove_finished is set to -1.
Return:
The created DownloadItem.

View File

@ -29,12 +29,13 @@ import os
import time
import urllib.parse
import datetime
import textwrap
import pkg_resources
from PyQt5.QtCore import QUrlQuery, QUrl
import qutebrowser
from qutebrowser.config import config
from qutebrowser.config import config, configdata, configexc, configdiff
from qutebrowser.utils import (version, utils, jinja, log, message, docutils,
objreg, usertypes, qtutils)
from qutebrowser.misc import objects
@ -122,8 +123,7 @@ class add_handler: # pylint: disable=invalid-name
title="Error while opening qute://url",
url=url.toDisplayString(),
error='{} is not available with this '
'backend'.format(url.toDisplayString()),
icon='')
'backend'.format(url.toDisplayString()))
return 'text/html', html
@ -225,14 +225,14 @@ def qute_history(url):
return 'text/html', json.dumps(history_data(start_time, offset))
else:
if (
config.get('content', 'allow-javascript') and
config.val.content.javascript.enabled and
(objects.backend == usertypes.Backend.QtWebEngine or
qtutils.is_qtwebkit_ng())
):
return 'text/html', jinja.render(
'history.html',
title='History',
session_interval=config.get('ui', 'history-session-interval')
gap_interval=config.val.history_gap_interval
)
else:
# Get current date from query parameter, if not given choose today.
@ -351,20 +351,6 @@ def qute_gpl(_url):
@add_handler('help')
def qute_help(url):
"""Handler for qute://help."""
try:
utils.read_file('html/doc/index.html')
except OSError:
html = jinja.render(
'error.html',
title="Error while loading documentation",
url=url.toDisplayString(),
error="This most likely means the documentation was not generated "
"properly. If you are running qutebrowser from the git "
"repository, please run scripts/asciidoc2html.py. "
"If you're running a released version this is a bug, please "
"use :report to report it.",
icon='')
return 'text/html', html
urlpath = url.path()
if not urlpath or urlpath == '/':
urlpath = 'index.html'
@ -373,11 +359,45 @@ def qute_help(url):
if not docutils.docs_up_to_date(urlpath):
message.error("Your documentation is outdated! Please re-run "
"scripts/asciidoc2html.py.")
path = 'html/doc/{}'.format(urlpath)
if urlpath.endswith('.png'):
return 'image/png', utils.read_file(path, binary=True)
else:
try:
data = utils.read_file(path)
except OSError:
# No .html around, let's see if we find the asciidoc
asciidoc_path = path.replace('.html', '.asciidoc')
if asciidoc_path.startswith('html/doc/'):
asciidoc_path = asciidoc_path.replace('html/doc/', '../doc/help/')
try:
asciidoc = utils.read_file(asciidoc_path)
except OSError:
asciidoc = None
if asciidoc is None:
raise
preamble = textwrap.dedent("""
There was an error loading the documentation!
This most likely means the documentation was not generated
properly. If you are running qutebrowser from the git repository,
please (re)run scripts/asciidoc2html.py and reload this page.
If you're running a released version this is a bug, please use
:report to report it.
Falling back to the plaintext version.
---------------------------------------------------------------
""")
return 'text/plain', (preamble + asciidoc).encode('utf-8')
else:
return 'text/html', data
@ -390,3 +410,47 @@ def qute_backend_warning(_url):
version=pkg_resources.parse_version,
title="Legacy backend warning")
return 'text/html', html
def _qute_settings_set(url):
"""Handler for qute://settings/set."""
query = QUrlQuery(url)
option = query.queryItemValue('option', QUrl.FullyDecoded)
value = query.queryItemValue('value', QUrl.FullyDecoded)
# https://github.com/qutebrowser/qutebrowser/issues/727
if option == 'content.javascript.enabled' and value == 'false':
msg = ("Refusing to disable javascript via qute://settings "
"as it needs javascript support.")
message.error(msg)
return 'text/html', b'error: ' + msg.encode('utf-8')
try:
config.instance.set_str(option, value, save_yaml=True)
return 'text/html', b'ok'
except configexc.Error as e:
message.error(str(e))
return 'text/html', b'error: ' + str(e).encode('utf-8')
@add_handler('settings')
def qute_settings(url):
"""Handler for qute://settings. View/change qute configuration."""
if url.path() == '/set':
return _qute_settings_set(url)
html = jinja.render('settings.html', title='settings',
configdata=configdata,
confget=config.instance.get_str)
return 'text/html', html
@add_handler('configdiff')
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

View File

@ -21,10 +21,8 @@
import html
import jinja2
from qutebrowser.config import config
from qutebrowser.utils import usertypes, message, log, objreg
from qutebrowser.utils import usertypes, message, log, objreg, jinja
class CallSuper(Exception):
@ -35,16 +33,18 @@ class CallSuper(Exception):
def custom_headers():
"""Get the combined custom headers."""
headers = {}
dnt = b'1' if config.get('network', 'do-not-track') else b'0'
headers[b'DNT'] = dnt
headers[b'X-Do-Not-Track'] = dnt
config_headers = config.get('network', 'custom-headers')
if config_headers is not None:
for header, value in config_headers.items():
headers[header.encode('ascii')] = value.encode('ascii')
dnt_config = config.val.content.headers.do_not_track
if dnt_config is not None:
dnt = b'1' if dnt_config else b'0'
headers[b'DNT'] = dnt
headers[b'X-Do-Not-Track'] = dnt
accept_language = config.get('network', 'accept-language')
conf_headers = config.val.content.headers.custom
for header, value in conf_headers.items():
headers[header.encode('ascii')] = value.encode('ascii')
accept_language = config.val.content.headers.accept_language
if accept_language is not None:
headers[b'Accept-Language'] = accept_language.encode('ascii')
@ -72,7 +72,7 @@ def authentication_required(url, authenticator, abort_on):
def javascript_confirm(url, js_msg, abort_on):
"""Display a javascript confirm prompt."""
log.js.debug("confirm: {}".format(js_msg))
if config.get('ui', 'modal-js-dialog'):
if config.val.content.javascript.modal_dialog:
raise CallSuper
msg = 'From <b>{}</b>:<br/>{}'.format(html.escape(url.toDisplayString()),
@ -86,9 +86,9 @@ def javascript_confirm(url, js_msg, abort_on):
def javascript_prompt(url, js_msg, default, abort_on):
"""Display a javascript prompt."""
log.js.debug("prompt: {}".format(js_msg))
if config.get('ui', 'modal-js-dialog'):
if config.val.content.javascript.modal_dialog:
raise CallSuper
if config.get('content', 'ignore-javascript-prompt'):
if not config.val.content.javascript.prompt:
return (False, "")
msg = '<b>{}</b> asks:<br/>{}'.format(html.escape(url.toDisplayString()),
@ -107,10 +107,10 @@ def javascript_prompt(url, js_msg, default, abort_on):
def javascript_alert(url, js_msg, abort_on):
"""Display a javascript alert."""
log.js.debug("alert: {}".format(js_msg))
if config.get('ui', 'modal-js-dialog'):
if config.val.content.javascript.modal_dialog:
raise CallSuper
if config.get('content', 'ignore-javascript-alert'):
if not config.val.content.javascript.alert:
return
msg = 'From <b>{}</b>:<br/>{}'.format(html.escape(url.toDisplayString()),
@ -119,6 +119,22 @@ def javascript_alert(url, js_msg, abort_on):
abort_on=abort_on)
def javascript_log_message(level, source, line, msg):
"""Display a JavaScript log message."""
logstring = "[{}:{}] {}".format(source, line, msg)
# Needs to line up with the values allowed for the
# content.javascript.log setting.
logmap = {
'none': lambda arg: None,
'debug': log.js.debug,
'info': log.js.info,
'warning': log.js.warning,
'error': log.js.error,
}
logger = logmap[config.val.content.javascript.log[level.name]]
logger(logstring)
def ignore_certificate_errors(url, errors, abort_on):
"""Display a certificate error question.
@ -129,7 +145,7 @@ def ignore_certificate_errors(url, errors, abort_on):
Return:
True if the error should be ignored, False otherwise.
"""
ssl_strict = config.get('network', 'ssl-strict')
ssl_strict = config.val.content.ssl_strict
log.webview.debug("Certificate errors {!r}, strict {}".format(
errors, ssl_strict))
@ -137,7 +153,7 @@ def ignore_certificate_errors(url, errors, abort_on):
assert error.is_overridable(), repr(error)
if ssl_strict == 'ask':
err_template = jinja2.Template("""
err_template = jinja.environment.from_string("""
Errors while loading <b>{{url.toDisplayString()}}</b>:<br/>
<ul>
{% for err in errors %}
@ -155,7 +171,7 @@ def ignore_certificate_errors(url, errors, abort_on):
ignore = False
return ignore
elif ssl_strict is False:
log.webview.debug("ssl-strict is False, only warning about errors")
log.webview.debug("ssl_strict is False, only warning about errors")
for err in errors:
# FIXME we might want to use warn here (non-fatal error)
# https://github.com/qutebrowser/qutebrowser/issues/114
@ -173,7 +189,7 @@ def feature_permission(url, option, msg, yes_action, no_action, abort_on):
Args:
url: The URL the request was done for.
option: A (section, option) tuple for the option to check.
option: An option name to check.
msg: A string like "show notifications"
yes_action: A callable to call if the request was approved
no_action: A callable to call if the request was denied
@ -182,7 +198,7 @@ def feature_permission(url, option, msg, yes_action, no_action, abort_on):
Return:
The Question object if a question was asked, None otherwise.
"""
config_val = config.get(*option)
config_val = config.instance.get(option)
if config_val == 'ask':
if url.isValid():
text = "Allow the website at <b>{}</b> to {}?".format(
@ -233,15 +249,14 @@ def get_tab(win_id, target):
def get_user_stylesheet():
"""Get the combined user-stylesheet."""
filename = config.get('ui', 'user-stylesheet')
css = ''
stylesheets = config.val.content.user_stylesheets
if filename is None:
css = ''
else:
for filename in stylesheets:
with open(filename, 'r', encoding='utf-8') as f:
css = f.read()
css += f.read()
if config.get('ui', 'hide-scrollbar'):
if not config.val.scrolling.bar:
css += '\nhtml > ::-webkit-scrollbar { width: 0px; height: 0px; }'
return css

View File

@ -182,7 +182,7 @@ class AbstractWebElement(collections.abc.MutableMapping):
# at least a classid attribute. Oh, and let's hope images/...
# DON'T have a classid attribute. HTML sucks.
log.webelem.debug("<object type='{}'> clicked.".format(objtype))
return config.get('input', 'insert-mode-on-plugins')
return config.val.input.insert_mode.plugins
else:
# Image/Audio/...
return False
@ -247,7 +247,7 @@ class AbstractWebElement(collections.abc.MutableMapping):
return self.is_writable()
elif tag in ['embed', 'applet']:
# Flash/Java/...
return config.get('input', 'insert-mode-on-plugins') and not strict
return config.val.input.insert_mode.plugins and not strict
elif tag == 'object':
return self._is_editable_object() and not strict
elif tag in ['div', 'pre']:
@ -329,7 +329,7 @@ class AbstractWebElement(collections.abc.MutableMapping):
usertypes.ClickTarget.tab: Qt.ControlModifier,
usertypes.ClickTarget.tab_bg: Qt.ControlModifier,
}
if config.get('tabs', 'background-tabs'):
if config.val.tabs.background:
modifiers[usertypes.ClickTarget.tab] |= Qt.ShiftModifier
else:
modifiers[usertypes.ClickTarget.tab_bg] |= Qt.ShiftModifier

View File

@ -63,6 +63,6 @@ class RequestInterceptor(QWebEngineUrlRequestInterceptor):
for header, value in shared.custom_headers():
info.setHttpHeader(header, value)
user_agent = config.get('network', 'user-agent')
user_agent = config.val.content.headers.user_agent
if user_agent is not None:
info.setHttpHeader(b'User-Agent', user_agent.encode('ascii'))

View File

@ -162,7 +162,7 @@ class WebEngineElement(webelem.AbstractWebElement):
top = rect['top']
if width > 1 and height > 1:
# Fix coordinates according to zoom level
# We're not checking for zoom-text-only here as that doesn't
# We're not checking for zoom.text_only here as that doesn't
# exist for QtWebEngine.
zoom = self._tab.zoom.factor()
rect = QRect(left * zoom, top * zoom,

View File

@ -38,7 +38,7 @@ from PyQt5.QtWebEngineWidgets import (QWebEngineSettings, QWebEngineProfile,
from qutebrowser.browser import shared
from qutebrowser.config import config, websettings
from qutebrowser.utils import objreg, utils, standarddir, javascript, qtutils
from qutebrowser.utils import utils, standarddir, javascript, qtutils
# The default QWebEngineProfile
@ -112,7 +112,7 @@ class DefaultProfileSetter(websettings.Base):
class PersistentCookiePolicy(DefaultProfileSetter):
"""The cookies -> store setting is different from other settings."""
"""The content.cookies.store setting is different from other settings."""
def __init__(self):
super().__init__('setPersistentCookiesPolicy')
@ -158,26 +158,29 @@ def _init_stylesheet(profile):
profile.scripts().insert(script)
def _set_user_agent(profile):
"""Set the user agent for the given profile.
def _set_http_headers(profile):
"""Set the user agent and accept-language for the given profile.
We override this per request in the URL interceptor (to allow for
per-domain user agents), but this one still gets used for things like
window.navigator.userAgent in JS.
We override those per request in the URL interceptor (to allow for
per-domain values), but this one still gets used for things like
window.navigator.userAgent/.languages in JS.
"""
user_agent = config.get('network', 'user-agent')
profile.setHttpUserAgent(user_agent)
profile.setHttpUserAgent(config.val.content.headers.user_agent)
accept_language = config.val.content.headers.accept_language
if accept_language is not None:
profile.setHttpAcceptLanguage(accept_language)
def update_settings(section, option):
def _update_settings(option):
"""Update global settings when qwebsettings changed."""
websettings.update_mappings(MAPPINGS, section, option)
if section == 'ui' and option in ['hide-scrollbar', 'user-stylesheet']:
websettings.update_mappings(MAPPINGS, option)
if option in ['scrollbar.hide', 'content.user_stylesheets']:
_init_stylesheet(default_profile)
_init_stylesheet(private_profile)
elif section == 'network' and option == 'user-agent':
_set_user_agent(default_profile)
_set_user_agent(private_profile)
elif option in ['content.headers.user_agent',
'content.headers.accept_language']:
_set_http_headers(default_profile)
_set_http_headers(private_profile)
def _init_profiles():
@ -189,12 +192,12 @@ def _init_profiles():
default_profile.setPersistentStoragePath(
os.path.join(standarddir.data(), 'webengine'))
_init_stylesheet(default_profile)
_set_user_agent(default_profile)
_set_http_headers(default_profile)
private_profile = QWebEngineProfile()
assert private_profile.isOffTheRecord()
_init_stylesheet(private_profile)
_set_user_agent(private_profile)
_set_http_headers(private_profile)
def init(args):
@ -212,11 +215,11 @@ def init(args):
# We need to do this here as a WORKAROUND for
# https://bugreports.qt.io/browse/QTBUG-58650
if not qtutils.version_check('5.9'):
PersistentCookiePolicy().set(config.get('content', 'cookies-store'))
PersistentCookiePolicy().set(config.val.content.cookies.store)
Attribute(QWebEngineSettings.FullScreenSupportEnabled).set(True)
websettings.init_mappings(MAPPINGS)
objreg.get('config').changed.connect(update_settings)
config.instance.changed.connect(_update_settings)
def shutdown():
@ -237,79 +240,70 @@ def shutdown():
MAPPINGS = {
'content': {
'allow-images':
Attribute(QWebEngineSettings.AutoLoadImages),
'allow-javascript':
Attribute(QWebEngineSettings.JavascriptEnabled),
'javascript-can-open-windows-automatically':
Attribute(QWebEngineSettings.JavascriptCanOpenWindows),
'javascript-can-access-clipboard':
Attribute(QWebEngineSettings.JavascriptCanAccessClipboard),
'allow-plugins':
Attribute(QWebEngineSettings.PluginsEnabled),
'hyperlink-auditing':
Attribute(QWebEngineSettings.HyperlinkAuditingEnabled),
'local-content-can-access-remote-urls':
Attribute(QWebEngineSettings.LocalContentCanAccessRemoteUrls),
'local-content-can-access-file-urls':
Attribute(QWebEngineSettings.LocalContentCanAccessFileUrls),
'webgl':
Attribute(QWebEngineSettings.WebGLEnabled),
},
'input': {
'spatial-navigation':
Attribute(QWebEngineSettings.SpatialNavigationEnabled),
'links-included-in-focus-chain':
Attribute(QWebEngineSettings.LinksIncludedInFocusChain),
},
'fonts': {
'web-family-standard':
FontFamilySetter(QWebEngineSettings.StandardFont),
'web-family-fixed':
FontFamilySetter(QWebEngineSettings.FixedFont),
'web-family-serif':
FontFamilySetter(QWebEngineSettings.SerifFont),
'web-family-sans-serif':
FontFamilySetter(QWebEngineSettings.SansSerifFont),
'web-family-cursive':
FontFamilySetter(QWebEngineSettings.CursiveFont),
'web-family-fantasy':
FontFamilySetter(QWebEngineSettings.FantasyFont),
'web-size-minimum':
Setter(QWebEngineSettings.setFontSize,
args=[QWebEngineSettings.MinimumFontSize]),
'web-size-minimum-logical':
Setter(QWebEngineSettings.setFontSize,
args=[QWebEngineSettings.MinimumLogicalFontSize]),
'web-size-default':
Setter(QWebEngineSettings.setFontSize,
args=[QWebEngineSettings.DefaultFontSize]),
'web-size-default-fixed':
Setter(QWebEngineSettings.setFontSize,
args=[QWebEngineSettings.DefaultFixedFontSize]),
},
'ui': {
'smooth-scrolling':
Attribute(QWebEngineSettings.ScrollAnimatorEnabled),
},
'storage': {
'local-storage':
Attribute(QWebEngineSettings.LocalStorageEnabled),
'cache-size':
# 0: automatically managed by QtWebEngine
DefaultProfileSetter('setHttpCacheMaximumSize', default=0),
},
'general': {
'xss-auditing':
Attribute(QWebEngineSettings.XSSAuditingEnabled),
'default-encoding':
Setter(QWebEngineSettings.setDefaultTextEncoding),
}
'content.images':
Attribute(QWebEngineSettings.AutoLoadImages),
'content.javascript.enabled':
Attribute(QWebEngineSettings.JavascriptEnabled),
'content.javascript.can_open_tabs_automatically':
Attribute(QWebEngineSettings.JavascriptCanOpenWindows),
'content.javascript.can_access_clipboard':
Attribute(QWebEngineSettings.JavascriptCanAccessClipboard),
'content.plugins':
Attribute(QWebEngineSettings.PluginsEnabled),
'content.hyperlink_auditing':
Attribute(QWebEngineSettings.HyperlinkAuditingEnabled),
'content.local_content_can_access_remote_urls':
Attribute(QWebEngineSettings.LocalContentCanAccessRemoteUrls),
'content.local_content_can_access_file_urls':
Attribute(QWebEngineSettings.LocalContentCanAccessFileUrls),
'content.webgl':
Attribute(QWebEngineSettings.WebGLEnabled),
'content.local_storage':
Attribute(QWebEngineSettings.LocalStorageEnabled),
'content.cache.size':
# 0: automatically managed by QtWebEngine
DefaultProfileSetter('setHttpCacheMaximumSize', default=0),
'content.xss_auditing':
Attribute(QWebEngineSettings.XSSAuditingEnabled),
'content.default_encoding':
Setter(QWebEngineSettings.setDefaultTextEncoding),
'input.spatial_navigation':
Attribute(QWebEngineSettings.SpatialNavigationEnabled),
'input.links_included_in_focus_chain':
Attribute(QWebEngineSettings.LinksIncludedInFocusChain),
'fonts.web.family.standard':
FontFamilySetter(QWebEngineSettings.StandardFont),
'fonts.web.family.fixed':
FontFamilySetter(QWebEngineSettings.FixedFont),
'fonts.web.family.serif':
FontFamilySetter(QWebEngineSettings.SerifFont),
'fonts.web.family.sans_serif':
FontFamilySetter(QWebEngineSettings.SansSerifFont),
'fonts.web.family.cursive':
FontFamilySetter(QWebEngineSettings.CursiveFont),
'fonts.web.family.fantasy':
FontFamilySetter(QWebEngineSettings.FantasyFont),
'fonts.web.size.minimum':
Setter(QWebEngineSettings.setFontSize,
args=[QWebEngineSettings.MinimumFontSize]),
'fonts.web.size.minimum_logical':
Setter(QWebEngineSettings.setFontSize,
args=[QWebEngineSettings.MinimumLogicalFontSize]),
'fonts.web.size.default':
Setter(QWebEngineSettings.setFontSize,
args=[QWebEngineSettings.DefaultFontSize]),
'fonts.web.size.default_fixed':
Setter(QWebEngineSettings.setFontSize,
args=[QWebEngineSettings.DefaultFixedFontSize]),
'scrolling.smooth':
Attribute(QWebEngineSettings.ScrollAnimatorEnabled),
}
try:
MAPPINGS['general']['print-element-backgrounds'] = Attribute(
MAPPINGS['content.print_element_backgrounds'] = Attribute(
QWebEngineSettings.PrintElementBackgrounds)
except AttributeError:
# Added in Qt 5.8
@ -318,4 +312,4 @@ except AttributeError:
if qtutils.version_check('5.9'):
# https://bugreports.qt.io/browse/QTBUG-58650
MAPPINGS['content']['cookies-store'] = PersistentCookiePolicy()
MAPPINGS['content.cookies.store'] = PersistentCookiePolicy()

View File

@ -153,20 +153,16 @@ class WebEngineSearch(browsertab.AbstractSearch):
callback(found)
self._widget.findText(text, flags, wrapped_callback)
def search(self, text, *, ignore_case=False, reverse=False,
def search(self, text, *, ignore_case='never', reverse=False,
result_cb=None):
flags = QWebEnginePage.FindFlags(0)
if ignore_case == 'smart':
if not text.islower():
flags |= QWebEnginePage.FindCaseSensitively
elif not ignore_case:
flags |= QWebEnginePage.FindCaseSensitively
if reverse:
flags |= QWebEnginePage.FindBackward
self.text = text
self._flags = flags
self._find(text, flags, result_cb, 'search')
self._flags = QWebEnginePage.FindFlags(0)
if self._is_case_sensitive(ignore_case):
self._flags |= QWebEnginePage.FindCaseSensitively
if reverse:
self._flags |= QWebEnginePage.FindBackward
self._find(text, self._flags, result_cb, 'search')
def clear(self):
self.search_displayed = False
@ -699,7 +695,7 @@ class WebEngineTab(browsertab.AbstractTab):
error_page = jinja.render(
'error.html',
title="Error loading page: {}".format(url_string),
url=url_string, error="Authentication required", icon='')
url=url_string, error="Authentication required")
self.set_html(error_page)
@pyqtSlot('QWebEngineFullScreenRequest')

View File

@ -28,8 +28,7 @@ from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage
from qutebrowser.browser import shared
from qutebrowser.browser.webengine import certificateerror, webenginesettings
from qutebrowser.config import config
from qutebrowser.utils import (log, debug, usertypes, jinja, urlutils, message,
objreg)
from qutebrowser.utils import log, debug, usertypes, jinja, urlutils, message
class WebEngineView(QWebEngineView):
@ -80,10 +79,10 @@ class WebEngineView(QWebEngineView):
The new QWebEngineView object.
"""
debug_type = debug.qenum_key(QWebEnginePage, wintype)
background_tabs = config.get('tabs', 'background-tabs')
background = config.val.tabs.background
log.webview.debug("createWindow with type {}, background_tabs "
"{}".format(debug_type, background_tabs))
log.webview.debug("createWindow with type {}, background {}".format(
debug_type, background))
if wintype == QWebEnginePage.WebBrowserWindow:
# Shift-Alt-Click
@ -95,13 +94,13 @@ class WebEngineView(QWebEngineView):
elif wintype == QWebEnginePage.WebBrowserTab:
# Middle-click / Ctrl-Click with Shift
# FIXME:qtwebengine this also affects target=_blank links...
if background_tabs:
if background:
target = usertypes.ClickTarget.tab
else:
target = usertypes.ClickTarget.tab_bg
elif wintype == QWebEnginePage.WebBrowserBackgroundTab:
# Middle-click / Ctrl-Click
if background_tabs:
if background:
target = usertypes.ClickTarget.tab_bg
else:
target = usertypes.ClickTarget.tab
@ -135,11 +134,11 @@ class WebEnginePage(QWebEnginePage):
self._on_feature_permission_requested)
self._theme_color = theme_color
self._set_bg_color()
objreg.get('config').changed.connect(self._set_bg_color)
config.instance.changed.connect(self._set_bg_color)
@config.change_filter('colors', 'webpage.bg')
@config.change_filter('colors.webpage.bg')
def _set_bg_color(self):
col = config.get('colors', 'webpage.bg')
col = config.val.colors.webpage.bg
if col is None:
col = self._theme_color
self.setBackgroundColor(col)
@ -148,11 +147,10 @@ class WebEnginePage(QWebEnginePage):
def _on_feature_permission_requested(self, url, feature):
"""Ask the user for approval for geolocation/media/etc.."""
options = {
QWebEnginePage.Geolocation: ('content', 'geolocation'),
QWebEnginePage.MediaAudioCapture: ('content', 'media-capture'),
QWebEnginePage.MediaVideoCapture: ('content', 'media-capture'),
QWebEnginePage.MediaAudioVideoCapture:
('content', 'media-capture'),
QWebEnginePage.Geolocation: 'content.geolocation',
QWebEnginePage.MediaAudioCapture: 'content.media_capture',
QWebEnginePage.MediaVideoCapture: 'content.media_capture',
QWebEnginePage.MediaAudioVideoCapture: 'content.media_capture',
}
messages = {
QWebEnginePage.Geolocation: 'access your location',
@ -214,7 +212,7 @@ class WebEnginePage(QWebEnginePage):
url_string = url.toDisplayString()
error_page = jinja.render(
'error.html', title="Error loading page: {}".format(url_string),
url=url_string, error=str(error), icon='')
url=url_string, error=str(error))
if error.is_overridable():
ignore = shared.ignore_certificate_errors(
@ -276,19 +274,12 @@ class WebEnginePage(QWebEnginePage):
def javaScriptConsoleMessage(self, level, msg, line, source):
"""Log javascript messages to qutebrowser's log."""
# FIXME:qtwebengine maybe unify this in the tab api somehow?
setting = config.get('general', 'log-javascript-console')
if setting == 'none':
return
level_to_logger = {
QWebEnginePage.InfoMessageLevel: log.js.info,
QWebEnginePage.WarningMessageLevel: log.js.warning,
QWebEnginePage.ErrorMessageLevel: log.js.error,
level_map = {
QWebEnginePage.InfoMessageLevel: usertypes.JsLogLevel.info,
QWebEnginePage.WarningMessageLevel: usertypes.JsLogLevel.warning,
QWebEnginePage.ErrorMessageLevel: usertypes.JsLogLevel.error,
}
logstring = "[{}:{}] {}".format(source, line, msg)
logger = level_to_logger[level]
logger(logstring)
shared.javascript_log_message(level_map[level], source, line, msg)
def acceptNavigationRequest(self,
url: QUrl,

View File

@ -24,7 +24,7 @@ import os.path
from PyQt5.QtNetwork import QNetworkDiskCache
from qutebrowser.config import config
from qutebrowser.utils import utils, objreg, qtutils
from qutebrowser.utils import utils, qtutils
class DiskCache(QNetworkDiskCache):
@ -35,17 +35,17 @@ class DiskCache(QNetworkDiskCache):
super().__init__(parent)
self.setCacheDirectory(os.path.join(cache_dir, 'http'))
self._set_cache_size()
objreg.get('config').changed.connect(self._set_cache_size)
config.instance.changed.connect(self._set_cache_size)
def __repr__(self):
return utils.get_repr(self, size=self.cacheSize(),
maxsize=self.maximumCacheSize(),
path=self.cacheDirectory())
@config.change_filter('storage', 'cache-size')
@config.change_filter('content.cache.size')
def _set_cache_size(self):
"""Set the cache size based on the config."""
size = config.get('storage', 'cache-size')
size = config.val.content.cache.size
if size is None:
size = 1024 * 1024 * 50 # default from QNetworkDiskCachePrivate
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-59909

View File

@ -50,7 +50,7 @@ class RAMCookieJar(QNetworkCookieJar):
Return:
True if one or more cookies are set for 'url', otherwise False.
"""
if config.get('content', 'cookies-accept') == 'never':
if config.val.content.cookies.accept == 'never':
return False
else:
self.changed.emit()
@ -74,10 +74,10 @@ class CookieJar(RAMCookieJar):
self._lineparser = lineparser.LineParser(
standarddir.data(), 'cookies', binary=True, parent=self)
self.parse_cookies()
objreg.get('config').changed.connect(self.cookies_store_changed)
config.instance.changed.connect(self._on_cookies_store_changed)
objreg.get('save-manager').add_saveable(
'cookies', self.save, self.changed,
config_opt=('content', 'cookies-store'))
config_opt='content.cookies.store')
def parse_cookies(self):
"""Parse cookies from lineparser and store them."""
@ -105,10 +105,10 @@ class CookieJar(RAMCookieJar):
self._lineparser.data = lines
self._lineparser.save()
@config.change_filter('content', 'cookies-store')
def cookies_store_changed(self):
"""Delete stored cookies if cookies-store changed."""
if not config.get('content', 'cookies-store'):
@config.change_filter('content.cookies.store')
def _on_cookies_store_changed(self):
"""Delete stored cookies if cookies.store changed."""
if not config.val.content.cookies.store:
self._lineparser.data = []
self._lineparser.save()
self.changed.emit()

View File

@ -101,13 +101,12 @@ def dirbrowser_html(path):
except OSError as e:
html = jinja.render('error.html',
title="Error while reading directory",
url='file:///{}'.format(path), error=str(e),
icon='')
url='file:///{}'.format(path), error=str(e))
return html.encode('UTF-8', errors='xmlcharrefreplace')
files = get_file_list(path, all_files, os.path.isfile)
directories = get_file_list(path, all_files, os.path.isdir)
html = jinja.render('dirbrowser.html', title=title, url=path, icon='',
html = jinja.render('dirbrowser.html', title=title, url=path,
parent=parent, files=files, directories=directories)
return html.encode('UTF-8', errors='xmlcharrefreplace')

View File

@ -274,7 +274,7 @@ class NetworkManager(QNetworkAccessManager):
# altogether.
reply.netrc_used = True
try:
net = netrc.netrc(config.get('network', 'netrc-file'))
net = netrc.netrc(config.val.content.netrc_file)
authenticators = net.authenticators(reply.url().host())
if authenticators is not None:
(user, _account, password) = authenticators
@ -338,7 +338,7 @@ class NetworkManager(QNetworkAccessManager):
def set_referer(self, req, current_url):
"""Set the referer header."""
referer_header_conf = config.get('network', 'referer-header')
referer_header_conf = config.val.content.headers.referer
try:
if referer_header_conf == 'never':

View File

@ -20,16 +20,12 @@
"""QtWebKit specific qute://* handlers and glue code."""
import mimetypes
import functools
import configparser
from PyQt5.QtCore import pyqtSlot, QObject
from PyQt5.QtNetwork import QNetworkReply
from qutebrowser.browser import pdfjs, qutescheme
from qutebrowser.browser.webkit.network import schemehandler, networkreply
from qutebrowser.utils import jinja, log, message, objreg, usertypes, qtutils
from qutebrowser.config import configexc, configdata
from qutebrowser.utils import log, usertypes, qtutils
class QuteSchemeHandler(schemehandler.SchemeHandler):
@ -70,34 +66,6 @@ class QuteSchemeHandler(schemehandler.SchemeHandler):
self.parent())
class JSBridge(QObject):
"""Javascript-bridge for special qute://... pages."""
@pyqtSlot(str, str, str)
def set(self, sectname, optname, value):
"""Slot to set a setting from qute://settings."""
# https://github.com/qutebrowser/qutebrowser/issues/727
if ((sectname, optname) == ('content', 'allow-javascript') and
value == 'false'):
message.error("Refusing to disable javascript via qute://settings "
"as it needs javascript support.")
return
try:
objreg.get('config').set('conf', sectname, optname, value)
except (configexc.Error, configparser.Error) as e:
message.error(str(e))
@qutescheme.add_handler('settings', backend=usertypes.Backend.QtWebKit)
def qute_settings(_url):
"""Handler for qute://settings. View/change qute configuration."""
config_getter = functools.partial(objreg.get('config').get, raw=True)
html = jinja.render('settings.html', title='settings', config=configdata,
confget=config_getter)
return 'text/html', html
@qutescheme.add_handler('pdfjs', backend=usertypes.Backend.QtWebKit)
def qute_pdfjs(url):
"""Handler for qute://pdfjs. Return the pdf.js viewer."""

View File

@ -168,7 +168,7 @@ class WebKitElement(webelem.AbstractWebElement):
if width > 1 and height > 1:
# fix coordinates according to zoom level
zoom = self._elem.webFrame().zoomFactor()
if not config.get('ui', 'zoom-text-only'):
if not config.val.zoom.text_only:
rect["left"] *= zoom
rect["top"] *= zoom
width *= zoom

View File

@ -36,9 +36,9 @@ class WebKitInspector(inspector.AbstractWebInspector):
self._set_widget(qwebinspector)
def inspect(self, page):
if not config.get('general', 'developer-extras'):
if not config.val.content.developer_extras:
raise inspector.WebInspectorError(
"Please enable developer-extras before using the "
"Please enable content.developer_extras before using the "
"webinspector!")
self._widget.setPage(page)
self.show()

View File

@ -33,7 +33,7 @@ from PyQt5.QtGui import QFont
from PyQt5.QtWebKit import QWebSettings
from qutebrowser.config import config, websettings
from qutebrowser.utils import standarddir, objreg, urlutils, qtutils
from qutebrowser.utils import standarddir, urlutils, qtutils
from qutebrowser.browser import shared
@ -111,12 +111,11 @@ def _set_user_stylesheet():
QWebSettings.globalSettings().setUserStyleSheetUrl(url)
def update_settings(section, option):
def _update_settings(option):
"""Update global settings when qwebsettings changed."""
if section == 'ui' and option in ['hide-scrollbar', 'user-stylesheet']:
if option in ['scrollbar.hide', 'content.user_stylesheets']:
_set_user_stylesheet()
websettings.update_mappings(MAPPINGS, section, option)
websettings.update_mappings(MAPPINGS, option)
def init(_args):
@ -132,7 +131,7 @@ def init(_args):
QWebSettings.setOfflineStoragePath(
os.path.join(data_path, 'offline-storage'))
if (config.get('general', 'private-browsing') and
if (config.val.content.private_browsing and
not qtutils.version_check('5.4.2')):
# WORKAROUND for https://codereview.qt-project.org/#/c/108936/
# Won't work when private browsing is not enabled globally, but that's
@ -141,7 +140,7 @@ def init(_args):
websettings.init_mappings(MAPPINGS)
_set_user_stylesheet()
objreg.get('config').changed.connect(update_settings)
config.instance.changed.connect(_update_settings)
def shutdown():
@ -152,96 +151,79 @@ def shutdown():
MAPPINGS = {
'content': {
'allow-images':
Attribute(QWebSettings.AutoLoadImages),
'allow-javascript':
Attribute(QWebSettings.JavascriptEnabled),
'javascript-can-open-windows-automatically':
Attribute(QWebSettings.JavascriptCanOpenWindows),
'javascript-can-close-windows':
Attribute(QWebSettings.JavascriptCanCloseWindows),
'javascript-can-access-clipboard':
Attribute(QWebSettings.JavascriptCanAccessClipboard),
'allow-plugins':
Attribute(QWebSettings.PluginsEnabled),
'webgl':
Attribute(QWebSettings.WebGLEnabled),
'hyperlink-auditing':
Attribute(QWebSettings.HyperlinkAuditingEnabled),
'local-content-can-access-remote-urls':
Attribute(QWebSettings.LocalContentCanAccessRemoteUrls),
'local-content-can-access-file-urls':
Attribute(QWebSettings.LocalContentCanAccessFileUrls),
'cookies-accept':
CookiePolicy(),
},
'network': {
'dns-prefetch':
Attribute(QWebSettings.DnsPrefetchEnabled),
},
'input': {
'spatial-navigation':
Attribute(QWebSettings.SpatialNavigationEnabled),
'links-included-in-focus-chain':
Attribute(QWebSettings.LinksIncludedInFocusChain),
},
'fonts': {
'web-family-standard':
FontFamilySetter(QWebSettings.StandardFont),
'web-family-fixed':
FontFamilySetter(QWebSettings.FixedFont),
'web-family-serif':
FontFamilySetter(QWebSettings.SerifFont),
'web-family-sans-serif':
FontFamilySetter(QWebSettings.SansSerifFont),
'web-family-cursive':
FontFamilySetter(QWebSettings.CursiveFont),
'web-family-fantasy':
FontFamilySetter(QWebSettings.FantasyFont),
'web-size-minimum':
Setter(QWebSettings.setFontSize,
args=[QWebSettings.MinimumFontSize]),
'web-size-minimum-logical':
Setter(QWebSettings.setFontSize,
args=[QWebSettings.MinimumLogicalFontSize]),
'web-size-default':
Setter(QWebSettings.setFontSize,
args=[QWebSettings.DefaultFontSize]),
'web-size-default-fixed':
Setter(QWebSettings.setFontSize,
args=[QWebSettings.DefaultFixedFontSize]),
},
'ui': {
'zoom-text-only':
Attribute(QWebSettings.ZoomTextOnly),
'frame-flattening':
Attribute(QWebSettings.FrameFlatteningEnabled),
# user-stylesheet is handled separately
'smooth-scrolling':
Attribute(QWebSettings.ScrollAnimatorEnabled),
#'accelerated-compositing':
# Attribute(QWebSettings.AcceleratedCompositingEnabled),
#'tiled-backing-store':
# Attribute(QWebSettings.TiledBackingStoreEnabled),
},
'storage': {
'offline-web-application-cache':
Attribute(QWebSettings.OfflineWebApplicationCacheEnabled),
'local-storage':
Attribute(QWebSettings.LocalStorageEnabled,
QWebSettings.OfflineStorageDatabaseEnabled),
'maximum-pages-in-cache':
StaticSetter(QWebSettings.setMaximumPagesInCache),
},
'general': {
'developer-extras':
Attribute(QWebSettings.DeveloperExtrasEnabled),
'print-element-backgrounds':
Attribute(QWebSettings.PrintElementBackgrounds),
'xss-auditing':
Attribute(QWebSettings.XSSAuditingEnabled),
'default-encoding':
Setter(QWebSettings.setDefaultTextEncoding),
}
'content.images':
Attribute(QWebSettings.AutoLoadImages),
'content.javascript.enabled':
Attribute(QWebSettings.JavascriptEnabled),
'content.javascript.can_open_tabs_automatically':
Attribute(QWebSettings.JavascriptCanOpenWindows),
'content.javascript.can_close_tabs':
Attribute(QWebSettings.JavascriptCanCloseWindows),
'content.javascript.can_access_clipboard':
Attribute(QWebSettings.JavascriptCanAccessClipboard),
'content.plugins':
Attribute(QWebSettings.PluginsEnabled),
'content.webgl':
Attribute(QWebSettings.WebGLEnabled),
'content.hyperlink_auditing':
Attribute(QWebSettings.HyperlinkAuditingEnabled),
'content.local_content_can_access_remote_urls':
Attribute(QWebSettings.LocalContentCanAccessRemoteUrls),
'content.local_content_can_access_file_urls':
Attribute(QWebSettings.LocalContentCanAccessFileUrls),
'content.cookies.accept':
CookiePolicy(),
'content.dns_prefetch':
Attribute(QWebSettings.DnsPrefetchEnabled),
'content.frame_flattening':
Attribute(QWebSettings.FrameFlatteningEnabled),
'content.cache.appcache':
Attribute(QWebSettings.OfflineWebApplicationCacheEnabled),
'content.local_storage':
Attribute(QWebSettings.LocalStorageEnabled,
QWebSettings.OfflineStorageDatabaseEnabled),
'content.cache.maximum_pages':
StaticSetter(QWebSettings.setMaximumPagesInCache),
'content.developer_extras':
Attribute(QWebSettings.DeveloperExtrasEnabled),
'content.print_element_backgrounds':
Attribute(QWebSettings.PrintElementBackgrounds),
'content.xss_auditing':
Attribute(QWebSettings.XSSAuditingEnabled),
'content.default_encoding':
Setter(QWebSettings.setDefaultTextEncoding),
# content.user_stylesheets is handled separately
'input.spatial_navigation':
Attribute(QWebSettings.SpatialNavigationEnabled),
'input.links_included_in_focus_chain':
Attribute(QWebSettings.LinksIncludedInFocusChain),
'fonts.web.family.standard':
FontFamilySetter(QWebSettings.StandardFont),
'fonts.web.family.fixed':
FontFamilySetter(QWebSettings.FixedFont),
'fonts.web.family.serif':
FontFamilySetter(QWebSettings.SerifFont),
'fonts.web.family.sans_serif':
FontFamilySetter(QWebSettings.SansSerifFont),
'fonts.web.family.cursive':
FontFamilySetter(QWebSettings.CursiveFont),
'fonts.web.family.fantasy':
FontFamilySetter(QWebSettings.FantasyFont),
'fonts.web.size.minimum':
Setter(QWebSettings.setFontSize, args=[QWebSettings.MinimumFontSize]),
'fonts.web.size.minimum_logical':
Setter(QWebSettings.setFontSize,
args=[QWebSettings.MinimumLogicalFontSize]),
'fonts.web.size.default':
Setter(QWebSettings.setFontSize, args=[QWebSettings.DefaultFontSize]),
'fonts.web.size.default_fixed':
Setter(QWebSettings.setFontSize,
args=[QWebSettings.DefaultFixedFontSize]),
'zoom.text_only':
Attribute(QWebSettings.ZoomTextOnly),
'scrolling.smooth':
Attribute(QWebSettings.ScrollAnimatorEnabled),
}

View File

@ -27,25 +27,15 @@ import sip
from PyQt5.QtCore import (pyqtSlot, Qt, QEvent, QUrl, QPoint, QTimer, QSizeF,
QSize)
from PyQt5.QtGui import QKeyEvent
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWebKitWidgets import QWebPage, QWebFrame
from PyQt5.QtWebKit import QWebSettings
from PyQt5.QtPrintSupport import QPrinter
from qutebrowser.browser import browsertab
from qutebrowser.browser.webkit import webview, tabhistory, webkitelem
from qutebrowser.browser.webkit.network import webkitqutescheme
from qutebrowser.utils import qtutils, objreg, usertypes, utils, log, debug
def init():
"""Initialize QtWebKit-specific modules."""
qapp = QApplication.instance()
log.init.debug("Initializing js-bridge...")
js_bridge = webkitqutescheme.JSBridge(qapp)
objreg.register('js-bridge', js_bridge)
class WebKitAction(browsertab.AbstractAction):
"""QtWebKit implementations related to web actions."""
@ -133,24 +123,21 @@ class WebKitSearch(browsertab.AbstractSearch):
self._widget.findText('')
self._widget.findText('', QWebPage.HighlightAllOccurrences)
def search(self, text, *, ignore_case=False, reverse=False,
def search(self, text, *, ignore_case='never', reverse=False,
result_cb=None):
self.search_displayed = True
flags = QWebPage.FindWrapsAroundDocument
if ignore_case == 'smart':
if not text.islower():
flags |= QWebPage.FindCaseSensitively
elif not ignore_case:
flags |= QWebPage.FindCaseSensitively
self.text = text
self._flags = QWebPage.FindWrapsAroundDocument
if self._is_case_sensitive(ignore_case):
self._flags |= QWebPage.FindCaseSensitively
if reverse:
flags |= QWebPage.FindBackward
self._flags |= QWebPage.FindBackward
# We actually search *twice* - once to highlight everything, then again
# to get a mark so we can navigate.
found = self._widget.findText(text, flags)
self._widget.findText(text, flags | QWebPage.HighlightAllOccurrences)
self.text = text
self._flags = flags
self._call_cb(result_cb, found, text, flags, 'search')
found = self._widget.findText(text, self._flags)
self._widget.findText(text,
self._flags | QWebPage.HighlightAllOccurrences)
self._call_cb(result_cb, found, text, self._flags, 'search')
def next_result(self, *, result_cb=None):
self.search_displayed = True

View File

@ -170,7 +170,7 @@ class BrowserPage(QWebPage):
title = "Error loading page: {}".format(urlstr)
error_html = jinja.render(
'error.html',
title=title, url=urlstr, error=error_str, icon='')
title=title, url=urlstr, error=error_str)
errpage.content = error_html.encode('utf-8')
errpage.encoding = 'utf-8'
return True
@ -277,7 +277,7 @@ class BrowserPage(QWebPage):
reply.finished.connect(functools.partial(
self.display_content, reply, 'image/jpeg'))
elif (mimetype in ['application/pdf', 'application/x-pdf'] and
config.get('content', 'enable-pdfjs')):
config.val.content.pdfjs):
# Use pdf.js to display the page
self._show_pdfjs(reply)
else:
@ -304,8 +304,8 @@ class BrowserPage(QWebPage):
return
options = {
QWebPage.Notifications: ('content', 'notifications'),
QWebPage.Geolocation: ('content', 'geolocation'),
QWebPage.Notifications: 'content.notifications',
QWebPage.Geolocation: 'content.geolocation',
}
messages = {
QWebPage.Notifications: 'show notifications',
@ -384,7 +384,7 @@ class BrowserPage(QWebPage):
def userAgentForUrl(self, url):
"""Override QWebPage::userAgentForUrl to customize the user agent."""
ua = config.get('network', 'user-agent')
ua = config.val.content.headers.user_agent
if ua is None:
return super().userAgentForUrl(url)
else:
@ -446,15 +446,8 @@ class BrowserPage(QWebPage):
def javaScriptConsoleMessage(self, msg, line, source):
"""Override javaScriptConsoleMessage to use debug log."""
log_javascript_console = config.get('general',
'log-javascript-console')
logstring = "[{}:{}] {}".format(source, line, msg)
logmap = {
'debug': log.js.debug,
'info': log.js.info,
'none': lambda arg: None
}
logmap[log_javascript_console](logstring)
shared.javascript_log_message(usertypes.JsLogLevel.unknown,
source, line, msg)
def acceptNavigationRequest(self,
_frame: QWebFrame,

View File

@ -25,7 +25,7 @@ from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QUrl
from PyQt5.QtGui import QPalette
from PyQt5.QtWidgets import QStyleFactory
from PyQt5.QtWebKit import QWebSettings
from PyQt5.QtWebKitWidgets import QWebView, QWebPage, QWebFrame
from PyQt5.QtWebKitWidgets import QWebView, QWebPage
from qutebrowser.config import config
from qutebrowser.keyinput import modeman
@ -88,7 +88,7 @@ class WebView(QWebView):
window=win_id)
mode_manager.entered.connect(self.on_mode_entered)
mode_manager.left.connect(self.on_mode_left)
objreg.get('config').changed.connect(self._set_bg_color)
config.instance.changed.connect(self._set_bg_color)
def __repr__(self):
url = utils.elide(self.url().toDisplayString(QUrl.EncodeUnicode), 100)
@ -107,10 +107,10 @@ class WebView(QWebView):
# deleted
pass
@config.change_filter('colors', 'webpage.bg')
@config.change_filter('colors.webpage.bg')
def _set_bg_color(self):
"""Set the webpage background color as configured."""
col = config.get('colors', 'webpage.bg')
col = config.val.colors.webpage.bg
palette = self.palette()
if col is None:
col = self.style().standardPalette().color(QPalette.Base)
@ -135,22 +135,6 @@ class WebView(QWebView):
url: The URL to load as QUrl
"""
self.load(url)
if url.scheme() == 'qute':
frame = self.page().mainFrame()
frame.javaScriptWindowObjectCleared.connect(self.add_js_bridge)
@pyqtSlot()
def add_js_bridge(self):
"""Add the javascript bridge for qute://... pages."""
frame = self.sender()
if not isinstance(frame, QWebFrame):
log.webview.error("Got non-QWebFrame {!r} in "
"add_js_bridge!".format(frame))
return
if frame.url().scheme() == 'qute':
bridge = objreg.get('js-bridge')
frame.addToJavaScriptWindowObject('qute', bridge)
@pyqtSlot(usertypes.KeyMode)
def on_mode_entered(self, mode):
@ -285,10 +269,10 @@ class WebView(QWebView):
This is implemented here as we don't need it for QtWebEngine.
"""
if e.button() == Qt.MidButton or e.modifiers() & Qt.ControlModifier:
background_tabs = config.get('tabs', 'background-tabs')
background = config.val.tabs.background
if e.modifiers() & Qt.ShiftModifier:
background_tabs = not background_tabs
if background_tabs:
background = not background
if background:
target = usertypes.ClickTarget.tab_bg
else:
target = usertypes.ClickTarget.tab

View File

@ -23,33 +23,33 @@ Defined here to avoid circular dependency hell.
"""
class CommandError(Exception):
class Error(Exception):
"""Base class for all cmdexc errors."""
class CommandError(Error):
"""Raised when a command encounters an error while running."""
pass
class CommandMetaError(Exception):
"""Common base class for exceptions occurring before a command is run."""
class NoSuchCommandError(CommandMetaError):
class NoSuchCommandError(Error):
"""Raised when a command wasn't found."""
pass
class ArgumentTypeError(CommandMetaError):
class ArgumentTypeError(Error):
"""Raised when an argument had an invalid type."""
pass
class PrerequisitesError(CommandMetaError):
class PrerequisitesError(Error):
"""Raised when a cmd can't be used because some prerequisites aren't met.

View File

@ -21,7 +21,6 @@
Module attributes:
cmd_dict: A mapping from command-strings to command objects.
aliases: A list of all aliases, needed for doc generation.
"""
import inspect
@ -30,7 +29,6 @@ from qutebrowser.utils import qtutils, log
from qutebrowser.commands import command, cmdexc
cmd_dict = {}
aliases = []
def check_overflow(arg, ctype):
@ -88,28 +86,6 @@ class register: # pylint: disable=invalid-name
self._name = name
self._kwargs = kwargs
def _get_names(self, func):
"""Get the name(s) which should be used for the current command.
If the name hasn't been overridden explicitly, the function name is
transformed.
If it has been set, it can either be a string which is
used directly, or an iterable.
Args:
func: The function to get the name of.
Return:
A list of names, with the main name being the first item.
"""
if self._name is None:
return [func.__name__.lower().replace('_', '-')]
elif isinstance(self._name, str):
return [self._name]
else:
return self._name
def __call__(self, func):
"""Register the command before running the function.
@ -124,17 +100,17 @@ class register: # pylint: disable=invalid-name
Return:
The original function (unmodified).
"""
global aliases
names = self._get_names(func)
log.commands.vdebug("Registering command {}".format(names[0]))
for name in names:
if name in cmd_dict:
raise ValueError("{} is already registered!".format(name))
cmd = command.Command(name=names[0], instance=self._instance,
if self._name is None:
name = func.__name__.lower().replace('_', '-')
else:
assert isinstance(self._name, str), self._name
name = self._name
log.commands.vdebug("Registering command {}".format(name))
if name in cmd_dict:
raise ValueError("{} is already registered!".format(name))
cmd = command.Command(name=name, instance=self._instance,
handler=func, **self._kwargs)
for name in names:
cmd_dict[name] = cmd
aliases += names[1:]
cmd_dict[name] = cmd
return func

View File

@ -90,7 +90,7 @@ class Command:
def __init__(self, *, handler, name, instance=None, maxsplit=None,
hide=False, modes=None, not_modes=None, debug=False,
ignore_args=False, deprecated=False, no_cmd_split=False,
deprecated=False, no_cmd_split=False,
star_args_optional=False, scope='global', backend=None,
no_replace_variables=False):
# I really don't know how to solve this in a better way, I tried.
@ -121,7 +121,6 @@ class Command:
self._scope = scope
self._star_args_optional = star_args_optional
self.debug = debug
self.ignore_args = ignore_args
self.handler = handler
self.no_cmd_split = no_cmd_split
self.backend = backend
@ -225,33 +224,31 @@ class Command:
else:
self.desc = ""
if not self.ignore_args:
for param in signature.parameters.values():
# https://docs.python.org/3/library/inspect.html#inspect.Parameter.kind
# "Python has no explicit syntax for defining positional-only
# parameters, but many built-in and extension module functions
# (especially those that accept only one or two parameters)
# accept them."
assert param.kind != inspect.Parameter.POSITIONAL_ONLY
if param.name == 'self':
continue
if self._inspect_special_param(param):
continue
if (param.kind == inspect.Parameter.KEYWORD_ONLY and
param.default is inspect.Parameter.empty):
raise TypeError("{}: handler has keyword only argument "
"{!r} without default!".format(self.name,
param.name))
typ = self._get_type(param)
is_bool = typ is bool
kwargs = self._param_to_argparse_kwargs(param, is_bool)
args = self._param_to_argparse_args(param, is_bool)
callsig = debug_utils.format_call(
self.parser.add_argument, args, kwargs,
full=False)
log.commands.vdebug('Adding arg {} of type {} -> {}'.format(
param.name, typ, callsig))
self.parser.add_argument(*args, **kwargs)
for param in signature.parameters.values():
# https://docs.python.org/3/library/inspect.html#inspect.Parameter.kind
# "Python has no explicit syntax for defining positional-only
# parameters, but many built-in and extension module functions
# (especially those that accept only one or two parameters) accept
# them."
assert param.kind != inspect.Parameter.POSITIONAL_ONLY
if param.name == 'self':
continue
if self._inspect_special_param(param):
continue
if (param.kind == inspect.Parameter.KEYWORD_ONLY and
param.default is inspect.Parameter.empty):
raise TypeError("{}: handler has keyword only argument {!r} "
"without default!".format(
self.name, param.name))
typ = self._get_type(param)
is_bool = typ is bool
kwargs = self._param_to_argparse_kwargs(param, is_bool)
args = self._param_to_argparse_args(param, is_bool)
callsig = debug_utils.format_call(self.parser.add_argument, args,
kwargs, full=False)
log.commands.vdebug('Adding arg {} of type {} -> {}'.format(
param.name, typ, callsig))
self.parser.add_argument(*args, **kwargs)
return signature.parameters.values()
def _param_to_argparse_kwargs(self, param, is_bool):
@ -453,12 +450,6 @@ class Command:
kwargs = {}
signature = inspect.signature(self.handler)
if self.ignore_args:
if self._instance is not None:
param = list(signature.parameters.values())[0]
self._get_self_arg(win_id, param, args)
return args, kwargs
for i, param in enumerate(signature.parameters.values()):
arg_info = self.get_arg_info(param)
if i == 0 and self._instance is not None:

View File

@ -25,7 +25,7 @@ import re
from PyQt5.QtCore import pyqtSlot, QUrl, QObject
from qutebrowser.config import config, configexc
from qutebrowser.config import config
from qutebrowser.commands import cmdexc, cmdutils
from qutebrowser.utils import message, objreg, qtutils, usertypes, utils
from qutebrowser.misc import split
@ -81,19 +81,17 @@ def replace_variables(win_id, arglist):
return args
class CommandRunner(QObject):
class CommandParser:
"""Parse and run qutebrowser commandline commands.
"""Parse qutebrowser commandline commands.
Attributes:
_win_id: The window this CommandRunner is associated with.
_partial_match: Whether to allow partial command matches.
"""
def __init__(self, win_id, partial_match=False, parent=None):
super().__init__(parent)
def __init__(self, partial_match=False):
self._partial_match = partial_match
self._win_id = win_id
def _get_alias(self, text, default=None):
"""Get an alias from the config.
@ -108,9 +106,10 @@ class CommandRunner(QObject):
"""
parts = text.strip().split(maxsplit=1)
try:
alias = config.get('aliases', parts[0])
except (configexc.NoOptionError, configexc.NoSectionError):
alias = config.val.aliases[parts[0]]
except KeyError:
return default
try:
new_cmd = '{} {}'.format(alias, parts[1])
except IndexError:
@ -119,7 +118,7 @@ class CommandRunner(QObject):
new_cmd += ' '
return new_cmd
def parse_all(self, text, aliases=True, *args, **kwargs):
def _parse_all_gen(self, text, aliases=True, *args, **kwargs):
"""Split a command on ;; and parse all parts.
If the first command in the commandline is a non-split one, it only
@ -154,6 +153,10 @@ class CommandRunner(QObject):
for sub in sub_texts:
yield self.parse(sub, *args, **kwargs)
def parse_all(self, *args, **kwargs):
"""Wrapper over parse_all."""
return list(self._parse_all_gen(*args, **kwargs))
def parse(self, text, *, fallback=False, keep=False):
"""Split the commandline text into command and arguments.
@ -253,6 +256,20 @@ class CommandRunner(QObject):
# already.
return split_args
class CommandRunner(QObject):
"""Parse and run qutebrowser commandline commands.
Attributes:
_win_id: The window this CommandRunner is associated with.
"""
def __init__(self, win_id, partial_match=False, parent=None):
super().__init__(parent)
self._parser = CommandParser(partial_match=partial_match)
self._win_id = win_id
def run(self, text, count=None):
"""Parse a command from a line of text and run it.
@ -267,7 +284,7 @@ class CommandRunner(QObject):
window=self._win_id)
cur_mode = mode_manager.mode
for result in self.parse_all(text):
for result in self._parser.parse_all(text):
if result.cmd.no_replace_variables:
args = result.args
else:
@ -294,7 +311,7 @@ class CommandRunner(QObject):
"""Run a command and display exceptions in the statusbar."""
try:
self.run(text, count)
except (cmdexc.CommandMetaError, cmdexc.CommandError) as e:
except cmdexc.Error as e:
message.error(str(e), stack=traceback.format_exc())
@pyqtSlot(str, int)
@ -306,5 +323,5 @@ class CommandRunner(QObject):
"""
try:
self.run(text, count)
except (cmdexc.CommandMetaError, cmdexc.CommandError) as e:
except cmdexc.Error as e:
message.error(str(e), stack=traceback.format_exc())

View File

@ -376,7 +376,7 @@ def _lookup_path(cmd):
"""
directories = [
os.path.join(standarddir.data(), "userscripts"),
os.path.join(standarddir.system_data(), "userscripts"),
os.path.join(standarddir.data(system=True), "userscripts"),
]
for directory in directories:
cmd_path = os.path.join(directory, cmd)
@ -417,7 +417,7 @@ def run_async(tab, cmd, *args, win_id, env, verbose=False):
lambda cmd:
log.commands.debug("Got userscript command: {}".format(cmd)))
runner.got_cmd.connect(commandrunner.run_safely)
user_agent = config.get('network', 'user-agent')
user_agent = config.val.content.headers.user_agent
if user_agent is not None:
env['QUTE_USER_AGENT'] = user_agent

View File

@ -19,6 +19,8 @@
"""Completer attached to a CompletionView."""
import collections
from PyQt5.QtCore import pyqtSlot, QObject, QTimer
from qutebrowser.config import config
@ -27,6 +29,11 @@ from qutebrowser.utils import log, utils, debug
from qutebrowser.completion.models import miscmodels
# Context passed into all completion functions
CompletionInfo = collections.namedtuple('CompletionInfo',
['config', 'keyconf'])
class Completer(QObject):
"""Completer which manages completions in a CompletionView.
@ -34,7 +41,6 @@ class Completer(QObject):
Attributes:
_cmd: The statusbar Command object this completer belongs to.
_ignore_change: Whether to ignore the next completion update.
_win_id: The window ID this completer is in.
_timer: The timer used to trigger the completion update.
_last_cursor_pos: The old cursor position so we avoid double completion
updates.
@ -42,9 +48,8 @@ class Completer(QObject):
_last_completion_func: The completion function used for the last text.
"""
def __init__(self, cmd, win_id, parent=None):
def __init__(self, cmd, parent=None):
super().__init__(parent)
self._win_id = win_id
self._cmd = cmd
self._ignore_change = False
self._timer = QTimer()
@ -106,7 +111,7 @@ class Completer(QObject):
"""
if not s:
return "''"
elif any(c in s for c in ' \'\t\n\\'):
elif any(c in s for c in ' "\'\t\n\\'):
# use single quotes, and put single quotes into double quotes
# the string $'b is then quoted as '$'"'"'b'
return "'" + s.replace("'", "'\"'\"'") + "'"
@ -123,8 +128,8 @@ class Completer(QObject):
if not text or not text.strip():
# Only ":", empty part under the cursor with nothing before/after
return [], '', []
runner = runners.CommandRunner(self._win_id)
result = runner.parse(text, fallback=True, keep=True)
parser = runners.CommandParser()
result = parser.parse(text, fallback=True, keep=True)
parts = [x for x in result.cmdline if x]
pos = self._cmd.cursorPosition() - len(self._cmd.prefix())
pos = min(pos, len(text)) # Qt treats 2-byte UTF-16 chars as 2 chars
@ -164,7 +169,7 @@ class Completer(QObject):
if maxsplit is None:
text = self._quote(text)
model = self._model()
if model.count() == 1 and config.get('completion', 'quick-complete'):
if model.count() == 1 and config.val.completion.quick:
# If we only have one item, we want to apply it immediately
# and go on to the next part.
self._change_completed_part(text, before, after, immediate=True)
@ -233,7 +238,9 @@ class Completer(QObject):
args = (x for x in before_cursor[1:] if not x.startswith('-'))
with debug.log_time(log.completion,
'Starting {} completion'.format(func.__name__)):
model = func(*args)
info = CompletionInfo(config=config.instance,
keyconf=config.key_instance)
model = func(*args, info=info)
with debug.log_time(log.completion, 'Set completion model'):
completion.set_model(model)

View File

@ -30,8 +30,8 @@ from PyQt5.QtCore import QRectF, QSize, Qt
from PyQt5.QtGui import (QIcon, QPalette, QTextDocument, QTextOption,
QAbstractTextDocumentLayout)
from qutebrowser.config import config, configexc, style
from qutebrowser.utils import qtutils
from qutebrowser.config import config
from qutebrowser.utils import qtutils, jinja
class CompletionItemDelegate(QStyledItemDelegate):
@ -147,16 +147,15 @@ class CompletionItemDelegate(QStyledItemDelegate):
# We can't use drawContents because then the color would be ignored.
clip = QRectF(0, 0, rect.width(), rect.height())
self._painter.save()
if self._opt.state & QStyle.State_Selected:
option = 'completion.item.selected.fg'
color = config.val.colors.completion.item.selected.fg
elif not self._opt.state & QStyle.State_Enabled:
option = 'completion.category.fg'
color = config.val.colors.completion.category.fg
else:
option = 'completion.fg'
try:
self._painter.setPen(config.get('colors', option))
except configexc.NoOptionError:
self._painter.setPen(config.get('colors', 'completion.fg'))
color = config.val.colors.completion.fg
self._painter.setPen(color)
ctx = QAbstractTextDocumentLayout.PaintContext()
ctx.palette.setColor(QPalette.Text, self._painter.pen().color())
if clip.isValid():
@ -188,13 +187,17 @@ class CompletionItemDelegate(QStyledItemDelegate):
self._doc = QTextDocument(self)
self._doc.setDefaultFont(self._opt.font)
self._doc.setDefaultTextOption(text_option)
self._doc.setDefaultStyleSheet(style.get_stylesheet("""
.highlight {
color: {{ color['completion.match.fg'] }};
}
"""))
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))
if index.parent().isValid():
view = self.parent()
pattern = view.pattern
@ -209,7 +212,7 @@ class CompletionItemDelegate(QStyledItemDelegate):
else:
self._doc.setHtml(
'<span style="font: {};">{}</span>'.format(
html.escape(config.get('fonts', 'completion.category')),
html.escape(config.val.fonts.completion.category),
html.escape(self._opt.text)))
def _draw_focus_rect(self):

View File

@ -26,9 +26,9 @@ subclasses to provide completions.
from PyQt5.QtWidgets import QStyle, QTreeView, QSizePolicy, QStyleFactory
from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QItemSelectionModel, QSize
from qutebrowser.config import config, style
from qutebrowser.config import config
from qutebrowser.completion import completiondelegate
from qutebrowser.utils import utils, usertypes, objreg, debug, log
from qutebrowser.utils import utils, usertypes, debug, log
from qutebrowser.commands import cmdexc, cmdutils
@ -57,27 +57,27 @@ class CompletionView(QTreeView):
# don't define that in this stylesheet.
STYLESHEET = """
QTreeView {
font: {{ font['completion'] }};
background-color: {{ color['completion.bg'] }};
alternate-background-color: {{ color['completion.alternate-bg'] }};
font: {{ conf.fonts.completion.entry }};
background-color: {{ conf.colors.completion.even.bg }};
alternate-background-color: {{ conf.colors.completion.odd.bg }};
outline: 0;
border: 0px;
}
QTreeView::item:disabled {
background-color: {{ color['completion.category.bg'] }};
background-color: {{ conf.colors.completion.category.bg }};
border-top: 1px solid
{{ color['completion.category.border.top'] }};
{{ conf.colors.completion.category.border.top }};
border-bottom: 1px solid
{{ color['completion.category.border.bottom'] }};
{{ conf.colors.completion.category.border.bottom }};
}
QTreeView::item:selected, QTreeView::item:selected:hover {
border-top: 1px solid
{{ color['completion.item.selected.border.top'] }};
{{ conf.colors.completion.item.selected.border.top }};
border-bottom: 1px solid
{{ color['completion.item.selected.border.bottom'] }};
background-color: {{ color['completion.item.selected.bg'] }};
{{ conf.colors.completion.item.selected.border.bottom }};
background-color: {{ conf.colors.completion.item.selected.bg }};
}
QTreeView:item::hover {
@ -85,14 +85,14 @@ class CompletionView(QTreeView):
}
QTreeView QScrollBar {
width: {{ config.get('completion', 'scrollbar-width') }}px;
background: {{ color['completion.scrollbar.bg'] }};
width: {{ conf.completion.scrollbar.width }}px;
background: {{ conf.colors.completion.scrollbar.bg }};
}
QTreeView QScrollBar::handle {
background: {{ color['completion.scrollbar.fg'] }};
border: {{ config.get('completion', 'scrollbar-padding') }}px solid
{{ color['completion.scrollbar.bg'] }};
background: {{ conf.colors.completion.scrollbar.fg }};
border: {{ conf.completion.scrollbar.padding }}px solid
{{ conf.colors.completion.scrollbar.bg }};
min-height: 10px;
}
@ -109,14 +109,14 @@ class CompletionView(QTreeView):
super().__init__(parent)
self.pattern = ''
self._win_id = win_id
objreg.get('config').changed.connect(self._on_config_changed)
config.instance.changed.connect(self._on_config_changed)
self._active = False
self._delegate = completiondelegate.CompletionItemDelegate(self)
self.setItemDelegate(self._delegate)
self.setStyle(QStyleFactory.create('Fusion'))
style.set_register_stylesheet(self)
config.set_register_stylesheet(self)
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self.setHeaderHidden(True)
self.setAlternatingRowColors(True)
@ -139,11 +139,9 @@ class CompletionView(QTreeView):
def __repr__(self):
return utils.get_repr(self)
@pyqtSlot(str, str)
def _on_config_changed(self, section, option):
if section != 'completion':
return
if option in ['height', 'shrink']:
@pyqtSlot(str)
def _on_config_changed(self, option):
if option in ['completion.height', 'completion.shrink']:
self.update_geometry.emit()
def _resize_columns(self):
@ -262,9 +260,9 @@ class CompletionView(QTreeView):
count = self.model().count()
if count == 0:
self.hide()
elif count == 1 and config.get('completion', 'quick-complete'):
elif count == 1 and config.val.completion.quick:
self.hide()
elif config.get('completion', 'show') == 'auto':
elif config.val.completion.show == 'auto':
self.show()
def set_model(self, model):
@ -306,7 +304,7 @@ class CompletionView(QTreeView):
self._maybe_show()
def _maybe_show(self):
if (config.get('completion', 'show') == 'always' and
if (config.val.completion.show == 'always' and
self.model().count() > 0):
self.show()
else:
@ -314,7 +312,7 @@ class CompletionView(QTreeView):
def _maybe_update_geometry(self):
"""Emit the update_geometry signal if the config says so."""
if config.get('completion', 'shrink'):
if config.val.completion.shrink:
self.update_geometry.emit()
@pyqtSlot()
@ -329,14 +327,14 @@ class CompletionView(QTreeView):
def sizeHint(self):
"""Get the completion size according to the config."""
# Get the configured height/percentage.
confheight = str(config.get('completion', 'height'))
confheight = str(config.val.completion.height)
if confheight.endswith('%'):
perc = int(confheight.rstrip('%'))
height = self.window().height() * perc / 100
else:
height = int(confheight)
# Shrink to content size if needed and shrinking is enabled
if config.get('completion', 'shrink'):
if config.val.completion.shrink:
contents_height = (
self.viewportSizeHint().height() +
self.horizontalScrollBar().sizeHint().height())

View File

@ -20,78 +20,63 @@
"""Functions that return config-related completion models."""
from qutebrowser.config import configdata, configexc
from qutebrowser.completion.models import completionmodel, listcategory
from qutebrowser.utils import objreg
from qutebrowser.completion.models import completionmodel, listcategory, util
from qutebrowser.commands import cmdutils
def section():
"""A CompletionModel filled with settings sections."""
def option(*, info):
"""A CompletionModel filled with settings and their descriptions."""
model = completionmodel.CompletionModel(column_widths=(20, 70, 10))
sections = ((name, configdata.SECTION_DESC[name].splitlines()[0].strip())
for name in configdata.DATA)
model.add_category(listcategory.ListCategory("Sections", sorted(sections)))
options = ((opt.name, opt.description, info.config.get_str(opt.name))
for opt in configdata.DATA.values())
model.add_category(listcategory.ListCategory("Options", sorted(options)))
return model
def option(sectname):
"""A CompletionModel filled with settings and their descriptions.
Args:
sectname: The name of the config section this model shows.
"""
model = completionmodel.CompletionModel(column_widths=(20, 70, 10))
try:
sectdata = configdata.DATA[sectname]
except KeyError:
return None
options = []
for name in sectdata:
try:
desc = sectdata.descriptions[name]
except (KeyError, AttributeError):
# Some stuff (especially ValueList items) don't have a
# description.
desc = ""
else:
desc = desc.splitlines()[0]
config = objreg.get('config')
val = config.get(sectname, name, raw=True)
options.append((name, desc, val))
model.add_category(listcategory.ListCategory(sectname, sorted(options)))
return model
def value(sectname, optname):
def value(optname, *_values, info):
"""A CompletionModel filled with setting values.
Args:
sectname: The name of the config section this model shows.
optname: The name of the config option this model shows.
_values: The values already provided on the command line.
info: A CompletionInfo instance.
"""
model = completionmodel.CompletionModel(column_widths=(20, 70, 10))
config = objreg.get('config')
model = completionmodel.CompletionModel(column_widths=(30, 70, 0))
try:
current = config.get(sectname, optname, raw=True) or '""'
except (configexc.NoSectionError, configexc.NoOptionError):
current = info.config.get_str(optname) or '""'
except configexc.NoOptionError:
return None
default = configdata.DATA[sectname][optname].default() or '""'
if hasattr(configdata.DATA[sectname], 'valtype'):
# Same type for all values (ValueList)
vals = configdata.DATA[sectname].valtype.complete()
else:
if optname is None:
raise ValueError("optname may only be None for ValueList "
"sections, but {} is not!".format(sectname))
# Different type for each value (KeyValue)
vals = configdata.DATA[sectname][optname].typ.complete()
opt = info.config.get_opt(optname)
default = opt.typ.to_str(opt.default)
cur_cat = listcategory.ListCategory("Current/Default",
[(current, "Current value"), (default, "Default value")])
model.add_category(cur_cat)
vals = opt.typ.complete()
if vals is not None:
model.add_category(listcategory.ListCategory("Completions",
sorted(vals)))
return model
def bind(key, *, info):
"""A CompletionModel filled with all bindable commands and descriptions.
Args:
key: the key being bound.
"""
model = completionmodel.CompletionModel(column_widths=(20, 60, 20))
cmd_text = info.keyconf.get_command(key, 'normal')
if cmd_text:
cmd_name = cmd_text.split(' ')[0]
cmd = cmdutils.cmd_dict.get(cmd_name)
data = [(cmd_text, cmd.desc, key)]
model.add_category(listcategory.ListCategory("Current", data))
cmdlist = util.get_cmd_completions(info, include_hidden=True,
include_aliases=True)
model.add_category(listcategory.ListCategory("Commands", cmdlist))
return model

View File

@ -38,9 +38,9 @@ class HistoryCategory(QSqlQueryModel):
self.name = "History"
# replace ' in timestamp-format to avoid breaking the query
timestamp_format = config.val.completion.timestamp_format
timefmt = ("strftime('{}', last_atime, 'unixepoch', 'localtime')"
.format(config.get('completion', 'timestamp-format')
.replace("'", "`")))
.format(timestamp_format.replace("'", "`")))
self._query = sql.Query(' '.join([
"SELECT url, title, {}".format(timefmt),
@ -58,7 +58,7 @@ class HistoryCategory(QSqlQueryModel):
def _atime_expr(self):
"""If max_items is set, return an expression to limit the query."""
max_items = config.get('completion', 'web-history-max-items')
max_items = config.val.completion.web_history_max_items
# HistoryCategory should not be added to the completion in that case.
assert max_items != 0

View File

@ -19,46 +19,35 @@
"""Functions that return miscellaneous completion models."""
from qutebrowser.config import config, configdata
from qutebrowser.config import configdata
from qutebrowser.utils import objreg, log
from qutebrowser.commands import cmdutils
from qutebrowser.completion.models import completionmodel, listcategory
from qutebrowser.completion.models import completionmodel, listcategory, util
def command():
def command(*, info):
"""A CompletionModel filled with non-hidden commands and descriptions."""
model = completionmodel.CompletionModel(column_widths=(20, 60, 20))
cmdlist = _get_cmd_completions(include_aliases=True, include_hidden=False)
cmdlist = util.get_cmd_completions(info, include_aliases=True,
include_hidden=False)
model.add_category(listcategory.ListCategory("Commands", cmdlist))
return model
def helptopic():
def helptopic(*, info):
"""A CompletionModel filled with help topics."""
model = completionmodel.CompletionModel()
cmdlist = _get_cmd_completions(include_aliases=False, include_hidden=True,
prefix=':')
settings = []
for sectname, sectdata in configdata.DATA.items():
for optname in sectdata:
try:
desc = sectdata.descriptions[optname]
except (KeyError, AttributeError):
# Some stuff (especially ValueList items) don't have a
# description.
desc = ""
else:
desc = desc.splitlines()[0]
name = '{}->{}'.format(sectname, optname)
settings.append((name, desc))
cmdlist = util.get_cmd_completions(info, include_aliases=False,
include_hidden=True, prefix=':')
settings = ((opt.name, opt.description)
for opt in configdata.DATA.values())
model.add_category(listcategory.ListCategory("Commands", cmdlist))
model.add_category(listcategory.ListCategory("Settings", sorted(settings)))
return model
def quickmark():
def quickmark(*, info=None): # pylint: disable=unused-argument
"""A CompletionModel filled with all quickmarks."""
def delete(data):
"""Delete a quickmark from the completion menu."""
@ -74,7 +63,7 @@ def quickmark():
return model
def bookmark():
def bookmark(*, info=None): # pylint: disable=unused-argument
"""A CompletionModel filled with all bookmarks."""
def delete(data):
"""Delete a bookmark from the completion menu."""
@ -90,7 +79,7 @@ def bookmark():
return model
def session():
def session(*, info=None): # pylint: disable=unused-argument
"""A CompletionModel filled with session names."""
model = completionmodel.CompletionModel()
try:
@ -103,7 +92,7 @@ def session():
return model
def buffer():
def buffer(*, info=None): # pylint: disable=unused-argument
"""A model to complete on open tabs across all windows.
Used for switching the buffer command.
@ -133,51 +122,3 @@ def buffer():
model.add_category(cat)
return model
def bind(key):
"""A CompletionModel filled with all bindable commands and descriptions.
Args:
key: the key being bound.
"""
model = completionmodel.CompletionModel(column_widths=(20, 60, 20))
cmd_text = objreg.get('key-config').get_bindings_for('normal').get(key)
if cmd_text:
cmd_name = cmd_text.split(' ')[0]
cmd = cmdutils.cmd_dict.get(cmd_name)
data = [(cmd_text, cmd.desc, key)]
model.add_category(listcategory.ListCategory("Current", data))
cmdlist = _get_cmd_completions(include_hidden=True, include_aliases=True)
model.add_category(listcategory.ListCategory("Commands", cmdlist))
return model
def _get_cmd_completions(include_hidden, include_aliases, prefix=''):
"""Get a list of completions info for commands, sorted by name.
Args:
include_hidden: True to include commands annotated with hide=True.
include_aliases: True to include command aliases.
prefix: String to append to the command name.
Return: A list of tuples of form (name, description, bindings).
"""
assert cmdutils.cmd_dict
cmdlist = []
cmd_to_keys = objreg.get('key-config').get_reverse_bindings_for('normal')
for obj in set(cmdutils.cmd_dict.values()):
hide_debug = obj.debug and not objreg.get('args').debug
hide_hidden = obj.hide and not include_hidden
if not (hide_debug or hide_hidden or obj.deprecated):
bindings = ', '.join(cmd_to_keys.get(obj.name, []))
cmdlist.append((prefix + obj.name, obj.desc, bindings))
if include_aliases:
for name, cmd in config.section('aliases').items():
bindings = ', '.join(cmd_to_keys.get(name, []))
cmdlist.append((name, "Alias for '{}'".format(cmd), bindings))
return sorted(cmdlist)

View File

@ -22,7 +22,6 @@
from qutebrowser.completion.models import (completionmodel, listcategory,
histcategory)
from qutebrowser.utils import log, objreg
from qutebrowser.config import config
_URLCOL = 0
@ -50,7 +49,7 @@ def _delete_quickmark(data):
quickmark_manager.delete(name)
def url():
def url(*, info):
"""A model which combines bookmarks, quickmarks and web history URLs.
Used for the `open` command.
@ -66,7 +65,7 @@ def url():
model.add_category(listcategory.ListCategory(
'Bookmarks', bookmarks, delete_func=_delete_bookmark))
if config.get('completion', 'web-history-max-items') != 0:
if info.config.get('completion.web_history_max_items') != 0:
hist_cat = histcategory.HistoryCategory(delete_func=_delete_history)
model.add_category(hist_cat)
return model

View File

@ -0,0 +1,52 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2017 Ryan Roden-Corrent (rcorre) <ryan@rcorre.net>
#
# 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/>.
"""Utility functions for completion models."""
from qutebrowser.utils import objreg
from qutebrowser.commands import cmdutils
def get_cmd_completions(info, include_hidden, include_aliases, prefix=''):
"""Get a list of completions info for commands, sorted by name.
Args:
info: The CompletionInfo.
include_hidden: True to include commands annotated with hide=True.
include_aliases: True to include command aliases.
prefix: String to append to the command name.
Return: A list of tuples of form (name, description, bindings).
"""
assert cmdutils.cmd_dict
cmdlist = []
cmd_to_keys = info.keyconf.get_reverse_bindings_for('normal')
for obj in set(cmdutils.cmd_dict.values()):
hide_debug = obj.debug and not objreg.get('args').debug
hide_hidden = obj.hide and not include_hidden
if not (hide_debug or hide_hidden or obj.deprecated):
bindings = ', '.join(cmd_to_keys.get(obj.name, []))
cmdlist.append((prefix + obj.name, obj.desc, bindings))
if include_aliases:
for name, cmd in info.config.get('aliases').items():
bindings = ', '.join(cmd_to_keys.get(name, []))
cmdlist.append((name, "Alias for '{}'".format(cmd), bindings))
return sorted(cmdlist)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,760 @@
# 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/>.
"""Code to show a diff of the legacy config format."""
import difflib
import os.path
import pygments
import pygments.lexers
import pygments.formatters
from qutebrowser.utils import standarddir
OLD_CONF = """
[general]
ignore-case = smart
startpage = https://start.duckduckgo.com
yank-ignored-url-parameters = ref,utm_source,utm_medium,utm_campaign,utm_term,utm_content
default-open-dispatcher =
default-page = ${startpage}
auto-search = naive
auto-save-config = true
auto-save-interval = 15000
editor = gvim -f "{}"
editor-encoding = utf-8
private-browsing = false
developer-extras = false
print-element-backgrounds = true
xss-auditing = false
default-encoding = iso-8859-1
new-instance-open-target = tab
new-instance-open-target.window = last-focused
log-javascript-console = debug
save-session = false
session-default-name =
url-incdec-segments = path,query
[ui]
history-session-interval = 30
zoom-levels = 25%,33%,50%,67%,75%,90%,100%,110%,125%,150%,175%,200%,250%,300%,400%,500%
default-zoom = 100%
downloads-position = top
status-position = bottom
message-timeout = 2000
message-unfocused = false
confirm-quit = never
zoom-text-only = false
frame-flattening = false
user-stylesheet =
hide-scrollbar = true
smooth-scrolling = false
remove-finished-downloads = -1
hide-statusbar = false
statusbar-padding = 1,1,0,0
window-title-format = {perc}{title}{title_sep}qutebrowser
modal-js-dialog = false
hide-wayland-decoration = false
keyhint-blacklist =
keyhint-delay = 500
prompt-radius = 8
prompt-filebrowser = true
[network]
do-not-track = true
accept-language = en-US,en
referer-header = same-domain
user-agent =
proxy = system
proxy-dns-requests = true
ssl-strict = ask
dns-prefetch = true
custom-headers =
netrc-file =
[completion]
show = always
download-path-suggestion = path
timestamp-format = %Y-%m-%d
height = 50%
cmd-history-max-items = 100
web-history-max-items = -1
quick-complete = true
shrink = false
scrollbar-width = 12
scrollbar-padding = 2
[input]
timeout = 500
partial-timeout = 5000
insert-mode-on-plugins = false
auto-leave-insert-mode = true
auto-insert-mode = false
forward-unbound-keys = auto
spatial-navigation = false
links-included-in-focus-chain = true
rocker-gestures = false
mouse-zoom-divider = 512
[tabs]
background-tabs = false
select-on-remove = next
new-tab-position = next
new-tab-position-explicit = last
last-close = ignore
show = always
show-switching-delay = 800
wrap = true
movable = true
close-mouse-button = middle
position = top
show-favicons = true
favicon-scale = 1.0
width = 20%
pinned-width = 43
indicator-width = 3
tabs-are-windows = false
title-format = {index}: {title}
title-format-pinned = {index}
title-alignment = left
mousewheel-tab-switching = true
padding = 0,0,5,5
indicator-padding = 2,2,0,4
[storage]
download-directory =
prompt-download-directory = true
remember-download-directory = true
maximum-pages-in-cache = 0
offline-web-application-cache = true
local-storage = true
cache-size =
[content]
allow-images = true
allow-javascript = true
allow-plugins = false
webgl = true
hyperlink-auditing = false
geolocation = ask
notifications = ask
media-capture = ask
javascript-can-open-windows-automatically = false
javascript-can-close-windows = false
javascript-can-access-clipboard = false
ignore-javascript-prompt = false
ignore-javascript-alert = false
local-content-can-access-remote-urls = false
local-content-can-access-file-urls = true
cookies-accept = no-3rdparty
cookies-store = true
host-block-lists = https://www.malwaredomainlist.com/hostslist/hosts.txt,http://someonewhocares.org/hosts/hosts,http://winhelp2002.mvps.org/hosts.zip,http://malwaredomains.lehigh.edu/files/justdomains.zip,https://pgl.yoyo.org/adservers/serverlist.php?hostformat=hosts&mimetype=plaintext
host-blocking-enabled = true
host-blocking-whitelist = piwik.org
enable-pdfjs = false
[hints]
border = 1px solid #E3BE23
mode = letter
chars = asdfghjkl
min-chars = 1
scatter = true
uppercase = false
dictionary = /usr/share/dict/words
auto-follow = unique-match
auto-follow-timeout = 0
next-regexes = \\bnext\\b,\\bmore\\b,\\bnewer\\b,\\b[>\u2192\u226b]\\b,\\b(>>|\xbb)\\b,\\bcontinue\\b
prev-regexes = \\bprev(ious)?\\b,\\bback\\b,\\bolder\\b,\\b[<\u2190\u226a]\\b,\\b(<<|\xab)\\b
find-implementation = python
hide-unmatched-rapid-hints = true
[searchengines]
DEFAULT = https://duckduckgo.com/?q={}
[aliases]
[colors]
completion.fg = white
completion.bg = #333333
completion.alternate-bg = #444444
completion.category.fg = white
completion.category.bg = qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #888888, stop:1 #505050)
completion.category.border.top = black
completion.category.border.bottom = ${completion.category.border.top}
completion.item.selected.fg = black
completion.item.selected.bg = #e8c000
completion.item.selected.border.top = #bbbb00
completion.item.selected.border.bottom = ${completion.item.selected.border.top}
completion.match.fg = #ff4444
completion.scrollbar.fg = ${completion.fg}
completion.scrollbar.bg = ${completion.bg}
statusbar.fg = white
statusbar.bg = black
statusbar.fg.private = ${statusbar.fg}
statusbar.bg.private = #666666
statusbar.fg.insert = ${statusbar.fg}
statusbar.bg.insert = darkgreen
statusbar.fg.command = ${statusbar.fg}
statusbar.bg.command = ${statusbar.bg}
statusbar.fg.command.private = ${statusbar.fg.private}
statusbar.bg.command.private = ${statusbar.bg.private}
statusbar.fg.caret = ${statusbar.fg}
statusbar.bg.caret = purple
statusbar.fg.caret-selection = ${statusbar.fg}
statusbar.bg.caret-selection = #a12dff
statusbar.progress.bg = white
statusbar.url.fg = ${statusbar.fg}
statusbar.url.fg.success = white
statusbar.url.fg.success.https = lime
statusbar.url.fg.error = orange
statusbar.url.fg.warn = yellow
statusbar.url.fg.hover = aqua
tabs.fg.odd = white
tabs.bg.odd = grey
tabs.fg.even = white
tabs.bg.even = darkgrey
tabs.fg.selected.odd = white
tabs.bg.selected.odd = black
tabs.fg.selected.even = ${tabs.fg.selected.odd}
tabs.bg.selected.even = ${tabs.bg.selected.odd}
tabs.bg.bar = #555555
tabs.indicator.start = #0000aa
tabs.indicator.stop = #00aa00
tabs.indicator.error = #ff0000
tabs.indicator.system = rgb
hints.fg = black
hints.bg = qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 rgba(255, 247, 133, 0.8), stop:1 rgba(255, 197, 66, 0.8))
hints.fg.match = green
downloads.bg.bar = black
downloads.fg.start = white
downloads.bg.start = #0000aa
downloads.fg.stop = ${downloads.fg.start}
downloads.bg.stop = #00aa00
downloads.fg.system = rgb
downloads.bg.system = rgb
downloads.fg.error = white
downloads.bg.error = red
webpage.bg = white
keyhint.fg = #FFFFFF
keyhint.fg.suffix = #FFFF00
keyhint.bg = rgba(0, 0, 0, 80%)
messages.fg.error = white
messages.bg.error = red
messages.border.error = #bb0000
messages.fg.warning = white
messages.bg.warning = darkorange
messages.border.warning = #d47300
messages.fg.info = white
messages.bg.info = black
messages.border.info = #333333
prompts.fg = white
prompts.bg = darkblue
prompts.selected.bg = #308cc6
[fonts]
_monospace = xos4 Terminus, Terminus, Monospace, "DejaVu Sans Mono", Monaco, "Bitstream Vera Sans Mono", "Andale Mono", "Courier New", Courier, "Liberation Mono", monospace, Fixed, Consolas, Terminal
completion = 8pt ${_monospace}
completion.category = bold ${completion}
tabbar = 8pt ${_monospace}
statusbar = 8pt ${_monospace}
downloads = 8pt ${_monospace}
hints = bold 13px ${_monospace}
debug-console = 8pt ${_monospace}
web-family-standard =
web-family-fixed =
web-family-serif =
web-family-sans-serif =
web-family-cursive =
web-family-fantasy =
web-size-minimum = 0
web-size-minimum-logical = 6
web-size-default = 16
web-size-default-fixed = 13
keyhint = 8pt ${_monospace}
messages.error = 8pt ${_monospace}
messages.warning = 8pt ${_monospace}
messages.info = 8pt ${_monospace}
prompts = 8pt sans-serif
"""
OLD_KEYS_CONF = """
[!normal]
leave-mode
<escape>
<ctrl-[>
[normal]
clear-keychain ;; search ;; fullscreen --leave
<escape>
<ctrl-[>
set-cmd-text -s :open
o
set-cmd-text :open {url:pretty}
go
set-cmd-text -s :open -t
O
set-cmd-text :open -t -i {url:pretty}
gO
set-cmd-text -s :open -b
xo
set-cmd-text :open -b -i {url:pretty}
xO
set-cmd-text -s :open -w
wo
set-cmd-text :open -w {url:pretty}
wO
set-cmd-text /
/
set-cmd-text ?
?
set-cmd-text :
:
open -t
ga
<ctrl-t>
open -w
<ctrl-n>
tab-close
d
<ctrl-w>
tab-close -o
D
tab-only
co
tab-focus
T
tab-move
gm
tab-move -
gl
tab-move +
gr
tab-next
J
<ctrl-pgdown>
tab-prev
K
<ctrl-pgup>
tab-clone
gC
reload
r
<f5>
reload -f
R
<ctrl-f5>
back
H
<back>
back -t
th
back -w
wh
forward
L
<forward>
forward -t
tl
forward -w
wl
fullscreen
<f11>
hint
f
hint all tab
F
hint all window
wf
hint all tab-bg
;b
hint all tab-fg
;f
hint all hover
;h
hint images
;i
hint images tab
;I
hint links fill :open {hint-url}
;o
hint links fill :open -t -i {hint-url}
;O
hint links yank
;y
hint links yank-primary
;Y
hint --rapid links tab-bg
;r
hint --rapid links window
;R
hint links download
;d
hint inputs
;t
scroll left
h
scroll down
j
scroll up
k
scroll right
l
undo
u
<ctrl-shift-t>
scroll-perc 0
gg
scroll-perc
G
search-next
n
search-prev
N
enter-mode insert
i
enter-mode caret
v
enter-mode set_mark
`
enter-mode jump_mark
'
yank
yy
yank -s
yY
yank title
yt
yank title -s
yT
yank domain
yd
yank domain -s
yD
yank pretty-url
yp
yank pretty-url -s
yP
open -- {clipboard}
pp
open -- {primary}
pP
open -t -- {clipboard}
Pp
open -t -- {primary}
PP
open -w -- {clipboard}
wp
open -w -- {primary}
wP
quickmark-save
m
set-cmd-text -s :quickmark-load
b
set-cmd-text -s :quickmark-load -t
B
set-cmd-text -s :quickmark-load -w
wb
bookmark-add
M
set-cmd-text -s :bookmark-load
gb
set-cmd-text -s :bookmark-load -t
gB
set-cmd-text -s :bookmark-load -w
wB
save
sf
set-cmd-text -s :set
ss
set-cmd-text -s :set -t
sl
set-cmd-text -s :bind
sk
zoom-out
-
zoom-in
+
zoom
=
navigate prev
[[
navigate next
]]
navigate prev -t
{{
navigate next -t
}}
navigate up
gu
navigate up -t
gU
navigate increment
<ctrl-a>
navigate decrement
<ctrl-x>
inspector
wi
download
gd
download-cancel
ad
download-clear
cd
view-source
gf
set-cmd-text -s :buffer
gt
tab-focus last
<ctrl-tab>
<ctrl-6>
<ctrl-^>
enter-mode passthrough
<ctrl-v>
quit
<ctrl-q>
ZQ
wq
ZZ
scroll-page 0 1
<ctrl-f>
scroll-page 0 -1
<ctrl-b>
scroll-page 0 0.5
<ctrl-d>
scroll-page 0 -0.5
<ctrl-u>
tab-focus 1
<alt-1>
g0
g^
tab-focus 2
<alt-2>
tab-focus 3
<alt-3>
tab-focus 4
<alt-4>
tab-focus 5
<alt-5>
tab-focus 6
<alt-6>
tab-focus 7
<alt-7>
tab-focus 8
<alt-8>
tab-focus -1
<alt-9>
g$
home
<ctrl-h>
stop
<ctrl-s>
print
<ctrl-alt-p>
open qute://settings
Ss
follow-selected
<return>
<ctrl-m>
<ctrl-j>
<shift-return>
<enter>
<shift-enter>
follow-selected -t
<ctrl-return>
<ctrl-enter>
repeat-command
.
tab-pin
<ctrl-p>
record-macro
q
run-macro
@
[insert]
open-editor
<ctrl-e>
insert-text {primary}
<shift-ins>
[hint]
follow-hint
<return>
<ctrl-m>
<ctrl-j>
<shift-return>
<enter>
<shift-enter>
hint --rapid links tab-bg
<ctrl-r>
hint links
<ctrl-f>
hint all tab-bg
<ctrl-b>
[passthrough]
[command]
command-history-prev
<ctrl-p>
command-history-next
<ctrl-n>
completion-item-focus prev
<shift-tab>
<up>
completion-item-focus next
<tab>
<down>
completion-item-focus next-category
<ctrl-tab>
completion-item-focus prev-category
<ctrl-shift-tab>
completion-item-del
<ctrl-d>
command-accept
<return>
<ctrl-m>
<ctrl-j>
<shift-return>
<enter>
<shift-enter>
[prompt]
prompt-accept
<return>
<ctrl-m>
<ctrl-j>
<shift-return>
<enter>
<shift-enter>
prompt-accept yes
y
prompt-accept no
n
prompt-open-download
<ctrl-x>
prompt-item-focus prev
<shift-tab>
<up>
prompt-item-focus next
<tab>
<down>
[command,prompt]
rl-backward-char
<ctrl-b>
rl-forward-char
<ctrl-f>
rl-backward-word
<alt-b>
rl-forward-word
<alt-f>
rl-beginning-of-line
<ctrl-a>
rl-end-of-line
<ctrl-e>
rl-unix-line-discard
<ctrl-u>
rl-kill-line
<ctrl-k>
rl-kill-word
<alt-d>
rl-unix-word-rubout
<ctrl-w>
rl-backward-kill-word
<alt-backspace>
rl-yank
<ctrl-y>
rl-delete-char
<ctrl-?>
rl-backward-delete-char
<ctrl-h>
[caret]
toggle-selection
v
<space>
drop-selection
<ctrl-space>
enter-mode normal
c
move-to-next-line
j
move-to-prev-line
k
move-to-next-char
l
move-to-prev-char
h
move-to-end-of-word
e
move-to-next-word
w
move-to-prev-word
b
move-to-start-of-next-block
]
move-to-start-of-prev-block
[
move-to-end-of-next-block
}
move-to-end-of-prev-block
{
move-to-start-of-line
0
move-to-end-of-line
$
move-to-start-of-document
gg
move-to-end-of-document
G
yank selection -s
Y
yank selection
y
<return>
<ctrl-m>
<ctrl-j>
<shift-return>
<enter>
<shift-enter>
scroll left
H
scroll down
J
scroll up
K
scroll right
L
"""
def get_diff():
"""Get a HTML diff for the old config files."""
old_conf_lines = []
old_key_lines = []
for filename, dest in [('qutebrowser.conf', old_conf_lines),
('keys.conf', old_key_lines)]:
path = os.path.join(standarddir.config(), filename)
with open(path, 'r', encoding='utf-8') as f:
for line in f:
if not line.strip() or line.startswith('#'):
continue
dest.append(line.rstrip())
conf_delta = difflib.unified_diff(OLD_CONF.lstrip().splitlines(),
old_conf_lines)
key_delta = difflib.unified_diff(OLD_KEYS_CONF.lstrip().splitlines(),
old_key_lines)
conf_diff = '\n'.join(conf_delta)
key_diff = '\n'.join(key_delta)
# pylint: disable=no-member
# WORKAROUND for https://bitbucket.org/logilab/pylint/issue/491/
lexer = pygments.lexers.DiffLexer()
formatter = pygments.formatters.HtmlFormatter(
full=True, linenos='table',
title='Config diff')
# pylint: enable=no-member
return pygments.highlight(conf_diff + key_diff, lexer, formatter)

View File

@ -41,46 +41,79 @@ class ValidationError(Error):
"""Raised when a value for a config type was invalid.
Attributes:
section: Section in which the error occurred (added when catching and
re-raising the exception).
option: Option in which the error occurred.
value: Config value that triggered the error.
msg: Additional error message.
"""
def __init__(self, value, msg):
super().__init__("Invalid value '{}' - {}".format(value, msg))
self.section = None
self.option = None
class NoSectionError(Error):
class KeybindingError(Error):
"""Raised when no section matches a requested option."""
"""Raised for issues with keybindings."""
def __init__(self, section):
super().__init__("Section {!r} does not exist!".format(section))
self.section = section
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."""
def __init__(self, option, section):
super().__init__("No option {!r} in section {!r}".format(
option, section))
def __init__(self, option):
super().__init__("No option {!r}".format(option))
self.option = option
self.section = section
class InterpolationSyntaxError(Error):
class ConfigErrorDesc:
"""Raised when the source text contains invalid syntax.
"""A description of an error happening while reading the config.
Current implementation raises this exception when the source text into
which substitutions are made does not conform to the required syntax.
Attributes:
text: The text to show.
exception: The exception which happened.
traceback: The formatted traceback of the exception.
"""
def __init__(self, option, section, msg):
super().__init__(msg)
self.option = option
self.section = section
def __init__(self, text, exception, traceback=None):
self.text = text
self.exception = exception
self.traceback = traceback
class ConfigFileErrors(Error):
"""Raised when multiple errors occurred inside the config."""
def __init__(self, basename, errors):
super().__init__("Errors occurred while reading {}".format(basename))
self.basename = basename
self.errors = errors
def to_html(self):
"""Get the error texts as a HTML snippet."""
from qutebrowser.utils import jinja
template = jinja.environment.from_string("""
Errors occurred while reading {{ basename }}:
<ul>
{% for error in errors %}
<li>
<b>{{ error.text }}</b>: {{ error.exception }}
{% if error.traceback != none %}
<pre>
""".rstrip() + "\n{{ error.traceback }}" + """
</pre>
{% endif %}
</li>
{% endfor %}
</ul>
""")
return template.render(basename=self.basename, errors=self.errors)

View File

@ -0,0 +1,235 @@
# 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/>.
"""Configuration files residing on disk."""
import types
import os.path
import textwrap
import traceback
import configparser
import contextlib
import yaml
from PyQt5.QtCore import QSettings
from qutebrowser.config import configexc
from qutebrowser.utils import objreg, standarddir, utils, qtutils
class StateConfig(configparser.ConfigParser):
"""The "state" file saving various application state."""
def __init__(self):
super().__init__()
save_manager = objreg.get('save-manager')
self._filename = os.path.join(standarddir.data(), 'state')
self.read(self._filename, encoding='utf-8')
for sect in ['general', 'geometry']:
try:
self.add_section(sect)
except configparser.DuplicateSectionError:
pass
# See commit a98060e020a4ba83b663813a4b9404edb47f28ad.
self['general'].pop('fooled', None)
save_manager.add_saveable('state-config', self._save)
def _save(self):
"""Save the state file to the configured location."""
with open(self._filename, 'w', encoding='utf-8') as f:
self.write(f)
class YamlConfig:
"""A config stored on disk as YAML file.
Class attributes:
VERSION: The current version number of the config file.
"""
VERSION = 1
def __init__(self):
save_manager = objreg.get('save-manager')
self._filename = os.path.join(standarddir.config(auto=True),
'autoconfig.yml')
save_manager.add_saveable('yaml-config', self._save)
self.values = {}
def _save(self):
"""Save the changed settings to the YAML file."""
data = {'config_version': self.VERSION, 'global': self.values}
with qtutils.savefile_open(self._filename) as f:
f.write(textwrap.dedent("""
# DO NOT edit this file by hand, qutebrowser will overwrite it.
# Instead, create a config.py - see :help for details.
""".lstrip('\n')))
utils.yaml_dump(data, f)
def load(self):
"""Load self.values from the configured YAML file."""
try:
with open(self._filename, 'r', encoding='utf-8') as f:
yaml_data = utils.yaml_load(f)
except FileNotFoundError:
return
except OSError as e:
desc = configexc.ConfigErrorDesc("While reading", e)
raise configexc.ConfigFileErrors('autoconfig.yml', [desc])
except yaml.YAMLError as e:
desc = configexc.ConfigErrorDesc("While parsing", e)
raise configexc.ConfigFileErrors('autoconfig.yml', [desc])
try:
global_obj = yaml_data['global']
except KeyError:
desc = configexc.ConfigErrorDesc(
"While loading data",
"Toplevel object does not contain 'global' key")
raise configexc.ConfigFileErrors('autoconfig.yml', [desc])
except TypeError:
desc = configexc.ConfigErrorDesc("While loading data",
"Toplevel object is not a dict")
raise configexc.ConfigFileErrors('autoconfig.yml', [desc])
if not isinstance(global_obj, dict):
desc = configexc.ConfigErrorDesc(
"While loading data",
"'global' object is not a dict")
raise configexc.ConfigFileErrors('autoconfig.yml', [desc])
self.values = global_obj
class ConfigAPI:
"""Object which gets passed to config.py as "config" object.
This is a small wrapper over the Config object, but with more
straightforward method names (get/set call get_obj/set_obj) and a more
shallow API.
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.
"""
def __init__(self, config, keyconfig):
self._config = config
self._keyconfig = keyconfig
self.load_autoconfig = True
self.errors = []
@contextlib.contextmanager
def _handle_error(self, action, name):
try:
yield
except configexc.Error as e:
text = "While {} '{}'".format(action, name)
self.errors.append(configexc.ConfigErrorDesc(text, e))
def finalize(self):
"""Do work which needs to be done after reading config.py."""
self._config.update_mutables()
def get(self, name):
with self._handle_error('getting', name):
return self._config.get_obj(name)
def set(self, name, value):
with self._handle_error('setting', name):
self._config.set_obj(name, value)
def bind(self, key, command, *, mode, force=False):
with self._handle_error('binding', key):
self._keyconfig.bind(key, command, mode=mode, force=force)
def unbind(self, key, *, mode):
with self._handle_error('unbinding', key):
self._keyconfig.unbind(key, mode=mode)
def read_config_py(filename=None):
"""Read a config.py file."""
from qutebrowser.config import config
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)
module = types.ModuleType('config')
module.config = api
module.c = container
module.__file__ = filename
try:
with open(filename, mode='rb') as f:
source = f.read()
except OSError as e:
text = "Error while reading {}".format(basename)
desc = configexc.ConfigErrorDesc(text, e)
raise configexc.ConfigFileErrors(basename, [desc])
try:
code = compile(source, filename, 'exec')
except (ValueError, TypeError) as e:
# source contains NUL bytes
desc = configexc.ConfigErrorDesc("Error while compiling", e)
raise configexc.ConfigFileErrors(basename, [desc])
except SyntaxError as e:
desc = configexc.ConfigErrorDesc("Syntax Error", e,
traceback=traceback.format_exc())
raise configexc.ConfigFileErrors(basename, [desc])
try:
exec(code, module.__dict__)
except Exception as e:
api.errors.append(configexc.ConfigErrorDesc(
"Unhandled exception",
exception=e, traceback=traceback.format_exc()))
api.finalize()
return api
def init():
"""Initialize config storage not related to the main config."""
state = StateConfig()
objreg.register('state-config', state)
# Set the QSettings path to something like
# ~/.config/qutebrowser/qsettings/qutebrowser/qutebrowser.conf so it
# doesn't overwrite our config.
#
# This fixes one of the corruption issues here:
# https://github.com/qutebrowser/qutebrowser/issues/515
path = os.path.join(standarddir.config(auto=True), 'qsettings')
for fmt in [QSettings.NativeFormat, QSettings.IniFormat]:
QSettings.setPath(fmt, QSettings.UserScope, path)

File diff suppressed because it is too large Load Diff

View File

@ -1,20 +0,0 @@
# 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/>.
"""Parser for different configuration formats."""

View File

@ -1,74 +0,0 @@
# 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/>.
"""Parsers for INI-like config files, based on Python's ConfigParser."""
import os
import os.path
import configparser
from qutebrowser.utils import log, utils, qtutils
class ReadConfigParser(configparser.ConfigParser):
"""Our own ConfigParser subclass to read the main config.
Attributes:
_configdir: The directory to read the config from.
_fname: The filename of the config.
_configfile: The config file path.
"""
def __init__(self, configdir, fname):
"""Config constructor.
Args:
configdir: Directory to read the config from.
fname: Filename of the config file.
"""
super().__init__(interpolation=None, comment_prefixes='#')
self.optionxform = lambda opt: opt # be case-insensitive
self._configdir = configdir
self._fname = fname
self._configfile = os.path.join(self._configdir, fname)
if not os.path.isfile(self._configfile):
return
log.init.debug("Reading config from {}".format(self._configfile))
self.read(self._configfile, encoding='utf-8')
def __repr__(self):
return utils.get_repr(self, constructor=True,
configdir=self._configdir, fname=self._fname)
class ReadWriteConfigParser(ReadConfigParser):
"""ConfigParser subclass used for auxiliary config files."""
def save(self):
"""Save the config file."""
if self._configdir is None:
return
if not os.path.exists(self._configdir):
os.makedirs(self._configdir, 0o755)
log.destroy.debug("Saving config to {}".format(self._configfile))
with qtutils.savefile_open(self._configfile) as f:
self.write(f)

View File

@ -1,486 +0,0 @@
# 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/>.
"""Parser for the key configuration."""
import collections
import os.path
import itertools
import sys
from PyQt5.QtCore import pyqtSignal, QObject
from qutebrowser.config import configdata, textwrapper
from qutebrowser.commands import cmdutils, cmdexc
from qutebrowser.utils import (log, utils, qtutils, message, usertypes, objreg,
standarddir, error)
from qutebrowser.completion.models import miscmodels
def init(parent=None):
"""Read and save keybindings.
Args:
parent: The parent to use for the KeyConfigParser.
"""
args = objreg.get('args')
try:
key_config = KeyConfigParser(standarddir.config(), 'keys.conf',
args.relaxed_config, parent=parent)
except (KeyConfigError, UnicodeDecodeError) as e:
log.init.exception(e)
errstr = "Error while reading key config:\n"
if e.lineno is not None:
errstr += "In line {}: ".format(e.lineno)
error.handle_fatal_exc(e, args, "Error while reading key config!",
pre_text=errstr)
# We didn't really initialize much so far, so we just quit hard.
sys.exit(usertypes.Exit.err_key_config)
else:
objreg.register('key-config', key_config)
save_manager = objreg.get('save-manager')
filename = os.path.join(standarddir.config(), 'keys.conf')
save_manager.add_saveable(
'key-config', key_config.save, key_config.config_dirty,
config_opt=('general', 'auto-save-config'), filename=filename,
dirty=key_config.is_dirty)
class KeyConfigError(Exception):
"""Raised on errors with the key config.
Attributes:
lineno: The config line in which the exception occurred.
"""
def __init__(self, msg=None):
super().__init__(msg)
self.lineno = None
class DuplicateKeychainError(KeyConfigError):
"""Error raised when there's a duplicate key binding."""
def __init__(self, keychain):
super().__init__("Duplicate key chain {}!".format(keychain))
self.keychain = keychain
class KeyConfigParser(QObject):
"""Parser for the keybind config.
Attributes:
_configfile: The filename of the config or None.
_cur_section: The section currently being processed by _read().
_cur_command: The command currently being processed by _read().
is_dirty: Whether the config is currently dirty.
Class attributes:
UNBOUND_COMMAND: The special command used for unbound keybindings.
Signals:
changed: Emitted when the internal data has changed.
arg: Name of the mode which was changed.
config_dirty: Emitted when the config should be re-saved.
"""
changed = pyqtSignal(str)
config_dirty = pyqtSignal()
UNBOUND_COMMAND = '<unbound>'
def __init__(self, configdir, fname, relaxed=False, parent=None):
"""Constructor.
Args:
configdir: The directory to save the configs in.
fname: The filename of the config.
relaxed: If given, unknown commands are ignored.
"""
super().__init__(parent)
self.is_dirty = False
self._cur_section = None
self._cur_command = None
# Mapping of section name(s) to key binding -> command dicts.
self.keybindings = collections.OrderedDict()
self._configfile = os.path.join(configdir, fname)
if not os.path.exists(self._configfile):
self._load_default()
else:
self._read(relaxed)
self._load_default(only_new=True)
log.init.debug("Loaded bindings: {}".format(self.keybindings))
def __str__(self):
"""Get the config as string."""
lines = configdata.KEY_FIRST_COMMENT.strip('\n').splitlines()
lines.append('')
for sectname, sect in self.keybindings.items():
lines.append('[{}]'.format(sectname))
lines += self._str_section_desc(sectname)
lines.append('')
data = collections.OrderedDict()
for key, cmd in sect.items():
if cmd in data:
data[cmd].append(key)
else:
data[cmd] = [key]
for cmd, keys in data.items():
lines.append(cmd)
for k in keys:
lines.append(' ' * 4 + k)
lines.append('')
return '\n'.join(lines) + '\n'
def __repr__(self):
return utils.get_repr(self, constructor=True,
configfile=self._configfile)
def _str_section_desc(self, sectname):
"""Get the section description string for sectname."""
wrapper = textwrapper.TextWrapper()
lines = []
try:
seclines = configdata.KEY_SECTION_DESC[sectname].splitlines()
except KeyError:
return []
else:
for secline in seclines:
if 'http://' in secline or 'https://' in secline:
lines.append('# ' + secline)
else:
lines += wrapper.wrap(secline)
return lines
def save(self):
"""Save the key config file."""
log.destroy.debug("Saving key config to {}".format(self._configfile))
try:
with qtutils.savefile_open(self._configfile,
encoding='utf-8') as f:
data = str(self)
f.write(data)
except OSError as e:
message.error("Could not save key config: {}".format(e))
@cmdutils.register(instance='key-config', maxsplit=1, no_cmd_split=True,
no_replace_variables=True)
@cmdutils.argument('command', completion=miscmodels.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`).
force: Rebind the key if it is already bound.
"""
if utils.is_special_key(key):
# <Ctrl-t>, <ctrl-T>, and <ctrl-t> should be considered equivalent
key = key.lower()
if command is None:
cmd = self.get_bindings_for(mode).get(key, None)
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
modenames = self._normalize_sectname(mode).split(',')
for m in modenames:
if m not in configdata.KEY_DATA:
raise cmdexc.CommandError("Invalid mode {}!".format(m))
try:
modes = [usertypes.KeyMode[m] for m in modenames]
self._validate_command(command, modes)
except KeyConfigError as e:
raise cmdexc.CommandError(str(e))
try:
self._add_binding(mode, key, command, force=force)
except DuplicateKeychainError as e:
raise cmdexc.CommandError("Duplicate keychain {} - use --force to "
"override!".format(str(e.keychain)))
except KeyConfigError as e:
raise cmdexc.CommandError(e)
for m in modenames:
self.changed.emit(m)
self._mark_config_dirty()
@cmdutils.register(instance='key-config')
def unbind(self, key, mode='normal'):
"""Unbind a keychain.
Args:
key: The keychain or special key (inside <...>) to unbind.
mode: A comma-separated list of modes to unbind the key in
(default: `normal`).
"""
if utils.is_special_key(key):
# <Ctrl-t>, <ctrl-T>, and <ctrl-t> should be considered equivalent
key = key.lower()
mode = self._normalize_sectname(mode)
for m in mode.split(','):
if m not in configdata.KEY_DATA:
raise cmdexc.CommandError("Invalid mode {}!".format(m))
try:
sect = self.keybindings[mode]
except KeyError:
raise cmdexc.CommandError("Can't find mode section '{}'!".format(
mode))
try:
del sect[key]
except KeyError:
raise cmdexc.CommandError("Can't find binding '{}' in section "
"'{}'!".format(key, mode))
else:
if key in itertools.chain.from_iterable(
configdata.KEY_DATA[mode].values()):
try:
self._add_binding(mode, key, self.UNBOUND_COMMAND)
except DuplicateKeychainError:
pass
for m in mode.split(','):
self.changed.emit(m)
self._mark_config_dirty()
def _normalize_sectname(self, s):
"""Normalize a section string like 'foo, bar,baz' to 'bar,baz,foo'."""
if s.startswith('!'):
inverted = True
s = s[1:]
else:
inverted = False
sections = ','.join(sorted(s.split(',')))
if inverted:
sections = '!' + sections
return sections
def _load_default(self, *, only_new=False):
"""Load the built-in default key bindings.
Args:
only_new: If set, only keybindings which are completely unused
(same command/key not bound) are added.
"""
# {'sectname': {'keychain1': 'command', 'keychain2': 'command'}, ...}
bindings_to_add = collections.OrderedDict()
mark_dirty = False
for sectname, sect in configdata.KEY_DATA.items():
sectname = self._normalize_sectname(sectname)
bindings_to_add[sectname] = collections.OrderedDict()
for command, keychains in sect.items():
for e in keychains:
if not only_new or self._is_new(sectname, command, e):
assert e not in bindings_to_add[sectname]
bindings_to_add[sectname][e] = command
mark_dirty = True
for sectname, sect in bindings_to_add.items():
if not sect:
if not only_new:
self.keybindings[sectname] = collections.OrderedDict()
else:
for keychain, command in sect.items():
self._add_binding(sectname, keychain, command)
self.changed.emit(sectname)
if mark_dirty:
self._mark_config_dirty()
def _is_new(self, sectname, command, keychain):
"""Check if a given binding is new.
A binding is considered new if both the command is not bound to any key
yet, and the key isn't used anywhere else in the same section.
"""
if utils.is_special_key(keychain):
keychain = keychain.lower()
try:
bindings = self.keybindings[sectname]
except KeyError:
return True
if keychain in bindings:
return False
else:
return command not in bindings.values()
def _read(self, relaxed=False):
"""Read the config file from disk and parse it.
Args:
relaxed: Ignore unknown commands.
"""
try:
with open(self._configfile, 'r', encoding='utf-8') as f:
for i, line in enumerate(f):
line = line.rstrip()
try:
if not line.strip() or line.startswith('#'):
continue
elif line.startswith('[') and line.endswith(']'):
sectname = line[1:-1]
self._cur_section = self._normalize_sectname(
sectname)
elif line.startswith((' ', '\t')):
line = line.strip()
self._read_keybinding(line)
else:
line = line.strip()
self._read_command(line)
except (KeyConfigError, cmdexc.CommandError) as e:
if relaxed:
continue
else:
e.lineno = i
raise
except OSError:
log.keyboard.exception("Failed to read key bindings!")
for sectname in self.keybindings:
self.changed.emit(sectname)
def _mark_config_dirty(self):
"""Mark the config as dirty."""
self.is_dirty = True
self.config_dirty.emit()
def _validate_command(self, line, modes=None):
"""Check if a given command is valid.
Args:
line: The commandline to validate.
modes: A list of modes to validate the commands for, or None.
"""
from qutebrowser.config import config
if line == self.UNBOUND_COMMAND:
return
commands = line.split(';;')
try:
first_cmd = commands[0].split(maxsplit=1)[0].strip()
cmd = cmdutils.cmd_dict[first_cmd]
if cmd.no_cmd_split:
commands = [line]
except (KeyError, IndexError):
pass
for cmd in commands:
if not cmd.strip():
raise KeyConfigError("Got empty command (line: {!r})!".format(
line))
commands = [c.split(maxsplit=1)[0].strip() for c in commands]
for cmd in commands:
aliases = config.section('aliases')
if cmd in cmdutils.cmd_dict:
cmdname = cmd
elif cmd in aliases:
cmdname = aliases[cmd].split(maxsplit=1)[0].strip()
else:
raise KeyConfigError("Invalid command '{}'!".format(cmd))
cmd_obj = cmdutils.cmd_dict[cmdname]
for m in modes or []:
cmd_obj.validate_mode(m)
def _read_command(self, line):
"""Read a command from a line."""
if self._cur_section is None:
raise KeyConfigError("Got command '{}' without getting a "
"section!".format(line))
else:
for rgx, repl in configdata.CHANGED_KEY_COMMANDS:
if rgx.match(line):
line = rgx.sub(repl, line)
self._mark_config_dirty()
break
self._validate_command(line)
self._cur_command = line
def _read_keybinding(self, line):
"""Read a key binding from a line."""
if self._cur_command is None:
raise KeyConfigError("Got key binding '{}' without getting a "
"command!".format(line))
else:
assert self._cur_section is not None
self._add_binding(self._cur_section, line, self._cur_command)
def _add_binding(self, sectname, keychain, command, *, force=False):
"""Add a new binding from keychain to command in section sectname."""
if utils.is_special_key(keychain):
# <Ctrl-t>, <ctrl-T>, and <ctrl-t> should be considered equivalent
keychain = keychain.lower()
log.keyboard.vdebug("Adding binding {} -> {} in mode {}.".format(
keychain, command, sectname))
if sectname not in self.keybindings:
self.keybindings[sectname] = collections.OrderedDict()
if keychain in self.get_bindings_for(sectname):
if force or command == self.UNBOUND_COMMAND:
self.unbind(keychain, mode=sectname)
else:
raise DuplicateKeychainError(keychain)
section = self.keybindings[sectname]
if (command != self.UNBOUND_COMMAND and
section.get(keychain, None) == self.UNBOUND_COMMAND):
# re-binding an unbound keybinding
del section[keychain]
self.keybindings[sectname][keychain] = command
def get_bindings_for(self, section):
"""Get a dict with all merged key bindings for a section."""
bindings = {}
for sectstring, d in self.keybindings.items():
if sectstring.startswith('!'):
inverted = True
sectstring = sectstring[1:]
else:
inverted = False
sects = [s.strip() for s in sectstring.split(',')]
matches = any(s == section for s in sects)
if (not inverted and matches) or (inverted and not matches):
bindings.update(d)
try:
bindings.update(self.keybindings['all'])
except KeyError:
pass
bindings = {k: v for k, v in bindings.items()
if v != self.UNBOUND_COMMAND}
return bindings
def get_reverse_bindings_for(self, section):
"""Get a dict of commands to a list of bindings for the section."""
cmd_to_keys = {}
for key, full_cmd in self.get_bindings_for(section).items():
for cmd in full_cmd.split(';;'):
cmd = cmd.strip()
cmd_to_keys.setdefault(cmd, [])
# put special bindings last
if utils.is_special_key(key):
cmd_to_keys[cmd].append(key)
else:
cmd_to_keys[cmd].insert(0, key)
return cmd_to_keys

View File

@ -1,231 +0,0 @@
# 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/>.
"""Setting sections used for qutebrowser."""
import collections
from qutebrowser.config import value as confvalue
class Section:
"""Base class for KeyValue/ValueList sections.
Attributes:
_readonly: Whether this section is read-only.
values: An OrderedDict with key as index and value as value.
key: string
value: SettingValue
descriptions: A dict with the description strings for the keys.
"""
def __init__(self):
self.values = None
self.descriptions = {}
self._readonly = False
def __getitem__(self, key):
"""Get the value for key.
Args:
key: The key to get a value for, as a string.
Return:
The value, as value class.
"""
return self.values[key]
def __iter__(self):
"""Iterate over all set values."""
return iter(self.values)
def __bool__(self):
"""Get boolean state of section."""
return bool(self.values)
def __contains__(self, key):
"""Return whether the section contains a given key."""
return key in self.values
def items(self):
"""Get dict items."""
return self.values.items()
def keys(self):
"""Get value keys."""
return self.values.keys()
def delete(self, key):
"""Delete item with given key."""
del self.values[key]
def setv(self, layer, key, value, interpolated):
"""Set the value on a layer.
Args:
layer: The layer to set the value on, an element name of the
ValueLayers dict.
key: The key of the element to set.
value: The value to set.
interpolated: The interpolated value, for checking, or None.
"""
raise NotImplementedError
def dump_userconfig(self):
"""Dump the part of the config which was changed by the user.
Return:
A list of (key, valuestr) tuples.
"""
raise NotImplementedError
class KeyValue(Section):
"""Representation of a section with ordinary key-value mappings.
This is a section which contains normal "key = value" pairs with a fixed
set of keys.
"""
def __init__(self, *defaults, readonly=False):
"""Constructor.
Args:
*defaults: A (key, value, description) list of defaults.
readonly: Whether this config is readonly.
"""
super().__init__()
self._readonly = readonly
if not defaults:
return
self.values = collections.OrderedDict()
for (k, v, desc) in defaults:
assert k not in self.values, k
self.values[k] = v
self.descriptions[k] = desc
def setv(self, layer, key, value, interpolated):
if self._readonly:
raise ValueError("Trying to modify a read-only config!")
self.values[key].setv(layer, value, interpolated)
def dump_userconfig(self):
changed = []
for k, v in self.items():
vals = v.values
if vals['temp'] is not None and vals['temp'] != vals['default']:
changed.append((k, vals['temp']))
elif vals['conf'] is not None and vals['conf'] != vals['default']:
changed.append((k, vals['conf']))
return changed
class ValueList(Section):
"""This class represents a section with a list key-value settings.
These are settings inside sections which don't have fixed keys, but instead
have a dynamic list of "key = value" pairs, like key bindings or
searchengines.
They basically consist of two different SettingValues.
Attributes:
layers: An OrderedDict of the config layers.
keytype: The type to use for the key (only used for validating)
valtype: The type to use for the value.
_ordered_value_cache: A ChainMap-like OrderedDict of all values.
_readonly: Whether this section is read-only.
"""
def __init__(self, keytype, valtype, *defaults, readonly=False):
"""Wrap types over default values. Take care when overriding this.
Args:
keytype: The type instance to be used for keys.
valtype: The type instance to be used for values.
*defaults: A (key, value) list of default values.
readonly: Whether this config is readonly.
"""
super().__init__()
self._readonly = readonly
self._ordered_value_cache = None
self.keytype = keytype
self.valtype = valtype
self.layers = collections.OrderedDict([
('default', collections.OrderedDict()),
('conf', collections.OrderedDict()),
('temp', collections.OrderedDict()),
])
defaultlayer = self.layers['default']
for key, value in defaults:
assert key not in defaultlayer, key
defaultlayer[key] = confvalue.SettingValue(valtype, value)
self.values = collections.ChainMap(
self.layers['temp'], self.layers['conf'], self.layers['default'])
def _ordered_values(self):
"""Get ordered values in layers.
This is more expensive than the ChainMap, but we need this for
iterating/items/etc. when order matters.
"""
if self._ordered_value_cache is None:
self._ordered_value_cache = collections.OrderedDict()
for layer in self.layers.values():
self._ordered_value_cache.update(layer)
return self._ordered_value_cache
def setv(self, layer, key, value, interpolated):
if self._readonly:
raise ValueError("Trying to modify a read-only config!")
self.keytype.validate(key)
if key in self.layers[layer]:
self.layers[layer][key].setv(layer, value, interpolated)
else:
val = confvalue.SettingValue(self.valtype)
val.setv(layer, value, interpolated)
self.layers[layer][key] = val
self._ordered_value_cache = None
def dump_userconfig(self):
changed = []
mapping = collections.ChainMap(self.layers['temp'],
self.layers['conf'])
for k, v in mapping.items():
try:
if v.value() != self.layers['default'][k].value():
changed.append((k, v.value()))
except KeyError:
changed.append((k, v.value()))
return changed
def __iter__(self):
"""Iterate over all set values."""
return self._ordered_values().__iter__()
def items(self):
"""Get dict items."""
return self._ordered_values().items()
def keys(self):
"""Get value keys."""
return self._ordered_values().keys()

View File

@ -1,100 +0,0 @@
# 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/>.
"""Utilities related to the look&feel of qutebrowser."""
import functools
import collections
import jinja2
import sip
from PyQt5.QtGui import QColor
from qutebrowser.config import config
from qutebrowser.utils import log, objreg
@functools.lru_cache(maxsize=16)
def get_stylesheet(template_str):
"""Format a stylesheet based on a template.
Args:
template_str: The stylesheet template as string.
Return:
The formatted template as string.
"""
colordict = ColorDict(config.section('colors'))
template = jinja2.Template(template_str)
return template.render(color=colordict, font=config.section('fonts'),
config=objreg.get('config'))
def set_register_stylesheet(obj):
"""Set the stylesheet for an object based on it's STYLESHEET attribute.
Also, register an update when the config is changed.
Args:
obj: The object to set the stylesheet for and register.
Must have a STYLESHEET attribute.
"""
qss = get_stylesheet(obj.STYLESHEET)
log.config.vdebug("stylesheet for {}: {}".format(
obj.__class__.__name__, qss))
obj.setStyleSheet(qss)
objreg.get('config').changed.connect(
functools.partial(_update_stylesheet, obj))
def _update_stylesheet(obj):
"""Update the stylesheet for obj."""
get_stylesheet.cache_clear()
if not sip.isdeleted(obj):
obj.setStyleSheet(get_stylesheet(obj.STYLESHEET))
class ColorDict(collections.UserDict):
"""A dict aimed at Qt stylesheet colors."""
def __getitem__(self, key):
"""Override dict __getitem__.
Args:
key: The key to get from the dict.
Return:
If a value wasn't found, return an empty string.
(Color not defined, so no output in the stylesheet)
else, return the plain value.
"""
try:
val = self.data[key]
except KeyError:
log.config.exception("No color defined for {}!".format(key))
return ''
if isinstance(val, QColor):
# This could happen when accidentally declaring something as
# QtColor instead of Color in the config, and it'd go unnoticed as
# the CSS is invalid then.
raise TypeError("QColor passed to ColorDict!")
else:
return val

View File

@ -1,39 +0,0 @@
# 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/>.
"""Textwrapper used for config files."""
import textwrap
class TextWrapper(textwrap.TextWrapper):
"""Text wrapper customized to be used in configs."""
def __init__(self, **kwargs):
kw = {
'width': 72,
'replace_whitespace': False,
'break_long_words': False,
'break_on_hyphens': False,
'initial_indent': '# ',
'subsequent_indent': '# ',
}
kw.update(kwargs)
super().__init__(**kw)

View File

@ -1,101 +0,0 @@
# 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/>.
"""A single value (with multiple layers possibly) in the config."""
import collections
class SettingValue:
"""Base class for setting values.
Intended to be sub-classed by config value "types".
Attributes:
typ: A BaseType subclass instance.
value: (readonly property) The currently valid, most important value.
values: An OrderedDict with the values on different layers, with the
most significant layer first.
"""
def __init__(self, typ, default=None, *, backends=None):
"""Constructor.
Args:
typ: The BaseType to use.
default: Raw value to set.
backend: A list of usertypes.Backend enum members to mark this
setting as unsupported with other backends.
"""
self.typ = typ
self.values = collections.OrderedDict.fromkeys(
['temp', 'conf', 'default'])
self.values['default'] = default
self.backends = backends
def __str__(self):
"""Get raw string value."""
return self.value()
def default(self):
"""Get the default value."""
return self.values['default']
def getlayers(self, startlayer):
"""Get a dict of values starting with startlayer.
Args:
startlayer: The first layer to include.
"""
idx = list(self.values.keys()).index(startlayer)
d = collections.OrderedDict(list(self.values.items())[idx:])
return d
def value(self, startlayer=None):
"""Get the first valid value starting from startlayer.
Args:
startlayer: The first layer to include.
"""
if startlayer is None:
d = self.values
else:
d = self.getlayers(startlayer)
for val in d.values():
if val is not None:
return val
raise ValueError("No valid config value found!")
def transformed(self):
"""Get the transformed value."""
return self.typ.transform(self.value())
def setv(self, layer, value, interpolated):
"""Set the value on a layer.
Args:
layer: The layer to set the value on, an element name of the
ValueLayers dict.
value: The value to set.
interpolated: The interpolated value, for typechecking (or None).
"""
if interpolated is not None:
self.typ.validate(interpolated)
self.values[layer] = value

View File

@ -192,21 +192,19 @@ class FontFamilySetter(Setter):
def init_mappings(mappings):
"""Initialize all settings based on a settings mapping."""
for sectname, section in mappings.items():
for optname, mapping in section.items():
value = config.get(sectname, optname)
log.config.vdebug("Setting {} -> {} to {!r}".format(
sectname, optname, value))
mapping.set(value)
for option, mapping in mappings.items():
value = config.instance.get(option)
log.config.vdebug("Setting {} to {!r}".format(option, value))
mapping.set(value)
def update_mappings(mappings, section, option):
def update_mappings(mappings, option):
"""Update global settings when QWeb(Engine)Settings changed."""
try:
mapping = mappings[section][option]
mapping = mappings[option]
except KeyError:
return
value = config.get(section, option)
value = config.instance.get(option)
mapping.set(value)

View File

@ -7,9 +7,6 @@ vim: ft=html fileencoding=utf-8 sts=4 sw=4 et:
<head>
<meta charset="utf-8">
<title>{{ title }}</title>
{% if icon %}
<link rel='icon' type='image/png' href="{{ icon }}">
{% endif %}
<style type="text/css">
{% block style %}
body {

View File

@ -54,13 +54,13 @@ ul.files > li {
<ul class="folders">
{% for item in directories %}
<li><a href="{{ file_url(item.absname) }}">{{item.name}}</a></li>
<li><a href="{{ file_url(item['absname']) }}">{{item['name']}}</a></li>
{% endfor %}
</ul>
<ul class="files">
{% for item in files %}
<li><a href="{{ file_url(item.absname) }}">{{item.name}}</a></li>
<li><a href="{{ file_url(item['absname']) }}">{{item['name']}}</a></li>
{% endfor %}
</ul>
</div>

View File

@ -54,7 +54,7 @@ table {
<a href="#" id="load" style="display: none">Show more</a>
<script type="text/javascript" src="qute://javascript/history.js"></script>
<script type="text/javascript">
window.SESSION_INTERVAL = {{session_interval}} * 60 * 1000;
window.GAP_INTERVAL = {{gap_interval}} * 60 * 1000;
window.onload = function() {
var loadLink = document.getElementById('load');

View File

@ -74,7 +74,7 @@ li {
<p>Error while opening {{ url }}<br>
<p id="error-message-text" style="color: #a31a1a;">qutebrowser can't find a suitable pdf.js installation</p></p>
<p>It looks like you set <code>content -> enable-pdfjs</code>
<p>It looks like you set <code>content.pdfjs</code>
to <em>true</em> but qutebrowser can't find the required files.</p>
<br>
@ -82,7 +82,7 @@ li {
<h2>Possible fixes</h2>
<ul>
<li>
Disable <code>content -> enable-pdfjs</code> and reload the page.
Disable <code>content.pdfjs</code> and reload the page.
You will need to download the pdf-file and open it with an external
tool instead.
</li>

View File

@ -1,9 +1,13 @@
{% extends "base.html" %}
{% block script %}
var cset = function(section, option, el) {
value = el.value;
window.qute.set(section, option, value);
var cset = function(option, value) {
// FIXME:conf we might want some error handling here?
var url = "qute://settings/set?option=" + encodeURIComponent(option);
url += "&value=" + encodeURIComponent(value);
var xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.send();
}
{% endblock %}
@ -22,24 +26,23 @@ 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>
{% for section in config.DATA %}
<tr><th colspan="2"><h3>{{ section }}</h3><pre>{{ config.SECTION_DESC.get(section)|wordwrap(width=120) }}</pre></th></tr>
{% for d, e in config.DATA.get(section).items() %}
{% for option in configdata.DATA.values() %}
<tr>
<td>{{ d }} (Current: {{ confget(section, d)|truncate(100) }})
{% if config.DATA.get(section).descriptions[d] %}
<p class="option_description">{{ config.DATA.get(section).descriptions[d]|e }}</p>
<!-- FIXME: convert to string properly -->
<td>{{ option.name }} (Current: {{ confget(option.name) | string |truncate(100) }})
{% if option.description %}
<p class="option_description">{{ option.description|e }}</p>
{% endif %}
</td>
<td>
<input type="text"
onblur="cset('{{ section }}', '{{ d }}', this)"
value="{{ confget(section, d) }}">
id="input-{{ option.name }}"
onblur="cset('{{ option.name }}', this.value)"
value="{{ confget(option.name) }}">
</input>
</td>
</tr>
{% endfor %}
{% endfor %}
</table>
{% endblock %}

View File

@ -79,9 +79,9 @@ window.loadHistory = (function() {
// Create session-separator and new tbody if necessary
if (tbody.lastChild !== null && lastItemDate !== null &&
window.SESSION_INTERVAL > 0) {
window.GAP_INTERVAL > 0) {
var interval = lastItemDate.getTime() - date.getTime();
if (interval > window.SESSION_INTERVAL) {
if (interval > window.GAP_INTERVAL) {
// Add session-separator
var sessionSeparator = document.createElement("td");
sessionSeparator.className = "session-separator";

View File

@ -20,13 +20,12 @@
"""Base class for vim-like key sequence parser."""
import re
import functools
import unicodedata
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject
from PyQt5.QtCore import pyqtSignal, QObject
from qutebrowser.config import config
from qutebrowser.utils import usertypes, log, utils, objreg
from qutebrowser.utils import usertypes, log, utils
class BaseKeyParser(QObject):
@ -41,7 +40,6 @@ class BaseKeyParser(QObject):
partial: No keychain matched yet, but it's still possible in the
future.
definitive: Keychain matches exactly.
ambiguous: There are both a partial and a definitive match.
none: No more matches possible.
Types: type of a key binding.
@ -59,7 +57,6 @@ class BaseKeyParser(QObject):
_warn_on_keychains: Whether a warning should be logged when binding
keychains in a section which does not support them.
_keystring: The currently entered key sequence
_ambiguous_timer: Timer for delayed execution with ambiguous bindings.
_modename: The name of the input mode associated with this keyparser.
_supports_count: Whether count is supported
_supports_chains: Whether keychains are supported
@ -78,16 +75,13 @@ class BaseKeyParser(QObject):
do_log = True
passthrough = False
Match = usertypes.enum('Match', ['partial', 'definitive', 'ambiguous',
'other', 'none'])
Match = usertypes.enum('Match', ['partial', 'definitive', 'other', 'none'])
Type = usertypes.enum('Type', ['chain', 'special'])
def __init__(self, win_id, parent=None, supports_count=None,
supports_chains=False):
super().__init__(parent)
self._win_id = win_id
self._ambiguous_timer = usertypes.Timer(self, 'ambiguous-match')
self._ambiguous_timer.setSingleShot(True)
self._modename = None
self._keystring = ''
if supports_count is None:
@ -97,6 +91,7 @@ class BaseKeyParser(QObject):
self._warn_on_keychains = True
self.bindings = {}
self.special_bindings = {}
config.instance.changed.connect(self._on_config_changed)
def __repr__(self):
return utils.get_repr(self, supports_count=self._supports_count,
@ -126,7 +121,13 @@ class BaseKeyParser(QObject):
if binding is None:
self._debug_log("Ignoring only-modifier keyeevent.")
return False
binding = binding.lower()
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:
@ -182,7 +183,8 @@ class BaseKeyParser(QObject):
self._debug_log("Ignoring, no text char")
return self.Match.none
self._stop_timers()
key_mappings = config.val.bindings.key_mappings
txt = key_mappings.get(txt, txt)
self._keystring += txt
count, cmd_input = self._split_count()
@ -198,10 +200,6 @@ class BaseKeyParser(QObject):
self._keystring))
self.clear_keystring()
self.execute(binding, self.Type.chain, count)
elif match == self.Match.ambiguous:
self._debug_log("Ambiguous match for '{}'.".format(
self._keystring))
self._handle_ambiguous_match(binding, count)
elif match == self.Match.partial:
self._debug_log("No match for '{}' (added {})".format(
self._keystring, txt))
@ -221,11 +219,9 @@ class BaseKeyParser(QObject):
Return:
A tuple (matchtype, binding).
matchtype: Match.definitive, Match.ambiguous, Match.partial or
Match.none
binding: - None with Match.partial/Match.none
- The found binding with Match.definitive/
Match.ambiguous
matchtype: Match.definitive, Match.partial or Match.none.
binding: - None with Match.partial/Match.none.
- The found binding with Match.definitive.
"""
# A (cmd_input, binding) tuple (k, v of bindings) or None.
definitive_match = None
@ -243,58 +239,13 @@ class BaseKeyParser(QObject):
elif binding.startswith(cmd_input):
partial_match = True
break
if definitive_match is not None and partial_match:
return (self.Match.ambiguous, definitive_match[1])
elif definitive_match is not None:
if definitive_match is not None:
return (self.Match.definitive, definitive_match[1])
elif partial_match:
return (self.Match.partial, None)
else:
return (self.Match.none, None)
def _stop_timers(self):
"""Stop a delayed execution if any is running."""
if self._ambiguous_timer.isActive() and self.do_log:
log.keyboard.debug("Stopping delayed execution.")
self._ambiguous_timer.stop()
try:
self._ambiguous_timer.timeout.disconnect()
except TypeError:
# no connections
pass
def _handle_ambiguous_match(self, binding, count):
"""Handle an ambiguous match.
Args:
binding: The command-string to execute.
count: The count to pass.
"""
self._debug_log("Ambiguous match for '{}'".format(self._keystring))
time = config.get('input', 'timeout')
if time == 0:
# execute immediately
self.clear_keystring()
self.execute(binding, self.Type.chain, count)
else:
# execute in `time' ms
self._debug_log("Scheduling execution of {} in {}ms".format(
binding, time))
self._ambiguous_timer.setInterval(time)
self._ambiguous_timer.timeout.connect(
functools.partial(self.delayed_exec, binding, count))
self._ambiguous_timer.start()
def delayed_exec(self, command, count):
"""Execute a delayed command.
Args:
command/count: As if passed to self.execute()
"""
self._debug_log("Executing delayed command now!")
self.clear_keystring()
self.execute(command, self.Type.chain, count)
def handle(self, e):
"""Handle a new keypress and call the respective handlers.
@ -314,7 +265,11 @@ class BaseKeyParser(QObject):
self.keystring_updated.emit(self._keystring)
return match != self.Match.none
def read_config(self, modename=None):
@config.change_filter('bindings')
def _on_config_changed(self):
self._read_config()
def _read_config(self, modename=None):
"""Read the configuration.
Config format: key = command, e.g.:
@ -332,16 +287,15 @@ class BaseKeyParser(QObject):
self._modename = modename
self.bindings = {}
self.special_bindings = {}
keyconfparser = objreg.get('key-config')
for (key, cmd) in keyconfparser.get_bindings_for(modename).items():
for key, cmd in config.key_instance.get_bindings_for(modename).items():
assert cmd
self._parse_key_command(modename, key, cmd)
def _parse_key_command(self, modename, key, cmd):
"""Parse the keys and their command and store them in the object."""
if utils.is_special_key(key):
keystr = utils.normalize_keystr(key[1:-1])
self.special_bindings[keystr] = cmd
self.special_bindings[key[1:-1]] = cmd
elif self._supports_chains:
self.bindings[key] = cmd
elif self._warn_on_keychains:
@ -359,15 +313,6 @@ class BaseKeyParser(QObject):
"""
raise NotImplementedError
@pyqtSlot(str)
def on_keyconfig_changed(self, mode):
"""Re-read the config if a key binding was changed."""
if self._modename is None:
raise AssertionError("on_keyconfig_changed called but no section "
"defined!")
if mode == self._modename:
self.read_config()
def clear_keystring(self):
"""Clear the currently entered key sequence."""
if self._keystring:

View File

@ -42,7 +42,7 @@ class CommandKeyParser(BaseKeyParser):
def execute(self, cmdstr, _keytype, count=None):
try:
self._commandrunner.run(cmdstr, count)
except (cmdexc.CommandMetaError, cmdexc.CommandError) as e:
except cmdexc.Error as e:
message.error(str(e), stack=traceback.format_exc())
@ -69,7 +69,7 @@ class PassthroughKeyParser(CommandKeyParser):
"""
super().__init__(win_id, parent, supports_chains=False)
self._warn_on_keychains = warn
self.read_config(mode)
self._read_config(mode)
self._mode = mode
def __repr__(self):

View File

@ -144,9 +144,6 @@ class ModeManager(QObject):
self._parsers = {}
self.mode = usertypes.KeyMode.normal
self._releaseevents_to_pass = set()
self._forward_unbound_keys = config.get(
'input', 'forward-unbound-keys')
objreg.get('config').changed.connect(self.set_forward_unbound_keys)
def __repr__(self):
return utils.get_repr(self, mode=self.mode)
@ -171,10 +168,12 @@ class ModeManager(QObject):
event.modifiers() not in [Qt.NoModifier, Qt.ShiftModifier] or
not event.text().strip())
forward_unbound_keys = config.val.input.forward_unbound_keys
if handled:
filter_this = True
elif (parser.passthrough or self._forward_unbound_keys == 'all' or
(self._forward_unbound_keys == 'auto' and is_non_alnum)):
elif (parser.passthrough or forward_unbound_keys == 'all' or
(forward_unbound_keys == 'auto' and is_non_alnum)):
filter_this = False
else:
filter_this = True
@ -184,10 +183,10 @@ class ModeManager(QObject):
if curmode != usertypes.KeyMode.insert:
focus_widget = QApplication.instance().focusWidget()
log.modes.debug("handled: {}, forward-unbound-keys: {}, "
log.modes.debug("handled: {}, forward_unbound_keys: {}, "
"passthrough: {}, is_non_alnum: {} --> "
"filter: {} (focused: {!r})".format(
handled, self._forward_unbound_keys,
handled, forward_unbound_keys,
parser.passthrough, is_non_alnum, filter_this,
focus_widget))
return filter_this
@ -313,12 +312,6 @@ class ModeManager(QObject):
raise ValueError("Can't leave normal mode!")
self.leave(self.mode, 'leave current')
@config.change_filter('input', 'forward-unbound-keys')
def set_forward_unbound_keys(self):
"""Update local setting when config changed."""
self._forward_unbound_keys = config.get(
'input', 'forward-unbound-keys')
def eventFilter(self, event):
"""Filter all events based on the currently set mode.

View File

@ -48,7 +48,7 @@ class NormalKeyParser(keyparser.CommandKeyParser):
def __init__(self, win_id, parent=None):
super().__init__(win_id, parent, supports_count=True,
supports_chains=True)
self.read_config('normal')
self._read_config('normal')
self._partial_timer = usertypes.Timer(self, 'partial-match')
self._partial_timer.setSingleShot(True)
self._inhibited = False
@ -74,7 +74,7 @@ class NormalKeyParser(keyparser.CommandKeyParser):
return self.Match.none
match = super()._handle_single_key(e)
if match == self.Match.partial:
timeout = config.get('input', 'partial-timeout')
timeout = config.val.input.partial_timeout
if timeout != 0:
self._partial_timer.setInterval(timeout)
self._partial_timer.timeout.connect(self._clear_partial_match)
@ -130,7 +130,7 @@ class PromptKeyParser(keyparser.CommandKeyParser):
supports_chains=True)
# We don't want an extra section for this in the config, so we just
# abuse the prompt section.
self.read_config('prompt')
self._read_config('prompt')
def __repr__(self):
return utils.get_repr(self)
@ -150,7 +150,7 @@ class HintKeyParser(keyparser.CommandKeyParser):
supports_chains=True)
self._filtertext = ''
self._last_press = LastPress.none
self.read_config('hint')
self._read_config('hint')
self.keystring_updated.connect(self.on_keystring_updated)
def _handle_special_key(self, e):
@ -264,7 +264,7 @@ class CaretKeyParser(keyparser.CommandKeyParser):
def __init__(self, win_id, parent=None):
super().__init__(win_id, parent, supports_count=True,
supports_chains=True)
self.read_config('caret')
self._read_config('caret')
class RegisterKeyParser(keyparser.CommandKeyParser):
@ -280,7 +280,7 @@ class RegisterKeyParser(keyparser.CommandKeyParser):
super().__init__(win_id, parent, supports_count=False,
supports_chains=False)
self._mode = mode
self.read_config('register')
self._read_config('register')
def handle(self, e):
"""Override handle to always match the next key and use the register.
@ -316,14 +316,9 @@ class RegisterKeyParser(keyparser.CommandKeyParser):
else:
raise ValueError(
"{} is not a valid register mode".format(self._mode))
except (cmdexc.CommandMetaError, cmdexc.CommandError) as err:
except cmdexc.Error as err:
message.error(str(err), stack=traceback.format_exc())
self.request_leave.emit(self._mode, "valid register key", True)
return True
@pyqtSlot(str)
def on_keyconfig_changed(self, mode):
"""RegisterKeyParser has no config section (no bindable keys)."""
pass

View File

@ -24,14 +24,13 @@ import base64
import itertools
import functools
import jinja2
from PyQt5.QtCore import pyqtSlot, QRect, QPoint, QTimer, Qt
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication, QSizePolicy
from qutebrowser.commands import runners, cmdutils
from qutebrowser.config import config
from qutebrowser.utils import (message, log, usertypes, qtutils, objreg, utils,
debug)
jinja, debug)
from qutebrowser.mainwindow import tabbedbrowser, messageview, prompt
from qutebrowser.mainwindow.statusbar import bar
from qutebrowser.completion import completionwidget, completer
@ -52,7 +51,7 @@ def get_window(via_ipc, force_window=False, force_tab=False,
via_ipc: Whether the request was made via IPC.
force_window: Whether to force opening in a window.
force_tab: Whether to force opening in a tab.
force_target: Override the new-instance-open-target config
force_target: Override the new_instance_open_target config
"""
if force_window and force_tab:
raise ValueError("force_window and force_tab are mutually exclusive!")
@ -61,7 +60,7 @@ def get_window(via_ipc, force_window=False, force_tab=False,
# Initial main window
return 0
open_target = config.get('general', 'new-instance-open-target')
open_target = config.val.new_instance_open_target
# Apply any target overrides, ordered by precedence
if force_target is not None:
@ -99,7 +98,7 @@ def get_window(via_ipc, force_window=False, force_tab=False,
def get_target_window():
"""Get the target window for new tabs, or None if none exist."""
try:
win_mode = config.get('general', 'new-instance-open-target.window')
win_mode = config.val.new_instance_open_target_window
if win_mode == 'last-focused':
return objreg.last_focused_window()
elif win_mode == 'first-opened':
@ -165,7 +164,7 @@ class MainWindow(QWidget):
self._init_downloadmanager()
self._downloadview = downloadview.DownloadView(self.win_id)
if config.get('general', 'private-browsing'):
if config.val.content.private_browsing:
# This setting always trumps what's passed in.
private = True
else:
@ -215,7 +214,7 @@ class MainWindow(QWidget):
# resizing will fail. Therefore, we use singleShot QTimers to make sure
# we defer this until everything else is initialized.
QTimer.singleShot(0, self._connect_overlay_signals)
objreg.get('config').changed.connect(self.on_config_changed)
config.instance.changed.connect(self._on_config_changed)
objreg.get("app").new_window.emit(self)
@ -254,7 +253,7 @@ class MainWindow(QWidget):
left = (self.width() - width) / 2 if centered else 0
height_padding = 20
status_position = config.get('ui', 'status-position')
status_position = config.val.statusbar.position
if status_position == 'bottom':
if self.status.isVisible():
status_height = self.status.height()
@ -308,7 +307,7 @@ class MainWindow(QWidget):
def _init_completion(self):
self._completion = completionwidget.CompletionView(self.win_id, self)
cmd = objreg.get('status-command', scope='window', window=self.win_id)
completer_obj = completer.Completer(cmd, self.win_id, self._completion)
completer_obj = completer.Completer(cmd, self._completion)
self._completion.selection_changed.connect(
completer_obj.on_selection_changed)
objreg.register('completion', self._completion, scope='window',
@ -327,16 +326,14 @@ class MainWindow(QWidget):
def __repr__(self):
return utils.get_repr(self)
@pyqtSlot(str, str)
def on_config_changed(self, section, option):
@pyqtSlot(str)
def _on_config_changed(self, option):
"""Resize the completion if related config options changed."""
if section != 'ui':
return
if option == 'statusbar-padding':
if option == 'statusbar.padding':
self._update_overlay_geometries()
elif option == 'downloads-position':
elif option == 'downloads.position':
self._add_widgets()
elif option == 'status-position':
elif option == 'statusbar.position':
self._add_widgets()
self._update_overlay_geometries()
@ -345,10 +342,9 @@ class MainWindow(QWidget):
self._vbox.removeWidget(self.tabbed_browser)
self._vbox.removeWidget(self._downloadview)
self._vbox.removeWidget(self.status)
downloads_position = config.get('ui', 'downloads-position')
status_position = config.get('ui', 'status-position')
widgets = [self.tabbed_browser]
downloads_position = config.val.downloads.position
if downloads_position == 'top':
widgets.insert(0, self._downloadview)
elif downloads_position == 'bottom':
@ -356,6 +352,7 @@ class MainWindow(QWidget):
else:
raise ValueError("Invalid position {}!".format(downloads_position))
status_position = config.val.statusbar.position
if status_position == 'top':
widgets.insert(0, self.status)
elif status_position == 'bottom':
@ -417,8 +414,6 @@ class MainWindow(QWidget):
def _connect_signals(self):
"""Connect all mainwindow signals."""
key_config = objreg.get('key-config')
status = self._get_object('statusbar')
keyparsers = self._get_object('keyparsers')
completion_obj = self._get_object('completion')
@ -448,10 +443,6 @@ class MainWindow(QWidget):
parser.keystring_updated.connect(functools.partial(
self._keyhint.update_keyhint, mode.name))
# config
for obj in keyparsers.values():
key_config.changed.connect(obj.on_keyconfig_changed)
# messages
message.global_bridge.show_message.connect(
self._messageview.show_message)
@ -545,24 +536,23 @@ class MainWindow(QWidget):
if crashsignal.is_crashing:
e.accept()
return
confirm_quit = config.get('ui', 'confirm-quit')
tab_count = self.tabbed_browser.count()
download_model = objreg.get('download-model', scope='window',
window=self.win_id)
download_count = download_model.running_downloads()
quit_texts = []
# Ask if multiple-tabs are open
if 'multiple-tabs' in confirm_quit and tab_count > 1:
if 'multiple-tabs' in config.val.confirm_quit and tab_count > 1:
quit_texts.append("{} {} open.".format(
tab_count, "tab is" if tab_count == 1 else "tabs are"))
# Ask if multiple downloads running
if 'downloads' in confirm_quit and download_count > 0:
if 'downloads' in config.val.confirm_quit and download_count > 0:
quit_texts.append("{} {} running.".format(
download_count,
"download is" if download_count == 1 else "downloads are"))
# Process all quit messages that user must confirm
if quit_texts or 'always' in confirm_quit:
msg = jinja2.Template("""
if quit_texts or 'always' in config.val.confirm_quit:
msg = jinja.environment.from_string("""
<ul>
{% for text in quit_texts %}
<li>{{text}}</li>

View File

@ -23,8 +23,8 @@
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QTimer, Qt, QSize
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QSizePolicy
from qutebrowser.config import config, style
from qutebrowser.utils import usertypes, objreg
from qutebrowser.config import config
from qutebrowser.utils import usertypes
class Message(QLabel):
@ -41,31 +41,32 @@ class Message(QLabel):
"""
if level == usertypes.MessageLevel.error:
stylesheet += """
background-color: {{ color['messages.bg.error'] }};
color: {{ color['messages.fg.error'] }};
font: {{ font['messages.error'] }};
border-bottom: 1px solid {{ color['messages.border.error'] }};
background-color: {{ conf.colors.messages.error.bg }};
color: {{ conf.colors.messages.error.fg }};
font: {{ conf.fonts.messages.error }};
border-bottom: 1px solid {{ conf.colors.messages.error.border }};
"""
elif level == usertypes.MessageLevel.warning:
stylesheet += """
background-color: {{ color['messages.bg.warning'] }};
color: {{ color['messages.fg.warning'] }};
font: {{ font['messages.warning'] }};
background-color: {{ conf.colors.messages.warning.bg }};
color: {{ conf.colors.messages.warning.fg }};
font: {{ conf.fonts.messages.warning }};
border-bottom:
1px solid {{ color['messages.border.warning'] }};
1px solid {{ conf.colors.messages.warning.border }};
"""
elif level == usertypes.MessageLevel.info:
stylesheet += """
background-color: {{ color['messages.bg.info'] }};
color: {{ color['messages.fg.info'] }};
font: {{ font['messages.info'] }};
border-bottom: 1px solid {{ color['messages.border.info'] }}
background-color: {{ conf.colors.messages.info.bg }};
color: {{ conf.colors.messages.info.fg }};
font: {{ conf.fonts.messages.info }};
border-bottom: 1px solid {{ conf.colors.messages.info.border }}
"""
else: # pragma: no cover
raise ValueError("Invalid level {!r}".format(level))
# We don't bother with set_register_stylesheet here as it's short-lived
# anyways.
self.setStyleSheet(style.get_stylesheet(stylesheet))
config.set_register_stylesheet(self, stylesheet=stylesheet,
update=False)
class MessageView(QWidget):
@ -76,6 +77,7 @@ class MessageView(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self._messages = []
self._vbox = QVBoxLayout(self)
self._vbox.setContentsMargins(0, 0, 0, 0)
self._vbox.setSpacing(0)
@ -83,10 +85,9 @@ class MessageView(QWidget):
self._clear_timer = QTimer()
self._clear_timer.timeout.connect(self.clear_messages)
objreg.get('config').changed.connect(self._set_clear_timer_interval)
config.instance.changed.connect(self._set_clear_timer_interval)
self._last_text = None
self._messages = []
def sizeHint(self):
"""Get the proposed height for the view."""
@ -94,10 +95,10 @@ class MessageView(QWidget):
# The width isn't really relevant as we're expanding anyways.
return QSize(-1, height)
@config.change_filter('ui', 'message-timeout')
@config.change_filter('messages.timeout')
def _set_clear_timer_interval(self):
"""Configure self._clear_timer according to the config."""
interval = config.get('ui', 'message-timeout')
interval = config.val.messages.timeout
if interval > 0:
interval *= min(5, len(self._messages))
self._clear_timer.setInterval(interval)
@ -131,7 +132,7 @@ class MessageView(QWidget):
self._last_text = text
self.show()
self.update_geometry.emit()
if config.get('ui', 'message-timeout') != 0:
if config.val.messages.timeout != 0:
self._set_clear_timer_interval()
self._clear_timer.start()

View File

@ -30,7 +30,7 @@ from PyQt5.QtWidgets import (QWidget, QGridLayout, QVBoxLayout, QLineEdit,
QLabel, QFileSystemModel, QTreeView, QSizePolicy)
from qutebrowser.browser import downloads
from qutebrowser.config import style, config
from qutebrowser.config import config
from qutebrowser.utils import usertypes, log, utils, qtutils, objreg, message
from qutebrowser.keyinput import modeman
from qutebrowser.commands import cmdutils, cmdexc
@ -233,29 +233,28 @@ class PromptContainer(QWidget):
"""
STYLESHEET = """
{% set prompt_radius = config.get('ui', 'prompt-radius') %}
QWidget#PromptContainer {
{% if config.get('ui', 'status-position') == 'top' %}
border-bottom-left-radius: {{ prompt_radius }}px;
border-bottom-right-radius: {{ prompt_radius }}px;
{% if conf.statusbar.position == 'top' %}
border-bottom-left-radius: {{ conf.prompt.radius }}px;
border-bottom-right-radius: {{ conf.prompt.radius }}px;
{% else %}
border-top-left-radius: {{ prompt_radius }}px;
border-top-right-radius: {{ prompt_radius }}px;
border-top-left-radius: {{ conf.prompt.radius }}px;
border-top-right-radius: {{ conf.prompt.radius }}px;
{% endif %}
}
QWidget {
font: {{ font['prompts'] }};
color: {{ color['prompts.fg'] }};
background-color: {{ color['prompts.bg'] }};
font: {{ conf.fonts.prompts }};
color: {{ conf.colors.prompts.fg }};
background-color: {{ conf.colors.prompts.bg }};
}
QTreeView {
selection-background-color: {{ color['prompts.selected.bg'] }};
selection-background-color: {{ conf.colors.prompts.selected.bg }};
}
QTreeView::item:selected, QTreeView::item:selected:hover {
background-color: {{ color['prompts.selected.bg'] }};
background-color: {{ conf.colors.prompts.selected.bg }};
}
"""
update_geometry = pyqtSignal()
@ -269,7 +268,7 @@ class PromptContainer(QWidget):
self.setObjectName('PromptContainer')
self.setAttribute(Qt.WA_StyledBackground, True)
style.set_register_stylesheet(self)
config.set_register_stylesheet(self)
message.global_bridge.prompt_done.connect(self._on_prompt_done)
prompt_queue.show_prompts.connect(self._on_show_prompts)
@ -483,9 +482,8 @@ class _BasePrompt(QWidget):
self._key_grid = QGridLayout()
self._key_grid.setVerticalSpacing(0)
key_config = objreg.get('key-config')
# The bindings are all in the 'prompt' mode, even for yesno prompts
all_bindings = key_config.get_reverse_bindings_for('prompt')
all_bindings = config.key_instance.get_reverse_bindings_for('prompt')
labels = []
for cmd, text in self._allowed_commands():
@ -566,7 +564,7 @@ class FilenamePrompt(_BasePrompt):
self.setFocusProxy(self._lineedit)
self._init_key_label()
if config.get('ui', 'prompt-filebrowser'):
if config.val.prompt.filebrowser:
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
@pyqtSlot(str)
@ -628,7 +626,7 @@ class FilenamePrompt(_BasePrompt):
self._file_view.setModel(self._file_model)
self._file_view.clicked.connect(self._insert_path)
if config.get('ui', 'prompt-filebrowser'):
if config.val.prompt.filebrowser:
self._vbox.addWidget(self._file_view)
else:
self._file_view.hide()

View File

@ -23,7 +23,7 @@ from PyQt5.QtCore import pyqtSignal, pyqtSlot, pyqtProperty, Qt, QSize, QTimer
from PyQt5.QtWidgets import QWidget, QHBoxLayout, QStackedLayout, QSizePolicy
from qutebrowser.browser import browsertab
from qutebrowser.config import config, style
from qutebrowser.config import config
from qutebrowser.utils import usertypes, log, objreg, utils
from qutebrowser.mainwindow.statusbar import (backforward, command, progress,
keystring, percentage, url,
@ -82,21 +82,21 @@ class ColorFlags:
def _generate_stylesheet():
flags = [
('private', 'statusbar.{}.private'),
('caret', 'statusbar.{}.caret'),
('caret-selection', 'statusbar.{}.caret-selection'),
('prompt', 'prompts.{}'),
('insert', 'statusbar.{}.insert'),
('command', 'statusbar.{}.command'),
('private-command', 'statusbar.{}.command.private'),
('private', 'statusbar.private'),
('caret', 'statusbar.caret'),
('caret-selection', 'statusbar.caret.selection'),
('prompt', 'prompts'),
('insert', 'statusbar.insert'),
('command', 'statusbar.command'),
('private-command', 'statusbar.command.private'),
]
stylesheet = """
QWidget#StatusBar,
QWidget#StatusBar QLabel,
QWidget#StatusBar QLineEdit {
font: {{ font['statusbar'] }};
background-color: {{ color['statusbar.bg'] }};
color: {{ color['statusbar.fg'] }};
font: {{ conf.fonts.statusbar }};
background-color: {{ conf.colors.statusbar.normal.bg }};
color: {{ conf.colors.statusbar.normal.fg }};
}
"""
for flag, option in flags:
@ -104,11 +104,11 @@ def _generate_stylesheet():
QWidget#StatusBar[color_flags~="%s"],
QWidget#StatusBar[color_flags~="%s"] QLabel,
QWidget#StatusBar[color_flags~="%s"] QLineEdit {
color: {{ color['%s'] }};
background-color: {{ color['%s'] }};
color: {{ conf.colors.%s }};
background-color: {{ conf.colors.%s }};
}
""" % (flag, flag, flag, # flake8: disable=S001
option.format('fg'), option.format('bg'))
option + '.fg', option + '.bg')
return stylesheet
@ -148,7 +148,7 @@ class StatusBar(QWidget):
objreg.register('statusbar', self, scope='window', window=win_id)
self.setObjectName(self.__class__.__name__)
self.setAttribute(Qt.WA_StyledBackground)
style.set_register_stylesheet(self)
config.set_register_stylesheet(self)
self.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Fixed)
@ -197,33 +197,31 @@ class StatusBar(QWidget):
self.prog = progress.Progress(self)
self._hbox.addWidget(self.prog)
objreg.get('config').changed.connect(self._on_config_changed)
config.instance.changed.connect(self._on_config_changed)
QTimer.singleShot(0, self.maybe_hide)
def __repr__(self):
return utils.get_repr(self)
@pyqtSlot(str, str)
def _on_config_changed(self, section, option):
if section != 'ui':
return
if option == 'hide-statusbar':
@pyqtSlot(str)
def _on_config_changed(self, option):
if option == 'statusbar.hide':
self.maybe_hide()
elif option == 'statusbar-pdading':
elif option == 'statusbar.padding':
self._set_hbox_padding()
@pyqtSlot()
def maybe_hide(self):
"""Hide the statusbar if it's configured to do so."""
hide = config.get('ui', 'hide-statusbar')
tab = self._current_tab()
hide = config.val.statusbar.hide
if hide or (tab is not None and tab.data.fullscreen):
self.hide()
else:
self.show()
def _set_hbox_padding(self):
padding = config.get('ui', 'statusbar-padding')
padding = config.val.statusbar.padding
self._hbox.setContentsMargins(padding.left, 0, padding.right, 0)
@pyqtProperty('QStringList')
@ -265,11 +263,21 @@ class StatusBar(QWidget):
self._color_flags.caret = ColorFlags.CaretMode.on
else:
self._color_flags.caret = ColorFlags.CaretMode.off
self.setStyleSheet(style.get_stylesheet(self.STYLESHEET))
config.set_register_stylesheet(self, update=False)
def _set_mode_text(self, mode):
"""Set the mode text."""
text = "-- {} MODE --".format(mode.upper())
if mode == 'passthrough':
key_instance = config.key_instance
all_bindings = key_instance.get_reverse_bindings_for('passthrough')
bindings = all_bindings.get('leave-mode')
if bindings:
suffix = ' ({} to leave)'.format(bindings[0])
else:
suffix = ''
else:
suffix = ''
text = "-- {} MODE --{}".format(mode.upper(), suffix)
self.txt.set_text(self.txt.Text.normal, text)
def _show_cmd_widget(self):
@ -349,7 +357,7 @@ class StatusBar(QWidget):
def minimumSizeHint(self):
"""Set the minimum height to the text height plus some padding."""
padding = config.get('ui', 'statusbar-padding')
padding = config.val.statusbar.padding
width = super().minimumSizeHint().width()
height = self.fontMetrics().height() + padding.top + padding.bottom
return QSize(width, height)

View File

@ -22,7 +22,7 @@
from PyQt5.QtCore import pyqtSlot, QSize
from PyQt5.QtWidgets import QProgressBar, QSizePolicy
from qutebrowser.config import style
from qutebrowser.config import config
from qutebrowser.utils import utils, usertypes
@ -35,17 +35,17 @@ class Progress(QProgressBar):
border-radius: 0px;
border: 2px solid transparent;
background-color: transparent;
font: {{ font['statusbar'] }};
font: {{ conf.fonts.statusbar }};
}
QProgressBar::chunk {
background-color: {{ color['statusbar.progress.bg'] }};
background-color: {{ conf.colors.statusbar.progress.bg }};
}
"""
def __init__(self, parent=None):
super().__init__(parent)
style.set_register_stylesheet(self)
config.set_register_stylesheet(self)
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
self.setTextVisible(False)
self.hide()

View File

@ -22,7 +22,7 @@
from PyQt5.QtCore import pyqtSlot, pyqtProperty, Qt, QUrl
from qutebrowser.mainwindow.statusbar import textbase
from qutebrowser.config import style
from qutebrowser.config import config
from qutebrowser.utils import usertypes, urlutils
@ -53,27 +53,27 @@ class UrlText(textbase.TextBase):
STYLESHEET = """
QLabel#UrlText[urltype="normal"] {
color: {{ color['statusbar.url.fg'] }};
color: {{ conf.colors.statusbar.url.fg }};
}
QLabel#UrlText[urltype="success"] {
color: {{ color['statusbar.url.fg.success'] }};
color: {{ conf.colors.statusbar.url.success.http.fg }};
}
QLabel#UrlText[urltype="success_https"] {
color: {{ color['statusbar.url.fg.success.https'] }};
color: {{ conf.colors.statusbar.url.success.https.fg }};
}
QLabel#UrlText[urltype="error"] {
color: {{ color['statusbar.url.fg.error'] }};
color: {{ conf.colors.statusbar.url.error.fg }};
}
QLabel#UrlText[urltype="warn"] {
color: {{ color['statusbar.url.fg.warn'] }};
color: {{ conf.colors.statusbar.url.warn.fg }};
}
QLabel#UrlText[urltype="hover"] {
color: {{ color['statusbar.url.fg.hover'] }};
color: {{ conf.colors.statusbar.url.hover.fg }};
}
"""
@ -81,7 +81,7 @@ class UrlText(textbase.TextBase):
"""Override TextBase.__init__ to elide in the middle by default."""
super().__init__(parent, Qt.ElideMiddle)
self.setObjectName(self.__class__.__name__)
style.set_register_stylesheet(self)
config.set_register_stylesheet(self)
self._hover_url = None
self._normal_url = None
self._normal_url_type = UrlType.normal
@ -109,7 +109,7 @@ class UrlText(textbase.TextBase):
else:
self.setText('')
self._urltype = UrlType.normal
self.setStyleSheet(style.get_stylesheet(self.STYLESHEET))
config.set_register_stylesheet(self, update=False)
@pyqtSlot(str)
def on_load_status_changed(self, status_str):

View File

@ -62,7 +62,7 @@ class TabbedBrowser(tabwidget.TabWidget):
_filter: A SignalFilter instance.
_now_focused: The tab which is focused now.
_tab_insert_idx_left: Where to insert a new tab with
tabbar -> new-tab-position set to 'prev'.
tabs.new_tab_position set to 'prev'.
_tab_insert_idx_right: Same as above, for 'next'.
_undo_stack: List of UndoEntry namedtuples of closed tabs.
shutting_down: Whether we're currently shutting down.
@ -122,13 +122,20 @@ class TabbedBrowser(tabwidget.TabWidget):
self._global_marks = {}
self.default_window_icon = self.window().windowIcon()
self.private = private
objreg.get('config').changed.connect(self.update_favicons)
objreg.get('config').changed.connect(self.update_window_title)
objreg.get('config').changed.connect(self.update_tab_titles)
config.instance.changed.connect(self._on_config_changed)
def __repr__(self):
return utils.get_repr(self, count=self.count())
@pyqtSlot(str)
def _on_config_changed(self, option):
if option == 'tabs.favicons.show':
self._update_favicons()
elif option == 'window.title_format':
self._update_window_title()
elif option in ['tabs.title.format', 'tabs.title.format_pinned']:
self._update_tab_titles()
def _tab_index(self, tab):
"""Get the index of a given tab.
@ -159,8 +166,7 @@ class TabbedBrowser(tabwidget.TabWidget):
widgets.append(widget)
return widgets
@config.change_filter('ui', 'window-title-format')
def update_window_title(self):
def _update_window_title(self):
"""Change the window title to match the current tab."""
idx = self.currentIndex()
if idx == -1:
@ -170,8 +176,9 @@ class TabbedBrowser(tabwidget.TabWidget):
fields = self.get_tab_fields(idx)
fields['id'] = self._win_id
fmt = config.get('ui', 'window-title-format')
self.window().setWindowTitle(fmt.format(**fields))
title_format = config.val.window.title_format
title = title_format.format(**fields)
self.window().setWindowTitle(title)
def _connect_tab_signals(self, tab):
"""Set up the needed signals for tab."""
@ -252,7 +259,7 @@ class TabbedBrowser(tabwidget.TabWidget):
tab: The QWebView to be closed.
add_undo: Whether the tab close can be undone.
"""
last_close = config.get('tabs', 'last-close')
last_close = config.val.tabs.last_close
count = self.count()
if last_close == 'ignore' and count == 1:
@ -270,11 +277,10 @@ class TabbedBrowser(tabwidget.TabWidget):
elif last_close == 'blank':
self.openurl(QUrl('about:blank'), newtab=True)
elif last_close == 'startpage':
url = QUrl(config.get('general', 'startpage')[0])
self.openurl(url, newtab=True)
for url in config.val.url.start_pages:
self.openurl(url, newtab=True)
elif last_close == 'default-page':
url = config.get('general', 'default-page')
self.openurl(url, newtab=True)
self.openurl(config.val.url.default_page, newtab=True)
def _remove_tab(self, tab, *, add_undo=True, crashed=False):
"""Remove a tab from the tab list and delete it properly.
@ -329,15 +335,15 @@ class TabbedBrowser(tabwidget.TabWidget):
def undo(self):
"""Undo removing of a tab."""
# Remove unused tab which may be created after the last tab is closed
last_close = config.get('tabs', 'last-close')
last_close = config.val.tabs.last_close
use_current_tab = False
if last_close in ['blank', 'startpage', 'default-page']:
only_one_tab_open = self.count() == 1
no_history = len(self.widget(0).history) == 1
urls = {
'blank': QUrl('about:blank'),
'startpage': QUrl(config.get('general', 'startpage')[0]),
'default-page': config.get('general', 'default-page'),
'startpage': config.val.url.start_pages[0],
'default-page': config.val.url.default_page,
}
first_tab_url = self.widget(0).url()
last_close_urlstr = urls[last_close].toString().rstrip('/')
@ -393,7 +399,7 @@ class TabbedBrowser(tabwidget.TabWidget):
@pyqtSlot('QUrl')
@pyqtSlot('QUrl', bool)
def tabopen(self, url=None, background=None, explicit=False, idx=None, *,
def tabopen(self, url=None, background=None, related=True, idx=None, *,
ignore_tabs_are_windows=False):
"""Open a new tab with a given URL.
@ -403,16 +409,17 @@ class TabbedBrowser(tabwidget.TabWidget):
Args:
url: The URL to open as QUrl or None for an empty tab.
background: Whether to open the tab in the background.
if None, the background-tabs setting decides.
explicit: Whether the tab was opened explicitly.
If this is set, the new position might be different. With
the default settings we handle it like Chromium does:
- Tabs from clicked links etc. are to the right of
the current.
- Explicitly opened tabs are at the very right.
if None, the `tabs.background_tabs`` setting decides.
related: Whether the tab was opened from another existing tab.
If this is set, the new position might be different. With
the default settings we handle it like Chromium does:
- Tabs from clicked links etc. are to the right of
the current (related=True).
- Explicitly opened tabs are at the very right
(related=False)
idx: The index where the new tab should be opened.
ignore_tabs_are_windows: If given, never open a new window, even
with tabs-are-windows set.
with tabs.tabs_are_windows set.
Return:
The opened WebView instance.
@ -420,31 +427,32 @@ class TabbedBrowser(tabwidget.TabWidget):
if url is not None:
qtutils.ensure_valid(url)
log.webview.debug("Creating new tab with URL {}, background {}, "
"explicit {}, idx {}".format(
url, background, explicit, idx))
"related {}, idx {}".format(
url, background, related, idx))
if (config.get('tabs', 'tabs-are-windows') and self.count() > 0 and
if (config.val.tabs.tabs_are_windows and self.count() > 0 and
not ignore_tabs_are_windows):
from qutebrowser.mainwindow import mainwindow
window = mainwindow.MainWindow(private=self.private)
window.show()
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=window.win_id)
return tabbed_browser.tabopen(url, background, explicit)
return tabbed_browser.tabopen(url=url, background=background,
related=related)
tab = browsertab.create(win_id=self._win_id, private=self.private,
parent=self)
self._connect_tab_signals(tab)
if idx is None:
idx = self._get_new_tab_idx(explicit)
idx = self._get_new_tab_idx(related)
self.insertTab(idx, tab, "")
if url is not None:
tab.openurl(url)
if background is None:
background = config.get('tabs', 'background-tabs')
background = config.val.tabs.background
if background:
# Make sure the background tab has the correct initial size.
# With a foreground tab, it's going to be resized correctly by the
@ -458,19 +466,19 @@ class TabbedBrowser(tabwidget.TabWidget):
self.new_tab.emit(tab, idx)
return tab
def _get_new_tab_idx(self, explicit):
def _get_new_tab_idx(self, related):
"""Get the index of a tab to insert.
Args:
explicit: Whether the tab was opened explicitly.
related: Whether the tab was opened from another tab (as a "child")
Return:
The index of the new tab.
"""
if explicit:
pos = config.get('tabs', 'new-tab-position-explicit')
if related:
pos = config.val.tabs.new_position.related
else:
pos = config.get('tabs', 'new-tab-position')
pos = config.val.tabs.new_position.unrelated
if pos == 'prev':
idx = self._tab_insert_idx_left
# On first sight, we'd think we have to decrement
@ -486,26 +494,23 @@ class TabbedBrowser(tabwidget.TabWidget):
elif pos == 'last':
idx = -1
else:
raise ValueError("Invalid new-tab-position '{}'.".format(pos))
log.webview.debug("new-tab-position {} -> opening new tab at {}, "
raise ValueError("Invalid tabs.new_position '{}'.".format(pos))
log.webview.debug("tabs.new_position {} -> opening new tab at {}, "
"next left: {} / right: {}".format(
pos, idx, self._tab_insert_idx_left,
self._tab_insert_idx_right))
return idx
@config.change_filter('tabs', 'show-favicons')
def update_favicons(self):
def _update_favicons(self):
"""Update favicons when config was changed."""
show = config.get('tabs', 'show-favicons')
tabs_are_wins = config.get('tabs', 'tabs-are-windows')
for i, tab in enumerate(self.widgets()):
if show:
if config.val.tabs.favicons.show:
self.setTabIcon(i, tab.icon())
if tabs_are_wins:
if config.val.tabs.tabs_are_windows:
self.window().setWindowIcon(tab.icon())
else:
self.setTabIcon(i, QIcon())
if tabs_are_wins:
if config.val.tabs.tabs_are_windows:
self.window().setWindowIcon(self.default_window_icon)
@pyqtSlot()
@ -520,16 +525,16 @@ class TabbedBrowser(tabwidget.TabWidget):
except TabDeletedError:
# We can get signals for tabs we already deleted...
return
self.update_tab_title(idx)
self._update_tab_title(idx)
if tab.data.keep_icon:
tab.data.keep_icon = False
else:
self.setTabIcon(idx, QIcon())
if (config.get('tabs', 'tabs-are-windows') and
config.get('tabs', 'show-favicons')):
if (config.val.tabs.tabs_are_windows and
config.val.tabs.favicons.show):
self.window().setWindowIcon(self.default_window_icon)
if idx == self.currentIndex():
self.update_window_title()
self._update_window_title()
@pyqtSlot()
def on_cur_load_started(self):
@ -561,7 +566,7 @@ class TabbedBrowser(tabwidget.TabWidget):
idx, text))
self.set_page_title(idx, text)
if idx == self.currentIndex():
self.update_window_title()
self._update_window_title()
@pyqtSlot(browsertab.AbstractTab, QUrl)
def on_url_changed(self, tab, url):
@ -590,7 +595,7 @@ class TabbedBrowser(tabwidget.TabWidget):
tab: The WebView where the title was changed.
icon: The new icon
"""
if not config.get('tabs', 'show-favicons'):
if not config.val.tabs.favicons.show:
return
try:
idx = self._tab_index(tab)
@ -598,7 +603,7 @@ class TabbedBrowser(tabwidget.TabWidget):
# We can get signals for tabs we already deleted...
return
self.setTabIcon(idx, icon)
if config.get('tabs', 'tabs-are-windows'):
if config.val.tabs.tabs_are_windows:
self.window().setWindowIcon(icon)
@pyqtSlot(usertypes.KeyMode)
@ -635,7 +640,7 @@ class TabbedBrowser(tabwidget.TabWidget):
scope='window', window=self._win_id)
self._now_focused = tab
self.current_tab_changed.emit(tab)
QTimer.singleShot(0, self.update_window_title)
QTimer.singleShot(0, self._update_window_title)
self._tab_insert_idx_left = self.currentIndex()
self._tab_insert_idx_right = self.currentIndex() + 1
@ -651,14 +656,14 @@ class TabbedBrowser(tabwidget.TabWidget):
except TabDeletedError:
# We can get signals for tabs we already deleted...
return
start = config.get('colors', 'tabs.indicator.start')
stop = config.get('colors', 'tabs.indicator.stop')
system = config.get('colors', 'tabs.indicator.system')
start = config.val.colors.tabs.indicator.start
stop = config.val.colors.tabs.indicator.stop
system = config.val.colors.tabs.indicator.system
color = utils.interpolate_color(start, stop, perc, system)
self.set_tab_indicator_color(idx, color)
self.update_tab_title(idx)
self._update_tab_title(idx)
if idx == self.currentIndex():
self.update_window_title()
self._update_window_title()
def on_load_finished(self, tab, ok):
"""Adjust tab indicator when loading finished."""
@ -668,16 +673,16 @@ class TabbedBrowser(tabwidget.TabWidget):
# We can get signals for tabs we already deleted...
return
if ok:
start = config.get('colors', 'tabs.indicator.start')
stop = config.get('colors', 'tabs.indicator.stop')
system = config.get('colors', 'tabs.indicator.system')
start = config.val.colors.tabs.indicator.start
stop = config.val.colors.tabs.indicator.stop
system = config.val.colors.tabs.indicator.system
color = utils.interpolate_color(start, stop, 100, system)
else:
color = config.get('colors', 'tabs.indicator.error')
color = config.val.colors.tabs.indicator.error
self.set_tab_indicator_color(idx, color)
self.update_tab_title(idx)
self._update_tab_title(idx)
if idx == self.currentIndex():
self.update_window_title()
self._update_window_title()
@pyqtSlot()
def on_scroll_pos_changed(self):
@ -688,8 +693,8 @@ class TabbedBrowser(tabwidget.TabWidget):
log.webview.debug("Not updating scroll position because index is "
"-1")
return
self.update_window_title()
self.update_tab_title(idx)
self._update_window_title()
self._update_tab_title(idx)
def _on_renderer_process_terminated(self, tab, status, code):
"""Show an error when a renderer process terminated."""
@ -716,7 +721,7 @@ class TabbedBrowser(tabwidget.TabWidget):
url_string = tab.url(requested=True).toDisplayString()
error_page = jinja.render(
'error.html', title="Error loading {}".format(url_string),
url=url_string, error=msg, icon='')
url=url_string, error=msg)
QTimer.singleShot(100, lambda: show_error_page(error_page))
else:
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-58698

View File

@ -57,27 +57,27 @@ class TabWidget(QTabWidget):
self.setTabBar(bar)
bar.tabCloseRequested.connect(self.tabCloseRequested)
bar.tabMoved.connect(functools.partial(
QTimer.singleShot, 0, self.update_tab_titles))
QTimer.singleShot, 0, self._update_tab_titles))
bar.currentChanged.connect(self._on_current_changed)
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self.setDocumentMode(True)
self.setElideMode(Qt.ElideRight)
self.setUsesScrollButtons(True)
bar.setDrawBase(False)
self.init_config()
objreg.get('config').changed.connect(self.init_config)
self._init_config()
config.instance.changed.connect(self._init_config)
@config.change_filter('tabs')
def init_config(self):
def _init_config(self):
"""Initialize attributes based on the config."""
if self is None: # pragma: no cover
# WORKAROUND for PyQt 5.2
return
tabbar = self.tabBar()
self.setMovable(config.get('tabs', 'movable'))
self.setMovable(True)
self.setTabsClosable(False)
position = config.get('tabs', 'position')
selection_behavior = config.get('tabs', 'select-on-remove')
position = config.val.tabs.position
selection_behavior = config.val.tabs.select_on_remove
self.setTabPosition(position)
tabbar.vertical = position in [QTabWidget.West, QTabWidget.East]
tabbar.setSelectionBehaviorOnRemove(selection_behavior)
@ -117,7 +117,7 @@ class TabWidget(QTabWidget):
bar.set_tab_data(idx, 'pinned', pinned)
tab.data.pinned = pinned
self.update_tab_title(idx)
self._update_tab_title(idx)
bar.refresh()
@ -128,21 +128,21 @@ class TabWidget(QTabWidget):
def set_page_title(self, idx, title):
"""Set the tab title user data."""
self.tabBar().set_tab_data(idx, 'page-title', title)
self.update_tab_title(idx)
self._update_tab_title(idx)
def page_title(self, idx):
"""Get the tab title user data."""
return self.tabBar().page_title(idx)
def update_tab_title(self, idx):
def _update_tab_title(self, idx):
"""Update the tab text for the given tab."""
tab = self.widget(idx)
fields = self.get_tab_fields(idx)
fields['title'] = fields['title'].replace('&', '&&')
fields['index'] = idx + 1
fmt = config.get('tabs', 'title-format')
fmt_pinned = config.get('tabs', 'title-format-pinned')
fmt = config.val.tabs.title.format
fmt_pinned = config.val.tabs.title.format_pinned
if tab.data.pinned:
title = '' if fmt_pinned is None else fmt_pinned.format(**fields)
@ -190,22 +190,20 @@ class TabWidget(QTabWidget):
fields['scroll_pos'] = scroll_pos
return fields
def update_tab_titles(self, section='tabs', option='title-format'):
def _update_tab_titles(self):
"""Update all texts."""
if section == 'tabs' and option in ['title-format',
'title-format-pinned']:
for idx in range(self.count()):
self.update_tab_title(idx)
for idx in range(self.count()):
self._update_tab_title(idx)
def tabInserted(self, idx):
"""Update titles when a tab was inserted."""
super().tabInserted(idx)
self.update_tab_titles()
self._update_tab_titles()
def tabRemoved(self, idx):
"""Update titles when a tab was removed."""
super().tabRemoved(idx)
self.update_tab_titles()
self._update_tab_titles()
def addTab(self, page, icon_or_text, text_or_empty=None):
"""Override addTab to use our own text setting logic.
@ -306,24 +304,17 @@ class TabBar(QTabBar):
super().__init__(parent)
self._win_id = win_id
self.setStyle(TabBarStyle())
self.set_font()
config_obj = objreg.get('config')
config_obj.changed.connect(self.set_font)
config_obj.changed.connect(self.set_icon_size)
self._set_font()
config.instance.changed.connect(self._on_config_changed)
self.vertical = False
self._auto_hide_timer = QTimer()
self._auto_hide_timer.setSingleShot(True)
self._auto_hide_timer.setInterval(
config.get('tabs', 'show-switching-delay'))
self._auto_hide_timer.timeout.connect(self.maybe_hide)
self._on_show_switching_delay_changed()
self.setAutoFillBackground(True)
self.set_colors()
self._set_colors()
self.pinned_count = 0
config_obj.changed.connect(self.set_colors)
QTimer.singleShot(0, self.maybe_hide)
config_obj.changed.connect(self.on_tab_colors_changed)
config_obj.changed.connect(self.on_show_switching_delay_changed)
config_obj.changed.connect(self.tabs_show)
def __repr__(self):
return utils.get_repr(self, count=self.count())
@ -332,29 +323,37 @@ class TabBar(QTabBar):
"""Get the current tab object."""
return self.parent().currentWidget()
@config.change_filter('tabs', 'show')
def tabs_show(self):
"""Hide or show tab bar if needed when tabs->show got changed."""
self.maybe_hide()
@pyqtSlot(str)
def _on_config_changed(self, option):
if option == 'fonts.tabs':
self._set_font()
elif option == 'tabs.favicons.scale':
self._set_icon_size()
elif option == 'colors.tabs.bar.bg':
self._set_colors()
elif option == 'tabs.show_switching_delay':
self._on_show_switching_delay_changed()
elif option == 'tabs.show':
self.maybe_hide()
@config.change_filter('tabs', 'show-switching-delay')
def on_show_switching_delay_changed(self):
"""Set timer interval when tabs->show-switching-delay got changed."""
self._auto_hide_timer.setInterval(
config.get('tabs', 'show-switching-delay'))
if option.startswith('colors.tabs.'):
self.update()
def _on_show_switching_delay_changed(self):
"""Set timer interval when tabs.show_switching_delay got changed."""
self._auto_hide_timer.setInterval(config.val.tabs.show_switching_delay)
def on_current_changed(self):
"""Show tab bar when current tab got changed."""
self.maybe_hide() # for fullscreen tabs
show = config.get('tabs', 'show')
if show == 'switching':
if config.val.tabs.show == 'switching':
self.show()
self._auto_hide_timer.start()
@pyqtSlot()
def maybe_hide(self):
"""Hide the tab bar if needed."""
show = config.get('tabs', 'show')
show = config.val.tabs.show
tab = self._current_tab()
if (show in ['never', 'switching'] or
(show == 'multiple' and self.count() == 1) or
@ -409,35 +408,26 @@ class TabBar(QTabBar):
# code sets layoutDirty so it actually relayouts the tabs.
self.setIconSize(self.iconSize())
@config.change_filter('fonts', 'tabbar')
def set_font(self):
def _set_font(self):
"""Set the tab bar font."""
self.setFont(config.get('fonts', 'tabbar'))
self.set_icon_size()
self.setFont(config.val.fonts.tabs)
self._set_icon_size()
@config.change_filter('tabs', 'favicon-scale')
def set_icon_size(self):
def _set_icon_size(self):
"""Set the tab bar favicon size."""
size = self.fontMetrics().height() - 2
size *= config.get('tabs', 'favicon-scale')
size *= config.val.tabs.favicons.scale
self.setIconSize(QSize(size, size))
@config.change_filter('colors', 'tabs.bg.bar')
def set_colors(self):
def _set_colors(self):
"""Set the tab bar colors."""
p = self.palette()
p.setColor(QPalette.Window, config.get('colors', 'tabs.bg.bar'))
p.setColor(QPalette.Window, config.val.colors.tabs.bar.bg)
self.setPalette(p)
@pyqtSlot(str, str)
def on_tab_colors_changed(self, section, option):
"""Set the tab colors."""
if section == 'colors' and option.startswith('tabs.'):
self.update()
def mousePressEvent(self, e):
"""Override mousePressEvent to close tabs if configured."""
button = config.get('tabs', 'close-mouse-button')
button = config.val.tabs.close_mouse_button
if (e.button() == Qt.RightButton and button == 'right' or
e.button() == Qt.MiddleButton and button == 'middle'):
e.accept()
@ -458,7 +448,7 @@ class TabBar(QTabBar):
A QSize.
"""
icon = self.tabIcon(index)
padding = config.get('tabs', 'padding')
padding = config.val.tabs.padding
padding_h = padding.left + padding.right
padding_v = padding.top + padding.bottom
if icon.isNull():
@ -469,10 +459,9 @@ class TabBar(QTabBar):
icon_size = icon.actualSize(QSize(extent, extent))
padding_h += self.style().pixelMetric(
PixelMetrics.icon_padding, None, self)
indicator_width = config.get('tabs', 'indicator-width')
height = self.fontMetrics().height() + padding_v
width = (self.fontMetrics().width('\u2026') + icon_size.width() +
padding_h + indicator_width)
padding_h + config.val.tabs.width.indicator)
return QSize(width, height)
def tabSizeHint(self, index):
@ -489,7 +478,7 @@ class TabBar(QTabBar):
minimum_size = self.minimumTabSizeHint(index)
height = minimum_size.height()
if self.vertical:
confwidth = str(config.get('tabs', 'width'))
confwidth = str(config.val.tabs.width.bar)
if confwidth.endswith('%'):
main_window = objreg.get('main-window', scope='window',
window=self._win_id)
@ -504,19 +493,30 @@ class TabBar(QTabBar):
# want to ensure it's valid in this special case.
return QSize()
else:
tab_width_pinned_conf = config.get('tabs', 'pinned-width')
try:
pinned = self.tab_data(index, 'pinned')
except KeyError:
pinned = False
no_pinned_count = self.count() - self.pinned_count
pinned_width = tab_width_pinned_conf * self.pinned_count
pinned_width = config.val.tabs.width.pinned * self.pinned_count
no_pinned_width = self.width() - pinned_width
if pinned:
width = tab_width_pinned_conf
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)
else:
# Tabs should attempt to occupy the whole window width. If
@ -547,31 +547,21 @@ class TabBar(QTabBar):
def paintEvent(self, _e):
"""Override paintEvent to draw the tabs like we want to."""
# pylint: disable=bad-config-call
# WORKAROUND for https://bitbucket.org/logilab/astroid/issue/104
p = QStylePainter(self)
selected = self.currentIndex()
for idx in range(self.count()):
tab = QStyleOptionTab()
self.initStyleOption(tab, idx)
bg_parts = ['tabs', 'bg']
fg_parts = ['tabs', 'fg']
# pylint: disable=bad-config-option
setting = config.val.colors.tabs
# pylint: enable=bad-config-option
if idx == selected:
bg_parts.append('selected')
fg_parts.append('selected')
setting = setting.selected
setting = setting.odd if idx % 2 else setting.even
if idx % 2:
bg_parts.append('odd')
fg_parts.append('odd')
else:
bg_parts.append('even')
fg_parts.append('even')
bg_color = config.get('colors', '.'.join(bg_parts))
fg_color = config.get('colors', '.'.join(fg_parts))
tab.palette.setColor(QPalette.Window, bg_color)
tab.palette.setColor(QPalette.WindowText, fg_color)
tab.palette.setColor(QPalette.Window, setting.bg)
tab.palette.setColor(QPalette.WindowText, setting.fg)
indicator_color = self.tab_indicator_color(idx)
tab.palette.setColor(QPalette.Base, indicator_color)
@ -597,7 +587,7 @@ class TabBar(QTabBar):
Args:
e: The QWheelEvent
"""
if config.get('tabs', 'mousewheel-tab-switching'):
if config.val.tabs.mousewheel_switching:
super().wheelEvent(e)
else:
tabbed_browser = objreg.get('tabbed-browser', scope='window',
@ -707,7 +697,7 @@ class TabBarStyle(QCommonStyle):
elif element == QStyle.CE_TabBarTabLabel:
if not opt.icon.isNull() and layouts.icon.isValid():
self._draw_icon(layouts, opt, p)
alignment = (config.get('tabs', 'title-alignment') |
alignment = (config.val.tabs.title.alignment |
Qt.AlignVCenter | Qt.TextHideMnemonic)
self._style.drawItemText(p, layouts.text, alignment, opt.palette,
opt.state & QStyle.State_Enabled,
@ -787,8 +777,8 @@ class TabBarStyle(QCommonStyle):
Return:
A Layout namedtuple with two QRects.
"""
padding = config.get('tabs', 'padding')
indicator_padding = config.get('tabs', 'indicator-padding')
padding = config.val.tabs.padding
indicator_padding = config.val.tabs.indicator_padding
text_rect = QRect(opt.rect)
if not text_rect.isValid():
@ -799,7 +789,7 @@ class TabBarStyle(QCommonStyle):
text_rect.adjust(padding.left, padding.top, -padding.right,
-padding.bottom)
indicator_width = config.get('tabs', 'indicator-width')
indicator_width = config.val.tabs.width.indicator
if indicator_width == 0:
indicator_rect = QRect()
else:
@ -842,10 +832,10 @@ class TabBarStyle(QCommonStyle):
icon_state = (QIcon.On if opt.state & QStyle.State_Selected
else QIcon.Off)
# reserve space for favicon when tab bar is vertical (issue #1968)
position = config.get('tabs', 'position')
position = config.val.tabs.position
if (opt.icon.isNull() and
position in [QTabWidget.East, QTabWidget.West] and
config.get('tabs', 'show-favicons')):
config.val.tabs.favicons.show):
tab_icon_size = icon_size
else:
actual_size = opt.icon.actualSize(icon_size, icon_mode, icon_state)

View File

@ -21,7 +21,8 @@
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject
from qutebrowser.utils import usertypes, log
from qutebrowser.utils import usertypes, log, standarddir, objreg
from qutebrowser.misc import lineparser
class HistoryEmptyError(Exception):
@ -129,3 +130,14 @@ class History(QObject):
if not self.history or text != self.history[-1]:
self.history.append(text)
self.changed.emit()
def init():
"""Initialize the LimitLineParser storing the history."""
save_manager = objreg.get('save-manager')
command_history = lineparser.LimitLineParser(
standarddir.data(), 'cmd-history',
limit='completion.cmd_history_max_items')
objreg.register('command-history', command_history)
save_manager.add_saveable('command-history', command_history.save,
command_history.changed)

View File

@ -51,8 +51,8 @@ class ConsoleLineEdit(miscwidgets.CommandLineEdit):
_namespace: The local namespace of the interpreter.
"""
super().__init__(parent=parent)
self.update_font()
objreg.get('config').changed.connect(self.update_font)
self._update_font()
config.instance.changed.connect(self._update_font)
self._history = cmdhistory.History(parent=self)
self.returnPressed.connect(self.on_return_pressed)
@ -102,10 +102,10 @@ class ConsoleLineEdit(miscwidgets.CommandLineEdit):
else:
super().keyPressEvent(e)
@config.change_filter('fonts', 'debug-console')
def update_font(self):
@config.change_filter('fonts.debug_console')
def _update_font(self):
"""Set the correct font."""
self.setFont(config.get('fonts', 'debug-console'))
self.setFont(config.val.fonts.debug_console)
class ConsoleTextEdit(QTextEdit):
@ -116,17 +116,17 @@ class ConsoleTextEdit(QTextEdit):
super().__init__(parent)
self.setAcceptRichText(False)
self.setReadOnly(True)
objreg.get('config').changed.connect(self.update_font)
self.update_font()
config.instance.changed.connect(self._update_font)
self._update_font()
self.setFocusPolicy(Qt.ClickFocus)
def __repr__(self):
return utils.get_repr(self)
@config.change_filter('fonts', 'debug-console')
def update_font(self):
@config.change_filter('fonts.debug_console')
def _update_font(self):
"""Update font when config changed."""
self.setFont(config.get('fonts', 'debug-console'))
self.setFont(config.val.fonts.debug_console)
def append_text(self, text):
"""Append new text and scroll output to bottom.

View File

@ -255,8 +255,8 @@ class _CrashDialog(QDialog):
except Exception:
self._crash_info.append(("Version info", traceback.format_exc()))
try:
conf = objreg.get('config')
self._crash_info.append(("Config", conf.dump_userconfig()))
self._crash_info.append(("Config",
config.instance.dump_userconfig()))
except Exception:
self._crash_info.append(("Config", traceback.format_exc()))
try:
@ -432,7 +432,7 @@ class ExceptionCrashDialog(_CrashDialog):
self._chk_log = QCheckBox("Include a debug log in the report",
checked=True)
try:
if config.get('general', 'private-browsing'):
if config.val.content.private_browsing:
self._chk_log.setChecked(False)
except Exception:
log.misc.exception("Error while checking private browsing mode")
@ -524,7 +524,7 @@ class FatalCrashDialog(_CrashDialog):
"accessed pages in the report.",
checked=True)
try:
if config.get('general', 'private-browsing'):
if config.val.content.private_browsing:
self._chk_history.setChecked(False)
except Exception:
log.misc.exception("Error while checking private browsing mode")
@ -635,8 +635,7 @@ def dump_exception_info(exc, pages, cmdhist, qobjects):
traceback.print_exc()
print("\n---- Config ----", file=sys.stderr)
try:
conf = objreg.get('config')
print(conf.dump_userconfig(), file=sys.stderr)
print(config.instance.dump_userconfig(), file=sys.stderr)
except Exception:
traceback.print_exc()
print("\n---- Commandline args ----", file=sys.stderr)

View File

@ -75,7 +75,7 @@ class ExternalEditor(QObject):
try:
if exitcode != 0:
return
encoding = config.get('general', 'editor-encoding')
encoding = config.val.editor.encoding
try:
with open(self._file.name, 'r', encoding=encoding) as f:
text = f.read()
@ -102,14 +102,14 @@ class ExternalEditor(QObject):
if self._text is not None:
raise ValueError("Already editing a file!")
self._text = text
encoding = config.get('general', 'editor-encoding')
try:
# Close while the external process is running, as otherwise systems
# with exclusive write access (e.g. Windows) may fail to update
# the file from the external editor, see
# https://github.com/qutebrowser/qutebrowser/issues/1767
with tempfile.NamedTemporaryFile(
mode='w', prefix='qutebrowser-editor-', encoding=encoding,
mode='w', prefix='qutebrowser-editor-',
encoding=config.val.editor.encoding,
delete=False) as fobj:
if text:
fobj.write(text)
@ -120,7 +120,7 @@ class ExternalEditor(QObject):
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.get('general', 'editor')
editor = config.val.editor.command
executable = editor[0]
args = [arg.replace('{}', self._file.name) for arg in editor[1:]]
log.procs.debug("Calling \"{}\" with args {}".format(executable, args))

View File

@ -30,8 +30,8 @@ import fnmatch
from PyQt5.QtWidgets import QLabel, QSizePolicy
from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt
from qutebrowser.config import config, style
from qutebrowser.utils import objreg, utils, usertypes
from qutebrowser.config import config
from qutebrowser.utils import utils, usertypes
class KeyHintView(QLabel):
@ -47,11 +47,11 @@ class KeyHintView(QLabel):
STYLESHEET = """
QLabel {
font: {{ font['keyhint'] }};
color: {{ color['keyhint.fg'] }};
background-color: {{ color['keyhint.bg'] }};
font: {{ conf.fonts.keyhint }};
color: {{ conf.colors.keyhint.fg }};
background-color: {{ conf.colors.keyhint.bg }};
padding: 6px;
{% if config.get('ui', 'status-position') == 'top' %}
{% if conf.statusbar.position == 'top' %}
border-bottom-right-radius: 6px;
{% else %}
border-top-right-radius: 6px;
@ -68,7 +68,7 @@ class KeyHintView(QLabel):
self.hide()
self._show_timer = usertypes.Timer(self, 'keyhint_show')
self._show_timer.timeout.connect(self.show)
style.set_register_stylesheet(self)
config.set_register_stylesheet(self)
def __repr__(self):
return utils.get_repr(self, win_id=self._win_id)
@ -90,16 +90,14 @@ class KeyHintView(QLabel):
self.hide()
return
blacklist = config.get('ui', 'keyhint-blacklist') or []
keyconf = objreg.get('key-config')
def blacklisted(keychain):
return any(fnmatch.fnmatchcase(keychain, glob)
for glob in blacklist)
for glob in config.val.keyhint.blacklist)
bindings = [(k, v) for (k, v)
in keyconf.get_bindings_for(modename).items()
if k.startswith(prefix) and not utils.is_special_key(k) and
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)]
if not bindings:
@ -107,9 +105,9 @@ class KeyHintView(QLabel):
return
# delay so a quickly typed keychain doesn't display hints
self._show_timer.setInterval(config.get('ui', 'keyhint-delay'))
self._show_timer.setInterval(config.val.keyhint.delay)
self._show_timer.start()
suffix_color = html.escape(config.get('colors', 'keyhint.fg.suffix'))
suffix_color = html.escape(config.val.colors.keyhint.suffix.fg)
text = ''
for key, cmd in bindings:

View File

@ -25,7 +25,7 @@ import contextlib
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject
from qutebrowser.utils import log, utils, objreg, qtutils
from qutebrowser.utils import log, utils, qtutils
from qutebrowser.config import config
@ -195,8 +195,7 @@ class LimitLineParser(LineParser):
"""A LineParser with a limited count of lines.
Attributes:
_limit: The config section/option used to limit the maximum number of
lines.
_limit: The config option used to limit the maximum number of lines.
"""
def __init__(self, configdir, fname, *, limit, binary=False, parent=None):
@ -205,33 +204,33 @@ class LimitLineParser(LineParser):
Args:
configdir: Directory to read the config from, or None.
fname: Filename of the config file.
limit: Config tuple (section, option) which contains a limit.
limit: Config option which contains a limit.
binary: Whether to open the file in binary mode.
"""
super().__init__(configdir, fname, binary=binary, parent=parent)
self._limit = limit
if limit is not None and configdir is not None:
objreg.get('config').changed.connect(self.cleanup_file)
config.instance.changed.connect(self._cleanup_file)
def __repr__(self):
return utils.get_repr(self, constructor=True,
configdir=self._configdir, fname=self._fname,
limit=self._limit, binary=self._binary)
@pyqtSlot(str, str)
def cleanup_file(self, section, option):
@pyqtSlot(str)
def _cleanup_file(self, option):
"""Delete the file if the limit was changed to 0."""
assert self._configfile is not None
if (section, option) != self._limit:
if option != self._limit:
return
value = config.get(section, option)
value = config.instance.get(option)
if value == 0:
if os.path.exists(self._configfile):
os.remove(self._configfile)
def save(self):
"""Save the config file."""
limit = config.get(*self._limit)
limit = config.instance.get(self._limit)
if limit == 0:
return
do_save = self._prepare_save()

View File

@ -24,7 +24,8 @@ from PyQt5.QtWidgets import (QLineEdit, QWidget, QHBoxLayout, QLabel,
QStyleOption, QStyle, QLayout, QApplication)
from PyQt5.QtGui import QValidator, QPainter
from qutebrowser.utils import utils, objreg, qtutils, log, usertypes
from qutebrowser.config import config
from qutebrowser.utils import utils, qtutils, log, usertypes
from qutebrowser.misc import cmdhistory, objects
@ -288,8 +289,7 @@ class FullscreenNotification(QLabel):
padding: 30px;
""")
key_config = objreg.get('key-config')
all_bindings = key_config.get_reverse_bindings_for('normal')
all_bindings = config.key_instance.get_reverse_bindings_for('normal')
bindings = all_bindings.get('fullscreen --leave')
if bindings:
key = bindings[0]

View File

@ -41,6 +41,7 @@ def msgbox(parent, title, text, *, icon, buttons=QMessageBox.Ok,
A new QMessageBox.
"""
box = QMessageBox(parent)
box.setAttribute(Qt.WA_DeleteOnClose)
box.setIcon(icon)
box.setStandardButtons(buttons)
if on_finished is not None:

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