← Back to team overview

clicompanion-devs team mailing list archive

[Merge] lp:~dcaro/clicompanion/fix-608608 into lp:clicompanion

 

David Caro has proposed merging lp:~dcaro/clicompanion/fix-608608 into lp:clicompanion with lp:~dcaro/clicompanion/fix-910355 as a prerequisite.

Requested reviews:
  CLI Companion Development Team (clicompanion-devs)
Related bugs:
  Bug #608608 in CLI Companion: "Wishlist: Integration with Snipplr & Commandlinefu Services"
  https://bugs.launchpad.net/clicompanion/+bug/608608

For more details, see:
https://code.launchpad.net/~dcaro/clicompanion/fix-608608/+merge/87868

Done a lot (and I mean a lot) of changes in the code, this new version has a lot of improvements and now it's easy to extend and mantain. A lot of things can still be done but I think that it is now usable and stable enough for at least replace the version wthat we have not.

See the commit message for the features, i've tested it during two days, but as usual, there's no hurry, test it until you are convinced that it is good to go.


Thanks!
-- 
https://code.launchpad.net/~dcaro/clicompanion/fix-608608/+merge/87868
Your team CLI Companion Development Team is requested to review the proposed merge of lp:~dcaro/clicompanion/fix-608608 into lp:clicompanion.
=== modified file '.bzrignore'
--- .bzrignore	2010-08-22 00:14:56 +0000
+++ .bzrignore	2012-01-08 01:05:24 +0000
@@ -3,3 +3,6 @@
 .ropeproject/globalnames
 .ropeproject/history
 .ropeproject/objectdb
+./tags
+./build
+*~

=== modified file 'clicompanion'
--- clicompanion	2012-01-08 01:05:24 +0000
+++ clicompanion	2012-01-08 01:05:24 +0000
@@ -25,13 +25,12 @@
 
 
 parser = OptionParser(usage="%prog [-f] [-q]", version="%prog 1.1")
-parser.add_option("-f", "--file", dest="filename",
-                  help="Write report to FILE", metavar="FILE")
+parser.add_option("-f", "--file", dest="conffile",
+                  help="Configuration file to use.", metavar="FILE")
 parser.add_option("-c", "--cheatsheet", dest="cheatsheet",
                   help="Read cheatsheet from FILE", metavar="FILE")
-parser.add_option("-q", "--quiet",
-                  action="store_false", dest="verbose", default=True,
-                  help="Don't print status messages to stdout")
+parser.add_option("-d", "--debug", dest="debug", action='store_true',
+                  default=False, help="Print debug messages",)
 
 (options, args) = parser.parse_args()
 

=== modified file 'clicompanionlib/__init__.py'
--- clicompanionlib/__init__.py	2010-11-30 16:03:59 +0000
+++ clicompanionlib/__init__.py	2012-01-08 01:05:24 +0000
@@ -1,7 +1,7 @@
 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
 #
-# clicompanion.py - commandline tool.
+# __init__.py
 #
 # Copyright 2010 Duane Hinnen, Kenny Meyer
 #

=== modified file 'clicompanionlib/config.py'
--- clicompanionlib/config.py	2012-01-08 01:05:24 +0000
+++ clicompanionlib/config.py	2012-01-08 01:05:24 +0000
@@ -1,9 +1,9 @@
 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
 #
-# clicompanion.py - commandline tool.
+# config.py - Configuration classes for the clicompanion
 #
-# Copyright 2010 Duane Hinnen
+# Copyright 2012 David Caro <david.caro.estevez@xxxxxxxxx>
 #
 # This program is free software: you can redistribute it and/or modify it
 # under the terms of the GNU General Public License version 3, as published
@@ -18,246 +18,323 @@
 # with this program.  If not, see <http://www.gnu.org/licenses/>.
 #
 #
+# This file has the CLIConfig class definition, and the CLIConfigView, the
+# first is the main configuration model of the progran, where all the config
+# is stored and processed to be correct, also sets up all the required
+# configuration (default sections and keybindings) if they are not set.
+#
+# The CLIConfigViewer, is something similar to a view in MySQL, is an object
+# that has the same (almos all) methods than the normal CLIConfig, but only
+# shows a part of it, used to allow the plugins to handle their own
+# configurations (a prefix will be added, like the 'profiles::' prefix or the
+# name of the plugin that stores the config).
+
+
 import os
 import ConfigParser
-import clicompanionlib.utils as utils
+import collections
+import gtk
+import pango
+import clicompanionlib.utils as cc_utils
 from clicompanionlib.utils import dbg
-import collections
 
-CHEATSHEET = os.path.expanduser("~/.clicompanion2")
 CONFIGDIR = os.path.expanduser("~/.config/clicompanion/")
 CONFIGFILE = os.path.expanduser("~/.config/clicompanion/config")
 CONFIG_ORIG = "/etc/clicompanion.d/clicompanion2.config"
-DEFAULTS = { "scrollb": '500',
-             "colorf": '#FFFFFF',
-             "colorb": '#000000',
-             "encoding": 'UTF-8',
-             "debug": 'False'}
-
-## To avoid parsing the config file each time, we store the loaded config here
-CONFIG = None
-
-def create_config(conffile=CONFIGFILE):
-    global CONFIG
-    config = CONFIG
-    configdir = conffile.rsplit(os.sep,1)[0]
-    if not os.path.exists(configdir):
-        try:
-            os.makedirs(configdir)
-        except Exception, e:
-            print _('Unable to create config at dir %s (%s)')%(configdir,e)
-            return False
-    # reuse the config if able
-    if not config:
-        config = ConfigParser.SafeConfigParser(DEFAULTS)
-        # set a number of parameters
-        if os.path.isfile(conffile):
-            config.read([conffile])
-        else:
-            config.add_section("terminal")
-            for option, value in DEFAULTS.items():
-                config.set("terminal", option, value)
-        CONFIG = config
-    # Writing our configuration file
-    save_config(config, conffile)
-    print _("INFO: Created config file at %s.")%conffile
-    return config
-        
-
-def get_config_copy(config=None):
-    global CONFIG
-    if not config:
-        config = CONFIG
-    new_cfg = ConfigParser.SafeConfigParser(DEFAULTS)
-    for section in config.sections():
-        new_cfg.add_section(section)
-        for option in config.options(section):
-            new_cfg.set(section, option, config.get(section, option))
-    return new_cfg
-    
-
-def get_config(conffile=CONFIGFILE, confdir=CONFIGDIR):
-    global CONFIG
-    config = CONFIG
-    if not config:
-        dbg('Loading new config')
-        if not os.path.isfile(conffile):
-            config = create_config(conffile)
-        config = ConfigParser.SafeConfigParser(DEFAULTS)
-        config.add_section("terminal")
-        config.read([conffile])
-        CONFIG = config
-    else:
-        dbg('Reusing already loaded config')
-    return config
-
-
-def save_config(config, conffile=CONFIGFILE):
-    global CONFIG
-    dbg('Saving conffile at %s'%conffile)
-    with open(CONFIGFILE, 'wb') as f:
-        config.write(f)
-    CONFIG = config
-
-class Cheatsheet:
-    '''
-    comtainer class for the cheatsheet
-
-    Example of usage:
-    >>> c = config.Cheatsheet()
-    >>> c.load('/home/cascara/.clicompanion2')
-    >>> c[3]
-    ['uname -a', '', 'What kernel am I running\n']
-    >>> c.file
-    '/home/cascara/.clicompanion2'
-    >>> c[2]=[ 'mycmd', 'userui', 'desc' ]
-    >>> c[2]
-    ['mycmd', 'userui', 'desc']
-    >>> del c[2]
-    >>> c[2]
-    ['ps aux | grep ?', 'search string', 'Search active processes for search string\n']
-    >>> c.insert('cmd2','ui2','desc2',2)
-    >>> c[2]
-    ['cmd2', 'ui2', 'desc2']
-
-    '''
-    def __init__(self):
-        self.file = CHEATSHEET
-        self.commands = []
-
-    def __repr__(self):
-        return 'Config: %s - %s'%(self.file, self.commands)
-    
-    def load(self, cheatfile=None):
-        if not cheatfile:
-            self.file = CHEATSHEET
-            if not os.path.exists(CHEATSHEET):
-                if os.path.exists(CONFIG_ORIG):
-                    os.system ("cp %s %s" % (CONFIG_ORIG, CHEATSHEET))
-                else:
-                    # Oops! Looks like there's no default cheatsheet.
-                    # Then, create an empty cheatsheet.
-                    open(CHEATSHEET, 'w').close()
-        else:
-            self.file = cheatfile
-        try:
-            dbg('Reading cheatsheet from file %s'%self.file)
-            with open(self.file, 'r') as ch_fd:
-                ## try to detect if the line is a old fashines config line
-                ## (separated by ':'), when saved will rewrite it
-                no_tabs = True
-                some_colon = False
-                for line in ch_fd:
-                    line = line.strip()
-                    if not line:
-                        continue
-                    cmd, ui, desc = [ l.strip() for l in line.split('\t',2)] \
-                                        + ['',]*(3-len(line.split('\t',2)))
-                    if ':' in cmd:
-                        some_colon = True
-                    if ui or desc:
-                        no_tabs = False
-                    if cmd and [ cmd, ui, desc ] not in self.commands:
-                        self.commands.append([cmd, ui, desc])
-                        dbg('Adding command %s'%[cmd, ui, desc])
-                if no_tabs and some_colon:
-                    ## None of the commands had tabs, and all had ':' in the 
-                    ## cmd... most probably old config style
-                    print _("Detected old cheatsheet style at")\
-                            +" %s"%self.file+_(", parsing to new one.")
-                    for i in range(len(self.commands)):
-                        cmd, ui, desc = self.commands[i]
-                        cmd, ui, desc = [ l.strip() for l in cmd.split(':',2)] \
-                                        + ['',]*(3-len(cmd.split(':',2)))
-                        self.commands[i] = [cmd, ui, desc]
-                    self.save()
-        except IOError, e:
-            print _("Error while loading cheatfile")+" %s: %s"%(self.file, e)
-
-    def save(self, cheatfile=None):
-        '''
-        Saves the current config to the file cheatfile, or the file that was 
-        loaded.
-        NOTE: It does not overwrite the value self.file, that points to the file 
-        that was loaded
-        '''
-        if not cheatfile and self.file:
-            cheatfile = self.file
-        elif not cheatfile:
-            return False
-        try:
-            with open(cheatfile, 'wb') as ch_fd:
-                for command in self.commands:
-                    ch_fd.write('\t'.join(command)+'\n')
-        except IOError, e:
-            print _("Error writing cheatfile")+" %s: %s"%(cheatfile, e)
-            return False
-        return True
-
-    def __len__(self):
-        return len(self.commands)
-
-    def __getitem__(self, key):
-        return self.commands[key]
-
-    def __setitem__(self, key, value):
-        if not isinstance(value, collections.Iterable) or len(value) < 3:
-            raise ValueError('Value must be a container with three items, but got %s'%value)
-        if key < len(self.commands):
-            self.commands[key]=list(value)
-        else:
+
+
+## All the options (except keybindings) passed as name: (default, test), where
+## test can be one of 'bool', 'str', 'encoding', 'font', or a function to test
+## the value (the function must throw an exception on fail)
+DEFAULTS = {'profile': {"scrollb": ('500', 'int'),
+                         "color_scheme": ("Custom", 'str'),
+                         "colorf": ('#FFFFFF', gtk.gdk.color_parse),
+                         "colorb": ('#000000', gtk.gdk.color_parse),
+                         "use_system_colors": ("False", 'bool'),
+                         "encoding": ('UTF-8', 'encoding'),
+                         "font": (cc_utils.get_system_font(), 'font'),
+                         "use_system_font": ("False", 'bool'),
+                         "use_system_colors": ("False", 'bool'),
+                         "bold_text": ("False", 'bool'),
+                         "antialias": ("True", 'bool'),
+                         "sel_word": (u"-A-Za-z0-9,./?%&#:_", 'str'),
+                         "update_login_records": ("True", 'bool'),
+                        },
+             'general': {"debug": ('False', 'bool'),
+                         "plugins": ('LocalCommandList, CommandLineFU', 'str')
+                        },
+             'LocalCommandList': {"cheatsheet":
+                               (os.path.expanduser("~/.clicompanion2"), 'str'),
+                        },
+             }
+
+## Note that the modifiers must be specified as 'mod1+mod2+key', where the
+## modifiers are 'shift', 'alt','ctrl', in that order (shift+alt+ctrl+key), and
+## that the key pressed is the key affecteed by the modifiers, for example,
+## shift+ctrl+D (not shift+ctrl+d). And the function keys go uppercase (F10).
+DEFAULT_KEY_BINDINGS = {
+        'run_command': 'F4',
+        'add_command': 'F5',
+        'remove_command': 'F6',
+        'edit_command': 'unused',
+        'add_tab': 'F7',
+        'close_tab': 'unused',
+        'toggle_fullscreen': 'F12',
+        'toggle_maximize': 'F11',
+        'toggle_hide_ui': 'F9',
+        }
+
+### funcname : labelname
+## a function with the name funcname and signature void(void) must exist in the
+## main window class, and is the one that will be called when the keybinding is
+## actibated
+KEY_BINDINGS = {
+        'run_command': 'Run command',
+        'add_command': 'Add command',
+        'remove_command': 'Remove command',
+        'edit_command': 'Edit command',
+        'add_tab': 'Add tab',
+        'close_tab': 'Close tab',
+        'toggle_fullscreen': 'Toggle fullscreen',
+        'toggle_maximize': 'Maximize',
+        'toggle_hide_ui': 'Hide UI',
+        }
+
+
+class CLIConfig(ConfigParser.RawConfigParser):
+    def __init__(self, defaults=DEFAULTS, conffile=CONFIGFILE):
+        ConfigParser.RawConfigParser.__init__(self)
+        self.conffile = os.path.abspath(conffile)
+        configdir = self.conffile.rsplit(os.sep, 1)[0]
+        if not os.path.exists(configdir):
             try:
-                self.insert(*value, pos=key)
-            except ValueError, e:
-                raise ValueError('Value must be a container with three items, but got %s'%value)
-
-    def __iter__(self):
-        for command in self.commands:
-            yield command
-
-    def insert(self, cmd, ui, desc, pos=None):
-        if not [cmd, ui, desc] in self.commands:
-            if not pos:
-                self.commands.append([cmd, ui, desc])
-            else:
-                self.commands.insert(pos, [cmd, ui, desc])
-
-    def append(self, cmd, ui, desc):
-        self.insert(cmd, ui, desc)
-
-    def index(self, cmd, ui, value):
-        return self.commands.index([cmd, ui, desc])
-                    
-    def __delitem__(self, key):
-        del self.commands[key]
-    
-    def pop(self, key):
-        return self.commands.pop(key)
-
-    def del_by_value(self, cmd, ui, desc):
-        if [cmd, ui, desc] in self.commands:
-            return self.commands.pop(self.commands.index([cmd, ui, desc]))
-
-    def drag_n_drop(self, cmd1, cmd2, before=True):
-        if cmd1 in self.commands:
-            dbg('Dropping command from inside %s'%'_\t_'.join(cmd1))
-            i1 = self.commands.index(cmd1)
-            del self.commands[i1]
-            if cmd2:
-                i2 = self.commands.index(cmd2)
-                if before:
-                    self.commands.insert(i2, cmd1)
-                else:
-                    self.commands.insert(i2+1, cmd1)
-            else:
-                self.commands.append(cmd1)
-        else:
-            dbg('Dropping command from outside %s'%'_\t_'.join(cmd1))
-            if cmd2:
-                i2 = self.commands.index(cmd2)
-                if before:
-                    self.commands.insert(i2, cmd1)
-                else:
-                    self.commands.insert(i2+1, cmd1)
-            else:
-                self.commands.append(cmd1)
+                os.makedirs(configdir)
+            except Exception, e:
+                print _('Unable to create config at dir %s (%s)') \
+                        % (configdir, e)
+                return False
+        # set a number of default parameters, and fill the missing ones
+        if os.path.isfile(self.conffile):
+            self.read([self.conffile])
+            print _("INFO: Reading config file at %s.") % self.conffile
+        else:
+            print _("INFO: Creating config file at %s.") % self.conffile
+
+        for section in DEFAULTS.keys():
+            fullsection = section + '::default'
+            ## Set default profile options
+            if fullsection not in self.sections():
+                self.add_section(fullsection)
+                for option, optdesc in DEFAULTS[section].items():
+                    value, test = optdesc
+                    self.set(fullsection, option, value)
+        ## Set default keybindings
+        if 'keybindings' not in self.sections():
+            self.add_section("keybindings")
+        for option, value in DEFAULT_KEY_BINDINGS.items():
+            if not self.has_option('keybindings', option):
+                self.set('keybindings', option, value)
+        self.parse()
+        # Writing our configuration file
+        self.save()
+
+    def parse(self):
+        ## clean the default options to avoid seeing options where they are not
+        for option in self.defaults().keys():
+            self.remove_option('DEFAULT', option)
+        ## now parse the rest of sections
+        for section in self.sections():
+            for option in self.options(section):
+                if section == 'keybindings':
+                    if option not in KEY_BINDINGS.keys():
+                        print _("Option %s:%s not recognised, deleting." \
+                                % (section, option))
+                        self.remove_option(section, option)
+                else:
+                    if not '::' in section:
+                        print _("Deleting unrecognzed section %s." % section)
+                        self.remove_section(section)
+                        break
+                    secttype = section.split('::')[0]
+                    if secttype not in DEFAULTS:
+                        print _("Deleting unrecognized section %s." % section)
+                        self.remove_section(section)
+                        break
+                    if option not in DEFAULTS[secttype].keys():
+                        print _("Option %s:%s not recognised, deleting." \
+                                % (section, option))
+                        self.remove_option(section, option)
+                    else:
+                        val = self.get(section, option)
+                        defval, test = DEFAULTS[secttype][option]
+                        try:
+                            if test == 'str':
+                                continue
+                            elif test == 'int':
+                                res = self.getint(section, option)
+                            elif test == 'bool':
+                                res = self.getboolean(section, option)
+                            elif test == 'encoding':
+                                if val.lower() not in [enc.lower()
+                                                       for enc, desc
+                                                       in cc_utils.encodings]:
+                                    raise ValueError(
+                                        _('Option %s is not valid.') % test)
+                            elif test == 'font':
+                                fname, fsize = val.split(' ', 2)
+                                fsize = int(fsize)
+                                cont = gtk.TextView().create_pango_context()
+                                avail_fonts = cont.list_families()
+                                found = False
+                                for font in avail_fonts:
+                                    if fname == font.get_name():
+                                        found = True
+                                        break
+                                if not found:
+                                    raise ValueError(
+                                        _('Option %s is not valid.') % type)
+                            elif callable(test):
+                                res = test(val)
+                                if not res:
+                                    raise Exception
+                            else:
+                                        print _("Wrong specification for "
+                                            "option %s in file %s") \
+                                            % (option, __file__)
+                        except Exception, e:
+                            print (_('ERROR: Wrong config value for %s: %s ') \
+                                    % (option, val) +
+                                    _(',using default one %s.') % defval)
+                            self.set(section, option, defval)
+
+    def set(self, section, option, value):
+        if section == 'DEFAULT':
+            raise ConfigParser.NoSectionError(
+                'Section "DEFAULT" is not allowed. Use section '
+                    '"TYPE::default instead"')
+        else:
+            return ConfigParser.RawConfigParser.set(self, section,
+                                                    option, value)
+
+    def get(self, section, option):
+        if '::' in section:
+            sectiontag = section.split('::')[0]
+            if not self.has_option(section, option):
+                if not self.has_option(sectiontag + '::default', option):
+                    raise ConfigParser.NoOptionError(option, section)
+                return ConfigParser.RawConfigParser.get(self,
+                            sectiontag + '::default', option)
+        elif not self.has_option(section, option):
+            raise ConfigParser.NoOptionError(option, section)
+        return ConfigParser.RawConfigParser.get(self, section, option)
+
+    def get_config_copy(self):
+        new_cfg = CLIConfig(DEFAULTS)
+        for section in self.sections():
+            if section not in new_cfg.sections():
+                new_cfg.add_section(section)
+            for option in self.options(section):
+                new_cfg.set(section, option, self.get(section, option))
+        return new_cfg
+
+    def save(self, conffile=None):
+        if not conffile:
+            conffile = self.conffile
+        dbg('Saving conffile at %s' % conffile)
+        with open(conffile, 'wb') as f:
+            self.write(f)
+
+    def get_plugin_conf(self, plugin):
+        return CLIConfigView(plugin, self)
+
+
+class CLIConfigView():
+    '''
+    This class implements an editable view (hiding unwanted options) of the
+    CLIConfig class, for example, to avoid the plugins editing other options
+    but their own, some methods of the configRaw Parser are not implemented.
+    '''
+    def __init__(self, sectionkey, config):
+        self.key = sectionkey + '::'
+        self._config = config
+
+    def get(self, section, option):
+        if section == 'DEFAULT':
+            section = 'default'
+        if self.key + section not in self._config.sections():
+            raise ConfigParser.NoSectionError(
+                'The section %s does not exist.' % section)
+        return self._config.get(self.key + section, option)
+
+    def getint(self, section, option):
+        return self._config.getint(self.key + section, option)
+
+    def getfloat(self, section, option):
+        return self._config.getfloat(self.key + section, option)
+
+    def getboolean(self, section, option):
+        return self._config.getboolean(self.key + section, option)
+
+    def items(self, section):
+        return self._config.items(self.key + section)
+
+    def write(self, fileobject):
+        pass
+
+    def remove_option(self, section, option):
+        return self._config.remove_option(self.key + section, option)
+
+    def optionxform(self, option):
+        pass
+
+    def set(self, section, option, value):
+        if section == 'DEFAULT':
+            section = 'default'
+        if self.key + section not in self._config.sections():
+            raise ConfigParser.NoSectionError(
+                'The section %s does not exist.' % section)
+        return self._config.set(self.key + section, option, value)
+
+    def add_section(self, section):
+        return self._config.add_section(self.key + section)
+
+    def remove_section(self, section):
+        return self._config.remove_section(self.key + section)
+
+    def sections(self):
+        sections = []
+        for section in self._config.sections():
+            if section.startswith(self.key):
+                sections.append(section.split('::', 1)[1])
+        return sections
+
+    def options(self, section):
+        return self._config.options(self.key + section)
+
+    def defaults(self):
+        return self._config.options(self.key + 'default')
+
+    def has_section(self, section):
+        return self._config.has_section(self.key + section)
+
+    def has_option(self, section, option):
+        return self._config.has_option(self.key + section, option)
+
+    def readfp(self, filedesc, name='<???>'):
+        tempconf = ConfigParser.RawConfigParser()
+        tempconf.readfp(filedesc, name)
+        for option in tempconf.defaults():
+            self.set('DEFAULT', option, tempconf.get('DEFAULT', option))
+        for section in tempconf.sections():
+            if not self.has_section(section):
+                self.add_section(section)
+            for option in tempconf.options():
+                self.set(section, option)
+
+    def read(self, files):
+        for file in files:
+            with open(file, 'r') as fd:
+                self.readfp(fd)
+
+    def save(self):
+        self._config.save()

