1#  TerminatorConfig - layered config classes
2#  Copyright (C) 2006-2010  cmsj@tenshu.net
3#
4#  This program is free software; you can redistribute it and/or modify
5#  it under the terms of the GNU General Public License as published by
6#  the Free Software Foundation, version 2 only.
7#
8#  This program is distributed in the hope that it will be useful,
9#  but WITHOUT ANY WARRANTY; without even the implied warranty of
10#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11#  GNU General Public License for more details.
12#
13#  You should have received a copy of the GNU General Public License
14#  along with this program; if not, write to the Free Software
15#  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
17"""Terminator by Chris Jones <cmsj@tenshu.net>
18
19Classes relating to configuration
20
21>>> DEFAULTS['global_config']['focus']
22'click'
23>>> config = Config()
24>>> config['focus'] = 'sloppy'
25>>> config['focus']
26'sloppy'
27>>> DEFAULTS['global_config']['focus']
28'click'
29>>> config2 = Config()
30>>> config2['focus']
31'sloppy'
32>>> config2['focus'] = 'click'
33>>> config2['focus']
34'click'
35>>> config['focus']
36'click'
37>>> config['geometry_hinting'].__class__.__name__
38'bool'
39>>> plugintest = {}
40>>> plugintest['foo'] = 'bar'
41>>> config.plugin_set_config('testplugin', plugintest)
42>>> config.plugin_get_config('testplugin')
43{'foo': 'bar'}
44>>> config.plugin_get('testplugin', 'foo')
45'bar'
46>>> config.plugin_get('testplugin', 'foo', 'new')
47'bar'
48>>> config.plugin_get('testplugin', 'algo')
49Traceback (most recent call last):
50...
51KeyError: 'ConfigBase::get_item: unknown key algo'
52>>> config.plugin_get('testplugin', 'algo', 1)
531
54>>> config.plugin_get('anothertestplugin', 'algo', 500)
55500
56>>> config.get_profile()
57'default'
58>>> config.set_profile('my_first_new_testing_profile')
59>>> config.get_profile()
60'my_first_new_testing_profile'
61>>> config.del_profile('my_first_new_testing_profile')
62>>> config.get_profile()
63'default'
64>>> config.list_profiles().__class__.__name__
65'list'
66>>> config.options_set({})
67>>> config.options_get()
68{}
69>>>
70
71"""
72
73import os
74import shutil
75from copy import copy
76from configobj import ConfigObj, flatten_errors
77from validate import Validator
78from .borg import Borg
79from .util import dbg, err, DEBUG, get_system_config_dir, get_config_dir, dict_diff
80
81from gi.repository import Gio
82
83DEFAULTS = {
84        'global_config':   {
85            'dbus'                  : True,
86            'focus'                 : 'click',
87            'handle_size'           : -1,
88            'geometry_hinting'      : False,
89            'window_state'          : 'normal',
90            'borderless'            : False,
91            'extra_styling'         : True,
92            'tab_position'          : 'top',
93            'broadcast_default'     : 'group',
94            'close_button_on_tab'   : True,
95            'hide_tabbar'           : False,
96            'scroll_tabbar'         : False,
97            'homogeneous_tabbar'    : True,
98            'hide_from_taskbar'     : False,
99            'always_on_top'         : False,
100            'hide_on_lose_focus'    : False,
101            'sticky'                : False,
102            'use_custom_url_handler': False,
103            'custom_url_handler'    : '',
104            'disable_real_transparency' : False,
105            'title_at_bottom'       : False,
106            'title_hide_sizetext'   : False,
107            'title_transmit_fg_color' : '#ffffff',
108            'title_transmit_bg_color' : '#c80003',
109            'title_receive_fg_color' : '#ffffff',
110            'title_receive_bg_color' : '#0076c9',
111            'title_inactive_fg_color' : '#000000',
112            'title_inactive_bg_color' : '#c0bebf',
113            'inactive_color_offset': 0.8,
114            'enabled_plugins'       : ['LaunchpadBugURLHandler',
115                                       'LaunchpadCodeURLHandler',
116                                       'APTURLHandler'],
117            'suppress_multiple_term_dialog': False,
118            'always_split_with_profile': False,
119            'title_use_system_font' : True,
120            'title_font'            : 'Sans 9',
121            'putty_paste_style'     : False,
122            'putty_paste_style_source_clipboard': False,
123            'smart_copy'            : True,
124            'clear_select_on_copy'  : False,
125            'line_height'           : 1.0,
126            'case_sensitive'        : True,
127            'invert_search'         : False,
128        },
129        'keybindings': {
130            'zoom_in'          : '<Control>plus',
131            'zoom_out'         : '<Control>minus',
132            'zoom_normal'      : '<Control>0',
133			'zoom_in_all'	   : '',
134			'zoom_out_all'	   : '',
135			'zoom_normal_all'  : '',
136            'new_tab'          : '<Shift><Control>t',
137            'cycle_next'       : '<Control>Tab',
138            'cycle_prev'       : '<Shift><Control>Tab',
139            'go_next'          : '<Shift><Control>n',
140            'go_prev'          : '<Shift><Control>p',
141            'go_up'            : '<Alt>Up',
142            'go_down'          : '<Alt>Down',
143            'go_left'          : '<Alt>Left',
144            'go_right'         : '<Alt>Right',
145            'rotate_cw'        : '<Super>r',
146            'rotate_ccw'       : '<Super><Shift>r',
147            'split_horiz'      : '<Shift><Control>o',
148            'split_vert'       : '<Shift><Control>e',
149            'close_term'       : '<Shift><Control>w',
150            'copy'             : '<Shift><Control>c',
151            'paste'            : '<Shift><Control>v',
152            'toggle_scrollbar' : '<Shift><Control>s',
153            'search'           : '<Shift><Control>f',
154            'page_up'          : '',
155            'page_down'        : '',
156            'page_up_half'     : '',
157            'page_down_half'   : '',
158            'line_up'          : '',
159            'line_down'        : '',
160            'close_window'     : '<Shift><Control>q',
161            'resize_up'        : '<Shift><Control>Up',
162            'resize_down'      : '<Shift><Control>Down',
163            'resize_left'      : '<Shift><Control>Left',
164            'resize_right'     : '<Shift><Control>Right',
165            'move_tab_right'   : '<Shift><Control>Page_Down',
166            'move_tab_left'    : '<Shift><Control>Page_Up',
167            'toggle_zoom'      : '<Shift><Control>x',
168            'scaled_zoom'      : '<Shift><Control>z',
169            'next_tab'         : '<Control>Page_Down',
170            'prev_tab'         : '<Control>Page_Up',
171            'switch_to_tab_1'  : '',
172            'switch_to_tab_2'  : '',
173            'switch_to_tab_3'  : '',
174            'switch_to_tab_4'  : '',
175            'switch_to_tab_5'  : '',
176            'switch_to_tab_6'  : '',
177            'switch_to_tab_7'  : '',
178            'switch_to_tab_8'  : '',
179            'switch_to_tab_9'  : '',
180            'switch_to_tab_10' : '',
181            'full_screen'      : 'F11',
182            'reset'            : '<Shift><Control>r',
183            'reset_clear'      : '<Shift><Control>g',
184            'hide_window'      : '<Shift><Control><Alt>a',
185            'create_group'     : '',
186            'group_all'        : '<Super>g',
187            'group_all_toggle' : '',
188            'ungroup_all'      : '<Shift><Super>g',
189            'group_tab'        : '<Super>t',
190            'group_tab_toggle' : '',
191            'ungroup_tab'      : '<Shift><Super>t',
192            'new_window'       : '<Shift><Control>i',
193            'new_terminator'   : '<Super>i',
194            'broadcast_off'    : '',
195            'broadcast_group'  : '',
196            'broadcast_all'    : '',
197            'insert_number'    : '<Super>1',
198            'insert_padded'    : '<Super>0',
199            'edit_window_title': '<Control><Alt>w',
200            'edit_tab_title'   : '<Control><Alt>a',
201            'edit_terminal_title': '<Control><Alt>x',
202            'layout_launcher'  : '<Alt>l',
203            'next_profile'     : '',
204            'previous_profile' : '',
205            'preferences'      : '',
206            'help'             : 'F1'
207        },
208        'profiles': {
209            'default':  {
210                'allow_bold'            : True,
211                'audible_bell'          : False,
212                'visible_bell'          : False,
213                'urgent_bell'           : False,
214                'icon_bell'             : True,
215                'background_color'      : '#000000',
216                'background_darkness'   : 0.5,
217                'background_type'       : 'solid',
218                'backspace_binding'     : 'ascii-del',
219                'delete_binding'        : 'escape-sequence',
220                'color_scheme'          : 'grey_on_black',
221                'cursor_blink'          : True,
222                'cursor_shape'          : 'block',
223                'cursor_color'          : '',
224                'cursor_color_fg'       : True,
225                'term'                  : 'xterm-256color',
226                'colorterm'             : 'truecolor',
227                'font'                  : 'Mono 10',
228                'foreground_color'      : '#aaaaaa',
229                'show_titlebar'         : True,
230                'scrollbar_position'    : "right",
231                'scroll_background'     : True,
232                'scroll_on_keystroke'   : True,
233                'scroll_on_output'      : False,
234                'scrollback_lines'      : 500,
235                'scrollback_infinite'   : False,
236                'disable_mousewheel_zoom': False,
237                'exit_action'           : 'close',
238                'palette'               : '#2e3436:#cc0000:#4e9a06:#c4a000:\
239#3465a4:#75507b:#06989a:#d3d7cf:#555753:#ef2929:#8ae234:#fce94f:\
240#729fcf:#ad7fa8:#34e2e2:#eeeeec',
241                'word_chars'            : '-,./?%&#:_',
242                'mouse_autohide'        : True,
243                'login_shell'           : False,
244                'use_custom_command'    : False,
245                'custom_command'        : '',
246                'use_system_font'       : True,
247                'use_theme_colors'      : False,
248                'bold_is_bright'        : False,
249                'line_height'           : 1.0,
250                'encoding'              : 'UTF-8',
251                'active_encodings'      : ['UTF-8', 'ISO-8859-1'],
252                'focus_on_close'        : 'auto',
253                'force_no_bell'         : False,
254                'cycle_term_tab'        : True,
255                'copy_on_selection'     : False,
256                'split_to_group'        : False,
257                'autoclean_groups'      : True,
258                'http_proxy'            : '',
259                'ignore_hosts'          : ['localhost','127.0.0.0/8','*.local'],
260                'background_image'      : '',
261                'background_alpha'      : 0.0
262            },
263        },
264        'layouts': {
265                'default': {
266                    'window0': {
267                        'type': 'Window',
268                        'parent': ''
269                        },
270                    'child1': {
271                        'type': 'Terminal',
272                        'parent': 'window0'
273                        }
274                    }
275                },
276        'plugins': {
277        },
278}
279
280class Config(object):
281    """Class to provide a slightly richer config API above ConfigBase"""
282    base = None
283    profile = None
284    system_mono_font = None
285    system_prop_font = None
286    system_focus = None
287    inhibited = None
288
289    def __init__(self, profile='default'):
290        self.base = ConfigBase()
291        self.set_profile(profile)
292        self.inhibited = False
293        self.connect_gsetting_callbacks()
294
295    def __getitem__(self, key, default=None):
296        """Look up a configuration item"""
297        return(self.base.get_item(key, self.profile, default=default))
298
299    def __setitem__(self, key, value):
300        """Set a particular configuration item"""
301        return(self.base.set_item(key, value, self.profile))
302
303    def get_profile(self):
304        """Get our profile"""
305        return(self.profile)
306
307    def set_profile(self, profile, force=False):
308        """Set our profile (which usually means change it)"""
309        options = self.options_get()
310        if not force and options and options.profile and profile == 'default':
311            dbg('overriding default profile to %s' % options.profile)
312            profile = options.profile
313        dbg('Config::set_profile: Changing profile to %s' % profile)
314        self.profile = profile
315        if profile not in self.base.profiles:
316            dbg('Config::set_profile: %s does not exist, creating' % profile)
317            self.base.profiles[profile] = copy(DEFAULTS['profiles']['default'])
318
319    def add_profile(self, profile):
320        """Add a new profile"""
321        return(self.base.add_profile(profile))
322
323    def del_profile(self, profile):
324        """Delete a profile"""
325        if profile == self.profile:
326            # FIXME: We should solve this problem by updating terminals when we
327            # remove a profile
328            err('Config::del_profile: Deleting in-use profile %s.' % profile)
329            self.set_profile('default')
330        if profile in self.base.profiles:
331            del(self.base.profiles[profile])
332        options = self.options_get()
333        if options and options.profile == profile:
334            options.profile = None
335            self.options_set(options)
336
337    def rename_profile(self, profile, newname):
338        """Rename a profile"""
339        if profile in self.base.profiles:
340            self.base.profiles[newname] = self.base.profiles[profile]
341            del(self.base.profiles[profile])
342            if profile == self.profile:
343                self.profile = newname
344
345    def list_profiles(self):
346        """List all configured profiles"""
347        return(list(self.base.profiles.keys()))
348
349    def add_layout(self, name, layout):
350        """Add a new layout"""
351        return(self.base.add_layout(name, layout))
352
353    def replace_layout(self, name, layout):
354        """Replace an existing layout"""
355        return(self.base.replace_layout(name, layout))
356
357    def del_layout(self, layout):
358        """Delete a layout"""
359        if layout in self.base.layouts:
360            del(self.base.layouts[layout])
361
362    def rename_layout(self, layout, newname):
363        """Rename a layout"""
364        if layout in self.base.layouts:
365            self.base.layouts[newname] = self.base.layouts[layout]
366            del(self.base.layouts[layout])
367
368    def list_layouts(self):
369        """List all configured layouts"""
370        return(list(self.base.layouts.keys()))
371
372    def connect_gsetting_callbacks(self):
373        """Get system settings and create callbacks for changes"""
374        dbg("GSetting connects for system changes")
375        # Have to preserve these to self, or callbacks don't happen
376        self.gsettings_interface=Gio.Settings.new('org.gnome.desktop.interface')
377        self.gsettings_interface.connect("changed::font-name", self.on_gsettings_change_event)
378        self.gsettings_interface.connect("changed::monospace-font-name", self.on_gsettings_change_event)
379        self.gsettings_wm=Gio.Settings.new('org.gnome.desktop.wm.preferences')
380        self.gsettings_wm.connect("changed::focus-mode", self.on_gsettings_change_event)
381
382    def get_system_prop_font(self):
383        """Look up the system font"""
384        if self.system_prop_font is not None:
385            return(self.system_prop_font)
386        elif 'org.gnome.desktop.interface' not in Gio.Settings.list_schemas():
387            return
388        else:
389            gsettings=Gio.Settings.new('org.gnome.desktop.interface')
390            value = gsettings.get_value('font-name')
391            if value:
392                self.system_prop_font = value.get_string()
393            else:
394                self.system_prop_font = "Sans 10"
395            return(self.system_prop_font)
396
397    def get_system_mono_font(self):
398        """Look up the system font"""
399        if self.system_mono_font is not None:
400            return(self.system_mono_font)
401        elif 'org.gnome.desktop.interface' not in Gio.Settings.list_schemas():
402            return
403        else:
404            gsettings=Gio.Settings.new('org.gnome.desktop.interface')
405            value = gsettings.get_value('monospace-font-name')
406            if value:
407                self.system_mono_font = value.get_string()
408            else:
409                self.system_mono_font = "Mono 10"
410            return(self.system_mono_font)
411
412    def get_system_focus(self):
413        """Look up the system focus setting"""
414        if self.system_focus is not None:
415            return(self.system_focus)
416        elif 'org.gnome.desktop.interface' not in Gio.Settings.list_schemas():
417            return
418        else:
419            gsettings=Gio.Settings.new('org.gnome.desktop.wm.preferences')
420            value = gsettings.get_value('focus-mode')
421            if value:
422                self.system_focus = value.get_string()
423            return(self.system_focus)
424
425    def on_gsettings_change_event(self, settings, key):
426        """Handle a gsetting change event"""
427        dbg('GSetting change event received. Invalidating caches')
428        self.system_focus = None
429        self.system_font = None
430        self.system_mono_font = None
431        # Need to trigger a reconfigure to change active terminals immediately
432        if "Terminator" not in globals():
433            from .terminator import Terminator
434        Terminator().reconfigure()
435
436    def save(self):
437        """Cause ConfigBase to save our config to file"""
438        if self.inhibited is True:
439            return(True)
440        else:
441            return(self.base.save())
442
443    def inhibit_save(self):
444        """Prevent calls to save() being honoured"""
445        self.inhibited = True
446
447    def uninhibit_save(self):
448        """Allow calls to save() to be honoured"""
449        self.inhibited = False
450
451    def options_set(self, options):
452        """Set the command line options"""
453        self.base.command_line_options = options
454
455    def options_get(self):
456        """Get the command line options"""
457        return(self.base.command_line_options)
458
459    def plugin_get(self, pluginname, key, default=None):
460        """Get a plugin config value, if doesn't exist
461            return default if specified
462        """
463        return(self.base.get_item(key, plugin=pluginname, default=default))
464
465    def plugin_set(self, pluginname, key, value):
466        """Set a plugin config value"""
467        return(self.base.set_item(key, value, plugin=pluginname))
468
469    def plugin_get_config(self, plugin):
470        """Return a whole config tree for a given plugin"""
471        return(self.base.get_plugin(plugin))
472
473    def plugin_set_config(self, plugin, tree):
474        """Set a whole config tree for a given plugin"""
475        return(self.base.set_plugin(plugin, tree))
476
477    def plugin_del_config(self, plugin):
478        """Delete a whole config tree for a given plugin"""
479        return(self.base.del_plugin(plugin))
480
481    def layout_get_config(self, layout):
482        """Return a layout"""
483        return(self.base.get_layout(layout))
484
485    def layout_set_config(self, layout, tree):
486        """Set a layout"""
487        return(self.base.set_layout(layout, tree))
488
489class ConfigBase(Borg):
490    """Class to provide access to our user configuration"""
491    loaded = None
492    whined = None
493    sections = None
494    global_config = None
495    profiles = None
496    keybindings = None
497    plugins = None
498    layouts = None
499    command_line_options = None
500
501    def __init__(self):
502        """Class initialiser"""
503
504        Borg.__init__(self, self.__class__.__name__)
505
506        self.prepare_attributes()
507        from . import optionparse
508        self.command_line_options = optionparse.options
509        self.load()
510
511    def prepare_attributes(self):
512        """Set up our borg environment"""
513        if self.loaded is None:
514            self.loaded = False
515        if self.whined is None:
516            self.whined = False
517        if self.sections is None:
518            self.sections = ['global_config', 'keybindings', 'profiles',
519                             'layouts', 'plugins']
520        if self.global_config is None:
521            self.global_config = copy(DEFAULTS['global_config'])
522        if self.profiles is None:
523            self.profiles = {}
524            self.profiles['default'] = copy(DEFAULTS['profiles']['default'])
525        if self.keybindings is None:
526            self.keybindings = copy(DEFAULTS['keybindings'])
527        if self.plugins is None:
528            self.plugins = {}
529        if self.layouts is None:
530            self.layouts = {}
531            for layout in DEFAULTS['layouts']:
532                self.layouts[layout] = copy(DEFAULTS['layouts'][layout])
533
534    def defaults_to_configspec(self):
535        """Convert our tree of default values into a ConfigObj validation
536        specification"""
537        configspecdata = {}
538
539        keymap = {
540                'int': 'integer',
541                'str': 'string',
542                'bool': 'boolean',
543                }
544
545        section = {}
546        for key in DEFAULTS['global_config']:
547            keytype = DEFAULTS['global_config'][key].__class__.__name__
548            value = DEFAULTS['global_config'][key]
549            if keytype in keymap:
550                keytype = keymap[keytype]
551            elif keytype == 'list':
552                value = 'list(%s)' % ','.join(value)
553
554            keytype = '%s(default=%s)' % (keytype, value)
555
556            if key == 'custom_url_handler':
557                keytype = 'string(default="")'
558
559            section[key] = keytype
560        configspecdata['global_config'] = section
561
562        section = {}
563        for key in DEFAULTS['keybindings']:
564            value = DEFAULTS['keybindings'][key]
565            if value is None or value == '':
566                continue
567            section[key] = 'string(default=%s)' % value
568        configspecdata['keybindings'] = section
569
570        section = {}
571        for key in DEFAULTS['profiles']['default']:
572            keytype = DEFAULTS['profiles']['default'][key].__class__.__name__
573            value = DEFAULTS['profiles']['default'][key]
574            if keytype in keymap:
575                keytype = keymap[keytype]
576            elif keytype == 'list':
577                value = 'list(%s)' % ','.join(value)
578            if keytype == 'string':
579                value = '"%s"' % value
580
581            keytype = '%s(default=%s)' % (keytype, value)
582
583            section[key] = keytype
584        configspecdata['profiles'] = {}
585        configspecdata['profiles']['__many__'] = section
586
587        section = {}
588        section['type'] = 'string'
589        section['parent'] = 'string'
590        section['profile'] = 'string(default=default)'
591        section['command'] = 'string(default="")'
592        section['position'] = 'string(default="")'
593        section['size'] = 'list(default=list(-1,-1))'
594        configspecdata['layouts'] = {}
595        configspecdata['layouts']['__many__'] = {}
596        configspecdata['layouts']['__many__']['__many__'] = section
597
598        configspecdata['plugins'] = {}
599
600        configspec = ConfigObj(configspecdata)
601        if DEBUG == True:
602            configspec.write(open('/tmp/terminator_configspec_debug.txt', 'wb'))
603        return(configspec)
604
605    def load(self):
606        """Load configuration data from our various sources"""
607        if self.loaded is True:
608            dbg('ConfigBase::load: config already loaded')
609            return
610
611        if self.command_line_options and self.command_line_options.config:
612            filename = self.command_line_options.config
613        else:
614            filename = os.path.join(get_config_dir(), 'config')
615            if not os.path.exists(filename):
616                filename = os.path.join(get_system_config_dir(), 'config')
617        dbg('looking for config file: %s' % filename)
618        try:
619            configfile = open(filename, 'r')
620        except Exception as ex:
621            if not self.whined:
622                err('ConfigBase::load: Unable to open %s (%s)' % (filename, ex))
623                self.whined = True
624            return
625        # If we have successfully loaded a config, allow future whining
626        self.whined = False
627
628        try:
629            configspec = self.defaults_to_configspec()
630            parser = ConfigObj(configfile, configspec=configspec)
631            validator = Validator()
632            result = parser.validate(validator, preserve_errors=True)
633        except Exception as ex:
634            err('Unable to load configuration: %s' % ex)
635            return
636
637        if result != True:
638            err('ConfigBase::load: config format is not valid')
639            for (section_list, key, _other) in flatten_errors(parser, result):
640                if key is not None:
641                    err('[%s]: %s is invalid' % (','.join(section_list), key))
642                else:
643                    err('[%s] missing' % ','.join(section_list))
644        else:
645            dbg('config validated successfully')
646
647        for section_name in self.sections:
648            dbg('ConfigBase::load: Processing section: %s' % section_name)
649            section = getattr(self, section_name)
650            if section_name == 'profiles':
651                for profile in parser[section_name]:
652                    dbg('ConfigBase::load: Processing profile: %s' % profile)
653                    if section_name not in section:
654                        # FIXME: Should this be outside the loop?
655                        section[profile] = copy(DEFAULTS['profiles']['default'])
656                    section[profile].update(parser[section_name][profile])
657            elif section_name == 'plugins':
658                if section_name not in parser:
659                    continue
660                for part in parser[section_name]:
661                    dbg('ConfigBase::load: Processing %s: %s' % (section_name,
662                                                                 part))
663                    section[part] = parser[section_name][part]
664            elif section_name == 'layouts':
665                for layout in parser[section_name]:
666                    dbg('ConfigBase::load: Processing %s: %s' % (section_name,
667                                                                 layout))
668                    if layout == 'default' and \
669                       parser[section_name][layout] == {}:
670                           continue
671                    section[layout] = parser[section_name][layout]
672            elif section_name == 'keybindings':
673                if section_name not in parser:
674                    continue
675                for part in parser[section_name]:
676                    dbg('ConfigBase::load: Processing %s: %s' % (section_name,
677                                                                 part))
678                    if parser[section_name][part] == 'None':
679                        section[part] = None
680                    else:
681                        section[part] = parser[section_name][part]
682            else:
683                try:
684                    section.update(parser[section_name])
685                except KeyError as ex:
686                    dbg('ConfigBase::load: skipping missing section %s' %
687                            section_name)
688
689        self.loaded = True
690
691    def reload(self):
692        """Force a reload of the base config"""
693        self.loaded = False
694        self.load()
695
696    def save(self):
697        """Save the config to a file"""
698        dbg('ConfigBase::save: saving config')
699        parser = ConfigObj(encoding='utf-8')
700        parser.indent_type = '  '
701
702        for section_name in ['global_config', 'keybindings']:
703            dbg('ConfigBase::save: Processing section: %s' % section_name)
704            section = getattr(self, section_name)
705            parser[section_name] = dict_diff(DEFAULTS[section_name], section)
706
707        from .configjson import JSON_PROFILE_NAME, JSON_LAYOUT_NAME
708
709        parser['profiles'] = {}
710        for profile in self.profiles:
711            if profile == JSON_PROFILE_NAME:
712                continue
713            dbg('ConfigBase::save: Processing profile: %s' % profile)
714            parser['profiles'][profile] = dict_diff(
715                    DEFAULTS['profiles']['default'], self.profiles[profile])
716
717        parser['layouts'] = {}
718        for layout in self.layouts:
719            if layout == JSON_LAYOUT_NAME:
720                continue
721            dbg('ConfigBase::save: Processing layout: %s' % layout)
722            parser['layouts'][layout] = self.layouts[layout]
723
724        parser['plugins'] = {}
725        for plugin in self.plugins:
726            dbg('ConfigBase::save: Processing plugin: %s' % plugin)
727            parser['plugins'][plugin] = self.plugins[plugin]
728
729        config_dir = get_config_dir()
730        if not os.path.isdir(config_dir):
731            os.makedirs(config_dir)
732
733        try:
734            if self.command_line_options.config:
735                filename = self.command_line_options.config
736            else:
737                filename = os.path.join(config_dir,'config')
738
739            if not os.path.isfile(filename):
740                open(filename, 'a').close()
741
742            backup_file = filename + '~'
743            shutil.copy2(filename, backup_file)
744
745            with open(filename, 'wb') as fh:
746                parser.write(fh)
747
748            os.remove(backup_file)
749        except Exception as ex:
750            err('ConfigBase::save: Unable to save config: %s' % ex)
751
752    def get_item(self, key, profile='default', plugin=None, default=None):
753        """Look up a configuration item"""
754        if profile not in self.profiles:
755            # Hitting this generally implies a bug
756            profile = 'default'
757
758        if key in self.global_config:
759            dbg('ConfigBase::get_item: %s found in globals: %s' %
760                    (key, self.global_config[key]))
761            return(self.global_config[key])
762        elif key in self.profiles[profile]:
763            dbg('ConfigBase::get_item: %s found in profile %s: %s' % (
764                    key, profile, self.profiles[profile][key]))
765            return(self.profiles[profile][key])
766        elif key == 'keybindings':
767            return(self.keybindings)
768        elif plugin and plugin in self.plugins and key in self.plugins[plugin]:
769            dbg('ConfigBase::get_item: %s found in plugin %s: %s' % (
770                    key, plugin, self.plugins[plugin][key]))
771            return(self.plugins[plugin][key])
772        elif default:
773            return default
774        else:
775            raise KeyError('ConfigBase::get_item: unknown key %s' % key)
776
777    def set_item(self, key, value, profile='default', plugin=None):
778        """Set a configuration item"""
779        dbg('ConfigBase::set_item: Setting %s=%s (profile=%s, plugin=%s)' %
780                (key, value, profile, plugin))
781
782        if key in self.global_config:
783            self.global_config[key] = value
784        elif key in self.profiles[profile]:
785            self.profiles[profile][key] = value
786        elif key == 'keybindings':
787            self.keybindings = value
788        elif plugin is not None:
789            if plugin not in self.plugins:
790                self.plugins[plugin] = {}
791            self.plugins[plugin][key] = value
792        else:
793            raise KeyError('ConfigBase::set_item: unknown key %s' % key)
794
795        return(True)
796
797    def get_plugin(self, plugin):
798        """Return a whole tree for a plugin"""
799        if plugin in self.plugins:
800            return(self.plugins[plugin])
801
802    def set_plugin(self, plugin, tree):
803        """Set a whole tree for a plugin"""
804        self.plugins[plugin] = tree
805
806    def del_plugin(self, plugin):
807        """Delete a whole tree for a plugin"""
808        if plugin in self.plugins:
809            del self.plugins[plugin]
810
811    def add_profile(self, profile):
812        """Add a new profile"""
813        if profile in self.profiles:
814            return(False)
815        self.profiles[profile] = copy(DEFAULTS['profiles']['default'])
816        return(True)
817
818    def add_layout(self, name, layout):
819        """Add a new layout"""
820        if name in self.layouts:
821            return(False)
822        self.layouts[name] = layout
823        return(True)
824
825    def replace_layout(self, name, layout):
826        """Replaces a layout with the given name"""
827        if not name in self.layouts:
828            return(False)
829        self.layouts[name] = layout
830        return(True)
831
832    def get_layout(self, layout):
833        """Return a layout"""
834        if layout in self.layouts:
835            return(self.layouts[layout])
836        else:
837            err('layout does not exist: %s' % layout)
838
839    def set_layout(self, layout, tree):
840        """Set a layout"""
841        self.layouts[layout] = tree
842
843