[seed] Add gjs compatibility layer to seed



commit caab25e4c8ae5a463eb256e3012c7e4f4d7be7bc
Author: Peter Rustler <peter rustler basyskom com>
Date:   Thu Dec 17 14:33:31 2015 +0000

    Add gjs compatibility layer to seed
    
    This Adds the following:
     * Add all js modules and extensions from gjs to the
       seed codebase.
     * Add a compile time flag to build with and without support.
     * Add a runtime commandline to switch compatibility mode on and off.
     * Loads the gjs modules first if commandline switch is given and do
       not load seed modules if a gjs module is present.
    
    Note:
    Not all gjs modules are working because of missing native modules in seed.
    There are: window, system, cairo and console.
    They need to be ported.
    The gjs modules are expected to start working when they are ported.
    
    This patch will not change current behaviour of seed when
    --seed-gjs-compatibility commandline is not given or when gjs support
    is not build in.
    Current seed code should still work.

 configure.ac               |   17 +
 extensions/Makefile.am     |    4 +
 extensions/gjs/GLib.js     |  285 +++++++++++++
 extensions/gjs/GObject.js  |  408 ++++++++++++++++++
 extensions/gjs/Gio.js      |  394 ++++++++++++++++++
 extensions/gjs/Gtk.js      |  113 +++++
 extensions/gjs/Makefile.am |    4 +
 libseed/Makefile.am        |    1 +
 libseed/seed-engine.c      |   10 +
 libseed/seed-importer.c    |   97 +++--
 libseed/seed-private.h     |    4 +
 modules/Makefile.am        |    4 +
 modules/gjs/Makefile.am    |    8 +
 modules/gjs/cairo.js       |  147 +++++++
 modules/gjs/coverage.js    |  978 ++++++++++++++++++++++++++++++++++++++++++++
 modules/gjs/format.js      |   86 ++++
 modules/gjs/gettext.js     |   99 +++++
 modules/gjs/jsUnit.js      |  473 +++++++++++++++++++++
 modules/gjs/lang.js        |  488 ++++++++++++++++++++++
 modules/gjs/mainloop.js    |   87 ++++
 modules/gjs/package.js     |  247 +++++++++++
 modules/gjs/signals.js     |  146 +++++++
 22 files changed, 4064 insertions(+), 36 deletions(-)
---
diff --git a/configure.ac b/configure.ac
index 3d5d482..7985583 100644
--- a/configure.ac
+++ b/configure.ac
@@ -446,6 +446,19 @@ AC_SUBST(SEED_DEBUG_CFLAGS)
 
 AC_CHECK_HEADERS(pty.h)
 
+dnl =============================Gjs-Compatibility=============================
+AC_ARG_ENABLE(gjs-compatibility,
+                         AC_HELP_STRING([--enable-gjs-compatibility],
+                                                        [enable gjs compatibility. [default=no]]),
+                         [enable_gjscompat="yes"],[enable_gjscompat="no"])
+
+if test "x$enable_gjscompat" = "xyes"; then
+       SEED_GJSCOMPAT_CFLAGS="-DSEED_ENABLE_GJSCOMPAT"
+fi
+
+AC_SUBST(SEED_GJSCOMPAT_CFLAGS)
+AM_CONDITIONAL(SEED_ENABLE_GJSCOMPAT, test "x$enable_gjscompat" = "xyes")
+
 dnl =============================gtk-doc=======================================
 GTK_DOC_CHECK(1.9)
 
@@ -507,6 +520,9 @@ modules/mpfr/Makefile
 modules/ffi/Makefile
 modules/DynamicObject/Makefile
 
+extensions/gjs/Makefile
+modules/gjs/Makefile
+
 libseed/seed-path.h
 ])
 AC_OUTPUT
@@ -517,6 +533,7 @@ Build Configuration:
    Profiling/Coverage.........$enable_profile
    Profiling for Modules......$enable_profile_modules
    gtk-doc....................$enable_gtk_doc
+   Gjs compatiblity...........$enable_gjscompat
 
 Installation:
    Prefix.....................$prefix
diff --git a/extensions/Makefile.am b/extensions/Makefile.am
index e9f11bf..241c9d9 100644
--- a/extensions/Makefile.am
+++ b/extensions/Makefile.am
@@ -1,3 +1,7 @@
+if SEED_ENABLE_GJSCOMPAT
+SUBDIRS = gjs
+endif
+
 EXTRA_DIST= GLib.js Gio.js Seed.js.in Gtk.js GObject.js Clutter.js Gst.js repl.js
 
 extensiondir=$(datadir)/seed SEED_GTK_VERSION@/extensions
