diff --git a/js/dbusServices/extensions/extensionsService.js b/js/dbusServices/extensions/extensionsService.js
index 1cf922550..cd87f6c79 100644
--- a/js/dbusServices/extensions/extensionsService.js
+++ b/js/dbusServices/extensions/extensionsService.js
@@ -7,7 +7,7 @@ import {ExtensionPrefsDialog} from './extensionPrefsDialog.js';
import {ServiceImplementation} from './dbusService.js';
const {deserializeExtension} = imports.misc.extensionUtils;
-const {setExtensionManager} = imports.misc.extensionUtils;
+const {setExtensionManager} = imports.extensions.sharedInternals;
const {loadInterfaceXML} = imports.misc.dbusUtils;
const ExtensionsIface = loadInterfaceXML('org.gnome.Shell.Extensions');
diff --git a/js/dbusServices/org.gnome.Shell.Extensions.src.gresource.xml b/js/dbusServices/org.gnome.Shell.Extensions.src.gresource.xml
index 3ab92a20e..51042f933 100644
--- a/js/dbusServices/org.gnome.Shell.Extensions.src.gresource.xml
+++ b/js/dbusServices/org.gnome.Shell.Extensions.src.gresource.xml
@@ -6,6 +6,8 @@
extensionPrefsDialog.js
dbusService.js
+ extensions/sharedInternals.js
+
misc/config.js
misc/extensionUtils.js
misc/dbusUtils.js
diff --git a/js/extensions/sharedInternals.js b/js/extensions/sharedInternals.js
new file mode 100644
index 000000000..23f553664
--- /dev/null
+++ b/js/extensions/sharedInternals.js
@@ -0,0 +1,201 @@
+/* exported getCurrentExtension, setExtensionManager, getSettings,
+ initTranslations, gettext, ngettext, pgettext, openPrefs */
+
+const Gio = imports.gi.Gio;
+const GLib = imports.gi.GLib;
+
+const Gettext = imports.gettext;
+
+const Config = imports.misc.config;
+
+let _extensionManager = null;
+
+/**
+ * @param {object} extensionManager to use in utilities like `initTranslations()`
+ */
+function setExtensionManager(extensionManager) {
+ if (_extensionManager)
+ throw new Error('Trying to override existing extension manager');
+
+ _extensionManager = extensionManager;
+}
+
+/**
+ * getCurrentExtension:
+ *
+ * @returns {?object} - The current extension, or null if not called from
+ * an extension.
+ */
+function getCurrentExtension() {
+ const basePath = '/gnome-shell/extensions/';
+
+ // Search for an occurrence of an extension stack frame
+ // Start at 1 because 0 is the stack frame of this function
+ const [, ...stack] = new Error().stack.split('\n');
+ const extensionLine = stack.find(
+ line => line.includes(basePath));
+
+ if (!extensionLine)
+ return null;
+
+ // local import, as the module is used from outside the gnome-shell process
+ // as well
+ if (!_extensionManager)
+ setExtensionManager(imports.ui.main.extensionManager);
+
+ // The exact stack line differs depending on where the function
+ // was called (function or module scope), and whether it's called
+ // from a module or legacy import (file:// URI vs. plain path).
+ //
+ // We don't have to care about the exact composition, all we need
+ // is a string that can be traversed as path and contains the UUID
+ let path = extensionLine.slice(extensionLine.indexOf(basePath));
+
+ // Walk up the directory tree, looking for an extension with
+ // the same UUID as a directory name.
+ do {
+ path = GLib.path_get_dirname(path);
+
+ const dirName = GLib.path_get_basename(path);
+ const extension = _extensionManager.lookup(dirName);
+ if (extension !== undefined)
+ return extension;
+ } while (path !== '/');
+
+ return null;
+}
+
+/**
+ * Initialize Gettext to load translations from extensionsdir/locale.
+ * If @domain is not provided, it will be taken from metadata['gettext-domain']
+ *
+ * @param {string=} domain - the gettext domain to use
+ */
+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));
+}
+
+/**
+ * Translate @str using the extension's gettext domain
+ *
+ * @param {string} str - the string to translate
+ *
+ * @returns {string} - the translated string
+ */
+function gettext(str) {
+ return callExtensionGettextFunc('gettext', str);
+}
+
+/**
+ * Translate @str and choose plural form using the extension's
+ * gettext domain
+ *
+ * @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
+ *
+ * @returns {string} - the translated string
+ */
+function ngettext(str, strPlural, n) {
+ return callExtensionGettextFunc('ngettext', str, strPlural, n);
+}
+
+/**
+ * Translate @str in the context of @context using the extension's
+ * gettext domain
+ *
+ * @param {string} context - context to disambiguate @str
+ * @param {string} str - the string to translate
+ *
+ * @returns {string} - the translated string
+ */
+function pgettext(context, str) {
+ return callExtensionGettextFunc('pgettext', context, str);
+}
+
+/**
+ * @private
+ * @param {string} func - function name
+ * @param {*[]} args - function arguments
+ */
+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);
+}
+
+/**
+ * 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'].
+ *
+ * @param {string=} schema - the GSettings schema id
+ * @returns {Gio.Settings} - a new settings object for @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});
+}
+
+/**
+ * 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');
+ }
+}
diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml
index 476671ce0..419ca024f 100644
--- a/js/js-resources.gresource.xml
+++ b/js/js-resources.gresource.xml
@@ -11,6 +11,8 @@
gdm/realmd.js
gdm/util.js
+ extensions/sharedInternals.js
+
misc/animationUtils.js
misc/config.js
misc/extensionUtils.js
diff --git a/js/misc/extensionUtils.js b/js/misc/extensionUtils.js
index 2f6c143b4..d96716f62 100644
--- a/js/misc/extensionUtils.js
+++ b/js/misc/extensionUtils.js
@@ -1,20 +1,12 @@
-// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
-/* exported ExtensionState, ExtensionType, getCurrentExtension,
- getSettings, initTranslations, gettext, ngettext, pgettext,
- openPrefs, serializeExtension, deserializeExtension, setExtensionManager */
+/* exported ExtensionState, ExtensionType,
+ * serializeExtension, deserializeExtension */
-// Common utils for the extension system and the extension
-// preferences tool
+// Common utils for the extension system, the extensions D-Bus service
+// and the Extensions app
const Gio = imports.gi.Gio;
const GLib = imports.gi.GLib;
-const Gettext = imports.gettext;
-
-const Config = imports.misc.config;
-
-let _extensionManager = null;
-
var ExtensionType = {
SYSTEM: 1,
PER_USER: 2,
@@ -45,196 +37,6 @@ const SERIALIZED_PROPERTIES = [
'canChange',
];
-/**
- * @param {object} extensionManager to use in utilities like `initTranslations()`
- */
-function setExtensionManager(extensionManager) {
- if (_extensionManager)
- throw new Error('Trying to override existing extension manager');
-
- _extensionManager = extensionManager;
-}
-
-/**
- * getCurrentExtension:
- *
- * @returns {?object} - The current extension, or null if not called from
- * an extension.
- */
-function getCurrentExtension() {
- const basePath = '/gnome-shell/extensions/';
-
- // Search for an occurrence of an extension stack frame
- // Start at 1 because 0 is the stack frame of this function
- const [, ...stack] = new Error().stack.split('\n');
- const extensionLine = stack.find(
- line => line.includes(basePath));
-
- if (!extensionLine)
- return null;
-
- // local import, as the module is used from outside the gnome-shell process
- // as well
- if (!_extensionManager)
- setExtensionManager(imports.ui.main.extensionManager);
-
- // The exact stack line differs depending on where the function
- // was called (function or module scope), and whether it's called
- // from a module or legacy import (file:// URI vs. plain path).
- //
- // We don't have to care about the exact composition, all we need
- // is a string that can be traversed as path and contains the UUID
- let path = extensionLine.slice(extensionLine.indexOf(basePath));
-
- // Walk up the directory tree, looking for an extension with
- // the same UUID as a directory name.
- do {
- path = GLib.path_get_dirname(path);
-
- const dirName = GLib.path_get_basename(path);
- const extension = _extensionManager.lookup(dirName);
- if (extension !== undefined)
- return extension;
- } while (path !== '/');
-
- return null;
-}
-
-/**
- * Initialize Gettext to load translations from extensionsdir/locale.
- * If @domain is not provided, it will be taken from metadata['gettext-domain']
- *
- * @param {string=} domain - the gettext domain to use
- */
-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));
-}
-
-/**
- * Translate @str using the extension's gettext domain
- *
- * @param {string} str - the string to translate
- *
- * @returns {string} - the translated string
- */
-function gettext(str) {
- return callExtensionGettextFunc('gettext', str);
-}
-
-/**
- * Translate @str and choose plural form using the extension's
- * gettext domain
- *
- * @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
- *
- * @returns {string} - the translated string
- */
-function ngettext(str, strPlural, n) {
- return callExtensionGettextFunc('ngettext', str, strPlural, n);
-}
-
-/**
- * Translate @str in the context of @context using the extension's
- * gettext domain
- *
- * @param {string} context - context to disambiguate @str
- * @param {string} str - the string to translate
- *
- * @returns {string} - the translated string
- */
-function pgettext(context, str) {
- return callExtensionGettextFunc('pgettext', context, str);
-}
-
-/**
- * @private
- * @param {string} func - function name
- * @param {*[]} args - function arguments
- */
-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);
-}
-
-/**
- * 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'].
- *
- * @param {string=} schema - the GSettings schema id
- * @returns {Gio.Settings} - a new settings object for @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});
-}
-
-/**
- * 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');
- }
-}
-
/**
* Serialize extension into an object that can be used
* in a vardict {GLib.Variant}
diff --git a/lint/eslintrc-shell.yml b/lint/eslintrc-shell.yml
index da9c26af1..697af8189 100644
--- a/lint/eslintrc-shell.yml
+++ b/lint/eslintrc-shell.yml
@@ -21,7 +21,7 @@ overrides:
- tests/shell/**
excludedFiles:
- js/portalHelper/*
- - js/misc/extensionUtils.js
+ - js/extensions/*
globals:
global: readonly
_: readonly