afefc88e02
The property reflects whether the extension is enabled in the setting. This does not mean that the extension is actually loaded, the API user must check the state property for this information. https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/695
236 lines
7.2 KiB
JavaScript
236 lines
7.2 KiB
JavaScript
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
|
|
/* exported ExtensionState, ExtensionType, getCurrentExtension,
|
|
getSettings, initTranslations, isOutOfDate, installImporter,
|
|
serializeExtension, deserializeExtension */
|
|
|
|
// Common utils for the extension system and the extension
|
|
// preferences tool
|
|
|
|
const { Gio, GLib } = imports.gi;
|
|
|
|
const Gettext = imports.gettext;
|
|
const Lang = imports.lang;
|
|
|
|
const Config = imports.misc.config;
|
|
|
|
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', 'canChange', 'isRequested'];
|
|
|
|
/**
|
|
* getCurrentExtension:
|
|
*
|
|
* Returns the current extension, or null if not called from an extension.
|
|
*/
|
|
function getCurrentExtension() {
|
|
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:
|
|
* @domain: (optional): 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 = 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);
|
|
}
|
|
|
|
/**
|
|
* getSettings:
|
|
* @schema: (optional): the GSettings schema id
|
|
*
|
|
* 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 = 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 });
|
|
}
|
|
|
|
/**
|
|
* versionCheck:
|
|
* @required: an array of versions we're compatible with
|
|
* @current: the version we have
|
|
*
|
|
* Check if a component is compatible for an extension.
|
|
* @required is an array, and at least one version must match.
|
|
* @current must be in the format <major>.<minor>.<point>.<micro>
|
|
* <micro> is always ignored
|
|
* <point> is ignored if <minor> is even (so you can target the
|
|
* whole stable release)
|
|
* <minor> and <major> must match
|
|
* Each target version must be at least <major> and <minor>
|
|
*/
|
|
function versionCheck(required, current) {
|
|
let currentArray = current.split('.');
|
|
let major = currentArray[0];
|
|
let minor = currentArray[1];
|
|
let point = currentArray[2];
|
|
for (let i = 0; i < required.length; i++) {
|
|
let requiredArray = required[i].split('.');
|
|
if (requiredArray[0] == major &&
|
|
requiredArray[1] == minor &&
|
|
(requiredArray[2] == point ||
|
|
(requiredArray[2] == undefined && parseInt(minor) % 2 == 0)))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function isOutOfDate(extension) {
|
|
if (!versionCheck(extension.metadata['shell-version'], Config.PACKAGE_VERSION))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
function serializeExtension(extension) {
|
|
let obj = {};
|
|
Lang.copyProperties(extension.metadata, obj);
|
|
|
|
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;
|
|
}
|