summaryrefslogtreecommitdiffstats
path: root/js/misc/extensionUtils.js
diff options
context:
space:
mode:
Diffstat (limited to 'js/misc/extensionUtils.js')
-rw-r--r--js/misc/extensionUtils.js318
1 files changed, 318 insertions, 0 deletions
diff --git a/js/misc/extensionUtils.js b/js/misc/extensionUtils.js
new file mode 100644
index 0000000..3e89a87
--- /dev/null
+++ b/js/misc/extensionUtils.js
@@ -0,0 +1,318 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* exported ExtensionState, ExtensionType, getCurrentExtension,
+ getSettings, initTranslations, gettext, ngettext, pgettext,
+ openPrefs, isOutOfDate, installImporter, serializeExtension,
+ deserializeExtension, setCurrentExtension */
+
+// Common utils for the extension system and the extension
+// preferences tool
+
+const { Gio, GLib } = imports.gi;
+
+const Gettext = imports.gettext;
+
+const Config = imports.misc.config;
+
+let Main = null;
+
+try {
+ Main = imports.ui.main;
+} catch (error) {
+ // Only log the error if it is not due to the
+ // missing import.
+ if (error?.name !== 'ImportError')
+ console.error(error);
+}
+
+let _extension = null;
+
+var ExtensionType = {
+ SYSTEM: 1,
+ PER_USER: 2,
+};
+
+var ExtensionState = {
+ ENABLED: 1,
+ DISABLED: 2,
+ ERROR: 3,
+ OUT_OF_DATE: 4,
+ DOWNLOADING: 5,
+ INITIALIZED: 6,
+
+ // Used as an error state for operations on unknown extensions,
+ // should never be in a real extensionMeta object.
+ UNINSTALLED: 99,
+};
+
+const SERIALIZED_PROPERTIES = [
+ 'type',
+ 'state',
+ 'path',
+ 'error',
+ 'hasPrefs',
+ 'hasUpdate',
+ 'canChange',
+];
+
+/**
+ * @param {object} extension the extension object to use in utilities like `initTranslations()`
+ */
+function setCurrentExtension(extension) {
+ if (Main)
+ throw new Error('setCurrentExtension() can only be called from outside the shell');
+
+ _extension = extension;
+}
+
+/**
+ * getCurrentExtension:
+ *
+ * @returns {?object} - The current extension, or null if not called from
+ * an extension.
+ */
+function getCurrentExtension() {
+ if (_extension)
+ return _extension;
+
+ let stack = new Error().stack.split('\n');
+ let extensionStackLine;
+
+ // Search for an occurrence of an extension stack frame
+ // Start at 1 because 0 is the stack frame of this function
+ for (let i = 1; i < stack.length; i++) {
+ if (stack[i].includes('/gnome-shell/extensions/')) {
+ extensionStackLine = stack[i];
+ break;
+ }
+ }
+ if (!extensionStackLine)
+ return null;
+
+ // The stack line is like:
+ // init([object Object])@/home/user/data/gnome-shell/extensions/u@u.id/prefs.js:8
+ //
+ // In the case that we're importing from
+ // module scope, the first field is blank:
+ // @/home/user/data/gnome-shell/extensions/u@u.id/prefs.js:8
+ let match = new RegExp('@(.+):\\d+').exec(extensionStackLine);
+ if (!match)
+ return null;
+
+ // local import, as the module is used from outside the gnome-shell process
+ // as well (not this function though)
+ let extensionManager = imports.ui.main.extensionManager;
+
+ let path = match[1];
+ let file = Gio.File.new_for_path(path);
+
+ // Walk up the directory tree, looking for an extension with
+ // the same UUID as a directory name.
+ while (file != null) {
+ let extension = extensionManager.lookup(file.get_basename());
+ if (extension !== undefined)
+ return extension;
+ file = file.get_parent();
+ }
+
+ return null;
+}
+
+/**
+ * initTranslations:
+ * @param {string=} domain - the gettext domain to use
+ *
+ * Initialize Gettext to load translations from extensionsdir/locale.
+ * If @domain is not provided, it will be taken from metadata['gettext-domain']
+ */
+function initTranslations(domain) {
+ let extension = getCurrentExtension();
+
+ if (!extension)
+ throw new Error('initTranslations() can only be called from extensions');
+
+ domain ||= extension.metadata['gettext-domain'];
+
+ // Expect USER extensions to have a locale/ subfolder, otherwise assume a
+ // SYSTEM extension that has been installed in the same prefix as the shell
+ let localeDir = extension.dir.get_child('locale');
+ if (localeDir.query_exists(null))
+ Gettext.bindtextdomain(domain, localeDir.get_path());
+ else
+ Gettext.bindtextdomain(domain, Config.LOCALEDIR);
+
+ Object.assign(extension, Gettext.domain(domain));
+}
+
+/**
+ * gettext:
+ * @param {string} str - the string to translate
+ *
+ * Translate @str using the extension's gettext domain
+ *
+ * @returns {string} - the translated string
+ *
+ */
+function gettext(str) {
+ return callExtensionGettextFunc('gettext', str);
+}
+
+/**
+ * ngettext:
+ * @param {string} str - the string to translate
+ * @param {string} strPlural - the plural form of the string
+ * @param {number} n - the quantity for which translation is needed
+ *
+ * Translate @str and choose plural form using the extension's
+ * gettext domain
+ *
+ * @returns {string} - the translated string
+ *
+ */
+function ngettext(str, strPlural, n) {
+ return callExtensionGettextFunc('ngettext', str, strPlural, n);
+}
+
+/**
+ * pgettext:
+ * @param {string} context - context to disambiguate @str
+ * @param {string} str - the string to translate
+ *
+ * Translate @str in the context of @context using the extension's
+ * gettext domain
+ *
+ * @returns {string} - the translated string
+ *
+ */
+function pgettext(context, str) {
+ return callExtensionGettextFunc('pgettext', context, str);
+}
+
+function callExtensionGettextFunc(func, ...args) {
+ const extension = getCurrentExtension();
+
+ if (!extension)
+ throw new Error(`${func}() can only be called from extensions`);
+
+ if (!extension[func])
+ throw new Error(`${func}() is used without calling initTranslations() first`);
+
+ return extension[func](...args);
+}
+
+/**
+ * getSettings:
+ * @param {string=} schema - the GSettings schema id
+ * @returns {Gio.Settings} - a new settings object for @schema
+ *
+ * Builds and returns a GSettings schema for @schema, using schema files
+ * in extensionsdir/schemas. If @schema is omitted, it is taken from
+ * metadata['settings-schema'].
+ */
+function getSettings(schema) {
+ let extension = getCurrentExtension();
+
+ if (!extension)
+ throw new Error('getSettings() can only be called from extensions');
+
+ schema ||= extension.metadata['settings-schema'];
+
+ const GioSSS = Gio.SettingsSchemaSource;
+
+ // Expect USER extensions to have a schemas/ subfolder, otherwise assume a
+ // SYSTEM extension that has been installed in the same prefix as the shell
+ let schemaDir = extension.dir.get_child('schemas');
+ let schemaSource;
+ if (schemaDir.query_exists(null)) {
+ schemaSource = GioSSS.new_from_directory(schemaDir.get_path(),
+ GioSSS.get_default(),
+ false);
+ } else {
+ schemaSource = GioSSS.get_default();
+ }
+
+ let schemaObj = schemaSource.lookup(schema, true);
+ if (!schemaObj)
+ throw new Error(`Schema ${schema} could not be found for extension ${extension.metadata.uuid}. Please check your installation`);
+
+ return new Gio.Settings({ settings_schema: schemaObj });
+}
+
+/**
+ * openPrefs:
+ *
+ * Open the preference dialog of the current extension
+ */
+function openPrefs() {
+ const extension = getCurrentExtension();
+
+ if (!extension)
+ throw new Error('openPrefs() can only be called from extensions');
+
+ try {
+ const extensionManager = imports.ui.main.extensionManager;
+ extensionManager.openExtensionPrefs(extension.uuid, '', {});
+ } catch (e) {
+ if (e.name === 'ImportError')
+ throw new Error('openPrefs() cannot be called from preferences');
+ logError(e, 'Failed to open extension preferences');
+ }
+}
+
+function isOutOfDate(extension) {
+ const [major] = Config.PACKAGE_VERSION.split('.');
+ return !extension.metadata['shell-version'].some(v => v.startsWith(major));
+}
+
+function serializeExtension(extension) {
+ let obj = { ...extension.metadata };
+
+ SERIALIZED_PROPERTIES.forEach(prop => {
+ obj[prop] = extension[prop];
+ });
+
+ let res = {};
+ for (let key in obj) {
+ let val = obj[key];
+ let type;
+ switch (typeof val) {
+ case 'string':
+ type = 's';
+ break;
+ case 'number':
+ type = 'd';
+ break;
+ case 'boolean':
+ type = 'b';
+ break;
+ default:
+ continue;
+ }
+ res[key] = GLib.Variant.new(type, val);
+ }
+
+ return res;
+}
+
+function deserializeExtension(variant) {
+ let res = { metadata: {} };
+ for (let prop in variant) {
+ let val = variant[prop].unpack();
+ if (SERIALIZED_PROPERTIES.includes(prop))
+ res[prop] = val;
+ else
+ res.metadata[prop] = val;
+ }
+ // add the 2 additional properties to create a valid extension object, as createExtensionObject()
+ res.uuid = res.metadata.uuid;
+ res.dir = Gio.File.new_for_path(res.path);
+ return res;
+}
+
+function installImporter(extension) {
+ let oldSearchPath = imports.searchPath.slice(); // make a copy
+ imports.searchPath = [extension.dir.get_parent().get_path()];
+ // importing a "subdir" creates a new importer object that doesn't affect
+ // the global one
+ extension.imports = imports[extension.uuid];
+ imports.searchPath = oldSearchPath;
+}