#!/usr/bin/python # TerminatorConfig - layered config classes # Copyright (C) 2006-2008 cmsj@tenshu.net # # This program 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, version 2 only. # # This program 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 this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA """TerminatorConfig by Chris Jones The config scheme works in layers, with defaults at the base, and a simple/flexible class which can be placed over the top in multiple layers. This was written for Terminator, but could be used generically. Its original use is to guarantee default values for any config item, while allowing them to be overridden by at least two other stores of configuration values. Those being gconf and a plain config file. In addition to the value, the default layer must also provide the datatype (str, int, float and bool are currently supported). values are found as attributes of the TerminatorConfig object. 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 os, platform, sys, re import pwd import gtk, pango try: import gio except ImportError: pass # set this to true to enable debugging output # These should be moved somewhere better. debug = False def dbg (log = ""): """Print a message if debugging is enabled""" if debug: print >> sys.stderr, log def err (log = ""): """Print an error message""" print >> sys.stderr, log from configfile import ConfigFile, ParsedWithErrors Defaults = { 'gt_dir' : '/apps/gnome-terminal', 'profile_dir' : '/apps/gnome-terminal/profiles', 'titlebars' : True, 'titletips' : False, 'allow_bold' : True, 'silent_bell' : True, 'background_color' : '#000000', 'background_darkness' : 0.5, 'background_type' : 'solid', 'background_image' : '', 'backspace_binding' : 'ascii-del', 'delete_binding' : 'delete-sequence', 'cursor_blink' : True, 'emulation' : 'xterm', 'font' : 'Mono', 'foreground_color' : '#AAAAAA', 'scrollbar_position' : "right", 'scroll_background' : True, 'scroll_on_keystroke' : True, 'scroll_on_output' : True, 'scrollback_lines' : 500, 'focus' : 'click', 'exit_action' : 'close', 'palette' : '#000000000000:#CDCD00000000:#0000CDCD0000:#CDCDCDCD0000:#30BF30BFA38E:#A53C212FA53C:#0000CDCDCDCD:#FAFAEBEBD7D7:#404040404040:#FFFF00000000:#0000FFFF0000:#FFFFFFFF0000:#00000000FFFF:#FFFF0000FFFF:#0000FFFFFFFF:#FFFFFFFFFFFF', 'word_chars' : '-A-Za-z0-9,./?%&#:_', 'mouse_autohide' : True, 'update_records' : True, 'login_shell' : False, 'use_custom_command' : False, 'custom_command' : '', 'use_system_font' : True, 'use_theme_colors' : False, 'http_proxy' : '', 'ignore_hosts' : ['localhost','127.0.0.0/8','*.local'], 'encoding' : 'UTF-8', 'active_encodings' : ['UTF-8', 'ISO-8859-1'], 'extreme_tabs' : False, 'fullscreen' : False, 'borderless' : False, 'maximise' : False, 'handle_size' : -1, 'focus_on_close' : 'auto', 'f11_modifier' : False, 'force_no_bell' : False, 'cycle_term_tab' : True, 'copy_on_selection' : False, 'close_button_on_tab' : True, 'tab_position' : 'top', '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','Tab'), 'go_prev' : ('P','Tab'), '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', 'full_screen' : 'F11', } } 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) # 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)) try: val = source[keyname] dbg (" TConfig: got: '%s' from a '%s'"%(val, source.type)) return (val) except KeyError: pass dbg (" TConfig: Out of sources") raise (AttributeError) class TerminatorConfValuestore: type = "Base" values = {} reconfigure_callback = None # Our settings def __getitem__ (self, keyname): if self.values.has_key (keyname): value = self.values[keyname] dbg ("Returning '%s':'%s'"%(keyname, value)) return value else: dbg ("Failed to find '%s'"%keyname) raise (KeyError) class TerminatorConfValuestoreDefault (TerminatorConfValuestore): def __init__ (self): self.type = "Default" self.values = Defaults class TerminatorConfValuestoreRC (TerminatorConfValuestore): rcfilename = "" type = "RCFile" def __init__ (self): try: directory = os.environ['XDG_CONFIG_HOME'] except KeyError, e: dbg(" VS_RCFile: Environment variable XDG_CONFIG_HOME not found. defaulting to ~/.config") directory = os.path.join (os.path.expanduser("~"), ".config") self.rcfilename = os.path.join(directory, "terminator/config") dbg(" VS_RCFile: config file located at %s" % self.rcfilename) self.call_parser(True) try: monfile = gio.File(self.rcfilename) monmon = monfile.monitor_file() monmon.connect("changed", self.file_changed) except NameError: dbg ("gio module not found, config file monitoring disabled") def set_reconfigure_callback (self, function): dbg (" VS_RCFile: setting callback to: %s"%function) self.reconfigure_callback = function return (True) def file_changed (self, monitor, file, unknown, event): if event == gio.FILE_MONITOR_EVENT_CHANGES_DONE_HINT: print "VS_RCFile: config file changed, reload" self.values = {} self.call_parser() self.reconfigure_callback() def call_parser (self, is_init = False): dbg (" VS_RCFile: parsing config file") try: ini = ConfigFile(self.rcfilename, self._rc_set_callback()) ini.parse() except IOError, e: dbg (" VS_RCFile: unable to open %s (%s)" % (self.rcfilename, repr(e))) except ParsedWithErrors, e: # We don't really want to produce an error dialog every run if not is_init: pass msg = _("""Configuration error Errors were encountered while parsing terminator_config(5) file: %s %d line(s) have been ignored.""") % (self.rcfilename, len(e.errors)) dialog = gtk.Dialog(_("Configuration error"), None, gtk.DIALOG_MODAL, (gtk.STOCK_OK, gtk.RESPONSE_ACCEPT)) dialog.set_has_separator(False) dialog.set_resizable(False) image = gtk.image_new_from_stock(gtk.STOCK_DIALOG_WARNING, gtk.ICON_SIZE_DIALOG) image.set_alignment (0.5, 0) dmsg = gtk.Label(msg) dmsg.set_use_markup(True) dmsg.set_alignment(0, 0.5) textbuff = gtk.TextBuffer() textbuff.set_text("\n".join(map(lambda e: str(e), e.errors))) textview = gtk.TextView(textbuff) textview.set_editable(False) textview.modify_font(pango.FontDescription(Defaults['font'])) textscroll = gtk.ScrolledWindow() textscroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) textscroll.add(textview) # This should be scaled with the size of the text and font textscroll.set_size_request(600, 200) root = gtk.VBox() root.pack_start(dmsg, padding = 6) root.pack_start(textscroll, padding = 6) box = gtk.HBox() box.pack_start (image, False, False, 6) box.pack_start (root, False, False, 6) dialog.vbox.pack_start (box, False, False, 12) dialog.show_all() dialog.run() dialog.destroy() dbg("ConfigFile settings are: %s" % repr(self.values)) def _rc_set_callback(self): def callback(sections, key, value): dbg("Setting: section=%s with %s => %s" % (repr(sections), repr(key), repr(value))) section = None if len(sections) > 0: section = sections[0] if section is None: if not Defaults.has_key (key): raise ValueError("Unknown configuration option %s" % repr(key)) deftype = Defaults[key].__class__.__name__ if key.endswith('_color'): try: gtk.gdk.color_parse(value) self.values[key] = value except ValueError: raise ValueError(_("Setting %s value %s not a valid colour; ignoring") % (key,repr(value))) elif key == 'tab_position': if value.lower() in ('top', 'left', 'bottom', 'right'): self.values[key] = value.lower() else: raise ValueError(_("%s must be one of: top, left, right, bottom") % key) elif 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: raise ValueError(_("Boolean setting %s expecting one of: yes, no, true, false, on, off") % key) elif deftype == 'int': self.values[key] = int (value) elif deftype == 'float': self.values[key] = float (value) elif deftype == 'list': raise ValueError(_("Reading list values from terminator_config(5) is not currently supported")) elif deftype == 'dict': if type(value) != dict: raise ValueError(_("Setting %s should be a section name") % repr(key)) self.values[key] = value else: self.values[key] = value dbg (" VS_RCFile: Set value '%s' to %s" % (key, repr(self.values[key]))) elif section == 'keybindings': self.values.setdefault(section, {}) if not Defaults[section].has_key(key): raise ValueError("Keybinding name %s is unknown" % repr(key)) else: self.values[section][key] = value else: raise ValueError("Section name %s is unknown" % repr(section)) return callback class TerminatorConfValuestoreGConf (TerminatorConfValuestore): profile = "" client = None cache = {} def __init__ (self, profile = None): self.type = "GConf" self.inactive = False import gconf self.client = gconf.client_get_default () # Grab a couple of values from base class to avoid recursing with our __getattr__ self._gt_dir = Defaults['gt_dir'] self._profile_dir = Defaults['profile_dir'] dbg ('VSGConf: Profile requested is: "%s"'%profile) if not profile: profile = self.client.get_string (self._gt_dir + '/global/default_profile') dbg ('VSGConf: Profile bet on is: "%s"'%profile) profiles = self.client.get_list (self._gt_dir + '/global/profile_list','string') dbg ('VSGConf: Found profiles: "%s"'%profiles) #set up the active encoding list self.active_encodings = self.client.get_list (self._gt_dir + '/global/active_encodings', 'string') #need to handle the list of Gconf.value if profile in profiles: dbg (" VSGConf: Found profile '%s' in profile_list"%profile) self.profile = '%s/%s'%(self._profile_dir, profile) elif "Default" in profiles: dbg (" VSGConf: profile '%s' not found, but 'Default' exists"%profile) self.profile = '%s/%s'%(self._profile_dir, "Default") else: # We're a bit stuck, there is no profile in the list # FIXME: Find a better way to handle this than setting a non-profile dbg ("VSGConf: No profile found, marking inactive") self.inactive = True return self.client.add_dir (self.profile, gconf.CLIENT_PRELOAD_RECURSIVE) if self.on_gconf_notify: self.client.notify_add (self.profile, self.on_gconf_notify) self.client.add_dir ('/apps/metacity/general', gconf.CLIENT_PRELOAD_RECURSIVE) self.client.notify_add ('/apps/metacity/general/focus_mode', self.on_gconf_notify) self.client.add_dir ('/desktop/gnome/interface', gconf.CLIENT_PRELOAD_RECURSIVE) self.client.notify_add ('/desktop/gnome/interface/monospace_font_name', self.on_gconf_notify) # FIXME: Do we need to watch more non-profile stuff here? def set_reconfigure_callback (self, function): dbg (" VSConf: setting callback to: %s"%function) self.reconfigure_callback = function return (True) def on_gconf_notify (self, client, cnxn_id, entry, what): dbg (" VSGConf: invalidating cache") self.cache = {} dbg (" VSGConf: gconf changed, may run a callback. %s, %s"%(entry.key, entry.value)) if entry.key[-12:] == 'visible_name': dbg (" VSGConf: only a visible_name change, ignoring") return False if self.reconfigure_callback: dbg (" VSGConf: callback is: %s"%self.reconfigure_callback) self.reconfigure_callback () def __getitem__ (self, key = ""): if self.inactive: raise KeyError if self.cache.has_key (key): dbg (" VSGConf: returning cached value: %s"%self.cache[key]) return (self.cache[key]) ret = None value = None dbg (' VSGConf: preparing: %s/%s'%(self.profile, key)) # FIXME: Ugly special cases we should look to fix in some other way. if key == 'font' and self['use_system_font']: value = self.client.get ('/desktop/gnome/interface/monospace_font_name') elif key == 'focus': value = self.client.get ('/apps/metacity/general/focus_mode') elif key == 'http_proxy': if self.client.get_bool ('/system/http_proxy/use_http_proxy'): dbg ('HACK: Mangling http_proxy') if self.client.get_bool ('/system/http_proxy/use_authentication'): dbg ('HACK: Using proxy authentication') value = 'http://%s:%s@%s:%s/'%( self.client.get_string ('/system/http_proxy/authentication_user'), self.client.get_string ('/system/http_proxy/authentication_password'), self.client.get_string ('/system/http_proxy/host'), self.client.get_int ('/system/http_proxy/port')) else: dbg ('HACK: Not using proxy authentication') value = 'http://%s:%s/'%( self.client.get_string ('/system/http_proxy/host'), self.client.get_int ('/system/http_proxy/port')) else: value = self.client.get ('%s/%s'%(self.profile, key)) if value: from types import StringType if type(value) is StringType: ret = value else: funcname = "get_" + Defaults[key].__class__.__name__ dbg (' GConf: picked function: %s'%funcname) # Special case for str if funcname == "get_str": funcname = "get_string" # Special case for strlist if funcname == "get_strlist": funcname = "get_list" typefunc = getattr (value, funcname) ret = typefunc () self.cache[key] = ret return (ret) else: raise (KeyError)