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 {