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