qutebrowser/qutebrowser/config/keyconfparser.py
2014-09-09 22:38:14 +02:00

185 lines
6.8 KiB
Python

# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014 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
from qutebrowser.config import configdata, textwrapper
from qutebrowser.commands import cmdutils
from qutebrowser.utils import log
class KeyConfigError(Exception):
"""Raised on errors with the key config.
Attributes:
lineno: The config line in which the exception occured.
"""
def __init__(self, msg=None):
super().__init__(msg)
self.lineno = None
class KeyConfigParser:
"""Parser for the keybind config."""
def __init__(self, configdir, fname):
"""Constructor.
Args:
configdir: The directory to save the configs in.
fname: The filename of the config.
"""
self._configdir = configdir
self._configfile = os.path.join(self._configdir, fname)
self._cur_section = None
self._cur_command = None
# Mapping of section name(s) to keybinding -> command dicts.
self.keybindings = collections.OrderedDict()
if not os.path.exists(self._configfile):
self._load_default()
else:
self._read()
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 _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))
with open(self._configfile, 'w', encoding='utf-8') as f:
f.write(str(self))
def _normalize_sectname(self, s):
"""Normalize a section string like 'foo, bar,baz' to 'bar,baz,foo'."""
return ','.join(sorted(s.split(',')))
def _load_default(self):
"""Load the built-in default keybindings."""
for sectname, sect in configdata.KEY_DATA.items():
sectname = self._normalize_sectname(sectname)
if not sect:
self.keybindings[sectname] = collections.OrderedDict()
else:
for command, keychains in sect.items():
for e in keychains:
self._add_binding(sectname, e, command)
def _read(self):
"""Read the config file from disk and parse it."""
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 as e:
e.lineno = i
raise
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:
command = line.split(maxsplit=1)[0]
if command not in cmdutils.cmd_dict:
raise KeyConfigError("Invalid command '{}'!".format(command))
self._cur_command = line
def _read_keybinding(self, line):
"""Read a keybinding from a line."""
if self._cur_command is None:
raise KeyConfigError("Got keybinding '{}' 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):
"""Add a new binding from keychain to command in section sectname."""
if sectname not in self.keybindings:
self.keybindings[sectname] = collections.OrderedDict()
if keychain in self.get_bindings_for(sectname):
raise KeyConfigError("Duplicate keychain '{}'!".format(keychain))
self.keybindings[sectname][keychain] = command
def get_bindings_for(self, section):
"""Get a dict with all merged keybindings for a section."""
bindings = {}
for sectstring, d in self.keybindings.items():
sects = [s.strip() for s in sectstring.split(',')]
if any(s == section for s in sects):
bindings.update(d)
try:
bindings.update(self.keybindings['all'])
except KeyError:
pass
return bindings