=== removed file 'clicompanionlib/controller.py'
--- clicompanionlib/controller.py	2012-01-08 01:05:24 +0000
+++ clicompanionlib/controller.py	1970-01-01 00:00:00 +0000
@@ -1,665 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-#
-# clicompanion - commandline tool.
-#
-# Copyright 2010 Duane Hinnen, Kenny Meyer
-#
-# This program is free software: you can redistribute it and/or modify it
-# under the terms of the GNU General Public License version 3, as published
-# by the Free Software Foundation.
-#
-# This program is distributed in the hope that it will be useful, but
-# WITHOUT ANY WARRANTY; without even the implied warranties of
-# MERCHANTABILITY, SATISFACTORY QUALITY, 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, see <http://www.gnu.org/licenses/>.
-#
-#
-
-import os
-import pygtk
-pygtk.require('2.0')
-import re
-import webbrowser
-import view
-import copy
-import clicompanionlib.tabs
-import clicompanionlib.config as cc_config
-import clicompanionlib.utils  as utils
-from clicompanionlib.utils import get_user_shell, dbg
-
-#if cc_config.get_config().get('terminal','debug') == 'True':
-#    utils.DEBUG = True
-
-# import vte and gtk or print error
-try:
-    import gtk
-except:
-    error = gtk.MessageDialog (None, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, gtk.BUTTONS_OK,
-        _("You need to install the python gtk bindings package 'python-gtk2'"))
-    error.run()
-    sys.exit (1)
-    
-try:
-    import vte
-except:
-    error = gtk.MessageDialog (None, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, gtk.BUTTONS_OK,
-        _("You need to install 'python-vte' the python bindings for libvte."))
-    error.run()
-    sys.exit (1)
-    
-
-
-class Actions(object):
-    ## Info Dialog Box
-    ## if a command needs more info EX: a package name, a path
-    def get_info(self, cmd, ui, desc):
-        dbg('Got command with user input')
-        ## Create Dialog object
-        dialog = gtk.MessageDialog(
-            None,
-            gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
-            gtk.MESSAGE_QUESTION,
-            gtk.BUTTONS_OK_CANCEL,
-            None)
-
-        # Primary text
-        dialog.set_markup(_("This command requires more information."))
-
-        ## create the text input field
-        entry = gtk.Entry()
-        ## allow the user to press enter to do ok
-        entry.connect("activate", self.responseToDialog, dialog, gtk.RESPONSE_OK)
-
-        ## create a horizontal box to pack the entry and a label
-        hbox = gtk.HBox()
-        hbox.pack_start(gtk.Label(ui+":"), False, 5, 5)
-        hbox.pack_end(entry)
-        ## some secondary text
-        dialog.format_secondary_markup(_("Please provide a "+ui))
-        ## add it and show it
-        dialog.vbox.pack_end(hbox, True, True, 0)
-        dialog.show_all()
-
-        ## Show the dialog
-        response = dialog.run()
-
-        ## user text assigned to a variable
-        text = entry.get_text()
-        user_input = text.split(' ')
-
-        ## The destroy method must be called otherwise the 'Close' button will
-        ## not work.
-        dialog.destroy()
-        if response != gtk.RESPONSE_OK:
-            user_input = None
-        return user_input
-
-    def responseToDialog(self, text, dialog, response):
-        dialog.response(response)
-
-    ## Add command dialog box
-    def add_command(self, mw):
-
-        ## Create Dialog object
-        dialog = gtk.MessageDialog(
-            None,
-            gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
-            gtk.MESSAGE_QUESTION,
-            gtk.BUTTONS_OK,
-            None)
-
-        ## primaary text
-        dialog.set_markup(_("Add a command to your command list"))
-
-        #create the text input field
-        entry1 = gtk.Entry()
-        entry2 = gtk.Entry()
-        entry3 = gtk.Entry()
-        ## allow the user to press enter to do ok
-        entry1.connect("activate", self.responseToDialog, dialog, gtk.RESPONSE_OK)
-        entry2.connect("activate", self.responseToDialog, dialog, gtk.RESPONSE_OK)
-        entry3.connect("activate", self.responseToDialog, dialog, gtk.RESPONSE_OK)
-
-        ## create three labels
-        hbox1 = gtk.HBox()
-        hbox1.pack_start(gtk.Label(_("Command")), False, 5, 5)
-        hbox1.pack_start(entry1, False, 5, 5)
-
-        hbox1.pack_start(gtk.Label(_("User Input")), False, 5, 5)
-        hbox1.pack_start(entry2, False, 5, 5)
-
-        hbox2 = gtk.HBox()
-        hbox2.pack_start(gtk.Label(_("Description")), False, 5, 5)
-        hbox2.pack_start(entry3, True, 5, 5)
-
-        ## cancel button
-        dialog.add_button(_('Cancel'), gtk.RESPONSE_DELETE_EVENT)
-        ## some secondary text
-        dialog.format_secondary_markup(
-            _("When entering a command use question marks(?) as placeholders if"
-              " user input is required when the command runs. Example: ls "
-              "/any/directory would be entered as, ls ? .For each question "
-              "mark(?) in your command, if any, use the User Input field to "
-              "provide a hint for each variable. Using our example ls ? you "
-              "could put directory as the User Input. Lastly provide a brief "
-              "Description."))
-
-        ## add it and show it
-        dialog.vbox.pack_end(hbox2, True, True, 0)
-        dialog.vbox.pack_end(hbox1, True, True, 0)
-        dialog.show_all()
-        ## Show the dialog
-        result = dialog.run()
-
-        if result == gtk.RESPONSE_OK:
-            ## user text assigned to a variable
-            text1 = entry1.get_text()
-            text2 = entry2.get_text()
-            text3 = entry3.get_text()
-            ## update commandsand sync with screen '''
-            view.CMNDS.append(text1, text2, text3)
-            mw.sync_cmnds()
-            view.CMNDS.save()
-
-        ## The destroy method must be called otherwise the 'Close' button will
-        ## not work.
-        dialog.destroy()
-        #return text
-
-    ## This the edit function
-    def edit_command(self, mw):
-        if not view.ROW:
-            return
-        lst_index = int(view.ROW[0][0])
-        model = mw.treeview.get_model()
-        cmd = ''.join(model[lst_index][0])
-        ui = ''.join(model[lst_index][1])
-        desc = ''.join(model[lst_index][2])
-
-        ## Create Dialog object
-        dialog = gtk.MessageDialog(
-            None,
-            gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
-            gtk.MESSAGE_QUESTION,
-            gtk.BUTTONS_OK,
-            None)
-
-        # primary text
-        dialog.set_markup(_("Edit a command in your command list"))
-
-        ## create the text input fields
-        entry1 = gtk.Entry()
-        entry1.set_text(cmd)
-        entry2 = gtk.Entry()
-        entry2.set_text(ui)
-        entry3 = gtk.Entry()
-        entry3.set_text(desc)
-        ## allow the user to press enter to do ok
-        entry1.connect("activate", self.responseToDialog, dialog, gtk.RESPONSE_OK)
-
-        ## create three labels
-        hbox1 = gtk.HBox()
-        hbox1.pack_start(gtk.Label(_("Command")), False, 5, 5)
-        hbox1.pack_start(entry1, False, 5, 5)
-
-        hbox1.pack_start(gtk.Label(_("User Input")), False, 5, 5)
-        hbox1.pack_start(entry2, False, 5, 5)
-
-        hbox2 = gtk.HBox()
-        hbox2.pack_start(gtk.Label(_("Description")), False, 5, 5)
-        hbox2.pack_start(entry3, True, 5, 5)
-
-        ## cancel button
-        dialog.add_button(_('Cancel'), gtk.RESPONSE_DELETE_EVENT)
-        ## some secondary text
-        dialog.format_secondary_markup(_("Please provide a command, description, and what type of user variable, if any, is required."))
-
-        ## add it and show it
-        dialog.vbox.pack_end(hbox2, True, True, 0)
-        dialog.vbox.pack_end(hbox1, True, True, 0)
-        dialog.show_all()
-        ## Show the dialog
-        result = dialog.run()
-
-        if result == gtk.RESPONSE_OK:
-            ## user text assigned to a variable
-            cmd = entry1.get_text()
-            ui = entry2.get_text()
-            desc = entry3.get_text()
-
-            if cmd != "":
-                cmd_index = model[lst_index][3]
-                dbg('Got index %d for command at pos %d'%(cmd_index, lst_index))
-                view.CMNDS[cmd_index] = [cmd, ui, desc]
-            mw.sync_cmnds()
-            view.CMNDS.save()
-        ## The destroy method must be called otherwise the 'Close' button will
-        ## not work.
-        dialog.destroy()
-
-
-    ## Remove command from command file and GUI
-    def remove_command(self, mw):
-        if not view.ROW:
-            return
-        ## get selected row
-        lst_index = int(view.ROW[0][0])
-        ## get selected element index, even from search filter
-        model = mw.treeview.get_model()
-        cmd_index = model[lst_index][3]
-        ## delete element from liststore and CMNDS
-        del view.CMNDS[cmd_index]
-        mw.sync_cmnds()
-        ## save changes
-        view.CMNDS.save()
-
-
-    def _filter_commands(self, widget, liststore, treeview):
-        """
-        Show commands matching a given search term.
-        The user should enter a term in the search box and the treeview should
-        only display the rows which contain the search term.
-        Pretty straight-forward.
-        """
-        search_term = widget.get_text().lower()
-        ## If the search term is empty, restore the liststore
-        if search_term == "":
-            view.FILTER = 0
-            treeview.set_model(liststore)
-            return
-
-        view.FILTER = 1
-        ## Create a TreeModelFilter object which provides auxiliary functions for
-        ## filtering data.
-        ## http://www.pygtk.org/pygtk2tutorial/sec-TreeModelSortAndTreeModelFilter.html
-        modelfilter = liststore.filter_new()
-        def search(modelfilter, iter, search_term):
-            try:
-                ## Iterate through every column and row and check if the search
-                ## term is there:
-                if search_term in modelfilter.get_value(iter, 0).lower() or \
-                   search_term in modelfilter.get_value(iter, 1).lower() or \
-                   search_term in modelfilter.get_value(iter, 2).lower() :
-                        return True
-                                         
-            except TypeError:
-                ## Python raises a TypeError if row data doesn't exist. Catch
-                ## that and fail silently.
-                pass
-            except AttributeError:
-                ## Python raises a AttributeError if row data was modified . Catch
-                ## that and fail silently.
-                pass
-        modelfilter.set_visible_func(search, search_term)
-        ## save the old liststore and cmnds
-        treeview.set_model(modelfilter)
-
-    ## send the command to the terminal
-    def run_command(self, mw):
-
-        ## if called without selecting a command from the list return
-        if not view.ROW:
-            return
-        text = ""
-        lst_index = int(view.ROW[0][0]) ## removes everything but number from [5,]
-
-        ## get the current notebook page so the function knows which terminal to run the command in.
-        pagenum = mw.notebook.get_current_page()
-        widget = mw.notebook.get_nth_page(pagenum)
-        page_widget = widget.get_child()
-
-        model = mw.treeview.get_model()
-        cmd = ''.join(model[lst_index][0])
-        ui = ''.join(model[lst_index][1])
-        desc = ''.join(model[lst_index][2])
-            
-        ## find how many ?(user arguments) are in command
-        match = re.findall('\?', cmd) 
-        '''
-        Make sure user arguments were found. Replace ? with something
-        .format can read. This is done so the user can just enter ?, when
-        adding a command where arguments are needed, instead
-        of {0[1]}, {0[1]}, {0[2]}
-        '''    
-        if match == False:
-            pass
-        else:
-            num = len(match)
-            ran = 0
-            new_cmnd = self.replace(cmd, num, ran)
-
-        if len(match) > 0: # command with user input
-            dbg('command with ui')
-            f_cmd = ""
-            while True:
-                try:
-                    ui_text = self.get_info(cmd, ui, desc)
-                    if ui_text == None:
-                        return
-                    dbg('Got ui "%s"'%' '.join(ui_text))
-                    if ''.join(ui_text) == '':
-                        raise IndexError
-                    f_cmd = new_cmnd.format(ui_text)
-                except IndexError, e: 
-                    error = gtk.MessageDialog (None, gtk.DIALOG_MODAL, \
-                        gtk.MESSAGE_ERROR, gtk.BUTTONS_OK,
-                        _("You need to enter full input. Space separated."))
-                    error.connect('response', lambda err, *x: err.destroy())
-                    error.run()
-                    continue
-                break
-            page_widget.feed_child(f_cmd+"\n") #send command w/ input
-            page_widget.show()
-            page_widget.grab_focus()
-        else: ## command that has no user input
-            page_widget.feed_child(cmd+"\n") #send command
-            page_widget.show()
-            page_widget.grab_focus()
-            
-    ## replace ? with {0[n]}
-    def replace(self, cmnd, num, ran):
-        replace_cmnd=re.sub('\?', '{0['+str(ran)+']}', cmnd, count=1)
-        cmnd = replace_cmnd
-        ran += 1
-        if ran < num:
-            return self.replace(cmnd, num, ran)    
-        else:
-            pass
-        return cmnd
-        
-    ## open the man page for selected command
-    def man_page(self, notebook):
-        import subprocess as sp
-        import shlex
-        try:
-            row_int = int(view.ROW[0][0]) # removes everything but number from EX: [5,]
-        except IndexError:  
-            ## When user not choose row, when is in filter mode
-            dialog = gtk.MessageDialog(
-                None,
-                gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
-                gtk.MESSAGE_QUESTION,
-                gtk.BUTTONS_OK,
-                None)
-            dialog.set_markup(_('You must choose a row to view the help'))
-            dialog.show_all()
-            dialog.run()
-            dialog.destroy()     
-            return 
-        ## get the manpage for the command
-        cmnd = view.CMNDS[row_int][0] #CMNDS is where commands are store
-        ## get each command for each pipe, It's not 100 accurate, but good 
-        ## enough (by now)
-        commands = []
-        next_part = True
-        found_sudo = False
-        for part in shlex.split(cmnd):
-            if next_part:
-                if part == 'sudo' and not found_sudo:
-                    found_sudo = True
-                    commands.append('sudo') 
-                else:
-                    if part not in commands:
-                        commands.append(part)
-                    next_part = False
-            else:
-                if part in [ '||', '&&', '&', '|']:
-                    next_part = True
-           
-        notebook = gtk.Notebook()
-        notebook.set_scrollable(True)
-        notebook.popup_enable()
-        notebook.set_properties(group_id=0, tab_vborder=0, tab_hborder=1, tab_pos=gtk.POS_TOP)
-        ## create a tab for each command
-        for command in commands:
-            scrolled_page = gtk.ScrolledWindow()
-            scrolled_page.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
-            tab = gtk.HBox()
-            tab_label = gtk.Label(command)
-            tab_label.show()
-            tab.pack_start(tab_label)
-            page = gtk.TextView()
-            page.set_wrap_mode(gtk.WRAP_WORD)
-            page.set_editable(False)
-            page.set_cursor_visible(False)
-            try:
-                manpage = sp.check_output(["man",command])
-            except sp.CalledProcessError, e:
-                manpage =  _('Failed to get manpage for command "%s"\nReason:\n%s')%(
-                        command, e)
-            textbuffer = page.get_buffer()
-            textbuffer.set_text(manpage)
-            scrolled_page.add(page)
-            notebook.append_page(scrolled_page, tab)
-        
-        help_win = gtk.Dialog()
-        help_win.set_title(_("Man page for %s")%cmnd)
-        help_win.vbox.pack_start(notebook, True, True, 0)
-        button = gtk.Button("close")
-        button.connect_object("clicked", lambda self: self.destroy(), help_win)
-        button.set_flags(gtk.CAN_DEFAULT)
-        help_win.action_area.pack_start( button, True, True, 0)
-        button.grab_default()
-        help_win.set_default_size(500,600)
-        help_win.show_all()
-
-
-    @staticmethod
-    def _filter_sudo_from(command):
-        """Filter the sudo from `command`, where `command` is a list.
-        Return the command list with the "sudo" filtered out.
-        """
-        if command[0].startswith("sudo"):
-            del command[0]
-            return command
-        return command
-
-
-    #TODO: Move to menus_buttons
-    def copy_paste(self, vte, event, data=None):
-        if event.button == 3:
-
-            time = event.time
-            ## right-click popup menu Copy
-            popupMenu = gtk.Menu()
-            menuPopup1 = gtk.ImageMenuItem (gtk.STOCK_COPY)
-            popupMenu.add(menuPopup1)
-            menuPopup1.connect('activate', lambda x: vte.copy_clipboard())
-            ## right-click popup menu Paste
-            menuPopup2 = gtk.ImageMenuItem (gtk.STOCK_PASTE)
-            popupMenu.add(menuPopup2)
-            menuPopup2.connect('activate', lambda x: vte.paste_clipboard())
-
-            ## Show popup menu
-            popupMenu.show_all()
-            popupMenu.popup( None, None, None, event.button, time)
-            return True
-        else:
-            pass
-            
-    ## close the window and quit
-    def delete_event(self, widget,  data=None):
-        gtk.main_quit()
-        return False
-    
-    ## Help --> About and Help --> Help menus
-    def about_event(self, widget, data=None):
-        # Create AboutDialog object
-        dialog = gtk.AboutDialog()
-
-        # Add the application name to the dialog
-        dialog.set_name('CLI Companion ')
-
-        # Set the application version
-        dialog.set_version('1.1')
-
-        # Pass a list of authors.  This is then connected to the 'Credits'
-        # button.  When clicked the buttons opens a new window showing
-        # each author on their own line.
-        dialog.set_authors(['Duane Hinnen', 'Kenny Meyer', 'Marcos Vanettai', 'Marek Bardoński'])
-
-        # Add a short comment about the application, this appears below the application
-        # name in the dialog
-        dialog.set_comments(_('This is a CLI Companion program.'))
-
-        # Add license information, this is connected to the 'License' button
-        # and is displayed in a new window.
-        dialog.set_license(_('Distributed under the GNU license. You can see it at <http://www.gnu.org/licenses/>.'))
-
-        # Show the dialog
-        dialog.run()
-
-        # The destroy method must be called otherwise the 'Close' button will
-        # not work.
-        dialog.destroy()
-
-
-    def help_event(self, widget, data=None):
-        webbrowser.open("http://launchpad.net/clicompanion";)
-        
-
-    def usage_event(self, widget, data=None):
-        dialog = gtk.Dialog("Usage",
-            None,
-            gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
-            (gtk.STOCK_CANCEL, gtk.RESPONSE_CLOSE))
-            
-        hbox1 = gtk.HBox()
-        hbox2 = gtk.HBox()
-        hbox21 = gtk.HBox()
-        hbox3 = gtk.HBox()
-        hbox4 = gtk.HBox()
-        hbox5 = gtk.HBox()
-        hbox6 = gtk.HBox()
-
-        hbox1.pack_start(gtk.Label(_("To maximize window, press F11")), False, 5, 5)
-        hbox2.pack_start(gtk.Label(_("To hide UI, press F12")), False, 5, 5)
-        hbox21.pack_start(gtk.Label(_("--------------------")), False, 5, 5)
-        hbox3.pack_start(gtk.Label(_("Run command - F4")), False, 5, 5) 
-        hbox4.pack_start(gtk.Label(_("Add command - F5")), False, 5, 5)        
-        hbox5.pack_start(gtk.Label(_("Remove command - F6")), False, 5, 5) 
-        hbox6.pack_start(gtk.Label(_("Add tab - F7")), False, 5, 5)        
-        
-        dialog.vbox.pack_end(hbox1, True, True, 0)
-        dialog.vbox.pack_end(hbox2, True, True, 0)
-        dialog.vbox.pack_end(hbox21, True, True, 0)
-        dialog.vbox.pack_end(hbox3, True, True, 0)
-        dialog.vbox.pack_end(hbox4, True, True, 0)
-        dialog.vbox.pack_end(hbox5, True, True, 0)
-        dialog.vbox.pack_end(hbox6, True, True, 0)
-        
-        dialog.show_all()
-        
-        result = dialog.run()
-        ## The destroy method must be called otherwise the 'Close' button will
-        ## not work.
-        dialog.destroy()
-        
-        
-    ## File --> Preferences    
-    def changed_cb(self, combobox, config):
-        dbg('Changed encoding')
-        model = combobox.get_model()
-        index = combobox.get_active()
-        if index>=0:
-            text_e = model[index][0]
-            encoding = text_e.split(':',1)[0].strip()
-            dbg('Setting encoding to "%s"'%encoding)
-            config.set("terminal", "encoding", encoding)
-
-        
-    def color_set_fg_cb(self, colorbutton_fg, config, tabs):
-        dbg('Changing fg color')
-        colorf = self.color2hex(colorbutton_fg)
-        config.set("terminal", "colorf", str(colorf))          
-        tabs.update_all_term_config(config)
-
-
-    def color_set_bg_cb(self, colorbutton_bg, config, tabs):
-        dbg('Changing bg color')
-        colorb = self.color2hex(colorbutton_bg)
-        config.set("terminal", "colorb", str(colorb))
-        tabs.update_all_term_config(config)
-        
-        
-    def color2hex(self, widget):
-        """Pull the colour values out of a Gtk ColorPicker widget and return them
-        as 8bit hex values, sinces its default behaviour is to give 16bit values"""
-        widcol = widget.get_color()
-        return('#%02x%02x%02x' % (widcol.red>>8, widcol.green>>8, widcol.blue>>8))
-        
-    def preferences(self, tabs, data=None):
-        '''
-        Preferences window
-        '''
-        dialog = gtk.Dialog(_("User Preferences"),
-            None,
-            gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
-            (gtk.STOCK_CANCEL, gtk.RESPONSE_CLOSE,
-            gtk.STOCK_OK, gtk.RESPONSE_OK))
-            
-        config = cc_config.get_config_copy()
-           
-        ##create the text input fields
-        entry1 = gtk.Entry()
-        entry1.set_text(config.get('terminal', 'scrollb'))
-        
-        ##combobox for selecting encoding
-        combobox = gtk.combo_box_new_text()
-        i=0
-        for encoding, desc in utils.encodings:
-            combobox.append_text(encoding + ': '+desc)
-            if encoding.strip().upper() == config.get('terminal','encoding').upper():
-                active = i
-            i=i+1
-        combobox.set_active(active)
-        combobox.connect('changed', self.changed_cb, config)
-        
-        ##colorbox for selecting text and background color
-        colorbutton_fg = gtk.ColorButton(
-            gtk.gdk.color_parse(config.get('terminal','colorf')))
-        colorbutton_bg = gtk.ColorButton(
-            gtk.gdk.color_parse(config.get('terminal','colorb')))
-
-        colorbutton_fg.connect('color-set', self.color_set_fg_cb, config, tabs)
-        colorbutton_bg.connect('color-set', self.color_set_bg_cb, config, tabs)
-        
-        ## allow the user to press enter to do ok
-        entry1.connect("activate", self.responseToDialog, dialog, gtk.RESPONSE_OK)
-
-        ## create the labels
-        hbox1 = gtk.HBox()
-        hbox1.pack_start(gtk.Label(_("Scrollback")), False, 5, 5)
-        hbox1.pack_start(entry1, False, 5, 5)
-
-        hbox1.pack_start(gtk.Label(_("Encoding")), False, 5, 5)
-        hbox1.pack_start(combobox, False, 5, 5)
-
-        hbox2 = gtk.HBox()
-        hbox2.pack_start(gtk.Label(_("Font color")), False, 5, 5)
-        hbox2.pack_start(colorbutton_fg, True, 5, 5)
-        
-        hbox2.pack_start(gtk.Label(_("Background color")), False, 5, 5)
-        hbox2.pack_start(colorbutton_bg, True, 5, 5)
-        
-        ## add it and show it
-        dialog.vbox.pack_end(hbox2, True, True, 0)
-        dialog.vbox.pack_end(hbox1, True, True, 0)
-        dialog.show_all()
-
-        result = dialog.run()
-        if result == gtk.RESPONSE_OK:
-            ## user text assigned to a variable
-            text_sb = entry1.get_text()
-            config.set("terminal", "scrollb", text_sb)
-            cc_config.save_config(config)
-        tabs.update_all_term_config()
-
-        ## The destroy method must be called otherwise the 'Close' button will
-        ## not work.
-        dialog.destroy()
-

=== added file 'clicompanionlib/helpers.py'
--- clicompanionlib/helpers.py	1970-01-01 00:00:00 +0000
+++ clicompanionlib/helpers.py	2012-01-08 01:05:24 +0000
@@ -0,0 +1,113 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# helpers.py - Helper dialogs for clicompanion
+#                                                                                                             
+# Copyright 2012 Duane Hinnen, Kenny Meyer, Marcos Vanetta, Marek Bardoński,
+#                David Caro
+#                                                                                                             
+# This program is free software: you can redistribute it and/or modify it       
+# under the terms of the GNU General Public License version 3, as published     
+# by the Free Software Foundation.                                                                            
+#                                                                                                             
+# This program is distributed in the hope that it will be useful, but           
+# WITHOUT ANY WARRANTY; without even the implied warranties of                  
+# MERCHANTABILITY, SATISFACTORY QUALITY, 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, see <http://www.gnu.org/licenses/>.               
+#    
+#    
+####
+## This file keeps some popups that are shown across the program execution,
+## that aren't directly related with a class, like the edit comand popup or the
+## about popup, but are too small to be kept in a separate file
+
+import os
+import re
+import pygtk
+pygtk.require('2.0')
+import gtk
+import subprocess as sp
+import shlex
+from clicompanionlib.utils import dbg
+
+
+class ManPage(gtk.Dialog):
+    def __init__(self, cmd):
+        if not cmd:
+            choose_row_error()
+            return
+        self.cmd = cmd
+        gtk.Dialog.__init__(self)
+        notebook = gtk.Notebook()
+        notebook.set_scrollable(True)
+        notebook.popup_enable()
+        notebook.set_properties(group_id=0, tab_vborder=0,
+                                tab_hborder=1, tab_pos=gtk.POS_TOP)
+        ## create a tab for each command
+        for command in self.get_commands():
+            scrolled_page = gtk.ScrolledWindow()
+            scrolled_page.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
+            tab = gtk.HBox()
+            tab_label = gtk.Label(command)
+            tab_label.show()
+            tab.pack_start(tab_label)
+            page = gtk.TextView()
+            page.set_wrap_mode(gtk.WRAP_WORD)
+            page.set_editable(False)
+            page.set_cursor_visible(False)
+            try:
+                manpage = sp.check_output(["man", command])
+            except sp.CalledProcessError, e:
+                manpage = _('Failed to get manpage for command '
+                            '"%s"\nReason:\n%s') % (command, e)
+            textbuffer = page.get_buffer()
+            textbuffer.set_text(manpage)
+            scrolled_page.add(page)
+            notebook.append_page(scrolled_page, tab)
+        self.set_title(_("Man page for %s") % cmd)
+        self.vbox.pack_start(notebook, True, True, 0)
+        button = gtk.Button("close")
+        button.connect_object("clicked", lambda *x: self.destroy(), self)
+        button.set_flags(gtk.CAN_DEFAULT)
+        self.action_area.pack_start(button, True, True, 0)
+        button.grab_default()
+        self.set_default_size(500, 600)
+        self.show_all()
+
+    def get_commands(self):
+        commands = []
+        next_part = True
+        found_sudo = False
+        try:
+            for part in shlex.split(self.cmd):
+                if next_part:
+                    if part == 'sudo' and not found_sudo:
+                        found_sudo = True
+                        commands.append('sudo')
+                    else:
+                        if part not in commands:
+                            commands.append(part)
+                        next_part = False
+                else:
+                    if part in ['||', '&&', '&', '|']:
+                        next_part = True
+        except Exception, e:
+            return [self.cmd]
+        return commands
+
+
+def show_about():
+    dialog = gtk.AboutDialog()
+    dialog.set_name('CLI Companion')
+    dialog.set_version('1.1')
+    dialog.set_authors([u'Duane Hinnen', u'Kenny Meyer', u'Marcos Vanettai',
+                        u'Marek Bardoński', u'David Caro'])
+    dialog.set_comments(_('This is a CLI Companion program.'))
+    dialog.set_license(_('Distributed under the GNU license. You can see it at'
+                         '<http://www.gnu.org/licenses/>.'))
+    dialog.run()
+    dialog.destroy()
+

=== modified file 'clicompanionlib/menus_buttons.py'
--- clicompanionlib/menus_buttons.py	2012-01-08 01:05:24 +0000
+++ clicompanionlib/menus_buttons.py	2012-01-08 01:05:24 +0000
@@ -1,7 +1,9 @@
 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
 #
-# Copyright 2010 Duane Hinnen, Kenny Meyer, Marcos Vanetta
+# menus_buttons.py - Menus and Buttons for the clicompanion
+#
+# Copyright 2010 Duane Hinnen, Kenny Meyer, Marcos Vanetta, David Caro
 #
 # This program is free software: you can redistribute it and/or modify it
 # under the terms of the GNU General Public License version 3, as published
@@ -16,199 +18,180 @@
 # with this program.  If not, see <http://www.gnu.org/licenses/>.
 #
 #
-#
-# This file contains the menus, buttons, and right clicks
-#
+# This file contains the upper menus (class FileMenu), and the lower buttons
+# (class Buttons) used in the CLI Companion main window
 
 import gtk
-import tabs
-
-
-class FileMenu(object):
-
-    def the_menu(self, mw):
-        actions = mw.actions
-        liststore = mw.liststore
-        tabs = mw.tabs
-        notebook = mw.notebook
+import gobject
+import webbrowser
+import clicompanionlib.helpers as cc_helpers
+
+
+class FileMenu(gtk.MenuBar):
+    __gsignals__ = {
+         'run_command': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
+                             ()),
+         'add_command': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
+                             ()),
+         'remove_command': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
+                             ()),
+         'edit_command': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
+                             ()),
+         'add_tab': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
+                             ()),
+         'close_tab': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
+                             ()),
+         'preferences': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
+                             ()),
+         'quit': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
+                             ()),
+         }
+
+    def __init__(self, config):
+        gtk.MenuBar.__init__(self)
         menu = gtk.Menu()
         #color = gtk.gdk.Color(65555, 62000, 65555)
         #menu.modify_bg(gtk.STATE_NORMAL, color)
-        root_menu = gtk.MenuItem(_("File"))       
+        root_menu = gtk.MenuItem(_("File"))
         root_menu.set_submenu(menu)
- 
-        menu2 = gtk.Menu()       
+
+        menu2 = gtk.Menu()
         #color = gtk.gdk.Color(65555, 62000, 60000)
         #menu2.modify_bg(gtk.STATE_NORMAL, color)
         root_menu2 = gtk.MenuItem(_("Help"))
         root_menu2.set_submenu(menu2)
 
