diff --git a/data/gnome-shell.schemas b/data/gnome-shell.schemas
index 9ace3bcd5..4e1f49620 100644
--- a/data/gnome-shell.schemas
+++ b/data/gnome-shell.schemas
@@ -88,6 +88,21 @@
+
+ /schemas/desktop/gnome/shell/disabled_extensions
+ /desktop/gnome/shell/disabled_extensions
+ gnome-shell
+ list
+ string
+ []
+
+ Uuids of extensions to disable
+
+ GNOME Shell extensions have a uuid property; this key lists extensions which should not be loaded.
+
+
+
+
diff --git a/js/ui/extensionSystem.js b/js/ui/extensionSystem.js
new file mode 100644
index 000000000..6e28623a5
--- /dev/null
+++ b/js/ui/extensionSystem.js
@@ -0,0 +1,158 @@
+/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
+
+const GLib = imports.gi.GLib;
+const Gio = imports.gi.Gio;
+const St = imports.gi.St;
+const Shell = imports.gi.Shell;
+
+const ExtensionState = {
+ ENABLED: 1,
+ DISABLED: 2,
+ ERROR: 3,
+ OUT_OF_DATE: 4
+};
+
+const ExtensionType = {
+ SYSTEM: 1,
+ PER_USER: 2
+};
+
+// Maps uuid -> metadata object
+const extensionMeta = {};
+// Maps uuid -> importer object (extension directory tree)
+const extensions = {};
+// Array of uuids
+var disabledExtensions;
+// GFile for user extensions
+var userExtensionsDir = null;
+
+function loadExtension(dir, enabled, type) {
+ let info;
+ let baseErrorString = 'While loading extension from "' + dir.get_parse_name() + '": ';
+
+ let metadataFile = dir.get_child('metadata.json');
+ if (!metadataFile.query_exists(null)) {
+ global.logError(baseErrorString + 'Missing metadata.json');
+ return;
+ }
+
+ let [success, metadataContents, len, etag] = metadataFile.load_contents(null);
+ let meta;
+ try {
+ meta = JSON.parse(metadataContents);
+ } catch (e) {
+ global.logError(baseErrorString + 'Failed to parse metadata.json: ' + e);
+ return;
+ }
+ let requiredProperties = ['uuid', 'name', 'description'];
+ for (let i = 0; i < requiredProperties; i++) {
+ let prop = requiredProperties[i];
+ if (!meta[prop]) {
+ global.logError(baseErrorString + 'missing "' + prop + '" property in metadata.json');
+ return;
+ }
+ }
+ // Encourage people to add this
+ if (!meta['url']) {
+ global.log(baseErrorString + 'Warning: Missing "url" property in metadata.json');
+ }
+
+ let base = dir.get_basename();
+ if (base != meta.uuid) {
+ global.logError(baseErrorString + 'uuid "' + meta.uuid + '" from metadata.json does not match directory name "' + base + '"');
+ return;
+ }
+
+ extensionMeta[meta.uuid] = meta;
+ extensionMeta[meta.uuid].type = type;
+ extensionMeta[meta.uuid].path = dir.get_path();
+ if (!enabled) {
+ extensionMeta[meta.uuid].state = ExtensionState.DISABLED;
+ return;
+ }
+
+ // Default to error, we set success as the last step
+ extensionMeta[meta.uuid].state = ExtensionState.ERROR;
+
+ let extensionJs = dir.get_child('extension.js');
+ if (!extensionJs.query_exists(null)) {
+ global.logError(baseErrorString + 'Missing extension.js');
+ return;
+ }
+ let stylesheetPath = null;
+ let themeContext = St.ThemeContext.get_for_stage(global.stage);
+ let theme = themeContext.get_theme();
+ let stylesheetFile = dir.get_child('stylesheet.css');
+ if (stylesheetFile.query_exists(null)) {
+ try {
+ theme.load_stylesheet(stylesheetFile.get_path());
+ } catch (e) {
+ global.logError(baseErrorString + 'Stylesheet parse error: ' + e);
+ return;
+ }
+ }
+
+ let extensionModule;
+ try {
+ global.add_extension_importer('imports.ui.extensionSystem.extensions', meta.uuid, dir.get_path());
+ extensionModule = extensions[meta.uuid].extension;
+ } catch (e) {
+ if (stylesheetPath != null)
+ theme.unload_stylesheet(stylesheetPath);
+ global.logError(baseErrorString + e);
+ return;
+ }
+ if (!extensionModule.main) {
+ global.logError(baseErrorString + 'missing \'main\' function');
+ return;
+ }
+ try {
+ extensionModule.main();
+ } catch (e) {
+ if (stylesheetPath != null)
+ theme.unload_stylesheet(stylesheetPath);
+ global.logError(baseErrorString + 'Failed to evaluate main function:' + e);
+ return;
+ }
+ extensionMeta[meta.uuid].state = ExtensionState.ENABLED;
+ global.log('Loaded extension ' + meta.uuid);
+}
+
+function init() {
+ let userConfigPath = GLib.get_user_config_dir();
+ let userExtensionsPath = GLib.build_filenamev([userConfigPath, 'gnome-shell', 'extensions']);
+ userExtensionsDir = Gio.file_new_for_path(userExtensionsPath);
+ try {
+ userExtensionsDir.make_directory_with_parents(null);
+ } catch (e) {
+ global.logError(""+e);
+ }
+
+ disabledExtensions = Shell.GConf.get_default().get_string_list('disabled_extensions');
+}
+
+function _loadExtensionsIn(dir, type) {
+ let fileEnum = dir.enumerate_children('standard::*', Gio.FileQueryInfoFlags.NONE, null);
+ let file, info;
+ while ((info = fileEnum.next_file(null)) != null) {
+ let fileType = info.get_file_type();
+ if (fileType != Gio.FileType.DIRECTORY)
+ continue;
+ let name = info.get_name();
+ let enabled = disabledExtensions.indexOf(name) < 0;
+ let child = dir.get_child(name);
+ loadExtension(child, enabled, type);
+ }
+ fileEnum.close(null);
+}
+
+function loadExtensions() {
+ _loadExtensionsIn(userExtensionsDir, ExtensionType.PER_USER);
+ let systemDataDirs = GLib.get_system_data_dirs();
+ for (let i = 0; i < systemDataDirs.length; i++) {
+ let dirPath = systemDataDirs[i] + '/gnome-shell/extensions';
+ let dir = Gio.file_new_for_path(dirPath);
+ if (dir.query_exists(null))
+ _loadExtensionsIn(dir, ExtensionType.SYSTEM);
+ }
+}
diff --git a/js/ui/main.js b/js/ui/main.js
index d481cc78e..7361ac301 100644
--- a/js/ui/main.js
+++ b/js/ui/main.js
@@ -14,6 +14,7 @@ const St = imports.gi.St;
const Chrome = imports.ui.chrome;
const Environment = imports.ui.environment;
+const ExtensionSystem = imports.ui.extensionSystem;
const Overview = imports.ui.overview;
const Panel = imports.ui.panel;
const PlaceDisplay = imports.ui.placeDisplay;
@@ -129,6 +130,9 @@ function start() {
_relayout();
+ ExtensionSystem.init();
+ ExtensionSystem.loadExtensions();
+
panel.startupAnimation();
let display = global.screen.get_display();
diff --git a/src/gnome-shell.in b/src/gnome-shell.in
old mode 100755
new mode 100644
index b009ec495..62c86d604
--- a/src/gnome-shell.in
+++ b/src/gnome-shell.in
@@ -212,6 +212,8 @@ parser.add_option("-w", "--wide", action="store_true",
help="Use widescreen (1280x800) with Xephyr")
parser.add_option("", "--eval-file", metavar="EVAL_FILE",
help="Evaluate the contents of the given JavaScript file")
+parser.add_option("", "--create-extension", action="store_true",
+ help="Create a new GNOME Shell extension")
options, args = parser.parse_args()
@@ -219,6 +221,87 @@ if args:
parser.print_usage()
sys.exit(1)
+if options.create_extension:
+ import json
+
+ print
+ print '''Name should be a very short (ideally descriptive) string.
+Examples are: "Click To Focus", "Adblock", "Shell Window Shrinker".
+'''
+ name = raw_input('Name: ').strip()
+ print
+ print '''Description is a single-sentence explanation of what your extension does.
+Examples are: "Make windows visible on click", "Block advertisement popups"
+ "Animate windows shrinking on minimize"
+'''
+ description = raw_input('Description: ').strip()
+ underifier = re.compile('[^A-Za-z]')
+ sample_uuid = underifier.sub('_', name)
+ # TODO use evolution data server
+ hostname = subprocess.Popen(['hostname'], stdout=subprocess.PIPE).communicate()[0].strip()
+ sample_uuid = sample_uuid + '@' + hostname
+
+ print
+ print '''Uuid is a globally-unique identifier for your extension.
+This should be in the format of an email address (foo.bar@extensions.example.com), but
+need not be an actual email address, though it's a good idea to base the uuid on your
+email address. For example, if your email address is janedoe@example.com, you might
+use an extension title clicktofocus@janedoe.example.com.'''
+ uuid = raw_input('Uuid [%s]: ' % (sample_uuid, )).strip()
+ if uuid == '':
+ uuid = sample_uuid
+
+ extension_path = os.path.join(os.path.expanduser('~/.config'), 'gnome-shell', 'extensions', uuid)
+ if os.path.exists(extension_path):
+ print "Extension path %r already exists" % (extension_path, )
+ sys.exit(0)
+ os.makedirs(extension_path)
+ meta = { 'name': name,
+ 'description': description,
+ 'uuid': uuid }
+ f = open(os.path.join(extension_path, 'metadata.json'), 'w')
+ json.dump(meta, f)
+ f.close()
+
+ extensionjs_path = os.path.join(extension_path, 'extension.js')
+ f = open(extensionjs_path, 'w')
+ f.write('''// Sample extension code, makes clicking on the panel show a message
+const St = imports.gi.St;
+const Mainloop = imports.mainloop;
+
+const Main = imports.ui.main;
+
+function _showHello() {
+ let text = new St.Label({ style_class: 'helloworld-label', text: "Hello, world!" });
+ let monitor = global.get_primary_monitor();
+ global.stage.add_actor(text);
+ text.set_position(Math.floor (monitor.width / 2 - text.width / 2), Math.floor(monitor.height / 2 - text.height / 2));
+ Mainloop.timeout_add(3000, function () { text.destroy(); });
+}
+
+// Put your extension initialization code here
+function main() {
+ Main.panel.actor.reactive = true;
+ Main.panel.actor.connect('button-release-event', _showHello);
+}
+''')
+ f.close()
+
+ f = open(os.path.join(extension_path, 'stylesheet.css'), 'w')
+ f.write('''/* Example stylesheet */
+.helloworld-label {
+ font-size: 36px;
+ font-weight: bold;
+ color: #ffffff;
+ background-color: rgba(10,10,10,0.7);
+ border-radius: 5px;
+}
+''')
+ f.close()
+
+ subprocess.Popen(['gedit', extensionjs_path])
+ sys.exit(0)
+
if options.eval_file:
import dbus
diff --git a/src/shell-global.c b/src/shell-global.c
index 542f28dc1..ac68bc5d7 100644
--- a/src/shell-global.c
+++ b/src/shell-global.c
@@ -522,6 +522,73 @@ shell_global_display_is_grabbed (ShellGlobal *global)
return meta_display_get_grab_op (display) != META_GRAB_OP_NONE;
}
+/* Defining this here for now, see
+ * https://bugzilla.gnome.org/show_bug.cgi?id=604075
+ * for upstreaming status.
+ */
+JSContext * gjs_context_get_context (GjsContext *context);
+
+/**
+ * shell_global_add_extension_importer:
+ * @target_object_script: JavaScript code evaluating to a target object
+ * @target_property: Name of property to use for importer
+ * @directory: Source directory:
+ * @error: A #GError
+ *
+ * This function sets a property named @target_property on the object
+ * resulting from the evaluation of @target_object_script code, which
+ * acts as a GJS importer for directory @directory.
+ *
+ * Returns: %TRUE on success
+ */
+gboolean
+shell_global_add_extension_importer (ShellGlobal *global,
+ const char *target_object_script,
+ const char *target_property,
+ const char *directory,
+ GError **error)
+{
+ jsval target_object;
+ JSObject *importer;
+ JSContext *context = gjs_context_get_context (global->js_context);
+ char *search_path[2] = { 0, 0 };
+
+ // This is a bit of a hack; ideally we'd be able to pass our target
+ // object directly into this function, but introspection doesn't
+ // support that at the moment. Instead evaluate a string to get it.
+ if (!JS_EvaluateScript(context,
+ JS_GetGlobalObject(context),
+ target_object_script,
+ strlen (target_object_script),
+ "",
+ 0,
+ &target_object))
+ {
+ char *message;
+ gjs_log_exception(context,
+ &message);
+ g_set_error(error,
+ G_IO_ERROR,
+ G_IO_ERROR_FAILED,
+ "%s", message ? message : "(unknown)");
+ g_free(message);
+ return FALSE;
+ }
+
+ if (!JSVAL_IS_OBJECT (target_object))
+ {
+ g_set_error(error,
+ G_IO_ERROR,
+ G_IO_ERROR_FAILED,
+ "Invalid object");
+ return FALSE;
+ }
+
+ search_path[0] = (char*)directory;
+ importer = gjs_define_importer (context, JSVAL_TO_OBJECT (target_object), target_property, (const char **)search_path, FALSE);
+ return TRUE;
+}
+
/* Code to close all file descriptors before we exec; copied from gspawn.c in GLib.
*
* Authors: Padraig O'Briain, Matthias Clasen, Lennart Poettering
diff --git a/src/shell-global.h b/src/shell-global.h
index ef70b6afd..12ebf5033 100644
--- a/src/shell-global.h
+++ b/src/shell-global.h
@@ -43,6 +43,12 @@ ShellGlobal *shell_global_get (void);
MetaScreen *shell_global_get_screen (ShellGlobal *global);
+gboolean shell_global_add_extension_importer (ShellGlobal *global,
+ const char *target_object_script,
+ const char *target_property,
+ const char *directory,
+ GError **error);
+
void shell_global_grab_dbus_service (ShellGlobal *global);
typedef enum {