diff --git a/extensions/gjs/GLib.js b/extensions/gjs/GLib.js
new file mode 100644
index 0000000..f4161a2
--- /dev/null
+++ b/extensions/gjs/GLib.js
@@ -0,0 +1,285 @@
+// Copyright 2011 Giovanni Campagna
+//
+// 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 THE SOFTWARE.
+
+const ByteArray = imports.byteArray;
+
+let GLib;
+let originalVariantClass;
+
+const SIMPLE_TYPES = ['b', 'y', 'n', 'q', 'i', 'u', 'x', 't', 'h', 'd', 's', 'o', 'g'];
+
+function _read_single_type(signature, forceSimple) {
+    let char = signature.shift();
+    let isSimple = false;
+
+    if (SIMPLE_TYPES.indexOf(char) == -1) {
+       if (forceSimple)
+           throw new TypeError('Invalid GVariant signature (a simple type was expected)');
+    } else
+       isSimple = true;
+
+    if (char == 'm' || char == 'a')
+       return [char].concat(_read_single_type(signature, false));
+    if (char == '{') {
+       let key = _read_single_type(signature, true);
+       let val = _read_single_type(signature, false);
+       let close = signature.shift();
+       if (close != '}')
+           throw new TypeError('Invalid GVariant signature for type DICT_ENTRY (expected "}"');
+       return [char].concat(key, val, close);
+    }
+    if (char == '(') {
+       let res = [char];
+       while (true) {
+           if (signature.length == 0)
+               throw new TypeError('Invalid GVariant signature for type TUPLE (expected ")")');
+           let next = signature[0];
+           if (next == ')') {
+               signature.shift();
+               return res.concat(next);
+           }
+           let el = _read_single_type(signature);
+           res = res.concat(el);
+       }
+    }
+
+    // Valid types are simple types, arrays, maybes, tuples, dictionary entries and variants
+    if (!isSimple && char != 'v')
+       throw new TypeError('Invalid GVariant signature (' + char + ' is not a valid type)');
+
+    return [char];
+}
+
+function _makeBytes(byteArray) {
+    if (byteArray instanceof ByteArray.ByteArray)
+        return byteArray.toGBytes();
+    else
+        return new GLib.Bytes(byteArray);
+}
+
+function _pack_variant(signature, value) {
+    if (signature.length == 0)
+           throw new TypeError('GVariant signature cannot be empty');
+
+    let char = signature.shift();
+    switch (char) {
+    case 'b':
+       return GLib.Variant.new_boolean(value);
+    case 'y':
+       return GLib.Variant.new_byte(value);
+    case 'n':
+       return GLib.Variant.new_int16(value);
+    case 'q':
+       return GLib.Variant.new_uint16(value);
+    case 'i':
+       return GLib.Variant.new_int32(value);
+    case 'u':
+       return GLib.Variant.new_uint32(value);
+    case 'x':
+       return GLib.Variant.new_int64(value);
+    case 't':
+       return GLib.Variant.new_uint64(value);
+    case 'h':
+       return GLib.Variant.new_handle(value);
+    case 'd':
+       return GLib.Variant.new_double(value);
+    case 's':
+       return GLib.Variant.new_string(value);
+    case 'o':
+       return GLib.Variant.new_object_path(value);
+    case 'g':
+       return GLib.Variant.new_signature(value);
+    case 'v':
+       return GLib.Variant.new_variant(value);
+    case 'm':
+       if (value != null)
+           return GLib.Variant.new_maybe(null, _pack_variant(signature, value));
+       else
+           return GLib.Variant.new_maybe(new GLib.VariantType(_read_single_type(signature, false).join('')), 
null);
+    case 'a':
+       let arrayType = _read_single_type(signature, false);
+       if (arrayType[0] == 's') {
+           // special case for array of strings
+           return GLib.Variant.new_strv(value);
+       }
+       if (arrayType[0] == 'y') {
+           // special case for array of bytes
+           return GLib.Variant.new_from_bytes(new GLib.VariantType('ay'),
+                                               _makeBytes(value), true);
+       }
+
+       let arrayValue = [];
+       if (arrayType[0] == '{') {
+           // special case for dictionaries
+           for (let key in value) {
+               let copy = [].concat(arrayType);
+               let child = _pack_variant(copy, [key, value[key]]);
+               arrayValue.push(child);
+           }
+       } else {
+           for (let i = 0; i < value.length; i++) {
+               let copy = [].concat(arrayType);
+               let child = _pack_variant(copy, value[i]);
+               arrayValue.push(child);
+           }
+       }
+       return GLib.Variant.new_array(new GLib.VariantType(arrayType.join('')), arrayValue);
+
+    case '(':
+       let children = [ ];
+       for (let i = 0; i < value.length; i++) {
+           let next = signature[0];
+           if (next == ')')
+               break;
+           children.push(_pack_variant(signature, value[i]));
+       }
+
+       if (signature[0] != ')')
+           throw new TypeError('Invalid GVariant signature for type TUPLE (expected ")")');
+       signature.shift();
+       return GLib.Variant.new_tuple(children);
+    case '{':
+       let key = _pack_variant(signature, value[0]);
+       let child = _pack_variant(signature, value[1]);
+
+       if (signature[0] != '}')
+           throw new TypeError('Invalid GVariant signature for type DICT_ENTRY (expected "}")');
+       signature.shift();
+
+       return GLib.Variant.new_dict_entry(key, child);
+    default:
+       throw new TypeError('Invalid GVariant signature (unexpected character ' + char + ')');
+    }
+}
+
+function _unpack_variant(variant, deep) {
+    switch (String.fromCharCode(variant.classify())) {
+    case 'b':
+       return variant.get_boolean();
+    case 'y':
+       return variant.get_byte();
+    case 'n':
+       return variant.get_int16();
+    case 'q':
+       return variant.get_uint16();
+    case 'i':
+       return variant.get_int32();
+    case 'u':
+       return variant.get_uint32();
+    case 'x':
+       return variant.get_int64();
+    case 't':
+       return variant.get_uint64();
+    case 'h':
+       return variant.get_handle();
+    case 'd':
+       return variant.get_double();
+    case 'o':
+    case 'g':
+    case 's':
+       // g_variant_get_string has length as out argument
+       return variant.get_string()[0];
+    case 'v':
+       return variant.get_variant();
+    case 'm':
+       let val = variant.get_maybe();
+       if (deep && val)
+           return _unpack_variant(val, deep);
+       else
+           return val;
+    case 'a':
+       if (variant.is_of_type(new GLib.VariantType('a{?*}'))) {
+           // special case containers
+           let ret = { };
+           let nElements = variant.n_children();
+           for (let i = 0; i < nElements; i++) {
+               // always unpack the dictionary entry, and always unpack
+               // the key (or it cannot be added as a key)
+               let val = _unpack_variant(variant.get_child_value(i), deep);
+               let key;
+               if (!deep)
+                   key = _unpack_variant(val[0], true);
+               else
+                   key = val[0];
+               ret[key] = val[1];
+           }
+           return ret;
+       }
+        if (variant.is_of_type(new GLib.VariantType('ay'))) {
+            // special case byte arrays
+            return variant.get_data_as_bytes().toArray();
+        }
+
+       // fall through
+    case '(':
+    case '{':
+       let ret = [ ];
+       let nElements = variant.n_children();
+       for (let i = 0; i < nElements; i++) {
+           let val = variant.get_child_value(i);
+           if (deep)
+               ret.push(_unpack_variant(val, deep));
+           else
+               ret.push(val);
+       }
+       return ret;
+    }
+
+    throw new Error('Assertion failure: this code should not be reached');
+}
+
+function _init() {
+    // this is imports.gi.GLib
+
+    GLib = this;
+
+    // small HACK: we add a matches() method to standard Errors so that
+    // you can do "catch(e if e.matches(Ns.FooError, Ns.FooError.SOME_CODE))"
+    // without checking instanceof
+    Error.prototype.matches = function() { return false; };
+
+    this.Variant._new_internal = function(sig, value) {
+       let signature = Array.prototype.slice.call(sig);
+
+       let variant = _pack_variant(signature, value);
+       if (signature.length != 0)
+           throw new TypeError('Invalid GVariant signature (more than one single complete type)');
+
+       return variant;
+    };
+
+    // Deprecate version of new GLib.Variant()
+    this.Variant.new = function(sig, value) {
+       return new GLib.Variant(sig, value);
+    };
+    this.Variant.prototype.unpack = function() {
+       return _unpack_variant(this, false);
+    };
+    this.Variant.prototype.deep_unpack = function() {
+       return _unpack_variant(this, true);
+    };
+    this.Variant.prototype.toString = function() {
+       return '[object variant of type "' + this.get_type_string() + '"]';
+    };
+
+    this.Bytes.prototype.toArray = function() {
+       return imports.byteArray.fromGBytes(this);
+    };
+}
diff --git a/extensions/gjs/GObject.js b/extensions/gjs/GObject.js
new file mode 100644
index 0000000..cc41485
--- /dev/null
+++ b/extensions/gjs/GObject.js
@@ -0,0 +1,408 @@
+// Copyright 2011 Jasper St. Pierre
+//
+// 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 THE SOFTWARE.
+
+const Lang = imports.lang;
+const Gi = imports._gi;
+const GjsPrivate = imports.gi.GjsPrivate;
+
+let GObject;
+
+// Some common functions between GObject.Class and GObject.Interface
+
+function _createSignals(gtype, signals) {
+    for (let signalName in signals) {
+        let obj = signals[signalName];
+        let flags = (obj.flags !== undefined) ? obj.flags : GObject.SignalFlags.RUN_FIRST;
+        let accumulator = (obj.accumulator !== undefined) ? obj.accumulator : GObject.AccumulatorType.NONE;
+        let rtype = (obj.return_type !== undefined) ? obj.return_type : GObject.TYPE_NONE;
+        let paramtypes = (obj.param_types !== undefined) ? obj.param_types : [];
+
+        try {
+            obj.signal_id = Gi.signal_new(gtype, signalName, flags, accumulator, rtype, paramtypes);
+        } catch (e) {
+            throw new TypeError('Invalid signal ' + signalName + ': ' + e.message);
+        }
+    }
+}
+
+function _createGTypeName(name, gtypename) {
+    if (gtypename)
+        return gtypename;
+    else
+        return 'Gjs_' + name;
+}
+
+function _getGObjectInterfaces(interfaces) {
+    return interfaces.filter((iface) => iface.hasOwnProperty('$gtype'));
+}
+
+function _propertiesAsArray(propertyObj) {
+    let propertiesArray = [];
+    if (propertyObj) {
+        for (let prop in propertyObj) {
+            propertiesArray.push(propertyObj[prop]);
+        }
+    }
+    return propertiesArray;
+}
+
+const GObjectMeta = new Lang.Class({
+    Name: 'GObjectClass',
+    Extends: Lang.Class,
+
+    _init: function (params) {
+        // retrieve signals and remove them from params before chaining
+       let signals = params.Signals;
+        delete params.Signals;
+
+        this.parent(params);
+
+        if (signals)
+            _createSignals(this.$gtype, signals);
+
+       let propertyObj = { };
+       Object.getOwnPropertyNames(params).forEach(function(name) {
+            if (name == 'Name' || name == 'Extends' || name == 'Abstract')
+               return;
+
+            let descriptor = Object.getOwnPropertyDescriptor(params, name);
+
+            if (typeof descriptor.value === 'function') {
+               let wrapped = this.prototype[name];
+
+                if (name.slice(0, 6) == 'vfunc_') {
+                    Gi.hook_up_vfunc(this.prototype, name.slice(6), wrapped);
+                } else if (name.slice(0, 3) == 'on_') {
+                    let id = GObject.signal_lookup(name.slice(3).replace('_', '-'), this.$gtype);
+                    if (id != 0) {
+                        GObject.signal_override_class_closure(id, this.$gtype, function() {
+                            let argArray = Array.prototype.slice.call(arguments);
+                            let emitter = argArray.shift();
+
+                            return wrapped.apply(emitter, argArray);
+                        });
+                    }
+                }
+           }
+       }.bind(this));
+    },
+
+    _isValidClass: function(klass) {
+        let proto = klass.prototype;
+
+        if (!proto)
+            return false;
+
+        // If proto == GObject.Object.prototype, then
+        // proto.__proto__ is Object, so "proto instanceof GObject.Object"
+        // will return false.
+        return proto == GObject.Object.prototype ||
+            proto instanceof GObject.Object;
+    },
+
+    // If we want an object with a custom JSClass, we can't just
+    // use a function. We have to use a custom constructor here.
+    _construct: function(params) {
+        if (!params.Name)
+            throw new TypeError("Classes require an explicit 'Name' parameter.");
+        let name = params.Name;
+
+        let gtypename = _createGTypeName(params.Name, params.GTypeName);
+
+        if (!params.Extends)
+            params.Extends = GObject.Object;
+        let parent = params.Extends;
+
+        if (!this._isValidClass(parent))
+            throw new TypeError('GObject.Class used with invalid base class (is ' + parent + ')');
+
+        let interfaces = params.Implements || [];
+        if (parent instanceof Lang.Class)
+            interfaces = interfaces.filter((iface) => !parent.implements(iface));
+        let gobjectInterfaces = _getGObjectInterfaces(interfaces);
+
+        let propertiesArray = _propertiesAsArray(params.Properties);
+        delete params.Properties;
+
+        let newClass = Gi.register_type(parent.prototype, gtypename,
+            gobjectInterfaces, propertiesArray);
+
+        // See Class.prototype._construct in lang.js for the reasoning
+        // behind this direct __proto__ set.
+        newClass.__proto__ = this.constructor.prototype;
+        newClass.__super__ = parent;
+
+        newClass._init.apply(newClass, arguments);
+
+        Object.defineProperties(newClass.prototype, {
+            '__metaclass__': { writable: false,
+                               configurable: false,
+                               enumerable: false,
+                               value: this.constructor },
+            '__interfaces__': { writable: false,
+                                configurable: false,
+                                enumerable: false,
+                                value: interfaces }
+        });
+
+        interfaces.forEach((iface) => {
+            if (iface instanceof Lang.Interface)
+                iface._check(newClass.prototype);
+        });
+
+        return newClass;
+    },
+
+    // Overrides Lang.Class.implements()
+    implements: function (iface) {
+        if (iface instanceof GObject.Interface) {
+            return GObject.type_is_a(this.$gtype, iface.$gtype);
+        } else {
+            return this.parent(iface);
+        }
+    }
+});
+
+function GObjectInterface(params) {
+    return this._construct.apply(this, arguments);
+}
+
+GObjectMeta.MetaInterface = GObjectInterface;
+
+GObjectInterface.__super__ = Lang.Interface;
+GObjectInterface.prototype = Object.create(Lang.Interface.prototype);
+GObjectInterface.prototype.constructor = GObjectInterface;
+GObjectInterface.prototype.__name__ = 'GObjectInterface';
+
+GObjectInterface.prototype._construct = function (params) {
+    if (!params.Name) {
+        throw new TypeError("Interfaces require an explicit 'Name' parameter.");
+    }
+
+    let gtypename = _createGTypeName(params.Name, params.GTypeName);
+    delete params.GTypeName;
+
+    let interfaces = params.Requires || [];
+    let gobjectInterfaces = _getGObjectInterfaces(interfaces);
+
+    let properties = _propertiesAsArray(params.Properties);
+    delete params.Properties;
+
+    let newInterface = Gi.register_interface(gtypename, gobjectInterfaces,
+        properties);
+
+    // See Class.prototype._construct in lang.js for the reasoning
+    // behind this direct __proto__ set.
+    newInterface.__proto__ = this.constructor.prototype;
+    newInterface.__super__ = GObjectInterface;
+    newInterface.prototype.constructor = newInterface;
+
+    newInterface._init.apply(newInterface, arguments);
+
+    Object.defineProperty(newInterface.prototype, '__metaclass__', {
+        writable: false,
+        configurable: false,
+        enumerable: false,
+        value: this.constructor
+    });
+
+    return newInterface;
+};
+
+GObjectInterface.prototype._init = function (params) {
+    let signals = params.Signals;
+    delete params.Signals;
+
+    Lang.Interface.prototype._init.call(this, params);
+
+    _createSignals(this.$gtype, signals);
+};
+
+function _init() {
+
+    GObject = this;
+
+    function _makeDummyClass(obj, name, upperName, gtypeName, actual) {
+        let gtype = GObject.type_from_name(gtypeName);
+        obj['TYPE_' + upperName] = gtype;
+        obj[name] = function(v) { return new actual(v); };
+        obj[name].$gtype = gtype;
+    }
+
+    _makeDummyClass(this, 'VoidType', 'NONE', 'void', function() {});
+    _makeDummyClass(this, 'Char', 'CHAR', 'gchar', Number);
+    _makeDummyClass(this, 'UChar', 'UCHAR', 'guchar', Number);
+    _makeDummyClass(this, 'Unichar', 'UNICHAR', 'gint', String);
+
+    this.TYPE_BOOLEAN = GObject.type_from_name('gboolean');
+    this.Boolean = Boolean;
+    Boolean.$gtype = this.TYPE_BOOLEAN;
+
+    _makeDummyClass(this, 'Int', 'INT', 'gint', Number);
+    _makeDummyClass(this, 'UInt', 'UINT', 'guint', Number);
+    _makeDummyClass(this, 'Long', 'LONG', 'glong', Number);
+    _makeDummyClass(this, 'ULong', 'ULONG', 'gulong', Number);
+    _makeDummyClass(this, 'Int64', 'INT64', 'gint64', Number);
+    _makeDummyClass(this, 'UInt64', 'UINT64', 'guint64', Number);
+
+    this.TYPE_ENUM = GObject.type_from_name('GEnum');
+    this.TYPE_FLAGS = GObject.type_from_name('GFlags');
+
+    _makeDummyClass(this, 'Float', 'FLOAT', 'gfloat', Number);
+    this.TYPE_DOUBLE = GObject.type_from_name('gdouble');
+    this.Double = Number;
+    Number.$gtype = this.TYPE_DOUBLE;
+
+    this.TYPE_STRING = GObject.type_from_name('gchararray');
+    this.String = String;
+    String.$gtype = this.TYPE_STRING;
+
+    this.TYPE_POINTER = GObject.type_from_name('gpointer');
+    this.TYPE_BOXED = GObject.type_from_name('GBoxed');
+    this.TYPE_PARAM = GObject.type_from_name('GParam');
+    this.TYPE_INTERFACE = GObject.type_from_name('GInterface');
+    this.TYPE_OBJECT = GObject.type_from_name('GObject');
+    this.TYPE_VARIANT = GObject.type_from_name('GVariant');
+
+    _makeDummyClass(this, 'Type', 'GTYPE', 'GType', GObject.type_from_name);
+
+    this.ParamSpec.char = function(name, nick, blurb, flags, minimum, maximum, default_value) {
+        return GObject.param_spec_char(name, nick, blurb, minimum, maximum, default_value, flags);
+    };
+
+    this.ParamSpec.uchar = function(name, nick, blurb, flags, minimum, maximum, default_value) {
+        return GObject.param_spec_uchar(name, nick, blurb, minimum, maximum, default_value, flags);
+    };
+
+    this.ParamSpec.int = function(name, nick, blurb, flags, minimum, maximum, default_value) {
+        return GObject.param_spec_int(name, nick, blurb, minimum, maximum, default_value, flags);
+    };
+
+    this.ParamSpec.uint = function(name, nick, blurb, flags, minimum, maximum, default_value) {
+        return GObject.param_spec_uint(name, nick, blurb, minimum, maximum, default_value, flags);
+    };
+
+    this.ParamSpec.long = function(name, nick, blurb, flags, minimum, maximum, default_value) {
+        return GObject.param_spec_long(name, nick, blurb, minimum, maximum, default_value, flags);
+    };
+
+    this.ParamSpec.ulong = function(name, nick, blurb, flags, minimum, maximum, default_value) {
+        return GObject.param_spec_ulong(name, nick, blurb, minimum, maximum, default_value, flags);
+    };
+
+    this.ParamSpec.int64 = function(name, nick, blurb, flags, minimum, maximum, default_value) {
+        return GObject.param_spec_int64(name, nick, blurb, minimum, maximum, default_value, flags);
+    };
+
+    this.ParamSpec.uint64 = function(name, nick, blurb, flags, minimum, maximum, default_value) {
+        return GObject.param_spec_uint64(name, nick, blurb, minimum, maximum, default_value, flags);
+    };
+
+    this.ParamSpec.float = function(name, nick, blurb, flags, minimum, maximum, default_value) {
+        return GObject.param_spec_float(name, nick, blurb, minimum, maximum, default_value, flags);
+    };
+
+    this.ParamSpec.boolean = function(name, nick, blurb, flags, default_value) {
+        return GObject.param_spec_boolean(name, nick, blurb, default_value, flags);
+    };
+
+    this.ParamSpec.flags = function(name, nick, blurb, flags, flags_type, default_value) {
+        return GObject.param_spec_flags(name, nick, blurb, flags_type, default_value, flags);
+    };
+
+    this.ParamSpec.enum = function(name, nick, blurb, flags, enum_type, default_value) {
+        return GObject.param_spec_enum(name, nick, blurb, enum_type, default_value, flags);
+    };
+
+    this.ParamSpec.double = function(name, nick, blurb, flags, minimum, maximum, default_value) {
+        return GObject.param_spec_double(name, nick, blurb, minimum, maximum, default_value, flags);
+    };
+
+    this.ParamSpec.string = function(name, nick, blurb, flags, default_value) {
+        return GObject.param_spec_string(name, nick, blurb, default_value, flags);
+    };
+
+    this.ParamSpec.boxed = function(name, nick, blurb, flags, boxed_type) {
+        return GObject.param_spec_boxed(name, nick, blurb, boxed_type, flags);
+    };
+
+    this.ParamSpec.object = function(name, nick, blurb, flags, object_type) {
+        return GObject.param_spec_object(name, nick, blurb, object_type, flags);
+    };
+
+    this.ParamSpec.param = function(name, nick, blurb, flags, param_type) {
+        return GObject.param_spec_param(name, nick, blurb, param_type, flags);
+    };
+
+    this.ParamSpec.override = Gi.override_property;
+
+    Object.defineProperties(this.ParamSpec.prototype, {
+        'name': { configurable: false,
+                  enumerable: false,
+                  get: function() { return this.get_name() } },
+        '_nick': { configurable: false,
+                   enumerable: false,
+                   get: function() { return this.get_nick() } },
+        'nick': { configurable: false,
+                  enumerable: false,
+                  get: function() { return this.get_nick() } },
+        '_blurb': { configurable: false,
+                    enumerable: false,
+                    get: function() { return this.get_blurb() } },
+        'blurb': { configurable: false,
+                   enumerable: false,
+                   get: function() { return this.get_blurb() } },
+        'default_value': { configurable: false,
+                           enumerable: false,
+                           get: function() { return this.get_default_value() } },
+        'flags':  { configurable: false,
+                    enumerable: false,
+                    get: function() { return GjsPrivate.param_spec_get_flags(this) } },
+        'value_type':  { configurable: false,
+                         enumerable: false,
+                         get: function() { return GjsPrivate.param_spec_get_value_type(this) } },
+        'owner_type':  { configurable: false,
+                         enumerable: false,
+                         get: function() { return GjsPrivate.param_spec_get_owner_type(this) } },
+    });
+
+
+    this.Class = GObjectMeta;
+    this.Interface = GObjectInterface;
+    this.Object.prototype.__metaclass__ = this.Class;
+
+    // For compatibility with Lang.Class... we need a _construct
+    // or the Lang.Class constructor will fail.
+    this.Object.prototype._construct = function() {
+        this._init.apply(this, arguments);
+        return this;
+    };
+
+    // fake enum for signal accumulators, keep in sync with gi/object.c
+    this.AccumulatorType = {
+        NONE: 0,
+        FIRST_WINS: 1,
+        TRUE_HANDLED: 2
+    };
+
+    this.Object.prototype.disconnect = function(id) {
+        return GObject.signal_handler_disconnect(this, id);
+    };
+}
diff --git a/extensions/gjs/Gio.js b/extensions/gjs/Gio.js
new file mode 100644
index 0000000..950641b
--- /dev/null
+++ b/extensions/gjs/Gio.js
@@ -0,0 +1,394 @@
+// Copyright 2011 Giovanni Campagna
+//
+// 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 THE SOFTWARE.
+
+var GLib = imports.gi.GLib;
+var GObject = imports.gi.GObject;
+var GjsPrivate = imports.gi.GjsPrivate;
+var Lang = imports.lang;
+var Signals = imports.signals;
+var Gio;
+
+function _signatureLength(sig) {
+    var counter = 0;
+    // make it an array
+    var signature = Array.prototype.slice.call(sig);
+    while (signature.length) {
+        GLib._read_single_type(sig);
+        counter++;
+    }
+    return counter;
+}
+
+function _proxyInvoker(methodName, sync, inSignature, arg_array) {
+    var replyFunc;
+    var flags = 0;
+    var cancellable = null;
+
+    /* Convert arg_array to a *real* array */
+    arg_array = Array.prototype.slice.call(arg_array);
+
+    /* The default replyFunc only logs the responses */
+    replyFunc = _logReply;
+
+    var signatureLength = inSignature.length;
+    var minNumberArgs = signatureLength;
+    var maxNumberArgs = signatureLength + 3;
+
+    if (arg_array.length < minNumberArgs) {
+        throw new Error("Not enough arguments passed for method: " + methodName +
+                       ". Expected " + minNumberArgs + ", got " + arg_array.length);
+    } else if (arg_array.length > maxNumberArgs) {
+        throw new Error("Too many arguments passed for method: " + methodName +
+                       ". Maximum is " + maxNumberArgs +
+                        " + one callback and/or flags");
+    }
+
+    while (arg_array.length > signatureLength) {
+        var argNum = arg_array.length - 1;
+        var arg = arg_array.pop();
+        if (typeof(arg) == "function" && !sync) {
+            replyFunc = arg;
+        } else if (typeof(arg) == "number") {
+            flags = arg;
+        } else if (arg instanceof Gio.Cancellable) {
+            cancellable = arg;
+        } else {
+            throw new Error("Argument " + argNum + " of method " + methodName +
+                            " is " + typeof(arg) + ". It should be a callback, flags or a Gio.Cancellable");
+        }
+    }
+
+    var inVariant = new GLib.Variant('(' + inSignature.join('') + ')', arg_array);
+
+    var asyncCallback = function (proxy, result) {
+        var outVariant = null, succeeded = false;
+        try {
+            outVariant = proxy.call_finish(result);
+            succeeded = true;
+        } catch (e) {
+            replyFunc(null, e);
+        }
+
+        if (succeeded)
+            replyFunc(outVariant.deep_unpack(), null);
+    };
+
+    if (sync) {
+        return this.call_sync(methodName,
+                              inVariant,
+                              flags,
+                              -1,
+                              cancellable).deep_unpack();
+    } else {
+        return this.call(methodName,
+                         inVariant,
+                         flags,
+                         -1,
+                         cancellable,
+                         asyncCallback);
+    }
+}
+
+function _logReply(result, exc) {
+    if (exc != null) {
+        log("Ignored exception from dbus method: " + exc.toString());
+    }
+}
+
+function _makeProxyMethod(method, sync) {
+    var i;
+    var name = method.name;
+    var inArgs = method.in_args;
+    var inSignature = [ ];
+    for (i = 0; i < inArgs.length; i++)
+        inSignature.push(inArgs[i].signature);
+
+    return function() {
+        return _proxyInvoker.call(this, name, sync, inSignature, arguments);
+    };
+}
+
+function _convertToNativeSignal(proxy, sender_name, signal_name, parameters) {
+    Signals._emit.call(proxy, signal_name, sender_name, parameters.deep_unpack());
+}
+
+function _propertyGetter(name) {
+    let value = this.get_cached_property(name);
+    return value ? value.deep_unpack() : null;
+}
+
+function _propertySetter(value, name, signature) {
+    let variant = new GLib.Variant(signature, value);
+    this.set_cached_property(name, variant);
+
+    this.call('org.freedesktop.DBus.Properties.Set',
+              new GLib.Variant('(ssv)',
+                               [this.g_interface_name,
+                                name, variant]),
+              Gio.DBusCallFlags.NONE, -1, null,
+              Lang.bind(this, function(proxy, result) {
+                  try {
+                      this.call_finish(result);
+                  } catch(e) {
+                      log('Could not set property ' + name + ' on remote object ' +
+                          this.g_object_path + ': ' + e.message);
+                  }
+              }));
+}
+
+function _addDBusConvenience() {
+    let info = this.g_interface_info;
+    if (!info)
+        return;
+
+    if (info.signals.length > 0)
+        this.connect('g-signal', _convertToNativeSignal);
+
+    let i, methods = info.methods;
+    for (i = 0; i < methods.length; i++) {
+        var method = methods[i];
+        this[method.name + 'Remote'] = _makeProxyMethod(methods[i], false);
+        this[method.name + 'Sync'] = _makeProxyMethod(methods[i], true);
+    }
+
+    let properties = info.properties;
+    for (i = 0; i < properties.length; i++) {
+        let name = properties[i].name;
+        let signature = properties[i].signature;
+        Object.defineProperty(this, name, { get: Lang.bind(this, _propertyGetter, name),
+                                            set: Lang.bind(this, _propertySetter, name, signature),
+                                            configurable: true,
+                                            enumerable: true });
+    }
+}
+
+function _makeProxyWrapper(interfaceXml) {
+    var info = _newInterfaceInfo(interfaceXml);
+    var iname = info.name;
+    return function(bus, name, object, asyncCallback, cancellable) {
+        var obj = new Gio.DBusProxy({ g_connection: bus,
+                                      g_interface_name: iname,
+                                      g_interface_info: info,
+                                      g_name: name,
+                                      g_object_path: object });
+        if (!cancellable)
+            cancellable = null;
+        if (asyncCallback)
+            obj.init_async(GLib.PRIORITY_DEFAULT, cancellable, function(initable, result) {
+                let caughtErrorWhenInitting = null;
+                try {
+                    initable.init_finish(result);
+                } catch(e) {
+                    caughtErrorWhenInitting = e;
+                }
+
+                if (caughtErrorWhenInitting === null) {
+                    asyncCallback(initable, null);
+                } else {
+                    asyncCallback(null, caughtErrorWhenInitting);
+                }
+            });
+        else
+            obj.init(cancellable);
+        return obj;
+    };
+}
+
+
+function _newNodeInfo(constructor, value) {
+    if (typeof value == 'string')
+        return constructor(value);
+    else if (value instanceof XML)
+        return constructor(value.toXMLString());
+    else
+        throw TypeError('Invalid type ' + Object.prototype.toString.call(value));
+}
+
+function _newInterfaceInfo(value) {
+    var nodeInfo = Gio.DBusNodeInfo.new_for_xml(value);
+    return nodeInfo.interfaces[0];
+}
+
+function _injectToMethod(klass, method, addition) {
+    var previous = klass[method];
+
+    klass[method] = function() {
+        addition.apply(this, arguments);
+        return previous.apply(this, arguments);
+    };
+}
+
+function _wrapFunction(klass, method, addition) {
+    var previous = klass[method];
+
+    klass[method] = function() {
+        var args = Array.prototype.slice.call(arguments);
+        args.unshift(previous);
+        return addition.apply(this, args);
+    };
+}
+
+function _makeOutSignature(args) {
+    var ret = '(';
+    for (var i = 0; i < args.length; i++)
+        ret += args[i].signature;
+
+    return ret + ')';
+}
+
+function _handleMethodCall(info, impl, method_name, parameters, invocation) {
+    // prefer a sync version if available
+    if (this[method_name]) {
+        let retval;
+        try {
+            retval = this[method_name].apply(this, parameters.deep_unpack());
+        } catch (e) {
+            if (e instanceof GLib.Error) {
+                invocation.return_gerror(e);
+            } else {
+                let name = e.name;
+                if (name.indexOf('.') == -1) {
+                    // likely to be a normal JS error
+                    name = 'org.gnome.gjs.JSError.' + name;
+                }
+                logError(e, "Exception in method call: " + method_name);
+                invocation.return_dbus_error(name, e.message);
+            }
+            return;
+        }
+        if (retval === undefined) {
+            // undefined (no return value) is the empty tuple
+            retval = new GLib.Variant('()', []);
+        }
+        try {
+            if (!(retval instanceof GLib.Variant)) {
+                // attempt packing according to out signature
+                let methodInfo = info.lookup_method(method_name);
+                let outArgs = methodInfo.out_args;
+                let outSignature = _makeOutSignature(outArgs);
+                if (outArgs.length == 1) {
+                    // if one arg, we don't require the handler wrapping it
+                    // into an Array
+                    retval = [retval];
+                }
+                retval = new GLib.Variant(outSignature, retval);
+            }
+            invocation.return_value(retval);
+        } catch(e) {
+            // if we don't do this, the other side will never see a reply
+            invocation.return_dbus_error('org.gnome.gjs.JSError.ValueError',
+                                         "Service implementation returned an incorrect value type");
+        }
+    } else if (this[method_name + 'Async']) {
+        this[method_name + 'Async'](parameters.deep_unpack(), invocation);
+    } else {
+        log('Missing handler for DBus method ' + method_name);
+        invocation.return_gerror(new Gio.DBusError({ code: Gio.DBusError.UNKNOWN_METHOD,
+                                                     message: 'Method ' + method_name + ' is not 
implemented' }));
+    }
+}
+
+function _handlePropertyGet(info, impl, property_name) {
+    let propInfo = info.lookup_property(property_name);
+    let jsval = this[property_name];
+    if (jsval != undefined)
+        return new GLib.Variant(propInfo.signature, jsval);
+    else
+        return null;
+}
+
+function _handlePropertySet(info, impl, property_name, new_value) {
+    this[property_name] = new_value.deep_unpack();
+}
+
+function _wrapJSObject(interfaceInfo, jsObj) {
+    var info;
+    if (interfaceInfo instanceof Gio.DBusInterfaceInfo)
+        info = interfaceInfo;
+    else
+        info = Gio.DBusInterfaceInfo.new_for_xml(interfaceInfo);
+    info.cache_build();
+
+    var impl = new GjsPrivate.DBusImplementation({ g_interface_info: info });
+    impl.connect('handle-method-call', function(impl, method_name, parameters, invocation) {
+        return _handleMethodCall.call(jsObj, info, impl, method_name, parameters, invocation);
+    });
+    impl.connect('handle-property-get', function(impl, property_name) {
+        return _handlePropertyGet.call(jsObj, info, impl, property_name);
+    });
+    impl.connect('handle-property-set', function(impl, property_name, value) {
+        return _handlePropertySet.call(jsObj, info, impl, property_name, value);
+    });
+
+    return impl;
+}
+
+function _init() {
+    Gio = this;
+
+    Gio.DBus = {
+        get session() {
+            return Gio.bus_get_sync(Gio.BusType.SESSION, null);
+        },
+        get system() {
+            return Gio.bus_get_sync(Gio.BusType.SYSTEM, null);
+        },
+
+        // Namespace some functions
+        get:        Gio.bus_get,
+        get_finish: Gio.bus_get_finish,
+        get_sync:   Gio.bus_get_sync,
+
+        own_name:               Gio.bus_own_name,
+        own_name_on_connection: Gio.bus_own_name_on_connection,
+        unown_name:             Gio.bus_unown_name,
+
+        watch_name:               Gio.bus_watch_name,
+        watch_name_on_connection: Gio.bus_watch_name_on_connection,
+        unwatch_name:             Gio.bus_unwatch_name
+    };
+
+    Gio.DBusConnection.prototype.watch_name = function(name, flags, appeared, vanished) {
+        return Gio.bus_watch_name_on_connection(this, name, flags, appeared, vanished);
+    };
+    Gio.DBusConnection.prototype.unwatch_name = function(id) {
+        return Gio.bus_unwatch_name(id);
+    };
+    Gio.DBusConnection.prototype.own_name = function(name, flags, acquired, lost) {
+        return Gio.bus_own_name_on_connection(this, name, flags, acquired, lost);
+    };
+    Gio.DBusConnection.prototype.unown_name = function(id) {
+        return Gio.bus_unown_name(id);
+    };
+
+    _injectToMethod(Gio.DBusProxy.prototype, 'init', _addDBusConvenience);
+    _injectToMethod(Gio.DBusProxy.prototype, 'init_async', _addDBusConvenience);
+    Gio.DBusProxy.prototype.connectSignal = Signals._connect;
+    Gio.DBusProxy.prototype.disconnectSignal = Signals._disconnect;
+
+    Gio.DBusProxy.makeProxyWrapper = _makeProxyWrapper;
+
+    // Some helpers
+    _wrapFunction(Gio.DBusNodeInfo, 'new_for_xml', _newNodeInfo);
+    Gio.DBusInterfaceInfo.new_for_xml = _newInterfaceInfo;
+
+    Gio.DBusExportedObject = GjsPrivate.DBusImplementation;
+    Gio.DBusExportedObject.wrapJSObject = _wrapJSObject;
+}
diff --git a/extensions/gjs/Gtk.js b/extensions/gjs/Gtk.js
new file mode 100644
index 0000000..8a23977
--- /dev/null
+++ b/extensions/gjs/Gtk.js
@@ -0,0 +1,113 @@
+// application/javascript;version=1.8
+// Copyright 2013 Giovanni Campagna
+//
+// 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 THE SOFTWARE.
+
+const Lang = imports.lang;
+const GObject = imports.gi.GObject;
+
+var GjsPrivate = imports.gi.GjsPrivate;
+
+let Gtk;
+
+const GtkWidgetClass = new Lang.Class({
+    Name: 'GtkWidgetClass',
+    Extends: GObject.Class,
+
+    _init: function(params) {
+        let template = params.Template;
+        delete params.Template;
+
+        let children = params.Children;
+        delete params.Children;
+
+        let internalChildren = params.InternalChildren;
+        delete params.InternalChildren;
+
+        if (template) {
+            params._instance_init = function() {
+                this.init_template();
+            };
+        }
+
+        this.parent(params);
+
+        if (template) {
+            if (typeof template == 'string' &&
+                template.startsWith('resource:///'))
+                Gtk.Widget.set_template_from_resource.call(this, template.slice(11));
+            else
+                Gtk.Widget.set_template.call(this, template);
+        }
+
+        this.Template = template;
+        this.Children = children;
+        this.InternalChildren = internalChildren;
+
+        if (children) {
+            for (let i = 0; i < children.length; i++)
+                Gtk.Widget.bind_template_child_full.call(this, children[i], false, 0);
+        }
+
+        if (internalChildren) {
+            for (let i = 0; i < internalChildren.length; i++)
+                Gtk.Widget.bind_template_child_full.call(this, internalChildren[i], true, 0);
+        }
+    },
+
+    _isValidClass: function(klass) {
+        let proto = klass.prototype;
+
+        if (!proto)
+            return false;
+
+        // If proto == Gtk.Widget.prototype, then
+        // proto.__proto__ is GObject.InitiallyUnowned, so
+        // "proto instanceof Gtk.Widget"
+        // will return false.
+        return proto == Gtk.Widget.prototype ||
+            proto instanceof Gtk.Widget;
+    },
+});
+
+function _init() {
+
+    Gtk = this;
+
+    Gtk.Widget.prototype.__metaclass__ = GtkWidgetClass;
+    if (GjsPrivate.gtk_container_child_set_property) {
+        Gtk.Container.prototype.child_set_property = function(child, property, value) {
+            GjsPrivate.gtk_container_child_set_property(this, child, property, value);
+        };
+    }
+
+    Gtk.Widget.prototype._init = function(params) {
+        GObject.Object.prototype._init.call(this, params);
+
+        if (this.constructor.Template) {
+            let children = this.constructor.Children || [];
+            for (let child of children)
+                this[child.replace('-', '_', 'g')] = this.get_template_child(this.constructor, child);
+
+            let internalChildren = this.constructor.InternalChildren || [];
+            for (let child of internalChildren)
+                this['_' + child.replace('-', '_', 'g')] = this.get_template_child(this.constructor, child);
+        }
+    };
+}
diff --git a/extensions/gjs/Makefile.am b/extensions/gjs/Makefile.am
new file mode 100644
index 0000000..ab5906f
--- /dev/null
+++ b/extensions/gjs/Makefile.am
@@ -0,0 +1,4 @@
+EXTRA_DIST= Gio.js GLib.js GObject.js Gtk.js
+
+gjs_overridesdir=$(datadir)/seed SEED_GTK_VERSION@/extensions/gjs
+gjs_overrides_DATA = Gio.js GLib.js GObject.js Gtk.js
diff --git a/libseed/Makefile.am b/libseed/Makefile.am
index d46cc56..4afeffe 100644
--- a/libseed/Makefile.am
+++ b/libseed/Makefile.am
@@ -31,6 +31,7 @@ libseed SEED_GTK_VERSION@_la_CFLAGS = \
        $(WEBKIT_CFLAGS) \
        $(SEED_OSX_CFLAGS) \
        $(SEED_DEBUG_CFLAGS) \
+       $(SEED_GJSCOMPAT_CFLAGS) \
        $(SEED_PROFILE_CFLAGS) \
        $(FFI_CFLAGS) \
        -DGOBJECT_INTROSPECTION_VERSION=$(GOBJECT_INTROSPECTION_VERSION)
diff --git a/libseed/seed-engine.c b/libseed/seed-engine.c
index 35be98f..1341551 100644
--- a/libseed/seed-engine.c
+++ b/libseed/seed-engine.c
@@ -53,6 +53,9 @@ GQuark js_ref_quark;
 
 guint seed_debug_flags = 0;    /* global seed debug flag */
 gboolean seed_arg_print_version = FALSE; // Flag to print version and quit
+#ifdef SEED_ENABLE_GJSCOMPAT
+gboolean seed_arg_gjs_compatiblity = FALSE;
+#endif
 
 pthread_key_t seed_next_gobject_wrapper_key;
 
@@ -106,6 +109,9 @@ seed_prepare_global_context (JSContextRef ctx)
   seed_object_set_property (ctx, global, "imports", importer);
   seed_object_set_property (ctx, global, "GType", seed_gtype_constructor);
   seed_object_set_property (ctx, global, "Seed", seed_obj_ref);
+#ifdef SEED_ENABLE_GJSCOMPAT
+  seed_object_set_property (ctx, global, "ARGV", ARGV_obj_ref);
+#endif
   seed_object_set_property (ctx, global, "print", seed_print_ref);
   seed_object_set_property (ctx, global, "printerr", seed_printerr_ref);
 
@@ -1659,6 +1665,10 @@ static GOptionEntry seed_args[] = {
   {"seed-no-debug", 0, 0, G_OPTION_ARG_CALLBACK, seed_arg_no_debug_cb,
    "Disable Seed debugging", "FLAGS"},
 #endif /* SEED_ENABLE_DEBUG */
+#ifdef SEED_ENABLE_GJSCOMPAT
+   {"seed-gjs-compatibility", 0, 0, G_OPTION_ARG_NONE, &seed_arg_gjs_compatiblity,
+   "Enable gjs compatibility", 0},
+#endif /* SEED_ENABLE_GJSCOMPAT */
   {"seed-version", 0, 0, G_OPTION_ARG_NONE, &seed_arg_print_version,
    "Print libseed version", 0},
   {NULL,},
diff --git a/libseed/seed-importer.c b/libseed/seed-importer.c
index 380e18f..ae75d1f 100644
--- a/libseed/seed-importer.c
+++ b/libseed/seed-importer.c
@@ -811,6 +811,46 @@ seed_importer_handle_file (JSContextRef ctx,
   return global;
 }
 
+static JSObjectRef seed_importer_try_load (JSContextRef ctx,
+               const gchar *test_path,
+               const gchar *script_path,
+               const gchar *prop,
+               const gchar *prop_as_js,
+               const gchar *prop_as_lib,
+               JSValueRef *exception)
+{
+    gchar *file_path = NULL;
+    JSObjectRef ret = NULL;
+    // replace '.' with current script_path if not null
+    if(script_path && !g_strcmp0(".",test_path))
+        test_path = script_path;
+
+    // check if prop is a file or dir (imports['foo.js'] or imports.mydir)
+    file_path = g_build_filename (test_path, prop, NULL);
+    if (g_file_test (file_path, G_FILE_TEST_IS_REGULAR | G_FILE_TEST_IS_DIR)) {
+        ret = seed_importer_handle_file (ctx, test_path, prop, exception);
+        g_free (file_path);
+        return ret;
+    }
+
+    // check if prop is file ending with '.js'
+    file_path = g_build_filename (test_path, prop_as_js, NULL);
+    if (g_file_test (file_path, G_FILE_TEST_IS_REGULAR)) {
+        ret = seed_importer_handle_file (ctx, test_path, prop_as_js, exception);
+        g_free (file_path);
+        return ret;
+    }
+
+    // check if file is native module
+    file_path = g_build_filename (test_path, prop_as_lib, NULL);
+    if (g_file_test (file_path, G_FILE_TEST_IS_REGULAR)) {
+        ret = seed_importer_handle_native_module (ctx, test_path, prop, exception);
+        g_free (file_path);
+        return ret;
+    }
+    return ret;
+}
+
 static JSObjectRef
 seed_importer_search_dirs (JSContextRef ctx, GSList *path, gchar *prop, JSValueRef *exception)
 {
@@ -832,43 +872,28 @@ seed_importer_search_dirs (JSContextRef ctx, GSList *path, gchar *prop, JSValueR
 
     ret = NULL;
     walk = path;
-    while (walk) {
-        gchar *test_path = walk->data;
-        gchar *file_path;
-        
-        // replace '.' with current script_path if not null
-        if(script_path && !g_strcmp0(".",test_path))
-            test_path = script_path;
-
-        // check if prop is a file or dir (imports['foo.js'] or imports.mydir)
-        file_path = g_build_filename (test_path, prop, NULL);
-        if (g_file_test (file_path, G_FILE_TEST_IS_REGULAR | G_FILE_TEST_IS_DIR)) {
-            ret = seed_importer_handle_file (ctx, test_path, prop, exception);
-            g_free (file_path);
-            break;
-        }
-        g_free (file_path);
-
-        // check if prop is file ending with '.js'
-        file_path = g_build_filename (test_path, prop_as_js, NULL);
-        if (g_file_test (file_path, G_FILE_TEST_IS_REGULAR)) {
-            ret = seed_importer_handle_file (ctx, test_path, prop_as_js, exception);
-            g_free (file_path);
-            break;
-        }
-        g_free (file_path);
-      
-        // check if file is native module
-        file_path = g_build_filename (test_path, prop_as_lib, NULL);
-        if (g_file_test (file_path, G_FILE_TEST_IS_REGULAR)) {
-            ret = seed_importer_handle_native_module (ctx, test_path, prop, exception);
-            g_free (file_path);
-            break;
-        }
-        g_free (file_path);
-
-        walk = walk->next;
+#ifdef SEED_ENABLE_GJSCOMPAT
+    if (seed_arg_gjs_compatiblity) {
+               while (walk) {
+                       gchar *test_path = g_strconcat(walk->data, "/gjs", NULL);
+
+                       ret = seed_importer_try_load (ctx, test_path, script_path, prop, prop_as_js,
+                                       prop_as_lib, exception);
+                       g_free (test_path);
+                       walk = walk->next;
+               }
     }
+#endif
+    if (!ret) {
+               walk = path;
+               while (walk) {
+                       gchar *test_path = walk->data;
+
+                       ret = seed_importer_try_load (ctx, test_path, script_path, prop, prop_as_js,
+                                       prop_as_lib, exception);
+                       walk = walk->next;
+               }
+       }
 
     g_free (prop_as_lib);
     g_free (prop_as_js);
diff --git a/libseed/seed-private.h b/libseed/seed-private.h
index c60ac38..1dd6275 100644
--- a/libseed/seed-private.h
+++ b/libseed/seed-private.h
@@ -54,4 +54,8 @@ struct _SeedEngine
 #include "seed-exceptions.h"
 #include "seed-importer.h"
 
+#ifdef SEED_ENABLE_GJSCOMPAT
+extern gboolean seed_arg_gjs_compatiblity;
+#endif
+
 #endif
diff --git a/modules/Makefile.am b/modules/Makefile.am
index 7a99ba2..3ed114d 100644
--- a/modules/Makefile.am
+++ b/modules/Makefile.am
@@ -1 +1,5 @@
 SUBDIRS = example sqlite canvas multiprocessing readline os sandbox dbus libxml cairo gtkbuilder gettext 
mpfr ffi DynamicObject xorg
+
+if SEED_ENABLE_GJSCOMPAT
+SUBDIRS += gjs
+endif
diff --git a/modules/gjs/Makefile.am b/modules/gjs/Makefile.am
new file mode 100644
index 0000000..b83f7ee
--- /dev/null
+++ b/modules/gjs/Makefile.am
@@ -0,0 +1,8 @@
+if SEED_ENABLE_GJSCOMPAT
+
+EXTRA_DIST= cairo.js coverage.js format.js gettext.js jsUnit.js lang.js mainloop.js package.js signals.js
+
+gjs_extensiondir=$(datadir)/seed SEED_GTK_VERSION@/gjs
+gjs_extension_DATA = cairo.js coverage.js format.js gettext.js jsUnit.js lang.js mainloop.js package.js 
signals.js
+
+endif
\ No newline at end of file
diff --git a/modules/gjs/cairo.js b/modules/gjs/cairo.js
new file mode 100644
index 0000000..7555c3c
--- /dev/null
+++ b/modules/gjs/cairo.js
@@ -0,0 +1,147 @@
+// Copyright 2010 litl, LLC.
+//
+// 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 THE SOFTWARE.
+
+const Lang = imports.lang;
+
+const Antialias = {
+    DEFAULT: 0,
+    NONE: 1,
+    GRAY: 2,
+    SUBPIXEL: 3
+};
+
+const Content = {
+    COLOR : 0x1000,
+    ALPHA : 0x2000,
+    COLOR_ALPHA : 0x3000
+};
+
+const Extend = {
+    NONE : 0,
+    REPEAT : 1,
+    REFLECT : 2,
+    PAD : 3
+};
+
+const FillRule = {
+    WINDING: 0,
+    EVEN_ODD: 1
+};
+
+const Filter = {
+    FAST : 0,
+    GOOD : 1,
+    BEST : 2,
+    NEAREST : 3,
+    BILINEAR : 4,
+    GAUSSIAN : 5
+};
+
+const FontSlant = {
+    NORMAL: 0,
+    ITALIC: 1,
+    OBLIQUE: 2
+};
+
+const FontWeight = {
+    NORMAL : 0,
+    BOLD : 1
+};
+
+const Format = {
+    ARGB32 : 0,
+    RGB24 : 1,
+    A8 : 2,
+    A1 : 3,
+    // The value of 4 is reserved by a deprecated enum value
+    RGB16_565: 5
+};
+
+const LineCap = {
+    BUTT: 0,
+    ROUND: 1,
+    SQUASH: 2
+};
+
+const LineJoin = {
+    MITER: 0,
+    ROUND: 1,
+    BEVEL: 2
+};
+
+const Operator = {
+    CLEAR: 0,
+    SOURCE: 1,
+    OVER: 2,
+    IN : 3,
+    OUT : 4,
+    ATOP : 5,
+    DEST : 6,
+    DEST_OVER : 7,
+    DEST_IN : 8,
+    DEST_OUT : 9,
+    DEST_ATOP : 10,
+    XOR : 11,
+    ADD : 12,
+    SATURATE : 13,
+    MULTIPLY : 14,
+    SCREEN : 15,
+    OVERLAY : 16,
+    DARKEN : 17,
+    LIGHTEN : 18,
+    COLOR_DODGE : 19,
+    COLOR_BURN : 20,
+    HARD_LIGHT : 21,
+    SOFT_LIGHT : 22,
+    DIFFERENCE : 23,
+    EXCLUSION : 24,
+    HSL_HUE : 25,
+    HSL_SATURATION : 26,
+    HSL_COLOR : 27,
+    HSL_LUMINOSITY : 28
+};
+
+const PatternType = {
+    SOLID : 0,
+    SURFACE : 1,
+    LINEAR : 2,
+    RADIAL : 3
+};
+
+const SurfaceType = {
+    IMAGE : 0,
+    PDF : 1,
+    PS : 2,
+    XLIB : 3,
+    XCB : 4,
+    GLITZ : 5,
+    QUARTZ : 6,
+    WIN32 : 7,
+    BEOS : 8,
+    DIRECTFB : 9,
+    SVG : 10,
+    OS2 : 11,
+    WIN32_PRINTING : 12,
+    QUARTZ_IMAGE : 13
+};
+
+// Merge stuff defined in native code
+Lang.copyProperties(imports.cairoNative, this);
+
diff --git a/modules/gjs/coverage.js b/modules/gjs/coverage.js
new file mode 100644
index 0000000..1b4f2cc
--- /dev/null
+++ b/modules/gjs/coverage.js
@@ -0,0 +1,978 @@
+/*
+ * Copyright (c) 2014 Endless Mobile, Inc.
+ *
+ * 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 THE SOFTWARE.
+ *
+ * Authored By: Sam Spilsbury <sam endlessm com>
+ */
+
+function getSubNodesForNode(node) {
+    let subNodes = [];
+    switch (node.type) {
+    /* These statements have a single body */
+    case 'LabelledStatement':
+    case 'WithStatement':
+    case 'FunctionDeclaration':
+    case 'FunctionExpression':
+    case 'CatchClause':
+        subNodes.push(node.body);
+        break;
+    case 'LetStatement':
+        Array.prototype.push.apply(subNodes, node.head);
+        subNodes.push(node.body);
+        break;
+    case 'WhileStatement':
+    case 'DoWhileStatement':
+        subNodes.push(node.body);
+        subNodes.push(node.test);
+        break;
+    case 'ForStatement':
+        if (node.init !== null)
+            subNodes.push(node.init);
+        if (node.test !== null)
+            subNodes.push(node.test);
+        if (node.update !== null)
+            subNodes.push(node.update);
+
+        subNodes.push(node.body);
+        break;
+    case 'ForInStatement':
+        if (node.each)
+            subNodes.push(node.left);
+
+        subNodes.push(node.right, node.body);
+        break;
+    case 'ForOfStatement':
+        subNodes.push(node.left, node.right, node.body);
+        break;
+    case 'BlockStatement':
+        Array.prototype.push.apply(subNodes, node.body);
+        break;
+    case 'ThrowStatement':
+    case 'ReturnStatement':
+    case 'YieldExpression':
+        if (node.argument !== null)
+            subNodes.push(node.argument);
+        break;
+    case 'ExpressionStatement':
+        subNodes.push(node.expression);
+        break;
+    case 'AssignmentExpression':
+    case 'BinaryExpression':
+    case 'LogicalExpression':
+        subNodes.push(node.left, node.right);
+        break;
+    case 'ConditionalExpression':
+        subNodes.push(node.test, node.consequent, node.alternate);
+        break;
+    case 'ObjectExpression':
+        node.properties.forEach(function(prop) {
+            subNodes.push(prop.value);
+        });
+        break;
+    case 'ArrayExpression':
+        node.elements.forEach(function(elem) {
+            if (elem !== null)
+                subNodes.push(elem);
+        });
+        break;
+    case 'ArrowExpression':
+        Array.prototype.push.apply(subNodes, node.defaults);
+        subNodes.push(node.body);
+        break;
+    case 'SequenceExpression':
+        Array.prototype.push.apply(subNodes, node.expressions);
+        break;
+    case 'UnaryExpression':
+    case 'UpdateExpression':
+        subNodes.push(node.argument);
+        break;
+    case 'ComprehensionExpression':
+    case 'GeneratorExpression':
+        subNodes.push(node.body);
+        Array.prototype.push.apply(subNodes, node.blocks);
+        if (node.filter !== null)
+            subNodes.push(node.filter);
+        break;
+    case 'ComprehensionBlock':
+        subNodes.push(node.right);
+        break;
+    /* It is very possible that there might be something
+     * interesting in the function arguments, so we need to
+     * walk them too */
+    case 'NewExpression':
+    case 'CallExpression':
+        Array.prototype.push.apply(subNodes, node.arguments);
+        subNodes.push(node.callee);
+        break;
+    /* These statements might have multiple different bodies
+     * depending on whether or not they were entered */
+    case 'IfStatement':
+        subNodes = [node.test, node.consequent];
+        if (node.alternate !== null)
+            subNodes.push(node.alternate);
+        break;
+    case 'TryStatement':
+        subNodes = [node.block];
+        if (node.handler !== null)
+            subNodes.push(node.handler);
+        if (node.finalizer !== null)
+            subNodes.push(node.finalizer);
+        break;
+    case 'SwitchStatement':
+        for (let caseClause of node.cases) {
+            caseClause.consequent.forEach(function(expression) {
+                subNodes.push(expression);
+            });
+        }
+        break;
+    case 'VariableDeclaration':
+        Array.prototype.push.apply(subNodes, node.declarations);
+        break;
+    case 'VariableDeclarator':
+        if (node.init !== null)
+            subNodes.push(node.init);
+        break;
+    case 'MemberExpression':
+        subNodes.push(node.object);
+        if (node.computed)
+            subNodes.push(node.property);
+
+        break;
+    }
+
+    return subNodes;
+}
+
+function collectForSubNodes(subNodes, collector) {
+    let result = [];
+    if (subNodes !== undefined &&
+        subNodes.length > 0) {
+
+        subNodes.forEach(function(node) {
+            let nodeResult = collector(node);
+            if (nodeResult !== undefined)
+                Array.prototype.push.apply(result, nodeResult);
+
+            let subNodeResults = collectForSubNodes(getSubNodesForNode(node),
+                                                    collector);
+
+            Array.prototype.push.apply(result, subNodeResults);
+        });
+    }
+
+    return result;
+}
+
+function _getFunctionKeyFromReflectedFunction(node) {
+    let name = node.id !== null ? node.id.name : '(anonymous)';
+    let line = node.loc.start.line;
+    let n_params = node.params.length;
+
+    return [name, line, n_params].join(':');
+}
+
+/* Unfortunately, the Reflect API doesn't give us enough information to
+ * uniquely identify a function. A function might be anonymous, in which
+ * case the JS engine uses some heurisitics to get a unique string identifier
+ * but that isn't available to us here.
+ *
+ * There's also the edge-case where functions with the same name might be
+ * defined within the same scope, or multiple anonymous functions might
+ * be defined on the same line. In that case, it will look like we entered
+ * the same function multiple times since we can't get column information
+ * from the engine-side.
+ *
+ * For instance:
+ *
+ * 1. function f() {
+ *       function f() {
+ *       }
+ *    }
+ *
+ * 2. let a = function() { function(a, b) {} };
+ *
+ * 3. let a = function() { function () {} }
+ *
+ * We can work-around case 1 by using the line numbers to get a unique identifier.
+ * We can work-around case 2 by using the arguments length to get a unique identifier
+ * We can't work-around case 3. The best thing we can do is warn that coverage
+ * reports might be inaccurate as a result */
+function functionsForNode(node) {
+    let functionNames = [];
+    switch (node.type) {
+    case 'FunctionDeclaration':
+    case 'FunctionExpression':
+    case 'ArrowExpression':
+        functionNames.push({ key: _getFunctionKeyFromReflectedFunction(node),
+                             line: node.loc.start.line,
+                             n_params: node.params.length });
+    }
+
+    return functionNames;
+}
+
+function functionsForAST(ast) {
+    return collectForSubNodes(ast.body, functionsForNode);
+}
+
+/* If a branch' consequent is a block statement, there's
+ * a chance that it could start on the same line, although
+ * that's not where execution really starts. If it is
+ * a block statement then handle the case and go
+ * to the first line where execution starts */
+function getBranchExitStartLine(branchBodyNode) {
+    switch (branchBodyNode.type) {
+    case 'BlockStatement':
+        /* Hit a block statement, but nothing inside, can never
+         * be executed, tell the upper level to move on to the next
+         * statement */
+        if (branchBodyNode.body.length === 0)
+            return -1;
+
+        /* Handle the case where we have nested block statements
+         * that never actually get to executable code by handling
+         * all statements within a block */
+        for (let statement of branchBodyNode.body) {
+            let startLine = getBranchExitStartLine(statement);
+            if (startLine !== -1)
+                return startLine;
+        }
+
+        /* Couldn't find an executable line inside this block */
+        return -1;
+
+    case 'SwitchCase':
+        /* Hit a switch, but nothing inside, can never
+         * be executed, tell the upper level to move on to the next
+         * statement */
+        if (branchBodyNode.consequent.length === 0)
+            return -1;
+
+        /* Handle the case where we have nested block statements
+         * that never actually get to executable code by handling
+         * all statements within a block */
+        for (let statement of branchBodyNode.consequent) {
+            let startLine = getBranchExitStartLine(statement);
+            if (startLine !== -1) {
+                return startLine;
+            }
+        }
+
+        /* Couldn't find an executable line inside this block */
+        return -1;
+    /* These types of statements are never executable */
+    case 'EmptyStatement':
+    case 'LabelledStatement':
+        return -1;
+    default:
+        break;
+    }
+
+    return branchBodyNode.loc.start.line;
+}
+
+function branchesForNode(node) {
+    let branches = [];
+
+    let branchExitNodes = [];
+    switch (node.type) {
+    case 'IfStatement':
+        branchExitNodes.push(node.consequent);
+        if (node.alternate !== null)
+            branchExitNodes.push(node.alternate);
+        break;
+    case 'WhileStatement':
+    case 'DoWhileStatement':
+        branchExitNodes.push(node.body);
+        break;
+    case 'SwitchStatement':
+
+        /* The case clauses by themselves are never executable
+         * so find the actual exits */
+        Array.prototype.push.apply(branchExitNodes, node.cases);
+        break;
+    default:
+        break;
+    }
+
+    let branchExitStartLines = branchExitNodes.map(getBranchExitStartLine);
+    branchExitStartLines = branchExitStartLines.filter(function(line) {
+        return line !== -1;
+    });
+
+    /* Branch must have at least one exit */
+    if (branchExitStartLines.length) {
+        branches.push({ point: node.loc.start.line,
+                        exits: branchExitStartLines });
+    }
+
+    return branches;
+}
+
+function branchesForAST(ast) {
+    return collectForSubNodes(ast.body, branchesForNode);
+}
+
+function expressionLinesForNode(statement) {
+    let expressionLines = [];
+
+    let expressionNodeTypes = ['Expression',
+                               'Declaration',
+                               'Statement',
+                               'Clause',
+                               'Literal',
+                               'Identifier'];
+
+    if (expressionNodeTypes.some(function(type) {
+            return statement.type.indexOf(type) !== -1;
+        })) {
+
+        /* These expressions aren't executable on their own */
+        switch (statement.type) {
+        case 'FunctionDeclaration':
+        case 'LiteralExpression':
+            break;
+        /* Perplexingly, an empty block statement is actually executable,
+         * push it if it is */
+        case 'BlockStatement':
+            if (statement.body.length !== 0)
+                break;
+            expressionLines.push(statement.loc.start.line);
+            break;
+        default:
+            expressionLines.push(statement.loc.start.line);
+            break;
+        }
+    }
+
+    return expressionLines;
+}
+
+function deduplicate(list) {
+    return list.filter(function(elem, pos, self) {
+        return self.indexOf(elem) === pos;
+    });
+}
+
+function expressionLinesForAST(ast) {
+    let allExpressions = collectForSubNodes(ast.body, expressionLinesForNode);
+    allExpressions = deduplicate(allExpressions);
+
+    return allExpressions;
+}
+
+function _getNumberOfLinesForScript(scriptContents) {
+    let scriptLines = scriptContents.split("\n");
+    let scriptLineCount = scriptLines.length;
+
+    return scriptLineCount;
+}
+
+/*
+ * The created array is a 1-1 representation of the hitcount in the filename. Each
+ * element refers to an individual line. In order to avoid confusion, our array
+ * is zero indexed, but the zero'th line is always ignored and the first element
+ * refers to the first line of the file.
+ *
+ * A value of undefined for an element means that the line is non-executable and never actually
+ * reached. A value of 0 means that it was executable but never reached. A positive value
+ * indicates the hit count.
+ *
+ * We care about non-executable lines because we don't want to report coverage misses for
+ * lines that could have never been executed anyways.
+ *
+ * The reason for using a 1-1 mapping as opposed to an array of key-value pairs for executable
+ * lines is:
+ *   1. Lookup speed is O(1) instead of O(log(n))
+ *   2. There's a possibility we might hit a line which we thought was non-executable, in which
+ *      case we can neatly handle the error by marking that line executable. A hit on a line
+ *      we thought was non-executable is not as much of a problem as noise generated by
+ *      ostensible "misses" which could in fact never be executed.
+ *
+ */
+function _expressionLinesToCounters(expressionLines, nLines) {
+    expressionLines.sort(function(left, right) { return left - right; });
+
+    let expressionLinesIndex = 0;
+    let counters = new Array(nLines + 1);
+
+    if (expressionLines.length === 0)
+        return counters;
+
+    for (let i = 1; i < counters.length; i++) {
+        if (expressionLines[expressionLinesIndex] == i) {
+            counters[i] = 0;
+            expressionLinesIndex++;
+        }
+    }
+
+    return counters;
+}
+
+/* As above, we are creating a 1-1 representation of script lines to potential branches
+ * where each element refers to a 1-index line (with the zero'th ignored).
+ *
+ * Each element is a GjsCoverageBranchData which, if the line at the element
+ * position describes a branch, will be populated with a GjsReflectedScriptBranchInfo
+ * and an array of unsigned each specifying the hit-count for each potential branch
+ * in the branch info */
+function _branchesToBranchCounters(branches, nLines) {
+    branches.sort(function(left, right) {
+        return left.point - right.point;
+    });
+
+    let branchIndex = 0;
+    let counters = new Array(nLines + 1);
+
+    if (branches.length === 0)
+        return counters;
+
+    for (let i = 1; i < counters.length; i++) {
+        let branch = branches[branchIndex];
+        let branchPoint = branch.point;
+
+        if (branchPoint == i) {
+            counters[i] = {
+                point: branchPoint,
+                exits: branch.exits.map(function(exit) {
+                    return {
+                        line: exit,
+                        hitCount: 0
+                    };
+                }),
+                lastExit: (function() {
+                    let lastExitLine = 0;
+                    for (let exit of branch.exits) {
+                        if (lastExitLine < exit)
+                            lastExitLine = exit;
+                    }
+
+                    return lastExitLine;
+                })(),
+                hit: false
+            };
+
+            if (++branchIndex >= branches.length)
+                break;
+        }
+    }
+
+    return counters;
+}
+
+function _functionsToFunctionCounters(script, functions) {
+    let functionCounters = {};
+
+    functions.forEach(function(func) {
+        let [name, line, args] = func.key.split(':');
+
+        if (functionCounters[name] === undefined) {
+            functionCounters[name] = {};
+        }
+
+        if (functionCounters[name][line] === undefined) {
+            functionCounters[name][line] = {};
+        }
+
+        if (functionCounters[name][line][args] === undefined) {
+            functionCounters[name][line][args] = { hitCount: 0 };
+        } else {
+            log(script + ':' + line + ' Function identified as ' +
+                func.key + ' already seen in this file. Function coverage ' +
+                'will be incomplete.');
+        }
+    });
+
+    return functionCounters;
+}
+
+function _populateKnownFunctions(functions, nLines) {
+    let knownFunctions = new Array(nLines + 1);
+
+    functions.forEach(function(func) {
+        knownFunctions[func.line] = true;
+    });
+
+    return knownFunctions;
+}
+
+function _identifyFunctionCounterInLinePartForDescription(linePart,
+                                                          nArgs) {
+    /* There is only one potential option for this line. We might have been
+     * called with the wrong number of arguments, but it doesn't matter. */
+    if (Object.getOwnPropertyNames(linePart).length === 1)
+        return linePart[Object.getOwnPropertyNames(linePart)[0]];
+
+    /* Have to disambiguate using nArgs and we have an exact match. */
+    if (linePart[nArgs] !== undefined)
+        return linePart[nArgs];
+
+    /* There are multiple options on this line. Pick the one where
+     * we satisfy the number of arguments exactly, or failing that,
+     * go through each and pick the closest. */
+    let allNArgsOptions = Object.keys(linePart).map(function(key) {
+        return parseInt(key);
+    });
+
+    let closest = allNArgsOptions.reduce(function(prev, current, index, array) {
+        let nArgsOption = array[index];
+        if (Math.abs(nArgsOption - nArgs) < Math.abs(current - nArgs)) {
+            return nArgsOption;
+        }
+
+        return current;
+    });
+
+    return linePart[String(closest)];
+}
+
+function _identifyFunctionCounterForDescription(functionCounters,
+                                                name,
+                                                line,
+                                                nArgs) {
+    let candidateCounter = functionCounters[name];
+
+    if (candidateCounter === undefined)
+        return null;
+
+    if (Object.getOwnPropertyNames(candidateCounter).length === 1) {
+        let linePart = candidateCounter[Object.getOwnPropertyNames(candidateCounter)[0]];
+        return _identifyFunctionCounterInLinePartForDescription(linePart, nArgs);
+    }
+
+    let linePart = functionCounters[name][line];
+
+    if (linePart === undefined) {
+        return null;
+    }
+
+    return _identifyFunctionCounterInLinePartForDescription(linePart, nArgs);
+}
+
+/**
+ * _incrementFunctionCounters
+ *
+ * functionCounters: An object which is a key-value pair with the following schema:
+ * {
+ *      "key" : { line, hitCount }
+ * }
+ * linesWithKnownFunctions: An array of either "true" or undefined, with true set to
+ * each element corresponding to a line that we know has a function on it.
+ * name: The name of the function or "(anonymous)" if it has no name
+ * line: The line at which execution first started on this function.
+ * nArgs: The number of arguments this function has.
+ */
+function _incrementFunctionCounters(functionCounters,
+                                    linesWithKnownFunctions,
+                                    name,
+                                    line,
+                                    nArgs) {
+    let functionCountersForKey = _identifyFunctionCounterForDescription(functionCounters,
+                                                                        name,
+                                                                        line,
+                                                                        nArgs);
+
+    /* Its possible that the JS Engine might enter a funciton
+     * at an executable line which is a little bit past the
+     * actual definition. Roll backwards until we reach the
+     * last known function definition line which we kept
+     * track of earlier to see if we can find this function first */
+    if (functionCountersForKey === null) {
+        do {
+            --line;
+            functionCountersForKey = _identifyFunctionCounterForDescription(functionCounters,
+                                                                            name,
+                                                                            line,
+                                                                            nArgs);
+        } while (linesWithKnownFunctions[line] !== true && line > 0);
+    }
+
+    if (functionCountersForKey !== null) {
+        functionCountersForKey.hitCount++;
+    } else {
+        let functionKey = [name, line, nArgs].join(':');
+        throw new Error("expected Reflect to find function " + functionKey);
+    }
+}
+
+/**
+ * _incrementExpressionCounters
+ *
+ * expressonCounters: An array of either a hit count for a found
+ * executable line or undefined for a known non-executable line.
+ * line: an executed line
+ */
+function _incrementExpressionCounters(expressionCounters,
+                                      script,
+                                      offsetLine) {
+    let expressionCountersLen = expressionCounters.length;
+
+    if (offsetLine >= expressionCountersLen)
+        throw new Error("Executed line " + offsetLine + " which was past the highest-found line " + 
expressionCountersLen);
+
+    /* If this happens it is not a huge problem - though it does
+     * mean that the reflection machinery is not doing its job, so we should
+     * print a debug message about it in case someone is interested.
+     *
+     * The reason why we don't have a proper log is because it
+     * is difficult to determine what the SpiderMonkey program counter
+     * will actually pass over, especially function declarations for some
+     * reason:
+     *
+     *     function f(a,b) {
+     *         a = 1;
+     *     }
+     *
+     * In some cases, the declaration itself will be executed
+     * but in other cases it won't be. Reflect.parse tells us that
+     * the only two expressions on that line are a FunctionDeclaration
+     * and BlockStatement, neither of which would ordinarily be
+     * executed */
+    if (expressionCounters[offsetLine] === undefined) {
+        log(script + ':' + offsetLine + ' Executed line previously marked ' +
+            'non-executable by Reflect');
+        expressionCounters[offsetLine] = 0;
+    }
+
+    expressionCounters[offsetLine]++;
+}
+
+function _BranchTracker(branchCounters) {
+    this._branchCounters = branchCounters;
+    this._activeBranch = undefined;
+
+    this.incrementBranchCounters = function(offsetLine) {
+        /* Set branch exits or find a new active branch */
+        let activeBranch = this._activeBranch;
+        if (activeBranch !== undefined) {
+            activeBranch.exits.forEach(function(exit) {
+                if (exit.line === offsetLine) {
+                    exit.hitCount++;
+                }
+            });
+
+            /* Only set the active branch to undefined once we're
+             * completely outside of it, since we might be in a case statement where
+             * we need to check every possible option before jumping to an
+             * exit */
+            if (offsetLine >= activeBranch.lastExit)
+                this._activeBranch = undefined;
+        }
+
+        let nextActiveBranch = branchCounters[offsetLine];
+        if (nextActiveBranch !== undefined) {
+            this._activeBranch = nextActiveBranch;
+            this._activeBranch.hit = true;
+        }
+    };
+}
+
+function _convertFunctionCountersToArray(functionCounters) {
+    let arrayReturn = [];
+    /* functionCounters is an object so explore it to create a
+     * set of function keys and then convert it to
+     * an array-of-object using the key as a property
+     * of that object */
+    for (let name of Object.getOwnPropertyNames(functionCounters)) {
+        let namePart = functionCounters[name];
+
+        for (let line of Object.getOwnPropertyNames(namePart)) {
+            let linePart = functionCounters[name][line];
+
+            for (let nArgs of Object.getOwnPropertyNames(linePart)) {
+                let functionKey = [name, line, nArgs].join(':');
+                arrayReturn.push({ name: functionKey,
+                                   line: Number(line),
+                                   nArgs: nArgs,
+                                   hitCount: linePart[nArgs].hitCount });
+            }
+        }
+    }
+
+    arrayReturn.sort(function(left, right) {
+        if (left.name < right.name)
+            return -1;
+        else if (left.name > right.name)
+            return 1;
+        else
+            return 0;
+    });
+    return arrayReturn;
+}
+
+/* Looks up filename in cache and fetches statistics
+ * directly from the cache */
+function _fetchCountersFromCache(filename, cache, nLines) {
+    if (!cache)
+        return null;
+
+    if (Object.keys(cache).indexOf(filename) !== -1) {
+        let cache_for_file = cache[filename];
+
+        if (cache_for_file.mtime) {
+             let mtime = getFileModificationTime(filename);
+             if (mtime[0] != cache[filename].mtime[0] ||
+                 mtime[1] != cache[filename].mtime[1])
+                 return null;
+        } else {
+            let checksum = getFileChecksum(filename);
+            if (checksum != cache[filename].checksum)
+                return null;
+        }
+
+        let functions = cache_for_file.functions;
+
+        return {
+            expressionCounters: _expressionLinesToCounters(cache_for_file.lines, nLines),
+            branchCounters: _branchesToBranchCounters(cache_for_file.branches, nLines),
+            functionCounters: _functionsToFunctionCounters(filename, functions),
+            linesWithKnownFunctions: _populateKnownFunctions(functions, nLines),
+            nLines: nLines
+        };
+    }
+
+    return null;
+}
+
+function _fetchCountersFromReflection(filename, contents, nLines) {
+    let reflection = Reflect.parse(contents);
+    let functions = functionsForAST(reflection);
+
+    return {
+        expressionCounters: _expressionLinesToCounters(expressionLinesForAST(reflection), nLines),
+        branchCounters: _branchesToBranchCounters(branchesForAST(reflection), nLines),
+        functionCounters: _functionsToFunctionCounters(filename, functions),
+        linesWithKnownFunctions: _populateKnownFunctions(functions, nLines),
+        nLines: nLines
+    };
+}
+
+function CoverageStatisticsContainer(prefixes, cache) {
+    /* Copy the files array, so that it can be re-used in the tests */
+    let cachedASTs = cache !== undefined ? JSON.parse(cache) : null;
+    let coveredFiles = {};
+    let cacheMisses = 0;
+
+    function wantsStatisticsFor(filename) {
+        return prefixes.some(function(prefix) {
+            return filename.startsWith(prefix);
+        });
+    }
+
+    function createStatisticsFor(filename) {
+        let contents = getFileContents(filename);
+        let nLines = _getNumberOfLinesForScript(contents);
+
+        let counters = _fetchCountersFromCache(filename, cachedASTs, nLines);
+        if (counters === null) {
+            cacheMisses++;
+            counters = _fetchCountersFromReflection(filename, contents, nLines);
+        }
+
+        if (counters === null)
+            throw new Error('Failed to parse and reflect file ' + filename);
+
+        /* Set contents here as we don't pass it to _fetchCountersFromCache. */
+        counters.contents = contents;
+
+        return counters;
+    }
+
+    function ensureStatisticsFor(filename) {
+        let wantStatistics = wantsStatisticsFor(filename);
+        let haveStatistics = !!coveredFiles[filename];
+
+        if (wantStatistics && !haveStatistics)
+            coveredFiles[filename] = createStatisticsFor(filename);
+        return coveredFiles[filename];
+    }
+
+    this.stringify = function() {
+        let cache_data = {};
+        Object.keys(coveredFiles).forEach(function(filename) {
+            let statisticsForFilename = coveredFiles[filename];
+            let mtime = getFileModificationTime(filename);
+            let cacheDataForFilename = {
+                mtime: mtime,
+                checksum: mtime === null ? getFileChecksum(filename) : null,
+                lines: [],
+                branches: [],
+                functions: 
_convertFunctionCountersToArray(statisticsForFilename.functionCounters).map(function(func) {
+                    return {
+                        key: func.name,
+                        line: func.line
+                    };
+                })
+            };
+
+            /* We're using a index based loop here since we need access to the
+             * index, since it actually represents the current line number
+             * on the file (see _expressionLinesToCounters). */
+            for (let line_index = 0;
+                 line_index < statisticsForFilename.expressionCounters.length;
+                 ++line_index) {
+                 if (statisticsForFilename.expressionCounters[line_index] !== undefined)
+                     cacheDataForFilename.lines.push(line_index);
+
+                 if (statisticsForFilename.branchCounters[line_index] !== undefined) {
+                     let branchCounters = statisticsForFilename.branchCounters[line_index];
+                     cacheDataForFilename.branches.push({
+                         point: statisticsForFilename.branchCounters[line_index].point,
+                         exits: statisticsForFilename.branchCounters[line_index].exits.map(function(exit) {
+                             return exit.line;
+                         })
+                     });
+                 }
+            }
+            cache_data[filename] = cacheDataForFilename;
+        });
+        return JSON.stringify(cache_data);
+    };
+
+    this.getCoveredFiles = function() {
+        return Object.keys(coveredFiles);
+    };
+
+    this.fetchStatistics = function(filename) {
+        return ensureStatisticsFor(filename);
+    };
+
+    this.staleCache = function() {
+        return cacheMisses > 0;
+    };
+
+    this.deleteStatistics = function(filename) {
+        coveredFiles[filename] = undefined;
+    };
+}
+
+/**
+ * Main class tying together the Debugger object and CoverageStatisticsContainer.
+ *
+ * It isn't poissible to unit test this class because it depends on running
+ * Debugger which in turn depends on objects injected in from another compartment */
+function CoverageStatistics(prefixes, cache) {
+    this.container = new CoverageStatisticsContainer(prefixes, cache);
+    let fetchStatistics = this.container.fetchStatistics.bind(this.container);
+    let deleteStatistics = this.container.deleteStatistics.bind(this.container);
+
+    /* 'debuggee' comes from the invocation from
+     * a separate compartment inside of coverage.cpp */
+    this.dbg = new Debugger(debuggee);
+
+    this.getCoveredFiles = function() {
+        return this.container.getCoveredFiles();
+    };
+
+    this.getNumberOfLinesFor = function(filename) {
+        return fetchStatistics(filename).nLines;
+    };
+
+    this.getExecutedLinesFor = function(filename) {
+        return fetchStatistics(filename).expressionCounters;
+    };
+
+    this.getBranchesFor = function(filename) {
+        return fetchStatistics(filename).branchCounters;
+    };
+
+    this.getFunctionsFor = function(filename) {
+        let functionCounters = fetchStatistics(filename).functionCounters;
+        return _convertFunctionCountersToArray(functionCounters);
+    };
+
+    this.dbg.onEnterFrame = function(frame) {
+        let statistics;
+
+        try {
+            statistics = fetchStatistics(frame.script.url);
+            if (!statistics) {
+                return undefined;
+            }
+        } catch (e) {
+            /* We don't care about this frame, return */
+            log(e.message + " " + e.stack);
+            return undefined;
+        }
+
+        function _logExceptionAndReset(exception, callee, line) {
+            log(exception.fileName + ":" + exception.lineNumber +
+                " (processing " + frame.script.url + ":" + callee + ":" +
+                line + ") - " + exception.message);
+            log("Will not log statistics for this file");
+            frame.onStep = undefined;
+            frame._branchTracker = undefined;
+            deleteStatistics(frame.script.url);
+        }
+
+        /* Log function calls */
+        if (frame.callee !== null && frame.callee.callable) {
+            let name = frame.callee.name ? frame.callee.name : "(anonymous)";
+            let line = frame.script.getOffsetLine(frame.offset);
+            let nArgs = frame.callee.parameterNames.length;
+
+            try {
+                _incrementFunctionCounters(statistics.functionCounters,
+                                           statistics.linesWithKnownFunctions,
+                                           name,
+                                           line,
+                                           nArgs);
+            } catch (e) {
+                /* Something bad happened. Log the exception and delete
+                 * statistics for this file */
+                _logExceptionAndReset(e, name, line);
+                return undefined;
+            }
+        }
+
+        /* Upon entering the frame, the active branch is always inactive */
+        frame._branchTracker = new _BranchTracker(statistics.branchCounters);
+
+        /* Set single-step hook */
+        frame.onStep = function() {
+            /* Line counts */
+            let offset = this.offset;
+            let offsetLine = this.script.getOffsetLine(offset);
+
+            try {
+                _incrementExpressionCounters(statistics.expressionCounters,
+                                             frame.script.url,
+                                             offsetLine);
+                this._branchTracker.incrementBranchCounters(offsetLine);
+            } catch (e) {
+                /* Something bad happened. Log the exception and delete
+                 * statistics for this file */
+                _logExceptionAndReset(e, frame.callee, offsetLine);
+            }
+        };
+
+        return undefined;
+    };
+
+    this.deactivate = function() {
+        /* This property is designed to be a one-stop-shop to
+         * disable the debugger for this debugee, without having
+         * to traverse all its scripts or frames */
+        this.dbg.enabled = false;
+    };
+
+    this.staleCache = this.container.staleCache.bind(this.container);
+    this.stringify = this.container.stringify.bind(this.container);
+}
diff --git a/modules/gjs/format.js b/modules/gjs/format.js
new file mode 100644
index 0000000..430b5d7
--- /dev/null
+++ b/modules/gjs/format.js
@@ -0,0 +1,86 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+const GjsPrivate = imports.gi.GjsPrivate;
+
+function vprintf(str, args) {
+    let i = 0;
+    let usePos = false;
+    return str.replace(/%(?:([1-9][0-9]*)\$)?(I+)?([0-9]+)?(?:\.([0-9]+))?(.)/g, function (str, posGroup, 
flagsGroup, widthGroup, precisionGroup, genericGroup) {
+        if (precisionGroup != '' && genericGroup != 'f')
+            throw new Error("Precision can only be specified for 'f'");
+
+        let hasAlternativeIntFlag = (flagsGroup.indexOf('I') != -1);
+        if (hasAlternativeIntFlag && genericGroup != 'd')
+            throw new Error("Alternative output digits can only be specfied for 'd'");
+
+        let pos = parseInt(posGroup, 10) || 0;
+        if (usePos == false && i == 0)
+            usePos = pos > 0;
+        if (usePos && pos == 0 || !usePos && pos > 0)
+            throw new Error("Numbered and unnumbered conversion specifications cannot be mixed");
+
+        let fillChar = (widthGroup[0] == '0') ? '0' : ' ';
+        let width = parseInt(widthGroup, 10) || 0;
+
+        function fillWidth(s, c, w) {
+            let fill = '';
+            for (let i = 0; i < w; i++)
+                fill += c;
+            return fill.substr(s.length) + s;
+        }
+
+        function getArg() {
+            return usePos ? args[pos - 1] : args[i++];
+        }
+
+        let s = '';
+        switch (genericGroup) {
+        case '%':
+            return '%';
+            break;
+        case 's':
+            s = String(getArg());
+            break;
+        case 'd':
+            let intV = parseInt(getArg());
+            if (hasAlternativeIntFlag)
+                s = GjsPrivate.format_int_alternative_output(intV);
+            else
+                s = intV.toString();
+            break;
+        case 'x':
+            s = parseInt(getArg()).toString(16);
+            break;
+        case 'f':
+            if (precisionGroup == '')
+                s = parseFloat(getArg()).toString();
+            else
+                s = parseFloat(getArg()).toFixed(parseInt(precisionGroup));
+            break;
+        default:
+            throw new Error('Unsupported conversion character %' + genericGroup);
+        }
+        return fillWidth(s, fillChar, width);
+    });
+}
+
+function printf() {
+    let args = Array.prototype.slice.call(arguments);
+    let fmt = args.shift();
+    print(vprintf(fmt, args));
+}
+
+/*
+ * This function is intended to extend the String object and provide
+ * an String.format API for string formatting.
+ * It has to be set up using String.prototype.format = Format.format;
+ * Usage:
+ * "somestring %s %d".format('hello', 5);
+ * It supports %s, %d, %x and %f, for %f it also support precisions like
+ * "%.2f".format(1.526). All specifiers can be prefixed with a minimum
+ * field width, e.g. "%5s".format("foo"). Unless the width is prefixed
+ * with '0', the formatted string will be padded with spaces.
+ */
+function format() {
+    return vprintf(this, arguments);
+}
diff --git a/modules/gjs/gettext.js b/modules/gjs/gettext.js
new file mode 100644
index 0000000..67c4504
--- /dev/null
+++ b/modules/gjs/gettext.js
@@ -0,0 +1,99 @@
+// Copyright 2009 Red Hat, Inc.
+//
+// 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 THE SOFTWARE.
+
+/**
+ * This module provides a convenience layer for the "gettext" family of functions,
+ * relying on GLib for the actual implementation.
+ *
+ * Usage:
+ *
+ * const Gettext = imports.gettext;
+ *
+ * Gettext.textdomain("myapp");
+ * Gettext.bindtextdomain("myapp", "/usr/share/locale");
+ *
+ * let translated = Gettext.gettext("Hello world!");
+ */
+
+const GLib = imports.gi.GLib;
+const GjsPrivate = imports.gi.GjsPrivate;
+
+const LocaleCategory = GjsPrivate.LocaleCategory;
+
+function setlocale(category, locale) {
+    return GjsPrivate.setlocale(category, locale);
+}
+
+function textdomain(domain) {
+    return GjsPrivate.textdomain(domain);
+}
+function bindtextdomain(domain, location) {
+    return GjsPrivate.bindtextdomain(domain, location);
+}
+
+function gettext(msgid) {
+    return GLib.dgettext(null, msgid);
+}
+function dgettext(domain, msgid) {
+    return GLib.dgettext(domain, msgid);
+}
+function dcgettext(domain, msgid, category) {
+    return GLib.dcgettext(domain, msgid, category);
+}
+
+function ngettext(msgid1, msgid2, n) {
+    return GLib.dngettext(null, msgid1, msgid2, n);
+}
+function dngettext(domain, msgid1, msgid2, n) {
+    return GLib.dngettext(domain, msgid1, msgid2, n);
+}
+// FIXME: missing dcngettext ?
+
+function pgettext(context, msgid) {
+    return GLib.dpgettext2(null, context, msgid);
+}
+function dpgettext(domain, context, msgid) {
+    return GLib.dpgettext2(domain, context, msgid);
+}
+
+/**
+ * Create an object with bindings for gettext, ngettext,
+ * and pgettext bound to a particular translation domain.
+ *
+ * @param domainName Translation domain string
+ * @returns: an object with gettext bindings
+ * @type: function
+ */
+var domain = function(domainName) {
+    return {
+        gettext: function(msgid) {
+            return GLib.dgettext(domainName, msgid);
+        },
+
+        ngettext: function(msgid1, msgid2, n) {
+            return GLib.dngettext(domainName, msgid1, msgid2, n);
+        },
+
+        pgettext: function(context, msgid) {
+            return GLib.dpgettext2(domainName, context, msgid);
+        }
+    };
+};
+
diff --git a/modules/gjs/jsUnit.js b/modules/gjs/jsUnit.js
new file mode 100644
index 0000000..93f5eef
--- /dev/null
+++ b/modules/gjs/jsUnit.js
@@ -0,0 +1,473 @@
+/* @author Edward Hieatt, edward jsunit net */
+
+/*
+ - JsUnit
+ - Copyright (C) 2001-4 Edward Hieatt, edward jsunit net
+ - Copyright (C) 2008 litl, LLC
+ -
+ - Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ -
+ - The contents of this file are subject to the Mozilla Public License Version
+ - 1.1 (the "License"); you may not use this file except in compliance with
+ - the License. You may obtain a copy of the License at
+ - http://www.mozilla.org/MPL/
+ -
+ - Software distributed under the License is distributed on an "AS IS" basis,
+ - WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ - for the specific language governing rights and limitations under the
+ - License.
+ -
+ - The Original Code is Edward Hieatt code.
+ -
+ - The Initial Developer of the Original Code is
+ - Edward Hieatt, edward jsunit net 
+ - Portions created by the Initial Developer are Copyright (C) 2003
+ - the Initial Developer. All Rights Reserved.
+ -
+ - Author Edward Hieatt, edward jsunit net
+ -
+ - Alternatively, the contents of this file may be used under the terms of
+ - either the GNU General Public License Version 2 or later (the "GPL"), or
+ - the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ - in which case the provisions of the GPL or the LGPL are applicable instead
+ - of those above. If you wish to allow use of your version of this file only
+ - under the terms of either the GPL or the LGPL, and not to allow others to
+ - use your version of this file under the terms of the MPL, indicate your
+ - decision by deleting the provisions above and replace them with the notice
+ - and other provisions required by the LGPL or the GPL. If you do not delete
+ - the provisions above, a recipient may use your version of this file under
+ - the terms of any one of the MPL, the GPL or the LGPL.
+*/
+
+var JSUNIT_UNDEFINED_VALUE;
+var JSUNIT_VERSION="2.1";
+var isTestPageLoaded = false;
+
+// GJS: introduce implicit variable to avoid exceptions
+var top = null;
+
+//hack for NS62 bug
+function jsUnitFixTop() {
+  var tempTop = top;
+  if (!tempTop) {
+    tempTop = window;
+    while (tempTop.parent) {
+      tempTop = tempTop.parent;
+      if (tempTop.top && tempTop.top.jsUnitTestSuite) {
+        tempTop = tempTop.top;
+        break;
+      }
+    }
+  }
+  top = tempTop;
+}
+
+jsUnitFixTop();
+
+function _displayStringForValue(aVar) {
+  if (aVar === null)
+    return 'null';
+
+  if (aVar === top.JSUNIT_UNDEFINED_VALUE)
+    return 'undefined';
+
+  return aVar;
+}
+
+function fail(failureMessage) {
+    throw new JsUnitException(null, failureMessage);
+}
+
+function error(errorMessage) {
+    throw new Error(errorMessage);
+}
+
+function argumentsIncludeComments(expectedNumberOfNonCommentArgs, args) {
+  return args.length == expectedNumberOfNonCommentArgs + 1;
+}
+
+function commentArg(expectedNumberOfNonCommentArgs, args) {
+  if (argumentsIncludeComments(expectedNumberOfNonCommentArgs, args))
+    return args[0];
+
+  return null;
+}
+
+function nonCommentArg(desiredNonCommentArgIndex, expectedNumberOfNonCommentArgs, args) {
+  return argumentsIncludeComments(expectedNumberOfNonCommentArgs, args) ?
+    args[desiredNonCommentArgIndex] :
+    args[desiredNonCommentArgIndex - 1];
+}
+
+function _validateArguments(expectedNumberOfNonCommentArgs, args) {
+  if (!( args.length == expectedNumberOfNonCommentArgs ||
+        (args.length == expectedNumberOfNonCommentArgs + 1 && typeof(args[0]) == 'string') ))
+    error('Incorrect arguments passed to assert function');
+}
+
+function _assert(comment, booleanValue, failureMessage) {
+  if (!booleanValue)
+    throw new JsUnitException(comment, failureMessage);
+}
+
+function assert() {
+  _validateArguments(1, arguments);
+  var booleanValue=nonCommentArg(1, 1, arguments);
+
+  if (typeof(booleanValue) != 'boolean')
+    error('Bad argument to assert(boolean)');
+
+  _assert(commentArg(1, arguments), booleanValue === true, 'Call to assert(boolean) with false');
+}
+
+function assertTrue() {
+  _validateArguments(1, arguments);
+  var booleanValue=nonCommentArg(1, 1, arguments);
+
+  if (typeof(booleanValue) != 'boolean')
+    error('Bad argument to assertTrue(boolean)');
+
+  _assert(commentArg(1, arguments), booleanValue === true, 'Call to assertTrue(boolean) with false');
+}
+
+function assertFalse() {
+  _validateArguments(1, arguments);
+  var booleanValue=nonCommentArg(1, 1, arguments);
+
+  if (typeof(booleanValue) != 'boolean')
+    error('Bad argument to assertFalse(boolean)');
+
+  _assert(commentArg(1, arguments), booleanValue === false, 'Call to assertFalse(boolean) with true');
+}
+
+function assertEquals() {
+  _validateArguments(2, arguments);
+  var var1=nonCommentArg(1, 2, arguments);
+  var var2=nonCommentArg(2, 2, arguments);
+  _assert(commentArg(2, arguments), var1 === var2, 'Expected ' + var1 + ' (' + typeof(var1) + ') but was ' + 
_displayStringForValue(var2) + ' (' + typeof(var2) + ')');
+}
+
+function assertNotEquals() {
+  _validateArguments(2, arguments);
+  var var1=nonCommentArg(1, 2, arguments);
+  var var2=nonCommentArg(2, 2, arguments);
+  _assert(commentArg(2, arguments), var1 !== var2, 'Expected not to be ' + _displayStringForValue(var2));
+}
+
+function assertNull() {
+  _validateArguments(1, arguments);
+  var aVar=nonCommentArg(1, 1, arguments);
+  _assert(commentArg(1, arguments), aVar === null, 'Expected null but was ' + _displayStringForValue(aVar));
+}
+
+function assertNotNull() {
+  _validateArguments(1, arguments);
+  var aVar=nonCommentArg(1, 1, arguments);
+  _assert(commentArg(1, arguments), aVar !== null, 'Expected not to be null');
+}
+
+function assertUndefined() {
+  _validateArguments(1, arguments);
+  var aVar=nonCommentArg(1, 1, arguments);
+  _assert(commentArg(1, arguments), aVar === top.JSUNIT_UNDEFINED_VALUE, 'Expected undefined but was ' + 
_displayStringForValue(aVar));
+}
+
+function assertNotUndefined() {
+  _validateArguments(1, arguments);
+  var aVar=nonCommentArg(1, 1, arguments);
+  _assert(commentArg(1, arguments), aVar !== top.JSUNIT_UNDEFINED_VALUE, 'Expected not to be undefined');
+}
+
+function assertNaN() {
+  _validateArguments(1, arguments);
+  var aVar=nonCommentArg(1, 1, arguments);
+  _assert(commentArg(1, arguments), isNaN(aVar), 'Expected NaN');
+}
+
+function assertNotNaN() {
+  _validateArguments(1, arguments);
+  var aVar=nonCommentArg(1, 1, arguments);
+  _assert(commentArg(1, arguments), !isNaN(aVar), 'Expected not NaN');
+}
+
+// GJS: assertRaises(function)
+function assertRaises() {
+    _validateArguments(1, arguments);
+    var fun=nonCommentArg(1, 1, arguments);
+    var exception;
+
+    if (typeof(fun) != 'function')
+        error("Bad argument to assertRaises(function)");
+
+    var retval;
+    try {
+        retval = fun();
+    } catch (e) {
+        exception = e;
+    }
+
+    _assert(commentArg(1, arguments), exception !== top.JSUNIT_UNDEFINED_VALUE, "Call to 
assertRaises(function) did not raise an exception. Return value was " + _displayStringForValue(retval) + ' (' 
+ typeof(retval) + ')');
+}
+
+function isLoaded() {
+  return isTestPageLoaded;
+}
+
+function setUp() {
+}
+
+function tearDown() {
+}
+
+function getFunctionName(aFunction) {
+  var name = aFunction.toString().match(/function (\w*)/)[1];
+
+  if ((name == null) || (name.length == 0))
+    name = 'anonymous';
+
+  return name;
+}
+
+function parseErrorStack(excp)
+{
+  var stack = [];
+  var name;
+
+  if (!excp || !excp.stack)
+  {
+    return stack;
+  }
+
+  var stacklist = excp.stack.split('\n');
+
+  for (var i = 0; i < stacklist.length - 1; i++)
+  {
+    var framedata = stacklist[i];
+
+    name = framedata.match(/^(\w*)/)[1];
+    if (!name) {
+      name = 'anonymous';
+    }
+
+    var line = framedata.match(/(:\d+)$/)[1];
+    if (line) {
+        name += line;
+    }
+
+    stack[stack.length] = name;
+  }
+  // remove top level anonymous functions to match IE
+
+  while (stack.length && stack[stack.length - 1] == 'anonymous')
+  {
+    stack.length = stack.length - 1;
+  }
+  return stack;
+}
+
+function JsUnitException(comment, message) {
+  this.isJsUnitException = true;
+  this.comment           = comment;
+  this.message           = message;
+  this.stack             = (new Error()).stack;
+}
+
+JsUnitException.prototype = Object.create(Error.prototype, {});
+
+function warn() {
+  if (top.tracer != null)
+    top.tracer.warn(arguments[0], arguments[1]);
+}
+
+function inform() {
+  if (top.tracer != null)
+    top.tracer.inform(arguments[0], arguments[1]);
+}
+
+function info() {
+  inform(arguments[0], arguments[1]);
+}
+
+function debug() {
+  if (top.tracer != null)
+    top.tracer.debug(arguments[0], arguments[1]);
+}
+
+function setjsUnitTracer(ajsUnitTracer) {
+  top.tracer=ajsUnitTracer;
+}
+
+function trim(str) {
+  if (str == null)
+    return null;
+
+  var startingIndex = 0;
+  var endingIndex   = str.length-1;
+
+  while (str.substring(startingIndex, startingIndex+1) == ' ')
+    startingIndex++;
+
+  while (str.substring(endingIndex, endingIndex+1) == ' ')
+    endingIndex--;
+
+  if (endingIndex < startingIndex)
+    return '';
+
+  return str.substring(startingIndex, endingIndex+1);
+}
+
+function isBlank(str) {
+  return trim(str) == '';
+}
+
+// the functions push(anArray, anObject) and pop(anArray)
+// exist because the JavaScript Array.push(anObject) and Array.pop()
+// functions are not available in IE 5.0
+
+function push(anArray, anObject) {
+  anArray[anArray.length]=anObject;
+}
+function pop(anArray) {
+  if (anArray.length>=1) {
+    delete anArray[anArray.length - 1];
+    anArray.length--;
+  }
+}
+
+// safe, strict access to jsUnitParmHash
+function jsUnitGetParm(name)
+{
+  if (typeof(top.jsUnitParmHash[name]) != 'undefined')
+  {
+    return top.jsUnitParmHash[name];
+  }
+  return null;
+}
+
+if (top && typeof(top.xbDEBUG) != 'undefined' && top.xbDEBUG.on && top.testManager)
+{
+  top.xbDebugTraceObject('top.testManager.containerTestFrame', 'JSUnitException');
+  // asserts
+  top.xbDebugTraceFunction('top.testManager.containerTestFrame', '_displayStringForValue');
+  top.xbDebugTraceFunction('top.testManager.containerTestFrame', 'error');
+  top.xbDebugTraceFunction('top.testManager.containerTestFrame', 'argumentsIncludeComments');
+  top.xbDebugTraceFunction('top.testManager.containerTestFrame', 'commentArg');
+  top.xbDebugTraceFunction('top.testManager.containerTestFrame', 'nonCommentArg');
+  top.xbDebugTraceFunction('top.testManager.containerTestFrame', '_validateArguments');
+  top.xbDebugTraceFunction('top.testManager.containerTestFrame', '_assert');
+  top.xbDebugTraceFunction('top.testManager.containerTestFrame', 'assert');
+  top.xbDebugTraceFunction('top.testManager.containerTestFrame', 'assertTrue');
+  top.xbDebugTraceFunction('top.testManager.containerTestFrame', 'assertEquals');
+  top.xbDebugTraceFunction('top.testManager.containerTestFrame', 'assertNotEquals');
+  top.xbDebugTraceFunction('top.testManager.containerTestFrame', 'assertNull');
+  top.xbDebugTraceFunction('top.testManager.containerTestFrame', 'assertNotNull');
+  top.xbDebugTraceFunction('top.testManager.containerTestFrame', 'assertUndefined');
+  top.xbDebugTraceFunction('top.testManager.containerTestFrame', 'assertNotUndefined');
+  top.xbDebugTraceFunction('top.testManager.containerTestFrame', 'assertNaN');
+  top.xbDebugTraceFunction('top.testManager.containerTestFrame', 'assertNotNaN');
+  top.xbDebugTraceFunction('top.testManager.containerTestFrame', 'isLoaded');
+  top.xbDebugTraceFunction('top.testManager.containerTestFrame', 'setUp');
+  top.xbDebugTraceFunction('top.testManager.containerTestFrame', 'tearDown');
+  top.xbDebugTraceFunction('top.testManager.containerTestFrame', 'getFunctionName');
+  top.xbDebugTraceFunction('top.testManager.containerTestFrame', 'warn');
+  top.xbDebugTraceFunction('top.testManager.containerTestFrame', 'inform');
+  top.xbDebugTraceFunction('top.testManager.containerTestFrame', 'debug');
+  top.xbDebugTraceFunction('top.testManager.containerTestFrame', 'setjsUnitTracer');
+  top.xbDebugTraceFunction('top.testManager.containerTestFrame', 'trim');
+  top.xbDebugTraceFunction('top.testManager.containerTestFrame', 'isBlank');
+  top.xbDebugTraceFunction('top.testManager.containerTestFrame', 'newOnLoadEvent');
+  top.xbDebugTraceFunction('top.testManager.containerTestFrame', 'push');
+  top.xbDebugTraceFunction('top.testManager.containerTestFrame', 'pop');
+}
+
+function newOnLoadEvent() {
+  isTestPageLoaded = true;
+}
+
+function jsUnitSetOnLoad(windowRef, onloadHandler)
+{
+  var isKonqueror = navigator.userAgent.indexOf('Konqueror/') != -1 ||
+                    navigator.userAgent.indexOf('Safari/')    != -1;
+
+  if (typeof(windowRef.attachEvent) != 'undefined') {
+    // Internet Explorer, Opera
+    windowRef.attachEvent("onload", onloadHandler);
+  } else if (typeof(windowRef.addEventListener) != 'undefined' && !isKonqueror){
+    // Mozilla, Konqueror
+    // exclude Konqueror due to load issues
+    windowRef.addEventListener("load", onloadHandler, false);
+  } else if (typeof(windowRef.document.addEventListener) != 'undefined' && !isKonqueror) {
+    // DOM 2 Events
+    // exclude Mozilla, Konqueror due to load issues
+    windowRef.document.addEventListener("load", onloadHandler, false);
+  } else if (typeof(windowRef.onload) != 'undefined' && windowRef.onload) {
+    windowRef.jsunit_original_onload = windowRef.onload;
+    windowRef.onload = function() { windowRef.jsunit_original_onload(); onloadHandler(); };
+  } else {
+    // browsers that do not support windowRef.attachEvent or
+    // windowRef.addEventListener will override a page's own onload event
+    windowRef.onload=onloadHandler;
+  }
+}
+
+// GJS: comment out as isLoaded() isn't terribly useful for us
+//jsUnitSetOnLoad(window, newOnLoadEvent);
+
+// GJS: entry point to run all functions named as test*, surrounded by
+// calls to setUp() and tearDown()
+function gjstestRun(window_, setUp, tearDown) {
+  var propName;
+  var rv = 0;
+  var failures = [];
+  if (!window_) window_ = window;
+  if (!setUp) setUp = window_.setUp;
+  if (!tearDown) tearDown = window_.tearDown;
+
+  for (propName in window_) {
+    if (!propName.match(/^test\w+/))
+      continue;
+
+    var testFunction = window_[propName];
+    if (typeof(testFunction) != 'function')
+      continue;
+
+    log("running test " + propName);
+
+    setUp();
+    try {
+      testFunction();
+    } catch (e) {
+      var result = null;
+      if (typeof(e.isJsUnitException) != 'undefined' && e.isJsUnitException) {
+        result = '';
+        if (e.comment != null)
+          result += ('"' + e.comment + '"\n');
+
+        result += e.message;
+
+        if (e.stack)
+          result += '\n\nStack trace follows:\n' + e.stack;
+
+        // assertion failure, kind of expected so just log it and flag the
+        // whole test as failed
+        log(result);
+        rv = 1;
+        failures.push(propName);
+      }
+      else {
+        // unexpected error, let the shell handle it
+        throw e;
+      }
+    }
+    tearDown();
+  }
+
+  if (failures.length > 0) {
+    log(failures.length + " tests failed in this file");
+    log("Failures were: " + failures.join(", "));
+  }
+
+  // if gjstestRun() is the last call in a file, this becomes the
+  // exit code of the test program, so 0 = success, 1 = failed
+  return rv;
+}
diff --git a/modules/gjs/lang.js b/modules/gjs/lang.js
new file mode 100644
index 0000000..1852dbe
--- /dev/null
+++ b/modules/gjs/lang.js
@@ -0,0 +1,488 @@
+/* -*- mode: js; indent-tabs-mode: nil; -*- */
+// Copyright (c) 2008  litl, LLC
+//
+// 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 THE SOFTWARE.
+
+// Utilities that are "meta-language" things like manipulating object props
+
+const Gi = imports._gi;
+
+function countProperties(obj) {
+    let count = 0;
+    for (let property in obj) {
+        count += 1;
+    }
+    return count;
+}
+
+function getPropertyDescriptor(obj, property) {
+    if (obj.hasOwnProperty(property))
+        return Object.getOwnPropertyDescriptor(obj, property);
+    return getPropertyDescriptor(Object.getPrototypeOf(obj), property);
+}
+
+function _copyProperty(source, dest, property) {
+    let descriptor = getPropertyDescriptor(source, property);
+    Object.defineProperty(dest, property, descriptor);
+}
+
+function copyProperties(source, dest) {
+    for (let property in source) {
+        _copyProperty(source, dest, property);
+    }
+}
+
+function copyPublicProperties(source, dest) {
+    for (let property in source) {
+        if (typeof(property) == 'string' &&
+            property.substring(0, 1) == '_') {
+            continue;
+        } else {
+            _copyProperty(source, dest, property);
+        }
+    }
+}
+
+/**
+ * Binds obj to callback. Makes it possible to refer to "obj"
+ * using this within the callback.
+ * @param {object} obj the object to bind
+ * @param {function} callback callback to bind obj in
+ * @param arguments additional arguments to the callback
+ * @returns: a new callback
+ * @type: function
+ */
+function bind(obj, callback) {
+    if (typeof(obj) != 'object') {
+        throw new Error(
+            "first argument to Lang.bind() must be an object, not " +
+                typeof(obj));
+    }
+
+    if (typeof(callback) != 'function') {
+        throw new Error(
+            "second argument to Lang.bind() must be a function, not " +
+                typeof(callback));
+    }
+
+    // Use ES5 Function.prototype.bind, but only if not passing any bindArguments,
+    // because ES5 has them at the beginning, not at the end
+    if (arguments.length == 2)
+       return callback.bind(obj);
+
+    let me = obj;
+    let bindArguments = Array.prototype.slice.call(arguments, 2);
+
+    return function() {
+        let args = Array.prototype.slice.call(arguments);
+        args = args.concat(bindArguments);
+        return callback.apply(me, args);
+    };
+}
+
+// Class magic
+// Adapted from MooTools, MIT license
+// https://github.com/mootools/mootools-core
+
+function _Base() {
+    throw new TypeError('Cannot instantiate abstract class _Base');
+}
+
+_Base.__super__ = null;
+_Base.prototype._init = function() { };
+_Base.prototype._construct = function() {
+    this._init.apply(this, arguments);
+    return this;
+};
+_Base.prototype.__name__ = '_Base';
+_Base.prototype.toString = function() {
+    return '[object ' + this.__name__ + ']';
+};
+
+function _parent() {
+    if (!this.__caller__)
+        throw new TypeError("The method 'parent' cannot be called");
+
+    let caller = this.__caller__;
+    let name = caller._name;
+    let parent = caller._owner.__super__;
+
+    let previous = parent ? parent.prototype[name] : undefined;
+
+    if (!previous)
+        throw new TypeError("The method '" + name + "' is not on the superclass");
+
+    return previous.apply(this, arguments);
+}
+
+function _interfacePresent(required, proto) {
+    if (!proto.__interfaces__)
+        return false;
+    if (proto.__interfaces__.indexOf(required) !== -1)
+        return true;  // implemented here
+    // Might be implemented on a parent class
+    return _interfacePresent(required, proto.constructor.__super__.prototype);
+}
+
+function getMetaClass(params) {
+    if (params.MetaClass)
+        return params.MetaClass;
+
+    if (params.Extends && params.Extends.prototype.__metaclass__)
+        return params.Extends.prototype.__metaclass__;
+
+    return null;
+}
+
+function Class(params) {
+    let metaClass = getMetaClass(params);
+
+    if (metaClass && metaClass != this.constructor) {
+        // Trick to apply variadic arguments to constructors --
+        // bind the arguments into the constructor function.
+        let args = Array.prototype.slice.call(arguments);
+        let curried = Function.prototype.bind.apply(metaClass, [,].concat(args));
+        return new curried();
+    } else {
+        return this._construct.apply(this, arguments);
+    }
+}
+
+Class.__super__ = _Base;
+Class.prototype = Object.create(_Base.prototype);
+Class.prototype.constructor = Class;
+Class.prototype.__name__ = 'Class';
+
+Class.prototype.wrapFunction = function(name, meth) {
+    if (meth._origin) meth = meth._origin;
+
+    function wrapper() {
+        let prevCaller = this.__caller__;
+        this.__caller__ = wrapper;
+        let result = meth.apply(this, arguments);
+        this.__caller__ = prevCaller;
+        return result;
+    }
+
+    wrapper._origin = meth;
+    wrapper._name = name;
+    wrapper._owner = this;
+
+    return wrapper;
+}
+
+Class.prototype.toString = function() {
+    return '[object ' + this.__name__ + ' for ' + this.prototype.__name__ + ']';
+};
+
+Class.prototype._construct = function(params) {
+    if (!params.Name) {
+        throw new TypeError("Classes require an explicit 'Name' parameter.");
+    }
+    let name = params.Name;
+
+    let parent = params.Extends;
+    if (!parent)
+        parent = _Base;
+
+    let newClass;
+    if (params.Abstract) {
+        newClass = function() {
+            throw new TypeError('Cannot instantiate abstract class ' + name);
+        };
+    } else {
+        newClass = function() {
+            this.__caller__ = null;
+
+            return this._construct.apply(this, arguments);
+        };
+    }
+
+    // Since it's not possible to create a constructor with
+    // a custom [[Prototype]], we have to do this to make
+    // "newClass instanceof Class" work, and so we can inherit
+    // methods/properties of Class.prototype, like wrapFunction.
+    newClass.__proto__ = this.constructor.prototype;
+
+    newClass.__super__ = parent;
+    newClass.prototype = Object.create(parent.prototype);
+    newClass.prototype.constructor = newClass;
+
+    newClass._init.apply(newClass, arguments);
+
+    let interfaces = params.Implements || [];
+    // If the parent already implements an interface, then we do too
+    if (parent instanceof Class)
+        interfaces = interfaces.filter((iface) => !parent.implements(iface));
+
+    Object.defineProperties(newClass.prototype, {
+        '__metaclass__': { writable: false,
+                           configurable: false,
+                           enumerable: false,
+                           value: this.constructor },
+        '__interfaces__': { writable: false,
+                            configurable: false,
+                            enumerable: false,
+                            value: interfaces }
+    });
+
+    interfaces.forEach((iface) => {
+        iface._check(newClass.prototype);
+    });
+
+    return newClass;
+};
+
+/**
+ * Check whether this class conforms to the interface "iface".
+ * @param {object} iface a Lang.Interface
+ * @returns: whether this class implements iface
+ * @type: boolean
+ */
+Class.prototype.implements = function (iface) {
+    if (_interfacePresent(iface, this.prototype))
+        return true;
+    if (this.__super__ instanceof Class)
+        return this.__super__.implements(iface);
+    return false;
+};
+
+Class.prototype._init = function(params) {
+    let name = params.Name;
+
+    let propertyObj = { };
+
+    let interfaces = params.Implements || [];
+    interfaces.forEach((iface) => {
+        Object.getOwnPropertyNames(iface.prototype)
+        .filter((name) => !name.startsWith('__') && name !== 'constructor')
+        .forEach((name) => {
+            let descriptor = Object.getOwnPropertyDescriptor(iface.prototype,
+                name);
+            // writable and enumerable are inherited, see note below
+            descriptor.configurable = false;
+            propertyObj[name] = descriptor;
+        });
+    });
+
+    Object.getOwnPropertyNames(params).forEach(function(name) {
+        if (['Name', 'Extends', 'Abstract', 'Implements'].indexOf(name) !== -1)
+            return;
+
+        let descriptor = Object.getOwnPropertyDescriptor(params, name);
+
+        if (typeof descriptor.value === 'function')
+            descriptor.value = this.wrapFunction(name, descriptor.value);
+
+        // we inherit writable and enumerable from the property
+        // descriptor of params (they're both true if created from an
+        // object literal)
+        descriptor.configurable = false;
+
+        propertyObj[name] = descriptor;
+    }.bind(this));
+
+    Object.defineProperties(this.prototype, propertyObj);
+    Object.defineProperties(this.prototype, {
+        '__name__': { writable: false,
+                      configurable: false,
+                      enumerable: false,
+                      value: name },
+        'parent': { writable: false,
+                    configurable: false,
+                    enumerable: false,
+                    value: _parent }});
+};
+
+// This introduces the concept of a "meta-interface" which is given by the
+// MetaInterface property on an object's metaclass. For objects whose metaclass
+// is Lang.Class, the meta-interface is Lang.Interface. Subclasses of Lang.Class
+// such as GObject.Class supply their own meta-interface.
+// This is in order to enable creating GObject interfaces with Lang.Interface,
+// much as you can create GObject classes with Lang.Class.
+function _getMetaInterface(params) {
+    if (!params.Requires || params.Requires.length === 0)
+        return null;
+
+    let metaInterface = params.Requires.map((req) => {
+        if (req instanceof Interface)
+            return req.__super__;
+        for (let metaclass = req.prototype.__metaclass__; metaclass;
+            metaclass = metaclass.__super__) {
+            if (metaclass.hasOwnProperty('MetaInterface'))
+                return metaclass.MetaInterface;
+        }
+        return null;
+    })
+    .reduce((best, candidate) => {
+        // This function reduces to the "most derived" meta interface in the list.
+        if (best === null)
+            return candidate;
+        if (candidate === null)
+            return best;
+        for (let sup = candidate; sup; sup = sup.__super__) {
+            if (sup === best)
+                return candidate;
+        }
+        return best;
+    }, null);
+
+    // If we reach this point and we don't know the meta-interface, then it's
+    // most likely because there were only pure-C interfaces listed in Requires
+    // (and those don't have our magic properties.) However, all pure-C
+    // interfaces should require GObject.Object anyway.
+    if (metaInterface === null)
+        throw new Error('Did you forget to include GObject.Object in Requires?');
+
+    return metaInterface;
+}
+
+function Interface(params) {
+    let metaInterface = _getMetaInterface(params);
+    if (metaInterface && metaInterface !== this.constructor) {
+        // Trick to apply variadic arguments to constructors --
+        // bind the arguments into the constructor function.
+        let args = Array.prototype.slice.call(arguments);
+        let curried = Function.prototype.bind.apply(metaInterface, [,].concat(args));
+        return new curried();
+    }
+    return this._construct.apply(this, arguments);
+}
+
+Class.MetaInterface = Interface;
+
+/**
+ * Use this to signify a function that must be overridden in an implementation
+ * of the interface. Creating a class that doesn't override the function will
+ * throw an error.
+ */
+Interface.UNIMPLEMENTED = function () {
+    throw new Error('Not implemented');
+};
+
+Interface.__super__ = _Base;
+Interface.prototype = Object.create(_Base.prototype);
+Interface.prototype.constructor = Interface;
+Interface.prototype.__name__ = 'Interface';
+
+Interface.prototype._construct = function (params) {
+    if (!params.Name)
+        throw new TypeError("Interfaces require an explicit 'Name' parameter.");
+    let name = params.Name;
+
+    let newInterface = function () {
+        throw new TypeError('Cannot instantiate interface ' + name);
+    };
+
+    // See note in Class._construct(); this makes "newInterface instanceof
+    // Interface" work, and allows inheritance.
+    newInterface.__proto__ = this.constructor.prototype;
+
+    newInterface.__super__ = Interface;
+    newInterface.prototype = Object.create(Interface.prototype);
+    newInterface.prototype.constructor = newInterface;
+
+    newInterface._init.apply(newInterface, arguments);
+
+    Object.defineProperty(newInterface.prototype, '__metaclass__',
+                          { writable: false,
+                            configurable: false,
+                            enumerable: false,
+                            value: this.constructor });
+
+    return newInterface;
+};
+
+Interface.prototype._check = function (proto) {
+    // Check that proto implements all of this interface's required interfaces.
+    // "proto" refers to the object's prototype (which implements the interface)
+    // whereas "this.prototype" is the interface's prototype (which may still
+    // contain unimplemented methods.)
+
+    let unfulfilledReqs = this.prototype.__requires__.filter((required) => {
+        // Either the interface is not present or it is not listed before the
+        // interface that requires it or the class does not inherit it. This is
+        // so that required interfaces don't copy over properties from other
+        // interfaces that require them.
+        let interfaces = proto.__interfaces__;
+        return ((!_interfacePresent(required, proto) ||
+            interfaces.indexOf(required) > interfaces.indexOf(this)) &&
+            !(proto instanceof required));
+    }).map((required) =>
+        // __name__ is only present on GJS-created classes and will be the most
+        // accurate name. required.name will be present on introspected GObjects
+        // but is not preferred because it will be the C name. The last option
+        // is just so that we print something if there is garbage in Requires.
+        required.prototype.__name__ || required.name || required);
+    if (unfulfilledReqs.length > 0) {
+        throw new Error('The following interfaces must be implemented before ' +
+            this.prototype.__name__ + ': ' + unfulfilledReqs.join(', '));
+    }
+
+    // Check that this interface's required methods are implemented
+    let unimplementedFns = Object.getOwnPropertyNames(this.prototype)
+    .filter((p) => this.prototype[p] === Interface.UNIMPLEMENTED)
+    .filter((p) => !(p in proto) || proto[p] === Interface.UNIMPLEMENTED);
+    if (unimplementedFns.length > 0)
+        throw new Error('The following members of ' + this.prototype.__name__ +
+            ' are not implemented yet: ' + unimplementedFns.join(', '));
+};
+
+Interface.prototype.toString = function () {
+    return '[interface ' + this.__name__ + ' for ' + this.prototype.__name__ + ']';
+};
+
+Interface.prototype._init = function (params) {
+    let name = params.Name;
+
+    let propertyObj = {};
+    Object.getOwnPropertyNames(params)
+    .filter((name) => ['Name', 'Requires'].indexOf(name) === -1)
+    .forEach((name) => {
+        let descriptor = Object.getOwnPropertyDescriptor(params, name);
+
+        // Create wrappers on the interface object so that generics work (e.g.
+        // SomeInterface.some_function(this, blah) instead of
+        // SomeInterface.prototype.some_function.call(this, blah)
+        if (typeof descriptor.value === 'function') {
+            let interfaceProto = this.prototype;  // capture in closure
+            this[name] = function () {
+                return interfaceProto[name].call.apply(interfaceProto[name],
+                    arguments);
+            };
+        }
+
+        // writable and enumerable are inherited, see note in Class._init()
+        descriptor.configurable = false;
+
+        propertyObj[name] = descriptor;
+    });
+
+    Object.defineProperties(this.prototype, propertyObj);
+    Object.defineProperties(this.prototype, {
+        '__name__': { writable: false,
+                      configurable: false,
+                      enumerable: false,
+                      value: name },
+        '__requires__': { writable: false,
+                          configurable: false,
+                          enumerable: false,
+                          value: params.Requires || [] }
+    });
+};
diff --git a/modules/gjs/mainloop.js b/modules/gjs/mainloop.js
new file mode 100644
index 0000000..89c37fe
--- /dev/null
+++ b/modules/gjs/mainloop.js
@@ -0,0 +1,87 @@
+/* -*- mode: js; indent-tabs-mode: nil; -*- */
+// Copyright (c) 2012 Giovanni Campagna <scampa giovanni gmail com>
+//
+// 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 THE SOFTWARE.
+
+// A layer of convenience and backwards-compatibility over GLib MainLoop facilities
+
+const GLib = imports.gi.GLib;
+const GObject = imports.gi.GObject;
+
+var _mainLoops = {};
+
+function run(name) {
+    if (!_mainLoops[name])
+        _mainLoops[name] = GLib.MainLoop.new(null, false);
+
+    _mainLoops[name].run();
+}
+
+function quit(name) {
+    if (!_mainLoops[name])
+       throw new Error("No main loop with this id");
+
+    let loop = _mainLoops[name];
+    delete _mainLoops[name];
+
+    if (!loop.is_running())
+       throw new Error("Main loop was stopped already");
+
+    loop.quit();
+}
+
+function idle_source(handler, priority) {
+    let s = GLib.idle_source_new();
+    GObject.source_set_closure(s, handler);
+    if (priority !== undefined)
+        s.set_priority(priority);
+    return s;
+}
+
+function idle_add(handler, priority) {
+    return idle_source(handler, priority).attach(null);
+}
+
+function timeout_source(timeout, handler, priority) {
+    let s = GLib.timeout_source_new(timeout);
+    GObject.source_set_closure(s, handler);
+    if (priority !== undefined)
+        s.set_priority(priority);
+    return s;
+}
+
+function timeout_seconds_source(timeout, handler, priority) {
+    let s = GLib.timeout_source_new_seconds(timeout);
+    GObject.source_set_closure(s, handler);
+    if (priority !== undefined)
+        s.set_priority(priority);
+    return s;
+}
+
+function timeout_add(timeout, handler, priority) {
+    return timeout_source(timeout, handler, priority).attach(null);
+}
+
+function timeout_add_seconds(timeout, handler, priority) {
+    return timeout_seconds_source(timeout, handler, priority).attach(null);
+}
+
+function source_remove(id) {
+    return GLib.source_remove(id);
+}
diff --git a/modules/gjs/package.js b/modules/gjs/package.js
new file mode 100644
index 0000000..fa1d5a8
--- /dev/null
+++ b/modules/gjs/package.js
@@ -0,0 +1,247 @@
+// Copyright 2012 Giovanni Campagna
+//
+// 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 THE SOFTWARE.
+
+/**
+ * This module provides a set of convenience APIs for building packaged
+ * applications.
+ */
+
+const GLib = imports.gi.GLib;
+const GIRepository = imports.gi.GIRepository;
+const Gio = imports.gi.Gio;
+const System = imports.system;
+
+const Gettext = imports.gettext;
+
+/*< public >*/
+var name;
+var version;
+var prefix;
+var datadir;
+var libdir;
+var pkgdatadir;
+var pkglibdir;
+var moduledir;
+var localedir;
+
+/*< private >*/
+let _pkgname;
+let _base;
+
+function _findEffectiveEntryPointName() {
+    let entryPoint = System.programInvocationName;
+    while (GLib.file_test(entryPoint, GLib.FileTest.IS_SYMLINK))
+        entryPoint = GLib.file_read_link(entryPoint);
+
+    return GLib.path_get_basename(entryPoint);
+}
+
+function _runningFromSource() {
+    let binary = Gio.File.new_for_path(System.programInvocationName);
+    let sourceBinary = Gio.File.new_for_path('./src/' + name);
+    return binary.equal(sourceBinary);
+}
+
+function _makeNamePath(name) {
+    return '/' + name.replace('.', '/', 'g');
+}
+
+/**
+ * init:
+ * @params: package parameters
+ *
+ * Initialize directories and global variables. Must be called
+ * before any of other API in Package is used.
+ * @params must be an object with at least the following keys:
+ *  - name: the package name ($(PACKAGE_NAME) in autotools,
+ *          eg. org.foo.Bar)
+ *  - version: the package version
+ *  - prefix: the installation prefix
+ *
+ * init() will take care to check if the program is running from
+ * the source directory or not, by looking for a 'src' directory.
+ *
+ * At the end, the global variable 'pkg' will contain the
+ * Package module (imports.package). Additionally, the following
+ * module variables will be available:
+ *  - name: the base name of the entry point (eg. org.foo.Bar.App)
+ *  - version: same as in @params
+ *  - prefix: the installation prefix (as passed in @params)
+ *  - datadir, libdir: the final datadir and libdir when installed;
+ *                     usually, these would be prefix + '/share' and
+ *                     and prefix + '/lib' (or '/lib64')
+ *  - pkgdatadir: the directory to look for private data files, such as
+ *                images, stylesheets and UI definitions;
+ *                this will be datadir + name when installed and
+ *                './data' when running from the source tree
+ *  - pkglibdir: the directory to look for private typelibs and C
+ *               libraries;
+ *               this will be libdir + name when installed and
+ *               './lib' when running from the source tree
+ *  - moduledir: the directory to look for JS modules;
+ *               this will be pkglibdir when installed and
+ *               './src' when running from the source tree
+ *  - localedir: the directory containing gettext translation files;
+ *               this will be datadir + '/locale' when installed
+ *               and './po' in the source tree
+ *
+ * All paths are absolute and will not end with '/'.
+ *
+ * As a side effect, init() calls GLib.set_prgname().
+ */
+function init(params) {
+    window.pkg = imports.package;
+    _pkgname = params.name;
+    name = _findEffectiveEntryPointName();
+    version = params.version;
+
+    // Must call it first, because it can only be called
+    // once, and other library calls might have it as a
+    // side effect
+    GLib.set_prgname(name);
+
+    prefix = params.prefix;
+    libdir = params.libdir;
+    datadir = GLib.build_filenamev([prefix, 'share']);
+    let libpath, girpath;
+
+    if (_runningFromSource()) {
+        log('Running from source tree, using local files');
+        // Running from source directory
+        _base = GLib.get_current_dir();
+        pkglibdir = GLib.build_filenamev([_base, 'lib']);
+        libpath = GLib.build_filenamev([pkglibdir, '.libs']);
+        girpath = pkglibdir;
+        pkgdatadir = GLib.build_filenamev([_base, 'data']);
+        localedir = GLib.build_filenamev([_base, 'po']);
+        moduledir = GLib.build_filenamev([_base, 'src']);
+    } else {
+        _base = prefix;
+        pkglibdir = GLib.build_filenamev([libdir, _pkgname]);
+        libpath = pkglibdir;
+        girpath = GLib.build_filenamev([pkglibdir, 'girepository-1.0']);
+        pkgdatadir = GLib.build_filenamev([datadir, _pkgname]);
+        localedir = GLib.build_filenamev([datadir, 'locale']);
+
+        try {
+            let resource = Gio.Resource.load(GLib.build_filenamev([pkgdatadir,
+                                                                   name + '.src.gresource']));
+            resource._register();
+
+            moduledir = 'resource://' + _makeNamePath(name) + '/js';
+        } catch(e) {
+            moduledir = pkgdatadir;
+        }
+    }
+
+    imports.searchPath.unshift(moduledir);
+    GIRepository.Repository.prepend_search_path(girpath);
+    GIRepository.Repository.prepend_library_path(libpath);
+
+    try {
+        let resource = Gio.Resource.load(GLib.build_filenamev([pkgdatadir,
+                                                               name + '.data.gresource']));
+        resource._register();
+    } catch(e) { }
+}
+
+/**
+ * start:
+ * @params: see init()
+ *
+ * This is a convenience function if your package has a
+ * single entry point.
+ * You must define a main(ARGV) function inside a main.js
+ * module in moduledir.
+ */
+function start(params) {
+    init(params);
+    run(imports.main);
+}
+
+/**
+ * run:
+ * @module: the module to run
+ *
+ * This is the function to use if you want to have multiple
+ * entry points in one package.
+ * You must define a main(ARGV) function inside the passed
+ * in module, and then the launcher would be
+ *
+ * imports.package.init(...);
+ * imports.package.run(imports.entrypoint);
+ */
+function run(module) {
+    return module.main([System.programInvocationName].concat(ARGV));
+}
+
+/**
+ * require:
+ * @libs: the external dependencies to import
+ *
+ * Mark a dependency on a specific version of one or more
+ * external GI typelibs.
+ * @libs must be an object whose keys are a typelib name,
+ * and values are the respective version. The empty string
+ * indicates any version.
+ */
+function require(libs) {
+    for (let l in libs) {
+        let version = libs[l];
+
+        if (version != '')
+            imports.gi.versions[l] = version;
+
+        try {
+            imports.gi[l];
+        } catch(e) {
+            printerr('Unsatisfied dependency: ' + e.message);
+            System.exit(1);
+        }
+    }
+}
+
+function initGettext() {
+    Gettext.bindtextdomain(_pkgname, localedir);
+    Gettext.textdomain(_pkgname);
+
+    let gettext = imports.gettext;
+    window._ = gettext.gettext;
+    window.C_ = gettext.pgettext;
+    window.N_ = function(x) { return x; }
+}
+
+function initFormat() {
+    let format = imports.format;
+    String.prototype.format = format.format;
+}
+
+function initSubmodule(name) {
+    if (moduledir != pkgdatadir) {
+        // Running from source tree, add './name' to search paths
+
+        let submoduledir = GLib.build_filenamev([_base, name]);
+        let libpath = GLib.build_filenamev([submoduledir, '.libs']);
+        GIRepository.Repository.prepend_search_path(submoduledir);
+        GIRepository.Repository.prepend_library_path(libpath);
+    } else {
+        // Running installed, submodule is in $(pkglibdir), nothing to do
+    }
+}
diff --git a/modules/gjs/signals.js b/modules/gjs/signals.js
new file mode 100644
index 0000000..0905e53
--- /dev/null
+++ b/modules/gjs/signals.js
@@ -0,0 +1,146 @@
+/*
+ * Copyright (c) 2008  litl, LLC
+ *
+ * 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 THE SOFTWARE.
+ */
+
+// A couple principals of this simple signal system:
+// 1) should look just like our GObject signal binding
+// 2) memory and safety matter more than speed of connect/disconnect/emit
+// 3) the expectation is that a given object will have a very small number of
+//    connections, but they may be to different signal names
+
+function _connect(name, callback) {
+    // be paranoid about callback arg since we'd start to throw from emit()
+    // if it was messed up
+    if (typeof(callback) != 'function')
+        throw new Error("When connecting signal must give a callback that is a function");
+
+    // we instantiate the "signal machinery" only on-demand if anything
+    // gets connected.
+    if (!('_signalConnections' in this)) {
+        this._signalConnections = [];
+        this._nextConnectionId = 1;
+    }
+
+    let id = this._nextConnectionId;
+    this._nextConnectionId += 1;
+
+    // this makes it O(n) in total connections to emit, but I think
+    // it's right to optimize for low memory and reentrancy-safety
+    // rather than speed
+    this._signalConnections.push({ 'id' : id,
+                                   'name' : name,
+                                   'callback' : callback,
+                                   'disconnected' : false
+                                 });
+    return id;
+}
+
+function _disconnect(id) {
+    if ('_signalConnections' in this) {
+        let i;
+        let length = this._signalConnections.length;
+        for (i = 0; i < length; ++i) {
+            let connection = this._signalConnections[i];
+            if (connection.id == id) {
+                if (connection.disconnected)
+                    throw new Error("Signal handler id " + id + " already disconnected");
+
+                // set a flag to deal with removal during emission
+                connection.disconnected = true;
+                this._signalConnections.splice(i, 1);
+
+                return;
+            }
+        }
+    }
+    throw new Error("No signal connection " + id + " found");
+}
+
+function _disconnectAll() {
+    if ('_signalConnections' in this) {
+        while (this._signalConnections.length > 0) {
+            _disconnect.call(this, this._signalConnections[0].id);
+        }
+    }
+}
+
+function _emit(name /* , arg1, arg2 */) {
+    // may not be any signal handlers at all, if not then return
+    if (!('_signalConnections' in this))
+        return;
+
+    // To deal with re-entrancy (removal/addition while
+    // emitting), we copy out a list of what was connected
+    // at emission start; and just before invoking each
+    // handler we check its disconnected flag.
+    let handlers = [];
+    let i;
+    let length = this._signalConnections.length;
+    for (i = 0; i < length; ++i) {
+        let connection = this._signalConnections[i];
+        if (connection.name == name) {
+            handlers.push(connection);
+        }
+    }
+
+    // create arg array which is emitter + everything passed in except
+    // signal name. Would be more convenient not to pass emitter to
+    // the callback, but trying to be 100% consistent with GObject
+    // which does pass it in. Also if we pass in the emitter here,
+    // people don't create closures with the emitter in them,
+    // which would be a cycle.
+
+    let arg_array = [ this ];
+    // arguments[0] should be signal name so skip it
+    length = arguments.length;
+    for (i = 1; i < length; ++i) {
+        arg_array.push(arguments[i]);
+    }
+
+    length = handlers.length;
+    for (i = 0; i < length; ++i) {
+        let connection = handlers[i];
+        if (!connection.disconnected) {
+            try {
+                // since we pass "null" for this, the global object will be used.
+                let ret = connection.callback.apply(null, arg_array);
+
+                // if the callback returns true, we don't call the next
+                // signal handlers
+                if (ret === true) {
+                    break;
+                }
+            } catch(e) {
+                // just log any exceptions so that callbacks can't disrupt
+                // signal emission
+                logError(e, "Exception in callback for signal: "+name);
+            }
+        }
+    }
+}
+
+function addSignalMethods(proto) {
+    proto.connect = _connect;
+    proto.disconnect = _disconnect;
+    proto.emit = _emit;
+    // this one is not in GObject, but useful
+    proto.disconnectAll = _disconnectAll;
+}


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]