-        ##FILE MENU ##     
+        ##FILE MENU ##
         ## Make 'Run' menu entry
-        menu_item1 = gtk.MenuItem(_("Run Command [F4]"))
+        menu_item1 = gtk.MenuItem(_("Run Command"))
         menu.append(menu_item1)
-        menu_item1.connect("activate", lambda *x: actions.run_command(mw))
+        menu_item1.connect("activate", lambda *x: self.emit('run_command'))
         menu_item1.show()
 
         ## Make 'Add' file menu entry
-        menu_item2 = gtk.MenuItem(_("Add Command [F5]"))
+        menu_item2 = gtk.MenuItem(_("Add Command"))
         menu.append(menu_item2)
-        menu_item2.connect("activate", lambda *x: actions.add_command(mw))
+        menu_item2.connect("activate", lambda *x: self.emit('add_command'))
         menu_item2.show()
-        
+
         ## Make 'Remove' file menu entry
-        menu_item3 = gtk.MenuItem(_("Remove Command [F6]"))
+        menu_item3 = gtk.MenuItem(_("Remove Command"))
         menu.append(menu_item3)
-        menu_item3.connect("activate", lambda *x: actions.remove_command(mw))
+        menu_item3.connect("activate", lambda *x: self.emit('remove_command'))
         menu_item3.show()
-        
+
         ## Make 'Add Tab' file menu entry
-        menu_item4 = gtk.MenuItem(_("Add Tab [F7]"))
-        menu.append(menu_item4)
-        menu_item4.connect("activate", lambda *x: tabs.add_tab(notebook))
-        menu_item4.show()
-        
+        menu_item4 = gtk.MenuItem(_("Add Tab"))
+        menu.append(menu_item4)
+        menu_item4.connect("activate", lambda *x: self.emit('add_tab'))
+        menu_item4.show()
+
+        ## Make 'Close Tab' file menu entry
+        menu_item4 = gtk.MenuItem(_("Close Tab"))
+        menu.append(menu_item4)
+        menu_item4.connect("activate", lambda *x: self.emit('close_tab'))
+        menu_item4.show()
+
         ## Make 'User Preferences' file menu entry
         menu_item5 = gtk.MenuItem(_("Preferences"))
         menu.append(menu_item5)
-        menu_item5.connect("activate", lambda *x: actions.preferences(tabs))
+        menu_item5.connect("activate", lambda *x: self.emit('preferences'))
         menu_item5.show()
 
         ## Make 'Quit' file menu entry
         menu_item6 = gtk.MenuItem(_("Quit"))
         menu.append(menu_item6)
-        menu_item6.connect("activate", actions.delete_event)
+        menu_item6.connect("activate", lambda *x: self.emit('quit'))
         menu_item6.show()
-        
-        
+
         ## HELP MENU ##
         ## Make 'About' file menu entry
         menu_item11 = gtk.MenuItem(_("About"))
         menu2.append(menu_item11)
-        menu_item11.connect("activate", actions.about_event)
+        menu_item11.connect("activate", lambda *x: cc_helpers.show_about())
         menu_item11.show()
 
-        ## Make 'Usage' file menu entry
-        menu_item22 = gtk.MenuItem(_("Usage"))
-        menu2.append(menu_item22)
-        menu_item22.connect("activate", actions.usage_event)
-        menu_item22.show()
-        
         ## Make 'Help' file menu entry
         menu_item22 = gtk.MenuItem(_("Help-online"))
         menu2.append(menu_item22)
-        menu_item22.connect("activate", actions.help_event)
+        menu_item22.connect("activate", lambda *x: webbrowser.open(
+                                        "http://launchpad.net/clicompanion";))
         menu_item22.show()
-       
-        
-
-        menu_bar = gtk.MenuBar()
-        #color = gtk.gdk.Color(60000, 65533, 60000) 
-        #menu_bar.modify_bg(gtk.STATE_NORMAL, color)
-        
-        menu_bar.append (root_menu) ##Menu bar(file)
-        menu_bar.append (root_menu2) ##Menu bar(help)       
-        #menu_bar.show() ##show File Menu # Menu Bar
-        ##Show 'File' Menu
-        #root_menu.show()
-        return menu_bar
-        
-        
-        
-    def buttons(self, mw,  spacing, layout):
-        #button box at bottom of main window
-        frame = gtk.Frame()
+
+        self.append(root_menu)  # Menu bar(file)
+        self.append(root_menu2)  # Menu bar(help)
+        self.show_all()
+
+
+class Buttons(gtk.Frame):
+    __gsignals__ = {
+         'run_command': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
+                             ()),
+         'add_command': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
+                             ()),
+         'remove_command': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
+                             ()),
+         'edit_command': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
+                             ()),
+         'add_tab': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
+                             ()),
+         'show_man': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
+                             ()),
+         'quit': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
+                             ()),
+         }
+
+    #button box at bottom of main window
+    def __init__(self, spacing, layout):
+        gtk.Frame.__init__(self)
         bbox = gtk.HButtonBox()
         bbox.set_border_width(5)
-        frame.add(bbox)
+        self.add(bbox)
 
         # Set the appearance of the Button Box
-        #color = gtk.gdk.Color(65000, 61000, 61000)
         bbox.set_layout(layout)
         bbox.set_spacing(spacing)
         # Run button
-        buttonRun = gtk.Button('_'+_("Run"))
+        buttonRun = gtk.Button(stock=gtk.STOCK_EXECUTE)
         bbox.add(buttonRun)
-        buttonRun.connect("clicked", lambda *x: mw.actions.run_command(mw))
+        buttonRun.connect("clicked", lambda *x: self.emit('run_command'))
         buttonRun.set_tooltip_text(_("Click to run a highlighted command"))
-        #buttonRun.modify_bg(gtk.STATE_NORMAL, color)        
-        #buttonRun.modify_bg(gtk.STATE_PRELIGHT, color)        
-        #buttonRun.modify_bg(gtk.STATE_INSENSITIVE, color)        
         # Add button
         buttonAdd = gtk.Button(stock=gtk.STOCK_ADD)
         bbox.add(buttonAdd)
-        buttonAdd.connect("clicked", lambda *x: mw.actions.add_command(mw))
-        buttonAdd.set_tooltip_text(_("Click to add a command to your command list"))
-        #buttonAdd.modify_bg(gtk.STATE_NORMAL, color)        
-        #buttonAdd.modify_bg(gtk.STATE_PRELIGHT, color)        
-        #buttonAdd.modify_bg(gtk.STATE_INSENSITIVE, color)
+        buttonAdd.connect("clicked", lambda *x: self.emit('add_command'))
+        buttonAdd.set_tooltip_text(_("Click to add a command to your"
+                                     "command list"))
         # Edit button
-        buttonEdit = gtk.Button('_'+_("Edit"))
+        buttonEdit = gtk.Button(stock=gtk.STOCK_EDIT)
         bbox.add(buttonEdit)
-        buttonEdit.connect("clicked", lambda *x: mw.actions.edit_command(mw))
-        buttonEdit.set_tooltip_text(_("Click to edit a command in your command list"))
-        #buttonEdit.modify_bg(gtk.STATE_NORMAL, color)        
-        #buttonEdit.modify_bg(gtk.STATE_PRELIGHT, color)        
-        #buttonEdit.modify_bg(gtk.STATE_INSENSITIVE, color)
+        buttonEdit.connect("clicked", lambda *x: self.emit('edit_command'))
+        buttonEdit.set_tooltip_text(_("Click to edit a command in your "
+                                      "command list"))
         # Delete button
         buttonDelete = gtk.Button(stock=gtk.STOCK_DELETE)
         bbox.add(buttonDelete)
-        buttonDelete.connect("clicked", lambda *x: mw.actions.remove_command(mw))
-        buttonDelete.set_tooltip_text(_("Click to delete a command in your command list"))
-        #buttonDelete.modify_bg(gtk.STATE_NORMAL, color)        
-        #buttonDelete.modify_bg(gtk.STATE_PRELIGHT, color)        
-        #buttonDelete.modify_bg(gtk.STATE_INSENSITIVE, color)
+        buttonDelete.connect("clicked", lambda *x: self.emit('remove_command'))
+        buttonDelete.set_tooltip_text(_("Click to delete a command in your "
+                                        "command list"))
         #Help Button
         buttonHelp = gtk.Button(stock=gtk.STOCK_HELP)
         bbox.add(buttonHelp)
-        buttonHelp.connect("clicked", lambda *x: mw.actions.man_page(mw.notebook))
-        buttonHelp.set_tooltip_text(_("Click to get help with a command in your command list"))
-        #buttonHelp.modify_bg(gtk.STATE_NORMAL, color)        
-        #buttonHelp.modify_bg(gtk.STATE_PRELIGHT, color)        
-        #buttonHelp.modify_bg(gtk.STATE_INSENSITIVE, color)
+        buttonHelp.connect("clicked", lambda *x: self.emit('show_man'))
+        buttonHelp.set_tooltip_text(_("Click to get help with a command in "
+                                      "your command list"))
+        #AddTab Button
+        button_addtab = gtk.Button(stock=gtk.STOCK_NEW)
+        bbox.add(button_addtab)
+        # Very ugly and nasty hack...
+        box = button_addtab.get_children()[0].get_children()[0]
+        lbl = box.get_children()[1]
+        lbl.set_text(_('Add tab'))
+        button_addtab.connect("clicked", lambda *x: self.emit('add_tab'))
+        button_addtab.set_tooltip_text(_("Click to add a terminal tab"))
         # Cancel button
         buttonCancel = gtk.Button(stock=gtk.STOCK_QUIT)
         bbox.add(buttonCancel)
-        buttonCancel.connect("clicked", mw.actions.delete_event)
+        buttonCancel.connect("clicked", lambda *x: self.emit('quit'))
         buttonCancel.set_tooltip_text(_("Click to quit CLI Companion"))
-        #buttonCancel.modify_bg(gtk.STATE_NORMAL, color)        
-        #buttonCancel.modify_bg(gtk.STATE_PRELIGHT, color)        
-        #buttonCancel.modify_bg(gtk.STATE_INSENSITIVE, color)
-        return frame      
-        
-        
-    #right-click popup menu for the Liststore(command list)
-    def right_click(self, widget, event, mw):
-        if event.button == 3:
-            x = int(event.x)
-            y = int(event.y)
-            time = event.time
-            pthinfo = mw.treeview.get_path_at_pos(x, y)
-            if pthinfo is not None:
-                path, col, cellx, celly = pthinfo
-                mw.treeview.grab_focus()
-                mw.treeview.set_cursor( path, col, 0)
-                
-                # right-click popup menu Apply(run)
-                popupMenu = gtk.Menu()
-                menuPopup1 = gtk.ImageMenuItem (gtk.STOCK_APPLY)
-                popupMenu.add(menuPopup1)
-                menuPopup1.connect("activate", lambda self, *x: mw.actions.run_command(mw))
-                # right-click popup menu Edit        
-                menuPopup2 = gtk.ImageMenuItem (gtk.STOCK_EDIT)
-                popupMenu.add(menuPopup2)
-                menuPopup2.connect("activate", lambda self, *x: mw.actions.edit_command(mw))
-                # right-click popup menu Delete                 
-                menuPopup3 = gtk.ImageMenuItem (gtk.STOCK_DELETE)
-                popupMenu.add(menuPopup3)
-                menuPopup3.connect("activate", lambda self, *x: mw.actions.remove_command(mw))
-                # right-click popup menu Help                
-                menuPopup4 = gtk.ImageMenuItem (gtk.STOCK_HELP)
-                popupMenu.add(menuPopup4)
-                menuPopup4.connect("activate", lambda self, *x: mw.actions.man_page(mw.notebook))
-                # Show popup menu
-                popupMenu.show_all()
-                popupMenu.popup( None, None, None, event.button, time)
-            return True          
-
-
+        self.show_all()

=== added file 'clicompanionlib/plugins.py'
--- clicompanionlib/plugins.py	1970-01-01 00:00:00 +0000
+++ clicompanionlib/plugins.py	2012-01-08 01:05:24 +0000
@@ -0,0 +1,207 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# plugins.py - Plugin related clases for the clicompanion
+#                                                                                                             
+# Copyright 2012 David Caro <david.caro.estevez@xxxxxxxxx>
+#                                                                                                             
+# This program is free software: you can redistribute it and/or modify it       
+# under the terms of the GNU General Public License version 3, as published     
+# by the Free Software Foundation.                                                                            
+#                                                                                                             
+# This program is distributed in the hope that it will be useful, but           
+# WITHOUT ANY WARRANTY; without even the implied warranties of                  
+# MERCHANTABILITY, SATISFACTORY QUALITY, 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, see <http://www.gnu.org/licenses/>.               
+#    
+#################################################
+## The plugins
+##
+## Here are defined the PluginLoader class and all the plugin base classes.
+##
+## The PluginLoader class
+## This class handles the loading and handpling of the plugins, it get a
+## directory and a list of the allowed plugins and loads all the allowed
+## plugins that it found on the dir *.py files (a plugin is a class that
+## inherits from the base class Plugin defined in this file).
+##
+## The Plugin class is the base class for all the plugins, if the plugin does
+## not inherit from that class, it will not be loaded.
+##
+## TabPlugin: This is a plugin that will be used as a tab in the upper notebook
+## of the application, like the LocalCommandList o CommandLineFU plugins
+##
+## PluginConfig: this will be the config tab in the preferences for the plugin.
+##
+## About the plugins:
+## The plugins have some attributes that should be set, like the __authors__,
+## and the __info__ attributes, that will be shown in the info page for the
+## plugin, and the __title__ that will be the plugin name shown in the plugins
+## list.
+
+import gobject
+import gtk
+import sys
+import os
+import inspect
+from clicompanionlib.utils import dbg
+
+
+class PluginLoader:
+    def __init__(self):
+        self.plugins = {}
+        self.allowed = []
+
+    def load(self, pluginsdir, allowed):
+        self.allowed = allowed
+        dbg('Allowing only the plugins %s' % allowed.__repr__())
+        sys.path.insert(0, pluginsdir)
+        try:
+            files = os.listdir(pluginsdir)
+        except OSError:
+            sys.path.remove(pluginsdir)
+            return False
+        for plugin in files:
+            pluginpath = os.path.join(pluginsdir, plugin)
+            if not os.path.isfile(pluginpath) or not  plugin[-3:] == '.py':
+                continue
+            dbg('Searching plugin file %s for plugins...' % plugin)
+            try:
+                module = __import__(plugin[:-3], globals(), locals(), [''])
+                for cname, mclass \
+                in inspect.getmembers(module, inspect.isclass):
+                    dbg('  Checking if class %s is a plugin.' % cname)
+                    if issubclass(mclass, Plugin):
+                        if cname not in self.plugins.keys():
+                            dbg('    Found plugin %s' % cname)
+                            self.plugins[cname] = mclass
+                            continue
+            except Exception, ex:
+                print 'Error searching plugin file %s: %s' % (plugin, ex)
+
+    def enable(self, plugins):
+        for plugin in plugins:
+            if plugin not in self.allowed:
+                self.allowed.append(plugin)
+
+    def get_plugins(self, capabilities=None):
+        plugins = []
+        if capabilities == None:
+            return [(pg, cs) for pg, cs in self.plugins.items()
+                        if pg in self.allowed()]
+        for plugin, pclass in self.plugins.items():
+            for capability in pclass.__capabilities__:
+                if capability in capabilities \
+                and plugin in self.allowed:
+                    plugins.append((plugin, pclass))
+                    dbg('Matching plugin %s for %s' % (plugin, capability))
+        return plugins
+
+    def get_plugin_conf(self, plugin):
+        if plugin + 'Config' in self.plugins:
+            return self.plugins[plugin + 'Config']
+
+    def is_enabled(self, plugin):
+        return plugin in self.allowed
+
+    def get_allowed(self):
+        return self.allowed
+
+    def get_disallowed(self):
+        disallowed = []
+        for plugin, pclass in self.plugins.items():
+            if plugin not in self.allowed \
+            and pclass.__capabilities__ != ['Config']:
+                disallowed.append(plugin)
+        return disallowed
+
+    def get_available_plugins(self):
+        return [pg for pg, cl
+                    in self.plugins.items()
+                    if ['Config'] == cl.__capabilities__]
+
+    def get_info(self, plugin):
+        if plugin in self.plugins.keys():
+            return self.plugins[plugin].__info__
+
+    def get_authors(self, plugin):
+        if plugin in self.plugins.keys():
+            return self.plugins[plugin].__authors__
+
+
+## To make all the classe sinherit from this one
+class Plugin(gtk.VBox):
+    def __init__(self):
+        gtk.VBox.__init__(self)
+
+
+class TabPlugin(Plugin):
+    '''
+    Generic Tab plugin that implements all the possible signals.
+    The *command signals are used to interact mainly with the LocalCommandList
+    plugin that handles the locally stored commands.
+    The add_tab is used to add  anew terminal tab.
+    '''
+    __gsignals__ = {
+         'run_command': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
+                             (str, str, str)),
+         'add_command': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
+                             (str, str, str)),
+         'remove_command': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
+                             (str, str, str)),
+         'edit_command': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
+                             (str, str, str)),
+         'add_tab': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
+                             ()),
+         'preferences': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
+                             ()),
+         'show_man': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
+                             (str,)),
+         'quit': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
+                             ()),
+         }
+    __capabilities__ = ['CommandTab']
+    __title__ = ''
+    __authors__ = ''
+    __info__ = ''
+
+    def reload(self):
+        '''
+        This method is called when a signal 'reload' is sent from it's
+        configurator
+        '''
+        pass
+
+    def get_command(self):
+        '''
+        This method is uset to retrieve a command, not sure if it's needed yet
+        '''
+        return None, None, None
+
+    def filter(self, string):
+        '''
+        This function is used to filter the commandslist, usually by the
+        search box
+        '''
+        pass
+
+
+class PluginConfig(Plugin):
+    '''
+    Generic plugin configuration window, to be used in the preferences plugins
+    tab
+    '''
+    __gsignals__ = {
+        ## when emited, this signal forces the reload of it's associated plugin
+        ## to reload the plugin config without having to restart the program
+         'reload': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
+                             ())
+         }
+    __capabilities__ = ['Config']
+
+    def __init__(self, config):
+        Plugin.__init__(self)
+        self.config = config

