#!/usr/bin/python

"""
Starter.py: a program to help accomplish some X startup tasks in a
mostly-environment-independent way.

Operational outline:

Initialize the user's ~/.config/starter
Load ~/.config/starter/starter.xml
Scan for 'module' tags
For each, load a python module according to some naming rule, looking
  in ~/.config/starter/modules, then the core modules directory
  (./modules)
Initialize the module by calling mod.init(...) with the 'module' tag
  subtree
Look at the mode from the command line to decide what to do (whether
  to start the GUI or run the modules by calling mod.run())
"""

import elementtree.ElementTree as ET
import sys
import os
import imp

def joined(func):
    def _joined(*path):
        path = os.path.join(*path)
        return func(path)
    return _joined

@joined
def tryMkdir(dir):
    try:
        os.makedirs(dir)
    except OSError, e:
        if e.errno != 17:
            raise

@joined
def touchFile(path):
    try:
        open(path, 'r')
    except Exception, e:
        open(path, 'w').close()

@joined
def installXML(path):
    xml = "<?xml version=\"1.0\"?><settings></settings>"

    if not os.path.exists(path):
        f = open(path, 'wt')
        f.write(xml)
        f.close()

def initHomedir():
    tryMkdir(starterPath, 'modules')
    touchFile(starterPath, 'modules', '__init__.py')
    installXML(starterPath, 'starter.xml')

def loadXML():
    # Parse the XML.  Should always exist since initHomedir will
    # create a default XML config file if it doesn't exist.
    f = open(os.path.join(starterPath, 'starter.xml'))
    xml = f.read()
    f.close()

    tree = ET.fromstring(xml)

    return dict([(m.attrib['name'], m)
                 for m in tree.findall('module')
                 if 'name' in m.attrib])

def checkModule(mod):
    attrs = ['gui', 'save', 'init', '__title__', '__description__']

    for a in attrs:
        try:
            getattr(mod, a)
        except AttributeError:
            raise Exception('Module "%s" lacks "%s" attribute' % (mod.__file__, a,))

def loadModules(paths):
    result = {}
    
    for fspath in paths:
        for filename in os.listdir(fspath):
            if filename.endswith('.py') and filename != '__init__.py' \
               and not filename.startswith('.'):
                try:
                    modname = filename[:-3]
                    if modname in result:
                        continue
                    m = imp.load_source(modname, os.path.join(fspath, filename))
                    checkModule(m)
                    result[modname] = m
                except Exception, e:
                    print "Tried %s (%s)" % (fspath + '/' + filename, str(e))

    return result

def guiMain(modules):
    import pygtk
    pygtk.require('2.0')
    import gtk

    class MainWindow:
        def delete(self, widget, event=None):
            gtk.main_quit()
            return False

        def __init__(self, modules):
            window = gtk.Window(gtk.WINDOW_TOPLEVEL)
            window.connect("delete_event", self.delete)
            window.set_title("Starter")
            window.set_default_size(400, 450)

            vbox = gtk.VBox(False, 10)
            vbox.set_border_width(10)
            window.add(vbox)

            notebook = gtk.Notebook()
            vbox.pack_start(notebook, True, True)

            for name, m in modules.iteritems():
                mod_gui = m.gui()
                tabLabel = gtk.Label(m.__title__)

                if mod_gui is not None:
                    contents = mod_gui
                else:
                    label = gtk.Label(m.__description__)
                    contents = label

                box = gtk.VBox()
                box.set_border_width(10)
                box.pack_start(contents, True, True)

                notebook.append_page(box, tabLabel)

            # Button box for "Close" button.
            bbox = gtk.HButtonBox()
            bbox.set_layout(gtk.BUTTONBOX_END)
            close_button = gtk.Button(stock=gtk.STOCK_CLOSE)
            bbox.add(close_button)

            close_button.connect('clicked', self.delete)

            vbox.pack_start(bbox, False, False)

            window.show_all()

    MainWindow(modules)
    gtk.main()

if __name__ == "__main__":
    def usage(modules):
        print "Usage: %s [run]" % (sys.argv[0],)
        print "  If 'run' is specified, run the modules configured in\n" \
              "  ~/.config/starter/.  Otherwise, start the configuration GUI to\n" \
              "  configure available modules."
        print
        print "Available modules:"
        for m in modules:
            print " ", m.__name__, "-", m.__description__

    try:
        starterPath = os.path.join(os.environ['HOME'], '.config', 'starter')
    except KeyError:
        print "For some reason, HOME isn't in your environment.  Bailing."
        sys.exit(1)

    initHomedir()
    modules = loadXML()

    pymods = loadModules([os.path.join(starterPath, 'modules'),
                          os.path.join(os.path.dirname(__file__), 'modules')])

    if "--help" in sys.argv:
        usage(pymods.values())
        sys.exit(0)

    for modname, mod in pymods.iteritems():
        data = modules.get(modname, None)

        try:
            mod.init(data)
            mod.__shortname__ = modname
        except Exception, e:
            print 'Error loading module "%s": %s' % (mod.__file__, e)

    if 'run' in sys.argv:
        for m in pymods:
            pymods[m].run()
        sys.exit(0)

    guiMain(pymods)

    for m in pymods:
        pymods[m].guiClose()

    root = ET.Element('settings')

    for modname, mod in pymods.iteritems():
        try:
            module_element = ET.Element('module', name=mod.__shortname__)
            mod.save(module_element)
            root.append(module_element)
        except Exception, e:
            if modname in modules:
                # Write the old settings; don't let a module save
                # failure blow away settings.
                root.append(modules[modname])

    # Should copy starter.xml to starter.xml.backup here in case the
    # output gets botched.  Or, better yet, write output to
    # starter.xml.temp and only overwrite starter.xml if the write
    # succeeds (no exceptions raised).
    ET.ElementTree(root).write(os.path.join(starterPath, 'starter.xml'))
