From c0ab0e3f34db38726a4484cce812fd0524439890 Mon Sep 17 00:00:00 2001 From: Thomas Hurst Date: Fri, 15 Aug 2008 04:11:24 +0100 Subject: [PATCH] Introduce a cut-down version of my .ini parser; this one doesn't do anything special with indents, and just builds a simple dict on parse. It does introduce the same error handling (other than that for indenting), so some previously working (but highly dubious) configs might break. Hook up a [keybindings] section, to override the default ones now in config.py; teach TerminatorConfig to merge configured keybindings from available sources; gconf support shouldn't be far away. --- terminatorlib/config.py | 119 ++++++++++++++++++++++--------- terminatorlib/configfile.py | 133 +++++++++++++++++++++++++++++++++++ terminatorlib/keybindings.py | 43 +++-------- terminatorlib/terminator.py | 7 +- 4 files changed, 232 insertions(+), 70 deletions(-) create mode 100644 terminatorlib/configfile.py diff --git a/terminatorlib/config.py b/terminatorlib/config.py index ee36cfe1..f2930419 100755 --- a/terminatorlib/config.py +++ b/terminatorlib/config.py @@ -31,13 +31,11 @@ Trying to read a value that doesn't exist will raise an AttributeError. This is by design. If you want to look something up, set a default for it first.""" -# import standard python libs import os, platform, sys, re - -# import unix-lib import pwd # set this to true to enable debugging output +# These should be moved somewhere better. debug = False def dbg (log = ""): @@ -49,12 +47,15 @@ def err (log = ""): """Print an error message""" print >> sys.stderr, log +from configfile import ConfigFile, ConfigSyntaxError + class TerminatorConfig: """This class is used as the base point of the config system""" callback = None sources = [] def __init__ (self, sources): + self._keys = None for source in sources: if isinstance(source, TerminatorConfValuestore): self.sources.append (source) @@ -62,7 +63,22 @@ class TerminatorConfig: # We always add a default valuestore last so no valid config item ever goes unset source = TerminatorConfValuestoreDefault () self.sources.append (source) - + + def _merge_keybindings(self): + if self._keys: + return self._keys + + self._keys = {} + for source in reversed(self.sources): + try: + val = source.keybindings + self._keys.update(val) + except: + pass + return self._keys + + keybindings = property(_merge_keybindings) + def __getattr__ (self, keyname): for source in self.sources: dbg ("TConfig: Looking for: '%s' in '%s'"%(keyname, source.type)) @@ -133,6 +149,36 @@ class TerminatorConfValuestore: 'close_button_on_tab' : True, 'enable_real_transparency' : False, 'try_posix_regexp' : platform.system() != 'Linux', + 'keybindings' : { + 'zoom_in': 'plus', + 'zoom_out': 'minus', + 'zoom_normal': '0', + 'new_root_tab': 'T', + 'new_tab': 'T', + 'go_next': 'N', + 'go_prev': 'P', + 'split_horiz': 'O', + 'split_vert': 'E', + 'close_term': 'W', + 'copy': 'C', + 'paste': 'V', + 'toggle_scrollbar': 'S', + 'search': 'F', + 'close_window': 'Q', + 'resize_up': 'Up', + 'resize_down': 'Down', + 'resize_left': 'Left', + 'resize_right': 'Right', + 'move_tab_right': 'Page_Down', + 'move_tab_left': 'Page_Up', + 'toggle_zoom': 'X', + 'scaled_zoom': 'Z', + 'next_tab': 'Page_Down', + 'prev_tab': 'Page_Up', + 'go_prev': 'Tab', + 'go_next': 'Tab', + 'full_screen': 'F11', + } } def __getattr__ (self, keyname): @@ -150,11 +196,10 @@ class TerminatorConfValuestoreDefault (TerminatorConfValuestore): class TerminatorConfValuestoreRC (TerminatorConfValuestore): rcfilename = "" - splitter = re.compile("\s*=\s*") + type = "RCFile" #FIXME: use inotify to watch the rc, split __init__ into a parsing function # that can be re-used when rc changes. def __init__ (self): - self.type = "RCFile" try: directory = os.environ['XDG_CONFIG_HOME'] except KeyError, e: @@ -163,39 +208,45 @@ class TerminatorConfValuestoreRC (TerminatorConfValuestore): self.rcfilename = os.path.join(directory, "terminator/config") dbg(" VS_RCFile: config file located at %s" % self.rcfilename) if os.path.exists (self.rcfilename): - rcfile = open (self.rcfilename) - rc = rcfile.readlines () - rcfile.close () + ini = ConfigFile(self.rcfilename) + try: + ini.parse() + except ConfigSyntaxError, e: + print "There was an error parsing your configuration" + print str(e) + sys.exit() - for item in rc: + for key in ini.settings: try: - item = item.strip () - if item and item[0] != '#': - (key, value) = self.splitter.split (item) + value = ini.settings[key] + # Check if this is actually a key we care about + if not self.defaults.has_key (key): + # We should really mention this to the user + continue - # Check if this is actually a key we care about - if not self.defaults.has_key (key): - continue - - deftype = self.defaults[key].__class__.__name__ - if deftype == 'bool': - if value.lower () == 'true': - self.values[key] = True - elif value.lower () == 'false': - self.values[key] = False - else: - raise AttributeError - elif deftype == 'int': - self.values[key] = int (value) - elif deftype == 'float': - self.values[key] = float (value) - elif deftype == 'list': - err (_(" VS_RCFile: Reading list values from .config/terminator/config is not currently supported")) - continue + deftype = self.defaults[key].__class__.__name__ + if deftype == 'bool': + if value.lower () in ('true', 'yes', 'on'): + self.values[key] = True + elif value.lower () in ('false', 'no', 'off'): + self.values[key] = False else: - self.values[key] = value + raise AttributeError + elif deftype == 'int': + self.values[key] = int (value) + elif deftype == 'float': + self.values[key] = float (value) + elif deftype == 'list': + err (_(" VS_RCFile: Reading list values from .config/terminator/config is not currently supported")) + continue + elif deftype == 'dict': + if type(value) != dict: + raise AttributeError + self.values[key] = value + else: + self.values[key] = value - dbg (" VS_RCFile: Set value '%s' to '%s'"%(key, self.values[key])) + dbg (" VS_RCFile: Set value '%s' to '%s'" % (key, self.values[key])) except Exception, e: dbg (" VS_RCFile: %s Exception handling: %s" % (type(e), item)) pass diff --git a/terminatorlib/configfile.py b/terminatorlib/configfile.py new file mode 100644 index 00000000..8c09bcfa --- /dev/null +++ b/terminatorlib/configfile.py @@ -0,0 +1,133 @@ +#!/usr/local/bin/python + +import re +from terminatorlib.config import dbg, debug + +def group(*choices): return '(' + '|'.join(choices) + ')' +def any(*choices): return group(*choices) + '*' +def maybe(*choices): return group(*choices) + '?' + +Newline = re.compile(r'[\r\n]+') +Whitespace = r'[ \f\t]*' +Comment = r'#[^\r\n]*' +Ignore = re.compile(Whitespace + maybe(Comment) + maybe(r'[\r\n]+')) + +WhitespaceRE = re.compile(Whitespace) +CommentRE = re.compile(Comment) + +QuotedStrings = {"'": re.compile(r"'([^'\r\n]*)'"), '"': re.compile(r'"([^"\r\n]*)"')} + +Section = re.compile(r"\[([^\r\n\]]+)\][ \f\t]*") +Setting = re.compile(r"(\w+)\s*=\s*") + +Colorvalue = re.compile(r'(#[0-9a-fA-F]{6}|#[0-9a-fA-F]{3})' + Whitespace + maybe(Comment)) +Barevalue = re.compile(r'([^\r\n#]+)' + Whitespace + maybe(Comment)) + +Tabsize = 8 + +class ConfigSyntaxError(Exception): + def __init__(self, message, cf): + self.message = message + self.file = cf.filename + self.lnum = cf._lnum + self.pos = cf._pos + self.line = cf._line + + def __str__(self): + return "File %s line %d:\n %s\n %s^\n%s" % (repr(self.file), self.lnum, + self.line.rstrip(), + ' ' * self.pos, self.message) + +class ConfigFile: + def __init__(self, filename = None): + self.filename = filename + self.settings = {} + + def _call_if_match(self, re, callable, group = 0): + if self._pos == self._max: + return False + mo = re.match(self._line, self._pos) + if mo: + if callable: + callable(mo.group(group)) + self._pos = mo.end() + return True + else: + return False + + def _call_if_quoted_string(self, callable): + if self._pos == self._max: + return False + chr = self._line[self._pos] + if chr in '"\'': + string = '' + while True: + mo = QuotedStrings[chr].match(self._line, self._pos) + if mo is None: + raise ConfigSyntaxError("Unterminated quoted string", self) + self._pos = mo.end() + if self._line[self._pos - 2] == '\\': + string += mo.group(1)[0:-1] + chr + self._pos -= 1 + else: + string += mo.group(1) + break + callable(string) + return True + else: + return False + + def parse(self): + file = open(self.filename) + rc = file.readlines() + file.close() + + self._indents = [0] + self._pos = 0 + self._max = 0 + self._lnum = 0 + self._line = '' + + self._currsection = None + self._cursetting = None + + for self._line in rc: + self._lnum += 1 + self._pos = 0 + self._max = len(self._line) + dbg("Line %d: %s" % (self._lnum, repr(self._line))) + + self._call_if_match(WhitespaceRE, None) + + # [Section] + self._call_if_match(Section, self._section, 1) + # setting = + if self._call_if_match(Setting, self._setting, 1): + # "quoted value" + if not self._call_if_quoted_string(self._value): + # #000000 # colour that would otherwise be a comment + if not self._call_if_match(Colorvalue, self._value, 1): + # bare value + if not self._call_if_match(Barevalue, self._value, 1): + raise ConfigSyntaxError("Setting without a value", self) + + self._call_if_match(Ignore, None) + + if self._line[self._pos:] != '': + raise ConfigSyntaxError("Unexpected token", self) + + def _section(self, section): + dbg("Section %s" % repr(section)) + self._currsection = section + + def _setting(self, setting): + dbg("Setting %s" % repr(setting)) + self._currsetting = setting + + def _value(self, value): + dbg("Value %s" % repr(value)) + if self._currsection is not None: + self.settings.setdefault(self._currsection, {})[self._currsetting] = value + else: + self.settings[self._currsetting] = value + diff --git a/terminatorlib/keybindings.py b/terminatorlib/keybindings.py index 38a073f6..3d9b185e 100644 --- a/terminatorlib/keybindings.py +++ b/terminatorlib/keybindings.py @@ -1,5 +1,6 @@ import re, gtk +import terminatorlib.config class KeymapError(Exception): def __init__(self, value): @@ -9,38 +10,8 @@ class KeymapError(Exception): def __str__(self): return "Keybinding '%s' invalid: %s" % (self.action, self.value) +Modifier = re.compile('<([^<]+)>') class TerminatorKeybindings: - Modifier = re.compile('<([^<]+)>') - keys = { - 'zoom_in': 'plus', - 'zoom_out': 'minus', - 'zoom_normal': '0', - 'new_root_tab': 'T', - 'new_tab': 'T', - 'go_next': 'N', - 'go_prev': 'P', - 'split_horiz': 'O', - 'split_vert': 'E', - 'close_term': 'W', - 'copy': 'C', - 'paste': 'V', - 'toggle_scrollbar': 'S', - 'search': 'F', - 'close_window': 'Q', - 'resize_up': 'Up', - 'resize_down': 'Down', - 'resize_left': 'Left', - 'resize_right': 'Right', - 'move_tab_right': 'Page_Down', - 'move_tab_left': 'Page_Up', - 'toggle_zoom': 'X', - 'scaled_zoom': 'Z', - 'next_tab': 'Page_Down', - 'prev_tab': 'Page_Up', - 'go_prev': 'Tab', - 'go_next': 'Tab', - 'full_screen': 'F11', - } modifiers = { 'ctrl': gtk.gdk.CONTROL_MASK, @@ -51,6 +22,10 @@ class TerminatorKeybindings: empty = {} def __init__(self): + self.configure({}) + + def configure(self, bindings): + self.keys = bindings self.reload() def reload(self): @@ -74,14 +49,14 @@ class TerminatorKeybindings: def _parsebinding(self, binding): mask = 0 - modifiers = re.findall(self.Modifier, binding) + modifiers = re.findall(Modifier, binding) if modifiers: for modifier in modifiers: mask |= self._lookup_modifier(modifier) - key = re.sub(self.Modifier, '', binding) + key = re.sub(Modifier, '', binding) if key == '': raise KeymapError('No key found') - return (mask, re.sub(self.Modifier, '', binding)) + return (mask, re.sub(Modifier, '', binding)) def _lookup_modifier(self, modifier): try: diff --git a/terminatorlib/terminator.py b/terminatorlib/terminator.py index 5c996d24..1cc01cf9 100755 --- a/terminatorlib/terminator.py +++ b/terminatorlib/terminator.py @@ -118,9 +118,12 @@ class Terminator: self.icon_theme = gtk.IconTheme () self.keybindings = TerminatorKeybindings() + self.keybindings.configure(self.conf.keybindings) if self.conf.f11_modifier: - self.keybindings.keys['full_screen'] = 'F11' - self.keybindings.reload() + print "Warning: f11_modifier is no longer supported" + print "Add the following to the end of your terminator config:" + print "[keybindings]" + print "full_screen = F11" if self.conf.handle_size in xrange (0,6): gtk.rc_parse_string("""