=== added file 'clicompanionlib/preferences.py'
--- clicompanionlib/preferences.py	1970-01-01 00:00:00 +0000
+++ clicompanionlib/preferences.py	2012-01-08 01:05:24 +0000
@@ -0,0 +1,708 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# preferences.py - Preferences dialogs for clicompanion
+#                                                                                                             
+# Copyright 2012 Duane Hinnen, Kenny Meyer, Marcos Vanettai, Marek Bardoński,
+#                David Caro <david.caro.estevez@xxxxxxxxx>
+#                                                                                                             
+# This program is free software: you can redistribute it and/or modify it       
+# under the terms of the GNU General Public License version 3, as published     
+# by the Free Software Foundation.                                                                            
+#                                                                                                             
+# This program is distributed in the hope that it will be useful, but           
+# WITHOUT ANY WARRANTY; without even the implied warranties of                  
+# MERCHANTABILITY, SATISFACTORY QUALITY, 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, see <http://www.gnu.org/licenses/>.               
+#    
+############################################
+## The preferences window is a popup that shows all the configuration options
+## allowing the user to change them, also handles the profiles creatin and
+## delete.
+##
+## The main class is the PreferencesWindow, that has all the other packed. Each
+## as a tab inside the preferences window.
+## The other classes are:
+##   - PluginsTab: handles the plugin activation and configuration
+##   - KeybindingsTab: handles the keybindings
+##   - ProfilesTab: The main tab for the profiles setting, this class is a
+##                   notebook with three classes (tabs) packed
+##      - ProfScrollingTab: the scrolling options tab inside the profiles tab
+##      - ProfColorsTab: the colors tab
+##      - ProfGeneralTab: the general setting tab
+##
+## About the profiles: each profile is a configuration section in the config
+## file with the name 'profile::profilename', having always the profile
+## 'profile::default', the will have all the default options (if it is not
+## found, it will be created with the harcoded options inside the code.
+
+import pygtk
+pygtk.require('2.0')
+import gtk
+import gobject
+import clicompanionlib.config as cc_config
+import clicompanionlib.utils as cc_utils
+from clicompanionlib.utils import dbg
+
+COLOR_SCHEMES = {
+        'Grey on Black': ['#aaaaaa', '#000000'],
+        'Black on Yellow': ['#000000', '#ffffdd'],
+        'Black on White': ['#000000', '#ffffff'],
+        'White on Black': ['#ffffff', '#000000'],
+        'Green on Black': ['#00ff00', '#000000'],
+        'Orange on Black': ['#e53c00', '#000000'],
+        'Custom': []
+        }
+
+
+def color2hex(color_16b):
+    """
+    Pull the colour values out of a Gtk ColorPicker widget and return them
+    as 8bit hex values, sinces its default behaviour is to give 16bit values
+    """
+    return('#%02x%02x%02x' % (color_16b.red >> 8,
+                              color_16b.green >> 8,
+                              color_16b.blue >> 8))
+
+
+class ProfGeneralTab(gtk.VBox):
+    def __init__(self, config, profile='default'):
+        gtk.VBox.__init__(self)
+        self.config = config
+        self.profile = profile
+        self.draw_all()
+
+    def draw_all(self):
+        ## 'use_system_font'
+        self.systemfont = gtk.CheckButton(label=_('Use system fixed'
+                                                  'width font'))
+        self.pack_start(self.systemfont, False, False, 8)
+        self.systemfont.set_active(
+                self.config.getboolean('profile::' + self.profile,
+                                       'use_system_font'))
+        self.systemfont.connect('toggled', lambda *x: self.update_font_btn())
+
+        ## 'font'
+        font_box = gtk.HBox()
+        font_box.pack_start(gtk.Label('Font:'), False, False, 8)
+        self.fontbtn = gtk.FontButton(self.config.get('profile::'
+                                            + self.profile, 'font'))
+        font_box.pack_start(self.fontbtn, False, False, 8)
+        self.pack_start(font_box, False, False, 8)
+        ## 'bold_text'
+        self.bold_text = gtk.CheckButton(label=_('Allow bold text'))
+        self.pack_start(self.bold_text, False, False, 8)
+        self.bold_text.set_active(self.config.getboolean('profile::'
+                                                + self.profile, 'bold_text'))
+        ## 'antialias'
+        self.antialias = gtk.CheckButton(label=_('Anti-alias text'))
+        self.pack_start(self.antialias, False, False, 8)
+        self.antialias.set_active(self.config.getboolean('profile::'
+                                                + self.profile, 'antialias'))
+        ## 'sel_word'
+        sel_word_box = gtk.HBox()
+        sel_word_box.pack_start(gtk.Label('Select-by-word characters:'))
+        self.sel_word_text = gtk.Entry()
+        self.sel_word_text.set_text(self.config.get('profile::'
+                                                + self.profile, 'sel_word'))
+        sel_word_box.pack_start(self.sel_word_text, False, False, 0)
+        self.pack_start(sel_word_box, False, False, 8)
+        ## System subsection
+        sys_lbl = gtk.Label()
+        sys_lbl.set_markup('<b>System Configuration</b>')
+        self.pack_start(sys_lbl, False, False, 4)
+        ## 'update_login_records'
+        self.update_login_records = gtk.CheckButton(
+                                        label=_('Update login records'))
+        self.pack_start(self.update_login_records, False, False, 8)
+        self.update_login_records.set_active(
+            self.config.getboolean('profile::' + self.profile,
+                                    'update_login_records'))
+        self.update_font_btn()
+
+    def update_font_btn(self):
+        if self.systemfont.get_active():
+            self.fontbtn.set_sensitive(False)
+        else:
+            self.fontbtn.set_sensitive(True)
+
+    def save_changes(self):
+        if 'profile::' + self.profile in self.config.sections():
+            self.config.set('profile::' + self.profile,
+                'use_system_font', '%s' % self.systemfont.get_active())
+            self.config.set('profile::' + self.profile,
+                'font', '%s' % self.fontbtn.get_font_name())
+            self.config.set('profile::' + self.profile,
+                'bold_text', '%s' % self.bold_text.get_active())
+            self.config.set('profile::' + self.profile,
+                'antialias', '%s' % self.antialias.get_active())
+            self.config.set('profile::' + self.profile,
+                'sel_word', self.sel_word_text.get_text())
+            self.config.set('profile::' + self.profile,
+                'update_login_records',
+                '%s' % self.update_login_records.get_active())
+
+    def set_profile(self, profile='default'):
+        self.save_changes()
+        if profile != self.profile:
+            self.profile = profile
+            self.update()
+            self.show_all()
+
+    def update(self):
+        for child in self.get_children():
+            self.remove(child)
+        self.draw_all()
+
+
+class ProfColorsTab(gtk.VBox):
+    def __init__(self, config, profile='default'):
+        gtk.VBox.__init__(self)
+        self.config = config
+        self.profile = profile
+        self.draw_all()
+
+    def draw_all(self):
+        ## 'use_system_colors'
+        self.systemcols = gtk.CheckButton(
+                            label=_('Use colors from system theme'))
+        self.pack_start(self.systemcols, False, False, 8)
+        self.systemcols.set_active(
+                self.config.getboolean('profile::' + self.profile,
+                                       'use_system_colors'))
+        self.systemcols.connect('toggled', lambda *x: self.update_sys_colors())
+
+        ## 'color_scheme'
+        hbox = gtk.HBox()
+        hbox.pack_start(gtk.Label('Color scheme:'), False, False, 8)
+        self.colsch_combo = gtk.combo_box_new_text()
+        color_scheme = self.config.get('profile::' + self.profile,
+                                        'color_scheme')
+        self.colsch_combo.append_text('Custom')
+        if color_scheme == 'Custom':
+            self.colsch_combo.set_active(0)
+        hbox.pack_start(self.colsch_combo, False, False, 8)
+        i = 0
+        for cs, colors in COLOR_SCHEMES.items():
+            i = i + 1
+            self.colsch_combo.append_text(cs)
+            if color_scheme == cs:
+                self.colsch_combo.set_active(i)
+        self.pack_start(hbox, False, False, 8)
+        self.colsch_combo.connect('changed',
+                lambda *x: self.update_color_btns())
+
+        ## 'colorf'
+        hbox = gtk.HBox()
+        hbox.pack_start(gtk.Label('Font color:'), False, False, 8)
+        self.colorf = gtk.ColorButton()
+        hbox.pack_start(self.colorf, False, False, 8)
+        self.pack_start(hbox, False, False, 8)
+        self.colorf.connect('color-set', lambda *x: self.update_custom_color())
+
+        ## 'colorb'
+        hbox = gtk.HBox()
+        hbox.pack_start(gtk.Label('Background color:'), False, False, 8)
+        self.colorb = gtk.ColorButton()
+        hbox.pack_start(self.colorb, False, False, 8)
+        self.pack_start(hbox, False, False, 8)
+        self.colorb.connect('color-set', lambda *x: self.update_custom_color())
+
+        self.update_sys_colors()
+
+    def update_sys_colors(self):
+        if not self.systemcols.get_active():
+            self.colsch_combo.set_sensitive(True)
+            self.update_color_btns()
+        else:
+            self.colsch_combo.set_sensitive(False)
+            self.update_color_btns()
+            self.colorb.set_sensitive(False)
+            self.colorf.set_sensitive(False)
+
+    def update_color_btns(self):
+        color_scheme = self.colsch_combo.get_active_text()
+        if color_scheme != 'Custom':
+            self.colorb.set_sensitive(False)
+            self.colorf.set_sensitive(False)
+            self.colorf.set_color(gtk.gdk.color_parse(
+                    COLOR_SCHEMES[color_scheme][0]))
+            self.colorb.set_color(gtk.gdk.color_parse(
+                    COLOR_SCHEMES[color_scheme][1]))
+        else:
+            self.colorb.set_sensitive(True)
+            self.colorf.set_sensitive(True)
+            self.colorf.set_color(gtk.gdk.color_parse(
+                    self.config.get('profile::' + self.profile, 'colorf')))
+            self.colorb.set_color(gtk.gdk.color_parse(
+                    self.config.get('profile::' + self.profile, 'colorb')))
+
+    def update_custom_color(self):
+        color_scheme = self.colsch_combo.get_active_text()
+        if color_scheme == 'Custom':
+            self.config.set('profile::' + self.profile, 'colorf',
+                    color2hex(self.colorf.get_color()))
+            self.config.set('profile::' + self.profile, 'colorb',
+                    color2hex(self.colorb.get_color()))
+
+    def save_changes(self):
+        if 'profile::' + self.profile in self.config.sections():
+            self.config.set('profile::' + self.profile, 'use_system_colors',
+                    self.systemcols.get_active().__repr__())
+            self.config.set('profile::' + self.profile, 'color_scheme',
+                    self.colsch_combo.get_active_text())
+            if self.colsch_combo.get_active_text() == 'Custom':
+                self.config.set('profile::' + self.profile, 'colorf',
+                        color2hex(self.colorf.get_color()))
+                self.config.set('profile::' + self.profile, 'colorb',
+                        color2hex(self.colorb.get_color()))
+
+    def set_profile(self, profile='default'):
+        self.save_changes()
+        if profile != self.profile:
+            self.profile = profile
+            self.update()
+            self.show_all()
+
+    def update(self):
+        for child in self.get_children():
+            self.remove(child)
+        self.draw_all()
+
+
+class ProfScrollingTab(gtk.VBox):
+    def __init__(self, config, profile='default'):
+        gtk.VBox.__init__(self)
+        self.config = config
+        self.profile = 'profile::' + profile
+        self.draw_all()
+
+    def draw_all(self):
+        hbox = gtk.HBox()
+        hbox.pack_start(gtk.Label('Number of history lines:'), False, False, 8)
+        self.scrollb_sb = gtk.SpinButton(
+            adjustment=gtk.Adjustment(upper=9999, step_incr=1))
+        self.scrollb_sb.set_wrap(True)
+        self.scrollb_sb.set_numeric(True)
+        self.scrollb_sb.set_value(self.config.getint(self.profile, 'scrollb'))
+        hbox.pack_start(self.scrollb_sb, False, False, 8)
+        self.pack_start(hbox, False, False, 8)
+
+    def set_profile(self, profile='default'):
+        if 'profile::' + profile != self.profile:
+            self.profile = 'profile::' + profile
+            self.update()
+            self.show_all()
+
+    def update(self):
+        for child in self.get_children():
+            self.remove(child)
+        self.draw_all()
+
+    def save_changes(self):
+        if self.profile in self.config.sections():
+            dbg('Setting scrollb to %d' % int(self.scrollb_sb.get_value()))
+            self.config.set(self.profile,
+                    'scrollb',
+                    str(int(self.scrollb_sb.get_value())))
+
+
+class ProfilesTab(gtk.HBox):
+    def __init__(self, config):
+        gtk.HBox.__init__(self)
+        self.config = config
+        self.tabs = []
+        self.gprofs = 0
+
+        vbox = gtk.VBox()
+        self.proflist = gtk.TreeView(gtk.ListStore(str))
+        self.init_proflist()
+        hbox = gtk.HBox()
+        add_btn = gtk.Button()
+        add_btn.add(self.get_img_box('Add', gtk.STOCK_ADD))
+        add_btn.connect('clicked', lambda *x: self.add_profile())
+        del_btn = gtk.Button()
+        del_btn.add(self.get_img_box('Remove', gtk.STOCK_DELETE))
+        del_btn.connect('clicked', lambda *x: self.del_profile())
+        hbox.pack_start(add_btn, False, False, 0)
+        hbox.pack_start(del_btn, False, False, 0)
+
+        vbox.pack_start(self.proflist, True, True, 0)
+        vbox.pack_start(hbox, False, False, 0)
+        self.pack_start(vbox)
+
+        self.tabs.append(('General', ProfGeneralTab(config)))
+        self.tabs.append(('Colors', ProfColorsTab(config)))
+        self.tabs.append(('Scrolling', ProfScrollingTab(config)))
+
+        self.options = gtk.Notebook()
+        for name, tab in self.tabs:
+            self.options.append_page(tab, gtk.Label(_(name)))
+
+        self.proflist.connect('cursor-changed', lambda *x: self.update_tabs())
+        self.pack_start(self.options)
+
+    def get_img_box(self, text, img):
+        box = gtk.HBox()
+        image = gtk.Image()
+        image.set_from_stock(img, gtk.ICON_SIZE_BUTTON)
+        label = gtk.Label(text)
+        box.pack_start(image, False, False, 0)
+        box.pack_start(label, False, False, 0)
+        return box
+
+    def add_text_col(self, colname, n=0):
+        col = gtk.TreeViewColumn()
+        col.set_title(_(colname))
+        self.render = gtk.CellRendererText()
+        self.render.connect('edited',
+                lambda cell, path, text: self.added_profile(path, text))
+        col.pack_start(self.render, expand=True)
+        col.add_attribute(self.render, 'text', n)
+        col.set_resizable(True)
+        col.set_sort_column_id(n)
+        self.proflist.append_column(col)
+
+    def init_proflist(self):
+        self.add_text_col('Profile')
+        model = self.proflist.get_model()
+        for section in self.config.sections():
+            if section.startswith('profile::'):
+                last = model.append((section[9:],))
+                if section == 'profile::default':
+                    self.proflist.set_cursor_on_cell(
+                        model.get_path(last),
+                        self.proflist.get_column(0))
+                self.gprofs += 1
+
+    def update_tabs(self):
+        selection = self.proflist.get_selection()
+        model, iterator = selection.get_selected()
+        if not iterator:
+            return
+        profile = model.get(iterator, 0)[0]
+        for name, tab in self.tabs:
+            if 'profile::' + profile in self.config.sections():
+                tab.set_profile(profile)
+
+    def save_all(self):
+        for name, tab in self.tabs:
+            tab.save_changes()
+
+    def add_profile(self):
+        self.gprofs += 1
+        model = self.proflist.get_model()
+        iterator = model.append(('New Profile %d' % self.gprofs,))
+        self.render.set_property('editable', True)
+        self.proflist.grab_focus()
+        self.proflist.set_cursor_on_cell(model.get_path(iterator),
+                self.proflist.get_column(0),
+                start_editing=True)
+
+    def added_profile(self, path, text):
+        dbg('Added profile %s' % text)
+        if 'profile::' + text in self.config.sections():
+            return
+        model = self.proflist.get_model()
+        model[path][0] = text
+        self.render.set_property('editable', False)
+        self.config.add_section('profile::' + text)
+        self.update_tabs()
+
+    def del_profile(self):
+        selection = self.proflist.get_selection()
+        model, iterator = selection.get_selected()
+        profile = model.get(iterator, 0)[0]
+        if profile != 'default':
+            self.config.remove_section('profile::' + profile)
+            model.remove(iterator)
+
+
+class KeybindingsTab(gtk.VBox):
+    def __init__(self, config):
+        gtk.VBox.__init__(self)
+        self.config = config
+        self.draw_all()
+
+    def draw_all(self):
+        self.labels = []
+        for kb_func, kb_name in cc_config.KEY_BINDINGS.items():
+            hbox = gtk.HBox()
+            lbl = gtk.Label(_(kb_name))
+            self.labels.append(lbl)
+            btn = gtk.Button(self.config.get('keybindings', kb_func))
+            btn.connect('clicked',
+                lambda wg, func: self.get_key(func), kb_func)
+            btn.set_size_request(100, -1)
+            hbox.pack_start(btn, False, False, 8)
+            del_btn = gtk.Button()
+            del_img = gtk.Image()
+            del_img.set_from_stock(gtk.STOCK_CLEAR, gtk.ICON_SIZE_BUTTON)
+            del_btn.add(del_img)
+            del_btn.connect('clicked',
+                lambda wg, func: self.get_key(func, 'not used'), kb_func)
+            hbox.pack_start(del_btn, False, False, 8)
+            hbox.pack_start(lbl, True, True, 8)
+            self.pack_start(hbox)
+
+    def update(self):
+        for child in self.children():
+            self.remove(child)
+        self.draw_all()
+        self.show_all()
+
+    def get_key(self, func, key=None):
+        if key != None:
+            self.config.set('keybindings', func, key)
+            self.update()
+            return
+        self.md = gtk.Dialog("Press the new key for '%s'" % func,
+                    None,
+                    gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT)
+        lbl = gtk.Label('Press a key')
+        self.md.get_content_area().pack_start((lbl), True, True, 0)
+        lbl.set_size_request(100, 100)
+        self.md.connect('key-press-event',
+            lambda w, event: self.key_pressed(func, event, lbl))
+        self.md.connect('key-release-event',
+            lambda w, event: self.key_released(event, lbl))
+        self.md.show_all()
+        self.md.run()
+
+    def key_released(self, event, lbl):
+        keycomb = cc_utils.get_keycomb(event)
+        combname = ''
+        activemods = keycomb.split('+')[:-1]
+        released = keycomb.rsplit('+', 1)[-1]
+        mods = {'shift': 'shift',
+                 'control': 'ctrl',
+                 'alt': 'alt',
+                 'super': 'super'}
+        for mod in mods.keys():
+            if mod in released.lower():
+                if mods[mod] in activemods:
+                    activemods.pop(activemods.index(mods[mod]))
+        combname = '+'.join(activemods) + '+'
+        if combname == '+':
+            combname = 'Press a key'
+        lbl.set_text(combname)
+
+    def key_pressed(self, func, event, lbl):
+        keycomb = cc_utils.get_keycomb(event)
+        if not cc_utils.only_modifier(event):
+            self.md.destroy()
+            self.config.set('keybindings', func, keycomb)
+            self.update()
+        else:
+            combname = ''
+            activemods = keycomb.split('+')[:-1]
+            pressed = keycomb.rsplit('+', 1)[-1]
+            mods = {'shift': 'shift',
+                     'control': 'ctrl',
+                     'alt': 'alt',
+                     'super': 'super'}
+            for mod in mods.keys():
+                if mod in pressed.lower():
+                    if mods[mod] not in activemods:
+                        activemods.append(mods[mod])
+            combname = '+'.join(activemods) + '+'
+            if combname == '+':
+                combname = 'Press a key'
+            lbl.set_text(combname)
+
+    def save_all(self):
+        pass
+
+
+class PluginsTab(gtk.HBox):
+    __gsignals__ = {
+         'changed-plugin': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
+                             (str, ))
+         }
+
+    def __init__(self, config, plugins):
+        gtk.HBox.__init__(self)
+        self.config = config
+        self.plugins = plugins
+
+        self.pluginlist = gtk.TreeView(gtk.ListStore(bool, str))
+        first = self.init_pluginlist()
+        self.pack_start(self.pluginlist, False, False, 8)
+
+        self.infonb = gtk.Notebook()
+        self.generate_infotabs(first)
+        self.pack_start(self.infonb, True, True, 8)
+
+        self.pluginlist.connect('row_activated',
+                lambda *x: self.toggle_plugin())
+        self.pluginlist.connect('cursor-changed',
+                lambda *x: self.update_info())
+
+    def init_pluginlist(self):
+        self.add_cbox_col('Enabled', 0)
+        self.add_text_col('Plugin Name', 1)
+        model = self.pluginlist.get_model()
+        first = None
+        for plugin in self.plugins.get_allowed():
+            if not first:
+                first = plugin
+            model.append((True, plugin))
+        for plugin in self.plugins.get_disallowed():
+            if not first:
+                first = plugin
+            model.append((False, plugin))
+        self.pluginlist.set_cursor((0,))
+        return first
+
+    def add_cbox_col(self, colname, n=0):
+        col = gtk.TreeViewColumn()
+        col.set_title(_(colname))
+        render = gtk.CellRendererToggle()
+        col.pack_start(render, expand=True)
+        col.add_attribute(render, 'active', n)
+        col.set_resizable(True)
+        col.set_sort_column_id(n)
+        self.pluginlist.append_column(col)
+
+    def add_text_col(self, colname, n=0):
+        col = gtk.TreeViewColumn()
+        col.set_title(_(colname))
+        render = gtk.CellRendererText()
+        col.pack_start(render, expand=True)
+        col.add_attribute(render, 'text', n)
+        col.set_resizable(True)
+        col.set_sort_column_id(n)
+        self.pluginlist.append_column(col)
+
+    def toggle_plugin(self):
+        selection = self.pluginlist.get_selection()
+        model, iterator = selection.get_selected()
+        oldvalue, plugin = model.get(iterator, 0, 1)
+        if plugin == 'LocalCommandList':
+            self.show_warning()
+            return
+        model.set(iterator, 0, not oldvalue)
+
+    def generate_infotabs(self, plugin):
+        '''
+        Adds the plugins info and config page (if any) to the plugins info
+        notebook
+        '''
+        confplg = self.plugins.get_plugin_conf(plugin)
+
+        if confplg:
+            conf_tab = confplg(self.config.get_plugin_conf(plugin))
+            self.infonb.append_page(
+                    conf_tab,
+                    gtk.Label(_('Configutarion')))
+            conf_tab.connect('reload',
+                lambda wg, pl: self.emit('changed-plugin', plugin),
+                plugin)
+
+        self.infonb.append_page(self.generate_infopage(plugin),
+                    gtk.Label(_('About')))
+
+    def generate_infopage(self, plugin):
+        '''
+        Generates the plugins info page
+        '''
+        info = self.plugins.get_info(plugin)
+        authors = self.plugins.get_authors(plugin)
+        page = gtk.TextView()
+        page.set_wrap_mode(gtk.WRAP_WORD)
+        page.set_editable(False)
+        buffer = page.get_buffer()
+        scrolled_window = gtk.ScrolledWindow()
+        scrolled_window.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
+        scrolled_window.add(page)
+        iter = buffer.get_iter_at_offset(0)
+        buffer.insert(iter, info + '\n\nAuthors:\n' + authors)
+        return scrolled_window
+
+    def update_info(self):
+        selection = self.pluginlist.get_selection()
+        model, iterator = selection.get_selected()
+        enabled, plugin = model.get(iterator, 0, 1)
+        for child in self.infonb.get_children():
+            self.infonb.remove(child)
+        self.generate_infotabs(plugin)
+        self.show_all()
+
+    def save_all(self):
+        newenabled = []
+        model = self.pluginlist.get_model()
+        elem = model.get_iter_first()
+        while elem:
+            enabled, plugin = model.get(elem, 0, 1)
+            if enabled:
+                newenabled.append(plugin)
+            elem = model.iter_next(elem)
+        self.config.set('general::default', 'plugins', ', '.join(newenabled))
+
+    def show_warning(self):
+        dlg = gtk.MessageDialog(
+                     None,
+                     gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
+                     gtk.MESSAGE_ERROR,
+                     gtk.BUTTONS_CLOSE,
+                     message_format=_('Can\'t disable "LocalCommandList'))
+        dlg.format_secondary_text(_('The plugin "LocalCommandList is the main'
+            ' plugin for CLI Companion, and can\'t be disalbed, sorry.'))
+        dlg.run()
+        dlg.destroy()
+
+
+class PreferencesWindow(gtk.Dialog):
+    '''
+    Preferences window, the tabs are needed for the preview
+    '''
+    __gsignals__ = {
+         'preview': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
+                             (str, str))
+         }
+
+    def __init__(self, config, plugins):
+        gtk.Dialog.__init__(self, _("User Preferences"),
+                     None,
+                     gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
+                     (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
+                      gtk.STOCK_OK, gtk.RESPONSE_OK))
+        self.config = config
+        self.plugins = plugins
+        self.config_bkp = self.config.get_config_copy()
+        self.changed_plugins = []
+
+        mainwdg = self.get_content_area()
+        self.tabs = gtk.Notebook()
+        mainwdg.pack_start(self.tabs, True, True, 0)
+
+        ## profiles
+        proftab = ProfilesTab(config)
+        self.tabs.append_page(proftab, gtk.Label('Profiles'))
+        ## keybindings
+        keybind_tab = KeybindingsTab(config)
+        self.tabs.append_page(keybind_tab, gtk.Label('Keybindings'))
+        ## plugins
+        plug_tab = PluginsTab(config, plugins)
+        plug_tab.connect('changed-plugin',
+            lambda wg, pl: self.mark_changed_plugins(pl))
+        self.tabs.append_page(plug_tab, gtk.Label('Plugins'))
+
+    def mark_changed_plugins(self, plugin):
+        if plugin not in self.changed_plugins:
+            self.changed_plugins.append(plugin)
+
+    def run(self):
+        self.show_all()
+        response = gtk.Dialog.run(self)
+        if response == gtk.RESPONSE_OK:
+            for i in range(self.tabs.get_n_pages()):
+                self.tabs.get_nth_page(i).save_all()
+            config = self.config
+        else:
+            config = None
+        self.destroy()
+        return config, self.changed_plugins

=== modified file 'clicompanionlib/tabs.py'
--- clicompanionlib/tabs.py	2012-01-08 01:05:24 +0000
+++ clicompanionlib/tabs.py	2012-01-08 01:05:24 +0000
@@ -1,6 +1,9 @@
 #!/usr/bin/env python
-#
-# Copyright 2010 Duane Hinnen, Kenny Meyer, Marcos Vanetta
+# -*- coding: utf-8 -*-
+#
+# tabs.py - Terminal tab handling classes for clicompanion
+#
+# Copyright 2010 Duane Hinnen, Kenny Meyer, Marcos Vanetta, David Caro
 #
 # This program is free software: you can redistribute it and/or modify it
 # under the terms of the GNU General Public License version 3, as published
@@ -15,6 +18,14 @@
 # with this program.  If not, see <http://www.gnu.org/licenses/>.
 #
 #
+# This file contains the classes used to provide the terminals secion of the
+# main window.
+#
+# TerminalTab: This class implements one tab of the terminals notebook, with
+# it's own profile and vte associated.
+#
+# TerminalsNotebook: This class implements the notebook, where the terminals
+# will be added.
 #
 
 import os
@@ -22,157 +33,388 @@
 pygtk.require('2.0')
 import gtk
 import vte
-import clicompanionlib.config as cc_config
-
-from clicompanionlib.utils import get_user_shell, dbg
-import clicompanionlib.controller
-import clicompanionlib.utils as utils
+import re
 import view
-
-
-class Tabs(object):
-    '''
-    add a new terminal in a tab above the current terminal
-    '''
-    def __init__(self):
-        #definition nop - (no of pages) reflects no of terminal tabs left (some may be closed by the user)
-        self.nop = 0
-        #definition gcp - how many pages is visible
+import gobject
+import pango
+import gconf
+from clicompanionlib.utils import dbg
+import clicompanionlib.utils as cc_utils
+import clicompanionlib.helpers as cc_helpers
+import clicompanionlib.preferences as cc_pref
+
+
+class TerminalTab(gtk.ScrolledWindow):
+    __gsignals__ = {
+         'quit': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
+                             ()),
+         'add_tab': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
+                             ()),
+         'rename': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
+                             (str, )),
+         'preferences': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
+                             ()),
+    }
+
+    def __init__(self, title, config, profile='default'):
+        gtk.ScrolledWindow.__init__(self)
+        self.config = config
+        self.title = title
+        self.profile = 'profile::' + profile
+        self.vte = vte.Terminal()
+        self.add(self.vte)
+        self.vte.connect("child-exited", lambda *x: self.emit('quit'))
+        self.update_records = self.config.getboolean(self.profile,
+                                        'update_login_records')
+        dbg('Updating login records: ' + self.update_records.__repr__())
+        self.vte.fork_command(cc_utils.shell_lookup(),
+           logutmp=self.update_records,
+           logwtmp=self.update_records,
+           loglastlog=self.update_records)
+        self.vte.connect("button_press_event", self.copy_paste_menu)
+        self.update_config()
+        self.show_all()
+
+    def update_config(self, config=None, preview=False):
+        if not config:
+            config = self.config
+        elif not preview:
+            self.config = config
+        if self.profile not in config.sections():
+            self.profile = 'profile::default'
+            if self.profile not in config.sections():
+                config.add_section(self.profile)
+        dbg(self.profile)
+        dbg(','.join([config.get(self.profile, option)
+                     for option in config.options(self.profile)]))
+
+        ## Scrollback
+        try:
+            config_scrollback = config.getint(self.profile, 'scrollb')
+        except ValueError:
+            print _("WARNING: Invalid value for property '%s', int expected:"
+                    " got '%s', using default '%s'") % (
+                        'scrollb',
+                        config.get(self.profile, 'scrollb'),
+                        config.get('DEFAULT', 'scrollb'))
+            config.set(self.profile, 'scrollb',
+                        config.get('DEFAULT', 'scrollb'))
+            config_scrollback = config.getint('DEFAULT', 'scrollb')
+        self.vte.set_scrollback_lines(config_scrollback)
+
+        color = ('#2e3436:#cc0000:#4e9a06:#c4a000:#3465a4:#75507b:#06989a:'
+                '#d3d7cf:#555753:#ef2929:#8ae234:#fce94f:#729fcf:#ad7fa8:'
+                '#34e2e2:#eeeeec')
+        colors = color.split(':')
+        palette = []
+        for color in colors:
+            if color:
+                palette.append(gtk.gdk.color_parse(color))
+
+        #### Colors
+        if config.getboolean(self.profile, 'use_system_colors'):
+            config_color_fore = self.vte.get_style().text[gtk.STATE_NORMAL]
+            config_color_back = self.vte.get_style().base[gtk.STATE_NORMAL]
+        else:
+            color_scheme = config.get(self.profile, 'color_scheme')
+            if color_scheme != 'Custom':
+                fgcolor, bgcolor = cc_pref.COLOR_SCHEMES[color_scheme]
+            else:
+                fgcolor = config.get(self.profile, 'colorf')
+                bgcolor = config.get(self.profile, 'colorb')
+
+            try:
+                config_color_fore = gtk.gdk.color_parse(fgcolor)
+            except ValueError, e:
+                print _("WARNING: Invalid value for property '%s':"
+                        " got '%s', using default '%s'.") % (
+                            'colorf',
+                            fgcolor,
+                            config.get('DEFAULT', 'colorf'))
+                config.set(self.profile, 'colorf',
+                            config.get('DEFAULT', 'colorf'))
+                config_color_fore = gtk.gdk.color_parse(config.get('DEFAULT',
+                                                            'colorf'))
+
+            try:
+                config_color_back = gtk.gdk.color_parse(bgcolor)
+            except ValueError, e:
+                print _("WARNING: Invalid value for property '%s':"
+                        " got '%s', using default '%s'.") % (
+                            'colorb',
+                            bgcolor,
+                            config.get('DEFAULT', 'colorb'))
+                config.set(self.profile, 'colorb',
+                        config.get('DEFAULT', 'colorb'))
+                config_color_back = gtk.gdk.color_parse(config.get('DEFAULT',
+                                                        'colorb'))
+        self.vte.set_colors(config_color_fore, config_color_back, palette)
+
+        ### Encoding
+        config_encoding = config.get(self.profile, 'encoding')
+        if config_encoding.upper() not in [enc.upper()
+                                          for enc, desc
+                                          in cc_utils.encodings]:
+            print _("WARNING: Invalid value for property '%s':"
+                    " got '%s', using default '%s'") \
+                    % ('encoding', config_encoding,
+                        config.get('DEFAULT', 'encoding'))
+            config.set(self.profile, 'encoding',
+                        config.get('DEFAULT', 'encoding'))
+            config_encoding = config.get('DEFAULT', 'encoding')
+        self.vte.set_encoding(config_encoding)
+
+        ## Font
+        if config.getboolean(self.profile, 'use_system_font'):
+            fontname = cc_utils.get_system_font(
+                        lambda *x: self.update_config())
+        else:
+            fontname = config.get(self.profile, 'font')
+        font = pango.FontDescription(fontname)
+        if not font or not fontname:
+            print _("WARNING: Invalid value for property '%s':"
+                    " got '%s', using default '%s'") % (
+                        'font',
+                        fontname,
+                        cc_utils.get_system_font())
+            config.set('DEFAULT', 'font', c_utils.get_system_font())
+            fontname = config.get('DEFAULT', 'font')
+        font = pango.FontDescription(fontname)
+        if font:
+            self.vte.set_font_full(font,
+                                  config.getboolean(self.profile, 'antialias'))
+
+        update_records = config.getboolean(self.profile,
+                                           'update_login_records')
+        if update_records != self.update_records:
+            if not preview:
+                self.update_records = update_records
+            dbg('Updating login records: ' + update_records.__repr__())
+            self.vte.feed('\n\r')
+            self.vte.fork_command(cc_utils.shell_lookup(),
+               logutmp=update_records,
+               logwtmp=update_records,
+               loglastlog=update_records)
+
+        self.vte.set_allow_bold(config.getboolean(self.profile, 'bold_text'))
+        self.vte.set_word_chars(config.get(self.profile, 'sel_word'))
+
+    def copy_paste_menu(self, vte, event):
+        if event.button == 3:
+            time = event.time
+            ## right-click popup menu Copy
+            popupMenu = gtk.Menu()
+            menuPopup1 = gtk.ImageMenuItem(gtk.STOCK_COPY)
+            popupMenu.add(menuPopup1)
+            menuPopup1.connect('activate', lambda x: vte.copy_clipboard())
+            ## right-click popup menu Paste
+            menuPopup2 = gtk.ImageMenuItem(gtk.STOCK_PASTE)
+            popupMenu.add(menuPopup2)
+            menuPopup2.connect('activate', lambda x: vte.paste_clipboard())
+            ## right-click popup menu Rename
+            menuPopup3 = gtk.ImageMenuItem(gtk.STOCK_EDIT)
+            menuPopup3.set_label(_('Rename'))
+            popupMenu.add(menuPopup3)
+            menuPopup3.connect('activate', lambda x: self.rename())
+            ## right-click popup menu Add Tab
+            menuPopup3 = gtk.ImageMenuItem(gtk.STOCK_PREFERENCES)
+            menuPopup3.set_label(_('Configure'))
+            popupMenu.add(menuPopup3)
+            menuPopup3.connect('activate', lambda x: self.emit('preferences'))
+            ## right-click popup menu Add Tab
+            menuPopup3 = gtk.ImageMenuItem(gtk.STOCK_ADD)
+            menuPopup3.set_label(_('Add tab'))
+            popupMenu.add(menuPopup3)
+            menuPopup3.connect('activate', lambda x: self.emit('add_tab'))
+            ## right-click popup menu Profiles
+            menuit_prof = gtk.MenuItem()
+            menuit_prof.set_label(_('Profiles'))
+            submenu_prof = gtk.Menu()
+            menuit_prof.set_submenu(submenu_prof)
+            popupMenu.add(menuit_prof)
+            for section in self.config.sections():
+                if section.startswith('profile::'):
+                    subitem = gtk.MenuItem()
+                    subitem.set_label(_(section[9:]))
+                    submenu_prof.add(subitem)
+                    subitem.connect('activate',
+                        lambda wg, *x: self.change_profile(
+                            wg.get_label()))
+            ## right-click popup menu Close Tab
+            menuPopup3 = gtk.ImageMenuItem(gtk.STOCK_CLOSE)
+            menuPopup3.set_label(_('Close tab'))
+            popupMenu.add(menuPopup3)
+            menuPopup3.connect('activate', lambda x: self.emit('quit'))
+            ## Show popup menu
+            popupMenu.show_all()
+            popupMenu.popup(None, None, None, event.button, time)
+            return True
+
+    def rename(self):
+        dlg = gtk.Dialog("Enter the new name",
+                   None,
+                   gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
+                   (gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
+                    gtk.STOCK_OK, gtk.RESPONSE_ACCEPT))
+        entry = gtk.Entry()
+        entry.connect("activate", lambda *x: dlg.response(gtk.RESPONSE_ACCEPT))
+        entry.set_text(self.title)
+        dlg.vbox.pack_start(entry)
+        dlg.show_all()
+        response = dlg.run()
+        if response == gtk.RESPONSE_ACCEPT:
+            text = entry.get_text()
+            self.emit('rename', text)
+        dlg.destroy()
+
+    def run_command(self, cmd, ui, desc):
+        if not cmd:
+            dbg('Empty command... doing nothing')
+            return
+        '''
+        Make sure user arguments were found. Replace ? with something
+        .format can read. This is done so the user can just enter ?, when
+        adding a command where arguments are needed, instead
+        of {0[1]}, {0[1]}, {0[2]}
+        '''
+        ## find how many ?(user arguments) are in command
+        match = re.findall('\?', cmd)
+        if match:
+            num = len(match)
+            ran = 0
+            new_cmd = cc_utils.replace(cmd, num, ran)
+
+        if len(match) > 0:  # command with user input
+            dbg('command with ui')
+            cmd_info_win = cc_helpers.CommandInfoWindow(new_cmd, ui, desc)
+            cmd = cmd_info_win.run()
+            if cmd == None:
+                return
+        self.vte.feed_child(cmd + "\n")  # send command
+        self.show()
+        self.grab_focus()
+
+    def change_profile(self, profile):
+        dbg(profile)
+        self.profile = 'profile::' + profile
+        self.update_config()
+
+
+class TerminalsNotebook(gtk.Notebook):
+    __gsignals__ = {
+         'quit': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
+                             ()),
+         'preferences': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
+                             ()),
+    }
+
+    def __init__(self, config):
+        gtk.Notebook.__init__(self)
+        #definition gcp - global page count, how many pages have been created
         self.gcp = 0
