terminator/terminatorlib/config.py

533 lines
19 KiB
Python
Raw Normal View History

2008-03-08 00:04:41 +00:00
#!/usr/bin/python
# TerminatorConfig - layered config classes
# Copyright (C) 2006-2008 cmsj@tenshu.net
#
2009-05-07 01:35:23 +00:00
# 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.
2008-03-08 00:04:41 +00:00
#
2009-05-07 01:35:23 +00:00
# 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.
2008-03-08 00:04:41 +00:00
#
2009-05-07 01:35:23 +00:00
# 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
2008-03-08 00:04:41 +00:00
"""TerminatorConfig by Chris Jones <cmsj@tenshu.net>
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."""
2009-05-07 01:35:23 +00:00
import os
import platform
import sys
import re
2008-03-08 00:04:41 +00:00
import pwd
2009-05-07 01:35:23 +00:00
import gtk
import pango
try:
import gconf
2009-05-07 00:25:51 +00:00
except ImportError:
gconf = None
from terminatorlib import translation
2009-08-08 00:22:31 +00:00
from terminatorlib.util import dbg, err
2009-05-07 01:35:23 +00:00
from terminatorlib.configfile import ConfigFile, ParsedWithErrors
2009-05-07 01:35:23 +00:00
DEFAULTS = {
2008-08-15 12:40:10 +00:00
'gt_dir' : '/apps/gnome-terminal',
'profile_dir' : '/apps/gnome-terminal/profiles',
'titlebars' : True,
'zoomedtitlebar' : True,
2008-08-15 12:40:10 +00:00
'titletips' : False,
'allow_bold' : True,
'audible_bell' : False,
'visible_bell' : True,
'urgent_bell' : False,
2008-08-15 12:40:10 +00:00
'background_color' : '#000000',
'background_darkness' : 0.5,
'background_type' : 'solid',
'background_image' : '',
'backspace_binding' : 'ascii-del',
'delete_binding' : 'delete-sequence',
'cursor_blink' : True,
'cursor_shape' : 'block',
'cursor_color' : '',
2008-08-15 12:40:10 +00:00
'emulation' : 'xterm',
'font' : 'Mono 10',
2008-08-15 12:40:10 +00:00
'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,
2009-01-25 14:26:20 +00:00
'hidden' : False,
2008-08-15 12:40:10 +00:00
'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,
2008-08-28 16:08:15 +00:00
'tab_position' : 'top',
'enable_real_transparency' : True,
'title_tx_txt_color' : '#FFFFFF',
'title_tx_bg_color' : '#C80003',
'title_rx_txt_color' : '#FFFFFF',
'title_rx_bg_color' : '#0076C9',
'title_ia_txt_color' : '#000000',
'title_ia_bg_color' : '#C0BEBF',
2008-08-15 12:40:10 +00:00
'try_posix_regexp' : platform.system() != 'Linux',
'hide_tabbar' : False,
'scroll_tabbar' : False,
'alternate_screen_scroll': True,
2008-08-15 12:40:10 +00:00
'keybindings' : {
'zoom_in' : '<Ctrl>plus',
'zoom_out' : '<Ctrl>minus',
'zoom_normal' : '<Ctrl>0',
'new_root_tab' : '<Ctrl><Shift><Alt>T',
'new_tab' : '<Ctrl><Shift>T',
'go_next' : ('<Ctrl><Shift>N','<Ctrl>Tab'),
'go_prev' : ('<Ctrl><Shift>P','<Ctrl><Shift>Tab'),
'go_up' : '<Alt>Up',
'go_down' : '<Alt>Down',
'go_left' : '<Alt>Left',
'go_right' : '<Alt>Right',
2008-08-15 12:40:10 +00:00
'split_horiz' : '<Ctrl><Shift>O',
'split_vert' : '<Ctrl><Shift>E',
'close_term' : '<Ctrl><Shift>W',
'copy' : '<Ctrl><Shift>C',
'paste' : '<Ctrl><Shift>V',
'toggle_scrollbar' : '<Ctrl><Shift>S',
'search' : '<Ctrl><Shift>F',
'close_window' : '<Ctrl><Shift>Q',
'resize_up' : '<Ctrl><Shift>Up',
'resize_down' : '<Ctrl><Shift>Down',
'resize_left' : '<Ctrl><Shift>Left',
'resize_right' : '<Ctrl><Shift>Right',
'move_tab_right' : '<Ctrl><Shift>Page_Down',
'move_tab_left' : '<Ctrl><Shift>Page_Up',
'toggle_zoom' : '<Ctrl><Shift>X',
'scaled_zoom' : '<Ctrl><Shift>Z',
'next_tab' : '<Ctrl>Page_Down',
'prev_tab' : '<Ctrl>Page_Up',
'switch_to_tab_1' : None,
'switch_to_tab_2' : None,
'switch_to_tab_3' : None,
'switch_to_tab_4' : None,
'switch_to_tab_5' : None,
'switch_to_tab_6' : None,
'switch_to_tab_7' : None,
'switch_to_tab_8' : None,
'switch_to_tab_9' : None,
'switch_to_tab_10' : None,
2008-08-15 12:40:10 +00:00
'full_screen' : 'F11',
'reset' : '<Ctrl><Shift>R',
'reset_clear' : '<Ctrl><Shift>G',
'hide_window' : '<Ctrl><Shift><Alt>a',
'group_all' : '<Super>g',
'ungroup_all' : '<Super><Shift>g',
'group_tab' : '<Super>t',
'ungroup_tab' : '<Super><Shift>T',
'new_window' : '<Ctrl><Shift>I',
2008-08-15 12:40:10 +00:00
}
}
2009-05-07 01:35:23 +00:00
class TerminatorConfig(object):
2008-07-22 11:03:30 +00:00
"""This class is used as the base point of the config system"""
2008-03-08 00:04:41 +00:00
callback = None
sources = None
_keys = None
2008-03-08 00:04:41 +00:00
2008-07-22 11:03:30 +00:00
def __init__ (self, sources):
self.sources = []
2008-03-08 00:04:41 +00:00
for source in sources:
if isinstance(source, TerminatorConfValuestore):
self.sources.append (source)
2009-05-07 01:35:23 +00:00
# We always add a default valuestore last so no valid config item ever
# goes unset
2008-03-08 00:04:41 +00:00
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)
2008-03-08 00:04:41 +00:00
def __getattr__ (self, keyname):
for source in self.sources:
dbg ("TConfig: Looking for: '%s' in '%s'"%(keyname, source.type))
2008-03-08 00:04:41 +00:00
try:
val = source[keyname]
dbg (" TConfig: got: '%s' from a '%s'"%(val, source.type))
2008-03-08 00:04:41 +00:00
return (val)
except KeyError:
2008-03-08 00:04:41 +00:00
pass
dbg (" TConfig: Out of sources")
2008-03-08 00:04:41 +00:00
raise (AttributeError)
2009-05-07 01:35:23 +00:00
class TerminatorConfValuestore(object):
2008-03-08 00:04:41 +00:00
type = "Base"
values = None
2008-03-08 01:38:42 +00:00
reconfigure_callback = None
def __init__ (self):
self.values = {}
2008-03-08 00:04:41 +00:00
# Our settings
def __getitem__ (self, keyname):
2008-03-08 00:04:41 +00:00
if self.values.has_key (keyname):
value = self.values[keyname]
dbg ("Returning '%s':'%s'"%(keyname, value))
return value
2008-03-08 00:04:41 +00:00
else:
dbg ("Failed to find '%s'"%keyname)
raise (KeyError)
2008-03-08 00:04:41 +00:00
class TerminatorConfValuestoreDefault (TerminatorConfValuestore):
def __init__ (self):
TerminatorConfValuestore.__init__ (self)
2008-03-08 00:04:41 +00:00
self.type = "Default"
2009-05-07 01:35:23 +00:00
self.values = DEFAULTS
2008-03-08 00:04:41 +00:00
class TerminatorConfValuestoreRC (TerminatorConfValuestore):
rcfilename = ""
type = "RCFile"
2008-03-08 00:04:41 +00:00
def __init__ (self):
TerminatorConfValuestore.__init__ (self)
try:
directory = os.environ['XDG_CONFIG_HOME']
2009-05-07 01:35:23 +00:00
except KeyError:
dbg(" VS_RCFile: 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)
def set_reconfigure_callback (self, function):
dbg (" VS_RCFile: setting callback to: %s"%function)
self.reconfigure_callback = function
return (True)
def call_parser (self, is_init = False):
dbg (" VS_RCFile: parsing config file")
try:
ini = ConfigFile(self.rcfilename, self._rc_set_callback())
ini.parse()
2009-05-07 01:35:23 +00:00
except IOError, ex:
dbg (" VS_RCFile: unable to open %s (%r)" % (self.rcfilename, ex))
except ParsedWithErrors, ex:
# We don't really want to produce an error dialog every run
if not is_init:
pass
msg = _("""<big><b>Configuration error</b></big>
Errors were encountered while parsing terminator_config(5) file:
<b>%s</b>
2009-05-07 01:35:23 +00:00
%d line(s) have been ignored.""") % (self.rcfilename, len(ex.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)
2009-05-07 01:35:23 +00:00
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()
2009-05-07 01:35:23 +00:00
textbuff.set_text("\n".join(map(lambda ex: str(ex), ex.errors)))
textview = gtk.TextView(textbuff)
textview.set_editable(False)
2009-05-07 01:35:23 +00:00
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)
2009-05-07 01:35:23 +00:00
vbox = dialog.get_content_area()
vbox.pack_start (box, False, False, 12)
dialog.show_all()
dialog.run()
dialog.destroy()
2008-03-08 00:04:41 +00:00
dbg("ConfigFile settings are: %r" % self.values)
def _rc_set_callback(self):
def callback(sections, key, value):
dbg("Setting: section=%r with %r => %r" % (sections, key, value))
section = None
if len(sections) > 0:
section = sections[0]
if section is None:
# handle some deprecated configs
if key == 'silent_bell':
err ("silent_bell config option is deprecated, for the new bell related config options, see: man terminator_config")
if value:
self.values['audible_bell'] = False
else:
self.values['audible_bell'] = True
key = 'visible_bell'
2009-05-07 01:35:23 +00:00
if not DEFAULTS.has_key (key):
raise ValueError("Unknown configuration option %r" % key)
2009-05-07 01:35:23 +00:00
deftype = DEFAULTS[key].__class__.__name__
if key.endswith('_color'):
try:
gtk.gdk.color_parse(value)
self.values[key] = value
except ValueError:
raise ValueError(_("Setting %r value %r not a valid colour; ignoring") % (key, value))
2008-08-28 16:08:15 +00:00
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 %r should be a section name") % key)
self.values[key] = value
else:
self.values[key] = value
dbg (" VS_RCFile: Set value %r to %r" % (key, self.values[key]))
elif section == 'keybindings':
self.values.setdefault(section, {})
2009-05-07 01:35:23 +00:00
if not DEFAULTS[section].has_key(key):
raise ValueError("Keybinding name %r is unknown" % key)
else:
self.values[section][key] = value
else:
raise ValueError("Section name %r is unknown" % section)
return callback
2008-03-08 00:04:41 +00:00
class TerminatorConfValuestoreGConf (TerminatorConfValuestore):
profile = ""
client = None
cache = None
notifies = None
2008-03-08 00:04:41 +00:00
def __init__ (self, profileName = None):
TerminatorConfValuestore.__init__ (self)
2008-03-08 00:04:41 +00:00
self.type = "GConf"
self.inactive = False
self.cache = {}
self.notifies = {}
2008-03-08 00:04:41 +00:00
2008-04-02 21:10:32 +00:00
import gconf
2008-03-08 00:04:41 +00:00
self.client = gconf.client_get_default ()
# Grab a couple of values from base class to avoid recursing with our __getattr__
2009-05-07 01:35:23 +00:00
self._gt_dir = DEFAULTS['gt_dir']
self._profile_dir = DEFAULTS['profile_dir']
2008-03-08 00:04:41 +00:00
dbg ('VSGConf: Profile bet on is: "%s"'%profileName)
2008-03-08 01:38:42 +00:00
profiles = self.client.get_list (self._gt_dir + '/global/profile_list','string')
dbg ('VSGConf: Found profiles: "%s"'%profiles)
2008-03-08 01:38:42 +00:00
dbg ('VSGConf: Profile requested is: "%s"'%profileName)
if not profileName:
profile = self.client.get_string (self._gt_dir + '/global/default_profile')
else:
profile = profileName
# In newer gnome-terminal, the profile keys are named Profile0/1 etc.
# We have to match using visible_name instead
for p in profiles:
profileName2 = self.client.get_string (
self._profile_dir + '/' + p + '/visible_name')
if profileName == profileName2:
profile = p
2008-05-20 23:37:04 +00:00
#need to handle the list of Gconf.value
2008-03-08 01:38:42 +00:00
if profile in profiles:
dbg (" VSGConf: Found profile '%s' in profile_list"%profile)
2009-05-07 01:35:23 +00:00
self.profile = '%s/%s' % (self._profile_dir, profile)
2008-03-08 01:38:42 +00:00
elif "Default" in profiles:
2009-05-07 01:35:23 +00:00
dbg (" VSGConf: profile '%s' not found, but 'Default' exists" % profile)
2008-03-08 01:38:42 +00:00
self.profile = '%s/%s'%(self._profile_dir, "Default")
2008-03-08 00:04:41 +00:00
else:
2008-03-08 01:38:42 +00:00
# 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
2008-03-08 01:38:42 +00:00
#set up the active encoding list
self.active_encodings = self.client.get_list (self._gt_dir + '/global/active_encodings', 'string')
2008-03-08 01:38:42 +00:00
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)
2008-04-02 21:10:32 +00:00
# FIXME: Do we need to watch more non-profile stuff here?
def set_reconfigure_callback (self, function):
dbg (" VSConf: setting callback to: %s"%function)
2008-04-02 21:10:32 +00:00
self.reconfigure_callback = function
return (True)
2008-03-08 01:38:42 +00:00
def on_gconf_notify (self, client, cnxn_id, entry, what):
2008-04-11 12:09:34 +00:00
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
2008-03-08 01:38:42 +00:00
if self.reconfigure_callback:
dbg (" VSGConf: callback is: %s"%self.reconfigure_callback)
2008-03-08 01:38:42 +00:00
self.reconfigure_callback ()
2008-03-08 00:04:41 +00:00
def __getitem__ (self, key = ""):
if self.inactive:
2008-08-28 12:56:16 +00:00
raise KeyError
2008-04-11 12:09:34 +00:00
if self.cache.has_key (key):
dbg (" VSGConf: returning cached value: %s"%self.cache[key])
return (self.cache[key])
2008-03-08 00:04:41 +00:00
ret = None
value = None
2008-03-08 00:04:41 +00:00
dbg (' VSGConf: preparing: %s/%s'%(self.profile, key))
2008-05-20 23:37:04 +00:00
# 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')
2008-09-02 14:25:12 +00:00
if self.client.get_bool ('/system/http_proxy/use_authentication'):
dbg ('HACK: Using proxy authentication')
2009-05-07 01:35:23 +00:00
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')
2009-05-07 01:35:23 +00:00
value = 'http://%s:%s/' % (
self.client.get_string ('/system/http_proxy/host'),
self.client.get_int ('/system/http_proxy/port'))
elif key == 'cursor_blink':
tmp = self.client.get_string('%s/cursor_blink_mode' % self.profile)
if tmp in ['on', 'off'] and self.notifies.has_key ('cursor_blink'):
self.client.notify_remove (self.notifies['cursor_blink'])
del (self.notifies['cursor_blink'])
if tmp == 'on':
value = True
elif tmp == 'off':
value = False
elif tmp == 'system':
value = self.client.get_bool ('/desktop/gnome/interface/cursor_blink')
self.notifies['cursor_blink'] = self.client.notify_add ('/desktop/gnome/interface/cursor_blink', self.on_gconf_notify)
else:
value = self.client.get ('%s/%s'%(self.profile, key))
2008-04-02 21:10:32 +00:00
else:
value = self.client.get ('%s/%s'%(self.profile, key))
if value != None:
2009-05-07 01:35:23 +00:00
from types import StringType, BooleanType
if type(value) in [StringType, BooleanType]:
2009-05-07 01:35:23 +00:00
ret = value
else:
2009-05-07 01:35:23 +00:00
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 ()
2008-03-08 00:04:41 +00:00
2008-04-11 12:09:34 +00:00
self.cache[key] = ret
2008-03-08 00:04:41 +00:00
return (ret)
else:
raise (KeyError)
2008-03-08 00:04:41 +00:00