2017-06-20 16:27:54 +02:00
|
|
|
# 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."""
|
|
|
|
|
2017-09-14 16:16:14 +02:00
|
|
|
import types
|
2017-06-20 16:27:54 +02:00
|
|
|
import os.path
|
2017-06-20 16:53:46 +02:00
|
|
|
import textwrap
|
2017-09-14 21:51:29 +02:00
|
|
|
import traceback
|
2017-06-20 16:27:54 +02:00
|
|
|
import configparser
|
2017-09-14 21:51:29 +02:00
|
|
|
import contextlib
|
2017-06-20 16:27:54 +02:00
|
|
|
|
2017-09-15 16:41:32 +02:00
|
|
|
import yaml
|
2017-06-20 16:27:54 +02:00
|
|
|
from PyQt5.QtCore import QSettings
|
|
|
|
|
2017-09-17 21:04:34 +02:00
|
|
|
import qutebrowser
|
2017-09-18 09:41:12 +02:00
|
|
|
from qutebrowser.config import configexc, config
|
2017-09-17 22:52:37 +02:00
|
|
|
from qutebrowser.utils import standarddir, utils, qtutils
|
2017-06-20 16:27:54 +02:00
|
|
|
|
|
|
|
|
2017-09-17 21:04:34 +02:00
|
|
|
# The StateConfig instance
|
|
|
|
state = None
|
|
|
|
|
|
|
|
|
2017-06-20 16:27:54 +02:00
|
|
|
class StateConfig(configparser.ConfigParser):
|
|
|
|
|
|
|
|
"""The "state" file saving various application state."""
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
super().__init__()
|
|
|
|
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
|
2017-09-18 21:15:14 +02:00
|
|
|
|
|
|
|
deleted_keys = ['fooled', 'backend-warning-shown']
|
|
|
|
for key in deleted_keys:
|
|
|
|
self['general'].pop(key, None)
|
2017-09-17 20:06:35 +02:00
|
|
|
|
|
|
|
def init_save_manager(self, save_manager):
|
|
|
|
"""Make sure the config gets saved properly.
|
|
|
|
|
|
|
|
We do this outside of __init__ because the config gets created before
|
|
|
|
the save_manager exists.
|
|
|
|
"""
|
2017-06-20 16:27:54 +02:00
|
|
|
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)
|
|
|
|
|
|
|
|
|
2017-06-20 16:53:46 +02:00
|
|
|
class YamlConfig:
|
|
|
|
|
2017-07-19 08:22:00 +02:00
|
|
|
"""A config stored on disk as YAML file.
|
|
|
|
|
|
|
|
Class attributes:
|
|
|
|
VERSION: The current version number of the config file.
|
|
|
|
"""
|
|
|
|
|
|
|
|
VERSION = 1
|
2017-06-20 16:53:46 +02:00
|
|
|
|
|
|
|
def __init__(self):
|
2017-09-13 14:19:36 +02:00
|
|
|
self._filename = os.path.join(standarddir.config(auto=True),
|
|
|
|
'autoconfig.yml')
|
2017-09-19 17:26:03 +02:00
|
|
|
self._values = {}
|
2017-09-20 10:04:34 +02:00
|
|
|
self._dirty = None
|
2017-06-20 16:53:46 +02:00
|
|
|
|
2017-09-17 20:06:35 +02:00
|
|
|
def init_save_manager(self, save_manager):
|
|
|
|
"""Make sure the config gets saved properly.
|
|
|
|
|
|
|
|
We do this outside of __init__ because the config gets created before
|
|
|
|
the save_manager exists.
|
|
|
|
"""
|
|
|
|
save_manager.add_saveable('yaml-config', self._save)
|
|
|
|
|
2017-09-19 17:26:03 +02:00
|
|
|
def __getitem__(self, name):
|
|
|
|
return self._values[name]
|
|
|
|
|
|
|
|
def __setitem__(self, name, value):
|
2017-09-20 10:04:34 +02:00
|
|
|
self._dirty = True
|
2017-09-19 17:26:03 +02:00
|
|
|
self._values[name] = value
|
|
|
|
|
|
|
|
def __contains__(self, name):
|
|
|
|
return name in self._values
|
|
|
|
|
|
|
|
def __iter__(self):
|
|
|
|
return iter(self._values.items())
|
|
|
|
|
2017-06-20 16:53:46 +02:00
|
|
|
def _save(self):
|
2017-09-19 14:08:59 +02:00
|
|
|
"""Save the settings to the YAML file if they've changed."""
|
2017-09-20 10:04:34 +02:00
|
|
|
if not self._dirty:
|
2017-09-19 14:08:59 +02:00
|
|
|
return
|
|
|
|
|
2017-09-19 17:26:03 +02:00
|
|
|
data = {'config_version': self.VERSION, 'global': self._values}
|
2017-06-20 16:53:46 +02:00
|
|
|
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):
|
2017-09-19 14:08:59 +02:00
|
|
|
"""Load configuration from the configured YAML file."""
|
2017-06-20 16:53:46 +02:00
|
|
|
try:
|
|
|
|
with open(self._filename, 'r', encoding='utf-8') as f:
|
2017-09-15 16:41:32 +02:00
|
|
|
yaml_data = utils.yaml_load(f)
|
2017-06-20 16:53:46 +02:00
|
|
|
except FileNotFoundError:
|
2017-09-19 15:30:28 +02:00
|
|
|
return {}
|
2017-09-15 16:41:32 +02:00
|
|
|
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",
|
2017-09-15 17:18:11 +02:00
|
|
|
"Toplevel object does not contain 'global' key")
|
2017-09-15 16:41:32 +02:00
|
|
|
raise configexc.ConfigFileErrors('autoconfig.yml', [desc])
|
|
|
|
except TypeError:
|
|
|
|
desc = configexc.ConfigErrorDesc("While loading data",
|
2017-09-15 17:18:11 +02:00
|
|
|
"Toplevel object is not a dict")
|
2017-09-15 16:41:32 +02:00
|
|
|
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])
|
|
|
|
|
2017-09-19 17:26:03 +02:00
|
|
|
self._values = global_obj
|
2017-09-20 10:04:34 +02:00
|
|
|
self._dirty = False
|
2017-06-20 16:53:46 +02:00
|
|
|
|
|
|
|
|
2017-09-14 16:16:14 +02:00
|
|
|
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.
|
2017-09-14 21:51:29 +02:00
|
|
|
errors: Errors which occurred while setting options.
|
2017-09-14 16:16:14 +02:00
|
|
|
"""
|
|
|
|
|
2017-09-18 09:41:12 +02:00
|
|
|
def __init__(self, conf, keyconfig):
|
|
|
|
self._config = conf
|
2017-09-14 16:16:14 +02:00
|
|
|
self._keyconfig = keyconfig
|
|
|
|
self.load_autoconfig = True
|
2017-09-14 21:51:29 +02:00
|
|
|
self.errors = []
|
|
|
|
|
|
|
|
@contextlib.contextmanager
|
|
|
|
def _handle_error(self, action, name):
|
|
|
|
try:
|
|
|
|
yield
|
|
|
|
except configexc.Error as e:
|
2017-09-14 22:47:06 +02:00
|
|
|
text = "While {} '{}'".format(action, name)
|
|
|
|
self.errors.append(configexc.ConfigErrorDesc(text, e))
|
2017-09-14 21:51:29 +02:00
|
|
|
|
|
|
|
def finalize(self):
|
2017-09-15 00:10:24 +02:00
|
|
|
"""Do work which needs to be done after reading config.py."""
|
2017-09-14 21:51:29 +02:00
|
|
|
self._config.update_mutables()
|
2017-09-14 16:16:14 +02:00
|
|
|
|
|
|
|
def get(self, name):
|
2017-09-14 21:51:29 +02:00
|
|
|
with self._handle_error('getting', name):
|
|
|
|
return self._config.get_obj(name)
|
2017-09-14 16:16:14 +02:00
|
|
|
|
|
|
|
def set(self, name, value):
|
2017-09-14 21:51:29 +02:00
|
|
|
with self._handle_error('setting', name):
|
|
|
|
self._config.set_obj(name, value)
|
2017-09-14 16:16:14 +02:00
|
|
|
|
2017-09-19 13:18:44 +02:00
|
|
|
def bind(self, key, command, mode='normal', *, force=False):
|
2017-09-14 21:51:29 +02:00
|
|
|
with self._handle_error('binding', key):
|
|
|
|
self._keyconfig.bind(key, command, mode=mode, force=force)
|
2017-09-14 16:16:14 +02:00
|
|
|
|
2017-09-19 13:18:44 +02:00
|
|
|
def unbind(self, key, mode='normal'):
|
2017-09-14 21:51:29 +02:00
|
|
|
with self._handle_error('unbinding', key):
|
|
|
|
self._keyconfig.unbind(key, mode=mode)
|
2017-09-14 16:16:14 +02:00
|
|
|
|
|
|
|
|
|
|
|
def read_config_py(filename=None):
|
|
|
|
"""Read a config.py file."""
|
2017-09-15 14:08:25 +02:00
|
|
|
api = ConfigAPI(config.instance, config.key_instance)
|
2017-09-14 21:51:29 +02:00
|
|
|
|
2017-09-14 16:16:14 +02:00
|
|
|
if filename is None:
|
|
|
|
filename = os.path.join(standarddir.config(), 'config.py')
|
|
|
|
if not os.path.exists(filename):
|
2017-09-15 14:08:25 +02:00
|
|
|
return api
|
2017-09-14 16:16:14 +02:00
|
|
|
|
2017-09-14 22:47:06 +02:00
|
|
|
container = config.ConfigContainer(config.instance, configapi=api)
|
2017-09-15 14:08:25 +02:00
|
|
|
basename = os.path.basename(filename)
|
2017-09-14 22:47:06 +02:00
|
|
|
|
2017-09-14 16:16:14 +02:00
|
|
|
module = types.ModuleType('config')
|
|
|
|
module.config = api
|
2017-09-14 22:47:06 +02:00
|
|
|
module.c = container
|
2017-09-14 16:16:14 +02:00
|
|
|
module.__file__ = filename
|
2017-09-14 22:47:06 +02:00
|
|
|
|
2017-09-14 21:51:29 +02:00
|
|
|
try:
|
|
|
|
with open(filename, mode='rb') as f:
|
|
|
|
source = f.read()
|
|
|
|
except OSError as e:
|
2017-09-14 22:47:06 +02:00
|
|
|
text = "Error while reading {}".format(basename)
|
|
|
|
desc = configexc.ConfigErrorDesc(text, e)
|
|
|
|
raise configexc.ConfigFileErrors(basename, [desc])
|
2017-09-14 21:51:29 +02:00
|
|
|
|
|
|
|
try:
|
|
|
|
code = compile(source, filename, 'exec')
|
2017-09-15 19:46:38 +02:00
|
|
|
except (ValueError, TypeError) as e:
|
2017-09-14 21:51:29 +02:00
|
|
|
# source contains NUL bytes
|
2017-09-14 22:47:06 +02:00
|
|
|
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])
|
2017-09-14 21:51:29 +02:00
|
|
|
|
|
|
|
try:
|
|
|
|
exec(code, module.__dict__)
|
2017-09-14 22:47:06 +02:00
|
|
|
except Exception as e:
|
|
|
|
api.errors.append(configexc.ConfigErrorDesc(
|
|
|
|
"Unhandled exception",
|
|
|
|
exception=e, traceback=traceback.format_exc()))
|
2017-09-14 21:51:29 +02:00
|
|
|
|
|
|
|
api.finalize()
|
2017-09-14 16:16:14 +02:00
|
|
|
return api
|
|
|
|
|
|
|
|
|
2017-09-15 17:22:50 +02:00
|
|
|
def init():
|
2017-06-20 16:27:54 +02:00
|
|
|
"""Initialize config storage not related to the main config."""
|
2017-09-17 21:04:34 +02:00
|
|
|
global state
|
2017-06-20 16:27:54 +02:00
|
|
|
state = StateConfig()
|
2017-09-17 21:04:34 +02:00
|
|
|
state['general']['version'] = qutebrowser.__version__
|
2017-06-20 16:27:54 +02:00
|
|
|
|
|
|
|
# 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
|
|
|
|
|
2017-09-13 14:19:36 +02:00
|
|
|
path = os.path.join(standarddir.config(auto=True), 'qsettings')
|
2017-06-20 16:27:54 +02:00
|
|
|
for fmt in [QSettings.NativeFormat, QSettings.IniFormat]:
|
|
|
|
QSettings.setPath(fmt, QSettings.UserScope, path)
|