-
-    def add_tab(self, notebook):
+        self.global_config = config
+        ## The "Add Tab" tab
+        add_tab_button = gtk.Button("+")
+        ## tooltip for "Add Tab" tab
+        add_tab_button.set_tooltip_text(_("Click to add another tab"))
+        ## create first tab
+        self.append_page(gtk.Label(""), add_tab_button)
+        add_tab_button.connect("clicked", lambda *x: self.add_tab())
+        self.set_size_request(700, 120)
+
+    def focus(self):
+        num = self.get_current_page()
+        self.get_nth_page(num).vte.grab_focus()
+
+    def add_tab(self, title=None):
         dbg('Adding a new tab')
-        _vte = vte.Terminal()
-        if view.NETBOOKMODE == 1:
-            _vte.set_size_request(700, 120)
-        else:
-		    _vte.set_size_request(700, 220) 
-       
-        _vte.connect("child-exited", lambda term: gtk.main_quit())
-        _vte.fork_command(get_user_shell()) # Get the user's default shell
-        
-        self.update_term_config(_vte)
-
-        vte_tab = gtk.ScrolledWindow()
-        vte_tab.add(_vte)
-        #notebook.set_show_tabs(True)
-        #notebook.set_show_border(True)
-        
-        self.nop += 1
         self.gcp += 1
-        pagenum = ('Tab %d') % self.gcp
-        if self.nop > 1:
+        if title == None:
+            title = 'Tab %d' % self.gcp
+
+        newtab = TerminalTab(title, self.global_config)
+
+        if self.get_n_pages() > 1:
             dbg('More than one tab, showing them.')
-            view.MainWindow.notebook.set_show_tabs(True)
+            self.set_show_tabs(True)
+        label = self.create_tab_label(title, newtab)
+        self.insert_page(newtab, label, self.get_n_pages() - 1)
+        self.set_current_page(self.get_n_pages() - 2)
+        self.set_scrollable(True)
+        # signal handler for tab
+        newtab.connect("quit", lambda *x: self.quit_tab(newtab))
+        newtab.connect("add_tab", lambda *x: self.add_tab())
+        newtab.connect("preferences", lambda *x: self.emit('preferences'))
+        newtab.connect("rename",
+                lambda wg, text: self.rename_tab(newtab, text))
+        self.focus()
+        return newtab
+
+    def create_tab_label(self, title, tab):
+        ## Create the tab's labe with button
         box = gtk.HBox()
-        label = gtk.Label(pagenum)
+        label = gtk.Label(title)
         box.pack_start(label, True, True)
-        
-        
         ## x image for tab close button
-        close_image = gtk.image_new_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU)
+        close_image = gtk.image_new_from_stock(gtk.STOCK_CLOSE,
+                                               gtk.ICON_SIZE_MENU)
         ## close button
         closebtn = gtk.Button()
         closebtn.set_relief(gtk.RELIEF_NONE)
         closebtn.set_focus_on_click(True)
-
         closebtn.add(close_image)
         ## put button in a box and show box
         box.pack_end(closebtn, False, False)
         box.show_all()
-
-        view.MainWindow.notebook.prepend_page(vte_tab, box) # add tab
-        view.MainWindow.notebook.set_scrollable(True)
-        actions = clicompanionlib.controller.Actions()
-        _vte.connect("button_press_event", actions.copy_paste, None)
-        vte_tab.grab_focus()
-        # signal handler for tab
-        closebtn.connect("clicked", lambda *x: self.close_tab(vte_tab, notebook))
-                
-        vte_tab.show_all()
-
-        return vte_tab
-
+        closebtn.connect("clicked", lambda *x: self.close_tab(tab))
+        return box
+
+    def rename_tab(self, tab, newname):
+        dbg('Renaming tab to %s' % newname)
+        label = self.create_tab_label(newname, tab)
+        self.set_tab_label(tab, label)
+        self.focus()
 
     ## Remove a page from the notebook
-    def close_tab(self, vte_tab, notebook):
+    def close_tab(self, tab):
         ## get the page number of the tab we wanted to close
-        pagenum = view.MainWindow.notebook.page_num(vte_tab)
+        pagenum = self.page_num(tab)
         ## and close it
-        view.MainWindow.notebook.remove_page(pagenum)
-        self.nop -= 1
-        if self.nop <= 1:
-            view.MainWindow.notebook.set_show_tabs(False)
-        
-        # check if the focus does not go to the last page (ie with only a + sign)
-        if view.MainWindow.notebook.get_current_page() == self.nop:
-            view.MainWindow.notebook.prev_page()
-        
+        self.remove_page(pagenum)
+        if self.get_n_pages() < 3:
+            self.set_show_tabs(False)
+
+        # check if the focus does not go to the last page (ie with only a +
+        # sign)
+        if self.get_current_page() == self.get_n_pages() - 1:
+            self.prev_page()
+        if self.get_n_pages() != 1:
+            self.focus()
+
+    def quit_tab(self, tab=None):
+        if not tab:
+            tab = self.get_nth_page(self.get_current_page())
+        self.close_tab(tab)
+        if self.get_n_pages() == 1:
+            self.emit('quit')
+
+    def run_command(self, cmd, ui, desc):
+        ## get the current notebook page so the function knows which terminal
+        ## to run the command in.
+        pagenum = self.get_current_page()
+        page = self.get_nth_page(pagenum)
+        page.run_command(cmd, ui, desc)
+        self.focus()
+
     def update_all_term_config(self, config=None):
-        for pagenum in range(view.MainWindow.notebook.get_n_pages()):
-            page = view.MainWindow.notebook.get_nth_page(pagenum)
-            dbg(page)
-            if isinstance(page, gtk.ScrolledWindow):
-                for grandson in page.get_children():
-                    dbg(grandson)
-                    if isinstance(grandson,vte.Terminal):
-                        self.update_term_config(grandson, config)
-        
-    def update_term_config(self, _vte, config=None):
+        self.global_config = config
+        for pagenum in range(self.get_n_pages()):
+            page = self.get_nth_page(pagenum)
+            if isinstance(page, TerminalTab):
+                self.update_term_config(page, config)
+
+    def update_term_config(self, tab, config=None):
         ##set terminal preferences from conig file data
         if not config:
-            config = cc_config.get_config()
-        try:
-            config_scrollback = config.getint('terminal', 'scrollb')
-        except ValueError:
-            print _("WARNING: Invalid value for property 'terminal', int expected:"
-                    " got '%s', using default '%s'")%(
-                        config.get('terminal', 'scrollb'),
-                        config.get('DEFAULT', 'scrollb'))
-            config.set('terminal','scrollb',config.get('DEFAULT', 'scrollb'))
-            config_scrollback = config.getint('DEFAULT', 'scrollb')
-        _vte.set_scrollback_lines(config_scrollback)
-        
-        color = '#2e3436:#cc0000:#4e9a06:#c4a000:#3465a4:#75507b:#06989a:#d3d7cf:#555753:#ef2929:#8ae234:#fce94f:#729fcf:#ad7fa8:#34e2e2:#eeeeec'
-        colors = color.split(':')
-        palette = []
-        for color in colors:
-            if color:
-                palette.append(gtk.gdk.color_parse(color))
-        
-        try:
-            config_color_fore = gtk.gdk.color_parse(config.get('terminal', 'colorf'))
-        except ValueError, e:
-            print _("WARNING: Invalid value for property '%s':"
-                    " got '%s', using default '%s'.")%(
-                        'colorf',
-                        config.get('terminal', 'colorf'),
-                        config.get('DEFAULT', 'colorf'))
-            config.set('terminal','colorf',config.get('DEFAULT', 'colorf'))
-            config_color_fore = gtk.gdk.color_parse(config.get('DEFAULT', 'colorf'))
-
-        try:
-            config_color_back = gtk.gdk.color_parse(config.get('terminal', 'colorb'))
-        except ValueError, e:
-            print _("WARNING: Invalid value for property '%s':"
-                    " got '%s', using default '%s'.")%(
-                        'colorb',
-                        config.get('terminal', 'colorb'),
-                        config.get('DEFAULT', 'colorb'))
-            config.set('terminal','colorb',config.get('DEFAULT', 'colorb'))
-            config_color_back = gtk.gdk.color_parse(config.get('DEFAULT', 'colorb'))
-        _vte.set_colors(config_color_fore, config_color_back, palette)
-        
-        config_encoding = config.get('terminal', 'encoding')
-        if config_encoding.upper() not in [ enc.upper() for enc, desc in utils.encodings]:
-            print _("WARNING: Invalid value for property '%s':"
-                    " got '%s', using default '%s'")%(
-                        'encoding',
-                        config_encoding,
-                        config.get('DEFAULT', 'encoding'))
-            config.set('terminal','encoding',config.get('DEFAULT', 'encoding'))
-            config_encoding = config.get('DEFAULT', 'encoding')
-        _vte.set_encoding(config_encoding)
-
-
-        
+            config = self.global_config
+        tab.update_config(config)

=== modified file 'clicompanionlib/utils.py'
--- clicompanionlib/utils.py	2012-01-08 01:05:24 +0000
+++ clicompanionlib/utils.py	2012-01-08 01:05:24 +0000
@@ -1,9 +1,9 @@
 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
 #
-# clicompanion.py - commandline tool.
+# utils.py - Some helpful functions for clicompanion
 #
-# Copyright 2010 Duane Hinnen, Kenny Meyer, Marcos Vanetta
+# Copyright 2010 Duane Hinnen, Kenny Meyer, Marcos Vanetta, David Caro
 #
 # This program is free software: you can redistribute it and/or modify it
 # under the terms of the GNU General Public License version 3, as published
@@ -18,17 +18,19 @@
 # with this program.  If not, see <http://www.gnu.org/licenses/>.
 #
 #
-
-"""
-A collection of useful functions.
-"""
-
-import getpass
+# In this file are implemented some functions that do not need any clicompanion
+# class and can somehow be useful in more than one module.
+
 import os
-import sys 
-import gtk 
-import pwd 
+import sys
+import gtk
+import pwd
 import inspect
+import re
+try:
+    import gconf
+except ImportError:
+    pass
 
 
 ## set to True if you want to see more logs
@@ -37,95 +39,97 @@
 DEBUGCLASSES = []
 DEBUGMETHODS = []
 
+gconf_cli = None
+
 ## list gotten from terminator (https://launchpad.net/terminator)
 encodings = [
     ["ISO-8859-1", _("Western")],
     ["ISO-8859-2", _("Central European")],
-    ["ISO-8859-3", _("South European") ],
-    ["ISO-8859-4", _("Baltic") ],
-    ["ISO-8859-5", _("Cyrillic") ],
-    ["ISO-8859-6", _("Arabic") ],
-    ["ISO-8859-7", _("Greek") ],
-    ["ISO-8859-8", _("Hebrew Visual") ],
-    ["ISO-8859-8-I", _("Hebrew") ],
-    ["ISO-8859-9", _("Turkish") ],
-    ["ISO-8859-10", _("Nordic") ],
-    ["ISO-8859-13", _("Baltic") ],
-    ["ISO-8859-14", _("Celtic") ],
-    ["ISO-8859-15", _("Western") ],
-    ["ISO-8859-16", _("Romanian") ],
-    #    ["UTF-7", _("Unicode") ],
-    ["UTF-8", _("Unicode") ],
-    #    ["UTF-16", _("Unicode") ],
-    #    ["UCS-2", _("Unicode") ],
-    #    ["UCS-4", _("Unicode") ],
-    ["ARMSCII-8", _("Armenian") ],
-    ["BIG5", _("Chinese Traditional") ],
-    ["BIG5-HKSCS", _("Chinese Traditional") ],
-    ["CP866", _("Cyrillic/Russian") ],
-    ["EUC-JP", _("Japanese") ],
-    ["EUC-KR", _("Korean") ],
-    ["EUC-TW", _("Chinese Traditional") ],
-    ["GB18030", _("Chinese Simplified") ],
-    ["GB2312", _("Chinese Simplified") ],
-    ["GBK", _("Chinese Simplified") ],
-    ["GEORGIAN-PS", _("Georgian") ],
-    ["HZ", _("Chinese Simplified") ],
-    ["IBM850", _("Western") ],
-    ["IBM852", _("Central European") ],
-    ["IBM855", _("Cyrillic") ],
-    ["IBM857", _("Turkish") ],
-    ["IBM862", _("Hebrew") ],
-    ["IBM864", _("Arabic") ],
-    ["ISO-2022-JP", _("Japanese") ],
-    ["ISO-2022-KR", _("Korean") ],
-    ["EUC-TW", _("Chinese Traditional") ],
-    ["GB18030", _("Chinese Simplified") ],
-    ["GB2312", _("Chinese Simplified") ],
-    ["GBK", _("Chinese Simplified") ],
-    ["GEORGIAN-PS", _("Georgian") ],
-    ["HZ", _("Chinese Simplified") ],
-    ["IBM850", _("Western") ],
-    ["IBM852", _("Central European") ],
-    ["IBM855", _("Cyrillic") ],
-    ["IBM857", _("Turkish") ],
-    ["IBM862", _("Hebrew") ],
-    ["IBM864", _("Arabic") ],
-    ["ISO-2022-JP", _("Japanese") ],
-    ["ISO-2022-KR", _("Korean") ],
-    ["ISO-IR-111", _("Cyrillic") ],
-    #    ["JOHAB", _("Korean") ],
-    ["KOI8-R", _("Cyrillic") ],
-    ["KOI8-U", _("Cyrillic/Ukrainian") ],
-    ["MAC_ARABIC", _("Arabic") ],
-    ["MAC_CE", _("Central European") ],
-    ["MAC_CROATIAN", _("Croatian") ],
-    ["MAC-CYRILLIC", _("Cyrillic") ],
-    ["MAC_DEVANAGARI", _("Hindi") ],
-    ["MAC_FARSI", _("Persian") ],
-    ["MAC_GREEK", _("Greek") ],
-    ["MAC_GUJARATI", _("Gujarati") ],
-    ["MAC_GURMUKHI", _("Gurmukhi") ],
-    ["MAC_HEBREW", _("Hebrew") ],
-    ["MAC_ICELANDIC", _("Icelandic") ],
-    ["MAC_ROMAN", _("Western") ],
-    ["MAC_ROMANIAN", _("Romanian") ],
-    ["MAC_TURKISH", _("Turkish") ],
-    ["MAC_UKRAINIAN", _("Cyrillic/Ukrainian") ],
-    ["SHIFT-JIS", _("Japanese") ],
-    ["TCVN", _("Vietnamese") ],
-    ["TIS-620", _("Thai") ],
-    ["UHC", _("Korean") ],
-    ["VISCII", _("Vietnamese") ],
-    ["WINDOWS-1250", _("Central European") ],
-    ["WINDOWS-1251", _("Cyrillic") ],
-    ["WINDOWS-1252", _("Western") ],
-    ["WINDOWS-1253", _("Greek") ],
-    ["WINDOWS-1254", _("Turkish") ],
-    ["WINDOWS-1255", _("Hebrew") ],
-    ["WINDOWS-1256", _("Arabic") ],
-    ["WINDOWS-1257", _("Baltic") ],
-    ["WINDOWS-1258", _("Vietnamese") ]
+    ["ISO-8859-3", _("South European")],
+    ["ISO-8859-4", _("Baltic")],
+    ["ISO-8859-5", _("Cyrillic")],
+    ["ISO-8859-6", _("Arabic")],
+    ["ISO-8859-7", _("Greek")],
+    ["ISO-8859-8", _("Hebrew Visual")],
+    ["ISO-8859-8-I", _("Hebrew")],
+    ["ISO-8859-9", _("Turkish")],
+    ["ISO-8859-10", _("Nordic")],
+    ["ISO-8859-13", _("Baltic")],
+    ["ISO-8859-14", _("Celtic")],
+    ["ISO-8859-15", _("Western")],
+    ["ISO-8859-16", _("Romanian")],
+    #    ["UTF-7", _("Unicode")],
+    ["UTF-8", _("Unicode")],
+    #    ["UTF-16", _("Unicode")],
+    #    ["UCS-2", _("Unicode")],
+    #    ["UCS-4", _("Unicode")],
+    ["ARMSCII-8", _("Armenian")],
+    ["BIG5", _("Chinese Traditional")],
+    ["BIG5-HKSCS", _("Chinese Traditional")],
+    ["CP866", _("Cyrillic/Russian")],
+    ["EUC-JP", _("Japanese")],
+    ["EUC-KR", _("Korean")],
+    ["EUC-TW", _("Chinese Traditional")],
+    ["GB18030", _("Chinese Simplified")],
+    ["GB2312", _("Chinese Simplified")],
+    ["GBK", _("Chinese Simplified")],
+    ["GEORGIAN-PS", _("Georgian")],
+    ["HZ", _("Chinese Simplified")],
+    ["IBM850", _("Western")],
+    ["IBM852", _("Central European")],
+    ["IBM855", _("Cyrillic")],
+    ["IBM857", _("Turkish")],
+    ["IBM862", _("Hebrew")],
+    ["IBM864", _("Arabic")],
+    ["ISO-2022-JP", _("Japanese")],
+    ["ISO-2022-KR", _("Korean")],
+    ["EUC-TW", _("Chinese Traditional")],
+    ["GB18030", _("Chinese Simplified")],
+    ["GB2312", _("Chinese Simplified")],
+    ["GBK", _("Chinese Simplified")],
+    ["GEORGIAN-PS", _("Georgian")],
+    ["HZ", _("Chinese Simplified")],
+    ["IBM850", _("Western")],
+    ["IBM852", _("Central European")],
+    ["IBM855", _("Cyrillic")],
+    ["IBM857", _("Turkish")],
+    ["IBM862", _("Hebrew")],
+    ["IBM864", _("Arabic")],
+    ["ISO-2022-JP", _("Japanese")],
+    ["ISO-2022-KR", _("Korean")],
+    ["ISO-IR-111", _("Cyrillic")],
+    #    ["JOHAB", _("Korean")],
+    ["KOI8-R", _("Cyrillic")],
+    ["KOI8-U", _("Cyrillic/Ukrainian")],
+    ["MAC_ARABIC", _("Arabic")],
+    ["MAC_CE", _("Central European")],
+    ["MAC_CROATIAN", _("Croatian")],
+    ["MAC-CYRILLIC", _("Cyrillic")],
+    ["MAC_DEVANAGARI", _("Hindi")],
+    ["MAC_FARSI", _("Persian")],
+    ["MAC_GREEK", _("Greek")],
+    ["MAC_GUJARATI", _("Gujarati")],
+    ["MAC_GURMUKHI", _("Gurmukhi")],
+    ["MAC_HEBREW", _("Hebrew")],
+    ["MAC_ICELANDIC", _("Icelandic")],
+    ["MAC_ROMAN", _("Western")],
+    ["MAC_ROMANIAN", _("Romanian")],
+    ["MAC_TURKISH", _("Turkish")],
+    ["MAC_UKRAINIAN", _("Cyrillic/Ukrainian")],
+    ["SHIFT-JIS", _("Japanese")],
+    ["TCVN", _("Vietnamese")],
+    ["TIS-620", _("Thai")],
+    ["UHC", _("Korean")],
+    ["VISCII", _("Vietnamese")],
+    ["WINDOWS-1250", _("Central European")],
+    ["WINDOWS-1251", _("Cyrillic")],
+    ["WINDOWS-1252", _("Western")],
+    ["WINDOWS-1253", _("Greek")],
+    ["WINDOWS-1254", _("Turkish")],
+    ["WINDOWS-1255", _("Hebrew")],
+    ["WINDOWS-1256", _("Arabic")],
+    ["WINDOWS-1257", _("Baltic")],
+    ["WINDOWS-1258", _("Vietnamese")]
     ]
 
 
@@ -135,8 +139,9 @@
         method = None
         for stackitem in stack:
             parent_frame = stackitem[0]
-            names, varargs, keywords, local_vars = inspect.getargvalues(parent_frame)
-            ## little trick to get the second stackline method, in case we do 
+            names, varargs, keywords, local_vars = \
+                        inspect.getargvalues(parent_frame)
+            ## little trick to get the second stackline method, in case we do
             ## not find self
             if not method and method != None:
                 method = stackitem[3]
@@ -163,37 +168,88 @@
         if DEBUGMETHODS != [] and method not in DEBUGMETHODS:
             return
         try:
-            print >> sys.stderr, "%s::%s: %s%s" % (classname, method, log, extra)
+            print >> sys.stderr, "%s::%s: %s%s" % (classname, method,
+                                                    log, extra)
         except IOError:
             pass
-    
-
-#TODO: Move this to controller.py
-def get_user_shell():
-    """Get the user's shell defined in /etc/passwd ."""
-    data = None
+
+
+def shell_lookup():
+    """Find an appropriate shell for the user
+    Function copied from the terminator project source code
+    www.launchpad.net/terminator"""
     try:
-        # Read out the data in /etc/passwd
-        with open('/etc/passwd') as f:
-            data = f.readlines()
-    except e:
-        print "Something unexpected happened!"
-        raise e
-
-    for i in data:
-        tmp = i.split(":")
-        # Check for the entry of the currently logged in user
-        if tmp[0] == getpass.getuser(): 
-            # Columns are separated by colons, so split each column.
-            # Sample /etc/passwd entry for a user:
-            # 
-            #  jorge:x:1001:1002:,,,:/home/jorge:/bin/bash
-            #
-            # The last column is relevant for us.
-            # Don't forget to strip the newline at the end of the string!
-            return i.split(":")[-1:][0].strip('\n')
-
+        usershell = pwd.getpwuid(os.getuid())[6]
+    except KeyError:
+        usershell = None
+    shells = [os.getenv('SHELL'), usershell, 'bash',
+            'zsh', 'tcsh', 'ksh', 'csh', 'sh']
+
+    for shell in shells:
+        if shell is None:
+            continue
+        elif os.path.isfile(shell):
+            dbg('Found shell %s' % (shell))
+            return shell
+        else:
+            rshell = path_lookup(shell)
+            if rshell is not None:
+                dbg('Found shell %s at %s' % (shell, rshell))
+                return rshell
+    dbg('Unable to locate a shell')
+
+
+## replace ? with {0[n]}
+def replace(cmnd, num, ran):
+    while ran < num:
+        replace_cmnd = re.sub('\?', '{0[' + str(ran) + ']}', cmnd, count=1)
+        cmnd = replace_cmnd
+        ran += 1
+    return cmnd
+
+
+def get_system_font(callback=None):
+    """Look up the system font"""
+    global gconf_cli
+    if 'gconf' not in globals():
+        return 'Monospace 10'
+    else:
+        if not gconf_cli:
+            gconf_cli = gconf.client_get_default()
+        value = gconf_cli.get(
+                    '/desktop/gnome/interface/monospace_font_name')
+        system_font = value.get_string()
+        if callback:
+            gconf_cli.notify_add(
+                        '/desktop/gnome/interface/monospace_font_name',
+                        callback)
+        return system_font
+
+
+## WARNING: the altgr key is detected as a normal key
+def get_keycomb(event):
+    keyname = gtk.gdk.keyval_name(event.keyval)
+    if event.state & gtk.gdk.CONTROL_MASK:
+        keyname = 'ctrl+' + keyname
+    if event.state & gtk.gdk.MOD1_MASK:
+        keyname = 'alt+' + keyname
+    if event.state & gtk.gdk.SHIFT_MASK:
+        keyname = 'shift+' + keyname
+    return keyname
+
+
+## WARNING: the altgr key is detected as shift
+def only_modifier(event):
+    key = gtk.gdk.keyval_name(event.keyval)
+    return 'shift' in key.lower() \
+        or 'control' in key.lower() \
+        or 'super' in key.lower() \
+        or 'alt' in key.lower()
+
+
+### Singleton implementation (kind of)
 class Borg:
     __shared_state = {}
+
     def __init__(self):
         self.__dict__ = self.__shared_state

=== modified file 'clicompanionlib/view.py'
--- clicompanionlib/view.py	2012-01-08 01:05:24 +0000
+++ clicompanionlib/view.py	2012-01-08 01:05:24 +0000
@@ -1,9 +1,9 @@
 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
 #
-# clicompanion.py - commandline tool.
+# view.py - Main window for the clicompanon
 #
-# Copyright 2010 Duane Hinnen, Kenny Meyer
+# Copyright 2010 Duane Hinnen, Kenny Meyer, David Caro
 #
 # This program is free software: you can redistribute it and/or modify it
 # under the terms of the GNU General Public License version 3, as published
@@ -17,45 +17,47 @@
 # You should have received a copy of the GNU General Public License along
 # with this program.  If not, see <http://www.gnu.org/licenses/>.
 #
-
-
+# This file is where the main window is defined, depends on all the modules of
+# the clicompanion libraries.
+#
+# Also holds the class that implements the commands notebook (the upper
+# notebook), where all the tab plugins will be added (like LocalCommandList and
+# CommandLineFU)
+
+
+import os
 import pygtk
 pygtk.require('2.0')
-import os
+import gobject
+import webbrowser
 
 # import vte and gtk or print error
 try:
     import gtk
 except:
-    error = gtk.MessageDialog (None, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, gtk.BUTTONS_OK,
-        _("You need to install the python gtk bindings package 'python-gtk2'"))
-    error.run()
-    sys.exit (1)
-    
+    ## do not use gtk, just print
+    print _("You need to install the python gtk bindings package"
+            "'python-gtk2'")
+    sys.exit(1)
+
 try:
     import vte
 except:
-    error = gtk.MessageDialog (None, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, gtk.BUTTONS_OK,
+    error = gtk.MessageDialog(None, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR,
+        gtk.BUTTONS_OK,
         _("You need to install 'python-vte' the python bindings for libvte."))
     error.run()
-    sys.exit (1)
-    
-import clicompanionlib.menus_buttons
-import clicompanionlib.controller
-from clicompanionlib.utils import get_user_shell , Borg, dbg
-import clicompanionlib.tabs
-import clicompanionlib.utils as utils
+    sys.exit(1)
+
+import clicompanionlib.menus_buttons as cc_menus_buttons
+import clicompanionlib.tabs as cc_tabs
+import clicompanionlib.utils as cc_utils
+from clicompanionlib.utils import dbg
 import clicompanionlib.config as cc_config
-
-
-## Changed two->three columns
-CMNDS = cc_config.Cheatsheet() 
-## will hold the commands. Actually the first three columns
-## note that this commands list will not change with searchers and filters, 
-## instead, when adding a command to the liststore, we will add also the index 
-## of the command in the CMND list
-
-ROW = '0' ## holds the currently selected row
+import clicompanionlib.helpers as cc_helpers
+import clicompanionlib.plugins as cc_plugins
+import clicompanionlib.preferences as cc_pref
+
 TARGETS = [
     ('MY_TREE_MODEL_ROW', gtk.TARGET_SAME_WIDGET, 0),
     ('text/plain', 0, 1),
@@ -63,250 +65,180 @@
     ('STRING', 0, 3),
     ]
 
-FILTER = 0
-NETBOOKMODE = 0
-HIDEUI = 0
-FULLSCREEN = 0
-
-menu_search_hbox = ''
-button_box = ''
-
-
-class MainWindow(Borg):
-    window = gtk.Window(gtk.WINDOW_TOPLEVEL) 
-    #color = gtk.gdk.Color(60000, 65533, 60000)
-    #window.modify_bg(gtk.STATE_NORMAL, color)
-    liststore = gtk.ListStore(str, str, str, int)	
-    treeview = gtk.TreeView()
-    expander = gtk.Expander()
-    scrolledwindow = gtk.ScrolledWindow()
-    notebook = gtk.Notebook()
-
-    screen = gtk.gdk.display_get_default().get_default_screen()
-    screen_size = screen.get_monitor_geometry(0)
-    height =  screen.get_height() ## screen height ##
-    global NETBOOKMODE
-    if height < 750:
-		NETBOOKMODE = 1
-
-
-    def sync_cmnds(self, rld=False):
-        global CMNDS
-        dbg('syncing commands')
-        if rld:
-            ## reload the commands list from the file
-            CMNDS.load()
-        self.liststore.clear()
-        ## Store also the index of the command in the CMNDS list
-        i = 0
-        for cmd, ui, desc in CMNDS:
-            self.liststore.append((cmd, ui, desc, i))
-            i = i +1
-
-    
-    #liststore in a scrolled window in an expander
-    def expanded_cb(self, expander, params, window, search_box):
-        if expander.get_expanded():
-
-            # Activate the search box when expanded
-            search_box.set_sensitive(True)
-        else:
-            # De-activate the search box when not expanded
-            self.search_box.set_sensitive(False)
-            expander.set_expanded(False)
-            #expander.remove(expander.child)
-            ##reset the size of the window to its original one
-            window.resize(1, 1)
-        return  
-        
-
-    # close the window and quit
-    def delete_event(self, widget,  data=None):
-        gtk.main_quit()
-        return False
-        
-    def key_clicked(self, widget, event):
-        actions = clicompanionlib.controller.Actions()
-        global HIDEUI
-        global FULLSCREEN
-        global menu_search_hbox
-        global button_box
-        keyname = gtk.gdk.keyval_name(event.keyval).upper()
-        if keyname == "F12":
-            HIDEUI = 1 - HIDEUI
-        if HIDEUI == 1:
-            self.treeview.hide_all()
-            self.expander.hide_all()
-            self.scrolledwindow.hide_all()
-            menu_search_hbox.hide_all()
-            button_box.hide_all()
-        else:
-            self.treeview.show_all()
-            self.expander.show_all()
-            self.scrolledwindow.show_all()
-            menu_search_hbox.show_all()
-            button_box.show_all()
-        if keyname == "F11":
-            FULLSCREEN = 1 - FULLSCREEN
-        if FULLSCREEN == 1:
-            pwin = button_box.get_window()
-            pwin.fullscreen()
-        else: 
-            pwin = button_box.get_window()
-            pwin.unfullscreen()
-        if keyname == "F4":
-			actions.run_command(self)
-        if keyname == "F5":
-			actions.add_command(self)
-        if keyname == "F6":
-			actions.remove_command(self)
-        if keyname == "F7":
-			self.tabs.add_tab(self)
-  
-    def __init__(self):
-        #import pdb  ##debug
-        #pdb.set_trace() ##debug
-        
-        ##For now TERM is hardcoded to xterm because of a change
-        ##in libvte in Ubuntu Maverick
-        os.putenv('TERM', 'xterm')
-
-
-        ## style to reduce padding around tabs
-        ## TODO: Find a better place for this? 
-    	gtk.rc_parse_string ("style \"tab-close-button-style\"\n"
-		     "{\n"
-		       "GtkWidget::focus-padding = 0\n"
-		       "GtkWidget::focus-line-width = 0\n"
-		       "xthickness = 0\n"
-		       "ythickness = 0\n"
-		     "}\n"
-		     "widget \"*.tab-close-button\" style \"tab-close-button-style\"");
-        
-        ## Create UI widgets
-
-        self.notebook.set_show_tabs(0)
-
-        ##attach the style to the widget
-        self.notebook.set_name ("tab-close-button")
-
-        ## set sizes and borders
-        global NETBOOKMODE
-        if NETBOOKMODE == 1:
-            self.scrolledwindow.set_size_request(700, 200)
-            self.window.set_default_size(700, 500)
-        else:
-            self.scrolledwindow.set_size_request(700, 220)
-            self.window.set_default_size(700, 625)
-        self.window.set_border_width(10)
-        ## Sets the position of the window relative to the screen
-        self.window.set_position(gtk.WIN_POS_CENTER_ALWAYS)
-        ## Allow user to resize window
-        self.window.set_resizable(True)
-        
-        ## set Window title and icon
-        self.window.set_title("CLI Companion")
-        icon = gtk.gdk.pixbuf_new_from_file("/usr/share/pixmaps/clicompanion.16.png")
-        self.window.set_icon(icon)
-	   
-        # sync liststore with commands
-        self.sync_cmnds()
-        
-        ## set renderer and colors
-        #color2 = gtk.gdk.Color(5000,5000,65000)
-        renderer = gtk.CellRendererText()
-        #renderer.set_property("cell-background-gdk", color)
-        #renderer.set_property("foreground-gdk", color2)
-        
-        ## create the TreeViewColumns to display the data
-        self.treeview.columns = [None]*3
-        self.treeview.columns[0] = gtk.TreeViewColumn(_('Command'), renderer)
-        self.treeview.columns[1] = gtk.TreeViewColumn(_('User Input'), renderer)
-        self.treeview.columns[2] = gtk.TreeViewColumn(_('Description'), renderer)     
-        
-        for n in range(3):
-            ## add columns to treeview
-            self.treeview.append_column(self.treeview.columns[n])
-            ## create a CellRenderers to render the data
-            self.treeview.columns[n].cell = gtk.CellRendererText()
-            #self.treeview.columns[n].cell.set_property("cell-background-gdk", color)
-            #self.treeview.columns[n].cell.set_property("foreground-gdk", color2)
-            ## add the cells to the columns
-            self.treeview.columns[n].pack_start(self.treeview.columns[n].cell,
-                                                True)
-            ## set the cell attributes to the appropriate liststore column
-            self.treeview.columns[n].set_attributes(
-                    self.treeview.columns[n].cell, text=n)   
-            self.treeview.columns[n].set_resizable(True)  
-        
-        ''' set treeview model and put treeview in the scrolled window
-        and the scrolled window in the expander. '''
-        self.treeview.set_model(self.liststore)
-        self.treeview.set_reorderable(True)
-        self.scrolledwindow.add(self.treeview)
-        
-        self.expander.add(self.scrolledwindow)
-        #self.window.show_all()
-
-        ## instantiate tabs
-        self.tabs = clicompanionlib.tabs.Tabs()
-        ## instantiate controller.Actions, where all the button actions are
-        self.actions = clicompanionlib.controller.Actions()
+
+class CommandsNotebook(gtk.Notebook):
+    '''
+    This is the notebook where the commands list and the commandlinefu commands
+    are displayed
+    '''
+    ### We need a way to tell the main window that a command must be runned on
+    ## the selected terminal tab
+    __gsignals__ = {
+         'run_command': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
+                             (str, str, str)),
+         'add_tab': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
+                             ()),
+         'preferences': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
+                             ()),
+         'show_man': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
+                             (str, )),
+         'quit': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
+                             ()),
+         }
+
+    def __init__(self, config, pluginloader):
+        self.config = config
+        self.pluginloader = pluginloader
+        gtk.Notebook.__init__(self)
+        self.loaded_plugins = {}
+        self.draw_all()
+
+    def draw_all(self):
+        ## Load the needed LocalCommandList plugin
+        if 'LocalCommandList' not in self.pluginloader.plugins.keys():
+            print _("ERROR: LocalCommandList plugin is needed for the "
+                    "execution of CLI Companion, please retore the plugin or "
+                    "reinstall.")
+            self.emit('quit')
+        else:
+            self.commandstab = self.pluginloader.plugins['LocalCommandList'](
+                    self.config.get_plugin_conf('LocalCommandList'))
+        for pname, pclass in self.pluginloader.get_plugins('CommandTab'):
+            if pname == 'LocalCommandList':
+                tab = self.commandstab
+            else:
+                tab = pclass(self.config.get_plugin_conf('LocalCommandList'))
+            self.append_tab(tab, pname)
+
+    def append_tab(self, tab, pname):
+        ## save the tab related to the plugin name
+        self.loaded_plugins[pname] = tab
+        self.append_page(tab, gtk.Label(tab.__title__ or pname))
+        ##All the available signals for the plugins
+        tab.connect('run_command',
+                lambda wg, *args: self.run_command(*args))
+        tab.connect('add_command',
+                lambda wg, *args: self.commandstab.add_command(*args))
+        tab.connect('remove_command',
+                lambda wg, *args: self.remove_command(*args))
+        tab.connect('edit_command',
+                lambda wg, *args: self.edit_command(*args))
+        tab.connect('add_tab',
+                lambda wg, *args: self.emit('add_tab', *args))
+        tab.connect('preferences',
+                lambda wg, *args: self.emit('preferences', *args))
+        tab.connect('show_man',
+                lambda wg, *args: self.emit('show_man', *args))
+        tab.connect('quit',
+                lambda wg, *args: self.emit('quit', *args))
+
+    def filter(self, filter_str, pagenum=None):
+        if pagenum == None:
+            pagenum = self.get_current_page()
+        page = self.get_nth_page(pagenum)
+        dbg('filtering by %s' % filter_str)
+        page.filter(filter_str)
+
+    def set_netbook(netbookmode=False):
+        if netbookmode:
+            self.set_size_request(700, 200)
+        else:
+            self.set_size_request(700, 220)
+
+    def get_command(self):
+        pagenum = self.get_current_page()
+        page = self.get_nth_page(pagenum)
+        return page.get_command()
+
+    def run_command(self, cmd=None, ui=None, desc=None):
+        if cmd == None:
+            cmd, ui, desc = self.get_command()
+        if cmd:
+            dbg('running command %s' % cmd)
+            self.emit('run_command', cmd, ui, desc)
+
+    def add_command(self):
+        if self.get_current_page() == 0:
+            self.commandstab.add_command()
+        else:
+            command = self.get_command()
+            if command[0] != None:
+                self.commandstab.add_command(*command)
+            else:
+                self.commandstab.add_command()
+
+    def remove_command(self):
+        if self.get_current_page() == 0:
+            self.commandstab.remove_command()
+
+    def edit_command(self):
+        if self.get_current_page() == 0:
+            self.commandstab.edit_command()
+        else:
+            command = self.get_command()
+            if command[0] != None:
+                self.commandstab.add_command(*command)
+
+    def update(self, config=None, force=None):
+        if config:
+            self.config = config
+        newplugins = self.pluginloader.get_plugins('CommandTab')
+        for plugin in self.loaded_plugins.keys():
+            if plugin not in [ name for name, cl in newplugins]:
+                dbg('Disabling plugin %s' % plugin)
+                self.remove_page(self.page_num(self.loaded_plugins[plugin]))
+                self.loaded_plugins.pop(plugin)
+        for pname, pclass in newplugins:
+            if pname not in self.loaded_plugins.keys():
+                dbg('Adding new selected plugin %s' % pname)
+                self.append_tab(
+                    pclass(self.config.get_plugin_conf('LocalCommandList')),
+                    pname)
+        for plugin in force:
+            if plugin in self.loaded_plugins:
+                dbg('Reloading plugin %s' % plugin)
+                self.loaded_plugins[plugin].reload(
+                        self.config.get_plugin_conf(plugin))
+        self.show_all()
+
+
+class MainWindow(gtk.Window):
+    def __init__(self, config):
+        gtk.Window.__init__(self, gtk.WINDOW_TOPLEVEL)
+        ###############
+        ### Some state variables
+        self.hiddenui = False
+        self.maximized = False
+        self.filtered = False
+        self.fullscr = False
+
+        self.config = config
+
+        self.load_plugins()
+
+        ## two sections, the menus and search box and the expander with the
+        ## commands notebook and in the botom, the terminals notebook
+
+        ## set various parameters on the main window (size, etc)
+        self.init_config()
+        self.term_notebook = cc_tabs.TerminalsNotebook(self.config)
+
+        ###########################
+        #### Here we create the commands notebook for the expander
+        self.cmd_notebook = CommandsNotebook(config, self.pluginloader)
+
+        ## Create the menus and the searchbox
+        ## hbox for menu and search Entry
+        self.menu_search_hbox = gtk.HBox(False)
+
+        #### The menus
         ## instantiate 'File' and 'Help' Drop Down Menu [menus_buttons.py]
-        bar = clicompanionlib.menus_buttons.FileMenu()
-        menu_bar = bar.the_menu(self)
-        
-
-        ## get row of a selection
-        def mark_selected(self, treeselection):
-            global ROW
-            (model, pathlist) = treeselection.get_selected_rows()
-            ROW = pathlist
-            
-            
-        ## double click to run a command    
-        def treeview_clicked(widget, path, column):
-            self.actions.run_command(self)
-
-
-        ## press enter to run a command                   
-        def treeview_button(widget, event):
-            keyname = gtk.gdk.keyval_name(event.keyval).upper()
-            dbg('Key %s pressed'%keyname)
-            if event.type == gtk.gdk.KEY_PRESS:
-                if keyname == 'RETURN':
-                    self.actions.run_command(self)
-                    
-                    
-
-        selection = self.treeview.get_selection()
-        #selection.set_mode(gtk.SELECTION_SINGLE)
-        ## open with top command selected
-        selection.select_path(0) 
-        selection.connect("changed", mark_selected, selection)
-        ## double-click
-        self.treeview.connect("row-activated", treeview_clicked)
-        #press enter to run command
-        self.treeview.connect("key-press-event", treeview_button)
-                
-        global menu_search_hbox
-        
-        ## The search section
-        search_label = gtk.Label(_("Search:"))
-        search_label.set_alignment(xalign=-1, yalign=0)
-        self.search_box = gtk.Entry()
-        self.search_box.connect("changed", self.actions._filter_commands, self.liststore, self.treeview)
-        ## search box tooltip
-        self.search_box.set_tooltip_text(_("Search your list of commands"))
-        ## Set the search box sensitive OFF at program start, because
-        ## expander is not unfolded by default
-        self.search_box.set_sensitive(False)
-        ## hbox for menu and search Entry
-        menu_search_hbox = gtk.HBox(False)
-        menu_search_hbox.pack_end(self.search_box, True)
-        menu_search_hbox.pack_end(search_label, False, False, 10)
-        menu_search_hbox.pack_start(menu_bar, True)
-
+        menu_bar = cc_menus_buttons.FileMenu(self.config)
+        self.menu_search_hbox.pack_start(menu_bar, True)
+
+        #### the expander
+        self.expander = gtk.Expander()
+        self.menu_search_hbox.pack_end(self.expander, False, False, 0)
         ## expander title
         expander_hbox = gtk.HBox()
         image = gtk.Image()
@@ -314,70 +246,286 @@
         label = gtk.Label(_('Command List'))
         ## tooltip for the label of the expander
         expander_hbox.set_tooltip_text(_("Click to show/hide command list"))
-        
         ## add expander widget to hbox
         expander_hbox.pack_start(image, False, False)
         expander_hbox.pack_start(label, True, False)
         self.expander.set_label_widget(expander_hbox)
+        self.expander.set_expanded(True)
+
+        #### The search box
+        self.search_hbox = gtk.HBox()
+        #### the search label
+        search_label = gtk.Label(_("Search:"))
+        search_label.set_alignment(xalign=-1, yalign=0)
+        self.search_box = gtk.Entry()
+        self.search_box.connect("changed",
+                lambda wg, *x: self.cmd_notebook.filter(wg.get_text()))
+        ## search box tooltip
+        self.search_box.set_tooltip_text(_("Search your list of commands"))
+        self.search_hbox.pack_start(search_label, False, False, 8)
+        self.search_hbox.pack_end(self.search_box, True)
+        self.menu_search_hbox.pack_end(self.search_hbox, True)
+
+        ############################
+        ## and now the terminals notebook
+        self.term_notebook.set_show_tabs(0)
+        ##attach the style to the widget
+        self.term_notebook.set_name("tab-close-button")
 
         ## Add the first tab with the Terminal
-        self.tabs.add_tab(self.notebook)
-        self.notebook.set_tab_pos(2)
+        self.term_notebook.add_tab()
 
-        ## The "Add Tab" tab
-        add_tab_button = gtk.Button("+")
-        ## tooltip for "Add Tab" tab
-        add_tab_button.set_tooltip_text(_("Click to add another tab"))
-        ## create first tab
-        self.notebook.append_page(gtk.Label(""), add_tab_button)
-        
-        global button_box
         ## buttons at bottom of main window [menus_buttons.py]
-        button_box = bar.buttons(self, 10, gtk.BUTTONBOX_END)
+        self.button_box = cc_menus_buttons.Buttons(10, gtk.BUTTONBOX_END)
 
+        ## pack everything
         ## vbox for search, notebook, buttonbar
-        vbox = gtk.VBox()
-        self.window.add(vbox)
         ## pack everytyhing in the vbox
-        #self.vbox.pack_start(menu_bar, False, False,  0) ##menuBar
-        vbox.pack_start(menu_search_hbox, False, False, 5)
-        vbox.pack_start(self.expander, False, False, 5)
-        vbox.pack_start(self.notebook, True, True, 5)
-        vbox.pack_start(button_box, False, False, 5)
-        
+        h_vbox = gtk.VBox()
+        h_vbox.pack_start(self.menu_search_hbox, False, False, 5)
+        h_vbox.pack_start(self.cmd_notebook, True, True, 5)
+        self.l_vbox = gtk.VBox()
+        self.l_vbox.pack_start(self.term_notebook, True, True, 0)
+        self.l_vbox.pack_start(self.button_box, False, False, 0)
+
+        ## Pack it in a vpant bo allow the user to resize
+        self.vpane = gtk.VPaned()
+        self.vpane.pack1(h_vbox, True, False)
+        self.vpane.pack2(self.l_vbox, True, True)
+        self.add(self.vpane)
+
         ## signals
-        self.expander.connect('notify::expanded', self.expanded_cb, self.window, self.search_box)
-        self.window.connect("delete_event", self.delete_event)
-        self.window.connect("key-press-event", self.key_clicked)
-        add_tab_button.connect("clicked", lambda *x: self.tabs.add_tab(self.notebook))
+        self.cmd_notebook.connect('run_command',
+            lambda wdg, *args: self.term_notebook.run_command(*args))
+        self.cmd_notebook.connect('show_man',
+                lambda wgt, cmd: cc_helpers.ManPage(cmd).run())
+        self.cmd_notebook.connect('quit', lambda *x: gtk.main_quit())
+        self.term_notebook.connect('quit', lambda *x: gtk.main_quit())
+        self.term_notebook.connect('preferences', lambda *x: self.edit_pref())
+        self.expander.connect('notify::expanded',
+                lambda *x: self.expanded_cb())
+        self.connect("delete_event", self.delete_event)
+        self.connect("key-press-event", self.key_clicked)
+        menu_bar.connect('quit', lambda *x: gtk.main_quit())
+        menu_bar.connect('run_command',
+                lambda *x: self.cmd_notebook.run_command())
+        menu_bar.connect('add_command',
+                lambda *x: self.cmd_notebook.add_command())
+        menu_bar.connect('edit_command',
+                lambda *x: self.cmd_notebook.edit_command())
+        menu_bar.connect('remove_command',
+                lambda *x: self.cmd_notebook.remove_command())
+        menu_bar.connect('preferences', lambda *x: self.edit_pref())
+        menu_bar.connect('add_tab', lambda *x: self.term_notebook.add_tab())
+        menu_bar.connect('close_tab', lambda *x: self.term_notebook.quit_tab())
+        self.button_box.connect('quit', lambda *x: gtk.main_quit())
+        self.button_box.connect('run_command',
+                lambda *x: self.cmd_notebook.run_command())
+        self.button_box.connect('add_command',
+                lambda *x: self.cmd_notebook.add_command())
+        self.button_box.connect('edit_command',
+                lambda *x: self.cmd_notebook.edit_command())
+        self.button_box.connect('remove_command',
+                lambda *x: self.cmd_notebook.remove_command())
+        self.button_box.connect('show_man',
+                lambda *x: cc_helpers.ManPage(
+                    self.cmd_notebook.get_command()[0]).run())
+        self.button_box.connect('add_tab',
+                lambda *x: self.term_notebook.add_tab())
         ## right click menu event capture
-        self.treeview.connect("button_press_event", bar.right_click, self)
-
         # Allow enable drag and drop of rows including row move
-        self.treeview.enable_model_drag_source( gtk.gdk.BUTTON1_MASK,
-                                                TARGETS,
-                                                gtk.gdk.ACTION_DEFAULT |
-                                                gtk.gdk.ACTION_COPY)
-        self.treeview.enable_model_drag_dest(TARGETS,
-                                                gtk.gdk.ACTION_DEFAULT)
-
-        self.treeview.connect ("drag_data_get", self.drag_data_get_event)
-        self.treeview.connect ("drag_data_received", self.drag_data_received_event)
-        self.treeview.connect("drag_drop", self.on_drag_drop )
-
-
-        #self.vte.grab_focus()
-        self.window.show_all()
-        return
-
+#        self.treeview.enable_model_drag_source( gtk.gdk.BUTTON1_MASK,
+#                                                TARGETS,
+#                                                gtk.gdk.ACTION_DEFAULT |
+#                                                gtk.gdk.ACTION_COPY)
+#        self.treeview.enable_model_drag_dest(TARGETS,
+#                                                gtk.gdk.ACTION_DEFAULT)
+#
+#        self.treeview.connect ("drag_data_get", self.drag_data_get_event)
+#        self.treeview.connect ("drag_data_received",
+#                self.drag_data_received_event)
+#        self.treeview.connect("drag_drop", self.on_drag_drop )
+
+        ## show everything
+        self.show_all()
+        ## set the focus on the terminal
+        self.term_notebook.focus()
+        return
+
+    def load_plugins(self):
+        self.pluginloader = cc_plugins.PluginLoader()
+        self.reload_plugins()
+
+    def reload_plugins(self):
+        (head, _tail) = os.path.split(cc_plugins.__file__)
+        pluginspath = os.path.join(head.rsplit(os.sep, 1)[0], 'plugins')
+        allowed = [plg.strip() for plg in self.config.get('general::default',
+                                                        'plugins').split(',')]
+        dbg('Allowed plugins: %s' % allowed.__repr__())
+        self.pluginloader.load(pluginspath, allowed)
+
+    def expanded_cb(self):
+        #liststore in a scrolled window in an expander
+        if self.expander.get_expanded():
+            self.search_hbox.show_all()
+            self.cmd_notebook.show_all()
+            self.cmd_notebook.set_current_page(self.currentpage)
+            self.button_box.show_all()
+            self.vpane.set_position(self.currentpos)
+        else:
+            self.currentpage = self.cmd_notebook.get_current_page()
+            self.currentpos = self.vpane.get_position()
+            self.cmd_notebook.hide_all()
+            self.search_hbox.hide_all()
+            self.button_box.hide_all()
+            self.vpane.set_position(0)
+        return
+
+    def init_config(self):
+        ## Set the netbookmode if needed
+        screen = gtk.gdk.display_get_default().get_default_screen()
+        height = screen.get_height()
+        if height < 750:
+            self.cmd_notebook.set_netbook(True)
+            self.set_default_size(700, 500)
+        else:
+            self.set_default_size(700, 625)
+        self.set_border_width(5)
+        ## Sets the position of the window relative to the screen
+        self.set_position(gtk.WIN_POS_CENTER_ALWAYS)
+        ## Allow user to resize window
+        self.set_resizable(True)
+
+        ## set Window title and icon
+        self.set_title("CLI Companion")
+        icon = gtk.gdk.pixbuf_new_from_file(
+                "/usr/share/pixmaps/clicompanion.16.png")
+        self.set_icon(icon)
+
+        ##For now TERM is hardcoded to xterm because of a change
+        ##in libvte in Ubuntu Maverick
+        os.putenv('TERM', 'xterm')
+
+        ## style to reduce padding around tabs
+        ## TODO: Find a better place for this?
+        gtk.rc_parse_string("style \"tab-close-button-style\"\n"
+             "{\n"
+               "GtkWidget::focus-padding = 0\n"
+               "GtkWidget::focus-line-width = 0\n"
+               "xthickness = 0\n"
+               "ythickness = 0\n"
+             "}\n"
+             "widget \"*.tab-close-button\" style \"tab-close-button-style\"")
+
+    def edit_pref(self):
+        pref_win = cc_pref.PreferencesWindow(
+            self.config.get_config_copy(), self.pluginloader)
+        newconfig, changed_plugins = pref_win.run()
+        if newconfig:
+            self.config = newconfig
+            self.config.save()
+            dbg(', '.join([self.config.get('profile::default', opt)
+                    for opt in self.config.options('profile::default')]))
+        self.reload_plugins()
+        self.term_notebook.update_all_term_config(self.config)
+        self.cmd_notebook.update(self.config, changed_plugins)
+
+    def delete_event(self, widget,  data=None):
+        # close the window and quit
+        gtk.main_quit()
+        return False
+
+    def key_pressed(self, event):
+        keyname = cc_utils.get_keycomb(event)
+        if keyname in cc_pref.KEY_BINDINGS.keys():
+            key, func = cc_pref.KEY_BINDINGS[keyname]
+
+    def key_clicked(self, widget, event):
+        if cc_utils.only_modifier(event):
+            return
+        keycomb = cc_utils.get_keycomb(event)
+        for func in cc_config.KEY_BINDINGS.keys():
+            if keycomb == self.config.get('keybindings', func):
+                getattr(self, func)()
+                ## this is to stop sending the keypress to the widgets
+                return True
+
+    def run_command(self):
+        self.cmd_notebook.run_command()
+
+    def add_command(self):
+        self.cmd_notebook.add_command()
+
+    def remove_command(self):
+        self.cmd_notebook.remove_command()
+
+    def edit_command(self):
+        self.cmd_notebook.edit_command()
+
+    def add_tab(self):
+        self.term_notebook.add_tab()
+
+    def close_tab(self):
+        self.term_notebook.quit_tab()
+
+    def toggle_hide_ui(self):
+        if self.hiddenui:
+            self.show_ui()
+        else:
+            self.hide_ui()
+
+    def hide_ui(self):
+        if self.hiddenui:
+            return
+        dbg('Hide UI')
+        self.l_vbox.remove(self.term_notebook)
+        self.remove(self.vpane)
+        self.add(self.term_notebook)
+        self.hiddenui = True
+
+    def show_ui(self):
+        if not self.hiddenui:
+            return
+        dbg('Show UI')
+        self.remove(self.term_notebook)
+        btns = self.l_vbox.get_children()[0]
+        self.l_vbox.remove(btns)
+        self.l_vbox.pack_start(self.term_notebook, True, True, 0)
+        self.l_vbox.pack_start(btns, False, False, 0)
+        self.add(self.vpane)
+        self.hiddenui = False
+
+    def toggle_maximize(self):
+        if not self.maximized:
+            self.maximize()
+        else:
+            self.unmaximize()
+        self.maximized = not self.maximized
+
+    def toggle_fullscreen(self):
+        '''
+        Maximize and hide everything, or unmaximize and unhide if it was on
+        fullscren mode
+        '''
+        if not self.fullscr:
+            self.hide_ui()
+            self.fullscreen()
+            self.set_border_width(0)
+        else:
+            self.show_ui()
+            self.unfullscreen()
+            self.set_border_width(5)
+        self.fullscr = not self.fullscr
+
+    ### TODO: pass these functions to the LocalCommandList class
     def on_drag_drop(self, treeview, *x):
         '''
         Stop the signal when in search mode
         '''
-        if FILTER:
+        if self.FILTER:
             treeview.stop_emission('drag_drop')
 
-    def drag_data_get_event(self, treeview, context, selection, target_id, 
+    def drag_data_get_event(self, treeview, context, selection, target_id,
                             etime):
         """
         Executed on dragging
@@ -387,20 +535,19 @@
         data = model.get(iter, 0, 1, 2)
         selection.set(selection.target, 8, '\t'.join(data))
 
-    def drag_data_received_event(self, treeview, context, x, y, selection, info,
-                            etime):
+    def drag_data_received_event(self, treeview, context, x, y, selection,
+            info, etime):
         """
         Executed when dropping.
         """
         global CMNDS
-        global FILTER
         ## if we are in a search, do nothing
-        if FILTER == 1:
+        if self.FILTER == 1:
             return
         model = treeview.get_model()
         ## get the destination
         drop_info = treeview.get_dest_row_at_pos(x, y)
-        if drop_info: 
+        if drop_info:
             path, position = drop_info
             iter = model.get_iter(path)
             dest = list(model.get(iter, 0, 1, 2))
@@ -408,21 +555,23 @@
         ## parse all the incoming commands
         for data in selection.data.split('\n'):
             # if we got an empty line skip it
-            if not data.replace('\r',''): continue
+            if not data.replace('\r', ''):
+                continue
             # format the incoming string
-            orig = data.replace('\r','').split('\t',2)
-            orig = [ fld.strip() for fld in orig ]
+            orig = data.replace('\r', '').split('\t', 2)
+            orig = [fld.strip() for fld in orig]
             # fill the empty fields
-            if len(orig) < 3: orig = orig + ('',)*(3-len(orig))
-            dbg('Got drop of command %s'%'_\t_'.join(orig))
+            if len(orig) < 3:
+                orig = orig + ('', ) * (3 - len(orig))
+            dbg('Got drop of command %s' % '_\t_'.join(orig))
 
             if drop_info:
                 if (position == gtk.TREE_VIEW_DROP_BEFORE
                         or position == gtk.TREE_VIEW_DROP_INTO_OR_BEFORE):
-                    dbg('\t to before dest %s'%'_\t_'.join(dest))
+                    dbg('\t to before dest %s' % '_\t_'.join(dest))
                     CMNDS.drag_n_drop(orig, dest, before=True)
                 else:
-                    dbg('\t to after dest %s'%'_\t_'.join(dest))
+                    dbg('\t to after dest %s' % '_\t_'.join(dest))
                     CMNDS.drag_n_drop(orig, dest, before=False)
             else:
                 dbg('\t to the end')
@@ -431,19 +580,21 @@
             context.finish(True, True, etime)
         self.sync_cmnds()
         CMNDS.save()
-        
+
     def main(self):
         try:
             gtk.main()
         except KeyboardInterrupt:
             pass
-        
-def run( options=None ):
-    ##create the config file
-    config = cc_config.create_config()
-    if config.get('terminal','debug') == 'True':
-        utils.DEBUG = True
-    CMNDS.load(options and options.cheatsheet or None)
-    dbg('Loaded commands %s'%CMNDS)
-    main_window = MainWindow()
+
+
+def run(options=None):
+    if options and options.conffile:
+        config = cc_config.CLIConfig(conffile)
+    else:
+        config = cc_config.CLIConfig()
+    if config.get('general::default', 'debug') == 'True' or options.debug:
+        cc_utils.DEBUG = True
+    main_window = MainWindow(
+                config=config)
     main_window.main()

=== added directory 'plugins'
=== added file 'plugins/CommandLineFU.py'
--- plugins/CommandLineFU.py	1970-01-01 00:00:00 +0000
+++ plugins/CommandLineFU.py	2012-01-08 01:05:24 +0000
@@ -0,0 +1,282 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# CommandLineFu.py - CommandlineFU commands plugin for CLI Comapnion
+#
+# Copyright 2012 David Caro <david.caro.estevez@xxxxxxxxx>
+#
+# This program is free software: you can redistribute it and/or modify it
+# under the terms of the GNU General Public License version 3, as published
+# by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranties of
+# MERCHANTABILITY, SATISFACTORY QUALITY, 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, see <http://www.gnu.org/licenses/>.
+#
+
+
+import os
+import pygtk
+pygtk.require('2.0')
+import gobject
+import webbrowser
+
+try:
+    import gtk
+except:
+    ## do not use gtk, just print
+    print _("You need to install the python gtk bindings package"
+            "'python-gtk2'")
+    sys.exit(1)
+
+from clicompanionlib.utils import dbg
+import clfu as cc_clf
+import clicompanionlib.plugins as plugins
+
+
+class CommandLineFU(plugins.TabPlugin):
+    '''
+    Tab with all the commandlinefu commands and search options
+    '''
+    __authors__ = 'David Caro <david.caro.estevez@xxxxxxxxx>'
+    __info__ = ('This plugin crates a tab on the commands list that allows you'
+        ' to search commands on the CommandlineFU website '
+        '(www.commandlinefu.com).')
+    __title__ = 'CommandlineFU Commands'
+
+    def __init__(self, config):
+        plugins.TabPlugin.__init__(self)
+        self.config = config
+        self.clfu = cc_clf.CLFu()
+        self.pages = []
+        ## las_search will store the params of the last search
+        self.lastsearch = []
+
+        ## box for commands tags and timerange comboboxes, and refresh
+        hbox = gtk.HBox()
+        self.pack_start(hbox, False, False, 0)
+
+        ## combobox for selecting command tag
+        self.tags_box = gtk.combo_box_new_text()
+        self.tags_box.append_text('Update')
+        self.tags_box.append_text('None')
+        self.tags_box.set_active(1)
+        self.tags_box.connect('changed', lambda *x: self.populate_tags())
+        hbox.pack_start(gtk.Label(_('Tag:')), False, False, 0)
+        hbox.pack_start(self.tags_box, False, False, 0)
+
+        ## time range combobox
+        self.trange_box = gtk.combo_box_new_text()
+        ## populate time ranges, no http call needed
+        for timerange in self.clfu.get_timeranges():
+            self.trange_box.append_text(timerange)
+        self.trange_box.set_active(0)
+        hbox.pack_start(gtk.Label(_('From:')), False, False, 0)
+        hbox.pack_start(self.trange_box, False, False, 0)
+
+        ## refresh button
+        refresh_btn = gtk.Button("Search")
+        refresh_btn.connect("clicked", lambda *x: self.populate())
+        hbox.pack_start(refresh_btn, False, False, 0)
+
+        ## add the commands list
+        sw = gtk.ScrolledWindow()
+        self.liststore = gtk.ListStore(gtk.gdk.Pixbuf, gtk.gdk.Pixbuf,
+                                str, str, int, str)
+        self.treeview = gtk.TreeView(gtk.TreeModelSort(self.liststore))
+        sw.add(self.treeview)
+        #self.treeview.set_reorderable(True)
+        self.treeview.connect('row-activated', self.clicked)
+        self.treeview.connect("button_press_event", self.right_click)
+        self.init_cols()
+        self.pack_start(sw, True, True, 0)
+
+        ## Get more button
+        self.more_btn = gtk.Button("Get more!")
+        self.more_btn.connect("clicked",
+                lambda *x: self.populate(next_page=True))
+        hbox.pack_start(self.more_btn, False, False, 0)
+        self.more_btn.set_sensitive(False)
+
+        ## Show everything
+        self.show_all()
+
+    def add_pixbuf_col(self, colname, n=0):
+        col = gtk.TreeViewColumn()
+        col.set_title(colname)
+        render_pixbuf = gtk.CellRendererPixbuf()
+        col.pack_start(render_pixbuf, expand=True)
+        col.add_attribute(render_pixbuf, 'pixbuf', n)
+        col.set_resizable(True)
+        self.treeview.append_column(col)
+
+    def add_text_col(self, colname, n=0):
+        col = gtk.TreeViewColumn()
+        col.set_title(_(colname))
+        render = gtk.CellRendererText()
+        col.pack_start(render, expand=True)
+        col.add_attribute(render, 'text', n)
+        col.set_resizable(True)
+        col.set_sort_column_id(n)
+        self.treeview.append_column(col)
+
+    def init_cols(self):
+        self.add_pixbuf_col('', 0)
+        self.add_pixbuf_col('', 1)
+        self.add_text_col('Command', 2)
+        self.add_text_col('Summary', 3)
+        self.add_text_col('Votes', 4)
+
+    def populate_tags(self):
+        dbg('Poulating clf tags')
+        tag = self.tags_box.get_active_text()
+        if tag == "Update":
+            self.tags_box.set_model(gtk.ListStore(str))
+            self.tags_box.append_text('Update')
+            self.tags_box.append_text('None')
+            ## populate commands tags
+            for tag, tagid in self.clfu.get_tags():
+                self.tags_box.append_text(tag)
+            self.tags_box.set_active(1)
+
+    def populate(self, next_page=False, filter_str=''):
+        '''
+        populate the widgets with the info from the commandlinefu website
+        '''
+        ## get the filter params, the saved ones if it's a next page request or
+        ## the new ones if it's  anew search
+        self.liststore.clear()
+        if next_page:
+            tag, timerange, filter_str = self.lastsearch
+            page = len(self.pages)
+        else:
+            tag = self.tags_box.get_active_text()
+            timerange = self.trange_box.get_active_text()
+            self.lastsearch = tag, timerange, filter_str
+            page = 0
+            self.pages = []
+        ## get new commands page
+        if tag != 'None':
+            commands = self.clfu.tag(tag=tag, timerange=timerange, page=page)
+        elif filter_str != '':
+            commands = self.clfu.search(filter_str, timerange=timerange,
+                                page=page)
+        else:
+            commands = self.clfu.browse(timerange=timerange, page=page)
+        ## if there were no results, do avoid requesting more pages, show the
+        ## user that there are no more pages
+        self.more_btn.set_sensitive(True)
+        if not commands or len(commands) < 25:
+            self.more_btn.set_sensitive(False)
+        ## append it to the revious pages if any
+        self.pages.append(commands)
+        ## filter and show all the commands
+        for page in self.pages:
+            for command in page:
+                if filter_str != '':
+                    if filter_str not in command['command'] \
+                    and filter_str not in command['summary']:
+                        continue
+                add_btn = self.treeview.render_icon(stock_id=gtk.STOCK_ADD,
+                                 size=gtk.ICON_SIZE_SMALL_TOOLBAR)
+                link_btn = self.treeview.render_icon(stock_id=gtk.STOCK_INFO,
+                                 size=gtk.ICON_SIZE_SMALL_TOOLBAR)
+                self.liststore.append((add_btn, link_btn,
+                        command['command'], command['summary'],
+                        int(command['votes']), command['url']))
+
+    def filter(self, search_term):
+        """
+        Show commands matching a given search term.
+        The user should enter a term in the search box and the treeview should
+        only display the rows which contain the search term.
+        No reordering allowed when filtering.
+        """
+        ## If the search term is empty, and we change filtering state
+        ## restore the liststore, else do nothing
+        if search_term == "":
+            if self.filtering:
+                dbg("Uniltering...")
+                self.filtering = False
+                self.treeview.set_model(gtk.TreeModelSort(self.liststore))
+            return
+        dbg("Filtering...")
+        self.filtering = True
+        modelfilter = self.liststore.filter_new()
+
+        def search(model, iter, search_term):
+            cmd, desc = model.get(iter, 2, 3)
+            if search_term in ('%s %s' % (cmd, desc)).lower():
+                    return True
+        modelfilter.set_visible_func(search, search_term)
+        self.treeview.set_model(gtk.TreeModelSort(modelfilter))
+
+    def clicked(self, treeview, path, column):
+        treeselection = treeview.get_selection()
+        model, iter = treeselection.get_selected()
+        data = model.get(iter, 2, 3, 4, 5)
+        if column == treeview.get_column(0):
+            dbg('Adding command %s, %s, %s' % (data[0], '', data[1]))
+            self.emit('add_command', data[0], '', data[1])
+        elif column == treeview.get_column(1):
+            webbrowser.open(data[3])
+        else:
+            self.emit('run_command', data[0], '', data[1])
+
+    def get_command(self, withurl=False):
+        treeselection = self.treeview.get_selection()
+        model, iter = treeselection.get_selected()
+        if not iter:
+            return None, None, None
+        cmd, desc, url = model.get(iter, 2, 3, 5)
+        if not withurl:
+            return cmd, '', desc
+        return cmd, '', desc, url
+
+
+    #right-click popup menu for the Liststore(command list)
+    def right_click(self, widget, event):
+        if event.button == 3:
+            x = int(event.x)
+            y = int(event.y)
+            time = event.time
+            row = self.treeview.get_path_at_pos(x, y)
+            if row:
+                path, col, x, y = row
+            if row:
+                popupMenu = gtk.Menu()
+                self.treeview.grab_focus()
+                self.treeview.set_cursor(path, col, 0)
+                data = self.get_command(withurl=True)
+                if data[0] == None:
+                    return
+                # right-click popup menu Apply(run)
+                menuPopup1 = gtk.ImageMenuItem(gtk.STOCK_APPLY)
+                popupMenu.add(menuPopup1)
+                menuPopup1.connect("activate",
+                    lambda *x: self.emit('run_command', *data[:-1]))
+                # right-click popup menu AddToLocal
+                menuPopup2 = gtk.ImageMenuItem(gtk.STOCK_ADD)
+                popupMenu.add(menuPopup2)
+                menuPopup2.connect("activate",
+                    lambda *x: self.emit('add_command', *data[:-1]))
+                # right-click popup menu Help
+                menuPopup4 = gtk.ImageMenuItem(gtk.STOCK_HELP)
+                popupMenu.add(menuPopup4)
+                menuPopup4.connect("activate",
+                    lambda *x: self.emit('show_man', data[0]))
+                # right-click popup menu Online Help
+                menuPopup4 = gtk.ImageMenuItem(gtk.STOCK_INFO)
+                box = menuPopup4.get_children()[0]
+                box.set_label(_('Show online info'))
+                popupMenu.add(menuPopup4)
+                menuPopup4.connect("activate",
+                    lambda wg, url: webbrowser.open(url), data[3])
+                # Show popup menu
+                popupMenu.show_all()
+                popupMenu.popup(None, None, None, event.button, time)
+                return True

=== added file 'plugins/LocalCommandList.py'
--- plugins/LocalCommandList.py	1970-01-01 00:00:00 +0000
+++ plugins/LocalCommandList.py	2012-01-08 01:05:24 +0000
@@ -0,0 +1,775 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# LocalCommandList.py - Plugin for handling locally stored commands
+#
+# Copyright 2010 Duane Hinnen, Kenny Meyer, David Caro
+#                                               <david.caro.estevez@xxxxxxxxx>
+#
+# This program is free software: you can redistribute it and/or modify it
+# under the terms of the GNU General Public License version 3, as published
+# by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranties of
+# MERCHANTABILITY, SATISFACTORY QUALITY, 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, see <http://www.gnu.org/licenses/>.
+#
+
+
+import os
+import pygtk
+pygtk.require('2.0')
+import gobject
+
+try:
+    import gtk
+except:
+    ## do not use gtk, just print
+    print _("You need to install the python gtk bindings package"
+             "'python-gtk2'")
+    sys.exit(1)
+
+## we should try to absolutely detach it from the clicompanion libs someday
+from clicompanionlib.utils import dbg
+import clicompanionlib.plugins as plugins
+
+## Targets for the Drag and Drop
+TARGETS = [
+    ('MY_TREE_MODEL_ROW', gtk.TARGET_SAME_WIDGET, 0),
+    ('text/plain', 0, 1),
+    ('TEXT', 0, 2),
+    ('STRING', 0, 3),
+    ]
+
+
+class LocalCommandList(plugins.TabPlugin):
+    '''
+    Tab with the list of local stored commands, the ponly signals that should
+    get emited (right now) are the run_command and show_man. The other can be
+    processed inhouse
+    '''
+    __authors__ = ('Duane Hinnen\n'
+                   'Kenny Meyer\n'
+                   'Marcos Vanettai\n'
+                   'Marek Bardoński\n'
+                   'David Caro <david.caro.estevez@xxxxxxxxx>\n')
+    __info__ = ('This is the main plugin for the CLI Companion, the one that '
+                'handles the locally stored commands.')
+    __title__ = 'Local Commands'
+
+    def __init__(self, config):
+        self.config = config
+        plugins.TabPlugin.__init__(self)
+        self.treeview = gtk.TreeView()
+        self.filtering = False
+        ## command, user input, description, and index in cmnds array
+        self.liststore = gtk.ListStore(str, str, str, int)
+        ## Load the given commands file
+        self.cmnds = Cheatsheet(
+                self.config.get('default', 'cheatsheet'))
+        ## will hold the commands. Actually the first three columns
+        ## note that this commands list will not change with searchers nor
+        ## filters, instead, when adding a command to the liststore, we will
+        ## add also the index of the command in the cmnds array as another
+        ## field
+        self.sync_cmnds(rld=True)
+        self.treeview.set_model(DummySort(self.liststore))
+
+        ### Only show the three firs columns
+        ## create the TreeViewColumns to display the data
+        self.add_text_col('Command', 0)
+        self.add_text_col('User Input', 1)
+        self.add_text_col('Description', 2)
+        self.treeview.set_tooltip_column(2)
+        self.treeview.set_reorderable(True)
+        ## open with top command selected
+        selection = self.treeview.get_selection()
+        selection.select_path(0)
+        selection.set_mode(gtk.SELECTION_SINGLE)
+        ### double-click
+        self.treeview.connect("row-activated", self.event_clicked)
+        ##press enter to run command
+        self.treeview.connect("key-press-event",
+                lambda wg, event: self.event_key_pressed(event))
+        ## Right click event
+        self.treeview.connect("button_press_event", self.right_click)
+        # Allow enable drag and drop of rows including row move
+        self.treeview.enable_model_drag_source( gtk.gdk.BUTTON1_MASK,
+                                                TARGETS,
+                                                gtk.gdk.ACTION_DEFAULT |
+                                                gtk.gdk.ACTION_COPY)
+        self.treeview.enable_model_drag_dest(TARGETS,
+                                                gtk.gdk.ACTION_DEFAULT)
+
+        self.treeview.connect ("drag_data_get", self.drag_data_get_event)
+        self.treeview.connect ("drag_data_received",
+                self.drag_data_received_event)
+        self.treeview.connect("drag_drop", self.on_drag_drop )
+
+
+        sw = gtk.ScrolledWindow()
+        sw.add(self.treeview)
+        self.pack_start(sw)
+        self.show_all()
+
+    def add_text_col(self, colname, n=0):
+        col = gtk.TreeViewColumn()
+        col.set_title(_(colname))
+        render = gtk.CellRendererText()
+        col.pack_start(render, expand=True)
+        col.add_attribute(render, 'text', n)
+        col.set_resizable(True)
+        col.set_sort_column_id(n)
+        self.treeview.append_column(col)
+
+    def sync_cmnds(self, rld=False, filt_str=None):
+        dbg('syncing commands')
+        if rld:
+            ## reload the commands list from the file
+            self.cmnds.load()
+        self.liststore.clear()
+        ## Store also the index of the command in the CMNDS list
+        i = 0
+        for cmd, ui, desc in self.cmnds:
+            if filt_str and filt_str in cmd + ui + desc or not filt_str:
+                self.liststore.append((cmd, ui, desc, i - 1))
+            i = i + 1
+        self.cmnds.save()
+
+    def get_command(self, path=None):
+        if not path:
+            selection = self.treeview.get_selection()
+            model, iterator = selection.get_selected()
+            if not iterator:
+                return None, None, None
+            path = model.get_path(iterator)
+        lst_index = int(path[0])  # removes everything but number from [5,]
+        model = self.treeview.get_model()
+        cmd = ''.join(model[lst_index][0])
+        ui = ''.join(model[lst_index][1])
+        desc = ''.join(model[lst_index][2])
+        return cmd, ui, desc
+
+    def add_command(self, cmd='', ui='', desc=''):
+        add_cmd_win = AddCommandWindow(cmd, ui, desc)
+        new_cmd = add_cmd_win.run()
+        if not new_cmd:
+            return
+        self.cmnds.append(*new_cmd)
+        self.sync_cmnds()
+
+    def edit_command(self, cmd='', ui='', desc=''):
+        if not cmd:
+            cmd, ui, desc = self.get_command()
+        if cmd == None:
+            return
+        edit_cmd_win = EditCommandWindow(cmd, ui, desc)
+        edited_cmd = edit_cmd_win.run()
+        if not edited_cmd:
+            return
+        index = self.cmnds.index(cmd, ui, desc)
+        del self.cmnds[index]
+        self.cmnds.insert(*edited_cmd, pos=index)
+        self.sync_cmnds()
+
+    def remove_command(self, cmd=None, ui=None, desc=None):
+        if not cmd:
+            cmd, ui, desc = self.get_command()
+        if cmd == None:
+            return
+        self.cmnds.del_by_value(cmd, ui, desc)
+        self.sync_cmnds()
+
+    def event_clicked(self, widget, path, column):
+        ## double click to run a command
+        cmd, ui, desc = self.get_command(path)
+        if cmd:
+            self.emit('run_command', cmd, ui, desc)
+
+    def event_key_pressed(self, event):
+        ## press enter to run a command
+        keyname = gtk.gdk.keyval_name(event.keyval)
+        dbg('Key %s pressed' % keyname)
+        if event.type == gtk.gdk.KEY_PRESS:
+            if keyname == 'Return':
+                cmd, ui, desc = self.get_command()
+                if cmd:
+                    self.emit('run_command', cmd, ui, desc)
+            if keyname == 'Delete' or keyname == 'BackSpace':
+                self.remove_command()
+
+    def filter(self, search_term):
+        """
+        Show commands matching a given search term.
+        The user should enter a term in the search box and the treeview should
+        only display the rows which contain the search term.
+        No reordering allowed when filtering.
+        """
+        ## If the search term is empty, and we change filtering state
+        ## restore the liststore, else do nothing
+        if search_term == "":
+            if self.filtering:
+                dbg("Uniltering...")
+                self.filtering = False
+                self.treeview.set_model(DummySort(self.liststore))
+            return
+        dbg("Filtering...")
+        self.filtering = True
+        ## Create a TreeModelFilter object which provides auxiliary functions
+        ## for filtering data.
+## http://www.pygtk.org/pygtk2tutorial/sec-TreeModelSortAndTreeModelFilter.html
+        modelfilter = self.liststore.filter_new()
+
+        def search(model, iter, search_term):
+            ## Iterate through every column and row and check if the search
+            ## term is there:
+            cmd, ui, desc = model.get(iter, 0, 1, 2)
+            if search_term in ('%s %s %s' % (cmd, ui, desc)).lower():
+                    return True
+        modelfilter.set_visible_func(search, search_term)
+        self.treeview.set_model(DummySort(modelfilter))
+
+    #right-click popup menu for the Liststore(command list)
+    def right_click(self, widget, event):
+        if event.button == 3:
+            x = int(event.x)
+            y = int(event.y)
+            time = event.time
+            row = self.treeview.get_path_at_pos(x, y)
+            if row:
+                path, col, x, y = row
+            popupMenu = gtk.Menu()
+            if row:
+                self.treeview.grab_focus()
+                self.treeview.set_cursor(path, col, 0)
+                command = self.get_command(path)
+                if command[0] == None:
+                    return
+                # right-click popup menu Apply(run)
+                menuPopup1 = gtk.ImageMenuItem(gtk.STOCK_APPLY)
+                popupMenu.add(menuPopup1)
+                menuPopup1.connect("activate",
+                    lambda *x: self.emit('run_command', *command))
+                # right-click popup menu Edit
+                menuPopup2 = gtk.ImageMenuItem(gtk.STOCK_EDIT)
+                popupMenu.add(menuPopup2)
+                menuPopup2.connect("activate",
+                    lambda *x: self.edit_command(*command))
+                # right-click popup menu Delete
+                menuPopup3 = gtk.ImageMenuItem(gtk.STOCK_DELETE)
+                popupMenu.add(menuPopup3)
+                menuPopup3.connect("activate",
+                    lambda *x: self.remove_command(*command))
+                # right-click popup menu Help
+                menuPopup4 = gtk.ImageMenuItem(gtk.STOCK_HELP)
+                popupMenu.add(menuPopup4)
+                menuPopup4.connect("activate",
+                    lambda wg, cmd: self.emit('show_man', cmd), command[0])
+            # right-click popup menu Help
+            menuPopup4 = gtk.ImageMenuItem(gtk.STOCK_ADD)
+            popupMenu.add(menuPopup4)
+            menuPopup4.connect("activate",
+                lambda *x: self.add_command())
+            # right-click popup menu Load file
+            menuPopup4 = gtk.ImageMenuItem(gtk.STOCK_OPEN)
+            box = menuPopup4.get_children()[0]
+            box.set_label(_('Load another cheatsheet'))
+            popupMenu.add(menuPopup4)
+            menuPopup4.connect("activate",
+                lambda *x: self.load_file())
+            # Show popup menu
+            popupMenu.show_all()
+            popupMenu.popup(None, None, None, event.button, time)
+            return True
+
+    def load_file(self):
+        chooser = CHFileSelector()
+        resp = chooser.run()
+        if resp:
+            self.config.set('default', 'cheatsheet', resp)
+            self.reload()
+
+    def reload(self, config=None):
+        if config:
+            self.config = config
+        self.cmnds = Cheatsheet(
+                self.config.get('default', 'cheatsheet'))
+        self.sync_cmnds()
+        self.config.save()
+
+    def on_drag_drop(self, treeview, context, x, y, time):
+        '''
+        Stop the signal when in search mode
+        '''
+        if self.filtering:
+            treeview.stop_emission('drag_drop')
+
+    def drag_data_get_event(self, treeview, context, selection, target_id,
+                            etime):
+        """
+        Executed on dragging
+        """
+        treeselection = treeview.get_selection()
+        model, iter = treeselection.get_selected()
+        data = model.get(iter, 0, 1, 2)
+        selection.set(selection.target, 8, '\t'.join(data))
+
+    def drag_data_received_event(self, treeview, context, x, y, selection,
+                                info, time):
+        """
+        Executed when dropping.
+        """
+        ## if we are in a search, do nothing
+        if self.filtering:
+            return
+        model = treeview.get_model()
+        ## get the destination
+        drop_info = treeview.get_dest_row_at_pos(x, y)
+        if drop_info:
+            path, position = drop_info
+            iter = model.get_iter(path)
+            dest = list(model.get(iter, 0, 1, 2))
+
+        ## parse all the incoming commands
+        for data in selection.data.split('\n'):
+            # if we got an empty line skip it
+            if not data.replace('\r', ''):
+                continue
+            # format the incoming string
+            orig = data.replace('\r', '').split('\t', 2)
+            orig = [fld.strip() for fld in orig]
+            # fill the empty fields
+            if len(orig) < 3:
+                orig = orig + ('', ) * (3 - len(orig))
+            dbg('Got drop of command %s' % '_\t_'.join(orig))
+
+            if drop_info:
+                if (position == gtk.TREE_VIEW_DROP_BEFORE
+                        or position == gtk.TREE_VIEW_DROP_INTO_OR_BEFORE):
+                    dbg('\t to before dest %s' % '_\t_'.join(dest))
+                    self.cmnds.drag_n_drop(orig, dest, before=True)
+                else:
+                    dbg('\t to after dest %s' % '_\t_'.join(dest))
+                    self.cmnds.drag_n_drop(orig, dest, before=False)
+            else:
+                dbg('\t to the end')
+                self.cmnds[len(cmnds)] = orig
+        if context.action == gtk.gdk.ACTION_MOVE:
+            context.finish(True, True, etime)
+        self.sync_cmnds()
+
+    def main(self):
+        try:
+            gtk.main()
+        except KeyboardInterrupt:
+            pass
+
+
+## The name of the config widget must be the same, but ended with 'Config'
+class LocalCommandListConfig(plugins.PluginConfig):
+    '''
+    Config tab for the plugin, just select the cheatsheet file.
+    '''
+    def __init__(self, config):
+        plugins.PluginConfig.__init__(self, config)
+        self.draw_all()
+
+    def draw_all(self):
+        hbox = gtk.HBox()
+        hbox.pack_start(gtk.Label('Cheatsheet file:'))
+        self.file_btn = gtk.Button(
+                            self.config.get('default', 'cheatsheet'))
+        self.file_btn.connect('clicked', self.select_file)
+        hbox.pack_end(self.file_btn, False, False, 8)
+        self.pack_start(hbox, False, False, 8)
+
+    def select_file(self, btn):
+        chooser = CHFileSelector()
+        resp = chooser.run()
+        if resp:
+            self.config.set('default', 'cheatsheet', newfile)
+            ## notify that the plugin must be reloaded
+            self.emit('reload')
+        self.update()
+
+    def update(self):
+        for child in self.children():
+            self.remove(child)
+        self.draw_all()
+        self.show_all()
+
+
+class CHFileSelector(gtk.FileChooserDialog):
+    '''
+    Popup for selecting the cheatsheet file
+    '''
+    def __init__(self, current_file='~'):
+        gtk.FileChooserDialog.__init__(self,
+            'Select cheatsheet file',
+            None,
+            gtk.FILE_CHOOSER_ACTION_OPEN,
+            (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
+            gtk.STOCK_OPEN, gtk.RESPONSE_OK))
+        filter = gtk.FileFilter()
+        filter.set_name("All files")
+        filter.add_pattern("*")
+        self.add_filter(filter)
+        filter = gtk.FileFilter()
+        filter.set_name("Cheatsheets")
+        filter.add_pattern("*cheatsheet")
+        filter.add_pattern("*clicompanion*")
+        filter.add_pattern("*.cs")
+        self.add_filter(filter)
+        self.set_uri(current_file)
+
+    def run(self):
+        newfile = None
+        resp = gtk.FileChooserDialog.run(self)
+        if resp == gtk.RESPONSE_OK:
+            newfile = self.get_filename()
+        self.destroy()
+        return newfile
+
+
+class DummySort(gtk.TreeModelSort, gtk.TreeDragDest):
+    '''
+    This class is needed to implement the drag and drop in a TreeModelSort
+    '''
+    __gtype_name__ = 'DummySort'
+
+    def __init__(self, *x):
+        gtk.TreeModelSort.__init__(self, *x)
+
+
+class Cheatsheet:
+    '''
+    Container class for the cheatsheet of commands
+
+    Example of usage:
+    >>> c = config.Cheatsheet()
+    >>> c.load('/home/myuser/.clicompanion2')
+    >>> c[3]
+    ['uname -a', '', 'What kernel am I running\n']
+    >>> c.file
+    '/home/cascara/.clicompanion2'
+    >>> c[2]=[ 'mycmd', 'userui', 'desc' ]
+    >>> c[2]
+    ['mycmd', 'userui', 'desc']
+    >>> del c[2]
+    >>> c[2]
+    ['ps aux | grep ?', 'search string',
+        'Search active processes for search string\n']
+    >>> c.insert('cmd2','ui2','desc2',2)
+    >>> c[2]
+    ['cmd2', 'ui2', 'desc2']
+
+    '''
+    def __init__(self, cheatsheet):
+        self.cheatsheet = cheatsheet
+        self.commands = []
+        self.load()
+
+    def __repr__(self):
+        return 'Config: %s - %s' % (self.cheatsheet, self.commands)
+
+    def load(self):
+        if not os.path.exists(self.cheatsheet):
+            if os.path.exists(CONFIG_ORIG):
+                os.system("cp %s %s" % (CONFIG_ORIG, self.cheatsheet))
+            else:
+                # Oops! Looks like there's no default cheatsheet.
+                # Then, create an empty cheatsheet.
+                open(self.ceatsheet, 'w').close()
+        try:
+            dbg('Reading cheatsheet from file %s' % self.cheatsheet)
+            with open(self.cheatsheet, 'r') as ch_fd:
+                ## try to detect if the line is a old fashines config line
+                ## (separated by ':'), when saved will rewrite it
+                no_tabs = True
+                some_colon = False
+                for line in ch_fd:
+                    line = line.strip()
+                    if not line:
+                        continue
+                    cmd, ui, desc = [l.strip()
+                                    for l in line.split('\t', 2)] + ['', ] * \
+                                        (3 - len(line.split('\t', 2)))
+                    if ':' in cmd:
+                        some_colon = True
+                    if ui or desc:
+                        no_tabs = False
+                    if cmd and [cmd, ui, desc] not in self.commands:
+                        self.commands.append([cmd, ui, desc])
+                        dbg('Adding command %s' % [cmd, ui, desc])
+                if no_tabs and some_colon:
+                    ## None of the commands had tabs, and all had ':' in the
+                    ## cmd... most probably old config style
+                    print _("Detected old cheatsheet style at") \
+                            + " %s" % self.cheatsheet \
+                            + _(", parsing to new one.")
+                    for i in range(len(self.commands)):
+                        cmd, ui, desc = self.commands[i]
+                        cmd, ui, desc = [l.strip()
+                                        for l in cmd.split(':', 2)] + ['', ] \
+                                            * (3 - len(cmd.split(':', 2)))
+                        self.commands[i] = [cmd, ui, desc]
+                    self.save()
+        except IOError, e:
+            print _("Error while loading cheatfile") + \
+                    " %s: %s" % (self.cheatsheet, e)
+
+    def save(self, cheatfile=None):
+        '''
+        Saves the current config to the file cheatfile, or the file that was
+        loaded.
+        NOTE: It does not overwrite the value self.cheatsheet, that points to
+        the file that was loaded
+        '''
+        if not cheatfile and self.cheatsheet:
+            cheatfile = self.cheatsheet
+        elif not cheatfile:
+            return False
+        try:
+            with open(cheatfile, 'wb') as ch_fd:
+                for command in self.commands:
+                    ch_fd.write('\t'.join(command) + '\n')
+        except IOError, e:
+            print _("Error writing cheatfile") + " %s: %s" % (cheatfile, e)
+            return False
+        return True
+
+    def __len__(self):
+        return len(self.commands)
+
+    def __getitem__(self, key):
+        return self.commands[key]
+
+    def __setitem__(self, key, value):
+        if not isinstance(value, collections.Iterable) or len(value) < 3:
+            raise ValueError('Value must be a container with three items, '
+                            'but got %s' % value)
+        if key < len(self.commands):
+            self.commands[key] = list(value)
+        else:
+            try:
+                self.insert(*value, pos=key)
+            except ValueError, e:
+                raise ValueError('Value must be a container with three items, '
+                                'but got %s' % value)
+
+    def __iter__(self):
+        for command in self.commands:
+            yield command
+
+    def insert(self, cmd, ui, desc, pos=None):
+        if not [cmd, ui, desc] in self.commands:
+            if not pos:
+                self.commands.append([cmd, ui, desc])
+            else:
+                self.commands.insert(pos, [cmd, ui, desc])
+
+    def append(self, cmd, ui, desc):
+        self.insert(cmd, ui, desc)
+
+    def index(self, cmd, ui, desc):
+        return self.commands.index([cmd, ui, desc])
+
+    def __delitem__(self, key):
+        del self.commands[key]
+
+    def pop(self, key):
+        return self.commands.pop(key)
+
+    def del_by_value(self, cmd, ui, desc):
+        if [cmd, ui, desc] in self.commands:
+            return self.commands.pop(self.commands.index([cmd, ui, desc]))
+
+    def drag_n_drop(self, cmd1, cmd2, before=True):
+        if cmd1 in self.commands:
+            dbg('Dropping command from inside %s' % '_\t_'.join(cmd1))
+            i1 = self.commands.index(cmd1)
+            del self.commands[i1]
+            if cmd2:
+                i2 = self.commands.index(cmd2)
+                if before:
+                    self.commands.insert(i2, cmd1)
+                else:
+                    self.commands.insert(i2 + 1, cmd1)
+            else:
+                self.commands.append(cmd1)
+        else:
+            dbg('Dropping command from outside %s' % '_\t_'.join(cmd1))
+            if cmd2:
+                i2 = self.commands.index(cmd2)
+                if before:
+                    self.commands.insert(i2, cmd1)
+                else:
+                    self.commands.insert(i2 + 1, cmd1)
+            else:
+                self.commands.append(cmd1)
+
+
+## Some hlper popus like edit command and so
+class CommandInfoWindow(gtk.MessageDialog):
+    def __init__(self, cmd, ui, desc):
+        self.cmd, self.ui, self.desc = cmd, ui, desc
+        gtk.MessageDialog.__init__(self,
+            None,
+            gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
+            gtk.MESSAGE_QUESTION,
+            gtk.BUTTONS_OK_CANCEL,
+            None)
+        self.set_markup(_("This command requires more information."))
+        ## create the text input field
+        self.entry = gtk.Entry()
+        ## allow the user to press enter to do ok
+        self.entry.connect("activate", lambda *x: self.response(
+                                                    gtk.RESPONSE_OK))
+        ## create a horizontal box to pack the entry and a label
+        hbox = gtk.HBox()
+        hbox.pack_start(gtk.Label(self.ui + ":"), False, 5, 5)
+        hbox.pack_end(self.entry)
+        ## some secondary text
+        self.format_secondary_markup(_("Please provide a " + self.ui))
+        ## add it and show it
+        self.vbox.pack_end(hbox, True, True, 0)
+        self.show_all()
+        ## The destroy method must be called otherwise the 'Close' button will
+        ## not work.
+
+    def run(self):
+        result = False
+        while not result:
+            result = gtk.MessageDialog.run(self)
+            if result == gtk.RESPONSE_OK:
+                ui = self.entry.get_text().strip()
+                dbg('Got ui "%s"' % ui)
+                if not ui:
+                    self.show_error()
+                    result = None
+                try:
+                    cmd = self.cmd.format(ui.split(' '))
+                except:
+                    result = None
+            else:
+                cmd = None
+        self.destroy()
+        return cmd
+
+    def show_error(self):
+        error = gtk.MessageDialog(None, gtk.DIALOG_MODAL, \
+            gtk.MESSAGE_ERROR, gtk.BUTTONS_OK,
+            _("You need to enter full input. Space separated."))
+        error.connect('response', lambda *x: error.destroy())
+        error.run()
+
+## Add command dialog box
+class AddCommandWindow(gtk.MessageDialog):
+    def __init__(self, cmd='', ui='', desc=''):
+        ## Create Dialog object
+        gtk.MessageDialog.__init__(self,
+            None,
+            gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
+            gtk.MESSAGE_QUESTION,
+            gtk.BUTTONS_OK,
+            None)
+
+        ## primaary text
+        self.set_markup(_("Add a command to your command list"))
+
+        #create the text input field
+        self.cmd_txt = gtk.Entry()
+        self.cmd_txt.set_text(cmd)
+        self.ui_txt = gtk.Entry()
+        self.ui_txt.set_text(ui)
+        self.desc_txt = gtk.Entry()
+        self.desc_txt.set_text(desc)
+        ## allow the user to press enter to do ok
+        self.cmd_txt.connect("activate",
+                lambda *x: self.response(gtk.RESPONSE_OK))
+        self.ui_txt.connect("activate",
+                lambda *x: self.response(gtk.RESPONSE_OK))
+        self.desc_txt.connect("activate",
+                lambda *x: self.response(gtk.RESPONSE_OK))
+
+        ## create three labels
+        hbox1 = gtk.HBox()
+        hbox1.pack_start(gtk.Label(_("Command")), False, 5, 5)
+        hbox1.pack_start(self.cmd_txt, False, 5, 5)
+
+        hbox1.pack_start(gtk.Label(_("User Input")), False, 5, 5)
+        hbox1.pack_start(self.ui_txt, False, 5, 5)
+
+        hbox2 = gtk.HBox()
+        hbox2.pack_start(gtk.Label(_("Description")), False, 5, 5)
+        hbox2.pack_start(self.desc_txt, True, 5, 5)
+
+        ## cancel button
+        self.add_button(_('Cancel'), gtk.RESPONSE_DELETE_EVENT)
+        ## some secondary text
+        self.format_secondary_markup(
+            _("When entering a command use question marks(?) as placeholders "
+          "if user input is required when the command runs. Example: ls "
+          "/any/directory would be entered as, ls ? .For each question "
+          "mark(?) in your command, if any, use the User Input field to "
+          "provide a hint for each variable. Using our example ls ? you "
+          "could put directory as the User Input. Lastly provide a brief "
+          "Description."))
+
+        ## add it and show it
+        self.vbox.pack_end(hbox2, True, True, 0)
+        self.vbox.pack_end(hbox1, True, True, 0)
+        self.show_all()
+
+    def run(self):
+        result = None
+        command = None
+        while not result:
+            ## Show the dialog
+            result = gtk.MessageDialog.run(self)
+            if result == gtk.RESPONSE_OK:
+                ## user text assigned to a variable
+                cmd = self.cmd_txt.get_text()
+                ui = self.ui_txt.get_text()
+                desc = self.desc_txt.get_text()
+                if cmd:
+                    command = cmd, ui, desc
+                else:
+                    self.show_nocmd_error()
+                    result = None
+        self.destroy()
+        return command
+
+    def show_nocmd_error(self):
+        error = gtk.MessageDialog(None, gtk.DIALOG_MODAL, \
+            gtk.MESSAGE_ERROR, gtk.BUTTONS_OK,
+            _("You need to enter at least a command."))
+        error.connect('response', lambda err, *x: err.destroy())
+        error.run()
+
+class EditCommandWindow(AddCommandWindow):
+    """
+    Reuse the add window changing the name
+    """
+    def __init__(self, cmd, ui, desc):
+        if not cmd:
+            choose_row_error()
+            return
+        AddCommandWindow.__init__(self, cmd, ui, desc)
+        self.set_markup(_("Edit a command in your command list"))
+
+def choose_row_error():
+    dialog = gtk.MessageDialog(
+            None,
+            gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
+            gtk.MESSAGE_QUESTION,
+            gtk.BUTTONS_OK,
+            None)
+    dialog.set_markup(_('You must choose a row to view the help'))
+    dialog.show_all()
+    dialog.run()
+    dialog.destroy()

=== added file 'plugins/clfu.py'
--- plugins/clfu.py	1970-01-01 00:00:00 +0000
+++ plugins/clfu.py	2012-01-08 01:05:24 +0000
@@ -0,0 +1,243 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# clfu.py - CommandlineFU API librearies for python
+#
+# Copyright 2012 David Caro <david.caro.estevez@xxxxxxxxx>
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+#
+# Inspired by the commandlinefu module by Olivier Hervieu
+# <olivier.hervieu@xxxxxxxxx>, https://github.com/ohe/commandlinefu
+
+from base64 import encodestring
+from json import loads
+from httplib import HTTPConnection
+from urllib import urlencode
+from re import search
+
+
+CLFU_API_HOST = 'www.commandlinefu.com'
+CLFU_API_URL = '/commands'
+
+CLFU_API_SORTS = {'date': '',
+                   'votes': '/sort-by-votes'}
+
+CLFU_API_RANGES = {'anytime': '',
+                    'last day': '/last-day',
+                    'last week': '/last-week',
+                    'last month': '/last-month'}
+
+## Not sure hot to use tags, needs the tad-id, but there's no easy way to get
+## it, so not implemented
+CLFU_API_COMMANDS = {'browse': '/browse',
+                      'search': '/matching',
+                      'tag': '/tagged',
+                      'using': '/using'}
+
+CLFU_API_FORMATS = {'json': '/json',
+                     'plain': '/plaintext',
+                     'rss': '/rss'}
+
+EMPTY = {'json': [],
+         'plain': '',
+         'rss': ''}
+
+DEBUG = False
+
+
+def dbg(string):
+    if DEBUG:
+        print string
+
+
+class CLFu:
+    def __init__(self):
+        self.conn = HTTPConnection(CLFU_API_HOST)
+        self.tags = {}
+        self.page = 0
+
+    def _send_request(self, command, string='', sort='date',
+                    form='json', timerange='anytime', page=0):
+
+        import socket
+        if string != '' and command == 'search':
+            string = '/%s/%s' % (string, encodestring(string)[:-1])
+        elif command == 'tag':
+            tagid = self.get_tag_id(string)
+            if not tagid:
+                raise Exception('Tag %s not found.' % string)
+            string = '/%s/%s' % (tagid, string)
+
+        if command == 'get_tags':
+            request = '/commands/browse'
+        else:
+            request = CLFU_API_URL \
+                 + CLFU_API_COMMANDS[command] \
+                 + string \
+                 + CLFU_API_SORTS[sort] \
+                 + CLFU_API_RANGES[timerange] \
+                 + CLFU_API_FORMATS[form] \
+                 + '/%s' % (page * 25)
+
+        response = False
+        ## try to connect 3 times, closing and opening again if failed
+        for i in range(3):
+            try:
+                if not response:
+                    self.conn.close()
+                    self.conn.connect()
+                dbg('Sending request number %d to: ' % i
+                    + CLFU_API_HOST + request)
+                self.conn.request('GET', request)
+                response = self.conn.getresponse().read()
+                return response
+            except socket.gaierror, e:
+                dbg('Connection failed: %s\nRetrying...' % e)
+        return response or EMPTY[form]
+
+    def _send(self, command, string='', sort='date',
+              form='json', timerange='anytime', page=0):
+        response = self._send_request(command, string, sort, form,
+                                    timerange, page)
+        if response:
+            if form == 'json':
+                try:
+                    response = loads(response)
+                except ValueError, e:
+                    raise ValueError(
+                        "Error parsing response from Commandlinefu web: "
+                        "%s\nResponse:%s" % (e, response))
+                except TypeError, e:
+                    raise TypeError(
+                        "Error parsing response from Commandlinefu"
+                        "web: %s\nResponse:%s" % (e, response))
+        return response
+
+    def _send_iter(self, command, sort='date', string='',
+                   form='json', timerange='anytime', page=0):
+        page = 0
+        response = 'do not match first time'
+        oldresponse = ''
+        ## whe check if the response is the same twice, if it is, we stop, to
+        ## avoid infinite loops
+        while not self._endpage(response, form) and oldresponse != response:
+            response = self._send_request(command, string, sort,
+                                          form, timerange, page)
+            oldresponse == response
+            if not response:
+                break
+            if form == 'json' and response:
+                try:
+                    for cmd in loads(response):
+                        yield cmd
+                except ValueError, e:
+                    raise ValueError(
+                        "Error parsing response from Commandlinefu web: "
+                        "%s\nResponse:%s" % (e, response))
+                except TypeError, e:
+                    raise TypeError(
+                        "Error parsing response from Commandlinefu web: "
+                        "%s\nResponse:%s" % (e, response))
+            elif not self._endpage(response, form) and response != oldresponse:
+                yield response
+            page = page + 1
+
+    def _endpage(self, page, form):
+        if page == 'do not match first time':
+            return False
+        if form == 'json':
+            return [] == loads(page)
+        elif form == 'rss':
+            endpage = True
+            channel = False
+            for line in page.split('\n'):
+                if line.strip().startswith('<item'):
+                    endpage = False
+                    break
+            return endpage
+        elif form == 'plain':
+            ## Note: This may change... use the oldresponse failsafe also
+            return page == '# commandlinefu.com by David Winterbottom\n\n'
+
+    def browse(self, sort='date', form='json', timerange='anytime', page=0):
+        return self._send('browse', sort=sort, form=form,
+                          timerange=timerange, page=page)
+
+    def browse_all(self, sort='date', form='json',
+                    timerange='anytime', page=0):
+        for response in self._send_iter('browse', sort=sort, form=form,
+                                        timerange=timerange, page=page):
+            yield response
+
+    def search(self, string, sort='date', form='json',
+                timerange='anytime', page=0):
+        return self._send('search', string, sort, form, timerange, page)
+
+    def search_all(self, string, sort='date', form='json',
+                    timerange='anytime', page=0):
+        for response in self._send_iter('search', string, sort,
+                                        form, timerange, page):
+            yield response
+
+    def using(self, string, sort='date', form='json',
+                timerange='anytime', page=0):
+        return self._send('using', string, sort, form, timerange, page)
+
+    def using_all(self, string, sort='date', form='json',
+                    timerange='anytime', page=0):
+        for response in self._send_iter('using', string, sort, form,
+                                        timerange, page):
+            yield response
+
+    def tag(self, tag, sort='date', form='json', timerange='anytime', page=0):
+        return self._send('tag', tag, sort, form, timerange, page)
+
+    def tag_iter(self, tag, sort='date', form='json',
+                    timerange='anytime', page=0):
+        for response in self._send_iter('tag', tag, sort, form,
+                                        timerange, page):
+            yield response
+
+    def get_tags(self):
+        response = self._send('get_tags', form='plain')
+        tags_cloud = False
+        self.tags = {}
+        for line in response.split('\n'):
+            if search('<div id="cloud"', line):
+                tags_cloud = True
+            if tags_cloud and line.strip():
+                match = search('/(?P<tagid>\d+)/(?P<tagname>[^"]+)', line)
+                if match:
+                    newtag = match.groupdict()
+                    self.tags[newtag['tagname']] = newtag['tagid']
+                elif search('</div', line):
+                    tags_cloud = False
+        return self.tags.items()
+
+    def get_tag_id(self, tag):
+        if not self.tags:
+            dbg('No tags loaded, trying to load them from the web.')
+            self.get_tags()
+        if tag in self.tags:
+            return self.tags[tag]
+        else:
+            return None
+
+    def get_timeranges(self):
+        return sorted(CLFU_API_RANGES.keys())


Follow ups